summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2013-01-18 16:46:04 +0100
committerPierre Schmitz <pierre@archlinux.de>2013-01-18 16:46:04 +0100
commit63601400e476c6cf43d985f3e7b9864681695ed4 (patch)
treef7846203a952e38aaf66989d0a4702779f549962 /includes
parent8ff01378c9e0207f9169b81966a51def645b6a51 (diff)
Update to MediaWiki 1.20.2
this update includes: * adjusted Arch Linux skin * updated FluxBBAuthPlugin * patch for https://bugzilla.wikimedia.org/show_bug.cgi?id=44024
Diffstat (limited to 'includes')
-rw-r--r--includes/Action.php64
-rw-r--r--includes/AjaxDispatcher.php56
-rw-r--r--includes/AjaxResponse.php109
-rw-r--r--includes/Article.php593
-rw-r--r--includes/AuthPlugin.php22
-rw-r--r--includes/AutoLoader.php223
-rw-r--r--includes/Autopromote.php25
-rw-r--r--includes/BacklinkCache.php62
-rw-r--r--includes/Block.php100
-rw-r--r--includes/CacheHelper.php392
-rw-r--r--includes/Category.php48
-rw-r--r--includes/CategoryPage.php21
-rw-r--r--includes/CategoryViewer.php52
-rw-r--r--includes/Categoryfinder.php21
-rw-r--r--includes/Cdb.php17
-rw-r--r--includes/Cdb_PHP.php17
-rw-r--r--includes/ChangeTags.php30
-rw-r--r--includes/ChangesFeed.php28
-rw-r--r--includes/ChangesList.php276
-rw-r--r--includes/Collation.php22
-rw-r--r--includes/ConfEditor.php32
-rw-r--r--includes/Cookie.php26
-rw-r--r--includes/CryptRand.php33
-rw-r--r--includes/DataUpdate.php124
-rw-r--r--includes/DefaultSettings.php1432
-rw-r--r--includes/DeferredUpdates.php36
-rw-r--r--includes/Defines.php27
-rw-r--r--includes/DeprecatedGlobal.php55
-rw-r--r--includes/EditPage.php690
-rw-r--r--includes/Exception.php229
-rw-r--r--includes/Export.php517
-rw-r--r--includes/ExternalEdit.php14
-rw-r--r--includes/ExternalStore.php25
-rw-r--r--includes/ExternalStoreDB.php23
-rw-r--r--includes/ExternalStoreHttp.php20
-rw-r--r--includes/ExternalUser.php4
-rw-r--r--includes/FakeTitle.php23
-rw-r--r--includes/Fallback.php4
-rw-r--r--includes/Feed.php34
-rw-r--r--includes/FeedUtils.php41
-rw-r--r--includes/FileDeleteForm.php108
-rw-r--r--includes/ForkController.php25
-rw-r--r--includes/FormOptions.php35
-rw-r--r--includes/GitInfo.php214
-rw-r--r--includes/GlobalFunctions.php746
-rw-r--r--includes/HTMLForm.php716
-rw-r--r--includes/HistoryBlob.php64
-rw-r--r--includes/Hooks.php2
-rw-r--r--includes/Html.php133
-rw-r--r--includes/HttpFunctions.old.php21
-rw-r--r--includes/HttpFunctions.php72
-rw-r--r--includes/IP.php48
-rw-r--r--includes/ImageFunctions.php87
-rw-r--r--includes/ImageGallery.php55
-rw-r--r--includes/ImagePage.php468
-rw-r--r--includes/ImageQueryPage.php21
-rw-r--r--includes/Import.php57
-rw-r--r--includes/Init.php21
-rw-r--r--includes/Licenses.php45
-rw-r--r--includes/LinkFilter.php23
-rw-r--r--includes/Linker.php415
-rw-r--r--includes/LinksUpdate.php153
-rw-r--r--includes/LocalisationCache.php192
-rw-r--r--includes/MWFunction.php4
-rw-r--r--includes/MagicWord.php48
-rw-r--r--includes/Message.php95
-rw-r--r--includes/MessageBlobStore.php7
-rw-r--r--includes/Metadata.php32
-rw-r--r--includes/MimeMagic.php22
-rw-r--r--includes/Namespace.php73
-rw-r--r--includes/OutputHandler.php25
-rw-r--r--includes/OutputPage.php403
-rw-r--r--includes/PHPVersionError.php25
-rw-r--r--includes/PageQueryPage.php29
-rw-r--r--includes/Pager.php264
-rw-r--r--includes/PathRouter.php62
-rw-r--r--includes/PoolCounter.php22
-rw-r--r--includes/Preferences.php102
-rw-r--r--includes/PrefixSearch.php24
-rw-r--r--includes/ProtectionForm.php85
-rw-r--r--includes/ProxyTools.php30
-rw-r--r--includes/QueryPage.php68
-rw-r--r--includes/RecentChange.php168
-rw-r--r--includes/Revision.php255
-rw-r--r--includes/RevisionList.php48
-rw-r--r--includes/Sanitizer.php111
-rw-r--r--includes/ScopedPHPTimeout.php84
-rw-r--r--includes/SeleniumWebSettings.php19
-rw-r--r--includes/Setup.php64
-rw-r--r--includes/SiteConfiguration.php149
-rw-r--r--includes/SiteStats.php226
-rw-r--r--includes/Skin.php233
-rw-r--r--includes/SkinLegacy.php186
-rw-r--r--includes/SkinTemplate.php332
-rw-r--r--includes/SpecialPage.php217
-rw-r--r--includes/SpecialPageFactory.php36
-rw-r--r--includes/SqlDataUpdate.php150
-rw-r--r--includes/SquidPurgeClient.php42
-rw-r--r--includes/Status.php76
-rw-r--r--includes/StreamFile.php51
-rw-r--r--includes/StringUtils.php94
-rw-r--r--includes/StubObject.php22
-rw-r--r--includes/Timestamp.php229
-rw-r--r--includes/Title.php306
-rw-r--r--includes/TitleArray.php19
-rw-r--r--includes/User.php326
-rw-r--r--includes/UserArray.php20
-rw-r--r--includes/UserMailer.php58
-rw-r--r--includes/UserRightsProxy.php21
-rw-r--r--includes/ViewCountUpdate.php9
-rw-r--r--includes/WatchedItem.php105
-rw-r--r--includes/WebRequest.php114
-rw-r--r--includes/WebStart.php2
-rw-r--r--includes/Wiki.php199
-rw-r--r--includes/WikiCategoryPage.php21
-rw-r--r--includes/WikiError.php12
-rw-r--r--includes/WikiFilePage.php29
-rw-r--r--includes/WikiMap.php40
-rw-r--r--includes/WikiPage.php629
-rw-r--r--includes/Xml.php87
-rw-r--r--includes/XmlTypeCheck.php20
-rw-r--r--includes/ZhClient.php20
-rw-r--r--includes/ZhConversion.php76
-rw-r--r--includes/ZipDirectoryReader.php46
-rw-r--r--includes/actions/CachedAction.php182
-rw-r--r--includes/actions/CreditsAction.php2
-rw-r--r--includes/actions/HistoryAction.php92
-rw-r--r--includes/actions/InfoAction.php629
-rw-r--r--includes/actions/PurgeAction.php6
-rw-r--r--includes/actions/RawAction.php26
-rw-r--r--includes/actions/RevertAction.php36
-rw-r--r--includes/actions/RevisiondeleteAction.php2
-rw-r--r--includes/actions/RollbackAction.php6
-rw-r--r--includes/actions/ViewAction.php3
-rw-r--r--includes/actions/WatchAction.php12
-rw-r--r--includes/api/ApiBase.php224
-rw-r--r--includes/api/ApiBlock.php61
-rw-r--r--includes/api/ApiComparePages.php51
-rw-r--r--includes/api/ApiDelete.php75
-rw-r--r--includes/api/ApiDisabled.php2
-rw-r--r--includes/api/ApiEditPage.php148
-rw-r--r--includes/api/ApiEmailUser.php24
-rw-r--r--includes/api/ApiExpandTemplates.php10
-rw-r--r--includes/api/ApiFeedContributions.php10
-rw-r--r--includes/api/ApiFeedWatchlist.php9
-rw-r--r--includes/api/ApiFileRevert.php38
-rw-r--r--includes/api/ApiFormatBase.php17
-rw-r--r--includes/api/ApiFormatDbg.php2
-rw-r--r--includes/api/ApiFormatJson.php2
-rw-r--r--includes/api/ApiFormatPhp.php2
-rw-r--r--includes/api/ApiFormatRaw.php2
-rw-r--r--includes/api/ApiFormatTxt.php2
-rw-r--r--includes/api/ApiFormatWddx.php2
-rw-r--r--includes/api/ApiFormatXml.php41
-rw-r--r--includes/api/ApiFormatYaml.php2
-rw-r--r--includes/api/ApiHelp.php2
-rw-r--r--includes/api/ApiImport.php42
-rw-r--r--includes/api/ApiLogin.php64
-rw-r--r--includes/api/ApiLogout.php6
-rw-r--r--includes/api/ApiMain.php161
-rw-r--r--includes/api/ApiMove.php51
-rw-r--r--includes/api/ApiOpenSearch.php4
-rw-r--r--includes/api/ApiOptions.php183
-rw-r--r--includes/api/ApiPageSet.php30
-rw-r--r--includes/api/ApiParamInfo.php58
-rw-r--r--includes/api/ApiParse.php115
-rw-r--r--includes/api/ApiPatrol.php15
-rw-r--r--includes/api/ApiProtect.php62
-rw-r--r--includes/api/ApiPurge.php38
-rw-r--r--includes/api/ApiQuery.php96
-rw-r--r--includes/api/ApiQueryAllCategories.php57
-rw-r--r--includes/api/ApiQueryAllImages.php409
-rw-r--r--includes/api/ApiQueryAllLinks.php68
-rw-r--r--includes/api/ApiQueryAllMessages.php (renamed from includes/api/ApiQueryAllmessages.php)25
-rw-r--r--includes/api/ApiQueryAllPages.php (renamed from includes/api/ApiQueryAllpages.php)45
-rw-r--r--includes/api/ApiQueryAllUsers.php103
-rw-r--r--includes/api/ApiQueryAllimages.php267
-rw-r--r--includes/api/ApiQueryBacklinks.php100
-rw-r--r--includes/api/ApiQueryBase.php13
-rw-r--r--includes/api/ApiQueryBlocks.php87
-rw-r--r--includes/api/ApiQueryCategories.php45
-rw-r--r--includes/api/ApiQueryCategoryInfo.php35
-rw-r--r--includes/api/ApiQueryCategoryMembers.php69
-rw-r--r--includes/api/ApiQueryDeletedrevs.php40
-rw-r--r--includes/api/ApiQueryDisabled.php2
-rw-r--r--includes/api/ApiQueryDuplicateFiles.php143
-rw-r--r--includes/api/ApiQueryExtLinksUsage.php17
-rw-r--r--includes/api/ApiQueryExternalLinks.php10
-rw-r--r--includes/api/ApiQueryFilearchive.php110
-rw-r--r--includes/api/ApiQueryIWBacklinks.php59
-rw-r--r--includes/api/ApiQueryIWLinks.php43
-rw-r--r--includes/api/ApiQueryImageInfo.php145
-rw-r--r--includes/api/ApiQueryImages.php38
-rw-r--r--includes/api/ApiQueryInfo.php259
-rw-r--r--includes/api/ApiQueryLangBacklinks.php59
-rw-r--r--includes/api/ApiQueryLangLinks.php38
-rw-r--r--includes/api/ApiQueryLinks.php43
-rw-r--r--includes/api/ApiQueryLogEvents.php66
-rw-r--r--includes/api/ApiQueryProtectedTitles.php38
-rw-r--r--includes/api/ApiQueryQueryPage.php37
-rw-r--r--includes/api/ApiQueryRandom.php10
-rw-r--r--includes/api/ApiQueryRecentChanges.php147
-rw-r--r--includes/api/ApiQueryRevisions.php98
-rw-r--r--includes/api/ApiQuerySearch.php59
-rw-r--r--includes/api/ApiQuerySiteinfo.php66
-rw-r--r--includes/api/ApiQueryStashImageInfo.php6
-rw-r--r--includes/api/ApiQueryTags.php21
-rw-r--r--includes/api/ApiQueryUserContributions.php100
-rw-r--r--includes/api/ApiQueryUserInfo.php74
-rw-r--r--includes/api/ApiQueryUsers.php96
-rw-r--r--includes/api/ApiQueryWatchlist.php85
-rw-r--r--includes/api/ApiQueryWatchlistRaw.php50
-rw-r--r--includes/api/ApiResult.php4
-rw-r--r--includes/api/ApiRollback.php26
-rw-r--r--includes/api/ApiSetNotificationTimestamp.php285
-rw-r--r--includes/api/ApiTokens.php158
-rw-r--r--includes/api/ApiUnblock.php48
-rw-r--r--includes/api/ApiUndelete.php22
-rw-r--r--includes/api/ApiUpload.php130
-rw-r--r--includes/api/ApiUserrights.php8
-rw-r--r--includes/api/ApiWatch.php18
-rw-r--r--includes/cache/CacheDependency.php23
-rw-r--r--includes/cache/FileCacheBase.php35
-rw-r--r--includes/cache/GenderCache.php93
-rw-r--r--includes/cache/HTMLCacheUpdate.php21
-rw-r--r--includes/cache/HTMLFileCache.php33
-rw-r--r--includes/cache/LinkBatch.php28
-rw-r--r--includes/cache/LinkCache.php22
-rw-r--r--includes/cache/MemcachedSessions.php98
-rw-r--r--includes/cache/MessageCache.php40
-rw-r--r--includes/cache/ObjectFileCache.php24
-rw-r--r--includes/cache/ProcessCacheLRU.php120
-rw-r--r--includes/cache/ResourceFileCache.php24
-rw-r--r--includes/cache/SquidUpdate.php64
-rw-r--r--includes/cache/UserCache.php134
-rw-r--r--includes/dao/IDBAccessObject.php55
-rw-r--r--includes/db/CloneDatabase.php1
-rw-r--r--includes/db/Database.php701
-rw-r--r--includes/db/DatabaseError.php27
-rw-r--r--includes/db/DatabaseIbm_db2.php165
-rw-r--r--includes/db/DatabaseMssql.php53
-rw-r--r--includes/db/DatabaseMysql.php159
-rw-r--r--includes/db/DatabaseOracle.php48
-rw-r--r--includes/db/DatabasePostgres.php622
-rw-r--r--includes/db/DatabaseSqlite.php80
-rw-r--r--includes/db/DatabaseUtility.php28
-rw-r--r--includes/db/IORMRow.php275
-rw-r--r--includes/db/IORMTable.php448
-rw-r--r--includes/db/LBFactory.php29
-rw-r--r--includes/db/LBFactory_Multi.php17
-rw-r--r--includes/db/LBFactory_Single.php21
-rw-r--r--includes/db/LoadBalancer.php25
-rw-r--r--includes/db/LoadMonitor.php17
-rw-r--r--includes/db/ORMIterator.php31
-rw-r--r--includes/db/ORMResult.php123
-rw-r--r--includes/db/ORMRow.php663
-rw-r--r--includes/db/ORMTable.php675
-rw-r--r--includes/debug/Debug.php371
-rw-r--r--includes/diff/DairikiDiff.php20
-rw-r--r--includes/diff/DifferenceEngine.php88
-rw-r--r--includes/filebackend/FSFile.php (renamed from includes/filerepo/backend/FSFile.php)29
-rw-r--r--includes/filebackend/FSFileBackend.php986
-rw-r--r--includes/filebackend/FileBackend.php1173
-rw-r--r--includes/filebackend/FileBackendGroup.php (renamed from includes/filerepo/backend/FileBackendGroup.php)37
-rw-r--r--includes/filebackend/FileBackendMultiWrite.php689
-rw-r--r--includes/filebackend/FileBackendStore.php1766
-rw-r--r--includes/filebackend/FileOp.php (renamed from includes/filerepo/backend/FileOp.php)523
-rw-r--r--includes/filebackend/FileOpBatch.php240
-rw-r--r--includes/filebackend/SwiftFileBackend.php1544
-rw-r--r--includes/filebackend/TempFSFile.php (renamed from includes/filerepo/backend/TempFSFile.php)41
-rw-r--r--includes/filebackend/filejournal/DBFileJournal.php152
-rw-r--r--includes/filebackend/filejournal/FileJournal.php196
-rw-r--r--includes/filebackend/lockmanager/DBLockManager.php374
-rw-r--r--includes/filebackend/lockmanager/FSLockManager.php (renamed from includes/filerepo/backend/lockmanager/FSLockManager.php)89
-rw-r--r--includes/filebackend/lockmanager/LSLockManager.php218
-rw-r--r--includes/filebackend/lockmanager/LockManager.php425
-rw-r--r--includes/filebackend/lockmanager/LockManagerGroup.php (renamed from includes/filerepo/backend/lockmanager/LockManagerGroup.php)62
-rw-r--r--includes/filebackend/lockmanager/MemcLockManager.php319
-rw-r--r--includes/filerepo/FSRepo.php20
-rw-r--r--includes/filerepo/FileRepo.php659
-rw-r--r--includes/filerepo/FileRepoStatus.php17
-rw-r--r--includes/filerepo/ForeignAPIRepo.php138
-rw-r--r--includes/filerepo/ForeignDBRepo.php40
-rw-r--r--includes/filerepo/ForeignDBViaLBRepo.php37
-rw-r--r--includes/filerepo/LocalRepo.php66
-rw-r--r--includes/filerepo/NullRepo.php54
-rw-r--r--includes/filerepo/RepoGroup.php120
-rw-r--r--includes/filerepo/backend/FSFileBackend.php600
-rw-r--r--includes/filerepo/backend/FileBackend.php1739
-rw-r--r--includes/filerepo/backend/FileBackendMultiWrite.php420
-rw-r--r--includes/filerepo/backend/SwiftFileBackend.php877
-rw-r--r--includes/filerepo/backend/lockmanager/DBLockManager.php469
-rw-r--r--includes/filerepo/backend/lockmanager/LSLockManager.php295
-rw-r--r--includes/filerepo/backend/lockmanager/LockManager.php182
-rw-r--r--includes/filerepo/file/ArchivedFile.php21
-rw-r--r--includes/filerepo/file/File.php219
-rw-r--r--includes/filerepo/file/ForeignAPIFile.php90
-rw-r--r--includes/filerepo/file/ForeignDBFile.php46
-rw-r--r--includes/filerepo/file/LocalFile.php491
-rw-r--r--includes/filerepo/file/OldLocalFile.php103
-rw-r--r--includes/filerepo/file/UnregisteredLocalFile.php57
-rw-r--r--includes/installer/CliInstaller.php21
-rw-r--r--includes/installer/DatabaseInstaller.php38
-rw-r--r--includes/installer/DatabaseUpdater.php127
-rw-r--r--includes/installer/Ibm_db2Installer.php17
-rw-r--r--includes/installer/Ibm_db2Updater.php25
-rw-r--r--includes/installer/InstallDocFormatter.php20
-rw-r--r--includes/installer/Installer.i18n.php7
-rw-r--r--includes/installer/Installer.php98
-rw-r--r--includes/installer/LocalSettingsGenerator.php34
-rw-r--r--includes/installer/MysqlInstaller.php33
-rw-r--r--includes/installer/MysqlUpdater.php115
-rw-r--r--includes/installer/OracleInstaller.php20
-rw-r--r--includes/installer/OracleUpdater.php59
-rw-r--r--includes/installer/PostgresInstaller.php57
-rw-r--r--includes/installer/PostgresUpdater.php248
-rw-r--r--includes/installer/SqliteInstaller.php17
-rw-r--r--includes/installer/SqliteUpdater.php33
-rw-r--r--includes/installer/WebInstaller.php57
-rw-r--r--includes/installer/WebInstallerOutput.php26
-rw-r--r--includes/installer/WebInstallerPage.php101
-rw-r--r--includes/interwiki/Interwiki.php41
-rw-r--r--includes/job/DoubleRedirectJob.php30
-rw-r--r--includes/job/EmaillingJob.php15
-rw-r--r--includes/job/EnotifNotifyJob.php15
-rw-r--r--includes/job/Job.php (renamed from includes/job/JobQueue.php)92
-rw-r--r--includes/job/RefreshLinksJob.php162
-rw-r--r--includes/job/UploadFromUrlJob.php33
-rw-r--r--includes/json/FormatJson.php32
-rw-r--r--includes/json/Services_JSON.php1
-rw-r--r--includes/libs/CSSJanus.php16
-rw-r--r--includes/libs/CSSMin.php26
-rw-r--r--includes/libs/GenericArrayObject.php244
-rw-r--r--includes/libs/HttpStatus.php21
-rw-r--r--includes/libs/IEContentAnalyzer.php6
-rw-r--r--includes/libs/IEUrlExtension.php26
-rw-r--r--includes/libs/JavaScriptMinifier.php10
-rw-r--r--includes/libs/jsminplus.php2
-rw-r--r--includes/logging/LogEntry.php43
-rw-r--r--includes/logging/LogEventsList.php347
-rw-r--r--includes/logging/LogFormatter.php393
-rw-r--r--includes/logging/LogPage.php72
-rw-r--r--includes/logging/LogPager.php2
-rw-r--r--includes/logging/PatrolLog.php53
-rw-r--r--includes/media/BMP.php17
-rw-r--r--includes/media/Bitmap.php64
-rw-r--r--includes/media/BitmapMetadataHandler.php41
-rw-r--r--includes/media/Bitmap_ClientOnly.php20
-rw-r--r--includes/media/DjVu.php50
-rw-r--r--includes/media/DjVuImage.php4
-rw-r--r--includes/media/Exif.php9
-rw-r--r--includes/media/ExifBitmap.php20
-rw-r--r--includes/media/FormatMetadata.php125
-rw-r--r--includes/media/GIF.php30
-rw-r--r--includes/media/GIFMetadataExtractor.php17
-rw-r--r--includes/media/IPTC.php31
-rw-r--r--includes/media/ImageHandler.php249
-rw-r--r--includes/media/Jpeg.php17
-rw-r--r--includes/media/JpegMetadataExtractor.php44
-rw-r--r--includes/media/MediaHandler.php (renamed from includes/media/Generic.php)287
-rw-r--r--includes/media/MediaTransformOutput.php99
-rw-r--r--includes/media/PNG.php29
-rw-r--r--includes/media/PNGMetadataExtractor.php16
-rw-r--r--includes/media/SVG.php79
-rw-r--r--includes/media/SVGMetadataExtractor.php33
-rw-r--r--includes/media/Tiff.php15
-rw-r--r--includes/media/XCF.php17
-rw-r--r--includes/media/XMP.php62
-rw-r--r--includes/media/XMPInfo.php22
-rw-r--r--includes/media/XMPValidate.php22
-rw-r--r--includes/mobile/DeviceDetection.php459
-rw-r--r--includes/normal/RandomTest.php4
-rw-r--r--includes/normal/UtfNormal.php3
-rw-r--r--includes/normal/UtfNormalDefines.php15
-rw-r--r--includes/normal/UtfNormalTest.php1
-rw-r--r--includes/normal/UtfNormalTest2.php18
-rw-r--r--includes/objectcache/APCBagOStuff.php60
-rw-r--r--includes/objectcache/BagOStuff.php119
-rw-r--r--includes/objectcache/DBABagOStuff.php127
-rw-r--r--includes/objectcache/EhcacheBagOStuff.php82
-rw-r--r--includes/objectcache/EmptyBagOStuff.php37
-rw-r--r--includes/objectcache/HashBagOStuff.php44
-rw-r--r--includes/objectcache/MemcachedBagOStuff.php180
-rw-r--r--includes/objectcache/MemcachedClient.php281
-rw-r--r--includes/objectcache/MemcachedPeclBagOStuff.php237
-rw-r--r--includes/objectcache/MemcachedPhpBagOStuff.php139
-rw-r--r--includes/objectcache/MultiWriteBagOStuff.php86
-rw-r--r--includes/objectcache/ObjectCache.php48
-rw-r--r--includes/objectcache/ObjectCacheSessionHandler.php145
-rw-r--r--includes/objectcache/RedisBagOStuff.php413
-rw-r--r--includes/objectcache/SqlBagOStuff.php314
-rw-r--r--includes/objectcache/WinCacheBagOStuff.php24
-rw-r--r--includes/objectcache/XCacheBagOStuff.php44
-rw-r--r--includes/parser/CacheTime.php132
-rw-r--r--includes/parser/CoreLinkFunctions.php16
-rw-r--r--includes/parser/CoreParserFunctions.php169
-rw-r--r--includes/parser/CoreTagHooks.php16
-rw-r--r--includes/parser/DateFormatter.php91
-rw-r--r--includes/parser/LinkHolderArray.php32
-rw-r--r--includes/parser/Parser.php373
-rw-r--r--includes/parser/ParserCache.php33
-rw-r--r--includes/parser/ParserOptions.php55
-rw-r--r--includes/parser/ParserOutput.php175
-rw-r--r--includes/parser/Parser_DiffTest.php16
-rw-r--r--includes/parser/Parser_LinkHooks.php20
-rw-r--r--includes/parser/Preprocessor.php28
-rw-r--r--includes/parser/Preprocessor_DOM.php80
-rw-r--r--includes/parser/Preprocessor_Hash.php78
-rw-r--r--includes/parser/Preprocessor_HipHop.hphp106
-rw-r--r--includes/parser/StripState.php55
-rw-r--r--includes/parser/Tidy.php22
-rw-r--r--includes/profiler/Profiler.php131
-rw-r--r--includes/profiler/ProfilerSimple.php78
-rw-r--r--includes/profiler/ProfilerSimpleText.php17
-rw-r--r--includes/profiler/ProfilerSimpleTrace.php36
-rw-r--r--includes/profiler/ProfilerSimpleUDP.php21
-rw-r--r--includes/profiler/ProfilerStub.php27
-rw-r--r--includes/resourceloader/ResourceLoader.php166
-rw-r--r--includes/resourceloader/ResourceLoaderContext.php13
-rw-r--r--includes/resourceloader/ResourceLoaderFileModule.php15
-rw-r--r--includes/resourceloader/ResourceLoaderFilePageModule.php21
-rw-r--r--includes/resourceloader/ResourceLoaderLanguageDataModule.php122
-rw-r--r--includes/resourceloader/ResourceLoaderModule.php21
-rw-r--r--includes/resourceloader/ResourceLoaderNoscriptModule.php2
-rw-r--r--includes/resourceloader/ResourceLoaderSiteModule.php2
-rw-r--r--includes/resourceloader/ResourceLoaderStartUpModule.php54
-rw-r--r--includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php14
-rw-r--r--includes/resourceloader/ResourceLoaderUserGroupsModule.php39
-rw-r--r--includes/resourceloader/ResourceLoaderUserModule.php57
-rw-r--r--includes/resourceloader/ResourceLoaderUserOptionsModule.php11
-rw-r--r--includes/resourceloader/ResourceLoaderUserTokensModule.php9
-rw-r--r--includes/resourceloader/ResourceLoaderWikiModule.php19
-rw-r--r--includes/revisiondelete/RevisionDelete.php152
-rw-r--r--includes/revisiondelete/RevisionDeleteAbstracts.php38
-rw-r--r--includes/revisiondelete/RevisionDeleteUser.php22
-rw-r--r--includes/revisiondelete/RevisionDeleter.php84
-rw-r--r--includes/search/SearchEngine.php47
-rw-r--r--includes/search/SearchIBM_DB2.php5
-rw-r--r--includes/search/SearchMssql.php7
-rw-r--r--includes/search/SearchMySQL.php5
-rw-r--r--includes/search/SearchOracle.php4
-rw-r--r--includes/search/SearchPostgres.php1
-rw-r--r--includes/search/SearchSqlite.php1
-rw-r--r--includes/search/SearchUpdate.php15
-rw-r--r--includes/specials/SpecialActiveusers.php54
-rw-r--r--includes/specials/SpecialAllmessages.php22
-rw-r--r--includes/specials/SpecialAllpages.php75
-rw-r--r--includes/specials/SpecialAncientpages.php6
-rw-r--r--includes/specials/SpecialBlock.php196
-rw-r--r--includes/specials/SpecialBlockList.php2
-rw-r--r--includes/specials/SpecialBooksources.php13
-rw-r--r--includes/specials/SpecialBrokenRedirects.php8
-rw-r--r--includes/specials/SpecialCachedPage.php198
-rw-r--r--includes/specials/SpecialCategories.php9
-rw-r--r--includes/specials/SpecialChangeEmail.php84
-rw-r--r--includes/specials/SpecialChangePassword.php31
-rw-r--r--includes/specials/SpecialConfirmemail.php4
-rw-r--r--includes/specials/SpecialContributions.php413
-rw-r--r--includes/specials/SpecialDeadendpages.php10
-rw-r--r--includes/specials/SpecialDeletedContributions.php55
-rw-r--r--includes/specials/SpecialDisambiguations.php75
-rw-r--r--includes/specials/SpecialDoubleRedirects.php16
-rw-r--r--includes/specials/SpecialEditWatchlist.php84
-rw-r--r--includes/specials/SpecialEmailuser.php74
-rw-r--r--includes/specials/SpecialExport.php49
-rw-r--r--includes/specials/SpecialFewestrevisions.php16
-rw-r--r--includes/specials/SpecialFileDuplicateSearch.php30
-rw-r--r--includes/specials/SpecialFilepath.php6
-rw-r--r--includes/specials/SpecialImport.php97
-rw-r--r--includes/specials/SpecialJavaScriptTest.php61
-rw-r--r--includes/specials/SpecialLinkSearch.php29
-rw-r--r--includes/specials/SpecialListfiles.php60
-rw-r--r--includes/specials/SpecialListgrouprights.php44
-rw-r--r--includes/specials/SpecialListredirects.php8
-rw-r--r--includes/specials/SpecialListusers.php126
-rw-r--r--includes/specials/SpecialLockdb.php10
-rw-r--r--includes/specials/SpecialLog.php35
-rw-r--r--includes/specials/SpecialLonelypages.php8
-rw-r--r--includes/specials/SpecialMIMEsearch.php31
-rw-r--r--includes/specials/SpecialMergeHistory.php66
-rw-r--r--includes/specials/SpecialMostcategories.php40
-rw-r--r--includes/specials/SpecialMostimages.php6
-rw-r--r--includes/specials/SpecialMostinterwikis.php112
-rw-r--r--includes/specials/SpecialMostlinked.php17
-rw-r--r--includes/specials/SpecialMostlinkedcategories.php22
-rw-r--r--includes/specials/SpecialMostlinkedtemplates.php24
-rw-r--r--includes/specials/SpecialMovepage.php77
-rw-r--r--includes/specials/SpecialNewimages.php34
-rw-r--r--includes/specials/SpecialNewpages.php66
-rw-r--r--includes/specials/SpecialPasswordReset.php17
-rw-r--r--includes/specials/SpecialPopularpages.php14
-rw-r--r--includes/specials/SpecialPreferences.php7
-rw-r--r--includes/specials/SpecialPrefixindex.php78
-rw-r--r--includes/specials/SpecialProtectedpages.php89
-rw-r--r--includes/specials/SpecialProtectedtitles.php55
-rw-r--r--includes/specials/SpecialRandompage.php2
-rw-r--r--includes/specials/SpecialRecentchanges.php119
-rw-r--r--includes/specials/SpecialRecentchangeslinked.php23
-rw-r--r--includes/specials/SpecialRevisiondelete.php105
-rw-r--r--includes/specials/SpecialSearch.php159
-rw-r--r--includes/specials/SpecialShortpages.php55
-rw-r--r--includes/specials/SpecialStatistics.php46
-rw-r--r--includes/specials/SpecialTags.php27
-rw-r--r--includes/specials/SpecialUnblock.php15
-rw-r--r--includes/specials/SpecialUncategorizedimages.php6
-rw-r--r--includes/specials/SpecialUncategorizedpages.php6
-rw-r--r--includes/specials/SpecialUndelete.php111
-rw-r--r--includes/specials/SpecialUnusedcategories.php7
-rw-r--r--includes/specials/SpecialUnusedimages.php8
-rw-r--r--includes/specials/SpecialUnusedtemplates.php16
-rw-r--r--includes/specials/SpecialUnwatchedpages.php20
-rw-r--r--includes/specials/SpecialUpload.php141
-rw-r--r--includes/specials/SpecialUploadStash.php44
-rw-r--r--includes/specials/SpecialUserlogin.php158
-rw-r--r--includes/specials/SpecialUserlogout.php2
-rw-r--r--includes/specials/SpecialUserrights.php38
-rw-r--r--includes/specials/SpecialVersion.php256
-rw-r--r--includes/specials/SpecialWantedcategories.php6
-rw-r--r--includes/specials/SpecialWantedfiles.php9
-rw-r--r--includes/specials/SpecialWantedpages.php8
-rw-r--r--includes/specials/SpecialWantedtemplates.php8
-rw-r--r--includes/specials/SpecialWatchlist.php81
-rw-r--r--includes/specials/SpecialWhatlinkshere.php44
-rw-r--r--includes/specials/SpecialWithoutinterwiki.php18
-rw-r--r--includes/templates/NoLocalSettings.php17
-rw-r--r--includes/templates/Usercreate.php22
-rw-r--r--includes/templates/Userlogin.php38
-rw-r--r--includes/tidy.conf1
-rw-r--r--includes/upload/UploadBase.php148
-rw-r--r--includes/upload/UploadFromChunks.php122
-rw-r--r--includes/upload/UploadFromFile.php33
-rw-r--r--includes/upload/UploadFromStash.php28
-rw-r--r--includes/upload/UploadFromUrl.php86
-rw-r--r--includes/upload/UploadStash.php48
-rw-r--r--includes/zhtable/Makefile.py14
-rw-r--r--includes/zhtable/simp2trad.manual1
-rw-r--r--includes/zhtable/toCN.manual1
-rw-r--r--includes/zhtable/toHK.manual58
-rw-r--r--includes/zhtable/toTW.manual1
-rw-r--r--includes/zhtable/trad2simp.manual1
-rw-r--r--includes/zhtable/tradphrases.manual3
541 files changed, 41910 insertions, 16311 deletions
diff --git a/includes/Action.php b/includes/Action.php
index 37c48488..51922251 100644
--- a/includes/Action.php
+++ b/includes/Action.php
@@ -1,15 +1,6 @@
<?php
/**
- * @defgroup Actions Action done on pages
- */
-
-/**
- * Actions are things which can be done to pages (edit, delete, rollback, etc). They
- * are distinct from Special Pages because an action must apply to exactly one page.
- *
- * To add an action in an extension, create a subclass of Action, and add the key to
- * $wgActions. There is also the deprecated UnknownAction hook
- *
+ * Base classes for actions done on 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
@@ -27,23 +18,39 @@
*
* @file
*/
+
+/**
+ * @defgroup Actions Action done on pages
+ */
+
+/**
+ * Actions are things which can be done to pages (edit, delete, rollback, etc). They
+ * are distinct from Special Pages because an action must apply to exactly one page.
+ *
+ * To add an action in an extension, create a subclass of Action, and add the key to
+ * $wgActions. There is also the deprecated UnknownAction hook
+ *
+ * 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.
+ */
abstract class Action {
/**
* Page on which we're performing the action
- * @var Page
+ * @var Page $page
*/
protected $page;
/**
* IContextSource if specified; otherwise we'll use the Context from the Page
- * @var IContextSource
+ * @var IContextSource $context
*/
protected $context;
/**
* The fields used to create the HTMLForm
- * @var Array
+ * @var Array $fields
*/
protected $fields;
@@ -78,7 +85,7 @@ abstract class Action {
* @param $action String
* @param $page Page
* @param $context IContextSource
- * @return Action|false|null false if the action is disabled, null
+ * @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 ) {
@@ -128,7 +135,7 @@ abstract class Action {
if ( !$context->canUseWikiPage() ) {
return 'view';
}
-
+
$action = Action::factory( $actionName, $context->getWikiPage() );
if ( $action instanceof Action ) {
return $action->getName();
@@ -266,6 +273,7 @@ abstract class Action {
*
* @param $user User: the user to check, or null to use the context user
* @throws ErrorPageError
+ * @return bool True on success
*/
protected function checkCanExecute( User $user ) {
$right = $this->getRestriction();
@@ -277,7 +285,7 @@ abstract class Action {
}
if ( $this->requiresUnblock() && $user->isBlocked() ) {
- $block = $user->mBlock;
+ $block = $user->getBlock();
throw new UserBlockedError( $block );
}
@@ -287,6 +295,7 @@ abstract class Action {
if ( $this->requiresWrite() && wfReadOnly() ) {
throw new ReadOnlyError();
}
+ return true;
}
/**
@@ -332,7 +341,7 @@ abstract class Action {
* @return String
*/
protected function getDescription() {
- return wfMsgHtml( strtolower( $this->getName() ) );
+ return $this->msg( strtolower( $this->getName() ) )->escaped();
}
/**
@@ -350,6 +359,9 @@ abstract class Action {
public abstract function execute();
}
+/**
+ * An action which shows a form and does something based on the input from the form
+ */
abstract class FormAction extends Action {
/**
@@ -385,7 +397,7 @@ abstract class FormAction extends Action {
// Give hooks a chance to alter the form, adding extra fields or text etc
wfRunHooks( 'ActionModifyFormFields', array( $this->getName(), &$this->fields, $this->page ) );
- $form = new HTMLForm( $this->fields, $this->getContext() );
+ $form = new HTMLForm( $this->fields, $this->getContext(), $this->getName() );
$form->setSubmitCallback( array( $this, 'onSubmit' ) );
// Retain query parameters (uselang etc)
@@ -444,8 +456,8 @@ abstract class FormAction extends Action {
/**
* @see Action::execute()
* @throws ErrorPageError
- * @param array|null $data
- * @param bool $captureErrors
+ * @param $data array|null
+ * @param $captureErrors bool
* @return bool
*/
public function execute( array $data = null, $captureErrors = true ) {
@@ -486,9 +498,7 @@ abstract class FormAction extends Action {
}
/**
- * 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).
+ * An action which just does something, without showing a form first.
*/
abstract class FormlessAction extends Action {
@@ -501,15 +511,23 @@ abstract class FormlessAction extends Action {
/**
* We don't want an HTMLForm
+ * @return bool
*/
protected function getFormFields() {
return false;
}
+ /**
+ * @param $data Array
+ * @return bool
+ */
public function onSubmit( $data ) {
return false;
}
+ /**
+ * @return bool
+ */
public function onSuccess() {
return false;
}
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php
index 5bc9f067..b00cf309 100644
--- a/includes/AjaxDispatcher.php
+++ b/includes/AjaxDispatcher.php
@@ -1,10 +1,28 @@
<?php
/**
- * @defgroup Ajax Ajax
+ * Handle ajax requests and send them to the proper handler.
+ *
+ * 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 Ajax
- * Handle ajax requests and send them to the proper handler.
+ */
+
+/**
+ * @defgroup Ajax Ajax
*/
/**
@@ -12,16 +30,26 @@
* @ingroup Ajax
*/
class AjaxDispatcher {
- /** The way the request was made, either a 'get' or a 'post' */
+ /**
+ * The way the request was made, either a 'get' or a 'post'
+ * @var string $mode
+ */
private $mode;
- /** Name of the requested handler */
+ /**
+ * Name of the requested handler
+ * @var string $func_name
+ */
private $func_name;
- /** Arguments passed */
+ /** Arguments passed
+ * @var array $args
+ */
private $args;
- /** Load up our object with user supplied data */
+ /**
+ * Load up our object with user supplied data
+ */
function __construct() {
wfProfileIn( __METHOD__ );
@@ -62,13 +90,14 @@ class AjaxDispatcher {
wfProfileOut( __METHOD__ );
}
- /** Pass the request to our internal function.
+ /**
+ * Pass the request to our internal function.
* BEWARE! Data are passed as they have been supplied by the user,
* they should be carefully handled in the function processing the
* request.
*/
function performAction() {
- global $wgAjaxExportList, $wgOut, $wgUser;
+ global $wgAjaxExportList, $wgUser;
if ( empty( $this->mode ) ) {
return;
@@ -84,7 +113,7 @@ class AjaxDispatcher {
'Bad Request',
"unknown function " . (string) $this->func_name
);
- } elseif ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true )
+ } elseif ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true )
&& !$wgUser->isAllowed( 'read' ) )
{
wfHttpError(
@@ -94,14 +123,8 @@ class AjaxDispatcher {
} else {
wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" );
- if ( strpos( $this->func_name, '::' ) !== false ) {
- $func = explode( '::', $this->func_name, 2 );
- } else {
- $func = $this->func_name;
- }
-
try {
- $result = call_user_func_array( $func, $this->args );
+ $result = call_user_func_array( $this->func_name, $this->args );
if ( $result === false || $result === null ) {
wfDebug( __METHOD__ . ' ERROR while dispatching '
@@ -134,7 +157,6 @@ class AjaxDispatcher {
}
}
- $wgOut = null;
wfProfileOut( __METHOD__ );
}
}
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php
index e60ca23c..6bf94ccb 100644
--- a/includes/AjaxResponse.php
+++ b/includes/AjaxResponse.php
@@ -1,6 +1,21 @@
<?php
/**
- * Response handler for Ajax requests
+ * Response handler for Ajax requests.
+ *
+ * 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 Ajax
@@ -13,27 +28,52 @@
* @ingroup Ajax
*/
class AjaxResponse {
- /** Number of seconds to get the response cached by a proxy */
+
+ /**
+ * Number of seconds to get the response cached by a proxy
+ * @var int $mCacheDuration
+ */
private $mCacheDuration;
- /** HTTP header Content-Type */
+ /**
+ * HTTP header Content-Type
+ * @var string $mContentType
+ */
private $mContentType;
- /** Disables output. Can be set by calling $AjaxResponse->disable() */
+ /**
+ * Disables output. Can be set by calling $AjaxResponse->disable()
+ * @var bool $mDisabled
+ */
private $mDisabled;
- /** Date for the HTTP header Last-modified */
+ /**
+ * Date for the HTTP header Last-modified
+ * @var string|false $mLastModified
+ */
private $mLastModified;
- /** HTTP response code */
+ /**
+ * HTTP response code
+ * @var string $mResponseCode
+ */
private $mResponseCode;
- /** HTTP Vary header */
+ /**
+ * HTTP Vary header
+ * @var string $mVary
+ */
private $mVary;
- /** Content of our HTTP response */
+ /**
+ * Content of our HTTP response
+ * @var string $mText
+ */
private $mText;
+ /**
+ * @param $text string|null
+ */
function __construct( $text = null ) {
$this->mCacheDuration = null;
$this->mVary = null;
@@ -49,41 +89,67 @@ class AjaxResponse {
}
}
+ /**
+ * Set the number of seconds to get the response cached by a proxy
+ * @param $duration int
+ */
function setCacheDuration( $duration ) {
$this->mCacheDuration = $duration;
}
+ /**
+ * Set the HTTP Vary header
+ * @param $vary string
+ */
function setVary( $vary ) {
$this->mVary = $vary;
}
+ /**
+ * Set the HTTP response code
+ * @param $code string
+ */
function setResponseCode( $code ) {
$this->mResponseCode = $code;
}
+ /**
+ * Set the HTTP header Content-Type
+ * @param $type string
+ */
function setContentType( $type ) {
$this->mContentType = $type;
}
+ /**
+ * Disable output.
+ */
function disable() {
$this->mDisabled = true;
}
- /** Add content to the response */
+ /**
+ * Add content to the response
+ * @param $text string
+ */
function addText( $text ) {
if ( ! $this->mDisabled && $text ) {
$this->mText .= $text;
}
}
- /** Output text */
+ /**
+ * Output text
+ */
function printText() {
if ( ! $this->mDisabled ) {
print $this->mText;
}
}
- /** Construct the header and output it */
+ /**
+ * Construct the header and output it
+ */
function sendHeaders() {
global $wgUseSquid, $wgUseESI;
@@ -139,8 +205,10 @@ class AjaxResponse {
/**
* checkLastModified tells the client to use the client-cached response if
* possible. If sucessful, the AjaxResponse is disabled so that
- * any future call to AjaxResponse::printText() have no effect. The method
- * returns true iff the response code was set to 304 Not Modified.
+ * 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 ) {
global $wgCachePages, $wgCacheEpoch, $wgUser;
@@ -148,21 +216,21 @@ class AjaxResponse {
if ( !$timestamp || $timestamp == '19700101000000' ) {
wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" );
- return;
+ return false;
}
if ( !$wgCachePages ) {
wfDebug( "$fname: CACHE DISABLED\n", false );
- return;
+ return false;
}
if ( $wgUser->getOption( 'nocache' ) ) {
wfDebug( "$fname: USER DISABLED CACHE\n", false );
- return;
+ return false;
}
$timestamp = wfTimestamp( TS_MW, $timestamp );
- $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) );
+ $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->getTouched(), $wgCacheEpoch ) );
if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
# IE sends sizes after the date like this:
@@ -191,11 +259,12 @@ class AjaxResponse {
wfDebug( "$fname: client did not send If-Modified-Since header\n", false );
$this->mLastModified = $lastmod;
}
+ return false;
}
/**
- * @param $mckey
- * @param $touched
+ * @param $mckey string
+ * @param $touched int
* @return bool
*/
function loadFromMemcached( $mckey, $touched ) {
@@ -222,7 +291,7 @@ class AjaxResponse {
}
/**
- * @param $mckey
+ * @param $mckey string
* @param $expiry int
* @return bool
*/
diff --git a/includes/Article.php b/includes/Article.php
index b07f309c..9ab4b6ba 100644
--- a/includes/Article.php
+++ b/includes/Article.php
@@ -1,6 +1,22 @@
<?php
/**
- * File for articles
+ * User interface for page actions.
+ *
+ * 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
*/
@@ -9,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
@@ -23,42 +39,69 @@ class Article extends Page {
*/
/**
- * @var IContextSource
+ * The context this Article is executed in
+ * @var IContextSource $mContext
*/
protected $mContext;
/**
- * @var WikiPage
+ * The WikiPage object of this instance
+ * @var WikiPage $mPage
*/
protected $mPage;
/**
- * @var ParserOptions: ParserOptions object for $wgUser articles
+ * ParserOptions object for $wgUser articles
+ * @var ParserOptions $mParserOptions
*/
public $mParserOptions;
+ /**
+ * Content of the revision we are working on
+ * @var string $mContent
+ */
var $mContent; // !<
+
+ /**
+ * Is the content ($mContent) already loaded?
+ * @var bool $mContentLoaded
+ */
var $mContentLoaded = false; // !<
+
+ /**
+ * The oldid of the article that is to be shown, 0 for the
+ * current revision
+ * @var int|null $mOldId
+ */
var $mOldId; // !<
/**
- * @var Title
+ * Title from which we were redirected here
+ * @var Title $mRedirectedFrom
*/
var $mRedirectedFrom = null;
/**
- * @var mixed: boolean false or URL string
+ * URL to redirect to or false if none
+ * @var string|false $mRedirectUrl
*/
var $mRedirectUrl = false; // !<
+
+ /**
+ * Revision ID of revision we are working on
+ * @var int $mRevIdFetched
+ */
var $mRevIdFetched = 0; // !<
/**
- * @var Revision
+ * Revision we are working on
+ * @var Revision $mRevision
*/
var $mRevision = null;
/**
- * @var ParserOutput
+ * ParserOutput object
+ * @var ParserOutput $mParserOutput
*/
var $mParserOutput;
@@ -188,11 +231,9 @@ class Article extends Page {
* This function has side effects! Do not use this function if you
* only want the real revision text if any.
*
- * @return Return the text of this revision
+ * @return string Return the text of this revision
*/
public function getContent() {
- global $wgUser;
-
wfProfileIn( __METHOD__ );
if ( $this->mPage->getID() === 0 ) {
@@ -204,7 +245,8 @@ class Article extends Page {
$text = '';
}
} else {
- $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
+ $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
+ $text = wfMessage( $message )->text();
}
wfProfileOut( __METHOD__ );
@@ -235,11 +277,10 @@ class Article extends Page {
* @return int The old id for the request
*/
public function getOldIDFromRequest() {
- global $wgRequest;
-
$this->mRedirectUrl = false;
- $oldid = $wgRequest->getIntOrNull( 'oldid' );
+ $request = $this->getContext()->getRequest();
+ $oldid = $request->getIntOrNull( 'oldid' );
if ( $oldid === null ) {
return 0;
@@ -248,17 +289,21 @@ class Article extends Page {
if ( $oldid !== 0 ) {
# Load the given revision and check whether the page is another one.
# In that case, update this instance to reflect the change.
- $this->mRevision = Revision::newFromId( $oldid );
- if ( $this->mRevision !== null ) {
- // Revision title doesn't match the page title given?
- if ( $this->mPage->getID() != $this->mRevision->getPage() ) {
- $function = array( get_class( $this->mPage ), 'newFromID' );
- $this->mPage = call_user_func( $function, $this->mRevision->getPage() );
+ if ( $oldid === $this->mPage->getLatest() ) {
+ $this->mRevision = $this->mPage->getRevision();
+ } else {
+ $this->mRevision = Revision::newFromId( $oldid );
+ if ( $this->mRevision !== null ) {
+ // Revision title doesn't match the page title given?
+ if ( $this->mPage->getID() != $this->mRevision->getPage() ) {
+ $function = array( get_class( $this->mPage ), 'newFromID' );
+ $this->mPage = call_user_func( $function, $this->mRevision->getPage() );
+ }
}
}
}
- if ( $wgRequest->getVal( 'direction' ) == 'next' ) {
+ if ( $request->getVal( 'direction' ) == 'next' ) {
$nextid = $this->getTitle()->getNextRevisionID( $oldid );
if ( $nextid ) {
$oldid = $nextid;
@@ -266,7 +311,7 @@ class Article extends Page {
} else {
$this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
}
- } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) {
+ } elseif ( $request->getVal( 'direction' ) == 'prev' ) {
$previd = $this->getTitle()->getPreviousRevisionID( $oldid );
if ( $previd ) {
$oldid = $previd;
@@ -306,9 +351,7 @@ class Article extends Page {
# Pre-fill content with error message so that if something
# fails we'll have something telling us what we intended.
- $t = $this->getTitle()->getPrefixedText();
- $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
- $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
+ $this->mContent = wfMessage( 'missing-revision', $oldid )->plain();
if ( $oldid ) {
# $this->mRevision might already be fetched by getOldIDFromRequest()
@@ -400,8 +443,7 @@ class Article extends Page {
* page of the given title.
*/
public function view() {
- global $wgUser, $wgOut, $wgRequest, $wgParser;
- global $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
+ global $wgParser, $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
wfProfileIn( __METHOD__ );
@@ -411,17 +453,19 @@ class Article extends Page {
# the first call of this method even if $oldid is used way below.
$oldid = $this->getOldID();
+ $user = $this->getContext()->getUser();
# Another whitelist check in case getOldID() is altering the title
- $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $wgUser );
+ $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $user );
if ( count( $permErrors ) ) {
wfDebug( __METHOD__ . ": denied on secondary read check\n" );
wfProfileOut( __METHOD__ );
throw new PermissionsError( 'read', $permErrors );
}
+ $outputPage = $this->getContext()->getOutput();
# getOldID() may as well want us to redirect somewhere else
if ( $this->mRedirectUrl ) {
- $wgOut->redirect( $this->mRedirectUrl );
+ $outputPage->redirect( $this->mRedirectUrl );
wfDebug( __METHOD__ . ": redirecting due to oldid\n" );
wfProfileOut( __METHOD__ );
@@ -429,7 +473,7 @@ class Article extends Page {
}
# If we got diff in the query, we want to see a diff page instead of the article.
- if ( $wgRequest->getCheck( 'diff' ) ) {
+ if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) {
wfDebug( __METHOD__ . ": showing diff page\n" );
$this->showDiffPage();
wfProfileOut( __METHOD__ );
@@ -438,31 +482,31 @@ class Article extends Page {
}
# Set page title (may be overridden by DISPLAYTITLE)
- $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() );
+ $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() );
- $wgOut->setArticleFlag( true );
+ $outputPage->setArticleFlag( true );
# Allow frames by default
- $wgOut->allowClickjacking();
+ $outputPage->allowClickjacking();
$parserCache = ParserCache::singleton();
$parserOptions = $this->getParserOptions();
# Render printable version, use printable version cache
- if ( $wgOut->isPrintable() ) {
+ if ( $outputPage->isPrintable() ) {
$parserOptions->setIsPrintable( true );
$parserOptions->setEditSection( false );
- } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit' ) ) {
+ } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) {
$parserOptions->setEditSection( false );
}
# Try client and file cache
if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) {
if ( $wgUseETag ) {
- $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) );
+ $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) );
}
# Is it client cached?
- if ( $wgOut->checkLastModified( $this->mPage->getTouched() ) ) {
+ if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) {
wfDebug( __METHOD__ . ": done 304\n" );
wfProfileOut( __METHOD__ );
@@ -471,8 +515,8 @@ class Article extends Page {
} elseif ( $wgUseFileCache && $this->tryFileCache() ) {
wfDebug( __METHOD__ . ": done file cache\n" );
# tell wgOut that output is taken care of
- $wgOut->disable();
- $this->mPage->doViewUpdates( $wgUser );
+ $outputPage->disable();
+ $this->mPage->doViewUpdates( $user );
wfProfileOut( __METHOD__ );
return;
@@ -482,7 +526,7 @@ class Article extends Page {
# Should the parser cache be used?
$useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid );
wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
- if ( $wgUser->getStubThreshold() ) {
+ if ( $user->getStubThreshold() ) {
wfIncrStats( 'pcache_miss_stub' );
}
@@ -520,14 +564,14 @@ class Article extends Page {
} else {
wfDebug( __METHOD__ . ": showing parser cache contents\n" );
}
- $wgOut->addParserOutput( $this->mParserOutput );
+ $outputPage->addParserOutput( $this->mParserOutput );
# Ensure that UI elements requiring revision ID have
# the correct version information.
- $wgOut->setRevisionId( $this->mPage->getLatest() );
+ $outputPage->setRevisionId( $this->mPage->getLatest() );
# Preload timestamp to avoid a DB hit
$cachedTimestamp = $this->mParserOutput->getTimestamp();
if ( $cachedTimestamp !== null ) {
- $wgOut->setRevisionTimestamp( $cachedTimestamp );
+ $outputPage->setRevisionTimestamp( $cachedTimestamp );
$this->mPage->setTimestamp( $cachedTimestamp );
}
$outputDone = true;
@@ -551,16 +595,16 @@ class Article extends Page {
# Ensure that UI elements requiring revision ID have
# the correct version information.
- $wgOut->setRevisionId( $this->getRevIdFetched() );
+ $outputPage->setRevisionId( $this->getRevIdFetched() );
# Preload timestamp to avoid a DB hit
- $wgOut->setRevisionTimestamp( $this->getTimestamp() );
+ $outputPage->setRevisionTimestamp( $this->getTimestamp() );
# Pages containing custom CSS or JavaScript get special treatment
if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) {
wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
$this->showCssOrJsPage();
$outputDone = true;
- } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) {
+ } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
# Allow extensions do their own custom view for certain pages
$outputDone = true;
} else {
@@ -569,10 +613,10 @@ class Article extends Page {
if ( $rt ) {
wfDebug( __METHOD__ . ": showing redirect=no page\n" );
# Viewing a redirect page (e.g. with parameter redirect=no)
- $wgOut->addHTML( $this->viewRedirect( $rt ) );
+ $outputPage->addHTML( $this->viewRedirect( $rt ) );
# Parse just to get categories, displaytitle, etc.
$this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
- $wgOut->addParserOutputNoText( $this->mParserOutput );
+ $outputPage->addParserOutputNoText( $this->mParserOutput );
$outputDone = true;
}
}
@@ -587,12 +631,12 @@ class Article extends Page {
if ( !$poolArticleView->execute() ) {
$error = $poolArticleView->getError();
if ( $error ) {
- $wgOut->clearHTML(); // for release() errors
- $wgOut->enableClientCache( false );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $outputPage->clearHTML(); // for release() errors
+ $outputPage->enableClientCache( false );
+ $outputPage->setRobotPolicy( 'noindex,nofollow' );
$errortext = $error->getWikiText( false, 'view-pool-error' );
- $wgOut->addWikiText( '<div class="errorbox">' . $errortext . '</div>' );
+ $outputPage->addWikiText( '<div class="errorbox">' . $errortext . '</div>' );
}
# Connection or timeout error
wfProfileOut( __METHOD__ );
@@ -600,12 +644,12 @@ class Article extends Page {
}
$this->mParserOutput = $poolArticleView->getParserOutput();
- $wgOut->addParserOutput( $this->mParserOutput );
+ $outputPage->addParserOutput( $this->mParserOutput );
# Don't cache a dirty ParserOutput object
if ( $poolArticleView->getIsDirty() ) {
- $wgOut->setSquidMaxage( 0 );
- $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" );
+ $outputPage->setSquidMaxage( 0 );
+ $outputPage->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" );
}
$outputDone = true;
@@ -634,17 +678,17 @@ class Article extends Page {
if ( $this->getTitle()->isMainPage() ) {
$msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage();
if ( !$msg->isDisabled() ) {
- $wgOut->setHTMLTitle( $msg->title( $this->getTitle() )->text() );
+ $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() );
}
}
# Check for any __NOINDEX__ tags on the page using $pOutput
$policy = $this->getRobotPolicy( 'view', $pOutput );
- $wgOut->setIndexPolicy( $policy['index'] );
- $wgOut->setFollowPolicy( $policy['follow'] );
+ $outputPage->setIndexPolicy( $policy['index'] );
+ $outputPage->setFollowPolicy( $policy['follow'] );
$this->showViewFooter();
- $this->mPage->doViewUpdates( $wgUser );
+ $this->mPage->doViewUpdates( $user );
wfProfileOut( __METHOD__ );
}
@@ -654,11 +698,10 @@ class Article extends Page {
* @param $pOutput ParserOutput
*/
public function adjustDisplayTitle( ParserOutput $pOutput ) {
- global $wgOut;
# Adjust the title if it was set by displaytitle, -{T|}- or language conversion
$titleText = $pOutput->getTitleText();
if ( strval( $titleText ) !== '' ) {
- $wgOut->setPageTitle( $titleText );
+ $this->getContext()->getOutput()->setPageTitle( $titleText );
}
}
@@ -667,13 +710,13 @@ class Article extends Page {
* Article::view() only, other callers should use the DifferenceEngine class.
*/
public function showDiffPage() {
- global $wgRequest, $wgUser;
-
- $diff = $wgRequest->getVal( 'diff' );
- $rcid = $wgRequest->getVal( 'rcid' );
- $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
- $purge = $wgRequest->getVal( 'action' ) == 'purge';
- $unhide = $wgRequest->getInt( 'unhide' ) == 1;
+ $request = $this->getContext()->getRequest();
+ $user = $this->getContext()->getUser();
+ $diff = $request->getVal( 'diff' );
+ $rcid = $request->getVal( 'rcid' );
+ $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) );
+ $purge = $request->getVal( 'action' ) == 'purge';
+ $unhide = $request->getInt( 'unhide' ) == 1;
$oldid = $this->getOldID();
$de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
@@ -683,7 +726,7 @@ class Article extends Page {
if ( $diff == 0 || $diff == $this->mPage->getLatest() ) {
# Run view updates for current revision only
- $this->mPage->doViewUpdates( $wgUser );
+ $this->mPage->doViewUpdates( $user );
}
}
@@ -695,22 +738,21 @@ class Article extends Page {
* page views.
*/
protected function showCssOrJsPage() {
- global $wgOut;
-
$dir = $this->getContext()->getLanguage()->getDir();
$lang = $this->getContext()->getLanguage()->getCode();
- $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
+ $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(), $wgOut ) ) ) {
+ 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 );
- $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
- $wgOut->addHTML( "\n</pre>\n" );
+ $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+ $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
+ $outputPage->addHTML( "\n</pre>\n" );
}
}
@@ -722,8 +764,7 @@ class Article extends Page {
* TODO: actions other than 'view'
*/
public function getRobotPolicy( $action, $pOutput ) {
- global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies;
- global $wgDefaultRobotPolicy, $wgRequest;
+ global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy;
$ns = $this->getTitle()->getNamespace();
@@ -745,13 +786,13 @@ class Article extends Page {
'index' => 'noindex',
'follow' => 'nofollow'
);
- } elseif ( $wgOut->isPrintable() ) {
+ } elseif ( $this->getContext()->getOutput()->isPrintable() ) {
# Discourage indexing of printable versions, but encourage following
return array(
'index' => 'noindex',
'follow' => 'follow'
);
- } elseif ( $wgRequest->getInt( 'curid' ) ) {
+ } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) {
# For ?curid=x urls, disallow indexing
return array(
'index' => 'noindex',
@@ -794,7 +835,7 @@ class Article extends Page {
* merging of several policies using array_merge().
* @param $policy Mixed, returns empty array on null/false/'', transparent
* to already-converted arrays, converts String.
- * @return Array: 'index' => <indexpolicy>, 'follow' => <followpolicy>
+ * @return Array: 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\>
*/
public static function formatRobotPolicy( $policy ) {
if ( is_array( $policy ) ) {
@@ -820,15 +861,16 @@ class Article extends Page {
/**
* If this request is a redirect view, send "redirected from" subtitle to
- * $wgOut. Returns true if the header was needed, false if this is not a
- * redirect view. Handles both local and remote redirects.
+ * the output. Returns true if the header was needed, false if this is not
+ * a redirect view. Handles both local and remote redirects.
*
* @return boolean
*/
public function showRedirectedFromHeader() {
- global $wgOut, $wgRequest, $wgRedirectSources;
+ global $wgRedirectSources;
+ $outputPage = $this->getContext()->getOutput();
- $rdfrom = $wgRequest->getVal( 'rdfrom' );
+ $rdfrom = $this->getContext()->getRequest()->getVal( 'rdfrom' );
if ( isset( $this->mRedirectedFrom ) ) {
// This is an internally redirected page view.
@@ -841,21 +883,21 @@ class Article extends Page {
array( 'redirect' => 'no' )
);
- $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
+ $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
// Set the fragment if one was specified in the redirect
if ( strval( $this->getTitle()->getFragment() ) != '' ) {
$fragment = Xml::escapeJsString( $this->getTitle()->getFragmentForURL() );
- $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" );
+ $outputPage->addInlineScript( "redirectToFragment(\"$fragment\");" );
}
// Add a <link rel="canonical"> tag
- $wgOut->addLink( array( 'rel' => 'canonical',
+ $outputPage->addLink( array( 'rel' => 'canonical',
'href' => $this->getTitle()->getLocalURL() )
);
- // Tell $wgOut the user arrived at this article through a redirect
- $wgOut->setRedirectedFrom( $this->mRedirectedFrom );
+ // Tell the output object that the user arrived at this article through a redirect
+ $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
return true;
}
@@ -864,7 +906,7 @@ class Article extends Page {
// If it was reported from a trusted site, supply a backlink.
if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
$redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
- $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
+ $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
return true;
}
@@ -878,11 +920,9 @@ class Article extends Page {
* [[MediaWiki:Talkpagetext]]. For Article::view().
*/
public function showNamespaceHeader() {
- global $wgOut;
-
if ( $this->getTitle()->isTalkPage() ) {
if ( !wfMessage( 'talkpageheader' )->isDisabled() ) {
- $wgOut->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) );
+ $this->getContext()->getOutput()->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) );
}
}
}
@@ -891,11 +931,9 @@ class Article extends Page {
* Show the footer section of an ordinary page view
*/
public function showViewFooter() {
- global $wgOut;
-
# check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
if ( $this->getTitle()->getNamespace() == NS_USER_TALK && IP::isValid( $this->getTitle()->getText() ) ) {
- $wgOut->addWikiMsg( 'anontalkpagetext' );
+ $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
}
# If we have been passed an &rcid= parameter, we want to give the user a
@@ -912,33 +950,32 @@ class Article extends Page {
* desired, does nothing.
*/
public function showPatrolFooter() {
- global $wgOut, $wgRequest, $wgUser;
-
- $rcid = $wgRequest->getVal( 'rcid' );
+ $request = $this->getContext()->getRequest();
+ $outputPage = $this->getContext()->getOutput();
+ $user = $this->getContext()->getUser();
+ $rcid = $request->getVal( 'rcid' );
- if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol' ) ) {
+ if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol', $user ) ) {
return;
}
- $token = $wgUser->getEditToken( $rcid );
- $wgOut->preventClickjacking();
+ $token = $user->getEditToken( $rcid );
+ $outputPage->preventClickjacking();
- $wgOut->addHTML(
+ $link = Linker::linkKnown(
+ $this->getTitle(),
+ wfMessage( 'markaspatrolledtext' )->escaped(),
+ array(),
+ array(
+ 'action' => 'markpatrolled',
+ 'rcid' => $rcid,
+ 'token' => $token,
+ )
+ );
+
+ $outputPage->addHTML(
"<div class='patrollink'>" .
- wfMsgHtml(
- 'markaspatrolledlink',
- Linker::link(
- $this->getTitle(),
- wfMsgHtml( 'markaspatrolledtext' ),
- array(),
- array(
- 'action' => 'markpatrolled',
- 'rcid' => $rcid,
- 'token' => $token,
- ),
- array( 'known', 'noclasses' )
- )
- ) .
+ wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
'</div>'
);
}
@@ -948,7 +985,8 @@ class Article extends Page {
* namespace, show the default message text. To be called from Article::view().
*/
public function showMissingArticle() {
- global $wgOut, $wgRequest, $wgUser, $wgSend404Code;
+ global $wgSend404Code;
+ $outputPage = $this->getContext()->getOutput();
# 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 ) {
@@ -958,13 +996,13 @@ class Article extends Page {
$ip = User::isIP( $rootPart );
if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist
- $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
+ $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
LogEventsList::showLogExtract(
- $wgOut,
+ $outputPage,
'block',
- $user->getUserPage()->getPrefixedText(),
+ $user->getUserPage(),
'',
array(
'lim' => 1,
@@ -981,7 +1019,7 @@ class Article extends Page {
wfRunHooks( 'ShowMissingArticle', array( $this ) );
# Show delete and move logs
- LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->getTitle()->getPrefixedText(), '',
+ LogEventsList::showLogExtract( $outputPage, array( 'delete', 'move' ), $this->getTitle(), '',
array( 'lim' => 10,
'conds' => array( "log_action != 'revision'" ),
'showIfEmpty' => false,
@@ -991,7 +1029,7 @@ class Article extends Page {
if ( !$this->mPage->hasViewableContent() && $wgSend404Code ) {
// If there's no backing content, send a 404 Not Found
// for better machine handling of broken links.
- $wgRequest->response()->header( "HTTP/1.1 404 Not Found" );
+ $this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" );
}
$hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) );
@@ -1003,56 +1041,50 @@ class Article extends Page {
# Show error message
$oldid = $this->getOldID();
if ( $oldid ) {
- $text = wfMsgNoTrans( 'missing-article',
- $this->getTitle()->getPrefixedText(),
- wfMsgNoTrans( 'missingarticle-rev', $oldid ) );
+ $text = wfMessage( 'missing-revision', $oldid )->plain();
} elseif ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) {
// Use the default message text
$text = $this->getTitle()->getDefaultMessageText();
+ } elseif ( $this->getTitle()->quickUserCan( 'create', $this->getContext()->getUser() )
+ && $this->getTitle()->quickUserCan( 'edit', $this->getContext()->getUser() )
+ ) {
+ $text = wfMessage( 'noarticletext' )->plain();
} else {
- $createErrors = $this->getTitle()->getUserPermissionsErrors( 'create', $wgUser );
- $editErrors = $this->getTitle()->getUserPermissionsErrors( 'edit', $wgUser );
- $errors = array_merge( $createErrors, $editErrors );
-
- if ( !count( $errors ) ) {
- $text = wfMsgNoTrans( 'noarticletext' );
- } else {
- $text = wfMsgNoTrans( 'noarticletext-nopermission' );
- }
+ $text = wfMessage( 'noarticletext-nopermission' )->plain();
}
$text = "<div class='noarticletext'>\n$text\n</div>";
- $wgOut->addWikiText( $text );
+ $outputPage->addWikiText( $text );
}
/**
* If the revision requested for view is deleted, check permissions.
- * Send either an error message or a warning header to $wgOut.
+ * Send either an error message or a warning header to the output.
*
* @return boolean true if the view is allowed, false if not.
*/
public function showDeletedRevisionHeader() {
- global $wgOut, $wgRequest;
-
if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
// Not deleted
return true;
}
+ $outputPage = $this->getContext()->getOutput();
+ $user = $this->getContext()->getUser();
// If the user is not allowed to see it...
- if ( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
+ if ( !$this->mRevision->userCan( Revision::DELETED_TEXT, $user ) ) {
+ $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
'rev-deleted-text-permission' );
return false;
// If the user needs to confirm that they want to see it...
- } elseif ( $wgRequest->getInt( 'unhide' ) != 1 ) {
+ } 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" );
$msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
+ $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
array( $msg, $link ) );
return false;
@@ -1060,7 +1092,7 @@ class Article extends Page {
} else {
$msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
'rev-suppressed-text-view' : 'rev-deleted-text-view';
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg );
+ $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg );
return true;
}
@@ -1072,30 +1104,36 @@ class Article extends Page {
* Revision as of \<date\>; view current revision
* \<- Previous version | Next Version -\>
*
- * @param $oldid String: revision ID of this article revision
+ * @param $oldid int: revision ID of this article revision
*/
public function setOldSubtitle( $oldid = 0 ) {
- global $wgLang, $wgOut, $wgUser, $wgRequest;
-
if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
return;
}
- $unhide = $wgRequest->getInt( 'unhide' ) == 1;
+ $unhide = $this->getContext()->getRequest()->getInt( 'unhide' ) == 1;
# Cascade unhide param in links for easy deletion browsing
$extraParams = array();
- if ( $wgRequest->getVal( 'unhide' ) ) {
+ if ( $unhide ) {
$extraParams['unhide'] = 1;
}
- $revision = Revision::newFromId( $oldid );
+ if ( $this->mRevision && $this->mRevision->getId() === $oldid ) {
+ $revision = $this->mRevision;
+ } else {
+ $revision = Revision::newFromId( $oldid );
+ }
+
$timestamp = $revision->getTimestamp();
$current = ( $oldid == $this->mPage->getLatest() );
- $td = $wgLang->timeanddate( $timestamp, true );
- $tddate = $wgLang->date( $timestamp, true );
- $tdtime = $wgLang->time( $timestamp, true );
+ $language = $this->getContext()->getLanguage();
+ $user = $this->getContext()->getUser();
+
+ $td = $language->userTimeAndDate( $timestamp, $user );
+ $tddate = $language->userDate( $timestamp, $user );
+ $tdtime = $language->userTime( $timestamp, $user );
# Show user links if allowed to see them. If hidden, then show them only if requested...
$userlinks = Linker::revUserTools( $revision, !$unhide );
@@ -1104,89 +1142,85 @@ class Article extends Page {
? 'revision-info-current'
: 'revision-info';
- $wgOut->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg,
+ $outputPage = $this->getContext()->getOutput();
+ $outputPage->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg,
$td )->rawParams( $userlinks )->params( $revision->getID(), $tddate,
$tdtime, $revision->getUser() )->parse() . "</div>" );
$lnk = $current
- ? wfMsgHtml( 'currentrevisionlink' )
- : Linker::link(
+ ? wfMessage( 'currentrevisionlink' )->escaped()
+ : Linker::linkKnown(
$this->getTitle(),
- wfMsgHtml( 'currentrevisionlink' ),
+ wfMessage( 'currentrevisionlink' )->escaped(),
array(),
- $extraParams,
- array( 'known', 'noclasses' )
+ $extraParams
);
$curdiff = $current
- ? wfMsgHtml( 'diff' )
- : Linker::link(
+ ? wfMessage( 'diff' )->escaped()
+ : Linker::linkKnown(
$this->getTitle(),
- wfMsgHtml( 'diff' ),
+ wfMessage( 'diff' )->escaped(),
array(),
array(
'diff' => 'cur',
'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
+ ) + $extraParams
);
$prev = $this->getTitle()->getPreviousRevisionID( $oldid ) ;
$prevlink = $prev
- ? Linker::link(
+ ? Linker::linkKnown(
$this->getTitle(),
- wfMsgHtml( 'previousrevision' ),
+ wfMessage( 'previousrevision' )->escaped(),
array(),
array(
'direction' => 'prev',
'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
+ ) + $extraParams
)
- : wfMsgHtml( 'previousrevision' );
+ : wfMessage( 'previousrevision' )->escaped();
$prevdiff = $prev
- ? Linker::link(
+ ? Linker::linkKnown(
$this->getTitle(),
- wfMsgHtml( 'diff' ),
+ wfMessage( 'diff' )->escaped(),
array(),
array(
'diff' => 'prev',
'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
+ ) + $extraParams
)
- : wfMsgHtml( 'diff' );
+ : wfMessage( 'diff' )->escaped();
$nextlink = $current
- ? wfMsgHtml( 'nextrevision' )
- : Linker::link(
+ ? wfMessage( 'nextrevision' )->escaped()
+ : Linker::linkKnown(
$this->getTitle(),
- wfMsgHtml( 'nextrevision' ),
+ wfMessage( 'nextrevision' )->escaped(),
array(),
array(
'direction' => 'next',
'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
+ ) + $extraParams
);
$nextdiff = $current
- ? wfMsgHtml( 'diff' )
- : Linker::link(
+ ? wfMessage( 'diff' )->escaped()
+ : Linker::linkKnown(
$this->getTitle(),
- wfMsgHtml( 'diff' ),
+ wfMessage( 'diff' )->escaped(),
array(),
array(
'diff' => 'next',
'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
+ ) + $extraParams
);
- $cdel = Linker::getRevDeleteLink( $wgUser, $revision, $this->getTitle() );
+ $cdel = Linker::getRevDeleteLink( $user, $revision, $this->getTitle() );
if ( $cdel !== '' ) {
$cdel .= ' ';
}
- $wgOut->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel .
- wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ),
- $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>" );
+ $outputPage->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel .
+ wfMessage( 'revision-nav' )->rawParams(
+ $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
+ )->escaped() . "</div>" );
}
/**
@@ -1198,7 +1232,7 @@ class Article extends Page {
* @return string containing HMTL with redirect link
*/
public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
- global $wgOut, $wgStylePath;
+ global $wgStylePath;
if ( !is_array( $target ) ) {
$target = array( $target );
@@ -1208,7 +1242,8 @@ class Article extends Page {
$imageDir = $lang->getDir();
if ( $appendSubtitle ) {
- $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) );
+ $out = $this->getContext()->getOutput();
+ $out->addSubtitle( wfMessage( 'redirectpagesub' )->escaped() );
}
// the loop prepends the arrow image before the link, so the first case needs to be outside
@@ -1246,9 +1281,7 @@ class Article extends Page {
* Handle action=render
*/
public function render() {
- global $wgOut;
-
- $wgOut->setArticleBodyOnly( true );
+ $this->getContext()->getOutput()->setArticleBodyOnly( true );
$this->view();
}
@@ -1271,8 +1304,6 @@ class Article extends Page {
* UI entry point for page deletion
*/
public function delete() {
- global $wgOut, $wgRequest, $wgLang;
-
# This code desperately needs to be totally rewritten
$title = $this->getTitle();
@@ -1290,48 +1321,54 @@ class Article extends Page {
}
# Better double-check that it hasn't been deleted yet!
- $dbw = wfGetDB( DB_MASTER );
- $conds = $title->pageCond();
- $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
- if ( $latest === false ) {
- $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) );
- $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
+ $this->mPage->loadPageData( 'fromdbmaster' );
+ if ( !$this->mPage->exists() ) {
+ $deleteLogPage = new LogPage( 'delete' );
+ $outputPage = $this->getContext()->getOutput();
+ $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) );
+ $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
array( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) )
);
- $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+ $outputPage->addHTML(
+ Xml::element( 'h2', null, $deleteLogPage->getName()->text() )
+ );
LogEventsList::showLogExtract(
- $wgOut,
+ $outputPage,
'delete',
- $title->getPrefixedText()
+ $title
);
return;
}
- $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
- $deleteReason = $wgRequest->getText( 'wpReason' );
+ $request = $this->getContext()->getRequest();
+ $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' );
+ $deleteReason = $request->getText( 'wpReason' );
if ( $deleteReasonList == 'other' ) {
$reason = $deleteReason;
} elseif ( $deleteReason != '' ) {
// Entry from drop down menu + additional comment
- $reason = $deleteReasonList . wfMsgForContent( 'colon-separator' ) . $deleteReason;
+ $colonseparator = wfMessage( 'colon-separator' )->inContentLanguage()->text();
+ $reason = $deleteReasonList . $colonseparator . $deleteReason;
} else {
$reason = $deleteReasonList;
}
- if ( $wgRequest->wasPosted() && $user->matchEditToken( $wgRequest->getVal( 'wpEditToken' ),
+ if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ),
array( 'delete', $this->getTitle()->getPrefixedText() ) ) )
{
# Flag to hide all contents of the archived revisions
- $suppress = $wgRequest->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' );
+ $suppress = $request->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' );
$this->doDelete( $reason, $suppress );
- if ( $wgRequest->getCheck( 'wpWatch' ) && $user->isLoggedIn() ) {
- $this->doWatch();
- } elseif ( $title->userIsWatching() ) {
- $this->doUnwatch();
+ if ( $user->isLoggedIn() && $request->getCheck( 'wpWatch' ) != $user->isWatched( $title ) ) {
+ if ( $request->getCheck( 'wpWatch' ) ) {
+ WatchAction::doWatch( $title, $user );
+ } else {
+ WatchAction::doUnwatch( $title, $user );
+ }
}
return;
@@ -1347,10 +1384,10 @@ class Article extends Page {
if ( $hasHistory ) {
$revisions = $this->mTitle->estimateRevisionCount();
// @todo FIXME: i18n issue/patchwork message
- $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' .
- wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) .
- wfMsgHtml( 'word-separator' ) . Linker::link( $title,
- wfMsgHtml( 'history' ),
+ $this->getContext()->getOutput()->addHTML( '<strong class="mw-delete-warning-revisions">' .
+ wfMessage( 'historywarning' )->numParams( $revisions )->parse() .
+ wfMessage( 'word-separator' )->plain() . Linker::linkKnown( $title,
+ wfMessage( 'history' )->escaped(),
array( 'rel' => 'archives' ),
array( 'action' => 'history' ) ) .
'</strong>'
@@ -1358,12 +1395,12 @@ class Article extends Page {
if ( $this->mTitle->isBigDeletion() ) {
global $wgDeleteRevisionsLimit;
- $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
- array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+ $this->getContext()->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
+ array( 'delete-warning-toobig', $this->getContext()->getLanguage()->formatNum( $wgDeleteRevisionsLimit ) ) );
}
}
- return $this->confirmDelete( $reason );
+ $this->confirmDelete( $reason );
}
/**
@@ -1372,16 +1409,15 @@ class Article extends Page {
* @param $reason String: prefilled reason
*/
public function confirmDelete( $reason ) {
- global $wgOut;
-
wfDebug( "Article::confirmDelete\n" );
- $wgOut->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) );
- $wgOut->addBacklinkSubtitle( $this->getTitle() );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->addWikiMsg( 'confirmdeletetext' );
+ $outputPage = $this->getContext()->getOutput();
+ $outputPage->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) );
+ $outputPage->addBacklinkSubtitle( $this->getTitle() );
+ $outputPage->setRobotPolicy( 'noindex,nofollow' );
+ $outputPage->addWikiMsg( 'confirmdeletetext' );
- wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) );
+ wfRunHooks( 'ArticleConfirmDelete', array( $this, $outputPage, &$reason ) );
$user = $this->getContext()->getUser();
@@ -1389,33 +1425,33 @@ class Article extends Page {
$suppress = "<tr id=\"wpDeleteSuppressRow\">
<td></td>
<td class='mw-input'><strong>" .
- Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
+ Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(),
'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
"</strong></td>
</tr>";
} else {
$suppress = '';
}
- $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching();
+ $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $user->isWatched( $this->getTitle() );
$form = Xml::openElement( 'form', array( 'method' => 'post',
'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) .
Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) .
- Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) .
+ Xml::tags( 'legend', null, wfMessage( 'delete-legend' )->escaped() ) .
Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) .
"<tr id=\"wpDeleteReasonListRow\">
<td class='mw-label'>" .
- Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) .
+ Xml::label( wfMessage( 'deletecomment' )->text(), 'wpDeleteReasonList' ) .
"</td>
<td class='mw-input'>" .
Xml::listDropDown( 'wpDeleteReasonList',
- wfMsgForContent( 'deletereason-dropdown' ),
- wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
+ wfMessage( 'deletereason-dropdown' )->inContentLanguage()->text(),
+ wfMessage( 'deletereasonotherlist' )->inContentLanguage()->text(), '', 'wpReasonDropDown', 1 ) .
"</td>
</tr>
<tr id=\"wpDeleteReasonRow\">
<td class='mw-label'>" .
- Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
+ Xml::label( wfMessage( 'deleteotherreason' )->text(), 'wpReason' ) .
"</td>
<td class='mw-input'>" .
Html::input( 'wpReason', $reason, 'text', array(
@@ -1434,7 +1470,7 @@ class Article extends Page {
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'watchthis' ),
+ Xml::checkLabel( wfMessage( 'watchthis' )->text(),
'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
"</td>
</tr>";
@@ -1445,7 +1481,7 @@ class Article extends Page {
<tr>
<td></td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'deletepage' ),
+ Xml::submitButton( wfMessage( 'deletepage' )->text(),
array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) .
"</td>
</tr>" .
@@ -1458,17 +1494,19 @@ class Article extends Page {
$title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' );
$link = Linker::link(
$title,
- wfMsgHtml( 'delete-edit-reasonlist' ),
+ wfMessage( 'delete-edit-reasonlist' )->escaped(),
array(),
array( 'action' => 'edit' )
);
$form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
}
- $wgOut->addHTML( $form );
- $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
- LogEventsList::showLogExtract( $wgOut, 'delete',
- $this->getTitle()->getPrefixedText()
+ $outputPage->addHTML( $form );
+
+ $deleteLogPage = new LogPage( 'delete' );
+ $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
+ LogEventsList::showLogExtract( $outputPage, 'delete',
+ $this->getTitle()
);
}
@@ -1478,34 +1516,35 @@ class Article extends Page {
* @param $suppress bool
*/
public function doDelete( $reason, $suppress = false ) {
- global $wgOut;
-
$error = '';
- if ( $this->mPage->doDeleteArticle( $reason, $suppress, 0, true, $error ) ) {
+ $outputPage = $this->getContext()->getOutput();
+ $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error );
+ if ( $status->isGood() ) {
$deleted = $this->getTitle()->getPrefixedText();
- $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
+ $outputPage->setRobotPolicy( 'noindex,nofollow' );
- $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
+ $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
- $wgOut->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
- $wgOut->returnToMain( false );
+ $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
+ $outputPage->returnToMain( false );
} else {
- $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) );
+ $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) );
if ( $error == '' ) {
- $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
- array( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) )
+ $outputPage->addWikiText(
+ "<div class=\"error mw-error-cannotdelete\">\n" . $status->getWikiText() . "\n</div>"
);
- $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+ $deleteLogPage = new LogPage( 'delete' );
+ $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
LogEventsList::showLogExtract(
- $wgOut,
+ $outputPage,
'delete',
- $this->getTitle()->getPrefixedText()
+ $this->getTitle()
);
} else {
- $wgOut->addHTML( $error );
+ $outputPage->addHTML( $error );
}
}
}
@@ -1578,22 +1617,22 @@ class Article extends Page {
* @return ParserOutput or false if the given revsion ID is not found
*/
public function getParserOutput( $oldid = null, User $user = null ) {
- global $wgUser;
-
- $user = is_null( $user ) ? $wgUser : $user;
- $parserOptions = $this->mPage->makeParserOptions( $user );
+ if ( $user === null ) {
+ $parserOptions = $this->getParserOptions();
+ } else {
+ $parserOptions = $this->mPage->makeParserOptions( $user );
+ }
return $this->mPage->getParserOutput( $parserOptions, $oldid );
}
/**
* Get parser options suitable for rendering the primary article wikitext
- * @return ParserOptions|false
+ * @return ParserOptions
*/
public function getParserOptions() {
- global $wgUser;
if ( !$this->mParserOptions ) {
- $this->mParserOptions = $this->mPage->makeParserOptions( $wgUser );
+ $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext() );
}
// Clone to allow modifications of the return value without affecting cache
return clone $this->mParserOptions;
@@ -1645,6 +1684,7 @@ class Article extends Page {
/**
* Handle action=purge
* @deprecated since 1.19
+ * @return Action|bool|null false if the action is disabled, null if it is not recognised
*/
public function purge() {
return Action::factory( 'purge', $this )->show();
@@ -1679,7 +1719,7 @@ class Article extends Page {
}
/**
- * Add this page to $wgUser's watchlist
+ * Add this page to the current user's watchlist
*
* This is safe to be called multiple times
*
@@ -1687,9 +1727,8 @@ class Article extends Page {
* @deprecated since 1.18
*/
public function doWatch() {
- global $wgUser;
wfDeprecated( __METHOD__, '1.18' );
- return WatchAction::doWatch( $this->getTitle(), $wgUser );
+ return WatchAction::doWatch( $this->getTitle(), $this->getContext()->getUser() );
}
/**
@@ -1708,24 +1747,21 @@ class Article extends Page {
* @deprecated since 1.18
*/
public function doUnwatch() {
- global $wgUser;
wfDeprecated( __METHOD__, '1.18' );
- return WatchAction::doUnwatch( $this->getTitle(), $wgUser );
+ return WatchAction::doUnwatch( $this->getTitle(), $this->getContext()->getUser() );
}
/**
* Output a redirect back to the article.
* This is typically used after an edit.
*
- * @deprecated in 1.18; call $wgOut->redirect() directly
+ * @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
*/
public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
wfDeprecated( __METHOD__, '1.18' );
- global $wgOut;
-
if ( $noRedir ) {
$query = 'redirect=no';
if ( $extraQuery )
@@ -1734,7 +1770,7 @@ class Article extends Page {
$query = $extraQuery;
}
- $wgOut->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor );
+ $this->getContext()->getOutput()->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor );
}
/**
@@ -1776,6 +1812,7 @@ class Article extends Page {
*
* @param $fname String Name of called method
* @param $args Array Arguments to the method
+ * @return mixed
*/
public function __call( $fname, $args ) {
if ( is_callable( array( $this->mPage, $fname ) ) ) {
@@ -1832,8 +1869,7 @@ class Article extends Page {
* @return array
*/
public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) {
- global $wgUser;
- $user = is_null( $user ) ? $wgUser : $user;
+ $user = is_null( $user ) ? $this->getContext()->getUser() : $user;
return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user );
}
@@ -1846,8 +1882,7 @@ class Article extends Page {
* @return array
*/
public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) {
- global $wgUser;
- $guser = is_null( $guser ) ? $wgUser : $guser;
+ $guser = is_null( $guser ) ? $this->getContext()->getUser() : $guser;
return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser );
}
diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php
index e8bab859..2e42439c 100644
--- a/includes/AuthPlugin.php
+++ b/includes/AuthPlugin.php
@@ -34,6 +34,12 @@
* someone logs in who can be authenticated externally.
*/
class AuthPlugin {
+
+ /**
+ * @var string
+ */
+ protected $domain;
+
/**
* Check whether there exists a user account with the given name.
* The name will be normalized to MediaWiki's requirements, so
@@ -84,6 +90,19 @@ class AuthPlugin {
}
/**
+ * Get the user's domain
+ *
+ * @return string
+ */
+ public function getDomain() {
+ if ( isset( $this->domain ) ) {
+ return $this->domain;
+ } else {
+ return 'invaliddomain';
+ }
+ }
+
+ /**
* Check to see if the specific domain is a valid domain.
*
* @param $domain String: authentication domain.
@@ -103,6 +122,7 @@ class AuthPlugin {
* forget the & on your function declaration.
*
* @param $user User object
+ * @return bool
*/
public function updateUser( &$user ) {
# Override this and do something
@@ -256,6 +276,8 @@ class AuthPlugin {
/**
* If you want to munge the case of an account name before the final
* check, now is your chance.
+ * @param $username string
+ * @return string
*/
public function getCanonicalName( $username ) {
return $username;
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 93fac45f..d3a2c548 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -2,6 +2,21 @@
/**
* This defines autoloading handler for whole MediaWiki framework
*
+ * 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
*/
@@ -27,6 +42,7 @@ $wgAutoloadLocalClasses = array(
'BadTitleError' => 'includes/Exception.php',
'BaseTemplate' => 'includes/SkinTemplate.php',
'Block' => 'includes/Block.php',
+ 'CacheHelper' => 'includes/CacheHelper.php',
'Category' => 'includes/Category.php',
'Categoryfinder' => 'includes/Categoryfinder.php',
'CategoryPage' => 'includes/CategoryPage.php',
@@ -53,9 +69,11 @@ $wgAutoloadLocalClasses = array(
'CurlHttpRequest' => 'includes/HttpFunctions.php',
'DeferrableUpdate' => 'includes/DeferredUpdates.php',
'DeferredUpdates' => 'includes/DeferredUpdates.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',
'Dump7ZipOutput' => 'includes/Export.php',
@@ -92,6 +110,7 @@ $wgAutoloadLocalClasses = array(
'FormAction' => 'includes/Action.php',
'FormOptions' => 'includes/FormOptions.php',
'FormSpecialPage' => 'includes/SpecialPage.php',
+ 'GitInfo' => 'includes/GitInfo.php',
'HashtableReplacer' => 'includes/StringUtils.php',
'HistoryBlob' => 'includes/HistoryBlob.php',
'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
@@ -117,7 +136,10 @@ $wgAutoloadLocalClasses = array(
'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',
@@ -140,6 +162,7 @@ $wgAutoloadLocalClasses = array(
'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',
'MagicWord' => 'includes/MagicWord.php',
@@ -153,6 +176,7 @@ $wgAutoloadLocalClasses = array(
'MWException' => 'includes/Exception.php',
'MWExceptionHandler' => 'includes/Exception.php',
'MWFunction' => 'includes/MWFunction.php',
+ 'MWHookException' => 'includes/Hooks.php',
'MWHttpRequest' => 'includes/HttpFunctions.php',
'MWInit' => 'includes/Init.php',
'MWNamespace' => 'includes/Namespace.php',
@@ -185,12 +209,16 @@ $wgAutoloadLocalClasses = array(
'ReplacementArray' => 'includes/StringUtils.php',
'Replacer' => 'includes/StringUtils.php',
'ReverseChronologicalPager' => 'includes/Pager.php',
+ 'RevisionItem' => 'includes/RevisionList.php',
'RevisionItemBase' => 'includes/RevisionList.php',
'RevisionListBase' => 'includes/RevisionList.php',
'Revision' => 'includes/Revision.php',
'RevisionList' => 'includes/RevisionList.php',
'RSSFeed' => 'includes/Feed.php',
'Sanitizer' => 'includes/Sanitizer.php',
+ 'DataUpdate' => 'includes/DataUpdate.php',
+ 'SqlDataUpdate' => 'includes/SqlDataUpdate.php',
+ 'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php',
'SiteConfiguration' => 'includes/SiteConfiguration.php',
'SiteStats' => 'includes/SiteStats.php',
'SiteStatsInit' => 'includes/SiteStats.php',
@@ -217,16 +245,20 @@ $wgAutoloadLocalClasses = array(
'StubObject' => 'includes/StubObject.php',
'StubUserLang' => 'includes/StubObject.php',
'TablePager' => 'includes/Pager.php',
+ 'MWTimestamp' => 'includes/Timestamp.php',
'Title' => 'includes/Title.php',
'TitleArray' => 'includes/TitleArray.php',
'TitleArrayFromResult' => 'includes/TitleArray.php',
'ThrottledError' => 'includes/Exception.php',
'UnlistedSpecialPage' => 'includes/SpecialPage.php',
+ 'UploadSourceAdapter' => 'includes/Import.php',
'UppercaseCollation' => 'includes/Collation.php',
'User' => 'includes/User.php',
'UserArray' => 'includes/UserArray.php',
'UserArrayFromResult' => 'includes/UserArray.php',
'UserBlockedError' => 'includes/Exception.php',
+ 'UserNotLoggedIn' => 'includes/Exception.php',
+ 'UserCache' => 'includes/cache/UserCache.php',
'UserMailer' => 'includes/UserMailer.php',
'UserRightsProxy' => 'includes/UserRightsProxy.php',
'ViewCountUpdate' => 'includes/ViewCountUpdate.php',
@@ -249,12 +281,15 @@ $wgAutoloadLocalClasses = array(
'Xml' => 'includes/Xml.php',
'XmlDumpWriter' => 'includes/Export.php',
'XmlJsCode' => 'includes/Xml.php',
+ 'XMLReader2' => 'includes/Import.php',
'XmlSelect' => 'includes/Xml.php',
'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
'ZhClient' => 'includes/ZhClient.php',
'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
+ 'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php',
# includes/actions
+ 'CachedAction' => 'includes/actions/CachedAction.php',
'CreditsAction' => 'includes/actions/CreditsAction.php',
'DeleteAction' => 'includes/actions/DeleteAction.php',
'EditAction' => 'includes/actions/EditAction.php',
@@ -310,6 +345,7 @@ $wgAutoloadLocalClasses = array(
'ApiMain' => 'includes/api/ApiMain.php',
'ApiMove' => 'includes/api/ApiMove.php',
'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
+ 'ApiOptions' => 'includes/api/ApiOptions.php',
'ApiPageSet' => 'includes/api/ApiPageSet.php',
'ApiParamInfo' => 'includes/api/ApiParamInfo.php',
'ApiParse' => 'includes/api/ApiParse.php',
@@ -318,10 +354,10 @@ $wgAutoloadLocalClasses = array(
'ApiPurge' => 'includes/api/ApiPurge.php',
'ApiQuery' => 'includes/api/ApiQuery.php',
'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
- 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php',
+ 'ApiQueryAllImages' => 'includes/api/ApiQueryAllImages.php',
'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
- 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php',
- 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
+ 'ApiQueryAllMessages' => 'includes/api/ApiQueryAllMessages.php',
+ 'ApiQueryAllPages' => 'includes/api/ApiQueryAllPages.php',
'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php',
'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
@@ -363,11 +399,14 @@ $wgAutoloadLocalClasses = array(
'ApiResult' => 'includes/api/ApiResult.php',
'ApiRollback' => 'includes/api/ApiRollback.php',
'ApiRsd' => 'includes/api/ApiRsd.php',
+ 'ApiSetNotificationTimestamp' => 'includes/api/ApiSetNotificationTimestamp.php',
+ 'ApiTokens' => 'includes/api/ApiTokens.php',
'ApiUnblock' => 'includes/api/ApiUnblock.php',
'ApiUndelete' => 'includes/api/ApiUndelete.php',
'ApiUpload' => 'includes/api/ApiUpload.php',
'ApiUserrights' => 'includes/api/ApiUserrights.php',
'ApiWatch' => 'includes/api/ApiWatch.php',
+ 'UsageException' => 'includes/api/ApiMain.php',
# includes/cache
'CacheDependency' => 'includes/cache/CacheDependency.php',
@@ -384,19 +423,21 @@ $wgAutoloadLocalClasses = array(
'LinkCache' => 'includes/cache/LinkCache.php',
'MessageCache' => 'includes/cache/MessageCache.php',
'ObjectFileCache' => 'includes/cache/ObjectFileCache.php',
+ 'ProcessCacheLRU' => 'includes/cache/ProcessCacheLRU.php',
'ResourceFileCache' => 'includes/cache/ResourceFileCache.php',
'SquidUpdate' => 'includes/cache/SquidUpdate.php',
'TitleDependency' => 'includes/cache/CacheDependency.php',
'TitleListDependency' => 'includes/cache/CacheDependency.php',
- 'UsageException' => 'includes/api/ApiMain.php',
-
# includes/context
'ContextSource' => 'includes/context/ContextSource.php',
'DerivativeContext' => 'includes/context/DerivativeContext.php',
'IContextSource' => 'includes/context/IContextSource.php',
'RequestContext' => 'includes/context/RequestContext.php',
+ # includes/dao
+ 'IDBAccessObject' => 'includes/dao/IDBAccessObject.php',
+
# includes/db
'Blob' => 'includes/db/DatabaseUtility.php',
'ChronologyProtector' => 'includes/db/LBFactory.php',
@@ -411,9 +452,12 @@ $wgAutoloadLocalClasses = array(
'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
'DatabaseSqliteStandalone' => 'includes/db/DatabaseSqlite.php',
'DatabaseType' => 'includes/db/Database.php',
+ 'DBAccessError' => 'includes/db/LBFactory.php',
'DBConnectionError' => 'includes/db/DatabaseError.php',
'DBError' => 'includes/db/DatabaseError.php',
'DBObject' => 'includes/db/DatabaseUtility.php',
+ 'IORMRow' => 'includes/db/IORMRow.php',
+ 'IORMTable' => 'includes/db/IORMTable.php',
'DBMasterPos' => 'includes/db/DatabaseUtility.php',
'DBQueryError' => 'includes/db/DatabaseError.php',
'DBUnexpectedError' => 'includes/db/DatabaseError.php',
@@ -421,7 +465,10 @@ $wgAutoloadLocalClasses = array(
'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',
'LBFactory_Simple' => 'includes/db/LBFactory.php',
'LBFactory_Single' => 'includes/db/LBFactory_Single.php',
@@ -431,12 +478,20 @@ $wgAutoloadLocalClasses = array(
'LoadMonitor' => 'includes/db/LoadMonitor.php',
'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php',
'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',
'ORAField' => 'includes/db/DatabaseOracle.php',
'ORAResult' => 'includes/db/DatabaseOracle.php',
+ 'ORMIterator' => 'includes/db/ORMIterator.php',
+ 'ORMResult' => 'includes/db/ORMResult.php',
+ 'ORMRow' => 'includes/db/ORMRow.php',
+ 'ORMTable' => 'includes/db/ORMTable.php',
'PostgresField' => 'includes/db/DatabasePostgres.php',
+ 'PostgresTransactionState' => 'includes/db/DatabasePostgres.php',
'ResultWrapper' => 'includes/db/DatabaseUtility.php',
+ 'SavepointPostgres' => 'includes/db/DatabasePostgres.php',
'SQLiteField' => 'includes/db/DatabaseSqlite.php',
# includes/debug
@@ -466,6 +521,50 @@ $wgAutoloadLocalClasses = array(
'ExternalUser_MediaWiki' => 'includes/extauth/MediaWiki.php',
'ExternalUser_vB' => 'includes/extauth/vB.php',
+ # includes/filebackend
+ 'FileBackendGroup' => 'includes/filebackend/FileBackendGroup.php',
+ 'FileBackend' => 'includes/filebackend/FileBackend.php',
+ 'FileBackendStore' => 'includes/filebackend/FileBackendStore.php',
+ 'FileBackendStoreShardListIterator' => 'includes/filebackend/FileBackendStore.php',
+ 'FileBackendStoreShardDirIterator' => 'includes/filebackend/FileBackendStore.php',
+ 'FileBackendStoreShardFileIterator' => 'includes/filebackend/FileBackendStore.php',
+ 'FileBackendMultiWrite' => 'includes/filebackend/FileBackendMultiWrite.php',
+ 'FileBackendStoreOpHandle' => 'includes/filebackend/FileBackendStore.php',
+ 'FSFile' => 'includes/filebackend/FSFile.php',
+ 'FSFileBackend' => 'includes/filebackend/FSFileBackend.php',
+ 'FSFileBackendList' => 'includes/filebackend/FSFileBackend.php',
+ 'FSFileBackendDirList' => 'includes/filebackend/FSFileBackend.php',
+ 'FSFileBackendFileList' => 'includes/filebackend/FSFileBackend.php',
+ 'FSFileOpHandle' => 'includes/filebackend/FSFileBackend.php',
+ 'SwiftFileBackend' => 'includes/filebackend/SwiftFileBackend.php',
+ 'SwiftFileBackendList' => 'includes/filebackend/SwiftFileBackend.php',
+ 'SwiftFileBackendDirList' => 'includes/filebackend/SwiftFileBackend.php',
+ 'SwiftFileBackendFileList' => 'includes/filebackend/SwiftFileBackend.php',
+ 'SwiftFileOpHandle' => 'includes/filebackend/SwiftFileBackend.php',
+ 'TempFSFile' => 'includes/filebackend/TempFSFile.php',
+ 'FileJournal' => 'includes/filebackend/filejournal/FileJournal.php',
+ 'DBFileJournal' => 'includes/filebackend/filejournal/DBFileJournal.php',
+ 'NullFileJournal' => 'includes/filebackend/filejournal/FileJournal.php',
+ 'LockManagerGroup' => 'includes/filebackend/lockmanager/LockManagerGroup.php',
+ 'LockManager' => 'includes/filebackend/lockmanager/LockManager.php',
+ 'ScopedLock' => 'includes/filebackend/lockmanager/LockManager.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',
+ 'NullLockManager' => 'includes/filebackend/lockmanager/LockManager.php',
+ 'FileOp' => 'includes/filebackend/FileOp.php',
+ 'FileOpBatch' => 'includes/filebackend/FileOpBatch.php',
+ 'StoreFileOp' => 'includes/filebackend/FileOp.php',
+ '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',
+ 'NullFileOp' => 'includes/filebackend/FileOp.php',
+
# includes/filerepo
'FileRepo' => 'includes/filerepo/FileRepo.php',
'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
@@ -476,6 +575,7 @@ $wgAutoloadLocalClasses = array(
'LocalRepo' => 'includes/filerepo/LocalRepo.php',
'NullRepo' => 'includes/filerepo/NullRepo.php',
'RepoGroup' => 'includes/filerepo/RepoGroup.php',
+ 'TempFileRepo' => 'includes/filerepo/FileRepo.php',
# includes/filerepo/file
'ArchivedFile' => 'includes/filerepo/file/ArchivedFile.php',
@@ -488,36 +588,6 @@ $wgAutoloadLocalClasses = array(
'LocalFileRestoreBatch' => 'includes/filerepo/file/LocalFile.php',
'OldLocalFile' => 'includes/filerepo/file/OldLocalFile.php',
'UnregisteredLocalFile' => 'includes/filerepo/file/UnregisteredLocalFile.php',
- 'FSFile' => 'includes/filerepo/backend/FSFile.php',
- 'TempFSFile' => 'includes/filerepo/backend/TempFSFile.php',
-
- # includes/filerepo/backend
- 'FileBackendGroup' => 'includes/filerepo/backend/FileBackendGroup.php',
- 'FileBackend' => 'includes/filerepo/backend/FileBackend.php',
- 'FileBackendStore' => 'includes/filerepo/backend/FileBackend.php',
- 'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
- 'FileBackendStoreShardListIterator' => 'includes/filerepo/backend/FileBackend.php',
- 'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
- 'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
- 'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',
- 'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php',
- 'LockManagerGroup' => 'includes/filerepo/backend/lockmanager/LockManagerGroup.php',
- 'LockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php',
- 'ScopedLock' => 'includes/filerepo/backend/lockmanager/LockManager.php',
- 'FSLockManager' => 'includes/filerepo/backend/lockmanager/FSLockManager.php',
- 'DBLockManager' => 'includes/filerepo/backend/lockmanager/DBLockManager.php',
- 'LSLockManager' => 'includes/filerepo/backend/lockmanager/LSLockManager.php',
- 'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php',
- 'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php',
- 'FileOp' => 'includes/filerepo/backend/FileOp.php',
- 'FileOpScopedPHPTimeout' => 'includes/filerepo/backend/FileOp.php',
- 'StoreFileOp' => 'includes/filerepo/backend/FileOp.php',
- 'CopyFileOp' => 'includes/filerepo/backend/FileOp.php',
- 'MoveFileOp' => 'includes/filerepo/backend/FileOp.php',
- 'DeleteFileOp' => 'includes/filerepo/backend/FileOp.php',
- 'ConcatenateFileOp' => 'includes/filerepo/backend/FileOp.php',
- 'CreateFileOp' => 'includes/filerepo/backend/FileOp.php',
- 'NullFileOp' => 'includes/filerepo/backend/FileOp.php',
# includes/installer
'CliInstaller' => 'includes/installer/CliInstaller.php',
@@ -563,7 +633,7 @@ $wgAutoloadLocalClasses = array(
'DoubleRedirectJob' => 'includes/job/DoubleRedirectJob.php',
'EmaillingJob' => 'includes/job/EmaillingJob.php',
'EnotifNotifyJob' => 'includes/job/EnotifNotifyJob.php',
- 'Job' => 'includes/job/JobQueue.php',
+ 'Job' => 'includes/job/Job.php',
'RefreshLinksJob' => 'includes/job/RefreshLinksJob.php',
'RefreshLinksJob2' => 'includes/job/RefreshLinksJob.php',
'UploadFromUrlJob' => 'includes/job/UploadFromUrlJob.php',
@@ -575,13 +645,19 @@ $wgAutoloadLocalClasses = array(
# includes/libs
'CSSJanus' => 'includes/libs/CSSJanus.php',
+ 'CSSJanus_Tokenizer' => 'includes/libs/CSSJanus.php',
'CSSMin' => 'includes/libs/CSSMin.php',
+ 'GenericArrayObject' => 'includes/libs/GenericArrayObject.php',
'HttpStatus' => 'includes/libs/HttpStatus.php',
'IEContentAnalyzer' => 'includes/libs/IEContentAnalyzer.php',
'IEUrlExtension' => 'includes/libs/IEUrlExtension.php',
'JavaScriptMinifier' => 'includes/libs/JavaScriptMinifier.php',
+ 'JSCompilerContext' => 'includes/libs/jsminplus.php',
'JSMinPlus' => 'includes/libs/jsminplus.php',
+ 'JSNode' => 'includes/libs/jsminplus.php',
'JSParser' => 'includes/libs/jsminplus.php',
+ 'JSToken' => 'includes/libs/jsminplus.php',
+ 'JSTokenizer' => 'includes/libs/jsminplus.php',
# includes/logging
'DatabaseLogEntry' => 'includes/logging/LogEntry.php',
@@ -613,17 +689,18 @@ $wgAutoloadLocalClasses = array(
'FormatMetadata' => 'includes/media/FormatMetadata.php',
'GIFHandler' => 'includes/media/GIF.php',
'GIFMetadataExtractor' => 'includes/media/GIFMetadataExtractor.php',
- 'ImageHandler' => 'includes/media/Generic.php',
+ 'ImageHandler' => 'includes/media/ImageHandler.php',
'IPTC' => 'includes/media/IPTC.php',
'JpegHandler' => 'includes/media/Jpeg.php',
'JpegMetadataExtractor' => 'includes/media/JpegMetadataExtractor.php',
- 'MediaHandler' => 'includes/media/Generic.php',
+ 'MediaHandler' => 'includes/media/MediaHandler.php',
'MediaTransformError' => 'includes/media/MediaTransformOutput.php',
'MediaTransformOutput' => 'includes/media/MediaTransformOutput.php',
'PNGHandler' => 'includes/media/PNG.php',
'PNGMetadataExtractor' => 'includes/media/PNGMetadataExtractor.php',
'SvgHandler' => 'includes/media/SVG.php',
'SVGMetadataExtractor' => 'includes/media/SVGMetadataExtractor.php',
+ 'SVGReader' => 'includes/media/SVGMetadataExtractor.php',
'ThumbnailImage' => 'includes/media/MediaTransformOutput.php',
'TiffHandler' => 'includes/media/Tiff.php',
'TransformParameterError' => 'includes/media/MediaTransformOutput.php',
@@ -645,16 +722,20 @@ $wgAutoloadLocalClasses = array(
'HashBagOStuff' => 'includes/objectcache/HashBagOStuff.php',
'MediaWikiBagOStuff' => 'includes/objectcache/SqlBagOStuff.php',
'MemCachedClientforWiki' => 'includes/objectcache/MemcachedClient.php',
+ 'MemcachedBagOStuff' => 'includes/objectcache/MemcachedBagOStuff.php',
+ 'MemcachedPeclBagOStuff' => 'includes/objectcache/MemcachedPeclBagOStuff.php',
'MemcachedPhpBagOStuff' => 'includes/objectcache/MemcachedPhpBagOStuff.php',
'MultiWriteBagOStuff' => 'includes/objectcache/MultiWriteBagOStuff.php',
'MWMemcached' => 'includes/objectcache/MemcachedClient.php',
'ObjectCache' => 'includes/objectcache/ObjectCache.php',
+ 'ObjectCacheSessionHandler' => 'includes/objectcache/ObjectCacheSessionHandler.php',
+ 'RedisBagOStuff' => 'includes/objectcache/RedisBagOStuff.php',
'SqlBagOStuff' => 'includes/objectcache/SqlBagOStuff.php',
'WinCacheBagOStuff' => 'includes/objectcache/WinCacheBagOStuff.php',
'XCacheBagOStuff' => 'includes/objectcache/XCacheBagOStuff.php',
# includes/parser
- 'CacheTime' => 'includes/parser/ParserOutput.php',
+ 'CacheTime' => 'includes/parser/CacheTime.php',
'CoreLinkFunctions' => 'includes/parser/CoreLinkFunctions.php',
'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php',
'CoreTagHooks' => 'includes/parser/CoreTagHooks.php',
@@ -662,6 +743,7 @@ $wgAutoloadLocalClasses = array(
'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',
@@ -727,11 +809,13 @@ $wgAutoloadLocalClasses = array(
'ResourceLoaderUserModule' => 'includes/resourceloader/ResourceLoaderUserModule.php',
'ResourceLoaderUserOptionsModule' => 'includes/resourceloader/ResourceLoaderUserOptionsModule.php',
'ResourceLoaderUserTokensModule' => 'includes/resourceloader/ResourceLoaderUserTokensModule.php',
+ 'ResourceLoaderLanguageDataModule' => 'includes/resourceloader/ResourceLoaderLanguageDataModule.php',
'ResourceLoaderWikiModule' => 'includes/resourceloader/ResourceLoaderWikiModule.php',
# includes/revisiondelete
'RevDel_ArchivedFileItem' => 'includes/revisiondelete/RevisionDelete.php',
'RevDel_ArchivedFileList' => 'includes/revisiondelete/RevisionDelete.php',
+ 'RevDel_ArchivedRevisionItem' => 'includes/revisiondelete/RevisionDelete.php',
'RevDel_ArchiveItem' => 'includes/revisiondelete/RevisionDelete.php',
'RevDel_ArchiveList' => 'includes/revisiondelete/RevisionDelete.php',
'RevDel_FileItem' => 'includes/revisiondelete/RevisionDelete.php',
@@ -747,6 +831,7 @@ $wgAutoloadLocalClasses = array(
'RevisionDeleteUser' => 'includes/revisiondelete/RevisionDeleteUser.php',
# includes/search
+ 'MssqlSearchResultSet' => 'includes/search/SearchMssql.php',
'MySQLSearchResultSet' => 'includes/search/SearchMySQL.php',
'PostgresSearchResult' => 'includes/search/SearchPostgres.php',
'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php',
@@ -756,6 +841,7 @@ $wgAutoloadLocalClasses = array(
'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php',
'SearchMssql' => 'includes/search/SearchMssql.php',
'SearchMySQL' => 'includes/search/SearchMySQL.php',
+ 'SearchNearMatchResultSet' => 'includes/search/SearchEngine.php',
'SearchOracle' => 'includes/search/SearchOracle.php',
'SearchPostgres' => 'includes/search/SearchPostgres.php',
'SearchResult' => 'includes/search/SearchEngine.php',
@@ -773,6 +859,7 @@ $wgAutoloadLocalClasses = array(
'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php',
'BlockListPager' => 'includes/specials/SpecialBlockList.php',
'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',
@@ -781,11 +868,14 @@ $wgAutoloadLocalClasses = array(
'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',
'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php',
'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php',
'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php',
'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php',
'HTMLBlockedUsersItemSelect' => 'includes/specials/SpecialBlockList.php',
+ 'ImageListPager' => 'includes/specials/SpecialListfiles.php',
'ImportReporter' => 'includes/specials/SpecialImport.php',
'IPBlockForm' => 'includes/specials/SpecialBlock.php',
'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php',
@@ -793,17 +883,22 @@ $wgAutoloadLocalClasses = array(
'LoginForm' => 'includes/specials/SpecialUserlogin.php',
'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php',
'LongPagesPage' => 'includes/specials/SpecialLongpages.php',
+ 'MergeHistoryPager' => 'includes/specials/SpecialMergeHistory.php',
'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php',
'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php',
'MostimagesPage' => 'includes/specials/SpecialMostimages.php',
+ 'MostinterwikisPage' => 'includes/specials/SpecialMostinterwikis.php',
'MostlinkedCategoriesPage' => 'includes/specials/SpecialMostlinkedcategories.php',
'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php',
'MostlinkedTemplatesPage' => 'includes/specials/SpecialMostlinkedtemplates.php',
'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php',
'MovePageForm' => 'includes/specials/SpecialMovepage.php',
+ 'NewFilesPager' => 'includes/specials/SpecialNewimages.php',
'NewPagesPager' => 'includes/specials/SpecialNewpages.php',
'PageArchive' => 'includes/specials/SpecialUndelete.php',
'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php',
+ 'ProtectedPagesPager' => 'includes/specials/SpecialProtectedpages.php',
+ 'ProtectedTitlesPager' => 'includes/specials/SpecialProtectedtitles.php',
'RandomPage' => 'includes/specials/SpecialRandompage.php',
'ShortPagesPage' => 'includes/specials/SpecialShortpages.php',
'SpecialActiveUsers' => 'includes/specials/SpecialActiveusers.php',
@@ -814,6 +909,7 @@ $wgAutoloadLocalClasses = array(
'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',
'SpecialChangeEmail' => 'includes/specials/SpecialChangeEmail.php',
'SpecialChangePassword' => 'includes/specials/SpecialChangePassword.php',
@@ -853,10 +949,11 @@ $wgAutoloadLocalClasses = array(
'SpecialUnlockdb' => 'includes/specials/SpecialUnlockdb.php',
'SpecialUpload' => 'includes/specials/SpecialUpload.php',
'SpecialUploadStash' => 'includes/specials/SpecialUploadStash.php',
+ 'SpecialUploadStashTooLargeException' => 'includes/specials/SpecialUploadStash.php',
'SpecialUserlogout' => 'includes/specials/SpecialUserlogout.php',
'SpecialVersion' => 'includes/specials/SpecialVersion.php',
'SpecialWatchlist' => 'includes/specials/SpecialWatchlist.php',
- 'SpecialWhatlinkshere' => 'includes/specials/SpecialWhatlinkshere.php',
+ 'SpecialWhatLinksHere' => 'includes/specials/SpecialWhatlinkshere.php',
'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php',
'UncategorizedImagesPage' => 'includes/specials/SpecialUncategorizedimages.php',
'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php',
@@ -865,6 +962,8 @@ $wgAutoloadLocalClasses = array(
'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php',
'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php',
'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php',
+ 'UploadChunkFileException' => 'includes/upload/UploadFromChunks.php',
+ 'UploadChunkZeroLengthFileException' => 'includes/upload/UploadFromChunks.php',
'UploadForm' => 'includes/specials/SpecialUpload.php',
'UploadSourceField' => 'includes/specials/SpecialUpload.php',
'UserrightsPage' => 'includes/specials/SpecialUserrights.php',
@@ -899,9 +998,12 @@ $wgAutoloadLocalClasses = array(
'UploadStashNoSuchKeyException' => 'includes/upload/UploadStash.php',
# languages
+ 'ConverterRule' => 'languages/LanguageConverter.php',
'FakeConverter' => 'languages/Language.php',
'Language' => 'languages/Language.php',
'LanguageConverter' => 'languages/LanguageConverter.php',
+ 'CLDRPluralRuleEvaluator' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+ 'CLDRPluralRuleError' => 'languages/utils/CLDRPluralRuleEvaluator.php',
# maintenance
'ConvertLinks' => 'maintenance/convertLinks.php',
@@ -928,6 +1030,7 @@ $wgAutoloadLocalClasses = array(
# maintenance/language
'csvStatsOutput' => 'maintenance/language/StatOutputs.php',
+ 'extensionLanguages' => 'maintenance/language/languages.inc',
'languages' => 'maintenance/language/languages.inc',
'MessageWriter' => 'maintenance/language/writeMessagesArray.inc',
'statsOutput' => 'maintenance/language/StatOutputs.php',
@@ -938,16 +1041,26 @@ $wgAutoloadLocalClasses = array(
'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',
- 'ParserTestStaticParserHook' => 'tests/parser/parserTestsStaticParserHook.php',
# tests/selenium
'Selenium' => 'tests/selenium/Selenium.php',
@@ -958,6 +1071,23 @@ $wgAutoloadLocalClasses = array(
'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',
);
class AutoLoader {
@@ -972,6 +1102,14 @@ 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.
+ $className = ltrim( $className, '\\' );
+
if ( isset( $wgAutoloadLocalClasses[$className] ) ) {
$filename = $wgAutoloadLocalClasses[$className];
} elseif ( isset( $wgAutoloadClasses[$className] ) ) {
@@ -1014,6 +1152,7 @@ class AutoLoader {
* Sanitizer that have define()s outside of their class definition. Of course
* this wouldn't be necessary if everything in MediaWiki was class-based. Sigh.
*
+ * @param $class string
* @return Boolean Return the results of class_exists() so we know if we were successful
*/
static function loadClass( $class ) {
diff --git a/includes/Autopromote.php b/includes/Autopromote.php
index a2336030..9c77855d 100644
--- a/includes/Autopromote.php
+++ b/includes/Autopromote.php
@@ -1,9 +1,30 @@
<?php
/**
+ * Automatic user rights promotion based on conditions specified
+ * in $wgAutopromote.
+ *
+ * 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 checks if user can get extra rights
* because of conditions specified in $wgAutopromote
*/
-
class Autopromote {
/**
* Get the groups for the given user based on $wgAutopromote.
@@ -32,7 +53,7 @@ class Autopromote {
*
* Does not return groups the user already belongs to or has once belonged.
*
- * @param $user The user to get the groups for
+ * @param $user User The user to get the groups for
* @param $event String key in $wgAutopromoteOnce (each one has groups/criteria)
*
* @return array Groups the user should be promoted to.
diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php
index d17104f8..05bf3186 100644
--- a/includes/BacklinkCache.php
+++ b/includes/BacklinkCache.php
@@ -1,7 +1,28 @@
<?php
/**
- * File for BacklinkCache class
+ * Class for fetching backlink lists, approximate backlink counts and
+ * partitions.
+ *
+ * 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 Tim Starling
+ * @copyright © 2009, Tim Starling, Domas Mituzas
+ * @copyright © 2010, Max Sem
+ * @copyright © 2011, Antoine Musso
*/
/**
@@ -18,13 +39,10 @@
* Introduced by r47317
*
* @internal documentation reviewed on 18 Mar 2011 by hashar
- *
- * @author Tim Starling
- * @copyright © 2009, Tim Starling, Domas Mituzas
- * @copyright © 2010, Max Sem
- * @copyright © 2011, Antoine Musso
*/
class BacklinkCache {
+ /** @var ProcessCacheLRU */
+ protected static $cache;
/**
* Multi dimensions array representing batches. Keys are:
@@ -65,13 +83,33 @@ class BacklinkCache {
/**
* Create a new BacklinkCache
- * @param Title $title : Title object to create a backlink cache for.
+ *
+ * @param Title $title : Title object to create a backlink cache for
*/
- function __construct( $title ) {
+ public function __construct( Title $title ) {
$this->title = $title;
}
/**
+ * Create a new BacklinkCache or reuse any existing one.
+ * Currently, only one cache instance can exist; callers that
+ * need multiple backlink cache objects should keep them in scope.
+ *
+ * @param Title $title : Title object to get a backlink cache for
+ * @return BacklinkCache
+ */
+ public static function get( Title $title ) {
+ if ( !self::$cache ) { // init cache
+ self::$cache = new ProcessCacheLRU( 1 );
+ }
+ $dbKey = $title->getPrefixedDBkey();
+ if ( !self::$cache->has( $dbKey, 'obj' ) ) {
+ self::$cache->set( $dbKey, 'obj', new self( $title ) );
+ }
+ return self::$cache->get( $dbKey, 'obj' );
+ }
+
+ /**
* Serialization handler, diasallows to serialize the database to prevent
* failures after this class is deserialized from cache with dead DB
* connection.
@@ -103,7 +141,7 @@ class BacklinkCache {
/**
* Get the slave connection to the database
* When non existing, will initialize the connection.
- * @return Database object
+ * @return DatabaseBase object
*/
protected function getDB() {
if ( !isset( $this->db ) ) {
@@ -179,6 +217,7 @@ class BacklinkCache {
/**
* Get the field name prefix for a given table
* @param $table String
+ * @return null|string
*/
protected function getPrefix( $table ) {
static $prefixes = array(
@@ -206,6 +245,7 @@ class BacklinkCache {
* Get the SQL condition array for selecting backlinks, with a join
* on the page table.
* @param $table String
+ * @return array|null
*/
protected function getConditions( $table ) {
$prefix = $this->getPrefix( $table );
@@ -285,7 +325,7 @@ class BacklinkCache {
*/
public function partition( $table, $batchSize ) {
- // 1) try partition cache ...
+ // 1) try partition cache ...
if ( isset( $this->partitionCache[$table][$batchSize] ) ) {
wfDebug( __METHOD__ . ": got from partition cache\n" );
@@ -340,7 +380,7 @@ class BacklinkCache {
* Partition a DB result with backlinks in it into batches
* @param $res ResultWrapper database result
* @param $batchSize integer
- * @return array @see
+ * @return array @see
*/
protected function partitionResult( $res, $batchSize ) {
$batches = array();
diff --git a/includes/Block.php b/includes/Block.php
index d80edb5e..732699dc 100644
--- a/includes/Block.php
+++ b/includes/Block.php
@@ -28,11 +28,15 @@ class Block {
$mBlockEmail,
$mDisableUsertalk,
- $mCreateAccount;
+ $mCreateAccount,
+ $mParentBlockId;
/// @var User|String
protected $target;
+ // @var Integer Hack for foreign blocking (CentralAuth)
+ protected $forcedTargetID;
+
/// @var Block::TYPE_ constant. Can only be USER, IP or RANGE internally
protected $type;
@@ -72,7 +76,7 @@ class Block {
$this->setTarget( $address );
if ( $this->target instanceof User && $user ) {
- $this->target->setId( $user ); // needed for foreign users
+ $this->forcedTargetID = $user; // needed for foreign users
}
if ( $by ) { // local user
$this->setBlocker( User::newFromID( $by ) );
@@ -122,18 +126,43 @@ class Block {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->selectRow(
'ipblocks',
- '*',
+ self::selectFields(),
array( 'ipb_id' => $id ),
__METHOD__
);
if ( $res ) {
- return Block::newFromRow( $res );
+ return self::newFromRow( $res );
} else {
return null;
}
}
/**
+ * Return the list of ipblocks fields that should be selected to create
+ * a new block.
+ * @return array
+ */
+ public static function selectFields() {
+ return array(
+ 'ipb_id',
+ 'ipb_address',
+ 'ipb_by',
+ 'ipb_by_text',
+ 'ipb_reason',
+ 'ipb_timestamp',
+ 'ipb_auto',
+ 'ipb_anon_only',
+ 'ipb_create_account',
+ 'ipb_enable_autoblock',
+ 'ipb_expiry',
+ 'ipb_deleted',
+ 'ipb_block_email',
+ 'ipb_allow_usertalk',
+ 'ipb_parent_block_id',
+ );
+ }
+
+ /**
* 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 *
*
@@ -243,7 +272,7 @@ class Block {
}
}
- $res = $db->select( 'ipblocks', '*', $conds, __METHOD__ );
+ $res = $db->select( 'ipblocks', self::selectFields(), $conds, __METHOD__ );
# This result could contain a block on the user, a block on the IP, and a russian-doll
# set of rangeblocks. We want to choose the most specific one, so keep a leader board.
@@ -256,7 +285,7 @@ class Block {
$bestBlockPreventsEdit = null;
foreach( $res as $row ){
- $block = Block::newFromRow( $row );
+ $block = self::newFromRow( $row );
# Don't use expired blocks
if( $block->deleteIfExpired() ){
@@ -365,6 +394,7 @@ class Block {
$this->mAuto = $row->ipb_auto;
$this->mHideName = $row->ipb_deleted;
$this->mId = $row->ipb_id;
+ $this->mParentBlockId = $row->ipb_parent_block_id;
// I wish I didn't have to do this
$db = wfGetDB( DB_SLAVE );
@@ -408,6 +438,7 @@ class Block {
}
$dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'ipblocks', array( 'ipb_parent_block_id' => $this->getId() ), __METHOD__ );
$dbw->delete( 'ipblocks', array( 'ipb_id' => $this->getId() ), __METHOD__ );
return $dbw->affectedRows() > 0;
@@ -483,9 +514,15 @@ class Block {
}
$expiry = $db->encodeExpiry( $this->mExpiry );
+ if ( $this->forcedTargetID ) {
+ $uid = $this->forcedTargetID;
+ } else {
+ $uid = $this->target instanceof User ? $this->target->getID() : 0;
+ }
+
$a = array(
'ipb_address' => (string)$this->target,
- 'ipb_user' => $this->target instanceof User ? $this->target->getID() : 0,
+ 'ipb_user' => $uid,
'ipb_by' => $this->getBy(),
'ipb_by_text' => $this->getByName(),
'ipb_reason' => $this->mReason,
@@ -499,7 +536,8 @@ class Block {
'ipb_range_end' => $this->getRangeEnd(),
'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite
'ipb_block_email' => $this->prevents( 'sendemail' ),
- 'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' )
+ 'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ),
+ 'ipb_parent_block_id' => $this->mParentBlockId
);
return $a;
@@ -575,7 +613,7 @@ class Block {
$key = wfMemcKey( 'ipb', 'autoblock', 'whitelist' );
$lines = $wgMemc->get( $key );
if ( !$lines ) {
- $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) );
+ $lines = explode( "\n", wfMessage( 'autoblock_whitelist' )->inContentLanguage()->plain() );
$wgMemc->set( $key, $lines, 3600 * 24 );
}
@@ -649,7 +687,7 @@ class Block {
wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" );
$autoblock->setTarget( $autoblockIP );
$autoblock->setBlocker( $this->getBlocker() );
- $autoblock->mReason = wfMsgForContent( 'autoblocker', $this->getTarget(), $this->mReason );
+ $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason )->inContentLanguage()->text();
$timestamp = wfTimestampNow();
$autoblock->mTimestamp = $timestamp;
$autoblock->mAuto = 1;
@@ -657,6 +695,7 @@ class Block {
# Continue suppressing the name if needed
$autoblock->mHideName = $this->mHideName;
$autoblock->prevents( 'editownusertalk', $this->prevents( 'editownusertalk' ) );
+ $autoblock->mParentBlockId = $this->mId;
if ( $this->mExpiry == 'infinity' ) {
# Original block was indefinite, start an autoblock now
@@ -896,7 +935,7 @@ class Block {
* Encode expiry for DB
*
* @param $expiry String: timestamp for expiry, or
- * @param $db Database object
+ * @param $db DatabaseBase object
* @return String
* @deprecated since 1.18; use $dbw->encodeExpiry() instead
*/
@@ -964,41 +1003,6 @@ class Block {
}
/**
- * Convert a DB-encoded expiry into a real string that humans can read.
- *
- * @param $encoded_expiry String: Database encoded expiry time
- * @return Html-escaped String
- * @deprecated since 1.18; use $wgLang->formatExpiry() instead
- */
- public static function formatExpiry( $encoded_expiry ) {
- wfDeprecated( __METHOD__, '1.18' );
-
- global $wgContLang;
- static $msg = null;
-
- if ( is_null( $msg ) ) {
- $msg = array();
- $keys = array( 'infiniteblock', 'expiringblock' );
-
- foreach ( $keys as $key ) {
- $msg[$key] = wfMsgHtml( $key );
- }
- }
-
- $expiry = $wgContLang->formatExpiry( $encoded_expiry, TS_MW );
- if ( $expiry == wfGetDB( DB_SLAVE )->getInfinity() ) {
- $expirystr = $msg['infiniteblock'];
- } else {
- global $wgLang;
- $expiredatestr = htmlspecialchars( $wgLang->date( $expiry, true ) );
- $expiretimestr = htmlspecialchars( $wgLang->time( $expiry, true ) );
- $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array( $expiredatestr, $expiretimestr ) );
- }
-
- return $expirystr;
- }
-
- /**
* 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
@@ -1066,8 +1070,6 @@ class Block {
* @return array( User|String, Block::TYPE_ constant )
*/
public static function parseTarget( $target ) {
- $target = trim( $target );
-
# We may have been through this before
if( $target instanceof User ){
if( IP::isValid( $target->getName() ) ){
@@ -1079,6 +1081,8 @@ class Block {
return array( null, null );
}
+ $target = trim( $target );
+
if ( IP::isValid( $target ) ) {
# We can still create a User if it's an IP address, but we need to turn
# off validation checking (which would exclude IP addresses)
diff --git a/includes/CacheHelper.php b/includes/CacheHelper.php
new file mode 100644
index 00000000..ac46fc42
--- /dev/null
+++ b/includes/CacheHelper.php
@@ -0,0 +1,392 @@
+<?php
+/**
+ * Cache of various elements in a single cache entry.
+ *
+ * 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
+ * @licence GNU GPL v2 or later
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+
+/**
+ * Interface for all classes implementing CacheHelper functionality.
+ *
+ * @since 1.20
+ */
+interface ICacheHelper {
+
+ /**
+ * Sets if the cache should be enabled or not.
+ *
+ * @since 1.20
+ * @param boolean $cacheEnabled
+ */
+ function setCacheEnabled( $cacheEnabled );
+
+ /**
+ * Initializes the caching.
+ * Should be called before the first time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ *
+ * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp.
+ * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not.
+ */
+ function startCache( $cacheExpiry = null, $cacheEnabled = null );
+
+ /**
+ * Get a cached value if available or compute it if not and then cache it if possible.
+ * The provided $computeFunction is only called when the computation needs to happen
+ * and should return a result value. $args are arguments that will be passed to the
+ * compute function when called.
+ *
+ * @since 1.20
+ *
+ * @param {function} $computeFunction
+ * @param array|mixed $args
+ * @param string|null $key
+ *
+ * @return mixed
+ */
+ function getCachedValue( $computeFunction, $args = array(), $key = null );
+
+ /**
+ * Saves the HTML to the cache in case it got recomputed.
+ * Should be called after the last time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ */
+ function saveCache();
+
+ /**
+ * Sets the time to live for the cache, in seconds or a unix timestamp
+ * indicating the point of expiry...
+ *
+ * @since 1.20
+ *
+ * @param integer $cacheExpiry
+ */
+ function setExpiry( $cacheExpiry );
+
+}
+
+/**
+ * Helper class for caching various elements in a single cache entry.
+ *
+ * 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!
+ *
+ * Before the first addCachedHTML call, you should call $this->startCache();
+ * After adding the last HTML that should be cached, call $this->saveCache();
+ *
+ * @since 1.20
+ */
+class CacheHelper implements ICacheHelper {
+
+ /**
+ * The time to live for the cache, in seconds or a unix timestamp indicating the point of expiry.
+ *
+ * @since 1.20
+ * @var integer
+ */
+ protected $cacheExpiry = 3600;
+
+ /**
+ * List of HTML chunks to be cached (if !hasCached) or that where cached (of hasCached).
+ * If not cached already, then the newly computed chunks are added here,
+ * if it as cached already, chunks are removed from this list as they are needed.
+ *
+ * @since 1.20
+ * @var array
+ */
+ protected $cachedChunks;
+
+ /**
+ * Indicates if the to be cached content was already cached.
+ * Null if this information is not available yet.
+ *
+ * @since 1.20
+ * @var boolean|null
+ */
+ protected $hasCached = null;
+
+ /**
+ * If the cache is enabled or not.
+ *
+ * @since 1.20
+ * @var boolean
+ */
+ protected $cacheEnabled = true;
+
+ /**
+ * Function that gets called when initialization is done.
+ *
+ * @since 1.20
+ * @var callable
+ */
+ protected $onInitHandler = false;
+
+ /**
+ * Elements to build a cache key with.
+ *
+ * @since 1.20
+ * @var array
+ */
+ protected $cacheKey = array();
+
+ /**
+ * Sets if the cache should be enabled or not.
+ *
+ * @since 1.20
+ * @param boolean $cacheEnabled
+ */
+ public function setCacheEnabled( $cacheEnabled ) {
+ $this->cacheEnabled = $cacheEnabled;
+ }
+
+ /**
+ * Initializes the caching.
+ * Should be called before the first time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ *
+ * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp.
+ * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not.
+ */
+ public function startCache( $cacheExpiry = null, $cacheEnabled = null ) {
+ if ( is_null( $this->hasCached ) ) {
+ if ( !is_null( $cacheExpiry ) ) {
+ $this->cacheExpiry = $cacheExpiry;
+ }
+
+ if ( !is_null( $cacheEnabled ) ) {
+ $this->setCacheEnabled( $cacheEnabled );
+ }
+
+ $this->initCaching();
+ }
+ }
+
+ /**
+ * Returns a message that notifies the user he/she is looking at
+ * a cached version of the page, including a refresh link.
+ *
+ * @since 1.20
+ *
+ * @param IContextSource $context
+ * @param boolean $includePurgeLink
+ *
+ * @return string
+ */
+ public function getCachedNotice( IContextSource $context, $includePurgeLink = true ) {
+ if ( $this->cacheExpiry < 86400 * 3650 ) {
+ $message = $context->msg(
+ 'cachedspecial-viewing-cached-ttl',
+ $context->getLanguage()->formatDuration( $this->cacheExpiry )
+ )->escaped();
+ }
+ else {
+ $message = $context->msg(
+ 'cachedspecial-viewing-cached-ts'
+ )->escaped();
+ }
+
+ if ( $includePurgeLink ) {
+ $refreshArgs = $context->getRequest()->getQueryValues();
+ unset( $refreshArgs['title'] );
+ $refreshArgs['action'] = 'purge';
+
+ $subPage = $context->getTitle()->getFullText();
+ $subPage = explode( '/', $subPage, 2 );
+ $subPage = count( $subPage ) > 1 ? $subPage[1] : false;
+
+ $message .= ' ' . Linker::link(
+ $context->getTitle( $subPage ),
+ $context->msg( 'cachedspecial-refresh-now' )->escaped(),
+ array(),
+ $refreshArgs
+ );
+ }
+
+ return $message;
+ }
+
+ /**
+ * Initializes the caching if not already done so.
+ * Should be called before any of the caching functionality is used.
+ *
+ * @since 1.20
+ */
+ protected function initCaching() {
+ if ( $this->cacheEnabled && is_null( $this->hasCached ) ) {
+ $cachedChunks = wfGetCache( CACHE_ANYTHING )->get( $this->getCacheKeyString() );
+
+ $this->hasCached = is_array( $cachedChunks );
+ $this->cachedChunks = $this->hasCached ? $cachedChunks : array();
+
+ if ( $this->onInitHandler !== false ) {
+ call_user_func( $this->onInitHandler, $this->hasCached );
+ }
+ }
+ }
+
+ /**
+ * Get a cached value if available or compute it if not and then cache it if possible.
+ * The provided $computeFunction is only called when the computation needs to happen
+ * and should return a result value. $args are arguments that will be passed to the
+ * compute function when called.
+ *
+ * @since 1.20
+ *
+ * @param {function} $computeFunction
+ * @param array|mixed $args
+ * @param string|null $key
+ *
+ * @return mixed
+ */
+ public function getCachedValue( $computeFunction, $args = array(), $key = null ) {
+ $this->initCaching();
+
+ if ( $this->cacheEnabled && $this->hasCached ) {
+ $value = null;
+
+ if ( is_null( $key ) ) {
+ $itemKey = array_keys( array_slice( $this->cachedChunks, 0, 1 ) );
+ $itemKey = array_shift( $itemKey );
+
+ if ( !is_integer( $itemKey ) ) {
+ wfWarn( "Attempted to get item with non-numeric key while the next item in the queue has a key ($itemKey) in " . __METHOD__ );
+ }
+ elseif ( is_null( $itemKey ) ) {
+ wfWarn( "Attempted to get an item while the queue is empty in " . __METHOD__ );
+ }
+ else {
+ $value = array_shift( $this->cachedChunks );
+ }
+ }
+ else {
+ if ( array_key_exists( $key, $this->cachedChunks ) ) {
+ $value = $this->cachedChunks[$key];
+ unset( $this->cachedChunks[$key] );
+ }
+ else {
+ wfWarn( "There is no item with key '$key' in this->cachedChunks in " . __METHOD__ );
+ }
+ }
+ }
+ else {
+ if ( !is_array( $args ) ) {
+ $args = array( $args );
+ }
+
+ $value = call_user_func_array( $computeFunction, $args );
+
+ if ( $this->cacheEnabled ) {
+ if ( is_null( $key ) ) {
+ $this->cachedChunks[] = $value;
+ }
+ else {
+ $this->cachedChunks[$key] = $value;
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Saves the HTML to the cache in case it got recomputed.
+ * Should be called after the last time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ */
+ public function saveCache() {
+ if ( $this->cacheEnabled && $this->hasCached === false && !empty( $this->cachedChunks ) ) {
+ wfGetCache( CACHE_ANYTHING )->set( $this->getCacheKeyString(), $this->cachedChunks, $this->cacheExpiry );
+ }
+ }
+
+ /**
+ * Sets the time to live for the cache, in seconds or a unix timestamp
+ * indicating the point of expiry...
+ *
+ * @since 1.20
+ *
+ * @param integer $cacheExpiry
+ */
+ public function setExpiry( $cacheExpiry ) {
+ $this->cacheExpiry = $cacheExpiry;
+ }
+
+ /**
+ * Returns the cache key to use to cache this page's HTML output.
+ * Is constructed from the special page name and language code.
+ *
+ * @since 1.20
+ *
+ * @return string
+ * @throws MWException
+ */
+ protected function getCacheKeyString() {
+ if ( $this->cacheKey === array() ) {
+ throw new MWException( 'No cache key set, so cannot obtain or save the CacheHelper values.' );
+ }
+
+ return call_user_func_array( 'wfMemcKey', $this->cacheKey );
+ }
+
+ /**
+ * Sets the cache key that should be used.
+ *
+ * @since 1.20
+ *
+ * @param array $cacheKey
+ */
+ public function setCacheKey( array $cacheKey ) {
+ $this->cacheKey = $cacheKey;
+ }
+
+ /**
+ * Rebuild the content, even if it's already cached.
+ * This effectively has the same effect as purging the cache,
+ * since it will be overridden with the new value on the next request.
+ *
+ * @since 1.20
+ */
+ public function rebuildOnDemand() {
+ $this->hasCached = false;
+ }
+
+ /**
+ * Sets a function that gets called when initialization of the cache is done.
+ *
+ * @since 1.20
+ *
+ * @param $handlerFunction
+ */
+ public function setOnInitializedHandler( $handlerFunction ) {
+ $this->onInitHandler = $handlerFunction;
+ }
+
+}
diff --git a/includes/Category.php b/includes/Category.php
index 9d9b5a67..b7b12e8a 100644
--- a/includes/Category.php
+++ b/includes/Category.php
@@ -1,14 +1,33 @@
<?php
/**
+ * Representation for a category.
+ *
+ * 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 Simetrical
+ */
+
+/**
* Category objects are immutable, strictly speaking. If you call methods that change the database,
* like to refresh link counts, the objects will be appropriately reinitialized.
* Member variables are lazy-initialized.
*
* TODO: Move some stuff from CategoryPage.php to here, and use that.
- *
- * @author Simetrical
*/
-
class Category {
/** Name of the category, normalized to DB-key form */
private $mName = null;
@@ -103,7 +122,7 @@ class Category {
* Factory function.
*
* @param $title Title for the category page
- * @return category|false on a totally invalid name
+ * @return Category|bool on a totally invalid name
*/
public static function newFromTitle( $title ) {
$cat = new self();
@@ -185,7 +204,7 @@ class Category {
public function getFileCount() { return $this->getX( 'mFiles' ); }
/**
- * @return Title|false Title for this category, or false on failure.
+ * @return Title|bool Title for this category, or false on failure.
*/
public function getTitle() {
if ( $this->mTitle ) return $this->mTitle;
@@ -231,7 +250,10 @@ class Category {
);
}
- /** Generic accessor */
+ /**
+ * Generic accessor
+ * @return bool
+ */
private function getX( $key ) {
if ( !$this->initialize() ) {
return false;
@@ -257,7 +279,7 @@ class Category {
}
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
+ $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
@@ -275,13 +297,13 @@ class Category {
'IGNORE'
);
- $cond1 = $dbw->conditional( 'page_namespace=' . NS_CATEGORY, 1, 'NULL' );
- $cond2 = $dbw->conditional( 'page_namespace=' . NS_FILE, 1, 'NULL' );
+ $cond1 = $dbw->conditional( array( 'page_namespace' => NS_CATEGORY ), 1, 'NULL' );
+ $cond2 = $dbw->conditional( array( 'page_namespace' => NS_FILE ), 1, 'NULL' );
$result = $dbw->selectRow(
array( 'categorylinks', 'page' ),
- array( 'COUNT(*) AS pages',
- "COUNT($cond1) AS subcats",
- "COUNT($cond2) AS files"
+ array( 'pages' => 'COUNT(*)',
+ 'subcats' => "COUNT($cond1)",
+ 'files' => "COUNT($cond2)"
),
array( 'cl_to' => $this->mName, 'page_id = cl_from' ),
__METHOD__,
@@ -297,7 +319,7 @@ class Category {
array( 'cat_title' => $this->mName ),
__METHOD__
);
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
# Now we should update our local counts.
$this->mPages = $result->pages;
diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php
index eab7a356..32e270e8 100644
--- a/includes/CategoryPage.php
+++ b/includes/CategoryPage.php
@@ -1,14 +1,26 @@
<?php
/**
- * Class for viewing MediaWiki category description pages.
+ * Special handling for category description pages.
* Modelled after ImagePage.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
*/
-if ( !defined( 'MEDIAWIKI' ) )
- die( 1 );
-
/**
* Special handling for category description pages, showing pages,
* subcategories and file that belong to the category
@@ -29,6 +41,7 @@ class CategoryPage extends Article {
/**
* Constructor from a page id
* @param $id Int article ID to load
+ * @return CategoryPage|null
*/
public static function newFromID( $id ) {
$t = Title::newFromID( $id );
diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php
index e8e91423..3bb2bc9b 100644
--- a/includes/CategoryViewer.php
+++ b/includes/CategoryViewer.php
@@ -1,7 +1,24 @@
<?php
-
-if ( !defined( 'MEDIAWIKI' ) )
- die( 1 );
+/**
+ * List and paging of category members.
+ *
+ * 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 CategoryViewer extends ContextSource {
var $limit, $from, $until,
@@ -105,7 +122,7 @@ class CategoryViewer extends ContextSource {
// Give a proper message if category is empty
if ( $r == '' ) {
- $r = wfMsgExt( 'category-empty', array( 'parse' ) );
+ $r = $this->msg( 'category-empty' )->parseAsBlock();
}
$lang = $this->getLanguage();
@@ -172,7 +189,8 @@ class CategoryViewer extends ContextSource {
*
* @param Title $title
* @param string $sortkey The human-readable sortkey (before transforming to icu or whatever).
- */
+ * @return string
+ */
function getSubcategorySortChar( $title, $sortkey ) {
global $wgContLang;
@@ -351,7 +369,7 @@ class CategoryViewer extends ContextSource {
if ( $rescnt > 0 ) {
# Showing subcategories
$r .= "<div id=\"mw-subcategories\">\n";
- $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
+ $r .= '<h2>' . $this->msg( 'subcategories' )->text() . "</h2>\n";
$r .= $countmsg;
$r .= $this->getSectionPagingLinks( 'subcat' );
$r .= $this->formatList( $this->children, $this->children_start_char );
@@ -365,7 +383,7 @@ class CategoryViewer extends ContextSource {
* @return string
*/
function getPagesSection() {
- $ti = htmlspecialchars( $this->title->getText() );
+ $ti = wfEscapeWikiText( $this->title->getText() );
# Don't show articles section if there are none.
$r = '';
@@ -380,7 +398,7 @@ class CategoryViewer extends ContextSource {
if ( $rescnt > 0 ) {
$r = "<div id=\"mw-pages\">\n";
- $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
+ $r .= '<h2>' . $this->msg( 'category_header', $ti )->text() . "</h2>\n";
$r .= $countmsg;
$r .= $this->getSectionPagingLinks( 'page' );
$r .= $this->formatList( $this->articles, $this->articles_start_char );
@@ -401,7 +419,7 @@ class CategoryViewer extends ContextSource {
$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
$r .= "<div id=\"mw-category-media\">\n";
- $r .= '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n";
+ $r .= '<h2>' . $this->msg( 'category-media-header', wfEscapeWikiText( $this->title->getText() ) )->text() . "</h2>\n";
$r .= $countmsg;
$r .= $this->getSectionPagingLinks( 'file' );
if ( $this->showGallery ) {
@@ -486,11 +504,11 @@ class CategoryViewer extends ContextSource {
# Split into three columns
$columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ );
- $ret = '<table width="100%"><tr valign="top">';
+ $ret = '<table style="width: 100%;"><tr style="vertical-align: top;">';
$prevchar = null;
foreach ( $columns as $column ) {
- $ret .= '<td width="33.3%">';
+ $ret .= '<td style="width: 33.3%;">';
$colContents = array();
# Kind of like array_flip() here, but we keep duplicates in an
@@ -508,7 +526,7 @@ class CategoryViewer extends ContextSource {
if ( $first && $char === $prevchar ) {
# We're continuing a previous chunk at the top of a new
# column, so add " cont." after the letter.
- $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
+ $ret .= ' ' . wfMessage( 'listingcontinuesabbrev' )->escaped();
}
$ret .= "</h3>\n";
@@ -558,7 +576,7 @@ class CategoryViewer extends ContextSource {
* @return String HTML
*/
private function pagingLinks( $first, $last, $type = '' ) {
- $prevLink = wfMessage( 'prevn' )->numParams( $this->limit )->escaped();
+ $prevLink = $this->msg( 'prevn' )->numParams( $this->limit )->escaped();
if ( $first != '' ) {
$prevQuery = $this->query;
@@ -572,7 +590,7 @@ class CategoryViewer extends ContextSource {
);
}
- $nextLink = wfMessage( 'nextn' )->numParams( $this->limit )->escaped();
+ $nextLink = $this->msg( 'nextn' )->numParams( $this->limit )->escaped();
if ( $last != '' ) {
$lastQuery = $this->query;
@@ -586,7 +604,7 @@ class CategoryViewer extends ContextSource {
);
}
- return "($prevLink) ($nextLink)";
+ return $this->msg('categoryviewer-pagedlinks')->rawParams($prevLink, $nextLink)->escaped();
}
/**
@@ -670,8 +688,8 @@ class CategoryViewer extends ContextSource {
$this->cat->refreshCounts();
} else {
# Case 3: hopeless. Don't give a total count at all.
- return wfMessage( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
+ return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
}
- return wfMessage( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
+ return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
}
}
diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php
index 4a8ed709..e2b6a0ca 100644
--- a/includes/Categoryfinder.php
+++ b/includes/Categoryfinder.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Recent changes filtering by category.
+ *
+ * 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
+ */
+
+/**
* The "Categoryfinder" class takes a list of articles, creates an internal
* representation of all their parent categories (as well as parents of
* parents etc.). From this representation, it determines which of these
diff --git a/includes/Cdb.php b/includes/Cdb.php
index 94aa1925..ae2e5b18 100644
--- a/includes/Cdb.php
+++ b/includes/Cdb.php
@@ -1,6 +1,21 @@
<?php
/**
- * Native CDB file reader and writer
+ * Native CDB file reader and writer.
+ *
+ * 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
*/
diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php
index 53175272..02be65f3 100644
--- a/includes/Cdb_PHP.php
+++ b/includes/Cdb_PHP.php
@@ -6,6 +6,21 @@
* * Exception thrown if sizes or offsets are between 2GB and 4GB
* * Some variables renamed
*
+ * 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
*/
@@ -51,7 +66,7 @@ class CdbFunctions {
/**
* The CDB hash function.
*
- * @param $s
+ * @param $s string
*
* @return
*/
diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php
index 63d37327..0ebc926f 100644
--- a/includes/ChangeTags.php
+++ b/includes/ChangeTags.php
@@ -1,9 +1,25 @@
<?php
/**
- * Functions related to change tags.
+ * Recent changes tagging.
+ *
+ * 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 ChangeTags {
/**
@@ -19,6 +35,8 @@ class ChangeTags {
*
*/
static function formatSummaryRow( $tags, $page ) {
+ global $wgLang;
+
if( !$tags )
return array( '', array() );
@@ -35,7 +53,7 @@ class ChangeTags {
);
$classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
}
- $markers = '(' . implode( ', ', $displayTags ) . ')';
+ $markers = wfMessage( 'parentheses' )->rawParams( $wgLang->commaList( $displayTags ) )->text();
$markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers );
return array( $markers, $classes );
@@ -209,17 +227,17 @@ class ChangeTags {
if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) )
return $fullForm ? '' : array();
- $data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMsgExt( 'tag-filter', 'parseinline' ) ),
- Xml::input( 'tagfilter', 20, $selected ) );
+ $data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMessage( 'tag-filter' )->parse() ),
+ Xml::input( 'tagfilter', 20, $selected, array( 'class' => 'mw-tagfilter-input' ) ) );
if ( !$fullForm ) {
return $data;
}
$html = implode( '&#160;', $data );
- $html .= "\n" . Xml::element( 'input', array( 'type' => 'submit', 'value' => wfMsg( 'tag-filter-submit' ) ) );
+ $html .= "\n" . Xml::element( 'input', array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() ) );
$html .= "\n" . Html::hidden( 'title', $title->getPrefixedText() );
- $html = Xml::tags( 'form', array( 'action' => $title->getLocalURL(), 'method' => 'get' ), $html );
+ $html = Xml::tags( 'form', array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ), $html );
return $html;
}
diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php
index bcedf2f3..ee4c2d64 100644
--- a/includes/ChangesFeed.php
+++ b/includes/ChangesFeed.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Feed for list of 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
+ */
/**
* Feed to Special:RecentChanges and Special:RecentChangesLiked
@@ -51,13 +71,13 @@ class ChangesFeed {
* @param $rows ResultWrapper object with rows in recentchanges table
* @param $lastmod Integer: timestamp of the last item in the recentchanges table (only used for the cache key)
* @param $opts FormOptions as in SpecialRecentChanges::getDefaultOptions()
- * @return null or true
+ * @return null|bool True or null
*/
public function execute( $feed, $rows, $lastmod, $opts ) {
global $wgLang, $wgRenderHashAppend;
if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
- return;
+ return null;
}
$optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend;
@@ -107,7 +127,7 @@ class ChangesFeed {
* @param $lastmod Integer: timestamp of the last item in the recentchanges table
* @param $timekey String: memcached key of the last modification
* @param $key String: memcached key of the content
- * @return feed's content on cache hit or false on cache miss
+ * @return string|bool feed's content on cache hit or false on cache miss
*/
public function loadFromCache( $lastmod, $timekey, $key ) {
global $wgFeedCacheTimeout, $wgOut, $messageMemc;
@@ -186,7 +206,7 @@ class ChangesFeed {
FeedUtils::formatDiff( $obj ),
$url,
$obj->rc_timestamp,
- ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $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
index fd97e0cb..84677124 100644
--- a/includes/ChangesList.php
+++ b/includes/ChangesList.php
@@ -1,10 +1,27 @@
<?php
/**
- * Classes to show various lists of changes:
+ * 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
*/
@@ -63,7 +80,7 @@ class ChangesList extends ContextSource {
* This first argument used to be an User object.
*
* @deprecated in 1.18; use newFromContext() instead
- * @param $unused Unused
+ * @param $unused string|User Unused
* @return ChangesList|EnhancedChangesList|OldChangesList derivative
*/
public static function newFromUser( $unused ) {
@@ -91,7 +108,7 @@ class ChangesList extends ContextSource {
}
/**
- * Sets the list to use a <li class="watchlist-(namespace)-(page)"> tag
+ * Sets the list to use a "<li class='watchlist-(namespace)-(page)'>" tag
* @param $value Boolean
*/
public function setWatchlistDivs( $value = true ) {
@@ -106,7 +123,7 @@ class ChangesList extends ContextSource {
if( !isset( $this->message ) ) {
foreach ( explode( ' ', 'cur diff hist last blocklink history ' .
'semicolon-separator pipe-separator' ) as $msg ) {
- $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
+ $this->message[$msg] = $this->msg( $msg )->escaped();
}
}
}
@@ -128,7 +145,7 @@ class ChangesList extends ContextSource {
}
/**
- * Provide the <abbr> element appropriate to a given abbreviated flag,
+ * 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.
@@ -146,8 +163,8 @@ class ChangesList extends ContextSource {
'unpatrolled' => array( 'unpatrolledletter', 'recentchanges-label-unpatrolled' ),
);
foreach( $messages as &$value ) {
- $value[0] = wfMsgExt( $value[0], 'escapenoentities' );
- $value[1] = wfMsgExt( $value[1], 'escapenoentities' );
+ $value[0] = wfMessage( $value[0] )->escaped();
+ $value[1] = wfMessage( $value[1] )->escaped();
}
}
@@ -175,6 +192,7 @@ class ChangesList extends ContextSource {
$this->rcCacheIndex = 0;
$this->lastdate = '';
$this->rclistOpen = false;
+ $this->getOutput()->addModuleStyles( 'mediawiki.special.changeslist' );
return '';
}
@@ -182,22 +200,31 @@ class ChangesList extends ContextSource {
* 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 ) {
- global $wgRCChangedSizeThreshold, $wgLang, $wgMiserMode;
+ 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;
- $code = $wgLang->getCode();
+ $lang = $context->getLanguage();
+ $code = $lang->getCode();
static $fastCharDiff = array();
if ( !isset($fastCharDiff[$code]) ) {
- $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1';
+ $fastCharDiff[$code] = $wgMiserMode || $context->msg( 'rc-change-size' )->plain() === '$1';
}
- $formattedSize = $wgLang->formatNum($szdiff);
+ $formattedSize = $lang->formatNum( $szdiff );
if ( !$fastCharDiff[$code] ) {
- $formattedSize = wfMsgExt( 'rc-change-size', array( 'parsemag' ), $formattedSize );
+ $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text();
}
if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) {
@@ -217,11 +244,34 @@ class ChangesList extends ContextSource {
$formattedSizeClass = 'mw-plusminus-neg';
}
- $formattedTotalSize = wfMsgExt( 'rc-change-size-new', 'parsemag', $wgLang->formatNum( $new ) );
+ $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text();
return Html::element( $tag,
array( 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ),
- wfMessage( 'parentheses', $formattedSize )->plain() ) . $wgLang->getDirMark();
+ $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() );
}
/**
@@ -238,7 +288,7 @@ class ChangesList extends ContextSource {
public function insertDateHeader( &$s, $rc_timestamp ) {
# Make date header if necessary
- $date = $this->getLanguage()->date( $rc_timestamp, true, true );
+ $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() );
if( $date != $this->lastdate ) {
if( $this->lastdate != '' ) {
$s .= "</ul>\n";
@@ -252,7 +302,7 @@ class ChangesList extends ContextSource {
public function insertLog( &$s, $title, $logtype ) {
$page = new LogPage( $logtype );
$logname = $page->getName()->escaped();
- $s .= '(' . Linker::linkKnown( $title, $logname ) . ')';
+ $s .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $title, $logname ) )->escaped();
}
/**
@@ -284,9 +334,9 @@ class ChangesList extends ContextSource {
$query
);
}
- $s .= '(' . $diffLink . $this->message['pipe-separator'];
+ $diffhist = $diffLink . $this->message['pipe-separator'];
# History link
- $s .= Linker::linkKnown(
+ $diffhist .= Linker::linkKnown(
$rc->getTitle(),
$this->message['hist'],
array(),
@@ -295,7 +345,7 @@ class ChangesList extends ContextSource {
'action' => 'history'
)
);
- $s .= ') . . ';
+ $s .= $this->msg( 'parentheses' )->rawParams( $diffhist )->escaped() . ' <span class="mw-changeslist-separator">. .</span> ';
}
/**
@@ -316,16 +366,14 @@ class ChangesList extends ContextSource {
$articlelink = Linker::linkKnown(
$rc->getTitle(),
null,
- array(),
+ array( 'class' => 'mw-changeslist-title' ),
$params
);
if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
$articlelink = '<span class="history-deleted">' . $articlelink . '</span>';
}
- # Bolden pages watched by this user
- if( $watched ) {
- $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>";
- }
+ # To allow for boldening pages watched by this user
+ $articlelink = "<span class=\"mw-title\">{$articlelink}</span>";
# RTL/LTR marker
$articlelink .= $this->getLanguage()->getDirMark();
@@ -340,8 +388,8 @@ class ChangesList extends ContextSource {
* @param $rc RecentChange
*/
public function insertTimestamp( &$s, $rc ) {
- $s .= $this->message['semicolon-separator'] .
- $this->getLanguage()->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . ';
+ $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> ';
}
/**
@@ -352,7 +400,7 @@ class ChangesList extends ContextSource {
*/
public function insertUserRelatedLinks( &$s, &$rc ) {
if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
- $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ $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'] );
@@ -364,22 +412,25 @@ class ChangesList extends ContextSource {
* 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">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>';
+ return ' <span class="history-deleted">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
} else {
return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
}
@@ -397,13 +448,13 @@ class ChangesList extends ContextSource {
/**
* 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] = wfMsgExt( 'number_of_watching_users_RCview',
- array('parsemag', 'escape' ), $this->getLanguage()->formatNum( $count ) );
+ $cache[$count] = $this->msg( 'number_of_watching_users_RCview' )->numParams( $count )->escaped();
}
return $cache[$count];
} else {
@@ -456,7 +507,7 @@ class ChangesList extends ContextSource {
* @param $rc RecentChange
*/
public function insertRollback( &$s, &$rc ) {
- if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) {
+ 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 */
@@ -497,7 +548,7 @@ class ChangesList extends ContextSource {
if ( !$rc->mAttribs['rc_patrolled'] ) {
if ( $this->getUser()->useRCPatrol() ) {
$unpatrolled = true;
- } elseif ( $this->getUser()->useNPPatrol() && $rc->mAttribs['rc_new'] ) {
+ } elseif ( $this->getUser()->useNPPatrol() && $rc->mAttribs['rc_type'] == RC_NEW ) {
$unpatrolled = true;
}
}
@@ -513,7 +564,10 @@ class OldChangesList extends ChangesList {
/**
* Format a line using the old system (aka without any javascript).
*
- * @param $rc RecentChange
+ * @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;
@@ -537,11 +591,15 @@ class OldChangesList extends ChangesList {
}
}
+ // 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 = Title::newFromText( 'Log/'.$rc->mAttribs['rc_log_type'], NS_SPECIAL );
+ $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 ) {
@@ -555,7 +613,7 @@ class OldChangesList extends ChangesList {
# M, N, b and ! (minor, new, bot and unpatrolled)
$s .= $this->recentChangesFlags(
array(
- 'newpage' => $rc->mAttribs['rc_new'],
+ 'newpage' => $rc->mAttribs['rc_type'] == RC_NEW,
'minor' => $rc->mAttribs['rc_minor'],
'unpatrolled' => $unpatrolled,
'bot' => $rc->mAttribs['rc_bot']
@@ -567,10 +625,10 @@ class OldChangesList extends ChangesList {
# Edit/log timestamp
$this->insertTimestamp( $s, $rc );
# Bytes added or removed
- if( $wgRCShowChangedSize ) {
- $cd = $rc->getCharacterDifference();
- if( $cd != '' ) {
- $s .= "$cd . . ";
+ if ( $wgRCShowChangedSize ) {
+ $cd = $this->formatCharacterDifference( $rc );
+ if ( $cd !== '' ) {
+ $s .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
}
}
@@ -593,8 +651,7 @@ class OldChangesList extends ChangesList {
# How many users watch this page
if( $rc->numberofWatchingusers > 0 ) {
- $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview',
- array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $rc->numberofWatchingusers ) );
+ $s .= ' ' . $this->numberofWatchingusers( $rc->numberofWatchingusers );
}
if( $this->watchlist ) {
@@ -646,7 +703,7 @@ class EnhancedChangesList extends ChangesList {
$curIdEq = array( 'curid' => $rc->mAttribs['rc_cur_id'] );
# If it's a new day, add the headline and flush the cache
- $date = $this->getLanguage()->date( $rc->mAttribs['rc_timestamp'], true );
+ $date = $this->getLanguage()->userDate( $rc->mAttribs['rc_timestamp'], $this->getUser() );
$ret = '';
if( $date != $this->lastdate ) {
# Process current cache
@@ -675,7 +732,7 @@ class EnhancedChangesList extends ChangesList {
$logtitle = SpecialPage::getTitleFor( 'Log', $logType );
$logpage = new LogPage( $logType );
$logname = $logpage->getName()->escaped();
- $clink = '(' . Linker::linkKnown( $logtitle, $logname ) . ')';
+ $clink = $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $logtitle, $logname ) )->escaped();
} else {
$clink = Linker::link( $rc->getTitle() );
}
@@ -694,7 +751,7 @@ class EnhancedChangesList extends ChangesList {
$showdifflinks = false;
}
- $time = $this->getLanguage()->time( $rc->mAttribs['rc_timestamp'], true, true );
+ $time = $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() );
$rc->watched = $watched;
$rc->link = $clink;
$rc->timestamp = $time;
@@ -743,7 +800,7 @@ class EnhancedChangesList extends ChangesList {
# Make user links
if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
- $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ $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'] );
@@ -779,6 +836,7 @@ class EnhancedChangesList extends ChangesList {
/**
* Enhanced RC group
+ * @return string
*/
protected function recentChangesBlockGroup( $block ) {
global $wgRCShowChangedSize;
@@ -786,14 +844,16 @@ class EnhancedChangesList extends ChangesList {
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 = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-'
+ $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-'
. $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] );
} else {
- $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns'
+ $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' );
@@ -808,7 +868,7 @@ class EnhancedChangesList extends ChangesList {
$allLogs = true;
foreach( $block as $rcObj ) {
$oldid = $rcObj->mAttribs['rc_last_oldid'];
- if( $rcObj->mAttribs['rc_new'] ) {
+ if( $rcObj->mAttribs['rc_type'] == RC_NEW ) {
$isnew = true;
}
// If all log actions to this page were hidden, then don't
@@ -847,24 +907,17 @@ class EnhancedChangesList extends ChangesList {
$text = $userlink;
$text .= $this->getLanguage()->getDirMark();
if( $count > 1 ) {
- $text .= ' (' . $this->getLanguage()->formatNum( $count ) . '×)';
+ $text .= ' ' . $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->formatNum( $count ) . '×' )->escaped();
}
array_push( $users, $text );
}
- $users = ' <span class="changedby">[' .
- implode( $this->message['semicolon-separator'], $users ) . ']</span>';
+ $users = ' <span class="changedby">'
+ . $this->msg( 'brackets' )->rawParams(
+ implode( $this->message['semicolon-separator'], $users )
+ )->escaped() . '</span>';
- # Title for <a> tags
- $expandTitle = htmlspecialchars( wfMsg( 'rc-enhanced-expand' ) );
- $closeTitle = htmlspecialchars( wfMsg( 'rc-enhanced-hide' ) );
-
- $tl = "<span class='mw-collapsible-toggle'>"
- . "<span class='mw-rc-openarrow'>"
- . "<a href='#' title='$expandTitle'>{$this->sideArrow()}</a>"
- . "</span><span class='mw-rc-closearrow'>"
- . "<a href='#' title='$closeTitle'>{$this->downArrow()}</a>"
- . "</span></span>";
+ $tl = '<span class="mw-collapsible-toggle mw-enhancedchanges-arrow"></span>';
$r .= "<td>$tl</td>";
# Main line
@@ -880,7 +933,7 @@ class EnhancedChangesList extends ChangesList {
# Article link
if( $namehidden ) {
- $r .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>';
+ $r .= ' <span class="history-deleted">' . $this->msg( 'rev-deleted-event' )->escaped() . '</span>';
} elseif( $allLogs ) {
$r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
} else {
@@ -894,22 +947,22 @@ class EnhancedChangesList extends ChangesList {
$n = count($block);
static $nchanges = array();
if ( !isset( $nchanges[$n] ) ) {
- $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $n ) );
+ $nchanges[$n] = $this->msg( 'nchanges' )->numParams( $n )->escaped();
}
# Total change link
$r .= ' ';
+ $logtext = '';
if( !$allLogs ) {
- $r .= '(';
if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) {
- $r .= $nchanges[$n];
+ $logtext .= $nchanges[$n];
} elseif( $isnew ) {
- $r .= $nchanges[$n];
+ $logtext .= $nchanges[$n];
} else {
$params = $queryParams;
$params['diff'] = $currentRevision;
$params['oldid'] = $oldid;
- $r .= Linker::link(
+ $logtext .= Linker::link(
$block[0]->getTitle(),
$nchanges[$n],
array(),
@@ -923,20 +976,25 @@ class EnhancedChangesList extends ChangesList {
if( $allLogs ) {
// don't show history link for logs
} elseif( $namehidden || !$block[0]->getTitle()->exists() ) {
- $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')';
+ $logtext .= $this->message['pipe-separator'] . $this->message['hist'];
} else {
$params = $queryParams;
$params['action'] = 'history';
- $r .= $this->message['pipe-separator'] .
+ $logtext .= $this->message['pipe-separator'] .
Linker::linkKnown(
$block[0]->getTitle(),
$this->message['hist'],
array(),
$params
- ) . ')';
+ );
}
- $r .= ' . . ';
+
+ 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 ) {
@@ -950,13 +1008,12 @@ class EnhancedChangesList extends ChangesList {
$first--;
}
# Get net change
- $chardiff = $rcObj->getCharacterDifference( $block[$first]->mAttribs['rc_old_len'],
- $block[$last]->mAttribs['rc_new_len'] );
+ $chardiff = $this->formatCharacterDifference( $block[$first], $block[$last] );
if( $chardiff == '' ) {
$r .= ' ';
} else {
- $r .= ' ' . $chardiff. ' . . ';
+ $r .= ' ' . $chardiff. ' <span class="mw-changeslist-separator">. .</span> ';
}
}
@@ -969,10 +1026,9 @@ class EnhancedChangesList extends ChangesList {
$classes = array();
$type = $rcObj->mAttribs['rc_type'];
- #$r .= '<tr><td valign="top">'.$this->spacerArrow();
$r .= '<tr><td></td><td class="mw-enhanced-rc">';
$r .= $this->recentChangesFlags( array(
- 'newpage' => $rcObj->mAttribs['rc_new'],
+ 'newpage' => $type == RC_NEW,
'minor' => $rcObj->mAttribs['rc_minor'],
'unpatrolled' => $rcObj->unpatrolled,
'bot' => $rcObj->mAttribs['rc_bot'],
@@ -1008,17 +1064,16 @@ class EnhancedChangesList extends ChangesList {
$r .= $link . '</span>';
if ( !$type == RC_LOG || $type == RC_NEW ) {
- $r .= ' (';
- $r .= $rcObj->curlink;
- $r .= $this->message['pipe-separator'];
- $r .= $rcObj->lastlink;
- $r .= ')';
+ $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->curlink . $this->message['pipe-separator'] . $rcObj->lastlink )->escaped();
}
- $r .= ' . . ';
+ $r .= ' <span class="mw-changeslist-separator">. .</span> ';
# Character diff
- if( $wgRCShowChangedSize && $rcObj->getCharacterDifference() ) {
- $r .= $rcObj->getCharacterDifference() . ' . . ' ;
+ if ( $wgRCShowChangedSize ) {
+ $cd = $this->formatCharacterDifference( $rcObj );
+ if ( $cd !== '' ) {
+ $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> ';
+ }
}
if ( $rcObj->mAttribs['rc_type'] == RC_LOG ) {
@@ -1051,7 +1106,7 @@ class EnhancedChangesList extends ChangesList {
* @param $dir String: one of '', 'd', 'l', 'r'
* @param $alt String: text
* @param $title String: text
- * @return String: HTML <img> tag
+ * @return String: HTML "<img>" tag
*/
protected function arrow( $dir, $alt='', $title='' ) {
global $wgStylePath;
@@ -1064,26 +1119,25 @@ class EnhancedChangesList extends ChangesList {
/**
* Generate HTML for a right- or left-facing arrow,
* depending on language direction.
- * @return String: HTML <img> tag
+ * @return String: HTML "<img>" tag
*/
protected function sideArrow() {
- global $wgLang;
- $dir = $wgLang->isRTL() ? 'l' : 'r';
- return $this->arrow( $dir, '+', wfMsg( 'rc-enhanced-expand' ) );
+ $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
+ * @return String: HTML "<img>" tag
*/
protected function downArrow() {
- return $this->arrow( 'd', '-', wfMsg( 'rc-enhanced-hide' ) );
+ return $this->arrow( 'd', '-', $this->msg( 'rc-enhanced-hide' )->text() );
}
/**
* Generate HTML for a spacer image
- * @return String: HTML <img> tag
+ * @return String: HTML "<img>" tag
*/
protected function spacerArrow() {
return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space
@@ -1103,18 +1157,20 @@ class EnhancedChangesList extends ChangesList {
$type = $rcObj->mAttribs['rc_type'];
$logType = $rcObj->mAttribs['rc_log_type'];
+ $classes = array( 'mw-enhanced-rc' );
if( $logType ) {
# Log entry
- $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-'
+ $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-'
. $logType . '-' . $rcObj->mAttribs['rc_title'] );
} else {
- $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' .
+ $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">' . $this->spacerArrow();
+ $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
@@ -1129,39 +1185,41 @@ class EnhancedChangesList extends ChangesList {
$r .= '&#160;'.$rcObj->timestamp.'&#160;</td><td>';
# Article or log link
if( $logType ) {
- $logtitle = SpecialPage::getTitleFor( 'Log', $logType );
- $logname = LogPage::logName( $logType );
- $r .= '(' . Linker::linkKnown( $logtitle, htmlspecialchars( $logname ) ) . ')';
+ $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 ) {
- $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator'];
$query['action'] = 'history';
- $r .= Linker::linkKnown(
+ $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->difflink . $this->message['pipe-separator'] . Linker::linkKnown(
$rcObj->getTitle(),
$this->message['hist'],
array(),
$query
- ) . ')';
+ ) )->escaped();
}
- $r .= ' . . ';
+ $r .= ' <span class="mw-changeslist-separator">. .</span> ';
# Character diff
- if( $wgRCShowChangedSize && ($cd = $rcObj->getCharacterDifference()) ) {
- $r .= "$cd . . ";
+ 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 {
+ } else {
$r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
$r .= $this->insertComment( $rcObj );
- $r .= $this->insertRollback( $r, $rcObj );
+ $this->insertRollback( $r, $rcObj );
}
# Tags
- $classes = explode( ' ', $classes );
$this->insertTags( $r, $rcObj, $classes );
# Show how many people are watching this if enabled
$r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers);
diff --git a/includes/Collation.php b/includes/Collation.php
index 0c510b78..ad2b94b1 100644
--- a/includes/Collation.php
+++ b/includes/Collation.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Database row sorting.
+ *
+ * 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
+ */
abstract class Collation {
static $instance;
@@ -311,7 +331,7 @@ class IcuCollation extends Collation {
* -1, 0 or 1 in the style of strcmp().
* @param $target string The target value to find.
*
- * @return The item index of the lower bound, or false if the target value
+ * @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 ) {
diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php
index 42a7173d..b68fc762 100644
--- a/includes/ConfEditor.php
+++ b/includes/ConfEditor.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Configuration file editor.
+ *
+ * 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 is a state machine style parser with two internal stacks:
@@ -139,6 +159,7 @@ class ConfEditor {
* insert
* Insert a new element at the start of the array.
*
+ * @return string
*/
public function edit( $ops ) {
$this->parse();
@@ -371,6 +392,7 @@ 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.
+ * @return array
*/
function findDeletionRegion( $pathName ) {
if ( !isset( $this->pathInfo[$pathName] ) ) {
@@ -428,6 +450,7 @@ class ConfEditor {
* or semicolon.
*
* The end position is the past-the-end (end + 1) value as per convention.
+ * @return array
*/
function findValueRegion( $pathName ) {
if ( !isset( $this->pathInfo[$pathName] ) ) {
@@ -444,6 +467,7 @@ class ConfEditor {
* Find the path name of the last element in the array.
* If the array is empty, this will return the \@extra interstitial element.
* If the specified path is not found or is not an array, it will return false.
+ * @return bool|int|string
*/
function findLastArrayElement( $path ) {
// Try for a real element
@@ -480,6 +504,7 @@ class ConfEditor {
* Find the path name of first element in the array.
* If the array is empty, this will return the \@extra interstitial element.
* If the specified path is not found or is not an array, it will return false.
+ * @return bool|int|string
*/
function findFirstArrayElement( $path ) {
// Try for an ordinary element
@@ -504,6 +529,7 @@ class ConfEditor {
/**
* Get the indent string which sits after a given start position.
* Returns false if the position is not at the start of the line.
+ * @return array
*/
function getIndent( $pos, $key = false, $arrowPos = false ) {
$arrowIndent = ' ';
@@ -725,6 +751,7 @@ class ConfEditor {
/**
* Create a ConfEditorToken from an element of token_get_all()
+ * @return ConfEditorToken
*/
function newTokenObj( $internalToken ) {
if ( is_array( $internalToken ) ) {
@@ -776,6 +803,7 @@ class ConfEditor {
/**
* Get the token $offset steps ahead of the current position.
* $offset may be negative, to get tokens behind the current position.
+ * @return ConfEditorToken
*/
function getTokenAhead( $offset ) {
$pos = $this->pos + $offset;
@@ -821,6 +849,7 @@ class ConfEditor {
/**
* Pop a state from the state stack.
+ * @return mixed
*/
function popState() {
return array_pop( $this->stateStack );
@@ -829,6 +858,7 @@ class ConfEditor {
/**
* Returns true if the user input path is valid.
* This exists to allow "/" and "@" to be reserved for string path keys
+ * @return bool
*/
function validatePath( $path ) {
return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
@@ -949,6 +979,7 @@ class ConfEditor {
/**
* Get a readable name for the given token type.
+ * @return string
*/
function getTypeName( $type ) {
if ( is_int( $type ) ) {
@@ -962,6 +993,7 @@ class ConfEditor {
* Looks ahead to see if the given type is the next token type, starting
* from the current position plus the given offset. Skips any intervening
* whitespace.
+ * @return bool
*/
function isAhead( $type, $offset = 0 ) {
$ahead = $offset;
diff --git a/includes/Cookie.php b/includes/Cookie.php
index 76739ccc..7984d63e 100644
--- a/includes/Cookie.php
+++ b/includes/Cookie.php
@@ -1,6 +1,24 @@
<?php
/**
- * @defgroup HTTP HTTP
+ * Cookie for HTTP requests.
+ *
+ * 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 HTTP
*/
class Cookie {
@@ -62,8 +80,8 @@ class Cookie {
* A better method might be to use a blacklist like
* http://publicsuffix.org/
*
- * @fixme fails to detect 3-letter top-level domains
- * @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 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)
*
* @param $domain String: the domain to validate
* @param $originDomain String: (optional) the domain the cookie originates from
@@ -193,6 +211,7 @@ class CookieJar {
/**
* @see Cookie::serializeToHttpRequest
+ * @return string
*/
public function serializeToHttpRequest( $path, $domain ) {
$cookies = array();
@@ -213,6 +232,7 @@ class CookieJar {
*
* @param $cookie String
* @param $domain String: cookie's domain
+ * @return null
*/
public function parseCookieResponseHeader ( $cookie, $domain ) {
$len = strlen( 'Set-Cookie:' );
diff --git a/includes/CryptRand.php b/includes/CryptRand.php
index e4be1b37..858eebf2 100644
--- a/includes/CryptRand.php
+++ b/includes/CryptRand.php
@@ -5,6 +5,21 @@
* This is based in part on Drupal code as well as what we used in our own code
* prior to introduction of this class.
*
+ * 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
+ *
* @author Daniel Friesen
* @file
*/
@@ -54,7 +69,7 @@ class MWCryptRand {
// It'll also vary slightly across different machines
$state = serialize( $_SERVER );
- // To try and vary the system information of the state a bit more
+ // To try vary the system information of the state a bit more
// by including the system's hostname into the state
$state .= wfHostname();
@@ -63,13 +78,22 @@ 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
$files[] = __FILE__;
+
+ // We must also have a parent folder, and with the usual file structure, a grandparent
+ $files[] = __DIR__;
+ $files[] = dirname( __DIR__ );
+
// The config file is likely the most often edited file we know should be around
- // so if the constant with it's location is defined include it's stat info into the state
+ // so include its stat info into the state.
+ // The constant with its location will almost always be defined, as WebStart.php defines
+ // MW_CONFIG_FILE to $IP/LocalSettings.php unless being configured with MW_CONFIG_CALLBACK (eg. the installer)
if ( defined( 'MW_CONFIG_FILE' ) ) {
$files[] = MW_CONFIG_FILE;
}
+
foreach ( $files as $file ) {
wfSuppressWarnings();
$stat = stat( $file );
@@ -275,7 +299,7 @@ class MWCryptRand {
if ( strlen( $buffer ) < $bytes ) {
// If available make use of mcrypt_create_iv URANDOM source to generate randomness
// On unix-like systems this reads from /dev/urandom but does it without any buffering
- // and bypasses openbasdir restrictions so it's preferable to reading directly
+ // and bypasses openbasedir restrictions, so it's preferable to reading directly
// On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
// entropy so this is also preferable to just trying to read urandom because it may work
// on Windows systems as well.
@@ -294,9 +318,10 @@ class MWCryptRand {
}
if ( strlen( $buffer ) < $bytes ) {
- // If available make use of openssl's random_pesudo_bytes method to attempt to generate randomness.
+ // If available make use of openssl's random_pseudo_bytes method to attempt to generate randomness.
// However don't do this on Windows with PHP < 5.3.4 due to a bug:
// http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php
+ // http://git.php.net/?p=php-src.git;a=commitdiff;h=cd62a70863c261b07f6dadedad9464f7e213cad5
if ( function_exists( 'openssl_random_pseudo_bytes' )
&& ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) )
) {
diff --git a/includes/DataUpdate.php b/includes/DataUpdate.php
new file mode 100644
index 00000000..377b64c0
--- /dev/null
+++ b/includes/DataUpdate.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Base code for update jobs that do something with some secondary
+ * data extracted from article.
+ *
+ * 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
+ */
+
+/**
+ * Abstract base class for update jobs that do something with some secondary
+ * data extracted from article.
+ *
+ * @note: subclasses should NOT start or commit transactions in their doUpdate() method,
+ * a transaction will automatically be wrapped around the update. If need be,
+ * subclasses can override the beginTransaction() and commitTransaction() methods.
+ */
+abstract class DataUpdate implements DeferrableUpdate {
+
+ /**
+ * Constructor
+ */
+ public function __construct( ) {
+ # noop
+ }
+
+ /**
+ * Begin an appropriate transaction, if any.
+ * This default implementation does nothing.
+ */
+ public function beginTransaction() {
+ //noop
+ }
+
+ /**
+ * Commit the transaction started via beginTransaction, if any.
+ * This default implementation does nothing.
+ */
+ public function commitTransaction() {
+ //noop
+ }
+
+ /**
+ * Abort / roll back the transaction started via beginTransaction, if any.
+ * This default implementation does nothing.
+ */
+ public function rollbackTransaction() {
+ //noop
+ }
+
+ /**
+ * Convenience method, calls doUpdate() on every DataUpdate in the array.
+ *
+ * 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()
+ * 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
+ */
+ public static function runUpdates( $updates ) {
+ if ( empty( $updates ) ) return; # nothing to do
+
+ $open_transactions = array();
+ $exception = null;
+
+ /**
+ * @var $update DataUpdate
+ * @var $trans DataUpdate
+ */
+
+ try {
+ // begin transactions
+ foreach ( $updates as $update ) {
+ $update->beginTransaction();
+ $open_transactions[] = $update;
+ }
+
+ // do work
+ foreach ( $updates as $update ) {
+ $update->doUpdate();
+ }
+
+ // commit transactions
+ while ( count( $open_transactions ) > 0 ) {
+ $trans = array_pop( $open_transactions );
+ $trans->commitTransaction();
+ }
+ } catch ( Exception $ex ) {
+ $exception = $ex;
+ wfDebug( "Caught exception, will rethrow after rollback: " . $ex->getMessage() );
+ }
+
+ // rollback remaining transactions
+ while ( count( $open_transactions ) > 0 ) {
+ $trans = array_pop( $open_transactions );
+ $trans->rollbackTransaction();
+ }
+
+ if ( $exception ) {
+ throw $exception; // rethrow after cleanup
+ }
+ }
+
+}
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index ef1ef402..8216beb8 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -1,6 +1,7 @@
<?php
/**
- * @file
+ * Default values for MediaWiki configuration settings.
+ *
*
* NEVER EDIT THIS FILE
*
@@ -15,25 +16,50 @@
*
* Documentation is in the source and on:
* http://www.mediawiki.org/wiki/Manual:Configuration_settings
+ *
+ * @warning Note: this (and other things) will break if the autoloader is not
+ * enabled. Please include includes/AutoLoader.php before including this file.
+ *
+ * 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 Globalsettings Global settings
*/
/**
* @cond file_level_code
- * This is not a valid entry point, perform no further processing unless MEDIAWIKI is defined
+ * This is not a valid entry point, perform no further processing unless
+ * MEDIAWIKI is defined
*/
if( !defined( 'MEDIAWIKI' ) ) {
echo "This file is part of MediaWiki and is not a valid entry point\n";
die( 1 );
}
-# Create a site configuration object. Not used for much in a default install.
-# Note: this (and other things) will break if the autoloader is not enabled.
-# Please include includes/AutoLoader.php before including this file.
+/**
+ * wgConf hold the site configuration.
+ * Not used for much in a default install.
+ */
$wgConf = new SiteConfiguration;
-/** @endcond */
/** MediaWiki version number */
-$wgVersion = '1.19.3';
+$wgVersion = '1.20.2';
/** Name of the site. It must be changed in LocalSettings.php */
$wgSitename = 'MediaWiki';
@@ -41,10 +67,10 @@ $wgSitename = 'MediaWiki';
/**
* URL of the server.
*
- * Example:
- * <code>
+ * @par Example:
+ * @code
* $wgServer = 'http://example.com';
- * </code>
+ * @endcode
*
* This is usually detected correctly by MediaWiki. If MediaWiki detects the
* wrong server, it will redirect incorrectly after you save a page. In that
@@ -110,28 +136,6 @@ $wgUsePathInfo =
*/
$wgScriptExtension = '.php';
-/**
- * The URL path to index.php.
- *
- * Will default to "{$wgScriptPath}/index{$wgScriptExtension}" in Setup.php
- */
-$wgScript = false;
-
-/**
- * The URL path to redirect.php. This is a script that is used by the Nostalgia
- * skin.
- *
- * Will default to "{$wgScriptPath}/redirect{$wgScriptExtension}" in Setup.php
- */
-$wgRedirectScript = false;
-
-/**
- * The URL path to load.php.
- *
- * Defaults to "{$wgScriptPath}/load{$wgScriptExtension}".
- */
-$wgLoadScript = false;
-
/**@}*/
@@ -154,7 +158,30 @@ $wgLoadScript = false;
*/
/**
- * The URL path of the skins directory. Will default to "{$wgScriptPath}/skins" in Setup.php
+ * The URL path to index.php.
+ *
+ * Defaults to "{$wgScriptPath}/index{$wgScriptExtension}".
+ */
+$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}".
+ */
+$wgLoadScript = false;
+
+/**
+ * The URL path of the skins directory.
+ * Defaults to "{$wgScriptPath}/skins".
*/
$wgStylePath = false;
$wgStyleSheetPath = &$wgStylePath;
@@ -173,7 +200,8 @@ $wgLocalStylePath = false;
$wgExtensionAssetsPath = false;
/**
- * Filesystem stylesheets directory. Will default to "{$IP}/skins" in Setup.php
+ * Filesystem stylesheets directory.
+ * Defaults to "{$IP}/skins".
*/
$wgStyleDirectory = false;
@@ -181,29 +209,31 @@ $wgStyleDirectory = false;
* The URL path for primary article page views. This path should contain $1,
* which is replaced by the article title.
*
- * Will default to "{$wgScript}/$1" or "{$wgScript}?title=$1" in Setup.php,
+ * Defaults to "{$wgScript}/$1" or "{$wgScript}?title=$1",
* depending on $wgUsePathInfo.
*/
$wgArticlePath = false;
/**
- * The URL path for the images directory. Will default to "{$wgScriptPath}/images" in Setup.php
+ * The URL path for the images directory.
+ * Defaults to "{$wgScriptPath}/images".
*/
$wgUploadPath = false;
/**
- * The maximum age of temporary (incomplete) uploaded files
+ * The filesystem path of the images directory. Defaults to "{$IP}/images".
*/
-$wgUploadStashMaxAge = 6 * 3600; // 6 hours
+$wgUploadDirectory = false;
/**
- * The filesystem path of the images directory. Defaults to "{$IP}/images".
+ * Directory where the cached page will be saved.
+ * Defaults to "{$wgUploadDirectory}/cache".
*/
-$wgUploadDirectory = false;
+$wgFileCacheDirectory = false;
/**
* The URL path of the wiki logo. The logo size should be 135x135 pixels.
- * Will default to "{$wgStylePath}/common/images/wiki.png" in Setup.php
+ * Defaults to "{$wgStylePath}/common/images/wiki.png".
*/
$wgLogo = false;
@@ -222,7 +252,16 @@ $wgAppleTouchIcon = false;
* The local filesystem path to a temporary directory. This is not required to
* be web accessible.
*
- * Will default to "{$wgUploadDirectory}/tmp" in Setup.php
+ * When this setting is set to false, its value will be set through a call
+ * to wfTempDir(). See that methods implementation for the actual detection
+ * logic.
+ *
+ * Developers should use the global function wfTempDir() instead of this
+ * variable.
+ *
+ * @see wfTempDir()
+ * @note Default changed to false in MediaWiki 1.20.
+ *
*/
$wgTmpDirectory = false;
@@ -242,11 +281,16 @@ $wgUploadStashScalerBaseUrl = false;
/**
* To set 'pretty' URL paths for actions other than
- * plain page views, add to this array. For instance:
+ * plain page views, add to this array.
+ *
+ * @par Example:
+ * Set pretty URL for the edit action:
+ * @code
* 'edit' => "$wgScriptPath/edit/$1"
+ * @endcode
*
- * There must be an appropriate script or rewrite rule
- * in place to handle these URLs.
+ * There must be an appropriate script or rewrite rule in place to handle these
+ * URLs.
*/
$wgActionPaths = array();
@@ -260,11 +304,16 @@ $wgActionPaths = array();
/** Uploads have to be specially set up to be secure */
$wgEnableUploads = false;
+/**
+ * The maximum age of temporary (incomplete) uploaded files
+ */
+$wgUploadStashMaxAge = 6 * 3600; // 6 hours
+
/** Allows to move images and other media files */
$wgAllowImageMoving = true;
/**
- * These are additional characters that should be replaced with '-' in file names
+ * These are additional characters that should be replaced with '-' in filenames
*/
$wgIllegalFileChars = ":";
@@ -274,9 +323,10 @@ $wgIllegalFileChars = ":";
$wgFileStore = array();
/**
- * What directory to place deleted uploads in
+ * What directory to place deleted uploads in.
+ * Defaults to "{$wgUploadDirectory}/deleted".
*/
-$wgDeletedDirectory = false; // Defaults to $wgUploadDirectory/deleted
+$wgDeletedDirectory = false;
/**
* Set this to true if you use img_auth and want the user to see details on why access failed.
@@ -308,11 +358,15 @@ $wgImgAuthPublicTest = true;
*
* For most core repos:
* - zones Associative array of zone names that each map to an array with:
- * container : backend container name the zone is in
- * directory : root path within container for the zone
- * Zones default to using <repo name>-<zone> as the
- * container name and the container root as the zone directory.
- * - url Base public URL
+ * 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
+ * (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.
+ * Nesting of zone locations within other zones should be avoided.
+ * - url Public zone URL. The 'zones' settings take precedence.
* - hashLevels The number of directory levels for hash-based division of files
* - thumbScriptUrl The URL for thumb.php (optional, not recommended)
* - transformVia404 Whether to skip media file transformation on parse and rely on a 404
@@ -329,9 +383,11 @@ $wgImgAuthPublicTest = true;
* is 0644.
* - directory The local filesystem directory where public files are stored. Not used for
* some remote repos.
- * - thumbDir The base thumbnail directory. Defaults to <directory>/thumb.
- * - thumbUrl The base thumbnail URL. Defaults to <url>/thumb.
- *
+ * - thumbDir The base thumbnail directory. Defaults to "<directory>/thumb".
+ * - thumbUrl The base thumbnail URL. Defaults to "<url>/thumb".
+ * - isPrivate Set this if measures should always be taken to keep the files private.
+ * One should not trust this to assure that the files are not web readable;
+ * the server configuration should be done manually depending on the backend.
*
* These settings describe a foreign MediaWiki installation. They are optional, and will be ignored
* for local repositories:
@@ -343,7 +399,9 @@ $wgImgAuthPublicTest = true;
*
* - articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1
* - fetchDescription Fetch the text of the remote file description page. Equivalent to
- * $wgFetchCommonsDescriptions.
+ * $wgFetchCommonsDescriptions.
+ * - abbrvThreshold File names over this size will use the short form of thumbnail names.
+ * Short thumbnail names only have the width, parameters, and the extension.
*
* ForeignDBRepo:
* - dbType, dbServer, dbUser, dbPassword, dbName, dbFlags
@@ -380,10 +438,11 @@ $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)
+ * - '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.
*/
$wgFileBackends = array();
@@ -391,8 +450,8 @@ $wgFileBackends = array();
/**
* Array of configuration arrays for each lock manager.
* Each backend configuration has the following parameters:
- * 'name' : A unique name for the lock manger
- * 'class' : The lock manger class to use
+ * - 'name' : A unique name for the lock manager
+ * - 'class' : The lock manger class to use
* Additional parameters are specific to the class used.
*/
$wgLockManagers = array();
@@ -401,12 +460,13 @@ $wgLockManagers = array();
* 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 extensions" section of php.ini:
- *
+ * @note FOR WINDOWS USERS:
+ * To enable EXIF functions, add the following lines to the "Windows
+ * extensions" section of php.ini:
+ * @code{.ini}
* extension=extensions/php_mbstring.dll
* extension=extensions/php_exif.dll
+ * @endcode
*/
$wgShowEXIF = function_exists( 'exif_read_data' );
@@ -431,23 +491,32 @@ $wgUpdateCompatibleMetadata = false;
* $wgForeignFileRepos variable.
*/
$wgUseSharedUploads = false;
+
/** 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? */
$wgFetchCommonsDescriptions = false;
+
/** 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. */
$wgSharedUploadDBname = false;
+
/** 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 */
$wgCacheSharedUploads = true;
+
/**
-* Allow for upload to be copied from an URL. Requires Special:Upload?source=web
-* The timeout for copy uploads is set by $wgHTTPTimeout.
-*/
+ * Allow for upload to be copied from an URL.
+ * The timeout for copy uploads is set by $wgHTTPTimeout.
+ * You have to assign the user right 'upload_by_url' to a user group, to use this.
+ */
$wgAllowCopyUploads = false;
+
/**
* Allow asynchronous copy uploads.
* This feature is experimental and broken as of r81612.
@@ -455,16 +524,31 @@ $wgAllowCopyUploads = false;
$wgAllowAsyncCopyUploads = false;
/**
+ * A list of domains copy uploads can come from
+ *
+ * @since 1.20
+ */
+$wgCopyUploadsDomains = array();
+
+/**
+ * Proxy to use for copy upload requests.
+ * @since 1.20
+ */
+$wgCopyUploadProxy = 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
* for non-specified types.
*
- * For example:
+ * @par Example:
+ * @code
* $wgMaxUploadSize = array(
* '*' => 250 * 1024,
* 'url' => 500 * 1024,
* );
+ * @endcode
* Sets the maximum for all uploads to 250 kB except for upload-by-url, which
* will have a maximum of 500 kB.
*
@@ -474,27 +558,37 @@ $wgMaxUploadSize = 1024*1024*100; # 100MB
/**
* Point the upload navigation link to an external URL
* Useful if you want to use a shared repository by default
- * without disabling local uploads (use $wgEnableUploads = false for that)
- * e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload';
+ * without disabling local uploads (use $wgEnableUploads = false for that).
+ *
+ * @par Example:
+ * @code
+ * $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload';
+ * @endcode
*/
$wgUploadNavigationUrl = false;
/**
* Point the upload link for missing files to an external URL, as with
- * $wgUploadNavigationUrl. The URL will get (?|&)wpDestFile=<filename>
+ * $wgUploadNavigationUrl. The URL will get "(?|&)wpDestFile=<filename>"
* appended to it as appropriate.
*/
$wgUploadMissingFileUrl = false;
/**
- * Give a path here to use thumb.php for thumbnail generation on client request, instead of
- * generating them on render and outputting a static URL. This is necessary if some of your
- * apache servers don't have read/write access to the thumbnail path.
+ * Give a path here to use thumb.php for thumbnail generation on client
+ * request, instead of generating them on render and outputting a static URL.
+ * This is necessary if some of your apache servers don't have read/write
+ * access to the thumbnail path.
*
- * Example:
+ * @par Example:
+ * @code
* $wgThumbnailScriptPath = "{$wgScriptPath}/thumb{$wgScriptExtension}";
+ * @endcode
*/
$wgThumbnailScriptPath = false;
+/**
+ * @see $wgThumbnailScriptPath
+ */
$wgSharedThumbnailScriptPath = false;
/**
@@ -507,7 +601,8 @@ $wgSharedThumbnailScriptPath = false;
* maintenance/rebuildImages.php to register them in the database. This is no
* longer recommended, use maintenance/importImages.php instead.
*
- * Note that this variable may be ignored if $wgLocalFileRepo is set.
+ * @note That this variable may be ignored if $wgLocalFileRepo is set.
+ * @todo Deprecate the setting and ultimately remove it from Core.
*/
$wgHashedUploadDirectory = true;
@@ -532,13 +627,17 @@ $wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/File:";
* This is the list of preferred extensions for uploading files. Uploading files
* with extensions not in this list will trigger a warning.
*
- * WARNING: If you add any OpenOffice or Microsoft Office file formats here,
+ * @warning If you add any OpenOffice or Microsoft Office file formats here,
* such as odt or doc, and untrusted users are allowed to upload files, then
* your wiki will be vulnerable to cross-site request forgery (CSRF).
*/
$wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' );
-/** Files with these extensions will never be allowed as uploads. */
+/**
+ * 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',
@@ -576,7 +675,7 @@ $wgAllowJavaUploads = false;
/**
* This is a flag to determine whether or not to check file extensions on upload.
*
- * WARNING: setting this to false is insecure for public wikis.
+ * @warning Setting this to false is insecure for public wikis.
*/
$wgCheckFileExtensions = true;
@@ -584,18 +683,21 @@ $wgCheckFileExtensions = true;
* If this is turned off, users may override the warning for files not covered
* by $wgFileExtensions.
*
- * WARNING: setting this to false is insecure for public wikis.
+ * @warning Setting this to false is insecure for public wikis.
*/
$wgStrictFileExtensions = true;
/**
* Setting this to true will disable the upload system's checks for HTML/JavaScript.
- * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions
- * TO RESTRICT UPLOADING to only those that you trust
+ *
+ * @warning THIS IS VERY DANGEROUS on a publicly editable site, so USE
+ * $wgGroupPermissions TO RESTRICT UPLOADING to only those that you trust
*/
$wgDisableUploadScriptChecks = false;
-/** Warn if uploaded files are larger than this (in bytes), or false to disable*/
+/**
+ * Warn if uploaded files are larger than this (in bytes), or false to disable
+ */
$wgUploadSizeWarning = false;
/**
@@ -622,18 +724,18 @@ $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
);
/**
@@ -667,17 +769,18 @@ $wgImageMagickTempDir = false;
* %s will be replaced with the source path, %d with the destination
* %w and %h will be replaced with the width and height.
*
- * Example for GraphicMagick:
- * <code>
+ * @par Example for GraphicMagick:
+ * @code
* $wgCustomConvertCommand = "gm convert %s -resize %wx%h %d"
- * </code>
+ * @endcode
*
* Leave as false to skip this.
*/
$wgCustomConvertCommand = false;
/**
- * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some image formats.
+ * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some
+ * image formats.
*/
$wgExiv2Command = '/usr/bin/exiv2';
@@ -699,22 +802,31 @@ $wgSVGConverters = array(
'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output',
'ImagickExt' => array( 'SvgHandler::rasterizeImagickExt' ),
);
+
/** Pick a converter defined in $wgSVGConverters */
$wgSVGConverter = 'ImageMagick';
+
/** If not in the executable PATH, specify the SVG converter path. */
$wgSVGConverterPath = '';
+
/** Don't scale a SVG larger than this */
$wgSVGMaxSize = 2048;
+
/** Don't read SVG metadata beyond this point.
- * Default is 1024*256 bytes */
+ * Default is 1024*256 bytes
+ */
$wgSVGMetadataCutoff = 262144;
/**
- * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't
- * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading
- * crap files as images. When this directive is on, <title> will be allowed in files with
- * an "image/svg+xml" MIME type. You should leave this disabled if your web server is misconfigured
- * and doesn't send appropriate MIME types for SVG images.
+ * Disallow <title> element in SVG files.
+ *
+ * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic
+ * browsers which can not perform basic stuff like MIME detection and which are
+ * vulnerable to further idiots uploading crap files as images.
+ *
+ * When this directive is on, "<title>" will be allowed in files with an
+ * "image/svg+xml" MIME type. You should leave this disabled if your web server
+ * is misconfigured and doesn't send appropriate MIME types for SVG images.
*/
$wgAllowTitlesInSVG = false;
@@ -744,13 +856,13 @@ $wgMaxAnimatedGifArea = 1.25e7;
* For inline display, we need to convert to PNG or JPEG.
* Note scaling should work with ImageMagick, but may not with GD scaling.
*
- * Example:
- * <code>
+ * @par Example:
+ * @code
* // PNG is lossless, but inefficient for photos
* $wgTiffThumbnailType = array( 'png', 'image/png' );
* // JPEG is good for photos, but has no transparency support. Bad for diagrams.
* $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' );
- * </code>
+ * @endcode
*/
$wgTiffThumbnailType = false;
@@ -763,7 +875,7 @@ $wgMaxAnimatedGifArea = 1.25e7;
$wgThumbnailEpoch = '20030516000000';
/**
- * If set, inline scaled images will still produce <img> tags ready for
+ * If set, inline scaled images will still produce "<img>" tags ready for
* output instead of showing an error message.
*
* This may be useful if errors are transitory, especially if the site
@@ -855,20 +967,6 @@ $wgAntivirusSetup = array(
'messagepattern' => '/.*?:(.*)/sim',
),
-
- #setup for f-prot
- 'f-prot' => array (
- 'command' => "f-prot ",
-
- 'codemap' => array (
- "0" => AV_NO_VIRUS, # no virus
- "3" => AV_VIRUS_FOUND, # virus found
- "6" => AV_VIRUS_FOUND, # virus found
- "*" => AV_SCAN_FAILED, # else scan failed
- ),
-
- 'messagepattern' => '/.*?Infection:(.*)$/m',
- ),
);
@@ -898,10 +996,11 @@ $wgLoadFileinfoExtension = false;
* 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.
- * Example:
- * <code>
+ *
+ * @par Example:
+ * @code
* #$wgMimeDetectorCommand = "file -bi"; # use external mime detector (Linux)
- * </code>
+ * @endcode
*/
$wgMimeDetectorCommand = null;
@@ -937,8 +1036,7 @@ $wgImageLimits = array(
array( 640, 480 ),
array( 800, 600 ),
array( 1024, 768 ),
- array( 1280, 1024 ),
- array( 10000, 10000 )
+ array( 1280, 1024 )
);
/**
@@ -956,7 +1054,7 @@ $wgThumbLimits = array(
);
/**
- * Default parameters for the <gallery> tag
+ * Default parameters for the "<gallery>" tag
*/
$wgGalleryOptions = array (
'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize
@@ -979,7 +1077,10 @@ $wgThumbUpright = 0.75;
$wgDirectoryMode = 0777;
/**
- * DJVU settings
+ * @name DJVU settings
+ * @{
+ */
+/**
* Path of the djvudump executable
* Enable this and $wgDjvuRenderer to enable djvu rendering
*/
@@ -1004,15 +1105,18 @@ $wgDjvuTxt = null;
* Path of the djvutoxml executable
* This works like djvudump except much, much slower as of version 3.5.
*
- * For now I recommend you use djvudump instead. The djvuxml output is
+ * For now we recommend you use djvudump instead. The djvuxml output is
* probably more stable, so we'll switch back to it as soon as they fix
* the efficiency problem.
* http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583
+ *
+ * @par Example:
+ * @code
+ * $wgDjvuToXML = 'djvutoxml';
+ * @endcode
*/
-# $wgDjvuToXML = 'djvutoxml';
$wgDjvuToXML = null;
-
/**
* Shell command for the DJVU post processor
* Default: pnmtopng, since ddjvu generates ppm output
@@ -1023,6 +1127,7 @@ $wgDjvuPostProcessor = 'pnmtojpeg';
* File extension for the DJVU post processor output
*/
$wgDjvuOutputExtension = 'jpg';
+/** @} */ # end of DJvu }
/** @} */ # end of file uploads }
@@ -1099,17 +1204,21 @@ $wgNewPasswordExpiry = 3600 * 24 * 7;
$wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60;
/**
- * SMTP Mode
+ * SMTP Mode.
+ *
* For using a direct (authenticated) SMTP server connection.
* Default to false or fill an array :
- * <code>
- * "host" => 'SMTP domain',
- * "IDHost" => 'domain for MessageID',
- * "port" => "25",
- * "auth" => true/false,
- * "username" => user,
- * "password" => password
- * </code>
+ *
+ * @code
+ * $wgSMTP = array(
+ * 'host' => 'SMTP domain',
+ * 'IDHost' => 'domain for MessageID',
+ * 'port' => '25',
+ * 'auth' => [true|false],
+ * 'username' => [SMTP username],
+ * 'password' => [SMTP password],
+ * );
+ * @endcode
*/
$wgSMTP = false;
@@ -1131,9 +1240,9 @@ $wgEnotifFromEditor = false;
# It call this to be a "user-preferences-option (UPO)"
/**
- * Require email authentication before sending mail to an email addres. This is
- * highly recommended. It prevents MediaWiki from being used as an open spam
- * relay.
+ * Require email authentication before sending mail to an email address.
+ * This is highly recommended. It prevents MediaWiki from being used as an open
+ * spam relay.
*/
$wgEmailAuthentication = true;
@@ -1211,6 +1320,10 @@ $wgDBuser = 'wikiuser';
$wgDBpassword = '';
/** Database type */
$wgDBtype = 'mysql';
+/** Whether to use SSL in DB connection. */
+$wgDBssl = false;
+/** Whether to use compression in DB connection. */
+$wgDBcompress = false;
/** Separate username for maintenance tasks. Leave as null to use the default. */
$wgDBadminuser = null;
@@ -1295,6 +1408,9 @@ $wgSharedTables = array( 'user', 'user_properties' );
* - 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
*
* - max lag: (optional) Maximum replication lag before a slave will taken out of rotation
* - max threads: (optional) Maximum number of running threads
@@ -1311,9 +1427,9 @@ $wgSharedTables = array( 'user', 'user_properties' );
* accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your
* slaves in my.cnf. You can set read_only mode at runtime using:
*
- * <code>
+ * @code
* SET @@read_only=1;
- * </code>
+ * @endcode
*
* Since the effect of writing to a slave is so damaging and difficult to clean
* up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even
@@ -1339,23 +1455,41 @@ $wgMasterWaitTimeout = 10;
/** 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:
+ * http://php.net/manual/en/timezones.php
+ *
+ * @par Examples:
+ * @code
+ * $wgLocaltimezone = 'UTC';
+ * $wgLocaltimezone = 'GMT';
+ * $wgLocaltimezone = 'PST8PDT';
+ * $wgLocaltimezone = 'Europe/Sweden';
+ * $wgLocaltimezone = 'CET';
+ * @endcode
+ *
+ * @since 1.20
+ */
+$wgDBerrorLogTZ = false;
+
/** When to give an error message */
$wgDBClusterTimeout = 10;
/**
- * Scale load balancer polling time so that under overload conditions, the database server
- * receives a SHOW STATUS query at an average interval of this many microseconds
+ * Scale load balancer polling time so that under overload conditions, the
+ * database server receives a SHOW STATUS query at an average interval of this
+ * many microseconds
*/
$wgDBAvgStatusPoll = 2000;
-/** Set to true if using InnoDB tables */
-$wgDBtransactions = false;
-
/**
* Set to true to engage MySQL 4.1/5.0 charset-related features;
* for now will just cause sending of 'SET NAMES=utf8' on connect.
*
- * WARNING: THIS IS EXPERIMENTAL!
+ * @warning THIS IS EXPERIMENTAL!
*
* May break if you're not using the table defs from mysql5/tables.sql.
* May break if you're upgrading an existing wiki if set differently.
@@ -1408,19 +1542,30 @@ $wgCompressRevisions = false;
/**
* External stores allow including content
- * from non database sources following URL links
+ * from non database sources following URL links.
*
* Short names of ExternalStore classes may be specified in an array here:
+ * @code
* $wgExternalStores = array("http","file","custom")...
+ * @endcode
*
* CAUTION: Access to database might lead to code execution
*/
$wgExternalStores = false;
/**
- * An array of external mysql servers, e.g.
- * $wgExternalServers = array( 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) );
- * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to another class.
+ * An array of external MySQL servers.
+ *
+ * @par Example:
+ * Create a cluster named 'cluster1' containing three servers:
+ * @code
+ * $wgExternalServers = array(
+ * 'cluster1' => array( 'srv28', 'srv29', 'srv30' )
+ * );
+ * @endcode
+ *
+ * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to
+ * another class.
*/
$wgExternalServers = array();
@@ -1429,9 +1574,12 @@ $wgExternalServers = array();
* Part of a URL, e.g. DB://cluster1
*
* Can be an array instead of a single string, to enable data distribution. Keys
- * must be consecutive integers, starting at zero. Example:
+ * must be consecutive integers, starting at zero.
*
+ * @par Example:
+ * @code
* $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' );
+ * @endcode
*
* @var array
*/
@@ -1471,17 +1619,10 @@ $wgUseDumbLinkUpdate = false;
/**
* Anti-lock flags - bitfield
- * - ALF_PRELOAD_LINKS:
- * Preload links during link update for save
- * - ALF_PRELOAD_EXISTENCE:
- * Preload cur_id during replaceLinkHolders
* - ALF_NO_LINK_LOCK:
* Don't use locking reads when updating the link table. This is
* necessary for wikis with a high edit rate for performance
* reasons, but may cause link table inconsistency
- * - ALF_NO_BLOCK_LOCK:
- * As for ALF_LINK_LOCK, this flag is a necessity for high-traffic
- * wikis.
*/
$wgAntiLockFlags = 0;
@@ -1552,11 +1693,29 @@ $wgMessageCacheType = CACHE_ANYTHING;
$wgParserCacheType = CACHE_ANYTHING;
/**
+ * The cache type for storing session data. Used if $wgSessionsInObjectCache is true.
+ *
+ * For available types see $wgMainCacheType.
+ */
+$wgSessionCacheType = CACHE_ANYTHING;
+
+/**
+ * The cache type for storing language conversion tables,
+ * which are used when parsing certain text and interface messages.
+ *
+ * For available types see $wgMainCacheType.
+ *
+ * @since 1.20
+ */
+$wgLanguageConverterCacheType = CACHE_ANYTHING;
+
+/**
* Advanced object cache configuration.
*
* Use this to define the class names and constructor parameters which are used
* for the various cache types. Custom cache types may be defined here and
- * referenced from $wgMainCacheType, $wgMessageCacheType or $wgParserCacheType.
+ * referenced from $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType,
+ * or $wgLanguageConverterCacheType.
*
* The format is an associative array where the key is a cache identifier, and
* the value is an associative array of parameters. The "class" parameter is the
@@ -1580,28 +1739,44 @@ $wgObjectCaches = array(
'xcache' => array( 'class' => 'XCacheBagOStuff' ),
'wincache' => array( 'class' => 'WinCacheBagOStuff' ),
'memcached-php' => array( 'class' => 'MemcachedPhpBagOStuff' ),
+ 'memcached-pecl' => array( 'class' => 'MemcachedPeclBagOStuff' ),
'hash' => array( 'class' => 'HashBagOStuff' ),
);
/**
- * The expiry time for the parser cache, in seconds. The default is 86.4k
- * seconds, otherwise known as a day.
+ * The expiry time for the parser cache, in seconds.
+ * The default is 86400 (one day).
*/
$wgParserCacheExpireTime = 86400;
/**
- * Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php> to use as CACHE_DBA backend
+ * Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php>
+ * to use as CACHE_DBA backend.
*/
$wgDBAhandler = 'db3';
/**
- * Store sessions in MemCached. This can be useful to improve performance, or to
- * avoid the locking behaviour of PHP's default session handler, which tends to
- * prevent multiple requests for the same user from acting concurrently.
+ * Deprecated alias for $wgSessionsInObjectCache.
+ *
+ * @deprecated 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
+ * PHP's default session handler, which tends to prevent multiple requests for
+ * the same user from acting concurrently.
+ */
+$wgSessionsInObjectCache = false;
+
+/**
+ * The expiry time to use for session storage when $wgSessionsInObjectCache is
+ * enabled, in seconds.
+ */
+$wgObjectCacheSessionExpiry = 3600;
+
+/**
* This is used for setting php's session.save_handler. In practice, you will
* almost never need to change this ever. Other options might be 'user' or
* 'session_mysql.' Setting to null skips setting this entirely (which might be
@@ -1624,7 +1799,7 @@ $wgMemCachedPersistent = false;
/**
* Read/write timeout for MemCached server communication, in microseconds.
*/
-$wgMemCachedTimeout = 100000;
+$wgMemCachedTimeout = 500000;
/**
* Set this to true to make a local copy of the message cache, for use in
@@ -1633,9 +1808,9 @@ $wgMemCachedTimeout = 100000;
$wgUseLocalMessageCache = false;
/**
- * Defines format of local cache
- * true - Serialized object
- * false - PHP source file (Warning - security risk)
+ * Defines format of local cache.
+ * - true: Serialized object
+ * - false: PHP source file (Warning - security risk)
*/
$wgLocalMessageCacheSerialized = true;
@@ -1648,23 +1823,23 @@ $wgAdaptiveMessageCache = false;
/**
* Localisation cache configuration. Associative array with keys:
- * class: The class to use. May be overridden by extensions.
+ * class: The class to use. May be overridden by extensions.
*
- * store: The location to store cache data. May be 'files', 'db' or
- * 'detect'. If set to "files", data will be in CDB files. If set
- * to "db", data will be stored to the database. If set to
- * "detect", files will be used if $wgCacheDirectory is set,
- * otherwise the database will be used.
+ * store: The location to store cache data. May be 'files', 'db' or
+ * 'detect'. If set to "files", data will be in CDB files. If set
+ * to "db", data will be stored to the database. If set to
+ * "detect", files will be used if $wgCacheDirectory is set,
+ * otherwise the database will be used.
*
- * storeClass: The class name for the underlying storage. If set to a class
- * name, it overrides the "store" setting.
+ * storeClass: The class name for the underlying storage. If set to a class
+ * name, it overrides the "store" setting.
*
- * storeDirectory: If the store class puts its data in files, this is the
- * directory it will use. If this is false, $wgCacheDirectory
- * will be used.
+ * storeDirectory: If the store class puts its data in files, this is the
+ * directory it will use. If this is false, $wgCacheDirectory
+ * will be used.
*
- * manualRecache: Set this to true to disable cache updates on web requests.
- * Use maintenance/rebuildLocalisationCache.php instead.
+ * manualRecache: Set this to true to disable cache updates on web requests.
+ * Use maintenance/rebuildLocalisationCache.php instead.
*/
$wgLocalisationCacheConf = array(
'class' => 'LocalisationCache',
@@ -1679,14 +1854,17 @@ $wgCachePages = true;
/**
* Set this to current time to invalidate all prior cached pages. Affects both
- * client- and server-side caching.
+ * client-side and server-side caching.
* You can get the current date on your server by using the command:
+ * @verbatim
* date +%Y%m%d%H%M%S
+ * @endverbatim
*/
$wgCacheEpoch = '20030516000000';
/**
* Bump this number when changing the global style sheets and JavaScript.
+ *
* It should be appended in the query string of static CSS and JS includes,
* to ensure that client-side caches do not keep obsolete copies of global
* styles.
@@ -1703,12 +1881,6 @@ $wgStyleVersion = '303';
$wgUseFileCache = false;
/**
- * Directory where the cached page will be saved.
- * Will default to "{$wgUploadDirectory}/cache" in Setup.php
- */
-$wgFileCacheDirectory = false;
-
-/**
* Depth of the subdirectory hierarchy to be created under
* $wgFileCacheDirectory. The subdirectories will be named based on
* the MD5 hash of the title. A value of 0 means all cache files will
@@ -1752,8 +1924,6 @@ $wgSidebarCacheExpiry = 86400;
/**
* When using the file cache, we can store the cached HTML gzipped to save disk
* space. Pages will then also be served compressed to clients that support it.
- * THIS IS NOT COMPATIBLE with ob_gzhandler which is now enabled if supported in
- * the default LocalSettings.php! If you enable this, remove that setting first.
*
* Requires zlib support enabled in PHP.
*/
@@ -1820,10 +1990,12 @@ $wgUseXVO = false;
$wgVaryOnXFP = false;
/**
- * Internal server name as known to Squid, if different. Example:
- * <code>
+ * Internal server name as known to Squid, if different.
+ *
+ * @par Example:
+ * @code
* $wgInternalServer = 'http://yourinternal.tld:8000';
- * </code>
+ * @endcode
*/
$wgInternalServer = false;
@@ -1860,22 +2032,61 @@ $wgSquidServersNoPurge = array();
$wgMaxSquidPurgeTitles = 400;
/**
+ * 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.
+ *
+ * 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.
+ *
+ * Example configuration to send purges for upload.wikimedia.org to one
+ * multicast group and all other purges to another:
+ * @code
+ * $wgHTCPMulticastRouting = array(
+ * '|^https?://upload\.wikimedia\.org|' => array(
+ * 'host' => '239.128.0.113',
+ * 'port' => 4827,
+ * ),
+ * '' => array(
+ * 'host' => '239.128.0.112',
+ * 'port' => 4827,
+ * ),
+ * );
+ * @endcode
+ *
+ * @since 1.20
+ *
+ * @see $wgHTCPMulticastTTL
+ */
+$wgHTCPMulticastRouting = array();
+
+/**
* HTCP multicast address. Set this to a multicast IP address to enable HTCP.
*
* 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.
+ *
+ * @deprecated in favor of $wgHTCPMulticastRouting
*/
$wgHTCPMulticastAddress = false;
/**
* HTCP multicast port.
+ * @deprecated in favor of $wgHTCPMulticastRouting
* @see $wgHTCPMulticastAddress
*/
$wgHTCPPort = 4827;
/**
* HTCP multicast TTL.
- * @see $wgHTCPMulticastAddress
+ * @see $wgHTCPMulticastRouting
*/
$wgHTCPMulticastTTL = 1;
@@ -1894,11 +2105,12 @@ $wgLanguageCode = 'en';
/**
* Some languages need different word forms, usually for different cases.
- * Used in Language::convertGrammar(). Example:
+ * Used in Language::convertGrammar().
*
- * <code>
+ * @par Example:
+ * @code
* $wgGrammarForms['en']['genitive']['car'] = 'car\'s';
- * </code>
+ * @endcode
*/
$wgGrammarForms = array();
@@ -1981,7 +2193,7 @@ $wgAllUnicodeFixes = false;
* converting a wiki from MediaWiki 1.4 or earlier to UTF-8 without the
* burdensome mass conversion of old text data.
*
- * NOTE! This DOES NOT touch any fields other than old_text.Titles, comments,
+ * @note This DOES NOT touch any fields other than old_text. Titles, comments,
* user names, etc still must be converted en masse in the database before
* continuing as a UTF-8 wiki.
*/
@@ -2090,28 +2302,27 @@ $wgCanonicalLanguageLinks = true;
$wgDefaultLanguageVariant = false;
/**
- * Disabled variants array of language variant conversion. Example:
- * <code>
+ * Disabled variants array of language variant conversion.
+ *
+ * @par Example:
+ * @code
* $wgDisabledVariants[] = 'zh-mo';
* $wgDisabledVariants[] = 'zh-my';
- * </code>
- *
- * or:
- *
- * <code>
- * $wgDisabledVariants = array('zh-mo', 'zh-my');
- * </code>
+ * @endcode
*/
$wgDisabledVariants = array();
/**
* Like $wgArticlePath, but on multi-variant wikis, this provides a
* path format that describes which parts of the URL contain the
- * language variant. For Example:
+ * language variant.
*
- * $wgLanguageCode = 'sr';
- * $wgVariantArticlePath = '/$2/$1';
- * $wgArticlePath = '/wiki/$1';
+ * @par Example:
+ * @code
+ * $wgLanguageCode = 'sr';
+ * $wgVariantArticlePath = '/$2/$1';
+ * $wgArticlePath = '/wiki/$1';
+ * @endcode
*
* A link to /wiki/ would be redirected to /sr/Главна_страна
*
@@ -2128,19 +2339,23 @@ $wgVariantArticlePath = false;
$wgLoginLanguageSelector = false;
/**
- * When translating messages with wfMsg(), it is not always clear what should
- * be considered UI messages and what should be content messages.
+ * When translating messages with wfMessage(), it is not always clear what
+ * should be considered UI messages and what should be content messages.
*
* For example, for the English Wikipedia, there should be only one 'mainpage',
* so when getting the link for 'mainpage', we should treat it as site content
- * and call wfMsgForContent(), but for rendering the text of the link, we call
- * wfMsg(). The code behaves this way by default. However, sites like the
- * Wikimedia Commons do offer different versions of 'mainpage' and the like for
- * different languages. This array provides a way to override the default
- * behavior. For example, to allow language-specific main page and community
- * portal, set
- *
- * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
+ * and call ->inContentLanguage()->text(), but for rendering the text of the
+ * link, we call ->text(). The code behaves this way by default. However,
+ * sites like the Wikimedia Commons do offer different versions of 'mainpage'
+ * and the like for different languages. This array provides a way to override
+ * the default behavior.
+ *
+ * @par Example:
+ * To allow language-specific main page and community
+ * portal:
+ * @code
+ * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
+ * @endcode
*/
$wgForceUIMsgAsContentMsg = array();
@@ -2155,13 +2370,17 @@ $wgForceUIMsgAsContentMsg = array();
* Timezones can be translated by editing MediaWiki messages of type
* timezone-nameinlowercase like timezone-utc.
*
- * Examples:
- * <code>
+ * A list of useable timezones can found at:
+ * http://php.net/manual/en/timezones.php
+ *
+ * @par Examples:
+ * @code
+ * $wgLocaltimezone = 'UTC';
* $wgLocaltimezone = 'GMT';
* $wgLocaltimezone = 'PST8PDT';
* $wgLocaltimezone = 'Europe/Sweden';
* $wgLocaltimezone = 'CET';
- * </code>
+ * @endcode
*/
$wgLocaltimezone = null;
@@ -2182,7 +2401,7 @@ $wgLocalTZoffset = null;
* language variant conversion is disabled in interface messages. Setting this
* to true re-enables it.
*
- * This variable should be removed (implicitly false) in 1.20 or earlier.
+ * @todo This variable should be removed (implicitly false) in 1.20 or earlier.
*/
$wgBug34832TransitionalRollback = true;
@@ -2255,11 +2474,6 @@ $wgAllowRdfaAttributes = false;
$wgAllowMicrodataAttributes = false;
/**
- * Cleanup as much presentational html like valign -> css vertical-align as we can
- */
-$wgCleanupPresentationalAttributes = true;
-
-/**
* Should we try to make our HTML output well-formed XML? If set to false,
* output will be a few bytes shorter, and the HTML will arguably be more
* readable. If set to true, life will be much easier for the authors of
@@ -2279,10 +2493,14 @@ $wgWellFormedXml = true;
/**
* Permit other namespaces in addition to the w3.org default.
- * Use the prefix for the key and the namespace for the value. For
- * example:
+ *
+ * Use the prefix for the key and the namespace for the value.
+ *
+ * @par Example:
+ * @code
* $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg';
- * Normally we wouldn't have to define this in the root <html>
+ * @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
@@ -2293,7 +2511,7 @@ $wgXhtmlNamespaces = array();
/**
* Show IP address, for non-logged in users. It's necessary to switch this off
* for some forms of caching.
- * Will disable file cache.
+ * @warning Will disable file cache.
*/
$wgShowIPinHeader = true;
@@ -2464,15 +2682,16 @@ $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> arround the text or icon, if not set an <a> will not be outputted
- * alt: This is the text form of the icon, it will be displayed without an image in
+ * - url: The url to use in the a element arround 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.
- * width and height: If the icon specified by src is not of the standard size
+ * - width and height: If the icon specified by src is not of the standard size
* you can specify the size of image to use with these keys.
* Otherwise they will default to the standard 88x31.
+ * @todo Reformat documentation.
*/
$wgFooterIcons = array(
"copyright" => array(
@@ -2488,23 +2707,24 @@ $wgFooterIcons = array(
);
/**
- * Login / create account link behavior when it's possible for anonymous users to create an account
- * true = use a combined login / create account link
- * false = split login and create account into two separate links
+ * Login / create account link behavior when it's possible for anonymous users
+ * to create an account.
+ * - true = use a combined login / create account link
+ * - false = split login and create account into two separate links
*/
-$wgUseCombinedLoginLink = true;
+$wgUseCombinedLoginLink = false;
/**
- * Search form behavior for Vector skin only
- * true = use an icon search button
- * false = use Go & Search buttons
+ * Search form look for Vector skin only.
+ * - true = use an icon search button
+ * - false = use Go & Search buttons
*/
$wgVectorUseSimpleSearch = false;
/**
- * Watch and unwatch as an icon rather than a link for Vector skin only
- * true = use an icon watch/unwatch button
- * false = use watch/unwatch text link
+ * 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;
@@ -2534,6 +2754,16 @@ $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.
+ *
+ * @since 1.20
+ */
+$wgShowRollbackEditCount = 10;
+
/** @} */ # End of output format settings }
/*************************************************************************//**
@@ -2542,17 +2772,21 @@ $wgSend404Code = true;
*/
/**
- * Client-side resource modules. Extensions should add their module definitions
- * here.
+ * Client-side resource modules.
+ *
+ * Extensions should add their resource loader module definitions
+ * to the $wgResourceModules variable.
*
- * Example:
+ * @par Example:
+ * @code
* $wgResourceModules['ext.myExtension'] = array(
* 'scripts' => 'myExtension.js',
* 'styles' => 'myExtension.css',
* 'dependencies' => array( 'jquery.cookie', 'jquery.tabIndex' ),
- * 'localBasePath' => dirname( __FILE__ ),
+ * 'localBasePath' => __DIR__,
* 'remoteExtPath' => 'MyExtension',
* );
+ * @endcode
*/
$wgResourceModules = array();
@@ -2561,22 +2795,26 @@ $wgResourceModules = array();
* built-in source that is not in this array, but defined by
* ResourceLoader::__construct() so that it cannot be unset.
*
- * Example:
+ * @par Example:
+ * @code
* $wgResourceLoaderSources['foo'] = array(
* 'loadScript' => 'http://example.org/w/load.php',
* 'apiScript' => 'http://example.org/w/api.php'
* );
+ * @endcode
*/
$wgResourceLoaderSources = array();
/**
- * Default 'remoteBasePath' value for resource loader modules.
+ * Default 'remoteBasePath' value for instances of ResourceLoaderFileModule.
* If not set, then $wgScriptPath will be used as a fallback.
*/
$wgResourceBasePath = null;
/**
- * Maximum time in seconds to cache resources served by the resource loader
+ * Maximum time in seconds to cache resources served by the resource loader.
+ *
+ * @todo Document array structure
*/
$wgResourceLoaderMaxage = array(
'versioned' => array(
@@ -2592,8 +2830,9 @@ $wgResourceLoaderMaxage = array(
);
/**
- * The default debug mode (on/off) for of ResourceLoader requests. This will still
- * be overridden when the debug URL parameter is used.
+ * The default debug mode (on/off) for of ResourceLoader requests.
+ *
+ * This will still be overridden when the debug URL parameter is used.
*/
$wgResourceLoaderDebug = false;
@@ -2619,33 +2858,54 @@ $wgResourceLoaderMinifierMaxLineLength = 1000;
/**
* Whether to include the mediawiki.legacy JS library (old wikibits.js), and its
- * dependencies
+ * dependencies.
*/
$wgIncludeLegacyJavaScript = true;
/**
- * Whether to preload the mediawiki.util module as blocking module in the top queue.
- * Before MediaWiki 1.19, modules used to load slower/less asynchronous which allowed
- * modules to lack dependencies on 'popular' modules that were likely loaded already.
+ * Whether to preload the mediawiki.util module as blocking module in the top
+ * queue.
+ *
+ * Before MediaWiki 1.19, modules used to load slower/less asynchronous which
+ * allowed modules to lack dependencies on 'popular' modules that were likely
+ * loaded already.
+ *
* This setting is to aid scripts during migration by providing mediawiki.util
* unconditionally (which was the most commonly missed dependency).
- * It doesn't cover all missing dependencies obviously but should fix most of them.
+ * It doesn't cover all missing dependencies obviously but should fix most of
+ * them.
+ *
* This should be removed at some point after site/user scripts have been fixed.
- * Enable this if your wiki has a large amount of user/site scripts that are lacking
- * dependencies.
+ * Enable this if your wiki has a large amount of user/site scripts that are
+ * lacking dependencies.
+ * @todo Deprecate
*/
$wgPreloadJavaScriptMwUtil = false;
/**
- * Whether or not to assing configuration variables to the global window object.
- * If this is set to false, old code using deprecated variables like:
- * " if ( window.wgRestrictionEdit ) ..."
+ * Whether or not to assign configuration variables to the global window object.
+ *
+ * If this is set to false, old code using deprecated variables will no longer
+ * work.
+ *
+ * @par Example of legacy code:
+ * @code{,js}
+ * if ( window.wgRestrictionEdit ) { ... }
+ * @endcode
* or:
- * " if ( wgIsArticle ) ..."
- * will no longer work and needs to use mw.config instead. For example:
- * " if ( mw.config.exists('wgRestrictionEdit') )"
- * or
- * " if ( mw.config.get('wgIsArticle') )".
+ * @code{,js}
+ * if ( wgIsArticle ) { ... }
+ * @endcode
+ *
+ * Instead, one needs to use mw.config.
+ * @par Example using mw.config global configuration:
+ * @code{,js}
+ * if ( mw.config.exists('wgRestrictionEdit') ) { ... }
+ * @endcode
+ * or:
+ * @code{,js}
+ * if ( mw.config.get('wgIsArticle') ) { ... }
+ * @endcode
*/
$wgLegacyJavaScriptGlobals = true;
@@ -2663,8 +2923,8 @@ $wgLegacyJavaScriptGlobals = true;
$wgResourceLoaderMaxQueryLength = -1;
/**
- * If set to true, JavaScript modules loaded from wiki pages will be parsed prior
- * to minification to validate it.
+ * If set to true, JavaScript modules loaded from wiki pages will be parsed
+ * prior to minification to validate it.
*
* Parse errors will result in a JS exception being thrown during module load,
* which avoids breaking other modules loaded in the same request.
@@ -2682,7 +2942,7 @@ $wgResourceLoaderValidateJS = true;
$wgResourceLoaderValidateStaticJS = false;
/**
- * If set to true, asynchronous loading of bottom-queue scripts in the <head>
+ * If set to true, asynchronous loading of bottom-queue scripts in the "<head>"
* will be enabled. This is an experimental feature that's supposed to make
* JavaScript load faster.
*/
@@ -2718,19 +2978,25 @@ $wgMetaNamespaceTalk = false;
* names of existing namespaces. Extensions developers should use
* $wgCanonicalNamespaceNames.
*
- * PLEASE NOTE: Once you delete a namespace, the pages in that namespace will
+ * @warning Once you delete a namespace, the pages in that namespace will
* no longer be accessible. If you rename it, then you can access them through
* the new namespace name.
*
* Custom namespaces should start at 100 to avoid conflicting with standard
* namespaces, and should always follow the even/odd main/talk pattern.
+ *
+ * @par Example:
+ * @code
+ * $wgExtraNamespaces = array(
+ * 100 => "Hilfe",
+ * 101 => "Hilfe_Diskussion",
+ * 102 => "Aide",
+ * 103 => "Discussion_Aide"
+ * );
+ * @endcode
+ *
+ * @todo Add a note about maintenance/namespaceDupes.php
*/
-# $wgExtraNamespaces = array(
-# 100 => "Hilfe",
-# 101 => "Hilfe_Diskussion",
-# 102 => "Aide",
-# 103 => "Discussion_Aide"
-# );
$wgExtraNamespaces = array();
/**
@@ -2742,18 +3008,22 @@ $wgExtraNamespaces = array();
$wgExtraGenderNamespaces = array();
/**
- * Namespace aliases
+ * Namespace aliases.
+ *
* These are alternate names for the primary localised namespace names, which
* are defined by $wgExtraNamespaces and the language file. If a page is
* requested with such a prefix, the request will be redirected to the primary
* name.
*
* Set this to a map from namespace names to IDs.
- * Example:
+ *
+ * @par Example:
+ * @code
* $wgNamespaceAliases = array(
* 'Wikipedian' => NS_USER,
* 'Help' => 100,
* );
+ * @endcode
*/
$wgNamespaceAliases = array();
@@ -2768,8 +3038,8 @@ $wgNamespaceAliases = array();
* - + 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, instead of a
- * rewrite rule of either variety.
+ * All three of these punctuation problems can be avoided by using an alias,
+ * instead of a rewrite rule of either variety.
*
* The problem with % is that when using a path to query rewrite rule, URLs are
* double-unescaped: once by Apache's path conversion code, and again by PHP. So
@@ -2795,33 +3065,47 @@ $wgLocalInterwiki = false;
*/
$wgInterwikiExpiry = 10800;
-/** Interwiki caching settings.
- $wgInterwikiCache specifies path to constant database file
- This cdb database is generated by dumpInterwiki from maintenance
- and has such key formats:
- dbname:key - a simple key (e.g. enwiki:meta)
- _sitename:key - site-scope key (e.g. wiktionary:meta)
- __global:key - global-scope key (e.g. __global:meta)
- __sites:dbname - site mapping (e.g. __sites:enwiki)
- Sites mapping just specifies site name, other keys provide
- "local url" data layout.
- $wgInterwikiScopes specify number of domains to check for messages:
- 1 - Just wiki(db)-level
- 2 - wiki and global levels
- 3 - site levels
- $wgInterwikiFallbackSite - if unable to resolve from cache
+/**
+ * @name Interwiki caching settings.
+ * @{
+ */
+/**
+ *$wgInterwikiCache specifies path to constant database file.
+ *
+ * This cdb database is generated by dumpInterwiki from maintenance and has
+ * such key formats:
+ * - dbname:key - a simple key (e.g. enwiki:meta)
+ * - _sitename:key - site-scope key (e.g. wiktionary:meta)
+ * - __global:key - global-scope key (e.g. __global:meta)
+ * - __sites:dbname - site mapping (e.g. __sites:enwiki)
+ *
+ * Sites mapping just specifies site name, other keys provide "local url"
+ * 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
+ */
$wgInterwikiScopes = 3;
+/**
+ * $wgInterwikiFallbackSite - if unable to resolve from cache
+ */
$wgInterwikiFallbackSite = 'wiki';
+/** @} */ # end of Interwiki caching settings.
/**
* If local interwikis are set up which allow redirects,
* set this regexp to restrict URLs which will be displayed
* as 'redirected from' links.
*
+ * @par Example:
* It might look something like this:
+ * @code
* $wgRedirectSources = '!^https?://[a-z-]+\.wikipedia\.org/!';
+ * @endcode
*
* Leave at false to avoid displaying any incoming redirect markers.
* This does not affect intra-wiki redirects, which don't change
@@ -2831,7 +3115,8 @@ $wgRedirectSources = false;
/**
* Set this to false to avoid forcing the first letter of links to capitals.
- * WARNING: may break links! This makes links COMPLETELY case-sensitive. Links
+ *
+ * @warning may break links! This makes links COMPLETELY case-sensitive. Links
* appearing with a capital at the beginning of a sentence will *not* go to the
* same place as links in the middle of a sentence using a lowercase initial.
*/
@@ -2845,7 +3130,11 @@ $wgCapitalLinks = true;
* associated content namespaces, the values for those are ignored in favor of the
* subject namespace's setting. Setting for NS_MEDIA is taken automatically from
* NS_FILE.
- * EX: $wgCapitalLinkOverrides[ NS_FILE ] = false;
+ *
+ * @par Example:
+ * @code
+ * $wgCapitalLinkOverrides[ NS_FILE ] = false;
+ * @endcode
*/
$wgCapitalLinkOverrides = array();
@@ -2930,11 +3219,19 @@ $wgParserConf = array(
$wgMaxTocLevel = 999;
/**
- * A complexity limit on template expansion
+ * A complexity limit on template expansion: the maximum number of nodes visited
+ * by PPFrame::expand()
*/
$wgMaxPPNodeCount = 1000000;
/**
+ * A complexity limit on template expansion: the maximum number of nodes
+ * generated by Preprocessor::preprocessToObj()
+ */
+$wgMaxGeneratedPPNodeCount = 1000000;
+
+
+/**
* Maximum recursion depth for templates within templates.
* The current parser adds two levels to the PHP call stack for each template,
* and xdebug limits the call stack to 100 by default. So this should hopefully
@@ -2978,11 +3275,11 @@ $wgAllowExternalImages = false;
* You can use this to set up a trusted, simple repository of images.
* You may also specify an array of strings to allow multiple sites
*
- * Examples:
- * <code>
+ * @par Examples:
+ * @code
* $wgAllowExternalImagesFrom = 'http://127.0.0.1/';
* $wgAllowExternalImagesFrom = array( 'http://127.0.0.1/', 'http://example.com' );
- * </code>
+ * @endcode
*/
$wgAllowExternalImagesFrom = '';
@@ -2997,7 +3294,7 @@ $wgAllowExternalImagesFrom = '';
$wgEnableImageWhitelist = true;
/**
- * A different approach to the above: simply allow the <img> tag to be used.
+ * A different approach to the above: simply allow the "<img>" tag to be used.
* This allows you to specify alt text and other attributes, copy-paste HTML to
* your wiki more easily, etc. However, allowing external images in any manner
* will allow anyone with editing rights to snoop on your visitors' IP
@@ -3039,7 +3336,7 @@ $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
*/
@@ -3245,7 +3542,6 @@ $wgDefaultUserOptions = array(
'gender' => 'unknown',
'hideminor' => 0,
'hidepatrolled' => 0,
- 'highlightbroken' => 1,
'imagesize' => 2,
'justify' => 0,
'math' => 1,
@@ -3308,7 +3604,7 @@ $wgInvalidUsernameCharacters = '@';
/**
* Character used as a delimiter when testing for interwiki userrights
* (In Special:UserRights, it is possible to modify users on different
- * databases if the delimiter is used, e.g. Someuser@enwiki).
+ * databases if the delimiter is used, e.g. "Someuser@enwiki").
*
* It is recommended that you have this delimiter in
* $wgInvalidUsernameCharacters above, or you will not be able to
@@ -3405,12 +3701,19 @@ $wgSysopEmailBans = true;
* Limits on the possible sizes of range blocks.
*
* CIDR notation is hard to understand, it's easy to mistakenly assume that a
- * /1 is a small range and a /31 is a large range. Setting this to half the
- * number of bits avoids such errors.
+ * /1 is a small range and a /31 is a large range. For IPv4, setting a limit of
+ * half the number of bits avoids such errors, and allows entire ISPs to be
+ * blocked using a small number of range blocks.
+ *
+ * For IPv6, RFC 3177 recommends that a /48 be allocated to every residential
+ * customer, so range blocks larger than /64 (half the number of bits) will
+ * plainly be required. RFC 4692 implies that a very large ISP may be
+ * allocated a /19 if a generous HD-Ratio of 0.8 is used, so we will use that
+ * as our limit. As of 2012, blocking the whole world would require a /4 range.
*/
$wgBlockCIDRLimit = array(
'IPv4' => 16, # Blocks larger than a /16 (64k addresses) will not be allowed
- 'IPv6' => 64, # 2^64 = ~1.8x10^19 addresses
+ 'IPv6' => 19,
);
/**
@@ -3423,18 +3726,19 @@ $wgBlockCIDRLimit = array(
$wgBlockDisablesLogin = false;
/**
- * Pages anonymous user may see as an array, e.g.
+ * Pages anonymous user may see, set as an array of pages titles.
*
- * <code>
+ * @par Example:
+ * @code
* $wgWhitelistRead = array ( "Main Page", "Wikipedia:Help");
- * </code>
+ * @endcode
*
* Special:Userlogin and Special:ChangePassword are always whitelisted.
*
- * NOTE: This will only work if $wgGroupPermissions['*']['read'] is false --
+ * @note This will only work if $wgGroupPermissions['*']['read'] is false --
* see below. Otherwise, ALL pages are accessible, regardless of this setting.
*
- * Also note that this will only protect _pages in the wiki_. Uploaded files
+ * @note Also that this will only protect _pages in the wiki_. Uploaded files
* will remain readable. You can use img_auth.php to protect uploaded files,
* see http://www.mediawiki.org/wiki/Manual:Image_Authorization
*/
@@ -3448,6 +3752,7 @@ $wgEmailConfirmToEdit = false;
/**
* Permission keys given to users in each group.
+ *
* This is an array where the keys are all groups and each value is an
* array of the format (right => boolean).
*
@@ -3538,7 +3843,6 @@ $wgGroupPermissions['sysop']['reupload'] = true;
$wgGroupPermissions['sysop']['reupload-shared'] = true;
$wgGroupPermissions['sysop']['unwatchedpages'] = true;
$wgGroupPermissions['sysop']['autoconfirmed'] = true;
-$wgGroupPermissions['sysop']['upload_by_url'] = true;
$wgGroupPermissions['sysop']['ipblock-exempt'] = true;
$wgGroupPermissions['sysop']['blockemail'] = true;
$wgGroupPermissions['sysop']['markbotedits'] = true;
@@ -3548,6 +3852,7 @@ $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;
// Permission to change users' group assignments
@@ -3558,6 +3863,7 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = 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;
// To hide usernames from users and Sysops
#$wgGroupPermissions['suppress']['hideuser'] = true;
@@ -3578,6 +3884,7 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true;
/**
* Permission keys revoked from users in each group.
+ *
* This acts the same way as wgGroupPermissions above, except that
* if the user is in a group here, the permission will be removed from them.
*
@@ -3595,16 +3902,20 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' );
* A map of group names that the user is in, to group names that those users
* are allowed to add or revoke.
*
- * Setting the list of groups to add or revoke to true is equivalent to "any group".
- *
- * For example, to allow sysops to add themselves to the "bot" group:
+ * Setting the list of groups to add or revoke to true is equivalent to "any
+ * group".
*
+ * @par Example:
+ * To allow sysops to add themselves to the "bot" group:
+ * @code
* $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) );
+ * @endcode
*
+ * @par Example:
* Implicit groups may be used for the source group, for instance:
- *
+ * @code
* $wgGroupsRemoveFromSelf = array( '*' => true );
- *
+ * @endcode
* This allows users in the '*' group (i.e. any user) to remove themselves from
* any group that they happen to be in.
*
@@ -3640,13 +3951,16 @@ $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' );
* namespace. If you list more than one permission, a user must
* have all of them to edit pages in that namespace.
*
- * Note: NS_MEDIAWIKI is implicitly restricted to editinterface.
+ * @note NS_MEDIAWIKI is implicitly restricted to 'editinterface'.
*/
$wgNamespaceProtection = array();
/**
* Pages in namespaces in this array can not be used as templates.
- * Elements must be numeric namespace ids.
+ *
+ * Elements MUST be numeric namespace ids, you can safely use the MediaWiki
+ * 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.
*/
@@ -3662,11 +3976,15 @@ $wgNonincludableNamespaces = array();
*
* When left at 0, all registered accounts will pass.
*
- * Example:
- * <code>
+ * @par Example:
+ * Set automatic confirmation to 10 minutes (which is 600 seconds):
+ * @code
* $wgAutoConfirmAge = 600; // ten minutes
+ * @endcode
+ * Set age to one day:
+ * @code
* $wgAutoConfirmAge = 3600*24; // one day
- * </code>
+ * @endcode
*/
$wgAutoConfirmAge = 0;
@@ -3674,14 +3992,18 @@ $wgAutoConfirmAge = 0;
* Number of edits an account requires before it is autoconfirmed.
* Passing both this AND the time requirement is needed. Example:
*
- * <code>
+ * @par Example:
+ * @code
* $wgAutoConfirmCount = 50;
- * </code>
+ * @endcode
*/
$wgAutoConfirmCount = 0;
/**
* Automatically add a usergroup to any user who matches certain conditions.
+ *
+ * @todo Redocument $wgAutopromote
+ *
* The format is
* array( '&' or '|' or '^' or '!', cond1, cond2, ... )
* where cond1, cond2, ... are themselves conditions; *OR*
@@ -3709,14 +4031,19 @@ $wgAutopromote = array(
/**
* Automatically add a usergroup to any user who matches certain conditions.
+ *
* Does not add the user to the group again if it has been removed.
* Also, does not remove the group if the user no longer meets the criteria.
*
- * The format is
+ * The format is:
+ * @code
* array( event => criteria, ... )
- * where event is
- * 'onEdit' (when user edits) or 'onView' (when user views the wiki)
- * and criteria has the same format as $wgAutopromote
+ * @endcode
+ * Where event is either:
+ * - 'onEdit' (when user edits)
+ * - 'onView' (when user views the wiki)
+ *
+ * Criteria has the same format as $wgAutopromote
*
* @see $wgAutopromote
* @since 1.18
@@ -3734,16 +4061,23 @@ $wgAutopromoteOnceLogInRC = true;
/**
* $wgAddGroups and $wgRemoveGroups can be used to give finer control over who
- * can assign which groups at Special:Userrights. Example configuration:
+ * can assign which groups at Special:Userrights.
*
+ * @par Example:
+ * Bureaucrats can add any group:
* @code
- * // Bureaucrat can add any group
* $wgAddGroups['bureaucrat'] = true;
- * // Bureaucrats can only remove bots and sysops
+ * @endcode
+ * Bureaucrats can only remove bots and sysops:
+ * @code
* $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' );
- * // Sysops can make bots
+ * @endcode
+ * Sysops can make bots:
+ * @code
* $wgAddGroups['sysop'] = array( 'bot' );
- * // Sysops can disable other sysops in an emergency, and disable bots
+ * @endcode
+ * Sysops can disable other sysops in an emergency, and disable bots:
+ * @code
* $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' );
* @endcode
*/
@@ -3763,8 +4097,10 @@ $wgAvailableRights = array();
*/
$wgDeleteRevisionsLimit = 0;
-/** Number of accounts each IP address may create, 0 to disable.
- * Requires memcached */
+/**
+ * Number of accounts each IP address may create, 0 to disable.
+ *
+ * @warning Requires memcached */
$wgAccountCreationThrottle = 0;
/**
@@ -3774,8 +4110,9 @@ $wgAccountCreationThrottle = 0;
* There's no administrator override on-wiki, so be careful what you set. :)
* May be an array of regexes or a single string for backwards compatibility.
*
- * See http://en.wikipedia.org/wiki/Regular_expression
- * Note that each regex needs a beginning/end delimiter, eg: # or /
+ * @see http://en.wikipedia.org/wiki/Regular_expression
+ *
+ * @note Each regex needs a beginning/end delimiter, eg: # or /
*/
$wgSpamRegex = array();
@@ -3783,54 +4120,46 @@ $wgSpamRegex = array();
$wgSummarySpamRegex = array();
/**
- * Similarly you can get a function to do the job. The function will be given
- * the following args:
- * - a Title object for the article the edit is made on
- * - the text submitted in the textarea (wpTextbox1)
- * - the section number.
- * The return should be boolean indicating whether the edit matched some evilness:
- * - true : block it
- * - false : let it through
- *
- * @deprecated since 1.17 Use hooks. See SpamBlacklist extension.
- * @var $wgFilterCallback bool|string|Closure
- */
-$wgFilterCallback = false;
-
-/**
- * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies
+ * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open
+ * proxies
* @since 1.16
*/
$wgEnableDnsBlacklist = false;
/**
- * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for backward
- * compatibility
+ * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for
+ * backward compatibility.
*/
$wgEnableSorbs = false;
/**
- * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. This is an
- * array of either a URL or an array with the URL and a key (should the blacklist
- * require a key). For example:
+ * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true.
+ *
+ * This is an array of either a URL or an array with the URL and a key (should
+ * the blacklist require a key).
+ *
+ * @par Example:
* @code
* $wgDnsBlacklistUrls = array(
* // String containing URL
- * 'http.dnsbl.sorbs.net',
+ * 'http.dnsbl.sorbs.net.',
* // Array with URL and key, for services that require a key
- * array( 'dnsbl.httpbl.net', 'mykey' ),
+ * array( 'dnsbl.httpbl.net.', 'mykey' ),
* // Array with just the URL. While this works, it is recommended that you
* // just use a string as shown above
- * array( 'opm.tornevall.org' )
+ * array( 'opm.tornevall.org.' )
* );
* @endcode
+ *
+ * @note You should end the domain name with a . to avoid searching your
+ * eventual domain search suffixes.
* @since 1.16
*/
$wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' );
/**
- * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for backward
- * compatibility
+ * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for
+ * backward compatibility.
*/
$wgSorbsUrl = array();
@@ -3841,13 +4170,24 @@ $wgSorbsUrl = array();
$wgProxyWhitelist = array();
/**
- * Simple rate limiter options to brake edit floods. Maximum number actions
- * allowed in the given number of seconds; after that the violating client re-
- * ceives HTTP 500 error pages until the period elapses.
+ * Simple rate limiter options to brake edit floods.
+ *
+ * Maximum number actions allowed in the given number of seconds; after that
+ * the violating client receives HTTP 500 error pages until the period
+ * elapses.
+ *
+ * @par Example:
+ * To set a generic maximum of 4 hits in 60 seconds:
+ * @code
+ * $wgRateLimits = array( 4, 60 );
+ * @endcode
*
- * array( 4, 60 ) for a maximum of 4 hits in 60 seconds.
+ * You could also limit per action and then type of users. See the inline
+ * code for a template to use.
*
- * This option set is experimental and likely to change. Requires memcached.
+ * This option set is experimental and likely to change.
+ *
+ * @warning Requires memcached.
*/
$wgRateLimits = array(
'edit' => array(
@@ -3896,7 +4236,8 @@ $wgQueryPageDefaultLimit = 50;
/**
* Limit password attempts to X attempts per Y seconds per IP per account.
- * Requires memcached.
+ *
+ * @warning Requires memcached.
*/
$wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
@@ -3911,10 +4252,10 @@ $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
* If you enable this, every editor's IP address will be scanned for open HTTP
* proxies.
*
- * Don't enable this. Many sysops will report "hostile TCP port scans" to your
- * ISP and ask for your server to be shut down.
- *
+ * @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.
+ *
*/
$wgBlockOpenProxies = false;
/** Port we want to scan for a proxy */
@@ -4048,22 +4389,29 @@ $wgDebugRedirects = false;
/**
* If true, log debugging data from action=raw and load.php.
- * This is normally false to avoid overlapping debug entries due to gen=css and
- * gen=js requests.
+ * This is normally false to avoid overlapping debug entries due to gen=css
+ * and gen=js requests.
*/
$wgDebugRawPage = false;
/**
* Send debug data to an HTML comment in the output.
*
- * This may occasionally be useful when supporting a non-technical end-user. It's
- * more secure than exposing the debug log file to the web, since the output only
- * contains private data for the current user. But it's not ideal for development
- * use since data is lost on fatal errors and redirects.
+ * This may occasionally be useful when supporting a non-technical end-user.
+ * It's more secure than exposing the debug log file to the web, since the
+ * output only contains private data for the current user. But it's not ideal
+ * for development use since data is lost on fatal errors and redirects.
*/
$wgDebugComments = false;
/**
+ * Extensive database transaction state debugging
+ *
+ * @since 1.20
+ */
+$wgDebugDBTransactions = false;
+
+/**
* Write SQL queries to the debug log
*/
$wgDebugDumpSql = false;
@@ -4120,11 +4468,23 @@ $wgShowExceptionDetails = false;
$wgShowDBErrorBacktrace = false;
/**
+ * If true, send the exception backtrace to the error log
+ */
+$wgLogExceptionBacktrace = true;
+
+/**
* Expose backend server host names through the API and various HTML comments
*/
$wgShowHostnames = false;
/**
+ * Override server hostname detection with a hardcoded value.
+ * Should be a string, default false.
+ * @since 1.20
+ */
+$wgOverrideHostname = false;
+
+/**
* If set to true MediaWiki will throw notices for some possible error
* conditions and for deprecated functions.
*/
@@ -4135,7 +4495,7 @@ $wgDevelopmentWarnings = false;
* development warnings will not be generated for deprecations added in releases
* after the limit.
*/
-$wgDeprecationReleaseLimit = '1.17';
+$wgDeprecationReleaseLimit = false;
/** Only record profiling info for pages that took longer than this */
$wgProfileLimit = 0.0;
@@ -4201,6 +4561,14 @@ $wgAggregateStatsID = false;
$wgDisableCounters = false;
/**
+ * 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.
+ * If set to -1, updates *only* go to $wgMemc (useful for daemons).
+ */
+$wgSiteStatsAsyncFactor = false;
+
+/**
* Parser test suite files to be run by parserTests.php when no specific
* filename is passed to it.
*
@@ -4228,7 +4596,7 @@ $wgParserTestFiles = array(
* );
*/
$wgParserTestRemote = false;
-
+
/**
* Allow running of javascript test suites via [[Special:JavaScriptTest]] (such as QUnit).
*/
@@ -4239,7 +4607,17 @@ $wgEnableJavaScriptTest = false;
*/
$wgJavaScriptTestConfig = array(
'qunit' => array(
+ // Page where documentation can be found relevant to the QUnit test suite being ran.
+ // Used in the intro paragraph on [[Special:JavaScriptTest/qunit]] for the
+ // documentation link in the "javascripttest-qunit-intro" message.
'documentation' => '//www.mediawiki.org/wiki/Manual:JavaScript_unit_testing',
+ // If you are submitting the QUnit test suite to a TestSwarm instance,
+ // point this to the "inject.js" script of that instance. This is was registers
+ // the QUnit hooks to extract the test results and push them back up into the
+ // TestSwarm database.
+ // @example 'http://localhost/testswarm/js/inject.js'
+ // @example '//integration.mediawiki.org/testswarm/js/inject.js'
+ 'testswarm-injectjs' => false,
),
);
@@ -4307,16 +4685,10 @@ $wgCountTotalSearchHits = false;
$wgOpenSearchTemplate = false;
/**
- * Enable suggestions while typing in search boxes
- * (results are passed around in OpenSearch format)
- * Requires $wgEnableOpenSearchSuggest = true;
- */
-$wgEnableMWSuggest = false;
-
-/**
* Enable OpenSearch suggestions requested by MediaWiki. Set this to
- * false if you've disabled MWSuggest or another suggestion script and
- * want reduce load caused by cached scripts pulling suggestions.
+ * false if you've disabled scripts that use api?action=opensearch and
+ * want reduce load caused by cached scripts still pulling suggestions.
+ * It will let the API fallback by responding with an empty array.
*/
$wgEnableOpenSearchSuggest = true;
@@ -4326,26 +4698,19 @@ $wgEnableOpenSearchSuggest = true;
$wgSearchSuggestCacheExpiry = 1200;
/**
- * Template for internal MediaWiki suggestion engine, defaults to API action=opensearch
- *
- * Placeholders: {searchTerms}, {namespaces}, {dbname}
- *
- */
-$wgMWSuggestTemplate = false;
-
-/**
* If you've disabled search semi-permanently, this also disables updates to the
* table. If you ever re-enable, be sure to rebuild the search table.
*/
$wgDisableSearchUpdate = false;
/**
- * List of namespaces which are searched by default. Example:
+ * List of namespaces which are searched by default.
*
- * <code>
+ * @par Example:
+ * @code
* $wgNamespacesToBeSearchedDefault[NS_MAIN] = true;
* $wgNamespacesToBeSearchedDefault[NS_PROJECT] = true;
- * </code>
+ * @endcode
*/
$wgNamespacesToBeSearchedDefault = array(
NS_MAIN => true,
@@ -4353,9 +4718,9 @@ $wgNamespacesToBeSearchedDefault = array(
/**
* Namespaces to be searched when user clicks the "Help" tab
- * on Special:Search
+ * on Special:Search.
*
- * Same format as $wgNamespacesToBeSearchedDefault
+ * Same format as $wgNamespacesToBeSearchedDefault.
*/
$wgNamespacesToBeSearchedHelp = array(
NS_PROJECT => true,
@@ -4363,8 +4728,10 @@ $wgNamespacesToBeSearchedHelp = array(
);
/**
- * If set to true the 'searcheverything' preference will be effective only for logged-in users.
- * Useful for big wikis to maintain different search profiles for anonymous and logged-in users.
+ * If set to true the 'searcheverything' preference will be effective only for
+ * logged-in users.
+ * Useful for big wikis to maintain different search profiles for anonymous and
+ * logged-in users.
*
*/
$wgSearchEverythingOnlyLoggedIn = false;
@@ -4380,18 +4747,22 @@ $wgDisableInternalSearch = false;
* If the URL includes '$1', this will be replaced with the URL-encoded
* search term.
*
- * For example, to forward to Google you'd have something like:
- * $wgSearchForwardUrl = 'http://www.google.com/search?q=$1' .
- * '&domains=http://example.com' .
- * '&sitesearch=http://example.com' .
- * '&ie=utf-8&oe=utf-8';
+ * @par Example:
+ * 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';
+ * @endcode
*/
$wgSearchForwardUrl = null;
/**
- * Search form behavior
- * true = use Go & Search buttons
- * false = use Go button & Advanced search link
+ * Search form behavior.
+ * - true = use Go & Search buttons
+ * - false = use Go button & Advanced search link
*/
$wgUseTwoButtonsSearchForm = true;
@@ -4408,11 +4779,13 @@ $wgSitemapNamespaces = false;
* maintenance/generateSitemap.php script.
*
* This should be a map of namespace IDs to priority
- * Example:
+ * @par Example:
+ * @code
* $wgSitemapNamespacesPriorities = array(
* NS_USER => '0.9',
* NS_HELP => '0.0',
* );
+ * @endcode
*/
$wgSitemapNamespacesPriorities = false;
@@ -4530,6 +4903,22 @@ $wgReadOnlyFile = false;
*/
$wgUpgradeKey = false;
+/**
+ * Map GIT repository URLs to viewer URLs to provide links in Special:Version
+ *
+ * Key is a pattern passed to preg_match() and preg_replace(),
+ * without the delimiters (which are #) and must match the whole URL.
+ * 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.
+ *
+ * @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',
+);
+
/** @} */ # End of maintenance }
/************************************************************************//**
@@ -4627,18 +5016,23 @@ $wgFeedDiffCutoff = 32768;
/** Override the site's default RSS/ATOM feed for recentchanges that appears on
* every page. Some sites might have a different feed they'd like to promote
* instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one).
- * Ex: $wgSiteFeed['format'] = "http://example.com/somefeed.xml"; Format can be one
- * of either 'rss' or 'atom'.
+ * Should be a format as key (either 'rss' or 'atom') and an URL to the feed
+ * as value.
+ * @par Example:
+ * Configure the 'atom' feed to http://example.com/somefeed.xml
+ * @code
+ * $wgSiteFeed['atom'] = "http://example.com/somefeed.xml";
+ * @endcode
*/
$wgOverrideSiteFeed = array();
/**
- * Available feeds objects
+ * Available feeds objects.
* Should probably only be defined when a page is syndicated ie when
- * $wgOut->isSyndicated() is true
+ * $wgOut->isSyndicated() is true.
*/
$wgFeedClasses = array(
- 'rss' => 'RSSFeed',
+ 'rss' => 'RSSFeed',
'atom' => 'AtomFeed',
);
@@ -4797,9 +5191,9 @@ $wgExportAllowListContributors = false;
* can become *insanely large* and could easily break your wiki,
* it's disabled by default for now.
*
- * There's a HARD CODED limit of 5 levels of recursion to prevent a
- * crazy-big export from being done by someone setting the depth
- * number too high. In other words, last resort safety net.
+ * @warning There's a HARD CODED limit of 5 levels of recursion to prevent a
+ * crazy-big export from being done by someone setting the depth number too
+ * high. In other words, last resort safety net.
*/
$wgExportMaxLinkDepth = 0;
@@ -4821,7 +5215,8 @@ $wgExportAllowAll = false;
*/
/**
- * A list of callback functions which are called once MediaWiki is fully initialised
+ * A list of callback functions which are called once MediaWiki is fully
+ * initialised
*/
$wgExtensionFunctions = array();
@@ -4836,9 +5231,10 @@ $wgExtensionFunctions = array();
* Variables defined in extensions will override conflicting variables defined
* in the core.
*
- * Example:
- * $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php';
- *
+ * @par Example:
+ * @code
+ * $wgExtensionMessagesFiles['ConfirmEdit'] = __DIR__.'/ConfirmEdit.i18n.php';
+ * @endcode
*/
$wgExtensionMessagesFiles = array();
@@ -4852,7 +5248,9 @@ $wgExtensionMessagesFiles = array();
* Registration is done with $pout->addOutputHook( $tag, $data ).
*
* The callback has the form:
+ * @code
* function outputHook( $outputPage, $parserOutput, $data ) { ... }
+ * @endcode
*/
$wgParserOutputHooks = array();
@@ -4883,7 +5281,7 @@ $wgAutoloadClasses = array();
* urls, descriptions and pointers to localized description msgs. Note that
* the version, url, description and descriptionmsg key can be omitted.
*
- * <code>
+ * @code
* $wgExtensionCredits[$type][] = array(
* 'name' => 'Example extension',
* 'version' => 1.9,
@@ -4893,7 +5291,7 @@ $wgAutoloadClasses = array();
* 'description' => 'An example extension',
* 'descriptionmsg' => 'exampleextension-desc',
* );
- * </code>
+ * @endcode
*
* Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'.
* Where 'descriptionmsg' can be an array with message key and parameters:
@@ -4909,12 +5307,30 @@ $wgAuth = null;
/**
* Global list of hooks.
- * Add a hook by doing:
+ *
+ * The key is one of the events made available by MediaWiki, you can find
+ * a description for most of them in docs/hooks.txt. The array is used
+ * internally by Hook:run().
+ *
+ * The value can be one of:
+ *
+ * - A function name:
+ * @code
* $wgHooks['event_name'][] = $function;
- * or:
+ * @endcode
+ * - A function with some data:
+ * @code
* $wgHooks['event_name'][] = array($function, $data);
- * or:
+ * @endcode
+ * - A an object method:
+ * @code
* $wgHooks['event_name'][] = array($object, 'method');
+ * @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?
*/
$wgHooks = array();
@@ -5068,17 +5484,19 @@ $wgLogRestrictions = array(
*
* See $wgLogTypes for a list of available log types.
*
- * For example:
+ * @par Example:
+ * @code
* $wgFilterLogTypes => array(
* 'move' => true,
* 'import' => false,
* );
+ * @endcode
*
* Will display show/hide links for the move and import logs. Move logs will be
* hidden by default unless the link is clicked. Import logs will be shown by
* default, and hidden when the link is clicked.
*
- * A message of the form log-show-hide-<type> should be added, and will be used
+ * A message of the form log-show-hide-[type] should be added, and will be used
* for the link text.
*/
$wgFilterLogTypes = array(
@@ -5091,7 +5509,7 @@ $wgFilterLogTypes = array(
*
* Extensions with custom log types may add to this array.
*
- * Since 1.19, if you follow the naming convention log-name-TYPE,
+ * @since 1.19, if you follow the naming convention log-name-TYPE,
* where TYPE is your log type, yoy don't need to use this array.
*/
$wgLogNames = array(
@@ -5114,7 +5532,7 @@ $wgLogNames = array(
*
* Extensions with custom log types may add to this array.
*
- * Since 1.19, if you follow the naming convention log-description-TYPE,
+ * @since 1.19, if you follow the naming convention log-description-TYPE,
* where TYPE is your log type, yoy don't need to use this array.
*/
$wgLogHeaders = array(
@@ -5164,10 +5582,12 @@ $wgLogActions = array(
* @see LogFormatter
*/
$wgLogActionsHandlers = array(
- // move, move_redir
- 'move/*' => 'MoveLogFormatter',
- // delete, restore, revision, event
- 'delete/*' => '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',
@@ -5266,6 +5686,7 @@ $wgSpecialPageGroups = array(
'Mostlinkedtemplates' => 'highuse',
'Mostcategories' => 'highuse',
'Mostimages' => 'highuse',
+ 'Mostinterwikis' => 'highuse',
'Mostrevisions' => 'highuse',
'Allpages' => 'pages',
@@ -5328,7 +5749,7 @@ $wgMaxRedirectLinksRetrieved = 500;
*/
/**
- * Array of allowed values for the title=foo&action=<action> parameter. Syntax is:
+ * Array of allowed values for the "title=foo&action=<action>" parameter. Syntax is:
* 'foo' => 'ClassName' Load the specified class which subclasses Action
* 'foo' => true Load the class FooAction which subclasses Action
* If something is specified in the getActionOverrides()
@@ -5364,11 +5785,6 @@ $wgActions = array(
*/
$wgDisabledActions = array();
-/**
- * Allow the "info" action, very inefficient at the moment
- */
-$wgAllowPageInfo = false;
-
/** @} */ # end actions }
/*************************************************************************//**
@@ -5393,8 +5809,10 @@ $wgDefaultRobotPolicy = 'index,follow';
* URLs, so search engine spiders risk getting lost in a maze of twisty special
* pages, all alike, and never reaching your actual content.
*
- * Example:
+ * @par Example:
+ * @code
* $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' );
+ * @endcode
*/
$wgNamespaceRobotPolicies = array();
@@ -5402,10 +5820,18 @@ $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.
- * Example:
- * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex,follow',
- * 'User:Bob' => 'index,follow' );
- * Example that DOES NOT WORK because the names are not canonical text forms:
+ *
+ * @par Example:
+ * @code
+ * $wgArticleRobotPolicies = array(
+ * 'Main Page' => 'noindex,follow',
+ * 'User:Bob' => 'index,follow',
+ * );
+ * @endcode
+ *
+ * @par Example that DOES NOT WORK because the names are not canonical text
+ * forms:
+ * @code
* $wgArticleRobotPolicies = array(
* # Underscore, not space!
* 'Main_Page' => 'noindex,follow',
@@ -5414,6 +5840,7 @@ $wgNamespaceRobotPolicies = array();
* # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false for that namespace)!
* 'abc' => 'noindex,nofollow'
* );
+ * @endcode
*/
$wgArticleRobotPolicies = array();
@@ -5421,8 +5848,11 @@ $wgArticleRobotPolicies = array();
* An array of namespace keys in which the __INDEX__/__NOINDEX__ magic words
* will not function, so users can't decide whether pages in that namespace are
* indexed by search engines. If set to null, default to $wgContentNamespaces.
- * Example:
+ *
+ * @par Example:
+ * @code
* $wgExemptFromUserRobotsControl = array( NS_MAIN, NS_TALK, NS_PROJECT );
+ * @endcode
*/
$wgExemptFromUserRobotsControl = null;
@@ -5452,9 +5882,10 @@ $wgEnableAPI = true;
$wgEnableWriteAPI = true;
/**
- * API module extensions
+ * API module extensions.
* Associative array mapping module name to class name.
* Extension modules may override the core modules.
+ * @todo Describe each of the variables, group them and add examples
*/
$wgAPIModules = array();
$wgAPIMetaModules = array();
@@ -5469,7 +5900,7 @@ $wgAPIMaxDBRows = 5000;
/**
* The maximum size (in bytes) of an API result.
- * Don't set this lower than $wgMaxArticleSize*1024
+ * @warning Do not set this lower than $wgMaxArticleSize*1024
*/
$wgAPIMaxResultSize = 8388608;
@@ -5524,17 +5955,18 @@ $wgAjaxLicensePreview = true;
* This is currently only used by the API (requests to api.php)
* $wgCrossSiteAJAXdomains can be set using a wildcard syntax:
*
- * '*' matches any number of characters
- * '?' matches any 1 character
- *
- * Example:
- $wgCrossSiteAJAXdomains = array(
- 'www.mediawiki.org',
- '*.wikipedia.org',
- '*.wikimedia.org',
- '*.wiktionary.org',
- );
+ * - '*' matches any number of characters
+ * - '?' matches any 1 character
*
+ * @par Example:
+ * @code
+ * $wgCrossSiteAJAXdomains = array(
+ * 'www.mediawiki.org',
+ * '*.wikipedia.org',
+ * '*.wikimedia.org',
+ * '*.wiktionary.org',
+ * );
+ * @endcode
*/
$wgCrossSiteAJAXdomains = array();
@@ -5638,7 +6070,7 @@ $wgUpdateRowsPerQuery = 100;
/**
* The build directory for HipHop compilation.
- * Defaults to $IP/maintenance/hiphop/build.
+ * Defaults to '$IP/maintenance/hiphop/build'.
*/
$wgHipHopBuildDirectory = false;
@@ -5658,8 +6090,9 @@ $wgHipHopCompilerProcs = 'detect';
*
* To compile extensions with HipHop, set $wgExtensionsDirectory correctly,
* and use code like:
- *
+ * @code
* require( MWInit::extensionSetupPath( 'Extension/Extension.php' ) );
+ * @endcode
*
* to include the extension setup file from LocalSettings.php. It is not
* necessary to set this variable unless you use MWInit::extensionSetupPath().
@@ -5682,6 +6115,19 @@ $wgCompiledFiles = array();
/************************************************************************//**
+ * @name Mobile support
+ * @{
+ */
+
+/**
+ * Name of the class used for mobile device detection, must be inherited from
+ * IDeviceDetector.
+ */
+$wgDeviceDetectionClass = 'DeviceDetection';
+
+/** @} */ # End of Mobile support }
+
+/************************************************************************//**
* @name Miscellaneous
* @{
*/
@@ -5691,9 +6137,11 @@ $wgExternalDiffEngine = false;
/**
* Disable redirects to special pages and interwiki redirects, which use a 302
- * and have no "redirected from" link. Note this is only for articles with #Redirect
- * in them. URL's containing a local interwiki prefix (or a non-canonical special
- * page name) are still hard redirected regardless of this setting.
+ * and have no "redirected from" link.
+ *
+ * @note This is only for articles with #REDIRECT in them. URL's containing a
+ * local interwiki prefix (or a non-canonical special page name) are still hard
+ * redirected regardless of this setting.
*/
$wgDisableHardRedirects = false;
@@ -5704,8 +6152,8 @@ $wgDisableHardRedirects = false;
$wgLinkHolderBatchSize = 1000;
/**
- * By default MediaWiki does not register links pointing to same server in externallinks dataset,
- * use this value to override:
+ * By default MediaWiki does not register links pointing to same server in
+ * externallinks dataset, use this value to override:
*/
$wgRegisterInternalExternals = false;
@@ -5733,8 +6181,10 @@ $wgRedirectOnLogin = null;
* This configuration array maps pool types to an associative array. The only
* defined key in the associative array is "class", which gives the class name.
* The remaining elements are passed through to the class as constructor
- * parameters. Example:
+ * parameters.
*
+ * @par Example:
+ * @code
* $wgPoolCounterConf = array( 'ArticleView' => array(
* 'class' => 'PoolCounter_Client',
* 'timeout' => 15, // wait timeout in seconds
@@ -5742,6 +6192,7 @@ $wgRedirectOnLogin = null;
* 'maxqueue' => 50, // maximum number of total threads in each pool
* ... any extension-specific options...
* );
+ * @endcode
*/
$wgPoolCounterConf = null;
@@ -5760,6 +6211,13 @@ $wgDBtestuser = ''; //db user that has permission to create and drop the test da
$wgDBtestpassword = '';
/**
+ * Whether the user must enter their password to change their e-mail address
+ *
+ * @since 1.20
+ */
+$wgRequirePasswordforEmailChange = true;
+
+/**
* 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 262994e3..b4989a69 100644
--- a/includes/DeferredUpdates.php
+++ b/includes/DeferredUpdates.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Interface and manager for deferred updates.
+ *
+ * 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
+ */
+
+/**
* Interface that deferrable updates should implement. Basically required so we
* can validate input on DeferredUpdates::addUpdate()
*
@@ -67,10 +88,19 @@ class DeferredUpdates {
}
foreach ( $updates as $update ) {
- $update->doUpdate();
+ try {
+ $update->doUpdate();
- if ( $doCommit && $dbw->trxLevel() ) {
- $dbw->commit( __METHOD__ );
+ if ( $doCommit && $dbw->trxLevel() ) {
+ $dbw->commit( __METHOD__ );
+ }
+ } 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() );
+ }
}
}
diff --git a/includes/Defines.php b/includes/Defines.php
index 26deb2ba..be9f9816 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -6,10 +6,29 @@
* since this file will not be executed during request startup for a compiled
* MediaWiki.
*
+ * 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 Constants MediaWiki constants
+ */
+
+/**
* Version constants for the benefit of extensions
*/
define( 'MW_SPECIALPAGE_VERSION', 2 );
@@ -25,6 +44,8 @@ define( 'DBO_DEFAULT', 16 );
define( 'DBO_PERSISTENT', 32 );
define( 'DBO_SYSDBA', 64 ); //for oracle maintenance
define( 'DBO_DDLMODE', 128 ); // when using schema files: mostly for Oracle
+define( 'DBO_SSL', 256 );
+define( 'DBO_COMPRESS', 512 );
/**@}*/
/**@{
@@ -125,8 +146,8 @@ define( 'AV_SCAN_FAILED', false ); #scan failed (scanner not found or error in
* Anti-lock flags
* See DefaultSettings.php for a description
*/
-define( 'ALF_PRELOAD_LINKS', 1 );
-define( 'ALF_PRELOAD_EXISTENCE', 2 );
+define( 'ALF_PRELOAD_LINKS', 1 ); // unused
+define( 'ALF_PRELOAD_EXISTENCE', 2 ); // unused
define( 'ALF_NO_LINK_LOCK', 4 );
define( 'ALF_NO_BLOCK_LOCK', 8 );
/**@}*/
@@ -184,7 +205,7 @@ define( 'LIST_SET_PREPARED', 8); // List of (?, ?, ?) for DatabaseIbm_db2
/**
* Unicode and normalisation related
*/
-require_once dirname(__FILE__).'/normal/UtfNormalDefines.php';
+require_once __DIR__.'/normal/UtfNormalDefines.php';
/**@{
* Hook support constants
diff --git a/includes/DeprecatedGlobal.php b/includes/DeprecatedGlobal.php
new file mode 100644
index 00000000..4d7b9689
--- /dev/null
+++ b/includes/DeprecatedGlobal.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Delayed loading of deprecated global 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
+ */
+
+/**
+ * Class to allow throwing wfDeprecated warnings
+ * when people use globals that we do not want them to.
+ * (For example like $wgArticle)
+ */
+
+class DeprecatedGlobal extends StubObject {
+ // The m's are to stay consistent with parent class.
+ protected $mRealValue, $mVersion;
+
+ function __construct( $name, $realValue, $version = false ) {
+ parent::__construct( $name );
+ $this->mRealValue = $realValue;
+ $this->mVersion = $version;
+ }
+
+ function _newObject() {
+ /* Put the caller offset for wfDeprecated as 6, as
+ * that gives the function that uses this object, since:
+ * 1 = this function ( _newObject )
+ * 2 = StubObject::_unstub
+ * 3 = StubObject::_call
+ * 4 = StubObject::__call
+ * 5 = DeprecatedGlobal::<method of global called>
+ * 6 = Actual function using the global.
+ * Of course its theoretically possible to have other call
+ * sequences for this method, but that seems to be
+ * rather unlikely.
+ */
+ wfDeprecated( '$' . $this->mGlobal, $this->mVersion, false, 6 );
+ return $this->mRealValue;
+ }
+}
diff --git a/includes/EditPage.php b/includes/EditPage.php
index d00d9114..b762cad1 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -1,6 +1,22 @@
<?php
/**
- * Contains the EditPage class
+ * Page edition user interface.
+ *
+ * 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
*/
@@ -37,11 +53,6 @@ class EditPage {
const AS_HOOK_ERROR = 210;
/**
- * Status: The filter function set in $wgFilterCallback returned true (= block it)
- */
- const AS_FILTERING = 211;
-
- /**
* Status: A hook function returned an error
*/
const AS_HOOK_ERROR_EXPECTED = 212;
@@ -145,6 +156,11 @@ class EditPage {
const AS_IMAGE_REDIRECT_LOGGED = 234;
/**
+ * HTML id and name for the beginning of the edit form.
+ */
+ const EDITFORM_ID = 'editform';
+
+ /**
* @var Article
*/
var $mArticle;
@@ -183,6 +199,12 @@ class EditPage {
*/
var $mParserOutput;
+ /**
+ * Has a summary been preset using GET parameter &summary= ?
+ * @var Bool
+ */
+ var $hasPresetSummary = false;
+
var $mBaseRevision = false;
var $mShowSummaryField = true;
@@ -282,7 +304,7 @@ class EditPage {
}
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__.": enter\n" );
+ wfDebug( __METHOD__ . ": enter\n" );
// If they used redlink=1 and the page exists, redirect to the main article
if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
@@ -333,7 +355,7 @@ class EditPage {
return;
}
- wfProfileIn( __METHOD__."-business-end" );
+ wfProfileIn( __METHOD__ . "-business-end" );
$this->isConflict = false;
// css / js subpages of user pages get a special treatment
@@ -355,7 +377,7 @@ class EditPage {
if ( 'save' == $this->formtype ) {
if ( !$this->attemptSave() ) {
- wfProfileOut( __METHOD__."-business-end" );
+ wfProfileOut( __METHOD__ . "-business-end" );
wfProfileOut( __METHOD__ );
return;
}
@@ -366,18 +388,18 @@ class EditPage {
if ( 'initial' == $this->formtype || $this->firsttime ) {
if ( $this->initialiseForm() === false ) {
$this->noSuchSectionPage();
- wfProfileOut( __METHOD__."-business-end" );
+ wfProfileOut( __METHOD__ . "-business-end" );
wfProfileOut( __METHOD__ );
return;
}
- if ( !$this->mTitle->getArticleId() )
+ if ( !$this->mTitle->getArticleID() )
wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
else
wfRunHooks( 'EditFormInitialText', array( $this ) );
}
$this->showEditForm();
- wfProfileOut( __METHOD__."-business-end" );
+ wfProfileOut( __METHOD__ . "-business-end" );
wfProfileOut( __METHOD__ );
}
@@ -394,7 +416,7 @@ class EditPage {
}
# Ignore some permissions errors when a user is just previewing/viewing diffs
$remove = array();
- foreach( $permErrors as $error ) {
+ foreach ( $permErrors as $error ) {
if ( ( $this->preview || $this->diff ) &&
( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' ) )
{
@@ -469,7 +491,7 @@ class EditPage {
*/
function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) {
wfDeprecated( __METHOD__, '1.19' );
-
+
global $wgRequest, $wgOut;
if ( $wgRequest->getBool( 'redlink' ) ) {
// The edit page was reached via a red link.
@@ -501,7 +523,7 @@ class EditPage {
// Standard preference behaviour
return true;
} elseif ( !$this->mTitle->exists() &&
- isset($wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]) &&
+ isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) &&
$wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
{
// Categories are special
@@ -518,7 +540,7 @@ class EditPage {
* @return bool
*/
protected function isWrongCaseCssJsPage() {
- if( $this->mTitle->isCssJsSubpage() ) {
+ if ( $this->mTitle->isCssJsSubpage() ) {
$name = $this->mTitle->getSkinFromCssJsSubpage();
$skins = array_merge(
array_keys( Skin::getSkinNames() ),
@@ -558,31 +580,31 @@ class EditPage {
# Also remove trailing whitespace, but don't remove _initial_
# whitespace from the text boxes. This may be significant formatting.
$this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
- if ( !$request->getCheck('wpTextbox2') ) {
+ if ( !$request->getCheck( 'wpTextbox2' ) ) {
// Skip this if wpTextbox2 has input, it indicates that we came
// from a conflict page with raw page text, not a custom form
// modified by subclasses
- wfProfileIn( get_class($this)."::importContentFormData" );
+ wfProfileIn( get_class( $this ) . "::importContentFormData" );
$textbox1 = $this->importContentFormData( $request );
- if ( isset($textbox1) )
+ if ( isset( $textbox1 ) )
$this->textbox1 = $textbox1;
- wfProfileOut( get_class($this)."::importContentFormData" );
+ wfProfileOut( get_class( $this ) . "::importContentFormData" );
}
- # Truncate for whole multibyte characters. +5 bytes for ellipsis
- $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 );
+ # Truncate for whole multibyte characters
+ $this->summary = $wgLang->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
# section titles.
$this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
-
+
# Treat sectiontitle the same way as summary.
# Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
# 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' ), 250 );
+ $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
$this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
$this->edittime = $request->getVal( 'wpEdittime' );
@@ -650,7 +672,7 @@ class EditPage {
{
$this->allowBlankSummary = true;
} else {
- $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary');
+ $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary' );
}
$this->autoSumm = $request->getText( 'wpAutoSummary' );
@@ -669,7 +691,7 @@ class EditPage {
$this->minoredit = false;
$this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden 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)
if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
@@ -679,6 +701,9 @@ class EditPage {
}
elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
$this->summary = $request->getText( 'summary' );
+ if ( $this->summary !== '' ) {
+ $this->hasPresetSummary = true;
+ }
}
if ( $request->getVal( 'minor' ) ) {
@@ -731,7 +756,7 @@ class EditPage {
} elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
# Watch creations
$this->watchthis = true;
- } elseif ( $this->mTitle->userIsWatching() ) {
+ } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
# Already watched
$this->watchthis = true;
}
@@ -796,7 +821,7 @@ class EditPage {
# Otherwise, $text will be left as-is.
if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
$undorev->getPage() == $oldrev->getPage() &&
- $undorev->getPage() == $this->mTitle->getArticleId() &&
+ $undorev->getPage() == $this->mTitle->getArticleID() &&
!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
!$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
@@ -810,12 +835,13 @@ class EditPage {
# If we just undid one rev, use an autosummary
$firstrev = $oldrev->getNext();
- if ( $firstrev->getId() == $undo ) {
- $undoSummary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() );
+ if ( $firstrev && $firstrev->getId() == $undo ) {
+ $undoSummary = wfMessage( 'undo-summary', $undo, $undorev->getUserText() )->inContentLanguage()->text();
if ( $this->summary === '' ) {
$this->summary = $undoSummary;
} else {
- $this->summary = $undoSummary . wfMsgForContent( 'colon-separator' ) . $this->summary;
+ $this->summary = $undoSummary . wfMessage( 'colon-separator' )
+ ->inContentLanguage()->text() . $this->summary;
}
$this->undidRev = $undo;
}
@@ -830,7 +856,7 @@ class EditPage {
$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
$this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
- wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true );
+ wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
}
if ( $text === false ) {
@@ -852,7 +878,7 @@ class EditPage {
*
* This difers from Article::getContent() that when a missing revision is
* encountered the result will be an empty string and not the
- * 'missing-article' message.
+ * 'missing-revision' message.
*
* @since 1.19
* @return string
@@ -907,7 +933,7 @@ class EditPage {
if ( !empty( $this->mPreloadText ) ) {
return $this->mPreloadText;
}
-
+
if ( $preload === '' ) {
return '';
}
@@ -959,7 +985,6 @@ class EditPage {
$bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
$status = $this->internalAttemptSave( $resultDetails, $bot );
// 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;
}
@@ -976,7 +1001,6 @@ class EditPage {
return true;
case self::AS_HOOK_ERROR:
- case self::AS_FILTERING:
return false;
case self::AS_SUCCESS_NEW_ARTICLE:
@@ -1011,7 +1035,7 @@ class EditPage {
return false;
case self::AS_BLOCKED_PAGE_FOR_USER:
- throw new UserBlockedError( $wgUser->mBlock );
+ throw new UserBlockedError( $wgUser->getBlock() );
case self::AS_IMAGE_REDIRECT_ANON:
case self::AS_IMAGE_REDIRECT_LOGGED:
@@ -1031,8 +1055,15 @@ class EditPage {
$permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
throw new PermissionsError( $permission );
+ default:
+ // We don't recognize $status->value. The only way that can happen
+ // is if an extension hook aborted from inside ArticleSave.
+ // Render the status object into $this->hookError
+ // FIXME this sucks, we should just use the Status object throughout
+ $this->hookError = '<div class="error">' . $status->getWikitext() .
+ '</div>';
+ return true;
}
- return false;
}
/**
@@ -1048,8 +1079,7 @@ class EditPage {
* AS_CONTENT_TOO_BIG and AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some time.
*/
function internalAttemptSave( &$result, $bot = false ) {
- global $wgFilterCallback, $wgUser, $wgRequest, $wgParser;
- global $wgMaxArticleSize;
+ global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
$status = Status::newGood();
@@ -1095,13 +1125,6 @@ class EditPage {
wfProfileOut( __METHOD__ );
return $status;
}
- if ( $wgFilterCallback && is_callable( $wgFilterCallback ) && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) {
- # Error messages or other handling should be performed by the filter function
- $status->setResult( false, self::AS_FILTERING );
- wfProfileOut( __METHOD__ . '-checks' );
- wfProfileOut( __METHOD__ );
- return $status;
- }
if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) {
# Error messages etc. could be handled within the hook...
$status->fatal( 'hookaborted' );
@@ -1179,9 +1202,10 @@ class EditPage {
wfProfileOut( __METHOD__ . '-checks' );
- # If article is new, insert it.
- $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
- $new = ( $aid == 0 );
+ # Load the page data from the master. If anything changes in the meantime,
+ # we detect it by using page_latest like a token in a 1 try compare-and-swap.
+ $this->mArticle->loadPageData( 'fromdbmaster' );
+ $new = !$this->mArticle->exists();
if ( $new ) {
// Late check for create permission, just in case *PARANOIA*
@@ -1215,44 +1239,37 @@ class EditPage {
return $status;
}
- # Handle the user preference to force summaries here. Check if it's not a redirect.
- if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) {
- if ( md5( $this->summary ) == $this->autoSumm ) {
- $this->missingSummary = true;
- $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
- $status->value = self::AS_SUMMARY_NEEDED;
- wfProfileOut( __METHOD__ );
- return $status;
- }
- }
-
$text = $this->textbox1;
$result['sectionanchor'] = '';
if ( $this->section == 'new' ) {
if ( $this->sectiontitle !== '' ) {
// Insert the section title above the content.
- $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text;
-
+ $text = wfMessage( 'newsectionheaderdefaultlevel', $this->sectiontitle )
+ ->inContentLanguage()->text() . "\n\n" . $text;
+
// Jump to the new section
$result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-
+
// If no edit summary was specified, create one automatically from the section
// title and have it link to the new section. Otherwise, respect the summary as
// passed.
if ( $this->summary === '' ) {
$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
- $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
+ $this->summary = wfMessage( 'newsectionsummary' )
+ ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
}
} elseif ( $this->summary !== '' ) {
// Insert the section title above the content.
- $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text;
-
+ $text = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )
+ ->inContentLanguage()->text() . "\n\n" . $text;
+
// Jump to the new section
$result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
// Create a link to the new section from the edit summary.
$cleanSummary = $wgParser->stripSectionName( $this->summary );
- $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+ $this->summary = wfMessage( 'newsectionsummary' )
+ ->rawParams( $cleanSummary )->inContentLanguage()->text();
}
}
@@ -1261,10 +1278,7 @@ class EditPage {
} else {
# 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 ) {
@@ -1279,15 +1293,15 @@ class EditPage {
} else {
// New comment; suppress conflict.
$this->isConflict = false;
- wfDebug( __METHOD__ .": conflict suppressed; new section\n" );
+ wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
}
- } elseif ( $this->section == '' && $this->userWasLastToEdit( $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 ( $this->sectiontitle !== '' ) {
@@ -1295,7 +1309,7 @@ class EditPage {
} else {
$sectionTitle = $this->summary;
}
-
+
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 );
@@ -1385,14 +1399,16 @@ class EditPage {
// passed.
if ( $this->summary === '' ) {
$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
- $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
+ $this->summary = wfMessage( 'newsectionsummary' )
+ ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
}
} elseif ( $this->summary !== '' ) {
$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
# This is a new section, so create a link to the new section
# in the revision summary.
$cleanSummary = $wgParser->stripSectionName( $this->summary );
- $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+ $this->summary = wfMessage( 'newsectionsummary' )
+ ->rawParams( $cleanSummary )->inContentLanguage()->text();
}
} elseif ( $this->section != '' ) {
# Try to get a section anchor from the section source, redirect to edited section if header found
@@ -1440,8 +1456,17 @@ class EditPage {
wfProfileOut( __METHOD__ );
return $status;
} else {
- $this->isConflict = true;
- $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
+ // Failure from doEdit()
+ // Show the edit conflict page for certain recognized errors from doEdit(),
+ // but don't show it for errors from extension hooks
+ $errors = $doEditStatus->getErrorsArray();
+ if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
+ 'edit-already-exists' ) ) )
+ {
+ $this->isConflict = true;
+ // Destroys data doEdit() put in $status->value but who cares
+ $doEditStatus->value = self::AS_END;
+ }
wfProfileOut( __METHOD__ );
return $doEditStatus;
}
@@ -1452,56 +1477,27 @@ class EditPage {
*/
protected function commitWatch() {
global $wgUser;
- if ( $this->watchthis xor $this->mTitle->userIsWatching() ) {
+ if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle ) ) {
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
if ( $this->watchthis ) {
WatchAction::doWatch( $this->mTitle, $wgUser );
} else {
WatchAction::doUnwatch( $this->mTitle, $wgUser );
}
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
}
}
/**
- * Check if no edits were made by other users since
- * the time a user started editing the page. Limit to
- * 50 revisions for the sake of performance.
- *
- * @param $id int
- * @param $edittime string
- *
- * @return bool
- */
- protected function userWasLastToEdit( $id, $edittime ) {
- if( !$id ) return false;
- $dbw = wfGetDB( DB_MASTER );
- $res = $dbw->select( 'revision',
- 'rev_user',
- array(
- 'rev_page' => $this->mTitle->getArticleId(),
- 'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) )
- ),
- __METHOD__,
- array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
- foreach ( $res as $row ) {
- if( $row->rev_user != $id ) {
- return false;
- }
- }
- return true;
- }
-
- /**
* @private
* @todo document
*
- * @parma $editText string
+ * @param $editText string
*
* @return bool
*/
- function mergeChangesInto( &$editText ){
+ function mergeChangesInto( &$editText ) {
wfProfileIn( __METHOD__ );
$db = wfGetDB( DB_MASTER );
@@ -1552,7 +1548,7 @@ class EditPage {
*
* @param $text string
*
- * @return string|false matching string or false
+ * @return string|bool matching string or false
*/
public static function matchSpamRegex( $text ) {
global $wgSpamRegex;
@@ -1564,9 +1560,9 @@ class EditPage {
/**
* Check given input text against $wgSpamRegex, and return the text of the first match.
*
- * @parma $text string
+ * @param $text string
*
- * @return string|false matching string or false
+ * @return string|bool matching string or false
*/
public static function matchSummarySpamRegex( $text ) {
global $wgSummarySpamRegex;
@@ -1580,9 +1576,9 @@ class EditPage {
* @return bool|string
*/
protected static function matchSpamRegexInternal( $text, $regexes ) {
- foreach( $regexes as $regex ) {
+ foreach ( $regexes as $regex ) {
$matches = array();
- if( preg_match( $regex, $text, $matches ) ) {
+ if ( preg_match( $regex, $text, $matches ) ) {
return $matches[0];
}
}
@@ -1595,7 +1591,7 @@ class EditPage {
$wgOut->addModules( 'mediawiki.action.edit' );
if ( $wgUser->getOption( 'uselivepreview', false ) ) {
- $wgOut->addModules( 'mediawiki.legacy.preview' );
+ $wgOut->addModules( 'mediawiki.action.edit.preview' );
}
// Bug #19334: textarea jumps when editing articles in IE8
$wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' );
@@ -1605,21 +1601,21 @@ class EditPage {
# Enabled article-related sidebar, toplinks, etc.
$wgOut->setArticleRelated( true );
+ $contextTitle = $this->getContextTitle();
if ( $this->isConflict ) {
- $wgOut->setPageTitle( wfMessage( 'editconflict', $this->getContextTitle()->getPrefixedText() ) );
- } elseif ( $this->section != '' ) {
+ $msg = 'editconflict';
+ } elseif ( $contextTitle->exists() && $this->section != '' ) {
$msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
- $wgOut->setPageTitle( wfMessage( $msg, $this->getContextTitle()->getPrefixedText() ) );
} else {
- # Use the title defined by DISPLAYTITLE magic word when present
- if ( isset( $this->mParserOutput )
- && ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) {
- $title = $dt;
- } else {
- $title = $this->getContextTitle()->getPrefixedText();
- }
- $wgOut->setPageTitle( wfMessage( 'editing', $title ) );
+ $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ?
+ 'editing' : 'creating';
+ }
+ # Use the title defined by DISPLAYTITLE magic word when present
+ $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
+ if ( $displayTitle === false ) {
+ $displayTitle = $contextTitle->getPrefixedText();
}
+ $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
}
/**
@@ -1636,6 +1632,24 @@ 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 ) {
+ # Show a hint to shared repo
+ $file = wfFindFile( $this->mTitle );
+ 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 (
+ 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
+ ) );
+ } else {
+ $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", array(
+ 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
+ ) );
+ }
+ }
+ }
}
# Show a warning message when someone creates/edits a user (talk) page but the user does not exist
@@ -1645,7 +1659,7 @@ class EditPage {
$username = $parts[0];
$user = User::newFromName( $username, false /* allow IP users*/ );
$ip = User::isIP( $username );
- if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist
+ if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
$wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) );
} elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked
@@ -1679,7 +1693,7 @@ class EditPage {
'', array( 'lim' => 10,
'conds' => array( "log_action != 'revision'" ),
'showIfEmpty' => false,
- 'msgKey' => array( 'recreate-moveddeleted-warn') )
+ 'msgKey' => array( 'recreate-moveddeleted-warn' ) )
);
}
}
@@ -1715,16 +1729,16 @@ class EditPage {
wfProfileIn( __METHOD__ );
- #need to parse the preview early so that we know which templates are used,
- #otherwise users with "show preview after edit box" will get a blank list
- #we parse this near the beginning so that setHeaders can do the title
- #setting work instead of leaving it in getPreviewText
+ # need to parse the preview early so that we know which templates are used,
+ # otherwise users with "show preview after edit box" will get a blank list
+ # we parse this near the beginning so that setHeaders can do the title
+ # setting work instead of leaving it in getPreviewText
$previewOutput = '';
if ( $this->formtype == 'preview' ) {
$previewOutput = $this->getPreviewText();
}
- wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) );
+ wfRunHooks( 'EditPage::showEditForm:initial', array( &$this, &$wgOut ) );
$this->setHeaders();
@@ -1753,7 +1767,7 @@ class EditPage {
}
}
- $wgOut->addHTML( Html::openElement( 'form', array( 'id' => 'editform', 'name' => 'editform',
+ $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' ) ) );
@@ -1777,14 +1791,19 @@ class EditPage {
: 'confirmrecreate';
$wgOut->addHTML(
'<div class="mw-confirm-recreate">' .
- wfMsgExt( $key, 'parseinline', $username, "<nowiki>$comment</nowiki>" ) .
- Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false,
+ wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
+ Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
array( 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' )
) .
'</div>'
);
}
+ # When the summary is hidden, also hide them on preview/show changes
+ if( $this->nosummary ) {
+ $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
+ }
+
# If a blank edit summary was previously provided, and the appropriate
# user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
# user being bounced back more than once in the event that a summary
@@ -1796,6 +1815,17 @@ class EditPage {
$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
}
+ if ( $this->undidRev ) {
+ $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
+ }
+
+ if ( $this->hasPresetSummary ) {
+ // If a summary has been preset using &summary= we dont want to prompt for
+ // a different summary. Only prompt for a summary if the summary is blanked.
+ // (Bug 17416)
+ $this->autoSumm = md5( '' );
+ }
+
$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
$wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
@@ -1827,10 +1857,6 @@ class EditPage {
$wgOut->addHTML( $this->editFormTextAfterContent );
- $wgOut->addWikiText( $this->getCopywarn() );
-
- $wgOut->addHTML( $this->editFormTextAfterWarn );
-
$this->showStandardInputs();
$this->showFormAfterText();
@@ -1870,7 +1896,7 @@ class EditPage {
preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
if ( !empty( $matches[2] ) ) {
global $wgParser;
- return $wgParser->stripSectionName(trim($matches[2]));
+ return $wgParser->stripSectionName( trim( $matches[2] ) );
} else {
return false;
}
@@ -1884,8 +1910,8 @@ class EditPage {
}
# Optional notices on a per-namespace and per-page basis
- $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace();
- $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage();
+ $editnotice_ns = 'editnotice-' . $this->mTitle->getNamespace();
+ $editnotice_ns_message = wfMessage( $editnotice_ns );
if ( $editnotice_ns_message->exists() ) {
$wgOut->addWikiText( $editnotice_ns_message->plain() );
}
@@ -1893,16 +1919,16 @@ class EditPage {
$parts = explode( '/', $this->mTitle->getDBkey() );
$editnotice_base = $editnotice_ns;
while ( count( $parts ) > 0 ) {
- $editnotice_base .= '-'.array_shift( $parts );
- $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage();
+ $editnotice_base .= '-' . array_shift( $parts );
+ $editnotice_base_msg = wfMessage( $editnotice_base );
if ( $editnotice_base_msg->exists() ) {
- $wgOut->addWikiText( $editnotice_base_msg->plain() );
+ $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 )->inContentLanguage();
+ $editnoticeMsg = wfMessage( $editnoticeText );
if ( $editnoticeMsg->exists() ) {
$wgOut->addWikiText( $editnoticeMsg->plain() );
}
@@ -1968,8 +1994,7 @@ class EditPage {
// Something went wrong
$wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
- array( 'missing-article', $this->mTitle->getPrefixedText(),
- wfMsgNoTrans( 'missingarticle-rev', $this->oldid ) ) );
+ array( 'missing-revision', $this->oldid ) );
}
}
}
@@ -2010,12 +2035,12 @@ class EditPage {
}
if ( $this->mTitle->isCascadeProtected() ) {
# Is this page under cascading protection from some source pages?
- list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources();
+ list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
$notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
$cascadeSourcesCount = count( $cascadeSources );
if ( $cascadeSourcesCount > 0 ) {
# Explain, and list the titles responsible
- foreach( $cascadeSources as $page ) {
+ foreach ( $cascadeSources as $page ) {
$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
}
}
@@ -2024,7 +2049,7 @@ class EditPage {
}
if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
- array( 'lim' => 1,
+ array( 'lim' => 1,
'showIfEmpty' => false,
'msgKey' => array( 'titleprotectedwarning' ),
'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ) );
@@ -2038,14 +2063,17 @@ class EditPage {
$wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
array( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ) );
} else {
- if( !wfMessage('longpage-hint')->isDisabled() ) {
+ if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
$wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
array( 'longpage-hint', $wgLang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) )
);
}
}
+ # Add header copyright warning
+ $this->showHeaderCopyrightWarning();
}
+
/**
* Standard summary input and label (wgSummary), abstracted so EditPage
* subclasses may reorganize the form.
@@ -2060,9 +2088,9 @@ class EditPage {
*
* @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 250 and to make it use UTF-8 bytes, not characters.
- $inputAttrs = ( is_array($inputAttrs) ? $inputAttrs : array() ) + array(
+ 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.
+ $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array(
'id' => 'wpSummary',
'maxlength' => '200',
'tabindex' => '1',
@@ -2070,7 +2098,7 @@ class EditPage {
'spellcheck' => 'true',
) + Linker::tooltipAndAccesskeyAttribs( 'summary' );
- $spanLabelAttrs = ( is_array($spanLabelAttrs) ? $spanLabelAttrs : array() ) + array(
+ $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : array() ) + array(
'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
'id' => "wpSummaryLabel"
);
@@ -2107,9 +2135,9 @@ class EditPage {
}
}
$summary = $wgContLang->recodeForEdit( $summary );
- $labelText = wfMsgExt( $isSubjectPreview ? 'subject' : 'summary', 'parseinline' );
- list($label, $input) = $this->getSummaryInput($summary, $labelText, array( 'class' => $summaryClass ), array());
- $wgOut->addHTML("{$label} {$input}");
+ $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
+ list( $label, $input ) = $this->getSummaryInput( $summary, $labelText, array( 'class' => $summaryClass ), array() );
+ $wgOut->addHTML( "{$label} {$input}" );
}
/**
@@ -2126,11 +2154,12 @@ class EditPage {
global $wgParser;
if ( $isSubjectPreview )
- $summary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $summary ) );
+ $summary = wfMessage( 'newsectionsummary', $wgParser->stripSectionName( $summary ) )
+ ->inContentLanguage()->text();
$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
- $summary = wfMsgExt( $message, 'parseinline' ) . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
+ $summary = wfMessage( $message )->parse() . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary );
}
@@ -2146,7 +2175,7 @@ class EditPage {
HTML
);
if ( !$this->checkUnicodeCompliantBrowser() )
- $wgOut->addHTML(Html::hidden( 'safemode', '1' ));
+ $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
}
protected function showFormAfterText() {
@@ -2183,7 +2212,7 @@ HTML
* The $textoverride method can be used by subclasses overriding showContentForm
* to pass back to this method.
*
- * @param $customAttribs An array of html attributes to use in the textarea
+ * @param $customAttribs array of html attributes to use in the textarea
* @param $textoverride String: optional text to override $this->textarea1 with
*/
protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
@@ -2230,7 +2259,7 @@ HTML
global $wgOut, $wgUser;
$wikitext = $this->safeUnicodeOutput( $content );
- if ( strval($wikitext) !== '' ) {
+ if ( strval( $wikitext ) !== '' ) {
// Ensure there's a newline at the end, otherwise adding lines
// is awkward.
// But don't add a newline if the ext is empty, or Firefox in XHTML
@@ -2272,7 +2301,7 @@ HTML
$wgOut->addHTML( '</div>' );
- if ( $this->formtype == 'diff') {
+ if ( $this->formtype == 'diff' ) {
$this->showDiff();
}
}
@@ -2285,12 +2314,12 @@ HTML
*/
protected function showPreview( $text ) {
global $wgOut;
- if ( $this->mTitle->getNamespace() == NS_CATEGORY) {
+ if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
$this->mArticle->openShowCategory();
}
# This hook seems slightly odd here, but makes things more
# consistent for extensions.
- wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) );
+ wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$text ) );
$wgOut->addHTML( $text );
if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
$this->mArticle->closeShowCategory();
@@ -2307,7 +2336,16 @@ HTML
function showDiff() {
global $wgUser, $wgContLang, $wgParser, $wgOut;
- $oldtext = $this->mArticle->getRawText();
+ $oldtitlemsg = 'currentrev';
+ # if message does not exist, show diff against the preloaded default
+ if( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
+ $oldtext = $this->mTitle->getDefaultMessageText();
+ if( $oldtext !== false ) {
+ $oldtitlemsg = 'defaultmessagetext';
+ }
+ } else {
+ $oldtext = $this->mArticle->getRawText();
+ }
$newtext = $this->mArticle->replaceSection(
$this->section, $this->textbox1, $this->summary, $this->edittime );
@@ -2317,8 +2355,8 @@ HTML
$newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
if ( $oldtext !== false || $newtext != '' ) {
- $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) );
- $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
+ $oldtitle = wfMessage( $oldtitlemsg )->parse();
+ $newtitle = wfMessage( 'yourtext' )->parse();
$de = new DifferenceEngine( $this->mArticle->getContext() );
$de->setText( $oldtext, $newtext );
@@ -2332,6 +2370,18 @@ HTML
}
/**
+ * Show the header copyright warning.
+ */
+ protected function showHeaderCopyrightWarning() {
+ $msg = 'editpage-head-copy-warn';
+ if ( !wfMessage( $msg )->isDisabled() ) {
+ global $wgOut;
+ $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
+ 'editpage-head-copy-warn' );
+ }
+ }
+
+ /**
* Give a chance for site and per-namespace customizations of
* terms of service summary link that might exist separately
* from the copyright notice.
@@ -2342,7 +2392,7 @@ HTML
protected function showTosSummary() {
$msg = 'editpage-tos-summary';
wfRunHooks( 'EditPageTosSummary', array( $this->mTitle, &$msg ) );
- if( !wfMessage( $msg )->isDisabled() ) {
+ if ( !wfMessage( $msg )->isDisabled() ) {
global $wgOut;
$wgOut->addHTML( '<div class="mw-tos-summary">' );
$wgOut->addWikiMsg( $msg );
@@ -2357,21 +2407,30 @@ HTML
'</div>' );
}
+ /**
+ * Get the copyright warning
+ *
+ * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
+ */
protected function getCopywarn() {
+ return self::getCopyrightWarning( $this->mTitle );
+ }
+
+ public static function getCopyrightWarning( $title ) {
global $wgRightsText;
if ( $wgRightsText ) {
$copywarnMsg = array( 'copyrightwarning',
- '[[' . wfMsgForContent( 'copyrightpage' ) . ']]',
+ '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
$wgRightsText );
} else {
$copywarnMsg = array( 'copyrightwarning2',
- '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
+ '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' );
}
// Allow for site and per-namespace customization of contribution/copyright notice.
- wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) );
+ wfRunHooks( 'EditPageCopyrightWarning', array( $title, &$copywarnMsg ) );
return "<div id=\"editpage-copywarn\">\n" .
- call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n</div>";
+ call_user_func_array( 'wfMessage', $copywarnMsg )->plain() . "\n</div>";
}
protected function showStandardInputs( &$tabindex = 2 ) {
@@ -2386,18 +2445,24 @@ HTML
$checkboxes = $this->getCheckboxes( $tabindex,
array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) );
$wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
+
+ // Show copyright warning.
+ $wgOut->addWikiText( $this->getCopywarn() );
+ $wgOut->addHTML( $this->editFormTextAfterWarn );
+
$wgOut->addHTML( "<div class='editButtons'>\n" );
$wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
$cancel = $this->getCancelLink();
if ( $cancel !== '' ) {
- $cancel .= wfMsgExt( 'pipe-separator' , 'escapenoentities' );
- }
- $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) );
- $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'.
- htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '.
- htmlspecialchars( wfMsg( 'newwindow' ) );
- $wgOut->addHTML( " <span class='editHelp'>{$cancel}{$edithelp}</span>\n" );
+ $cancel .= wfMessage( 'pipe-separator' )->text();
+ }
+ $edithelpurl = Skin::makeInternalOrExternalUrl( wfMessage( 'edithelppage' )->inContentLanguage()->text() );
+ $edithelp = '<a target="helpwindow" href="' . $edithelpurl . '">' .
+ wfMessage( 'edithelp' )->escaped() . '</a> ' .
+ 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" );
}
@@ -2413,7 +2478,10 @@ HTML
$de = new DifferenceEngine( $this->mArticle->getContext() );
$de->setText( $this->textbox2, $this->textbox1 );
- $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
+ $de->showDiff(
+ wfMessage( 'yourtext' )->parse(),
+ wfMessage( 'storedversion' )->text()
+ );
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
$this->showTextbox2();
@@ -2431,7 +2499,7 @@ HTML
return Linker::linkKnown(
$this->getContextTitle(),
- wfMsgExt( 'cancel', array( 'parseinline' ) ),
+ wfMessage( 'cancel' )->parse(),
array( 'id' => 'mw-editform-cancel' ),
$cancelParams
);
@@ -2499,11 +2567,11 @@ HTML
array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' )
);
// Quick paranoid permission checks...
- if( is_object( $data ) ) {
- if( $data->log_deleted & LogPage::DELETED_USER )
- $data->user_name = wfMsgHtml( 'rev-deleted-user' );
- if( $data->log_deleted & LogPage::DELETED_COMMENT )
- $data->log_comment = wfMsgHtml( 'rev-deleted-comment' );
+ if ( is_object( $data ) ) {
+ if ( $data->log_deleted & LogPage::DELETED_USER )
+ $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
+ if ( $data->log_deleted & LogPage::DELETED_COMMENT )
+ $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
}
return $data;
}
@@ -2513,7 +2581,7 @@ HTML
* @return string
*/
function getPreviewText() {
- global $wgOut, $wgUser, $wgParser, $wgRawHtml;
+ global $wgOut, $wgUser, $wgParser, $wgRawHtml, $wgLang;
wfProfileIn( __METHOD__ );
@@ -2526,7 +2594,7 @@ HTML
// string, which happens when you initially edit
// a category page, due to automatic preview-on-open.
$parsedNote = $wgOut->parse( "<div class='previewnote'>" .
- wfMsg( 'session_fail_preview_html' ) . "</div>", true, /* interface */true );
+ wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
}
wfProfileOut( __METHOD__ );
return $parsedNote;
@@ -2534,29 +2602,28 @@ HTML
if ( $this->mTriedSave && !$this->mTokenOk ) {
if ( $this->mTokenOkExceptSuffix ) {
- $note = wfMsg( 'token_suffix_mismatch' );
+ $note = wfMessage( 'token_suffix_mismatch' )->plain();
} else {
- $note = wfMsg( 'session_fail_preview' );
+ $note = wfMessage( 'session_fail_preview' )->plain();
}
} elseif ( $this->incompleteForm ) {
- $note = wfMsg( 'edit_form_incomplete' );
+ $note = wfMessage( 'edit_form_incomplete' )->plain();
} else {
- $note = wfMsg( 'previewnote' );
+ $note = wfMessage( 'previewnote' )->plain() .
+ ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
}
- $parserOptions = ParserOptions::newFromUser( $wgUser );
+ $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+
$parserOptions->setEditSection( false );
- $parserOptions->setTidy( true );
$parserOptions->setIsPreview( true );
- $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
+ $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
# don't parse non-wikitext pages, show message about preview
- # XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago?
-
- if ( $this->isCssJsSubpage || !$this->mTitle->isWikitextPage() ) {
- if( $this->mTitle->isCssJsSubpage() ) {
+ if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
+ if ( $this->mTitle->isCssJsSubpage() ) {
$level = 'user';
- } elseif( $this->mTitle->isCssOrJsPage() ) {
+ } elseif ( $this->mTitle->isCssOrJsPage() ) {
$level = 'site';
} else {
$level = false;
@@ -2564,64 +2631,66 @@ HTML
# Used messages to make sure grep find them:
# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
- if( $level ) {
- if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
- $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
- $class = "mw-code mw-css";
- } elseif (preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
- $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
- $class = "mw-code mw-js";
+ $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!' );
}
+ $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
+ $previewHTML = $parserOutput->getText();
+ } else {
+ $previewHTML = '';
}
- $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
- $previewHTML = $parserOutput->mText;
$previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
} else {
- $rt = Title::newFromRedirectArray( $this->textbox1 );
- if ( $rt ) {
- $previewHTML = $this->mArticle->viewRedirect( $rt, false );
- } else {
- $toparse = $this->textbox1;
-
- # If we're adding a comment, we need to show the
- # summary as the headline
- if ( $this->section == "new" && $this->summary != "" ) {
- $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse;
- }
+ $toparse = $this->textbox1;
- wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
+ # 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;
+ }
- $parserOptions->enableLimitReport();
+ wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
- $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
- $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
+ $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
+ $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
+ $rt = Title::newFromRedirectArray( $this->textbox1 );
+ if ( $rt ) {
+ $previewHTML = $this->mArticle->viewRedirect( $rt, false );
+ } else {
$previewHTML = $parserOutput->getText();
- $this->mParserOutput = $parserOutput;
- $wgOut->addParserOutputNoText( $parserOutput );
+ }
- if ( count( $parserOutput->getWarnings() ) ) {
- $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
- }
+ $this->mParserOutput = $parserOutput;
+ $wgOut->addParserOutputNoText( $parserOutput );
+
+ if ( count( $parserOutput->getWarnings() ) ) {
+ $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
}
}
- if( $this->isConflict ) {
- $conflict = '<h2 id="mw-previewconflict">' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
+ if ( $this->isConflict ) {
+ $conflict = '<h2 id="mw-previewconflict">' . wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
} else {
$conflict = '<hr />';
}
$previewhead = "<div class='previewnote'>\n" .
- '<h2 id="mw-previewheader">' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>" .
+ '<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() );
+ 'class' => 'mw-content-' . $pageLang->getDir() );
$previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
wfProfileOut( __METHOD__ );
@@ -2637,9 +2706,9 @@ HTML
if ( !isset( $this->mParserOutput ) ) {
return $templates;
}
- foreach( $this->mParserOutput->getTemplates() as $ns => $template) {
- foreach( array_keys( $template ) as $dbk ) {
- $templates[] = Title::makeTitle($ns, $dbk);
+ foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
+ foreach ( array_keys( $template ) as $dbk ) {
+ $templates[] = Title::makeTitle( $ns, $dbk );
}
}
return $templates;
@@ -2680,8 +2749,8 @@ HTML
'id' => 'mw-editbutton-bold',
'open' => '\'\'\'',
'close' => '\'\'\'',
- 'sample' => wfMsg( 'bold_sample' ),
- 'tip' => wfMsg( 'bold_tip' ),
+ 'sample' => wfMessage( 'bold_sample' )->text(),
+ 'tip' => wfMessage( 'bold_tip' )->text(),
'key' => 'B'
),
array(
@@ -2689,8 +2758,8 @@ HTML
'id' => 'mw-editbutton-italic',
'open' => '\'\'',
'close' => '\'\'',
- 'sample' => wfMsg( 'italic_sample' ),
- 'tip' => wfMsg( 'italic_tip' ),
+ 'sample' => wfMessage( 'italic_sample' )->text(),
+ 'tip' => wfMessage( 'italic_tip' )->text(),
'key' => 'I'
),
array(
@@ -2698,8 +2767,8 @@ HTML
'id' => 'mw-editbutton-link',
'open' => '[[',
'close' => ']]',
- 'sample' => wfMsg( 'link_sample' ),
- 'tip' => wfMsg( 'link_tip' ),
+ 'sample' => wfMessage( 'link_sample' )->text(),
+ 'tip' => wfMessage( 'link_tip' )->text(),
'key' => 'L'
),
array(
@@ -2707,8 +2776,8 @@ HTML
'id' => 'mw-editbutton-extlink',
'open' => '[',
'close' => ']',
- 'sample' => wfMsg( 'extlink_sample' ),
- 'tip' => wfMsg( 'extlink_tip' ),
+ 'sample' => wfMessage( 'extlink_sample' )->text(),
+ 'tip' => wfMessage( 'extlink_tip' )->text(),
'key' => 'X'
),
array(
@@ -2716,8 +2785,8 @@ HTML
'id' => 'mw-editbutton-headline',
'open' => "\n== ",
'close' => " ==\n",
- 'sample' => wfMsg( 'headline_sample' ),
- 'tip' => wfMsg( 'headline_tip' ),
+ 'sample' => wfMessage( 'headline_sample' )->text(),
+ 'tip' => wfMessage( 'headline_tip' )->text(),
'key' => 'H'
),
$imagesAvailable ? array(
@@ -2725,8 +2794,8 @@ HTML
'id' => 'mw-editbutton-image',
'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
'close' => ']]',
- 'sample' => wfMsg( 'image_sample' ),
- 'tip' => wfMsg( 'image_tip' ),
+ 'sample' => wfMessage( 'image_sample' )->text(),
+ 'tip' => wfMessage( 'image_tip' )->text(),
'key' => 'D',
) : false,
$imagesAvailable ? array(
@@ -2734,17 +2803,17 @@ HTML
'id' => 'mw-editbutton-media',
'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
'close' => ']]',
- 'sample' => wfMsg( 'media_sample' ),
- 'tip' => wfMsg( 'media_tip' ),
+ 'sample' => wfMessage( 'media_sample' )->text(),
+ 'tip' => wfMessage( 'media_tip' )->text(),
'key' => 'M'
) : false,
- $wgUseTeX ? array(
+ $wgUseTeX ? array(
'image' => $wgLang->getImageFile( 'button-math' ),
'id' => 'mw-editbutton-math',
'open' => "<math>",
'close' => "</math>",
- 'sample' => wfMsg( 'math_sample' ),
- 'tip' => wfMsg( 'math_tip' ),
+ 'sample' => wfMessage( 'math_sample' )->text(),
+ 'tip' => wfMessage( 'math_tip' )->text(),
'key' => 'C'
) : false,
array(
@@ -2752,8 +2821,8 @@ HTML
'id' => 'mw-editbutton-nowiki',
'open' => "<nowiki>",
'close' => "</nowiki>",
- 'sample' => wfMsg( 'nowiki_sample' ),
- 'tip' => wfMsg( 'nowiki_tip' ),
+ 'sample' => wfMessage( 'nowiki_sample' )->text(),
+ 'tip' => wfMessage( 'nowiki_tip' )->text(),
'key' => 'N'
),
array(
@@ -2762,7 +2831,7 @@ HTML
'open' => '--~~~~',
'close' => '',
'sample' => '',
- 'tip' => wfMsg( 'sig_tip' ),
+ 'tip' => wfMessage( 'sig_tip' )->text(),
'key' => 'Y'
),
array(
@@ -2771,7 +2840,7 @@ HTML
'open' => "\n----\n",
'close' => '',
'sample' => '',
- 'tip' => wfMsg( 'hr_tip' ),
+ 'tip' => wfMessage( 'hr_tip' )->text(),
'key' => 'R'
)
);
@@ -2797,7 +2866,7 @@ HTML
$script .= Xml::encodeJsCall( 'mw.toolbar.addButton', $params );
}
-
+
// This used to be called on DOMReady from mediawiki.action.edit, which
// ended up causing race conditions with the setup code above.
$script .= "\n" .
@@ -2818,7 +2887,7 @@ HTML
* Returns an array of html code of the following checkboxes:
* minor and watch
*
- * @param $tabindex Current tabindex
+ * @param $tabindex int Current tabindex
* @param $checked Array of checkbox => bool, where bool indicates the checked
* status of the checkbox
*
@@ -2832,11 +2901,11 @@ HTML
// don't show the minor edit checkbox if it's a new page or section
if ( !$this->isNew ) {
$checkboxes['minor'] = '';
- $minorLabel = wfMsgExt( 'minoredit', array( 'parseinline' ) );
+ $minorLabel = wfMessage( 'minoredit' )->parse();
if ( $wgUser->isAllowed( 'minoredit' ) ) {
$attribs = array(
'tabindex' => ++$tabindex,
- 'accesskey' => wfMsg( 'accesskey-minoredit' ),
+ 'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
'id' => 'wpMinoredit',
);
$checkboxes['minor'] =
@@ -2847,12 +2916,12 @@ HTML
}
}
- $watchLabel = wfMsgExt( 'watchthis', array( 'parseinline' ) );
+ $watchLabel = wfMessage( 'watchthis' )->parse();
$checkboxes['watch'] = '';
if ( $wgUser->isLoggedIn() ) {
$attribs = array(
'tabindex' => ++$tabindex,
- 'accesskey' => wfMsg( 'accesskey-watch' ),
+ 'accesskey' => wfMessage( 'accesskey-watch' )->text(),
'id' => 'wpWatchthis',
);
$checkboxes['watch'] =
@@ -2869,7 +2938,7 @@ HTML
* Returns an array of html code of the following buttons:
* save, diff, preview and live
*
- * @param $tabindex Current tabindex
+ * @param $tabindex int Current tabindex
*
* @return array
*/
@@ -2881,11 +2950,11 @@ HTML
'name' => 'wpSave',
'type' => 'submit',
'tabindex' => ++$tabindex,
- 'value' => wfMsg( 'savearticle' ),
- 'accesskey' => wfMsg( 'accesskey-save' ),
- 'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']',
+ 'value' => wfMessage( 'savearticle' )->text(),
+ 'accesskey' => wfMessage( 'accesskey-save' )->text(),
+ 'title' => wfMessage( 'tooltip-save' )->text() . ' [' . wfMessage( 'accesskey-save' )->text() . ']',
);
- $buttons['save'] = Xml::element('input', $temp, '');
+ $buttons['save'] = Xml::element( 'input', $temp, '' );
++$tabindex; // use the same for preview and live preview
$temp = array(
@@ -2893,9 +2962,9 @@ HTML
'name' => 'wpPreview',
'type' => 'submit',
'tabindex' => $tabindex,
- 'value' => wfMsg( 'showpreview' ),
- 'accesskey' => wfMsg( 'accesskey-preview' ),
- 'title' => wfMsg( 'tooltip-preview' ) . ' [' . wfMsg( 'accesskey-preview' ) . ']',
+ 'value' => wfMessage( 'showpreview' )->text(),
+ 'accesskey' => wfMessage( 'accesskey-preview' )->text(),
+ 'title' => wfMessage( 'tooltip-preview' )->text() . ' [' . wfMessage( 'accesskey-preview' )->text() . ']',
);
$buttons['preview'] = Xml::element( 'input', $temp, '' );
$buttons['live'] = '';
@@ -2905,9 +2974,9 @@ HTML
'name' => 'wpDiff',
'type' => 'submit',
'tabindex' => ++$tabindex,
- 'value' => wfMsg( 'showdiff' ),
- 'accesskey' => wfMsg( 'accesskey-diff' ),
- 'title' => wfMsg( 'tooltip-diff' ) . ' [' . wfMsg( 'accesskey-diff' ) . ']',
+ 'value' => wfMessage( 'showdiff' )->text(),
+ 'accesskey' => wfMessage( 'accesskey-diff' )->text(),
+ 'title' => wfMessage( 'tooltip-diff' )->text() . ' [' . wfMessage( 'accesskey-diff' )->text() . ']',
);
$buttons['diff'] = Xml::element( 'input', $temp, '' );
@@ -2923,8 +2992,8 @@ HTML
* failure, etc).
*
* @todo This doesn't include category or interlanguage links.
- * Would need to enhance it a bit, <s>maybe wrap them in XML
- * or something...</s> that might also require more skin
+ * Would need to enhance it a bit, "<s>maybe wrap them in XML
+ * or something...</s>" that might also require more skin
* initialization, so check whether that's a problem.
*/
function livePreview() {
@@ -2954,7 +3023,7 @@ HTML
wfDeprecated( __METHOD__, '1.19' );
global $wgUser;
- throw new UserBlockedError( $wgUser->mBlock );
+ throw new UserBlockedError( $wgUser->getBlock() );
}
/**
@@ -2988,7 +3057,7 @@ HTML
$wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
- $res = wfMsgExt( 'nosuchsectiontext', 'parse', $this->section );
+ $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) );
$wgOut->addHTML( $res );
@@ -2998,12 +3067,12 @@ HTML
/**
* Produce the stock "your edit contains spam" page
*
- * @param $match Text which triggered one or more filters
+ * @param $match string Text which triggered one or more filters
* @deprecated since 1.17 Use method spamPageWithContent() instead
*/
static function spamPage( $match = false ) {
wfDeprecated( __METHOD__, '1.17' );
-
+
global $wgOut, $wgTitle;
$wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
@@ -3021,12 +3090,15 @@ HTML
/**
* Show "your edit contains spam" page with your diff and text
*
- * @param $match Text which triggered one or more filters
+ * @param $match string|Array|bool Text (or array of texts) which triggered one or more filters
*/
public function spamPageWithContent( $match = false ) {
- global $wgOut;
+ global $wgOut, $wgLang;
$this->textbox2 = $this->textbox1;
+ if( is_array( $match ) ){
+ $match = $wgLang->listToText( $match );
+ }
$wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
$wgOut->addHTML( '<div id="spamprotected">' );
@@ -3037,9 +3109,7 @@ HTML
$wgOut->addHTML( '</div>' );
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
- $de = new DifferenceEngine( $this->mArticle->getContext() );
- $de->setText( $this->getCurrentText(), $this->textbox2 );
- $de->showDiff( wfMsg( "storedversion" ), wfMsgExt( 'yourtext', 'parseinline' ) );
+ $this->showDiff();
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
$this->showTextbox2();
@@ -3066,14 +3136,16 @@ HTML
* @private
*/
function checkUnicodeCompliantBrowser() {
- global $wgBrowserBlackList;
- if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) {
+ global $wgBrowserBlackList, $wgRequest;
+
+ $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
+ if ( $currentbrowser === false ) {
// No User-Agent header sent? Trust it by default...
return true;
}
- $currentbrowser = $_SERVER["HTTP_USER_AGENT"];
+
foreach ( $wgBrowserBlackList as $browser ) {
- if ( preg_match($browser, $currentbrowser) ) {
+ if ( preg_match( $browser, $currentbrowser ) ) {
return false;
}
}
@@ -3144,25 +3216,25 @@ HTML
$bytesleft = 0;
$result = "";
$working = 0;
- for( $i = 0; $i < strlen( $invalue ); $i++ ) {
+ for ( $i = 0; $i < strlen( $invalue ); $i++ ) {
$bytevalue = ord( $invalue[$i] );
- if ( $bytevalue <= 0x7F ) { //0xxx xxxx
+ if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
$result .= chr( $bytevalue );
$bytesleft = 0;
- } elseif ( $bytevalue <= 0xBF ) { //10xx xxxx
+ } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
$working = $working << 6;
- $working += ($bytevalue & 0x3F);
+ $working += ( $bytevalue & 0x3F );
$bytesleft--;
if ( $bytesleft <= 0 ) {
$result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
}
- } elseif ( $bytevalue <= 0xDF ) { //110x xxxx
+ } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
$working = $bytevalue & 0x1F;
$bytesleft = 1;
- } elseif ( $bytevalue <= 0xEF ) { //1110 xxxx
+ } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
$working = $bytevalue & 0x0F;
$bytesleft = 2;
- } else { //1111 0xxx
+ } else { // 1111 0xxx
$working = $bytevalue & 0x07;
$bytesleft = 3;
}
@@ -3181,20 +3253,20 @@ HTML
*/
function unmakesafe( $invalue ) {
$result = "";
- for( $i = 0; $i < strlen( $invalue ); $i++ ) {
- if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i+3] != '0' ) ) {
+ for ( $i = 0; $i < strlen( $invalue ); $i++ ) {
+ if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
$i += 3;
$hexstring = "";
do {
$hexstring .= $invalue[$i];
$i++;
- } while( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
+ } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
// Do some sanity checks. These aren't needed for reversability,
// but should help keep the breakage down if the editor
// breaks one of the entities whilst editing.
- if ( (substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6) ) {
- $codepoint = hexdec($hexstring);
+ if ( ( substr( $invalue, $i, 1 ) == ";" ) and ( strlen( $hexstring ) <= 6 ) ) {
+ $codepoint = hexdec( $hexstring );
$result .= codepointToUtf8( $codepoint );
} else {
$result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
diff --git a/includes/Exception.php b/includes/Exception.php
index 3bd89b6e..714f73e8 100644
--- a/includes/Exception.php
+++ b/includes/Exception.php
@@ -1,6 +1,21 @@
<?php
/**
- * Exception class and handler
+ * Exception class and handler.
+ *
+ * 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
*/
@@ -15,8 +30,11 @@
* @ingroup Exception
*/
class MWException extends Exception {
+ var $logId;
+
/**
- * Should the exception use $wgOut to output the error ?
+ * Should the exception use $wgOut to output the error?
+ *
* @return bool
*/
function useOutputPage() {
@@ -27,7 +45,8 @@ class MWException extends Exception {
}
/**
- * Can the extension use wfMsg() to get i18n messages ?
+ * Can the extension use the Message class/wfMessage to get i18n-ed messages?
+ *
* @return bool
*/
function useMessageCache() {
@@ -45,19 +64,19 @@ 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
- * @return Mixed: string to output or null if any hook has been called
+ * @param $name string: class name of the exception
+ * @param $args array: 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() ) {
global $wgExceptionHooks;
if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) {
- return; // Just silently ignore
+ return null; // Just silently ignore
}
if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) ) {
- return;
+ return null;
}
$hooks = $wgExceptionHooks[ $name ];
@@ -74,22 +93,23 @@ class MWException extends Exception {
return $result;
}
}
+ return null;
}
/**
* Get a message from i18n
*
- * @param $key String: message name
- * @param $fallback String: default message if the message cache can't be
+ * @param $key string: message name
+ * @param $fallback string: 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
+ * @return string message with arguments replaced
*/
function msg( $key, $fallback /*[, params...] */ ) {
$args = array_slice( func_get_args(), 2 );
if ( $this->useMessageCache() ) {
- return wfMsgNoTrans( $key, $args );
+ return wfMessage( $key, $args )->plain();
} else {
return wfMsgReplaceArgs( $fallback, $args );
}
@@ -100,7 +120,7 @@ class MWException extends Exception {
* backtrace to the error, otherwise show a message to ask to set it to true
* to show that information.
*
- * @return String html to output
+ * @return string html to output
*/
function getHTML() {
global $wgShowExceptionDetails;
@@ -110,15 +130,22 @@ class MWException extends Exception {
'</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
"</p>\n";
} else {
- return "<p>Set <b><tt>\$wgShowExceptionDetails = true;</tt></b> " .
+ return
+ "<div class=\"errorbox\">" .
+ '[' . $this->getLogId() . '] ' .
+ gmdate( 'Y-m-d H:i:s' ) .
+ ": Fatal exception of type " . get_class( $this ) . "</div>\n" .
+ "<!-- Set \$wgShowExceptionDetails = true; " .
"at the bottom of LocalSettings.php to show detailed " .
- "debugging information.</p>";
+ "debugging information. -->";
}
}
/**
+ * Get the text to display when reporting the error on the command line.
* If $wgShowExceptionDetails is true, return a text message with a
* backtrace to the error.
+ *
* @return string
*/
function getText() {
@@ -134,22 +161,38 @@ class MWException extends Exception {
}
/**
- * Return titles of this error page
- * @return String
+ * Return the title of the page when reporting this error in a HTTP response.
+ *
+ * @return string
*/
function getPageTitle() {
return $this->msg( 'internalerror', "Internal error" );
}
/**
+ * Get a random ID for this error.
+ * This allows to link the exception to its correspoding log entry when
+ * $wgShowExceptionDetails is set to false.
+ *
+ * @return string
+ */
+ function getLogId() {
+ if ( $this->logId === null ) {
+ $this->logId = wfRandomString( 8 );
+ }
+ return $this->logId;
+ }
+
+ /**
* Return the requested URL and point to file and line number from which the
- * exception occured
+ * exception occurred
*
- * @return String
+ * @return string
*/
function getLogMessage() {
global $wgRequest;
+ $id = $this->getLogId();
$file = $this->getFile();
$line = $this->getLine();
$message = $this->getMessage();
@@ -163,10 +206,12 @@ class MWException extends Exception {
$url = '[no req]';
}
- return "$url Exception from line $line of $file: $message";
+ return "[$id] $url Exception from line $line of $file: $message";
}
- /** Output the exception report using HTML */
+ /**
+ * Output the exception report using HTML.
+ */
function reportHTML() {
global $wgOut;
if ( $this->useOutputPage() ) {
@@ -182,13 +227,19 @@ class MWException extends Exception {
$wgOut->output();
} else {
header( "Content-Type: text/html; charset=utf-8" );
+ echo "<!doctype html>\n" .
+ '<html><head>' .
+ '<title>' . htmlspecialchars( $this->getPageTitle() ) . '</title>' .
+ "</head><body>\n";
+
$hookResult = $this->runHooks( get_class( $this ) . "Raw" );
if ( $hookResult ) {
- die( $hookResult );
+ echo $hookResult;
+ } else {
+ echo $this->getHTML();
}
- echo $this->getHTML();
- die(1);
+ echo "</body></html>\n";
}
}
@@ -197,21 +248,35 @@ class MWException extends Exception {
* It will be either HTML or plain text based on isCommandLine().
*/
function report() {
+ global $wgLogExceptionBacktrace;
$log = $this->getLogMessage();
if ( $log ) {
- wfDebugLog( 'exception', $log );
+ if ( $wgLogExceptionBacktrace ) {
+ wfDebugLog( 'exception', $log . "\n" . $this->getTraceAsString() . "\n" );
+ } else {
+ wfDebugLog( 'exception', $log );
+ }
}
- if ( self::isCommandLine() ) {
+ 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() );
+ } elseif ( self::isCommandLine() ) {
MWExceptionHandler::printError( $this->getText() );
} else {
+ header( "HTTP/1.1 500 MediaWiki exception" );
+ header( "Status: 500 MediaWiki exception", true );
+
$this->reportHTML();
}
}
/**
- * @static
+ * Check whether we are in command line mode or not to report the exception
+ * in the correct format.
+ *
* @return bool
*/
static function isCommandLine() {
@@ -222,6 +287,8 @@ class MWException extends Exception {
/**
* Exception class which takes an HTML error message, and does not
* produce a backtrace. Replacement for OutputPage::fatalError().
+ *
+ * @since 1.7
* @ingroup Exception
*/
class FatalError extends MWException {
@@ -242,14 +309,20 @@ class FatalError extends MWException {
}
/**
- * An error page which can definitely be safely rendered using the OutputPage
+ * An error page which can definitely be safely rendered using the OutputPage.
+ *
+ * @since 1.7
* @ingroup Exception
*/
class ErrorPageError extends MWException {
public $title, $msg, $params;
/**
- * Note: these arguments are keys into wfMsg(), not text!
+ * 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()
*/
function __construct( $title, $msg, $params = null ) {
$this->title = $title;
@@ -259,14 +332,13 @@ class ErrorPageError extends MWException {
if( $msg instanceof Message ){
parent::__construct( $msg );
} else {
- parent::__construct( wfMsg( $msg ) );
+ parent::__construct( wfMessage( $msg )->text() );
}
}
function report() {
global $wgOut;
-
$wgOut->showErrorPage( $this->title, $this->msg, $this->params );
$wgOut->output();
}
@@ -276,12 +348,14 @@ class ErrorPageError extends MWException {
* Show an error page on a badtitle.
* Similar to ErrorPage, but emit a 400 HTTP error code to let mobile
* browser it is not really a valid content.
+ *
+ * @since 1.19
+ * @ingroup Exception
*/
class BadTitleError extends ErrorPageError {
-
/**
- * @param $msg string A message key (default: 'badtitletext')
- * @param $params Array parameter to wfMsg()
+ * @param $msg string|Message A message key (default: 'badtitletext')
+ * @param $params Array parameter to wfMessage()
*/
function __construct( $msg = 'badtitletext', $params = null ) {
parent::__construct( 'badtitle', $msg, $params );
@@ -305,6 +379,8 @@ class BadTitleError extends ErrorPageError {
/**
* Show an error when a user tries to do something they do not have the necessary
* permissions for.
+ *
+ * @since 1.18
* @ingroup Exception
*/
class PermissionsError extends ErrorPageError {
@@ -341,7 +417,9 @@ class PermissionsError extends ErrorPageError {
/**
* Show an error when the wiki is locked/read-only and the user tries to do
- * something that requires write access
+ * something that requires write access.
+ *
+ * @since 1.18
* @ingroup Exception
*/
class ReadOnlyError extends ErrorPageError {
@@ -355,7 +433,9 @@ class ReadOnlyError extends ErrorPageError {
}
/**
- * Show an error when the user hits a rate limit
+ * Show an error when the user hits a rate limit.
+ *
+ * @since 1.18
* @ingroup Exception
*/
class ThrottledError extends ErrorPageError {
@@ -369,12 +449,14 @@ class ThrottledError extends ErrorPageError {
public function report(){
global $wgOut;
$wgOut->setStatusCode( 503 );
- return parent::report();
+ parent::report();
}
}
/**
- * Show an error when the user tries to do something whilst blocked
+ * Show an error when the user tries to do something whilst blocked.
+ *
+ * @since 1.18
* @ingroup Exception
*/
class UserBlockedError extends ErrorPageError {
@@ -391,7 +473,7 @@ class UserBlockedError extends ErrorPageError {
$reason = $block->mReason;
if( $reason == '' ) {
- $reason = wfMsg( 'blockednoreason' );
+ $reason = wfMessage( 'blockednoreason' )->text();
}
/* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
@@ -416,9 +498,57 @@ class UserBlockedError extends ErrorPageError {
}
/**
+ * Shows a generic "user is not logged in" error page.
+ *
+ * This is essentially an ErrorPageError exception which by default use 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 ) {
+ * 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.
+ *
+ * @par Example:
+ * @code
+ * if( $user->isAnon ) {
+ * throw new UserNotLoggedIn( 'action-require-loggedin' );
+ * }
+ * @endcode
+ *
+ * @ingroup Exception
+ */
+class UserNotLoggedIn extends ErrorPageError {
+
+ /**
+ * @param $reasonMsg A message key containing the reason for the error.
+ * Optional, default: 'exception-nologin-text'
+ * @param $titleMsg A message key to set the page title.
+ * Optional, default: 'exception-nologin'
+ * @param $params Parameters to wfMessage().
+ * Optiona, default: null
+ */
+ public function __construct(
+ $reasonMsg = 'exception-nologin-text',
+ $titleMsg = 'exception-nologin',
+ $params = null
+ ) {
+ parent::__construct( $titleMsg, $reasonMsg, $params );
+ }
+}
+
+/**
* Show an error that looks like an HTTP server error.
* Replacement for wfHttpError().
*
+ * @since 1.19
* @ingroup Exception
*/
class HttpError extends MWException {
@@ -438,7 +568,7 @@ class HttpError extends MWException {
$this->content = $content;
}
- public function reportHTML() {
+ public function report() {
$httpMessage = HttpStatus::getMessage( $this->httpCode );
header( "Status: {$this->httpCode} {$httpMessage}" );
@@ -458,7 +588,7 @@ class HttpError extends MWException {
$content = htmlspecialchars( $this->content );
}
- print "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n".
+ print "<!DOCTYPE html>\n".
"<html><head><title>$header</title></head>\n" .
"<body><h1>$header</h1><p>$content</p></body></html>\n";
}
@@ -508,7 +638,7 @@ class MWExceptionHandler {
if ( $cmdLine ) {
self::printError( $message );
} else {
- self::escapeEchoAndDie( $message );
+ echo nl2br( htmlspecialchars( $message ) ) . "\n";
}
}
} else {
@@ -522,7 +652,7 @@ class MWExceptionHandler {
if ( $cmdLine ) {
self::printError( $message );
} else {
- self::escapeEchoAndDie( $message );
+ echo nl2br( htmlspecialchars( $message ) ) . "\n";
}
}
}
@@ -530,7 +660,8 @@ class MWExceptionHandler {
/**
* Print a message, if possible to STDERR.
* Use this in command line mode only (see isCommandLine)
- * @param $message String Failure text
+ *
+ * @param $message string 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).
@@ -543,16 +674,6 @@ class MWExceptionHandler {
}
/**
- * Print a message after escaping it and converting newlines to <br>
- * Use this for non-command line failures
- * @param $message String Failure text
- */
- private static function escapeEchoAndDie( $message ) {
- echo nl2br( htmlspecialchars( $message ) ) . "\n";
- die(1);
- }
-
- /**
* Exception handler which simulates the appropriate catch() handling:
*
* try {
diff --git a/includes/Export.php b/includes/Export.php
index 7773d03c..f01fb237 100644
--- a/includes/Export.php
+++ b/includes/Export.php
@@ -49,6 +49,23 @@ class WikiExporter {
const TEXT = 0;
const STUB = 1;
+ var $buffer;
+
+ var $text;
+
+ /**
+ * @var DumpOutput
+ */
+ var $sink;
+
+ /**
+ * Returns the export schema version.
+ * @return string
+ */
+ public static function schemaVersion() {
+ return "0.7";
+ }
+
/**
* If using WikiExporter::STREAM to stream a large amount of data,
* provide a database connection which is not managed by
@@ -103,7 +120,7 @@ class WikiExporter {
* the most recent version.
*/
public function allPages() {
- return $this->dumpFrom( '' );
+ $this->dumpFrom( '' );
}
/**
@@ -118,7 +135,7 @@ class WikiExporter {
if ( $end ) {
$condition .= ' AND page_id < ' . intval( $end );
}
- return $this->dumpFrom( $condition );
+ $this->dumpFrom( $condition );
}
/**
@@ -133,27 +150,34 @@ class WikiExporter {
if ( $end ) {
$condition .= ' AND rev_id < ' . intval( $end );
}
- return $this->dumpFrom( $condition );
+ $this->dumpFrom( $condition );
}
/**
* @param $title Title
*/
public function pageByTitle( $title ) {
- return $this->dumpFrom(
+ $this->dumpFrom(
'page_namespace=' . $title->getNamespace() .
' AND page_title=' . $this->db->addQuotes( $title->getDBkey() ) );
}
+ /**
+ * @param $name string
+ * @throws MWException
+ */
public function pageByName( $name ) {
$title = Title::newFromText( $name );
if ( is_null( $title ) ) {
throw new MWException( "Can't export invalid title" );
} else {
- return $this->pageByTitle( $title );
+ $this->pageByTitle( $title );
}
}
+ /**
+ * @param $names array
+ */
public function pagesByName( $names ) {
foreach ( $names as $name ) {
$this->pageByName( $name );
@@ -161,20 +185,28 @@ class WikiExporter {
}
public function allLogs() {
- return $this->dumpFrom( '' );
+ $this->dumpFrom( '' );
}
+ /**
+ * @param $start int
+ * @param $end int
+ */
public function logsByRange( $start, $end ) {
$condition = 'log_id >= ' . intval( $start );
if ( $end ) {
$condition .= ' AND log_id < ' . intval( $end );
}
- return $this->dumpFrom( $condition );
+ $this->dumpFrom( $condition );
}
- # Generates the distinct list of authors of an article
- # Not called by default (depends on $this->list_authors)
- # Can be set by Special:Export when not exporting whole history
+ /**
+ * Generates the distinct list of authors of an article
+ * Not called by default (depends on $this->list_authors)
+ * Can be set by Special:Export when not exporting whole history
+ *
+ * @param $cond
+ */
protected function do_list_authors( $cond ) {
wfProfileIn( __METHOD__ );
$this->author_list = "<contributors>";
@@ -205,13 +237,15 @@ class WikiExporter {
wfProfileOut( __METHOD__ );
}
+ /**
+ * @param $cond string
+ * @throws MWException
+ * @throws Exception
+ */
protected function dumpFrom( $cond = '' ) {
wfProfileIn( __METHOD__ );
# For logging dumps...
if ( $this->history & self::LOGS ) {
- if ( $this->buffer == WikiExporter::STREAM ) {
- $prev = $this->db->bufferResults( false );
- }
$where = array( 'user_id = log_user' );
# Hide private logs
$hideLogs = LogEventsList::getExcludeClause( $this->db );
@@ -220,16 +254,49 @@ class WikiExporter {
if ( $cond ) $where[] = $cond;
# Get logging table name for logging.* clause
$logging = $this->db->tableName( 'logging' );
- $result = $this->db->select( array( 'logging', 'user' ),
- array( "{$logging}.*", 'user_name' ), // grab the user name
- $where,
- __METHOD__,
- array( 'ORDER BY' => 'log_id', 'USE INDEX' => array( 'logging' => 'PRIMARY' ) )
- );
- $wrapper = $this->db->resultObject( $result );
- $this->outputLogStream( $wrapper );
+
if ( $this->buffer == WikiExporter::STREAM ) {
- $this->db->bufferResults( $prev );
+ $prev = $this->db->bufferResults( false );
+ }
+ $wrapper = null; // Assuring $wrapper is not undefined, if exception occurs early
+ try {
+ $result = $this->db->select( array( 'logging', 'user' ),
+ array( "{$logging}.*", 'user_name' ), // grab the user name
+ $where,
+ __METHOD__,
+ array( 'ORDER BY' => 'log_id', 'USE INDEX' => array( 'logging' => 'PRIMARY' ) )
+ );
+ $wrapper = $this->db->resultObject( $result );
+ $this->outputLogStream( $wrapper );
+ if ( $this->buffer == WikiExporter::STREAM ) {
+ $this->db->bufferResults( $prev );
+ }
+ } catch ( Exception $e ) {
+ // Throwing the exception does not reliably free the resultset, and
+ // would also leave the connection in unbuffered mode.
+
+ // Freeing result
+ try {
+ if ( $wrapper ) {
+ $wrapper->free();
+ }
+ } catch ( Exception $e2 ) {
+ // Already in panic mode -> ignoring $e2 as $e has
+ // higher priority
+ }
+
+ // Putting database back in previous buffer mode
+ try {
+ if ( $this->buffer == WikiExporter::STREAM ) {
+ $this->db->bufferResults( $prev );
+ }
+ } catch ( Exception $e2 ) {
+ // Already in panic mode -> ignoring $e2 as $e has
+ // higher priority
+ }
+
+ // Inform caller about problem
+ throw $e;
}
# For page dumps...
} else {
@@ -279,7 +346,7 @@ class WikiExporter {
} elseif ( $this->history & WikiExporter::RANGE ) {
# Dump of revisions within a specified range
$join['revision'] = array( 'INNER JOIN', 'page_id=rev_page' );
- $opts['ORDER BY'] = 'rev_page ASC, rev_id ASC';
+ $opts['ORDER BY'] = array( 'rev_page ASC', 'rev_id ASC' );
} else {
# Uknown history specification parameter?
wfProfileOut( __METHOD__ );
@@ -300,20 +367,46 @@ class WikiExporter {
$prev = $this->db->bufferResults( false );
}
- wfRunHooks( 'ModifyExportQuery',
+ $wrapper = null; // Assuring $wrapper is not undefined, if exception occurs early
+ try {
+ wfRunHooks( 'ModifyExportQuery',
array( $this->db, &$tables, &$cond, &$opts, &$join ) );
- # Do the query!
- $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join );
- $wrapper = $this->db->resultObject( $result );
- # Output dump results
- $this->outputPageStream( $wrapper );
- if ( $this->list_authors ) {
+ # Do the query!
+ $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join );
+ $wrapper = $this->db->resultObject( $result );
+ # Output dump results
$this->outputPageStream( $wrapper );
- }
- if ( $this->buffer == WikiExporter::STREAM ) {
- $this->db->bufferResults( $prev );
+ if ( $this->buffer == WikiExporter::STREAM ) {
+ $this->db->bufferResults( $prev );
+ }
+ } catch ( Exception $e ) {
+ // Throwing the exception does not reliably free the resultset, and
+ // would also leave the connection in unbuffered mode.
+
+ // Freeing result
+ try {
+ if ( $wrapper ) {
+ $wrapper->free();
+ }
+ } catch ( Exception $e2 ) {
+ // Already in panic mode -> ignoring $e2 as $e has
+ // higher priority
+ }
+
+ // Putting database back in previous buffer mode
+ try {
+ if ( $this->buffer == WikiExporter::STREAM ) {
+ $this->db->bufferResults( $prev );
+ }
+ } catch ( Exception $e2 ) {
+ // Already in panic mode -> ignoring $e2 as $e has
+ // higher priority
+ }
+
+ // Inform caller about problem
+ throw $e;
}
}
wfProfileOut( __METHOD__ );
@@ -324,7 +417,7 @@ class WikiExporter {
* The result set should be sorted/grouped by page to avoid duplicate
* page records in the output.
*
- * The result set will be freed once complete. Should be safe for
+ * Should be safe for
* streaming (non-buffered) queries, as long as it was made on a
* separate database connection not managed by LoadBalancer; some
* blob storage types will make queries to pull source data.
@@ -363,6 +456,9 @@ class WikiExporter {
}
}
+ /**
+ * @param $resultset array
+ */
protected function outputLogStream( $resultset ) {
foreach ( $resultset as $row ) {
$output = $this->writer->writeLogItem( $row );
@@ -377,14 +473,16 @@ class WikiExporter {
class XmlDumpWriter {
/**
* Returns the export schema version.
+ * @deprecated in 1.20; use WikiExporter::schemaVersion() instead
* @return string
*/
function schemaVersion() {
- return "0.6";
+ wfDeprecated( __METHOD__, '1.20' );
+ return WikiExporter::schemaVersion();
}
/**
- * Opens the XML output stream's root <mediawiki> element.
+ * Opens the XML output stream's root "<mediawiki>" element.
* This does not include an xml directive, so is safe to include
* as a subelement in a larger XML stream. Namespace and XML Schema
* references are included.
@@ -395,7 +493,7 @@ class XmlDumpWriter {
*/
function openStream() {
global $wgLanguageCode;
- $ver = $this->schemaVersion();
+ $ver = WikiExporter::schemaVersion();
return Xml::element( 'mediawiki', array(
'xmlns' => "http://www.mediawiki.org/xml/export-$ver/",
'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
@@ -408,6 +506,9 @@ class XmlDumpWriter {
$this->siteInfo();
}
+ /**
+ * @return string
+ */
function siteInfo() {
$info = array(
$this->sitename(),
@@ -420,20 +521,32 @@ class XmlDumpWriter {
"\n </siteinfo>\n";
}
+ /**
+ * @return string
+ */
function sitename() {
global $wgSitename;
return Xml::element( 'sitename', array(), $wgSitename );
}
+ /**
+ * @return string
+ */
function generator() {
global $wgVersion;
return Xml::element( 'generator', array(), "MediaWiki $wgVersion" );
}
+ /**
+ * @return string
+ */
function homelink() {
return Xml::element( 'base', array(), Title::newMainPage()->getCanonicalUrl() );
}
+ /**
+ * @return string
+ */
function caseSetting() {
global $wgCapitalLinks;
// "case-insensitive" option is reserved for future
@@ -441,6 +554,9 @@ class XmlDumpWriter {
return Xml::element( 'case', array(), $sensitivity );
}
+ /**
+ * @return string
+ */
function namespaces() {
global $wgContLang;
$spaces = "<namespaces>\n";
@@ -466,7 +582,7 @@ class XmlDumpWriter {
}
/**
- * Opens a <page> section on the output stream, with data
+ * Opens a "<page>" section on the output stream, with data
* from the given database row.
*
* @param $row object
@@ -486,13 +602,7 @@ class XmlDumpWriter {
$out .= ' ' . Xml::element( 'redirect', array( 'title' => self::canonicalTitle( $redirect ) ) ) . "\n";
}
}
-
- if ( $row->rev_sha1 ) {
- $out .= " " . Xml::element('sha1', null, strval($row->rev_sha1) ) . "\n";
- } else {
- $out .= " <sha1/>\n";
- }
-
+
if ( $row->page_restrictions != '' ) {
$out .= ' ' . Xml::element( 'restrictions', array(),
strval( $row->page_restrictions ) ) . "\n";
@@ -504,16 +614,17 @@ class XmlDumpWriter {
}
/**
- * Closes a <page> section on the output stream.
+ * Closes a "<page>" section on the output stream.
*
* @access private
+ * @return string
*/
function closePage() {
return " </page>\n";
}
/**
- * Dumps a <revision> section on the output stream, with
+ * Dumps a "<revision>" section on the output stream, with
* data filled in from the given database row.
*
* @param $row object
@@ -525,6 +636,9 @@ class XmlDumpWriter {
$out = " <revision>\n";
$out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n";
+ if( $row->rev_parent_id ) {
+ $out .= " " . Xml::element( 'parentid', null, strval( $row->rev_parent_id ) ) . "\n";
+ }
$out .= $this->writeTimestamp( $row->rev_timestamp );
@@ -540,7 +654,13 @@ class XmlDumpWriter {
if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
$out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
} elseif ( $row->rev_comment != '' ) {
- $out .= " " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
+ $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 = '';
@@ -568,7 +688,7 @@ class XmlDumpWriter {
}
/**
- * Dumps a <logitem> section on the output stream, with
+ * Dumps a "<logitem>" section on the output stream, with
* data filled in from the given database row.
*
* @param $row object
@@ -578,64 +698,78 @@ class XmlDumpWriter {
function writeLogItem( $row ) {
wfProfileIn( __METHOD__ );
- $out = " <logitem>\n";
- $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
+ $out = " <logitem>\n";
+ $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
- $out .= $this->writeTimestamp( $row->log_timestamp );
+ $out .= $this->writeTimestamp( $row->log_timestamp, " " );
if ( $row->log_deleted & LogPage::DELETED_USER ) {
- $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
+ $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
} else {
- $out .= $this->writeContributor( $row->log_user, $row->user_name );
+ $out .= $this->writeContributor( $row->log_user, $row->user_name, " " );
}
if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
- $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
+ $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
} elseif ( $row->log_comment != '' ) {
- $out .= " " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n";
+ $out .= " " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n";
}
- $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
- $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
+ $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
+ $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
if ( $row->log_deleted & LogPage::DELETED_ACTION ) {
- $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
+ $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
} else {
$title = Title::makeTitle( $row->log_namespace, $row->log_title );
- $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
- $out .= " " . Xml::elementClean( 'params',
+ $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
+ $out .= " " . Xml::elementClean( 'params',
array( 'xml:space' => 'preserve' ),
strval( $row->log_params ) ) . "\n";
}
- $out .= " </logitem>\n";
+ $out .= " </logitem>\n";
wfProfileOut( __METHOD__ );
return $out;
}
- function writeTimestamp( $timestamp ) {
+ /**
+ * @param $timestamp string
+ * @param $indent string Default to six spaces
+ * @return string
+ */
+ function writeTimestamp( $timestamp, $indent = " " ) {
$ts = wfTimestamp( TS_ISO_8601, $timestamp );
- return " " . Xml::element( 'timestamp', null, $ts ) . "\n";
+ return $indent . Xml::element( 'timestamp', null, $ts ) . "\n";
}
- function writeContributor( $id, $text ) {
- $out = " <contributor>\n";
+ /**
+ * @param $id
+ * @param $text string
+ * @param $indent string Default to six spaces
+ * @return string
+ */
+ function writeContributor( $id, $text, $indent = " " ) {
+ $out = $indent . "<contributor>\n";
if ( $id || !IP::isValid( $text ) ) {
- $out .= " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
- $out .= " " . Xml::element( 'id', null, strval( $id ) ) . "\n";
+ $out .= $indent . " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
+ $out .= $indent . " " . Xml::element( 'id', null, strval( $id ) ) . "\n";
} else {
- $out .= " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
+ $out .= $indent . " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
}
- $out .= " </contributor>\n";
+ $out .= $indent . "</contributor>\n";
return $out;
}
/**
* Warning! This data is potentially inconsistent. :(
+ * @param $row
+ * @param $dumpContents bool
+ * @return string
*/
function writeUploads( $row, $dumpContents = false ) {
- if ( $row->page_namespace == NS_IMAGE ) {
+ if ( $row->page_namespace == NS_FILE ) {
$img = wfLocalFile( $row->page_title );
if ( $img && $img->exists() ) {
$out = '';
@@ -670,10 +804,15 @@ class XmlDumpWriter {
} else {
$contents = '';
}
+ if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
+ $comment = Xml::element( 'comment', array( 'deleted' => 'deleted' ) );
+ } else {
+ $comment = Xml::elementClean( 'comment', null, $file->getDescription() );
+ }
return " <upload>\n" .
$this->writeTimestamp( $file->getTimestamp() ) .
$this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) .
- " " . Xml::elementClean( 'comment', null, $file->getDescription() ) . "\n" .
+ " " . $comment . "\n" .
" " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
$archiveName .
" " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
@@ -688,7 +827,7 @@ class XmlDumpWriter {
* Return prefixed text form of title, but using the content language's
* canonical namespace. This skips any special-casing such as gendered
* user namespaces -- which while useful, are not yet listed in the
- * XML <siteinfo> data so are unsafe in export.
+ * XML "<siteinfo>" data so are unsafe in export.
*
* @param Title $title
* @return string
@@ -716,32 +855,55 @@ class XmlDumpWriter {
* @ingroup Dump
*/
class DumpOutput {
+
+ /**
+ * @param $string string
+ */
function writeOpenStream( $string ) {
$this->write( $string );
}
+ /**
+ * @param $string string
+ */
function writeCloseStream( $string ) {
$this->write( $string );
}
+ /**
+ * @param $page
+ * @param $string string
+ */
function writeOpenPage( $page, $string ) {
$this->write( $string );
}
+ /**
+ * @param $string string
+ */
function writeClosePage( $string ) {
$this->write( $string );
}
+ /**
+ * @param $rev
+ * @param $string string
+ */
function writeRevision( $rev, $string ) {
$this->write( $string );
}
+ /**
+ * @param $rev
+ * @param $string string
+ */
function writeLogItem( $rev, $string ) {
$this->write( $string );
}
/**
* Override to write to a different stream type.
+ * @param $string string
* @return bool
*/
function write( $string ) {
@@ -773,6 +935,7 @@ class DumpOutput {
/**
* Returns the name of the file or files which are
* being written to, if there are any.
+ * @return null
*/
function getFilenames() {
return NULL;
@@ -784,27 +947,56 @@ class DumpOutput {
* @ingroup Dump
*/
class DumpFileOutput extends DumpOutput {
- protected $handle, $filename;
+ protected $handle = false, $filename;
+ /**
+ * @param $file
+ */
function __construct( $file ) {
$this->handle = fopen( $file, "wt" );
$this->filename = $file;
}
+ /**
+ * @param $string string
+ */
+ function writeCloseStream( $string ) {
+ parent::writeCloseStream( $string );
+ if ( $this->handle ) {
+ fclose( $this->handle );
+ $this->handle = false;
+ }
+ }
+
+ /**
+ * @param $string string
+ */
function write( $string ) {
fputs( $this->handle, $string );
}
+ /**
+ * @param $newname
+ */
function closeRenameAndReopen( $newname ) {
$this->closeAndRename( $newname, true );
}
+ /**
+ * @param $newname
+ * @throws MWException
+ */
function renameOrException( $newname ) {
if (! rename( $this->filename, $newname ) ) {
throw new MWException( __METHOD__ . ": rename of file {$this->filename} to $newname failed\n" );
}
}
+ /**
+ * @param $newname array
+ * @return mixed
+ * @throws MWException
+ */
function checkRenameArgCount( $newname ) {
if ( is_array( $newname ) ) {
if ( count( $newname ) > 1 ) {
@@ -816,10 +1008,17 @@ class DumpFileOutput extends DumpOutput {
return $newname;
}
+ /**
+ * @param $newname mixed
+ * @param $open bool
+ */
function closeAndRename( $newname, $open = false ) {
$newname = $this->checkRenameArgCount( $newname );
if ( $newname ) {
- fclose( $this->handle );
+ if ( $this->handle ) {
+ fclose( $this->handle );
+ $this->handle = false;
+ }
$this->renameOrException( $newname );
if ( $open ) {
$this->handle = fopen( $this->filename, "wt" );
@@ -827,6 +1026,9 @@ class DumpFileOutput extends DumpOutput {
}
}
+ /**
+ * @return string|null
+ */
function getFilenames() {
return $this->filename;
}
@@ -840,7 +1042,12 @@ class DumpFileOutput extends DumpOutput {
*/
class DumpPipeOutput extends DumpFileOutput {
protected $command, $filename;
+ protected $procOpenResource = false;
+ /**
+ * @param $command
+ * @param $file null
+ */
function __construct( $command, $file = null ) {
if ( !is_null( $file ) ) {
$command .= " > " . wfEscapeShellArg( $file );
@@ -851,6 +1058,20 @@ class DumpPipeOutput extends DumpFileOutput {
$this->filename = $file;
}
+ /**
+ * @param $string string
+ */
+ function writeCloseStream( $string ) {
+ parent::writeCloseStream( $string );
+ if ( $this->procOpenResource ) {
+ proc_close( $this->procOpenResource );
+ $this->procOpenResource = false;
+ }
+ }
+
+ /**
+ * @param $command
+ */
function startCommand( $command ) {
$spec = array(
0 => array( "pipe", "r" ),
@@ -860,15 +1081,28 @@ class DumpPipeOutput extends DumpFileOutput {
$this->handle = $pipes[0];
}
+ /**
+ * @param mixed $newname
+ */
function closeRenameAndReopen( $newname ) {
$this->closeAndRename( $newname, true );
}
+ /**
+ * @param $newname mixed
+ * @param $open bool
+ */
function closeAndRename( $newname, $open = false ) {
$newname = $this->checkRenameArgCount( $newname );
if ( $newname ) {
- fclose( $this->handle );
- proc_close( $this->procOpenResource );
+ if ( $this->handle ) {
+ fclose( $this->handle );
+ $this->handle = false;
+ }
+ if ( $this->procOpenResource ) {
+ proc_close( $this->procOpenResource );
+ $this->procOpenResource = false;
+ }
$this->renameOrException( $newname );
if ( $open ) {
$command = $this->command;
@@ -885,6 +1119,10 @@ class DumpPipeOutput extends DumpFileOutput {
* @ingroup Dump
*/
class DumpGZipOutput extends DumpPipeOutput {
+
+ /**
+ * @param $file string
+ */
function __construct( $file ) {
parent::__construct( "gzip", $file );
}
@@ -895,6 +1133,10 @@ class DumpGZipOutput extends DumpPipeOutput {
* @ingroup Dump
*/
class DumpBZip2Output extends DumpPipeOutput {
+
+ /**
+ * @param $file string
+ */
function __construct( $file ) {
parent::__construct( "bzip2", $file );
}
@@ -905,12 +1147,20 @@ class DumpBZip2Output extends DumpPipeOutput {
* @ingroup Dump
*/
class Dump7ZipOutput extends DumpPipeOutput {
+
+ /**
+ * @param $file string
+ */
function __construct( $file ) {
$command = $this->setup7zCommand( $file );
parent::__construct( $command );
$this->filename = $file;
}
+ /**
+ * @param $file string
+ * @return string
+ */
function setup7zCommand( $file ) {
$command = "7za a -bd -si " . wfEscapeShellArg( $file );
// Suppress annoying useless crap from p7zip
@@ -919,6 +1169,10 @@ class Dump7ZipOutput extends DumpPipeOutput {
return( $command );
}
+ /**
+ * @param $newname string
+ * @param $open bool
+ */
function closeAndRename( $newname, $open = false ) {
$newname = $this->checkRenameArgCount( $newname );
if ( $newname ) {
@@ -933,8 +1187,6 @@ class Dump7ZipOutput extends DumpPipeOutput {
}
}
-
-
/**
* Dump output filter class.
* This just does output filtering and streaming; XML formatting is done
@@ -942,18 +1194,44 @@ class Dump7ZipOutput extends DumpPipeOutput {
* @ingroup Dump
*/
class DumpFilter {
+
+ /**
+ * @var DumpOutput
+ * FIXME will need to be made protected whenever legacy code
+ * is updated.
+ */
+ public $sink;
+
+ /**
+ * @var bool
+ */
+ protected $sendingThisPage;
+
+ /**
+ * @param $sink DumpOutput
+ */
function __construct( &$sink ) {
$this->sink =& $sink;
}
+ /**
+ * @param $string string
+ */
function writeOpenStream( $string ) {
$this->sink->writeOpenStream( $string );
}
+ /**
+ * @param $string string
+ */
function writeCloseStream( $string ) {
$this->sink->writeCloseStream( $string );
}
+ /**
+ * @param $page
+ * @param $string string
+ */
function writeOpenPage( $page, $string ) {
$this->sendingThisPage = $this->pass( $page, $string );
if ( $this->sendingThisPage ) {
@@ -961,6 +1239,9 @@ class DumpFilter {
}
}
+ /**
+ * @param $string string
+ */
function writeClosePage( $string ) {
if ( $this->sendingThisPage ) {
$this->sink->writeClosePage( $string );
@@ -968,30 +1249,49 @@ class DumpFilter {
}
}
+ /**
+ * @param $rev
+ * @param $string string
+ */
function writeRevision( $rev, $string ) {
if ( $this->sendingThisPage ) {
$this->sink->writeRevision( $rev, $string );
}
}
+ /**
+ * @param $rev
+ * @param $string string
+ */
function writeLogItem( $rev, $string ) {
$this->sink->writeRevision( $rev, $string );
}
+ /**
+ * @param $newname string
+ */
function closeRenameAndReopen( $newname ) {
$this->sink->closeRenameAndReopen( $newname );
}
+ /**
+ * @param $newname string
+ * @param $open bool
+ */
function closeAndRename( $newname, $open = false ) {
$this->sink->closeAndRename( $newname, $open );
}
+ /**
+ * @return array
+ */
function getFilenames() {
return $this->sink->getFilenames();
}
/**
* Override for page-based filter types.
+ * @param $page
* @return bool
*/
function pass( $page ) {
@@ -1004,6 +1304,11 @@ class DumpFilter {
* @ingroup Dump
*/
class DumpNotalkFilter extends DumpFilter {
+
+ /**
+ * @param $page
+ * @return bool
+ */
function pass( $page ) {
return !MWNamespace::isTalk( $page->page_namespace );
}
@@ -1017,6 +1322,10 @@ class DumpNamespaceFilter extends DumpFilter {
var $invert = false;
var $namespaces = array();
+ /**
+ * @param $sink DumpOutput
+ * @param $param
+ */
function __construct( &$sink, $param ) {
parent::__construct( $sink );
@@ -1059,6 +1368,10 @@ class DumpNamespaceFilter extends DumpFilter {
}
}
+ /**
+ * @param $page
+ * @return bool
+ */
function pass( $page ) {
$match = isset( $this->namespaces[$page->page_namespace] );
return $this->invert xor $match;
@@ -1073,11 +1386,18 @@ class DumpNamespaceFilter extends DumpFilter {
class DumpLatestFilter extends DumpFilter {
var $page, $pageString, $rev, $revString;
+ /**
+ * @param $page
+ * @param $string string
+ */
function writeOpenPage( $page, $string ) {
$this->page = $page;
$this->pageString = $string;
}
+ /**
+ * @param $string string
+ */
function writeClosePage( $string ) {
if ( $this->rev ) {
$this->sink->writeOpenPage( $this->page, $this->pageString );
@@ -1090,6 +1410,10 @@ class DumpLatestFilter extends DumpFilter {
$this->pageString = null;
}
+ /**
+ * @param $rev
+ * @param $string string
+ */
function writeRevision( $rev, $string ) {
if ( $rev->rev_id == $this->page->page_latest ) {
$this->rev = $rev;
@@ -1103,51 +1427,82 @@ class DumpLatestFilter extends DumpFilter {
* @ingroup Dump
*/
class DumpMultiWriter {
+
+ /**
+ * @param $sinks
+ */
function __construct( $sinks ) {
$this->sinks = $sinks;
$this->count = count( $sinks );
}
+ /**
+ * @param $string string
+ */
function writeOpenStream( $string ) {
for ( $i = 0; $i < $this->count; $i++ ) {
$this->sinks[$i]->writeOpenStream( $string );
}
}
+ /**
+ * @param $string string
+ */
function writeCloseStream( $string ) {
for ( $i = 0; $i < $this->count; $i++ ) {
$this->sinks[$i]->writeCloseStream( $string );
}
}
+ /**
+ * @param $page
+ * @param $string string
+ */
function writeOpenPage( $page, $string ) {
for ( $i = 0; $i < $this->count; $i++ ) {
$this->sinks[$i]->writeOpenPage( $page, $string );
}
}
+ /**
+ * @param $string
+ */
function writeClosePage( $string ) {
for ( $i = 0; $i < $this->count; $i++ ) {
$this->sinks[$i]->writeClosePage( $string );
}
}
+ /**
+ * @param $rev
+ * @param $string
+ */
function writeRevision( $rev, $string ) {
for ( $i = 0; $i < $this->count; $i++ ) {
$this->sinks[$i]->writeRevision( $rev, $string );
}
}
+ /**
+ * @param $newnames
+ */
function closeRenameAndReopen( $newnames ) {
$this->closeAndRename( $newnames, true );
}
+ /**
+ * @param $newnames array
+ * @param bool $open
+ */
function closeAndRename( $newnames, $open = false ) {
for ( $i = 0; $i < $this->count; $i++ ) {
$this->sinks[$i]->closeAndRename( $newnames[$i], $open );
}
}
+ /**
+ * @return array
+ */
function getFilenames() {
$filenames = array();
for ( $i = 0; $i < $this->count; $i++ ) {
@@ -1158,6 +1513,10 @@ class DumpMultiWriter {
}
+/**
+ * @param $string string
+ * @return string
+ */
function xmlsafe( $string ) {
wfProfileIn( __FUNCTION__ );
diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php
index b8704758..34683253 100644
--- a/includes/ExternalEdit.php
+++ b/includes/ExternalEdit.php
@@ -80,10 +80,16 @@ class ExternalEdit extends ContextSource {
} elseif ( $this->getRequest()->getVal( 'mode' ) == 'file' ) {
$type = "Edit file";
$image = wfLocalFile( $this->getTitle() );
- $urls = array( 'File' => array(
- 'Extension' => $image->getExtension(),
- 'URL' => $image->getCanonicalURL()
- ) );
+ 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
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
index 3bee6ed8..61d4ef7c 100644
--- a/includes/ExternalStore.php
+++ b/includes/ExternalStore.php
@@ -1,5 +1,26 @@
<?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
*/
@@ -24,7 +45,7 @@ class ExternalStore {
*
* @param $url String: The URL of the text to get
* @param $params Array: associative array of parameters for the ExternalStore object.
- * @return The text stored or false on error
+ * @return string|bool The text stored or false on error
*/
static function fetchFromURL( $url, $params = array() ) {
global $wgExternalStores;
@@ -81,7 +102,7 @@ class ExternalStore {
* @param $url
* @param $data
* @param $params array
- * @return string|false The URL of the stored data item, or false on error
+ * @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 );
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
index 4920a91c..6f2b33e1 100644
--- a/includes/ExternalStoreDB.php
+++ b/includes/ExternalStoreDB.php
@@ -1,4 +1,24 @@
<?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
@@ -73,6 +93,7 @@ class ExternalStoreDB {
/**
* 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 );
@@ -157,7 +178,7 @@ class ExternalStoreDB {
throw new MWException( __METHOD__.': no insert ID' );
}
if ( $dbw->getFlag( DBO_TRX ) ) {
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
}
return "DB://$cluster/$id";
}
diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php
index 092ff7d8..311e32b3 100644
--- a/includes/ExternalStoreHttp.php
+++ b/includes/ExternalStoreHttp.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * External storage using HTTP requests.
+ *
+ * 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
+ */
/**
* Example class for HTTP accessable external objects.
diff --git a/includes/ExternalUser.php b/includes/ExternalUser.php
index 37716390..9a01deb7 100644
--- a/includes/ExternalUser.php
+++ b/includes/ExternalUser.php
@@ -18,6 +18,8 @@
* 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
*/
/**
@@ -98,7 +100,7 @@ abstract class ExternalUser {
* This is a wrapper around newFromId().
*
* @param $user User
- * @return ExternalUser|false
+ * @return ExternalUser|bool False on failure
*/
public static function newFromUser( $user ) {
global $wgExternalAuthType;
diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php
index 8415ec08..60f7600d 100644
--- a/includes/FakeTitle.php
+++ b/includes/FakeTitle.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Fake title class that triggers an error if any members are called.
+ *
+ * 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
+ */
/**
* Fake title class that triggers an error if any members are called
@@ -59,7 +79,6 @@ class FakeTitle extends Title {
function getSkinFromCssJsSubpage() { $this->error(); }
function isCssSubpage() { $this->error(); }
function isJsSubpage() { $this->error(); }
- function userCanEditCssJsSubpage() { $this->error(); }
function userCanEditCssSubpage() { $this->error(); }
function userCanEditJsSubpage() { $this->error(); }
function isCascadeProtected() { $this->error(); }
@@ -88,8 +107,6 @@ class FakeTitle extends Title {
function moveNoAuth( &$nt ) { $this->error(); }
function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { $this->error(); }
function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); }
- function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) { $this->error(); }
- function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) { $this->error(); }
function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); }
function isSingleRevRedirect() { $this->error(); }
function isValidMoveTarget( $nt ) { $this->error(); }
diff --git a/includes/Fallback.php b/includes/Fallback.php
index b517cd16..4b138c11 100644
--- a/includes/Fallback.php
+++ b/includes/Fallback.php
@@ -1,6 +1,7 @@
<?php
-
/**
+ * Fallback functions for PHP installed without mbstring support.
+ *
* 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
@@ -16,6 +17,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
+ * @file
*/
/**
diff --git a/includes/Feed.php b/includes/Feed.php
index 351f3572..f9dbf5ba 100644
--- a/includes/Feed.php
+++ b/includes/Feed.php
@@ -183,34 +183,35 @@ class FeedItem {
* @todo document (needs one-sentence top-level class description).
* @ingroup Feed
*/
-class ChannelFeed extends FeedItem {
- /**#@+
- * Abstract function, override!
- * @abstract
- */
-
+abstract class ChannelFeed extends FeedItem {
/**
* Generate Header of the feed
+ * @par Example:
+ * @code
+ * print "<feed>";
+ * @endcode
+ * @param $item
*/
- function outHeader() {
- # print "<feed>";
- }
+ abstract public function outHeader();
/**
* Generate an item
+ * @par Example:
+ * @code
+ * print "<item>...</item>";
+ * @endcode
* @param $item
*/
- function outItem( $item ) {
- # print "<item>...</item>";
- }
+ abstract public function outItem( $item );
/**
* Generate Footer of the feed
+ * @par Example:
+ * @code
+ * print "</feed>";
+ * @endcode
*/
- function outFooter() {
- # print "</feed>";
- }
- /**#@-*/
+ abstract public function outFooter();
/**
* Setup and send HTTP headers. Don't send any content;
@@ -334,6 +335,7 @@ class RSSFeed extends ChannelFeed {
class AtomFeed extends ChannelFeed {
/**
* @todo document
+ * @return string
*/
function formatTime( $ts ) {
// need to use RFC 822 time format at least for rss2.0
diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php
index cf42329b..11b2675d 100644
--- a/includes/FeedUtils.php
+++ b/includes/FeedUtils.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Helper functions for feeds.
+ *
+ * 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 Feed
+ */
/**
* Helper functions for feeds
@@ -64,7 +85,7 @@ class FeedUtils {
$row->rc_last_oldid, $row->rc_this_oldid,
$timestamp,
($row->rc_deleted & Revision::DELETED_COMMENT)
- ? wfMsgHtml('rev-deleted-comment')
+ ? wfMessage('rev-deleted-comment')->escaped()
: $row->rc_comment,
$actiontext
);
@@ -108,21 +129,22 @@ class FeedUtils {
if( $oldid ) {
wfProfileIn( __METHOD__."-dodiff" );
- #$diffText = $de->getDiff( wfMsg( 'revisionasof',
+ #$diffText = $de->getDiff( wfMessage( 'revisionasof',
# $wgLang->timeanddate( $timestamp ),
# $wgLang->date( $timestamp ),
- # $wgLang->time( $timestamp ) ),
- # wfMsg( 'currentrev' ) );
+ # $wgLang->time( $timestamp ) )->text(),
+ # wfMessage( 'currentrev' )->text() );
+ $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(
- wfMsg( 'previousrevision' ), // hack
- wfMsg( 'revisionasof',
+ wfMessage( 'previousrevision' )->text(), // hack
+ wfMessage( 'revisionasof',
$wgLang->timeanddate( $timestamp ),
$wgLang->date( $timestamp ),
- $wgLang->time( $timestamp ) ) );
+ $wgLang->time( $timestamp ) )->text() );
}
if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) {
@@ -148,7 +170,7 @@ class FeedUtils {
// Omit large new page diffs, bug 29110
$diffText = self::getDiffLink( $title, $newid );
} else {
- $diffText = '<p><b>' . wfMsg( 'newpage' ) . '</b></p>' .
+ $diffText = '<p><b>' . wfMessage( 'newpage' )->text() . '</b></p>' .
'<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>';
}
}
@@ -165,6 +187,7 @@ class FeedUtils {
* @param $title Title object: used to generate the diff URL
* @param $newid Integer newid for this diff
* @param $oldid Integer|null oldid for the diff. Null means it is a new article
+ * @return string
*/
protected static function getDiffLink( Title $title, $newid, $oldid = null ) {
$queryParameters = ($oldid == null)
@@ -173,7 +196,7 @@ class FeedUtils {
$diffUrl = $title->getFullUrl( $queryParameters );
$diffLink = Html::element( 'a', array( 'href' => $diffUrl ),
- wfMsgForContent( 'showdiff' ) );
+ wfMessage( 'showdiff' )->inContentLanguage()->text() );
return $diffLink;
}
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
index 11f9aea5..e75ad729 100644
--- a/includes/FileDeleteForm.php
+++ b/includes/FileDeleteForm.php
@@ -1,10 +1,31 @@
<?php
+/**
+ * File deletion user interface.
+ *
+ * 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 Rob Church <robchur@gmail.com>
+ * @ingroup Media
+ */
/**
* File deletion user interface
*
* @ingroup Media
- * @author Rob Church <robchur@gmail.com>
*/
class FileDeleteForm {
@@ -80,7 +101,8 @@ class FileDeleteForm {
$reason = $deleteReason;
} elseif ( $deleteReason != '' ) {
// Entry from drop down menu + additional comment
- $reason = $deleteReasonList . wfMsgForContent( 'colon-separator' ) . $deleteReason;
+ $reason = $deleteReasonList . wfMessage( 'colon-separator' )
+ ->inContentLanguage()->text() . $deleteReason;
} else {
$reason = $deleteReasonList;
}
@@ -89,9 +111,7 @@ class FileDeleteForm {
if( !$status->isGood() ) {
$wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" );
- $wgOut->addHTML( '<span class="error">' );
- $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
- $wgOut->addHTML( '</span>' );
+ $wgOut->addWikiText( '<div class="error">' . $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) . '</div>' );
}
if( $status->ok ) {
$wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
@@ -100,10 +120,12 @@ class FileDeleteForm {
// file, otherwise go back to the description page
$wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
- if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) {
- WatchAction::doWatch( $this->title, $wgUser );
- } elseif ( $this->title->userIsWatching() ) {
- WatchAction::doUnwatch( $this->title, $wgUser );
+ 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 );
+ }
}
}
return;
@@ -122,6 +144,7 @@ class FileDeleteForm {
* @param $reason String: reason of the deletion
* @param $suppress Boolean: whether to mark all deleted versions as restricted
* @param $user User object performing the request
+ * @return bool|Status
*/
public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress, User $user = null ) {
if ( $user === null ) {
@@ -134,12 +157,20 @@ class FileDeleteForm {
$status = $file->deleteOld( $oldimage, $reason, $suppress );
if( $status->ok ) {
// Need to do a log item
- $log = new LogPage( 'delete' );
- $logComment = wfMsgForContent( 'deletedrevision', $oldimage );
+ $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
if( trim( $reason ) != '' ) {
- $logComment .= wfMsgForContent( 'colon-separator' ) . $reason;
+ $logComment .= wfMessage( 'colon-separator' )
+ ->inContentLanguage()->text() . $reason;
}
- $log->addEntry( 'delete', $title, $logComment );
+
+ $logtype = $suppress ? 'suppress' : 'delete';
+
+ $logEntry = new ManualLogEntry( $logtype, 'delete' );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $title );
+ $logEntry->setComment( $logComment );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
}
} else {
$status = Status::newFatal( 'cannotdelete',
@@ -150,17 +181,20 @@ class FileDeleteForm {
try {
// delete the associated article first
$error = '';
- if ( $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user ) >= WikiPage::DELETE_SUCCESS ) {
+ $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user );
+ // doDeleteArticleReal() returns a non-fatal error status if the page
+ // or revision is missing, so check for isOK() rather than isGood()
+ if ( $deleteStatus->isOK() ) {
$status = $file->delete( $reason, $suppress );
if( $status->isOK() ) {
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
} else {
- $dbw->rollback();
+ $dbw->rollback( __METHOD__ );
}
}
} catch ( MWException $e ) {
// rollback before returning to prevent UI from displaying incorrect "View or restore N deleted edits?"
- $dbw->rollback();
+ $dbw->rollback( __METHOD__ );
throw $e;
}
}
@@ -182,7 +216,7 @@ class FileDeleteForm {
$suppress = "<tr id=\"wpDeleteSuppressRow\">
<td></td>
<td class='mw-input'><strong>" .
- Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
+ Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(),
'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '3' ) ) .
"</strong></td>
</tr>";
@@ -190,27 +224,32 @@ class FileDeleteForm {
$suppress = '';
}
- $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->title->userIsWatching();
+ $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $wgUser->isWatched( $this->title );
$form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction(),
'id' => 'mw-img-deleteconfirm' ) ) .
Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) .
+ Xml::element( 'legend', null, wfMessage( 'filedelete-legend' )->text() ) .
Html::hidden( 'wpEditToken', $wgUser->getEditToken( $this->oldimage ) ) .
$this->prepareMessage( 'filedelete-intro' ) .
Xml::openElement( 'table', array( 'id' => 'mw-img-deleteconfirm-table' ) ) .
"<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'filedelete-comment' ), 'wpDeleteReasonList' ) .
+ Xml::label( wfMessage( 'filedelete-comment' )->text(), 'wpDeleteReasonList' ) .
"</td>
<td class='mw-input'>" .
- Xml::listDropDown( 'wpDeleteReasonList',
- wfMsgForContent( 'filedelete-reason-dropdown' ),
- wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) .
+ Xml::listDropDown(
+ 'wpDeleteReasonList',
+ wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->text(),
+ wfMessage( 'filedelete-reason-otherlist' )->inContentLanguage()->text(),
+ '',
+ 'wpReasonDropDown',
+ 1
+ ) .
"</td>
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'filedelete-otherreason' ), 'wpReason' ) .
+ Xml::label( wfMessage( 'filedelete-otherreason' )->text(), 'wpReason' ) .
"</td>
<td class='mw-input'>" .
Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ),
@@ -223,7 +262,7 @@ class FileDeleteForm {
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'watchthis' ),
+ Xml::checkLabel( wfMessage( 'watchthis' )->text(),
'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
"</td>
</tr>";
@@ -232,7 +271,7 @@ class FileDeleteForm {
<tr>
<td></td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'filedelete-submit' ),
+ Xml::submitButton( wfMessage( 'filedelete-submit' )->text(),
array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '4' ) ) .
"</td>
</tr>" .
@@ -244,7 +283,7 @@ class FileDeleteForm {
$title = Title::makeTitle( NS_MEDIAWIKI, 'Filedelete-reason-dropdown' );
$link = Linker::link(
$title,
- wfMsgHtml( 'filedelete-edit-reasonlist' ),
+ wfMessage( 'filedelete-edit-reasonlist' )->escaped(),
array(),
array( 'action' => 'edit' )
);
@@ -259,7 +298,8 @@ class FileDeleteForm {
*/
private function showLogEntries() {
global $wgOut;
- $wgOut->addHTML( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ $deleteLogPage = new LogPage( 'delete' );
+ $wgOut->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" );
LogEventsList::showLogExtract( $wgOut, 'delete', $this->title );
}
@@ -274,19 +314,17 @@ class FileDeleteForm {
private function prepareMessage( $message ) {
global $wgLang;
if( $this->oldimage ) {
- return wfMsgExt(
+ return wfMessage(
"{$message}-old", # To ensure grep will find them: 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
- 'parse',
wfEscapeWikiText( $this->title->getText() ),
$wgLang->date( $this->getTimestamp(), true ),
$wgLang->time( $this->getTimestamp(), true ),
- wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) );
+ wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) )->parseAsBlock();
} else {
- return wfMsgExt(
+ return wfMessage(
$message,
- 'parse',
wfEscapeWikiText( $this->title->getText() )
- );
+ )->parseAsBlock();
}
}
diff --git a/includes/ForkController.php b/includes/ForkController.php
index 9cacef54..448bc03b 100644
--- a/includes/ForkController.php
+++ b/includes/ForkController.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Class for managing forking command line scripts.
+ *
+ * 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 managing forking command line scripts.
@@ -49,6 +69,7 @@ class ForkController {
* This will return 'child' in the child processes. In the parent process,
* it will run until all the child processes exit or a TERM signal is
* received. It will then return 'done'.
+ * @return string
*/
public function start() {
// Trap SIGTERM
@@ -116,8 +137,10 @@ class ForkController {
protected function prepareEnvironment() {
global $wgMemc;
- // Don't share DB or memcached connections
+ // Don't share DB, storage, or memcached connections
wfGetLBFactory()->destroyInstance();
+ FileBackendGroup::destroySingleton();
+ LockManagerGroup::destroySingleton();
ObjectCache::clear();
$wgMemc = null;
}
diff --git a/includes/FormOptions.php b/includes/FormOptions.php
index ccc87d8a..33bbd86a 100644
--- a/includes/FormOptions.php
+++ b/includes/FormOptions.php
@@ -1,16 +1,35 @@
<?php
/**
* Helper class to keep track of options when mixing links and form elements.
- * @todo This badly need some examples and tests :-)
*
* Copyright © 2008, Niklas Laxstiröm
- *
* Copyright © 2011, Antoine Musso
*
+ * 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
* @author Antoine Musso
*/
+/**
+ * Helper class to keep track of options when mixing links and form elements.
+ *
+ * @todo This badly need some examples and tests :-)
+ */
class FormOptions implements ArrayAccess {
/** @name Type constants
* Used internally to map an option value to a WebRequest accessor
@@ -65,7 +84,7 @@ class FormOptions implements ArrayAccess {
*
* @param $data Mixed: value to guess type for
* @exception MWException Unsupported datatype
- * @return Type constant
+ * @return int Type constant
*/
public static function guessType( $data ) {
if ( is_bool( $data ) ) {
@@ -291,11 +310,17 @@ class FormOptions implements ArrayAccess {
* @see http://php.net/manual/en/class.arrayaccess.php
*/
/* @{ */
- /** Whether option exist*/
+ /**
+ * Whether option exist
+ * @return bool
+ */
public function offsetExists( $name ) {
return isset( $this->options[$name] );
}
- /** Retrieve an option value */
+ /**
+ * Retrieve an option value
+ * @return Mixed
+ */
public function offsetGet( $name ) {
return $this->getValue( $name );
}
diff --git a/includes/GitInfo.php b/includes/GitInfo.php
new file mode 100644
index 00000000..c3c30733
--- /dev/null
+++ b/includes/GitInfo.php
@@ -0,0 +1,214 @@
+<?php
+/**
+ * A class to help return information about a git repo MediaWiki may be inside
+ * This is used by Special:Version and is also useful for the LocalSettings.php
+ * of anyone working on large branches in git to setup config that show up only
+ * when specific branches are currently checked out.
+ *
+ * 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 GitInfo {
+
+ /**
+ * Singleton for the repo at $IP
+ */
+ protected static $repo = null;
+
+ /**
+ * Location of the .git directory
+ */
+ protected $basedir;
+
+ /**
+ * Map of repo URLs to viewer URLs. Access via static method getViewers().
+ */
+ private static $viewers = false;
+
+ /**
+ * @param $dir string The root directory of the repo where the .git dir can be found
+ */
+ public function __construct( $dir ) {
+ $this->basedir = "{$dir}/.git/";
+ }
+
+ /**
+ * Return a singleton for the repo at $IP
+ * @return GitInfo
+ */
+ public static function repo() {
+ global $IP;
+ if ( is_null( self::$repo ) ) {
+ self::$repo = new self( $IP );
+ }
+ return self::$repo;
+ }
+
+ /**
+ * Check if a string looks like a hex encoded SHA1 hash
+ *
+ * @param $str string The string to check
+ * @return bool Whether or not the string looks like a SHA1
+ */
+ public static function isSHA1( $str ) {
+ return !!preg_match( '/^[0-9A-F]{40}$/i', $str );
+ }
+
+ /**
+ * Return the HEAD of the repo (without any opening "ref: ")
+ * @return string The HEAD
+ */
+ public function getHead() {
+ $HEADfile = "{$this->basedir}/HEAD";
+
+ if ( !is_readable( $HEADfile ) ) {
+ return false;
+ }
+
+ $HEAD = file_get_contents( $HEADfile );
+
+ if ( preg_match( "/ref: (.*)/", $HEAD, $m ) ) {
+ return rtrim( $m[1] );
+ } else {
+ return rtrim( $HEAD );
+ }
+ }
+
+ /**
+ * Return the SHA1 for the current HEAD of the repo
+ * @return string A SHA1 or false
+ */
+ public function getHeadSHA1() {
+ $HEAD = $this->getHead();
+
+ // If detached HEAD may be a SHA1
+ if ( self::isSHA1( $HEAD ) ) {
+ return $HEAD;
+ }
+
+ // If not a SHA1 it may be a ref:
+ $REFfile = "{$this->basedir}{$HEAD}";
+ if ( !is_readable( $REFfile ) ) {
+ return false;
+ }
+
+ $sha1 = rtrim( file_get_contents( $REFfile ) );
+
+ return $sha1;
+ }
+
+ /**
+ * Return the name of the current branch, or HEAD if not found
+ * @return string The branch name, HEAD, or false
+ */
+ public function getCurrentBranch() {
+ $HEAD = $this->getHead();
+ if ( $HEAD && preg_match( "#^refs/heads/(.*)$#", $HEAD, $m ) ) {
+ return $m[1];
+ } else {
+ return $HEAD;
+ }
+ }
+
+ /**
+ * Get an URL to a web viewer link to the HEAD revision.
+ *
+ * @return string|bool string if an URL is available or false otherwise.
+ */
+ public function getHeadViewUrl() {
+ $config = "{$this->basedir}/config";
+ if ( !is_readable( $config ) ) {
+ return false;
+ }
+
+ $configArray = parse_ini_file( $config, true );
+ $remote = false;
+
+ // Use the "origin" remote repo if available or any other repo if not.
+ if ( isset( $configArray['remote origin'] ) ) {
+ $remote = $configArray['remote origin'];
+ } else {
+ foreach( $configArray as $sectionName => $sectionConf ) {
+ if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
+ $remote = $sectionConf;
+ }
+ }
+ }
+
+ if ( $remote === false || !isset( $remote['url'] ) ) {
+ return false;
+ }
+
+ $url = $remote['url'];
+ if ( substr( $url, -4 ) !== '.git' ) {
+ $url .= '.git';
+ }
+ foreach( self::getViewers() as $repo => $viewer ) {
+ $pattern = '#^' . $repo . '$#';
+ if ( preg_match( $pattern, $url ) ) {
+ $viewerUrl = preg_replace( $pattern, $viewer, $url );
+ $headSHA1 = $this->getHeadSHA1();
+ $replacements = array(
+ '%h' => substr( $headSHA1, 0, 7 ),
+ '%H' => $headSHA1
+ );
+ return strtr( $viewerUrl, $replacements );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @see self::getHeadSHA1
+ * @return string
+ */
+ public static function headSHA1() {
+ return self::repo()->getHeadSHA1();
+ }
+
+ /**
+ * @see self::getCurrentBranch
+ * @return string
+ */
+ public static function currentBranch() {
+ return self::repo()->getCurrentBranch();
+ }
+
+ /**
+ * @see self::getHeadViewUrl()
+ * @return bool|string
+ */
+ public static function headViewUrl() {
+ return self::repo()->getHeadViewUrl();
+ }
+
+ /**
+ * Gets the list of repository viewers
+ * @return array
+ */
+ protected static function getViewers() {
+ global $wgGitRepositoryViewers;
+
+ if( self::$viewers === false ) {
+ self::$viewers = $wgGitRepositoryViewers;
+ wfRunHooks( 'GitViewers', array( &self::$viewers ) );
+ }
+
+ return self::$viewers;
+ }
+}
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 65fc643e..8f701c6b 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -1,6 +1,22 @@
<?php
/**
- * Global functions used everywhere
+ * Global functions used everywhere.
+ *
+ * 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
*/
@@ -14,39 +30,54 @@ if ( !defined( 'MEDIAWIKI' ) ) {
/**
* Compatibility functions
*
- * We support PHP 5.2.3 and up.
+ * We support PHP 5.3.2 and up.
* Re-implementations of newer functions or functions in non-standard
* PHP extensions may be included here.
*/
if( !function_exists( 'iconv' ) ) {
- /** @codeCoverageIgnore */
+ /**
+ * @codeCoverageIgnore
+ * @return string
+ */
function iconv( $from, $to, $string ) {
return Fallback::iconv( $from, $to, $string );
}
}
if ( !function_exists( 'mb_substr' ) ) {
- /** @codeCoverageIgnore */
+ /**
+ * @codeCoverageIgnore
+ * @return string
+ */
function mb_substr( $str, $start, $count='end' ) {
return Fallback::mb_substr( $str, $start, $count );
}
- /** @codeCoverageIgnore */
+ /**
+ * @codeCoverageIgnore
+ * @return int
+ */
function mb_substr_split_unicode( $str, $splitPos ) {
return Fallback::mb_substr_split_unicode( $str, $splitPos );
}
}
if ( !function_exists( 'mb_strlen' ) ) {
- /** @codeCoverageIgnore */
+ /**
+ * @codeCoverageIgnore
+ * @return int
+ */
function mb_strlen( $str, $enc = '' ) {
return Fallback::mb_strlen( $str, $enc );
}
}
if( !function_exists( 'mb_strpos' ) ) {
- /** @codeCoverageIgnore */
+ /**
+ * @codeCoverageIgnore
+ * @return int
+ */
function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) {
return Fallback::mb_strpos( $haystack, $needle, $offset, $encoding );
}
@@ -54,7 +85,10 @@ if( !function_exists( 'mb_strpos' ) ) {
}
if( !function_exists( 'mb_strrpos' ) ) {
- /** @codeCoverageIgnore */
+ /**
+ * @codeCoverageIgnore
+ * @return int
+ */
function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) {
return Fallback::mb_strrpos( $haystack, $needle, $offset, $encoding );
}
@@ -63,7 +97,10 @@ if( !function_exists( 'mb_strrpos' ) ) {
// Support for Wietse Venema's taint feature
if ( !function_exists( 'istainted' ) ) {
- /** @codeCoverageIgnore */
+ /**
+ * @codeCoverageIgnore
+ * @return int
+ */
function istainted( $var ) {
return 0;
}
@@ -200,7 +237,7 @@ function wfMergeErrorArrays( /*...*/ ) {
* @param $after Mixed: The key to insert after
* @return Array
*/
-function wfArrayInsertAfter( $array, $insert, $after ) {
+function wfArrayInsertAfter( array $array, array $insert, $after ) {
// Find the offset of the element to insert after.
$keys = array_keys( $array );
$offsetByKey = array_flip( $keys );
@@ -274,6 +311,24 @@ function wfRandom() {
}
/**
+ * Get a random string containing a number of pesudo-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
+ * @return String
+ * @since 1.20
+ */
+function wfRandomString( $length = 32 ) {
+ $str = '';
+ while ( strlen( $str ) < $length ) {
+ $str .= dechex( mt_rand() );
+ }
+ return substr( $str, 0, $length );
+}
+
+/**
* We want some things to be included as literal characters in our title URLs
* for prettiness, which urlencode encodes by default. According to RFC 1738,
* all of the following should be safe:
@@ -329,7 +384,7 @@ function wfUrlencode( $s ) {
* @param $prefix String
* @return String
*/
-function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) {
+function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) {
if ( !is_null( $array2 ) ) {
$array1 = $array1 + $array2;
}
@@ -348,7 +403,7 @@ function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) {
foreach ( $value as $k => $v ) {
$cgi .= $firstTime ? '' : '&';
if ( is_array( $v ) ) {
- $cgi .= wfArrayToCGI( $v, null, $key . "[$k]" );
+ $cgi .= wfArrayToCgi( $v, null, $key . "[$k]" );
} else {
$cgi .= urlencode( $key . "[$k]" ) . '=' . urlencode( $v );
}
@@ -366,7 +421,7 @@ function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) {
}
/**
- * This is the logical opposite of wfArrayToCGI(): it accepts a query string as
+ * This is the logical opposite of wfArrayToCgi(): it accepts a query string as
* its argument and returns the same string in array form. This allows compa-
* tibility with legacy functions that accept raw query strings instead of nice
* arrays. Of course, keys and values are urldecode()d.
@@ -423,7 +478,7 @@ function wfCgiToArray( $query ) {
*/
function wfAppendQuery( $url, $query ) {
if ( is_array( $query ) ) {
- $query = wfArrayToCGI( $query );
+ $query = wfArrayToCgi( $query );
}
if( $query != '' ) {
if( false === strpos( $url, '?' ) ) {
@@ -731,6 +786,9 @@ function wfParseUrl( $url ) {
return false;
}
+ // parse_url() incorrectly handles schemes case-sensitively. Convert it to lowercase.
+ $bits['scheme'] = strtolower( $bits['scheme'] );
+
// most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it
if ( in_array( $bits['scheme'] . '://', $wgUrlProtocols ) ) {
$bits['delimiter'] = '://';
@@ -765,6 +823,31 @@ function wfParseUrl( $url ) {
}
/**
+ * Take a URL, make sure it's expanded to fully qualified, and replace any
+ * encoded non-ASCII Unicode characters with their UTF-8 original forms
+ * for more compact display and legibility for local audiences.
+ *
+ * @todo handle punycode domains too
+ *
+ * @param $url string
+ * @return string
+ */
+function wfExpandIRI( $url ) {
+ return preg_replace_callback( '/((?:%[89A-F][0-9A-F])+)/i', 'wfExpandIRI_callback', wfExpandUrl( $url ) );
+}
+
+/**
+ * Private callback for wfExpandIRI
+ * @param array $matches
+ * @return string
+ */
+function wfExpandIRI_callback( $matches ) {
+ return urldecode( $matches[1] );
+}
+
+
+
+/**
* Make URL indexes, appropriate for the el_index field of externallinks.
*
* @param $url String
@@ -852,25 +935,21 @@ function wfMatchesDomainList( $url, $domains ) {
* @param $logonly Bool: set true to avoid appearing in HTML when $wgDebugComments is set
*/
function wfDebug( $text, $logonly = false ) {
- global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage;
- global $wgDebugLogPrefix, $wgShowDebug;
-
- static $cache = array(); // Cache of unoutputted messages
- $text = wfDebugTimer() . $text;
+ global $wgDebugLogFile, $wgProfileOnly, $wgDebugRawPage, $wgDebugLogPrefix;
if ( !$wgDebugRawPage && wfIsDebugRawPage() ) {
return;
}
- if ( ( $wgDebugComments || $wgShowDebug ) && !$logonly ) {
- $cache[] = $text;
+ $timer = wfDebugTimer();
+ if ( $timer !== '' ) {
+ $text = preg_replace( '/[^\n]/', $timer . '\0', $text, 1 );
+ }
- if ( isset( $wgOut ) && is_object( $wgOut ) ) {
- // add the message and any cached messages to the output
- array_map( array( $wgOut, 'debug' ), $cache );
- $cache = array();
- }
+ if ( !$logonly ) {
+ 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
@@ -880,12 +959,11 @@ function wfDebug( $text, $logonly = false ) {
wfErrorLog( $text, $wgDebugLogFile );
}
}
-
- MWDebug::debugMsg( $text );
}
/**
* Returns true if debug logging should be suppressed if $wgDebugRawPage = false
+ * @return bool
*/
function wfIsDebugRawPage() {
static $cache;
@@ -968,11 +1046,28 @@ function wfDebugLog( $logGroup, $text, $public = true ) {
* @param $text String: database error message.
*/
function wfLogDBError( $text ) {
- global $wgDBerrorLog;
+ global $wgDBerrorLog, $wgDBerrorLogTZ;
+ static $logDBErrorTimeZoneObject = null;
+
if ( $wgDBerrorLog ) {
$host = wfHostname();
$wiki = wfWikiID();
- $text = date( 'D M j G:i:s T Y' ) . "\t$host\t$wiki\t$text";
+
+ if ( $wgDBerrorLogTZ && !$logDBErrorTimeZoneObject ) {
+ $logDBErrorTimeZoneObject = new DateTimeZone( $wgDBerrorLogTZ );
+ }
+
+ // Workaround for https://bugs.php.net/bug.php?id=52063
+ // Can be removed when min PHP > 5.3.2
+ if ( $logDBErrorTimeZoneObject === null ) {
+ $d = date_create( "now" );
+ } else {
+ $d = date_create( "now", $logDBErrorTimeZoneObject );
+ }
+
+ $date = $d->format( 'D M j G:i:s T Y' );
+
+ $text = "$date\t$host\t$wiki\t$text";
wfErrorLog( $text, $wgDBerrorLog );
}
}
@@ -981,41 +1076,16 @@ function wfLogDBError( $text ) {
* Throws a warning that $function is deprecated
*
* @param $function String
- * @param $version String|false: Added in 1.19.
- * @param $component String|false: Added in 1.19.
- *
+ * @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
+ * caller. 2 = function that called the function that called
+ * wfDeprecated (Added in 1.20)
+ *
* @return null
*/
-function wfDeprecated( $function, $version = false, $component = false ) {
- static $functionsWarned = array();
-
- MWDebug::deprecated( $function, $version, $component );
-
- if ( !isset( $functionsWarned[$function] ) ) {
- $functionsWarned[$function] = true;
-
- if ( $version ) {
- global $wgDeprecationReleaseLimit;
-
- if ( $wgDeprecationReleaseLimit && $component === false ) {
- # Strip -* off the end of $version so that branches can use the
- # format #.##-branchname to avoid issues if the branch is merged into
- # a version of MediaWiki later than what it was branched from
- $comparableVersion = preg_replace( '/-.*$/', '', $version );
-
- # If the comparableVersion is larger than our release limit then
- # skip the warning message for the deprecation
- if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
- return;
- }
- }
-
- $component = $component === false ? 'MediaWiki' : $component;
- wfWarn( "Use of $function was deprecated in $component $version.", 2 );
- } else {
- wfWarn( "Use of $function is deprecated.", 2 );
- }
- }
+function wfDeprecated( $function, $version = false, $component = false, $callerOffset = 2 ) {
+ MWDebug::deprecated( $function, $version, $component, $callerOffset + 1 );
}
/**
@@ -1029,34 +1099,7 @@ function wfDeprecated( $function, $version = false, $component = false ) {
* is true
*/
function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) {
- global $wgDevelopmentWarnings;
-
- MWDebug::warning( $msg, $callerOffset + 2 );
-
- $callers = wfDebugBacktrace();
- if ( isset( $callers[$callerOffset + 1] ) ) {
- $callerfunc = $callers[$callerOffset + 1];
- $callerfile = $callers[$callerOffset];
- if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
- $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
- } else {
- $file = '(internal function)';
- }
- $func = '';
- if ( isset( $callerfunc['class'] ) ) {
- $func .= $callerfunc['class'] . '::';
- }
- if ( isset( $callerfunc['function'] ) ) {
- $func .= $callerfunc['function'];
- }
- $msg .= " [Called from $func in $file]";
- }
-
- if ( $wgDevelopmentWarnings ) {
- trigger_error( $msg, $level );
- } else {
- wfDebug( "$msg\n" );
- }
+ MWDebug::warning( $msg, $callerOffset + 1, $level );
}
/**
@@ -1177,6 +1220,57 @@ function wfLogProfilingData() {
}
/**
+ * Increment a statistics counter
+ *
+ * @param $key String
+ * @param $count Int
+ */
+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
+ }
+}
+
+/**
* 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.
@@ -1247,7 +1341,7 @@ function wfGetLangObj( $langcode = false ) {
return $wgLang;
}
- $validCodes = array_keys( Language::getLanguageNames() );
+ $validCodes = array_keys( Language::fetchLanguageNames() );
if( in_array( $langcode, $validCodes ) ) {
# $langcode corresponds to a valid language.
return Language::factory( $langcode );
@@ -1260,7 +1354,7 @@ function wfGetLangObj( $langcode = false ) {
/**
* Old function when $wgBetterDirectionality existed
- * Removed in core, kept in extensions for backwards compat.
+ * All usage removed, wfUILang can be removed in near future
*
* @deprecated since 1.18
* @return Language
@@ -1308,6 +1402,8 @@ function wfMessageFallback( /*...*/ ) {
* Use wfMsgForContent() instead if the message should NOT
* change depending on the user preferences.
*
+ * @deprecated since 1.18
+ *
* @param $key String: lookup key for the message, usually
* defined in languages/Language.php
*
@@ -1328,6 +1424,8 @@ function wfMsg( $key ) {
/**
* Same as above except doesn't transform the message
*
+ * @deprecated since 1.18
+ *
* @param $key String
* @return String
*/
@@ -1356,6 +1454,8 @@ function wfMsgNoTrans( $key ) {
* customize potentially hundreds of messages in
* order to, e.g., fix a link in every possible language.
*
+ * @deprecated since 1.18
+ *
* @param $key String: lookup key for the message, usually
* defined in languages/Language.php
* @return String
@@ -1376,6 +1476,8 @@ function wfMsgForContent( $key ) {
/**
* Same as above except doesn't transform the message
*
+ * @deprecated since 1.18
+ *
* @param $key String
* @return String
*/
@@ -1395,6 +1497,8 @@ function wfMsgForContentNoTrans( $key ) {
/**
* Really get a message
*
+ * @deprecated since 1.18
+ *
* @param $key String: key to get.
* @param $args
* @param $useDB Boolean
@@ -1413,6 +1517,8 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform
/**
* Fetch a message string value, but don't replace any keys yet.
*
+ * @deprecated since 1.18
+ *
* @param $key String
* @param $useDB Bool
* @param $langCode String: Code of the language to get the message for, or
@@ -1468,6 +1574,8 @@ function wfMsgReplaceArgs( $message, $args ) {
* to pre-escape them if you really do want plaintext, or just wrap
* the whole thing in htmlspecialchars().
*
+ * @deprecated since 1.18
+ *
* @param $key String
* @param string ... parameters
* @return string
@@ -1485,6 +1593,8 @@ function wfMsgHtml( $key ) {
* to pre-escape them if you really do want plaintext, or just wrap
* the whole thing in htmlspecialchars().
*
+ * @deprecated since 1.18
+ *
* @param $key String
* @param string ... parameters
* @return string
@@ -1500,6 +1610,9 @@ function wfMsgWikiHtml( $key ) {
/**
* Returns message in the requested format
+ *
+ * @deprecated since 1.18
+ *
* @param $key String: key of the message
* @param $options Array: processing rules. Can take the following options:
* <i>parse</i>: parses wikitext to HTML
@@ -1592,6 +1705,8 @@ function wfMsgExt( $key, $options ) {
* looked up didn't exist but a XHTML string, this function checks for the
* nonexistance of messages by checking the MessageCache::get() result directly.
*
+ * @deprecated since 1.18. Use Message::isDisabled().
+ *
* @param $key String: the message key looked up
* @return Boolean True if the message *doesn't* exist.
*/
@@ -1619,6 +1734,15 @@ function wfDebugDieBacktrace( $msg = '' ) {
function wfHostname() {
static $host;
if ( is_null( $host ) ) {
+
+ # Hostname overriding
+ global $wgOverrideHostname;
+ if( $wgOverrideHostname !== false ) {
+ # Set static and skip any detection
+ $host = $wgOverrideHostname;
+ return $host;
+ }
+
if ( function_exists( 'posix_uname' ) ) {
// This function not present on Windows
$uname = posix_uname();
@@ -1692,7 +1816,7 @@ function wfDebugBacktrace( $limit = 0 ) {
}
if ( $limit && version_compare( PHP_VERSION, '5.4.0', '>=' ) ) {
- return array_slice( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit ), 1 );
+ return array_slice( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit + 1 ), 1 );
} else {
return array_slice( debug_backtrace(), 1 );
}
@@ -1751,25 +1875,27 @@ function wfBacktrace() {
/**
* Get the name of the function which called this function
+ * wfGetCaller( 1 ) is the function with the wfGetCaller() call (ie. __FUNCTION__)
+ * wfGetCaller( 2 ) [default] is the caller of the function running wfGetCaller()
+ * wfGetCaller( 3 ) is the parent of that.
*
* @param $level Int
- * @return Bool|string
+ * @return string
*/
function wfGetCaller( $level = 2 ) {
- $backtrace = wfDebugBacktrace( $level );
+ $backtrace = wfDebugBacktrace( $level + 1 );
if ( isset( $backtrace[$level] ) ) {
return wfFormatStackFrame( $backtrace[$level] );
} else {
- $caller = 'unknown';
+ return 'unknown';
}
- return $caller;
}
/**
* Return a string consisting of callers in the stack. Useful sometimes
* for profiling specific points.
*
- * @param $limit The maximum depth of the stack frame to return, or false for
+ * @param $limit int The maximum depth of the stack frame to return, or false for
* the entire stack.
* @return String
*/
@@ -1786,7 +1912,7 @@ function wfGetAllCallers( $limit = 3 ) {
* Return a string representation of frame
*
* @param $frame Array
- * @return Bool
+ * @return string
*/
function wfFormatStackFrame( $frame ) {
return isset( $frame['class'] ) ?
@@ -1806,13 +1932,7 @@ function wfFormatStackFrame( $frame ) {
* @return String
*/
function wfShowingResults( $offset, $limit ) {
- global $wgLang;
- return wfMsgExt(
- 'showingresults',
- array( 'parseinline' ),
- $wgLang->formatNum( $limit ),
- $wgLang->formatNum( $offset + 1 )
- );
+ return wfMessage( 'showingresults' )->numParams( $limit, $offset + 1 )->parse();
}
/**
@@ -1828,7 +1948,7 @@ function wfShowingResults( $offset, $limit ) {
*/
function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
wfDeprecated( __METHOD__, '1.19' );
-
+
global $wgLang;
$query = wfCgiToArray( $query );
@@ -1856,6 +1976,8 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
* @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 );
}
@@ -1910,7 +2032,7 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) {
* Escapes the given text so that it may be output using addWikiText()
* without any linking, formatting, etc. making its way through. This
* is achieved by substituting certain characters with HTML entities.
- * As required by the callers, <nowiki> is not used.
+ * As required by the callers, "<nowiki>" is not used.
*
* @param $text String: text to be escaped
* @return String
@@ -1978,7 +2100,7 @@ function wfSetBit( &$dest, $bit, $state = true ) {
* A wrapper around the PHP function var_export().
* Either print it or add it to the regular output ($wgOut).
*
- * @param $var A PHP variable to dump.
+ * @param $var mixed A PHP variable to dump.
*/
function wfVarDump( $var ) {
global $wgOut;
@@ -2057,13 +2179,7 @@ function wfResetOutputBuffers( $resetGzipEncoding = true ) {
if( $status['name'] == 'ob_gzhandler' ) {
// Reset the 'Content-Encoding' field set by this handler
// so we can start fresh.
- if ( function_exists( 'header_remove' ) ) {
- // Available since PHP 5.3.0
- header_remove( 'Content-Encoding' );
- } else {
- // We need to provide a valid content-coding. See bug 28069
- header( 'Content-Encoding: identity' );
- }
+ header_remove( 'Content-Encoding' );
break;
}
}
@@ -2212,11 +2328,7 @@ function wfSuppressWarnings( $end = false ) {
}
} else {
if ( !$suppressCount ) {
- // E_DEPRECATED is undefined in PHP 5.2
- if( !defined( 'E_DEPRECATED' ) ) {
- define( 'E_DEPRECATED', 8192 );
- }
- $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED ) );
+ $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED ) );
}
++$suppressCount;
}
@@ -2297,118 +2409,13 @@ define( 'TS_ISO_8601_BASIC', 9 );
* @return Mixed: String / false The same date in the format specified in $outputtype or false
*/
function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) {
- $uts = 0;
- $da = array();
- $strtime = '';
-
- if ( !$ts ) { // We want to catch 0, '', null... but not date strings starting with a letter.
- $uts = time();
- $strtime = "@$uts";
- } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
- # TS_DB
- } elseif ( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
- # TS_EXIF
- } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) {
- # TS_MW
- } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) {
- # TS_UNIX
- $uts = $ts;
- $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php
- } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) {
- # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6
- $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3",
- str_replace( '+00:00', 'UTC', $ts ) );
- } elseif ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) {
- # TS_ISO_8601
- } elseif ( preg_match( '/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) {
- #TS_ISO_8601_BASIC
- } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/', $ts, $da ) ) {
- # TS_POSTGRES
- } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\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
- # TS_RFC2822, accepting a trailing comment. See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171
- # The regex is a superset of rfc2822 for readability
- $strtime = strtok( $ts, ';' );
- } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) {
- # TS_RFC850
- $strtime = $ts;
- } elseif ( preg_match( '/^[A-Z][a-z]{2} [A-Z][a-z]{2} +\d{1,2} \d\d:\d\d:\d\d \d{4}/', $ts ) ) {
- # asctime
- $strtime = $ts;
- } else {
- # Bogus value...
+ try {
+ $timestamp = new MWTimestamp( $ts );
+ return $timestamp->getTimestamp( $outputtype );
+ } catch( TimestampException $e ) {
wfDebug("wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n");
-
return false;
}
-
- static $formats = array(
- TS_UNIX => 'U',
- TS_MW => 'YmdHis',
- TS_DB => 'Y-m-d H:i:s',
- TS_ISO_8601 => 'Y-m-d\TH:i:s\Z',
- TS_ISO_8601_BASIC => 'Ymd\THis\Z',
- TS_EXIF => 'Y:m:d H:i:s', // This shouldn't ever be used, but is included for completeness
- 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',
- );
-
- if ( !isset( $formats[$outputtype] ) ) {
- throw new MWException( 'wfTimestamp() called with illegal output type.' );
- }
-
- if ( function_exists( "date_create" ) ) {
- if ( count( $da ) ) {
- $ds = sprintf("%04d-%02d-%02dT%02d:%02d:%02d.00+00:00",
- (int)$da[1], (int)$da[2], (int)$da[3],
- (int)$da[4], (int)$da[5], (int)$da[6]);
-
- $d = date_create( $ds, new DateTimeZone( 'GMT' ) );
- } elseif ( $strtime ) {
- $d = date_create( $strtime, new DateTimeZone( 'GMT' ) );
- } else {
- return false;
- }
-
- if ( !$d ) {
- wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n");
- return false;
- }
-
- $output = $d->format( $formats[$outputtype] );
- } else {
- if ( count( $da ) ) {
- // Warning! gmmktime() acts oddly if the month or day is set to 0
- // We may want to handle that explicitly at some point
- $uts = gmmktime( (int)$da[4], (int)$da[5], (int)$da[6],
- (int)$da[2], (int)$da[3], (int)$da[1] );
- } elseif ( $strtime ) {
- $uts = strtotime( $strtime );
- }
-
- if ( $uts === false ) {
- wfDebug("wfTimestamp() can't parse the timestamp (non 32-bit time? Update php): $outputtype; $ts\n");
- return false;
- }
-
- if ( TS_UNIX == $outputtype ) {
- return $uts;
- }
- $output = gmdate( $formats[$outputtype], $uts );
- }
-
- if ( ( $outputtype == TS_RFC2822 ) || ( $outputtype == TS_POSTGRES ) ) {
- $output .= ' GMT';
- }
-
- return $output;
}
/**
@@ -2472,11 +2479,10 @@ function swap( &$x, &$y ) {
}
/**
- * Tries to get the system directory for temporary files. The TMPDIR, TMP, and
- * TEMP environment variables are then checked in sequence, and if none are set
- * try sys_get_temp_dir() for PHP >= 5.2.1. All else fails, return /tmp for Unix
- * or C:\Windows\Temp for Windows and hope for the best.
- * It is common to call it with tempnam().
+ * Tries to get the system directory for temporary files. First
+ * $wgTmpDirectory is checked, and then the TMPDIR, TMP, and TEMP
+ * environment variables are then checked in sequence, and if none are
+ * set try sys_get_temp_dir().
*
* NOTE: When possible, use instead the tmpfile() function to create
* temporary files to avoid race conditions on file creation, etc.
@@ -2484,17 +2490,20 @@ function swap( &$x, &$y ) {
* @return String
*/
function wfTempDir() {
- foreach( array( 'TMPDIR', 'TMP', 'TEMP' ) as $var ) {
- $tmp = getenv( $var );
+ global $wgTmpDirectory;
+
+ if ( $wgTmpDirectory !== false ) {
+ return $wgTmpDirectory;
+ }
+
+ $tmpDir = array_map( "getenv", array( 'TMPDIR', 'TMP', 'TEMP' ) );
+
+ foreach( $tmpDir as $tmp ) {
if( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
return $tmp;
}
}
- if( function_exists( 'sys_get_temp_dir' ) ) {
- return sys_get_temp_dir();
- }
- # Usual defaults
- return wfIsWindows() ? 'C:\Windows\Temp' : '/tmp';
+ return sys_get_temp_dir();
}
/**
@@ -2509,7 +2518,7 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) {
global $wgDirectoryMode;
if ( FileBackend::isStoragePath( $dir ) ) { // sanity
- throw new MWException( __FUNCTION__ . " given storage path `$dir`.");
+ throw new MWException( __FUNCTION__ . " given storage path '$dir'." );
}
if ( !is_null( $caller ) ) {
@@ -2533,63 +2542,13 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) {
if( !$ok ) {
// PHP doesn't report the path in its warning message, so add our own to aid in diagnosis.
- trigger_error( __FUNCTION__ . ": failed to mkdir \"$dir\" mode $mode", E_USER_WARNING );
+ trigger_error( sprintf( "%s: failed to mkdir \"%s\" mode 0%o", __FUNCTION__, $dir, $mode ),
+ E_USER_WARNING );
}
return $ok;
}
/**
- * Increment a statistics counter
- *
- * @param $key String
- * @param $count Int
- */
-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} - {$count} 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
- }
-}
-
-/**
* Remove a directory and all its content.
* Does not hide error.
*/
@@ -2686,9 +2645,7 @@ function wfDl( $extension, $fileName = null ) {
$canDl = false;
$sapi = php_sapi_name();
- if( version_compare( PHP_VERSION, '5.3.0', '<' ) ||
- $sapi == 'cli' || $sapi == 'cgi' || $sapi == 'embed' )
- {
+ if( $sapi == 'cli' || $sapi == 'cgi' || $sapi == 'embed' ) {
$canDl = ( function_exists( 'dl' ) && is_callable( 'dl' )
&& wfIniGetBool( 'enable_dl' ) && !wfIniGetBool( 'safe_mode' ) );
}
@@ -2773,13 +2730,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 optional, will receive the program's exit code.
+ * @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.
- * @return collected stdout as a string (trailing newlines stripped)
+ * @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)
*/
-function wfShellExec( $cmd, &$retval = null, $environ = array() ) {
+function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array() ) {
global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime;
static $disabled;
@@ -2826,19 +2785,10 @@ function wfShellExec( $cmd, &$retval = null, $environ = array() ) {
}
$cmd = $envcmd . $cmd;
- if ( wfIsWindows() ) {
- if ( version_compare( PHP_VERSION, '5.3.0', '<' ) && /* Fixed in 5.3.0 :) */
- ( version_compare( PHP_VERSION, '5.2.1', '>=' ) || php_uname( 's' ) == 'Windows NT' ) )
- {
- # Hack to work around PHP's flawed invocation of cmd.exe
- # http://news.php.net/php.internals/21796
- # Windows 9x doesn't accept any kind of quotes
- $cmd = '"' . $cmd . '"';
- }
- } elseif ( php_uname( 's' ) == 'Linux' ) {
- $time = intval( $wgMaxShellTime );
- $mem = intval( $wgMaxShellMemory );
- $filesize = intval( $wgMaxShellFileSize );
+ 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";
@@ -2879,21 +2829,29 @@ function wfInitShellLocale() {
}
/**
- * Generate a shell-escaped command line string to run a maintenance script.
+ * Alias to wfShellWikiCmd()
+ * @see wfShellWikiCmd()
+ */
+function wfShellMaintenanceCmd( $script, array $parameters = array(), array $options = array() ) {
+ return wfShellWikiCmd( $script, $parameters, $options );
+}
+
+/**
+ * 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 maintenance script path
+ * @param $script string MediaWiki cli script path
* @param $parameters Array Arguments and options to the script
* @param $options Array Associative array of options:
* 'php': The path to the php executable
* 'wrapper': Path to a PHP wrapper to handle the maintenance script
* @return Array
*/
-function wfShellMaintenanceCmd( $script, array $parameters = array(), array $options = array() ) {
+function wfShellWikiCmd( $script, array $parameters = array(), array $options = array() ) {
global $wgPhpCli;
// Give site config file a chance to run the script in a wrapper.
// The caller may likely want to call wfBasename() on $script.
- wfRunHooks( 'wfShellMaintenanceCmd', array( &$script, &$parameters, &$options ) );
+ wfRunHooks( 'wfShellWikiCmd', array( &$script, &$parameters, &$options ) );
$cmd = isset( $options['php'] ) ? array( $options['php'] ) : array( $wgPhpCli );
if ( isset( $options['wrapper'] ) ) {
$cmd[] = $options['wrapper'];
@@ -3096,11 +3054,11 @@ function wfUseMW( $req_ver ) {
/**
* Return the final portion of a pathname.
- * Reimplemented because PHP5's basename() is buggy with multibyte text.
+ * Reimplemented because PHP5's "basename()" is buggy with multibyte text.
* http://bugs.php.net/bug.php?id=33898
*
* PHP's basename() only considers '\' a pathchar on Windows and Netware.
- * We'll consider it so always, as we don't want \s in our Unix paths either.
+ * 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
@@ -3294,11 +3252,6 @@ function wfHttpOnlySafe() {
/**
* Check if there is sufficent entropy in php's built-in session generation
- * PHP's built-in session entropy is enabled if:
- * - entropy_file is set or you're on Windows with php 5.3.3+
- * - AND entropy_length is > 0
- * We treat it as disabled if it doesn't have an entropy length of at least 32
- *
* @return bool true = there is sufficient entropy
*/
function wfCheckEntropy() {
@@ -3319,6 +3272,10 @@ function wfFixSessionID() {
return;
}
+ // PHP's built-in session entropy is enabled if:
+ // - entropy_file is set or you're on Windows with php 5.3.3+
+ // - AND entropy_length is > 0
+ // We treat it as disabled if it doesn't have an entropy length of at least 32
$entropyEnabled = wfCheckEntropy();
// If built-in entropy is not enabled or not sufficient override php's built in session id generation code
@@ -3334,21 +3291,10 @@ function wfFixSessionID() {
* @param $sessionId Bool
*/
function wfSetupSession( $sessionId = false ) {
- global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain,
+ global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain,
$wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler;
- if( $wgSessionsInMemcached ) {
- if ( !defined( 'MW_COMPILED' ) ) {
- global $IP;
- require_once( "$IP/includes/cache/MemcachedSessions.php" );
- }
- session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read',
- 'memsess_write', 'memsess_destroy', 'memsess_gc' );
-
- // It's necessary to register a shutdown function to call session_write_close(),
- // because by the time the request shutdown function for the session module is
- // called, $wgMemc has already been destroyed. Shutdown functions registered
- // this way are called before object destruction.
- register_shutdown_function( 'memsess_write_close' );
+ if( $wgSessionsInObjectCache || $wgSessionsInMemcached ) {
+ ObjectCacheSessionHandler::install();
} 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)
@@ -3507,7 +3453,7 @@ function &wfGetLBFactory() {
* Shortcut for RepoGroup::singleton()->findFile()
*
* @param $title String or Title object
- * @param $options Associative array of options:
+ * @param $options array 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.
@@ -3531,7 +3477,7 @@ function wfFindFile( $title, $options = array() ) {
* Returns a valid placeholder object if the file does not exist.
*
* @param $title Title|String
- * @return File|null A File, or null if passed an invalid Title
+ * @return LocalFile|null A File, or null if passed an invalid Title
*/
function wfLocalFile( $title ) {
return RepoGroup::singleton()->getLocalRepo()->newFile( $title );
@@ -3563,19 +3509,26 @@ function wfQueriesMustScale() {
/**
* Get the path to a specified script file, respecting file
* extensions; this is a wrapper around $wgScriptExtension etc.
+ * except for 'index' and 'load' which use $wgScript/$wgLoadScript
*
* @param $script String: script filename, sans extension
* @return String
*/
function wfScript( $script = 'index' ) {
- global $wgScriptPath, $wgScriptExtension;
- return "{$wgScriptPath}/{$script}{$wgScriptExtension}";
+ global $wgScriptPath, $wgScriptExtension, $wgScript, $wgLoadScript;
+ if ( $script === 'index' ) {
+ return $wgScript;
+ } else if ( $script === 'load' ) {
+ return $wgLoadScript;
+ } else {
+ return "{$wgScriptPath}/{$script}{$wgScriptExtension}";
+ }
}
/**
* Get the script URL.
*
- * @return script URL
+ * @return string script URL
*/
function wfGetScriptUrl() {
if( isset( $_SERVER['SCRIPT_NAME'] ) ) {
@@ -3689,25 +3642,29 @@ 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.
*/
function wfGenerateToken( $salt = '' ) {
+ wfDeprecated( __METHOD__, '1.20' );
$salt = serialize( $salt );
return md5( mt_rand( 0, 0x7fffffff ) . $salt );
}
/**
* Replace all invalid characters with -
+ * Additional characters can be defined in $wgIllegalFileChars (see bug 20489)
+ * By default, $wgIllegalFileChars = ':'
*
* @param $name Mixed: filename to process
* @return String
*/
function wfStripIllegalFilenameChars( $name ) {
global $wgIllegalFileChars;
+ $illegalFileChars = $wgIllegalFileChars ? "|[" . $wgIllegalFileChars . "]" : '';
$name = wfBaseName( $name );
$name = preg_replace(
- "/[^" . Title::legalChars() . "]" .
- ( $wgIllegalFileChars ? "|[" . $wgIllegalFileChars . "]" : '' ) .
- "/",
+ "/[^" . Title::legalChars() . "]" . $illegalFileChars . "/",
'-',
$name
);
@@ -3846,6 +3803,16 @@ function wfGetParserCacheStorage() {
}
/**
+ * Get the cache object used by the language converter
+ *
+ * @return BagOStuff
+ */
+function wfGetLangConverterCacheStorage() {
+ global $wgLanguageConverterCacheType;
+ return ObjectCache::getInstance( $wgLanguageConverterCacheType );
+}
+
+/**
* Call hook functions defined in $wgHooks
*
* @param $event String: event name
@@ -3868,7 +3835,7 @@ function wfRunHooks( $event, $args = array() ) {
* because php might make it negative.
*
* @throws MWException if $data not long enough, or if unpack fails
- * @return Associative array of the extracted data
+ * @return array Associative array of the extracted data
*/
function wfUnpack( $format, $data, $length=false ) {
if ( $length !== false ) {
@@ -3891,3 +3858,84 @@ function wfUnpack( $format, $data, $length=false ) {
}
return $result;
}
+
+/**
+ * Determine if an image exists on the 'bad image list'.
+ *
+ * The format of MediaWiki:Bad_image_list is as follows:
+ * * Only list items (lines starting with "*") are considered
+ * * The first link on a line must be a link to a bad image
+ * * 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 $contextTitle Title|bool the page on which the image occurs, if known
+ * @param $blacklist string wikitext of a file blacklist
+ * @return bool
+ */
+function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
+ static $badImageCache = null; // based on bad_image_list msg
+ wfProfileIn( __METHOD__ );
+
+ # Handle redirects
+ $redirectTitle = RepoGroup::singleton()->checkRedirect( Title::makeTitle( NS_FILE, $name ) );
+ if( $redirectTitle ) {
+ $name = $redirectTitle->getDbKey();
+ }
+
+ # Run the extension hook
+ $bad = false;
+ if( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) {
+ wfProfileOut( __METHOD__ );
+ return $bad;
+ }
+
+ $cacheable = ( $blacklist === null );
+ if( $cacheable && $badImageCache !== null ) {
+ $badImages = $badImageCache;
+ } else { // cache miss
+ if ( $blacklist === null ) {
+ $blacklist = wfMessage( 'bad_image_list' )->inContentLanguage()->plain(); // site list
+ }
+ # Build the list now
+ $badImages = array();
+ $lines = explode( "\n", $blacklist );
+ foreach( $lines as $line ) {
+ # List items only
+ if ( substr( $line, 0, 1 ) !== '*' ) {
+ continue;
+ }
+
+ # Find all links
+ $m = array();
+ if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) {
+ continue;
+ }
+
+ $exceptions = array();
+ $imageDBkey = false;
+ foreach ( $m[1] as $i => $titleText ) {
+ $title = Title::newFromText( $titleText );
+ if ( !is_null( $title ) ) {
+ if ( $i == 0 ) {
+ $imageDBkey = $title->getDBkey();
+ } else {
+ $exceptions[$title->getPrefixedDBkey()] = true;
+ }
+ }
+ }
+
+ if ( $imageDBkey !== false ) {
+ $badImages[$imageDBkey] = $exceptions;
+ }
+ }
+ if ( $cacheable ) {
+ $badImageCache = $badImages;
+ }
+ }
+
+ $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false;
+ $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] );
+ wfProfileOut( __METHOD__ );
+ return $bad;
+}
diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php
index 7326bf5c..5c00b9f6 100644
--- a/includes/HTMLForm.php
+++ b/includes/HTMLForm.php
@@ -1,5 +1,26 @@
<?php
/**
+ * HTML form generation and submission 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
+ */
+
+/**
* Object handling generic submission, CSRF protection, layout and
* other logic for UI forms. in a reusable manner.
*
@@ -13,6 +34,10 @@
* object, and typically implement at least getInputHTML, which generates
* the HTML for the input field to be placed in the table.
*
+ * You can find extensive documentation on the www.mediawiki.org wiki:
+ * - http://www.mediawiki.org/wiki/HTMLForm
+ * - http://www.mediawiki.org/wiki/HTMLForm/tutorial
+ *
* The constructor input is an associative array of $fieldname => $info,
* where $info is an Associative Array with any of the following:
*
@@ -30,13 +55,14 @@
* the message.
* 'label' -- alternatively, a raw text message. Overridden by
* label-message
+ * 'help' -- message text for a message to use as a help text.
* 'help-message' -- message key for a message to use as a help text.
* can be an array of msg key and then parameters to
* the message.
- * Overwrites 'help-messages'.
+ * Overwrites 'help-messages' and 'help'.
* 'help-messages' -- array of message key. As above, each item can
* be an array of msg key and then parameters.
- * Overwrites 'help-message'.
+ * Overwrites 'help'.
* 'required' -- passed through to the object, indicating that it
* is a required field.
* 'size' -- the length of text fields
@@ -51,6 +77,19 @@
* (eg one without the "wp" prefix), specify it here and
* it will be used without modification.
*
+ * Since 1.20, you can chain mutators to ease the form generation:
+ * @par Example:
+ * @code
+ * $form = new HTMLForm( $someFields );
+ * $form->setMethod( 'get' )
+ * ->setWrapperLegendMsg( 'message-key' )
+ * ->suppressReset()
+ * ->prepareForm()
+ * ->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 :(
+ *
* TODO: Document 'section' / 'subsection' stuff
*/
class HTMLForm extends ContextSource {
@@ -111,7 +150,7 @@ class HTMLForm extends ContextSource {
/**
* Form action URL. false means we will use the URL to set Title
* @since 1.19
- * @var false|string
+ * @var bool|string
*/
protected $mAction = false;
@@ -120,17 +159,34 @@ class HTMLForm extends ContextSource {
protected $mButtons = array();
protected $mWrapperLegend = false;
-
+
/**
* If true, sections that contain both fields and subsections will
* render their subsections before their fields.
- *
+ *
* Subclasses may set this to false to render subsections after fields
* instead.
*/
protected $mSubSectionBeforeFields = true;
/**
+ * Format in which to display form. For viable options,
+ * @see $availableDisplayFormats
+ * @var String
+ */
+ protected $displayFormat = 'table';
+
+ /**
+ * Available formats in which to display the form
+ * @var Array
+ */
+ protected $availableDisplayFormats = array(
+ 'table',
+ 'div',
+ 'raw',
+ );
+
+ /**
* Build a new HTMLForm from an array of field attributes
* @param $descriptor Array of Field constructs, as described above
* @param $context IContextSource available since 1.18, will become compulsory in 1.18.
@@ -138,13 +194,13 @@ class HTMLForm extends ContextSource {
* @param $messagePrefix String a prefix to go in front of default messages
*/
public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) {
- if( $context instanceof IContextSource ){
+ if ( $context instanceof IContextSource ) {
$this->setContext( $context );
$this->mTitle = false; // We don't need them to set a title
$this->mMessagePrefix = $messagePrefix;
} else {
// B/C since 1.18
- if( is_string( $context ) && $messagePrefix === '' ){
+ if ( is_string( $context ) && $messagePrefix === '' ) {
// it's actually $messagePrefix
$this->mMessagePrefix = $context;
}
@@ -189,6 +245,30 @@ 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
+ * $this->availableDisplayFormats
+ * @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 ) );
+ }
+ $this->displayFormat = $format;
+ return $this;
+ }
+
+ /**
+ * Getter for displayFormat
+ * @since 1.20
+ * @return String
+ */
+ public function getDisplayFormat() {
+ return $this->displayFormat;
+ }
+
+ /**
* Add the HTMLForm-specific JavaScript, if it hasn't been
* done already.
* @deprecated since 1.18 load modules with ResourceLoader instead
@@ -217,13 +297,22 @@ class HTMLForm extends ContextSource {
$descriptor['fieldname'] = $fieldname;
+ # TODO
+ # This will throw a fatal error whenever someone try to use
+ # 'class' to feed a CSS class instead of 'cssclass'. Would be
+ # great to avoid the fatal error and show a nice error.
$obj = new $class( $descriptor );
return $obj;
}
/**
- * Prepare form for submission
+ * Prepare form for submission.
+ *
+ * @attention When doing method chaining, that should be the very last
+ * method call before displayForm().
+ *
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function prepareForm() {
# Check if we have the info we need
@@ -233,6 +322,7 @@ class HTMLForm extends ContextSource {
# Load data from the request.
$this->loadData();
+ return $this;
}
/**
@@ -249,7 +339,7 @@ class HTMLForm extends ContextSource {
$editToken = $this->getRequest()->getVal( 'wpEditToken' );
if ( $this->getUser()->isLoggedIn() || $editToken != null ) {
// Session tokens for logged-out users have no security value.
- // However, if the user gave one, check it in order to give a nice
+ // However, if the user gave one, check it in order to give a nice
// "session expired" error instead of "permission denied" or such.
$submit = $this->getUser()->matchEditToken( $editToken );
} else {
@@ -266,7 +356,7 @@ class HTMLForm extends ContextSource {
/**
* The here's-one-I-made-earlier option: do the submission if
- * posted, or display the form with or without funky valiation
+ * posted, or display the form with or without funky validation
* errors
* @return Bool or Status whether submission was successful.
*/
@@ -274,7 +364,7 @@ class HTMLForm extends ContextSource {
$this->prepareForm();
$result = $this->tryAuthorizedSubmit();
- if ( $result === true || ( $result instanceof Status && $result->isGood() ) ){
+ if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
return $result;
}
@@ -307,6 +397,9 @@ class HTMLForm extends ContextSource {
}
$callback = $this->mSubmitCallback;
+ if ( !is_callable( $callback ) ) {
+ throw new MWException( 'HTMLForm: no submit callback provided. Use setSubmitCallback() to set one.' );
+ }
$data = $this->filterDataForSubmit( $this->mFieldData );
@@ -322,45 +415,60 @@ class HTMLForm extends ContextSource {
* 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.
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setSubmitCallback( $cb ) {
$this->mSubmitCallback = $cb;
+ return $this;
}
/**
* Set a message to display on a validation error.
- * @param $msg Mixed String or Array of valid inputs to wfMsgExt()
+ * @param $msg Mixed String or Array of valid inputs to wfMessage()
* (so each entry can be either a String or Array)
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setValidationErrorMessage( $msg ) {
$this->mValidationErrorMessage = $msg;
+ return $this;
}
/**
* Set the introductory message, overwriting any existing message.
* @param $msg String complete text of message to display
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setIntro( $msg ) {
$this->setPreText( $msg );
+ return $this;
}
/**
* Set the introductory message, overwriting any existing message.
* @since 1.19
* @param $msg String complete text of message to display
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
- function setPreText( $msg ) { $this->mPre = $msg; }
+ function setPreText( $msg ) {
+ $this->mPre = $msg;
+ return $this;
+ }
/**
* Add introductory text.
* @param $msg String complete text of message to display
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
- function addPreText( $msg ) { $this->mPre .= $msg; }
+ function addPreText( $msg ) {
+ $this->mPre .= $msg;
+ return $this;
+ }
/**
* 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
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function addHeaderText( $msg, $section = null ) {
if ( is_null( $section ) ) {
@@ -371,6 +479,7 @@ class HTMLForm extends ContextSource {
}
$this->mSectionHeaders[$section] .= $msg;
}
+ return $this;
}
/**
@@ -378,6 +487,7 @@ class HTMLForm extends ContextSource {
* @since 1.19
* @param $msg String complete text of message to display
* @param $section The section to add the header to
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setHeaderText( $msg, $section = null ) {
if ( is_null( $section ) ) {
@@ -385,12 +495,14 @@ class HTMLForm extends ContextSource {
} else {
$this->mSectionHeaders[$section] = $msg;
}
+ return $this;
}
/**
* 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
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function addFooterText( $msg, $section = null ) {
if ( is_null( $section ) ) {
@@ -401,6 +513,7 @@ class HTMLForm extends ContextSource {
}
$this->mSectionFooters[$section] .= $msg;
}
+ return $this;
}
/**
@@ -408,6 +521,7 @@ class HTMLForm extends ContextSource {
* @since 1.19
* @param $msg String complete text of message to display
* @param $section string The section to add the footer text to
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setFooterText( $msg, $section = null ) {
if ( is_null( $section ) ) {
@@ -415,39 +529,65 @@ class HTMLForm extends ContextSource {
} else {
$this->mSectionFooters[$section] = $msg;
}
+ return $this;
}
/**
* Add text to the end of the display.
* @param $msg String complete text of message to display
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
- function addPostText( $msg ) { $this->mPost .= $msg; }
+ function addPostText( $msg ) {
+ $this->mPost .= $msg;
+ return $this;
+ }
/**
* Set text at the end of the display.
* @param $msg String complete text of message to display
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
- function setPostText( $msg ) { $this->mPost = $msg; }
+ function setPostText( $msg ) {
+ $this->mPost = $msg;
+ return $this;
+ }
/**
* 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 $attribs Array
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
public function addHiddenField( $name, $value, $attribs = array() ) {
$attribs += array( 'name' => $name );
$this->mHiddenFields[] = array( $value, $attribs );
+ 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 $attribs Array
+ * @return HTMLForm $this for chaining calls (since 1.20)
+ */
public function addButton( $name, $value, $id = null, $attribs = null ) {
$this->mButtons[] = compact( 'name', 'value', 'id', 'attribs' );
+ return $this;
}
/**
* Display the form (sending to $wgOut), 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
+ * call just after prepareForm().
+ *
* @param $submitResult Mixed output from HTMLForm::trySubmit()
+ * @return Nothing, should be last call
*/
function displayForm( $submitResult ) {
$this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
@@ -478,7 +618,7 @@ class HTMLForm extends ContextSource {
}
/**
- * Wrap the form innards in an actual <form> element
+ * Wrap the form innards in an actual "<form>" element
* @param $html String HTML contents to wrap.
* @return String wrapped HTML.
*/
@@ -511,15 +651,15 @@ class HTMLForm extends ContextSource {
* @return String HTML.
*/
function getHiddenFields() {
- global $wgUsePathInfo;
+ global $wgArticlePath;
$html = '';
- if( $this->getMethod() == 'post' ){
+ if ( $this->getMethod() == 'post' ) {
$html .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken(), array( 'id' => 'wpEditToken' ) ) . "\n";
$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
}
- if ( !$wgUsePathInfo && $this->getMethod() == 'get' ) {
+ if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() == 'get' ) {
$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
}
@@ -560,7 +700,7 @@ class HTMLForm extends ContextSource {
'input',
array(
'type' => 'reset',
- 'value' => wfMsg( 'htmlform-reset' )
+ 'value' => $this->msg( 'htmlform-reset' )->text()
)
) . "\n";
}
@@ -620,7 +760,7 @@ class HTMLForm extends ContextSource {
/**
* Format a stack of error messages into a single HTML string
* @param $errors Array of message keys/values
- * @return String HTML, a <ul> list of errors
+ * @return String HTML, a "<ul>" list of errors
*/
public static function formatErrors( $errors ) {
$errorstr = '';
@@ -636,7 +776,7 @@ class HTMLForm extends ContextSource {
$errorstr .= Html::rawElement(
'li',
array(),
- wfMsgExt( $msg, array( 'parseinline' ), $error )
+ wfMessage( $msg, $error )->parse()
);
}
@@ -648,84 +788,115 @@ class HTMLForm extends ContextSource {
/**
* Set the text for the submit button
* @param $t String plaintext.
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setSubmitText( $t ) {
$this->mSubmitText = $t;
+ return $this;
}
/**
* Set the text for the submit button to a message
* @since 1.19
* @param $msg String message key
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitTextMsg( $msg ) {
- return $this->setSubmitText( $this->msg( $msg )->escaped() );
+ $this->setSubmitText( $this->msg( $msg )->text() );
+ return $this;
}
/**
* Get the text for the submit button, either customised or a default.
- * @return unknown_type
+ * @return string
*/
function getSubmitText() {
return $this->mSubmitText
? $this->mSubmitText
- : wfMsg( 'htmlform-submit' );
+ : $this->msg( 'htmlform-submit' )->text();
}
+ /**
+ * @param $name String Submit button name
+ * @return HTMLForm $this for chaining calls (since 1.20)
+ */
public function setSubmitName( $name ) {
$this->mSubmitName = $name;
+ return $this;
}
+ /**
+ * @param $name String Tooltip for the submit button
+ * @return HTMLForm $this for chaining calls (since 1.20)
+ */
public function setSubmitTooltip( $name ) {
$this->mSubmitTooltip = $name;
+ return $this;
}
/**
* Set the id for the submit button.
* @param $t String.
* @todo FIXME: Integrity of $t is *not* validated
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setSubmitID( $t ) {
$this->mSubmitID = $t;
+ return $this;
}
+ /**
+ * @param $id String 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.
+ * Prompt the whole form to be wrapped in a "<fieldset>", with
+ * this text as its "<legend>" element.
+ * @param $legend String HTML to go inside the "<legend>" element.
* Will be escaped
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
- public function setWrapperLegend( $legend ) { $this->mWrapperLegend = $legend; }
+ public function setWrapperLegend( $legend ) {
+ $this->mWrapperLegend = $legend;
+ return $this;
+ }
/**
- * Prompt the whole form to be wrapped in a <fieldset>, with
- * this message as its <legend> element.
+ * 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
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setWrapperLegendMsg( $msg ) {
- return $this->setWrapperLegend( $this->msg( $msg )->escaped() );
+ $this->setWrapperLegend( $this->msg( $msg )->text() );
+ return $this;
}
/**
* Set the prefix for various default messages
- * TODO: currently only used for the <fieldset> legend on forms
+ * @todo currently only used for the "<fieldset>" legend on forms
* with multiple sections; should be used elsewhre?
* @param $p String
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setMessagePrefix( $p ) {
$this->mMessagePrefix = $p;
+ return $this;
}
/**
* Set the title for form submission
* @param $t Title of page the form is on/should be posted to
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function setTitle( $t ) {
$this->mTitle = $t;
+ return $this;
}
/**
@@ -741,36 +912,43 @@ class HTMLForm extends ContextSource {
/**
* Set the method used to submit the form
* @param $method String
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
- public function setMethod( $method='post' ){
+ public function setMethod( $method = 'post' ) {
$this->mMethod = $method;
+ return $this;
}
- public function getMethod(){
+ public function getMethod() {
return $this->mMethod;
}
/**
- * TODO: Document
+ * @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 $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
* @return String
*/
- function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) {
- $tableHtml = '';
+ public function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) {
+ $displayFormat = $this->getDisplayFormat();
+
+ $html = '';
$subsectionHtml = '';
- $hasLeftColumn = false;
+ $hasLabel = false;
+
+ $getFieldHtmlMethod = ( $displayFormat == 'table' ) ? 'getTableRow' : 'get' . ucfirst( $displayFormat );
foreach ( $fields as $key => $value ) {
- if ( is_object( $value ) ) {
+ if ( $value instanceof HTMLFormField ) {
$v = empty( $value->mParams['nodata'] )
? $this->mFieldData[$key]
: $value->getDefault();
- $tableHtml .= $value->getTableRow( $v );
+ $html .= $value->$getFieldHtmlMethod( $v );
- if ( $value->getLabel() != '&#160;' ) {
- $hasLeftColumn = true;
+ $labelValue = trim( $value->getLabel() );
+ if ( $labelValue != '&#160;' && $labelValue !== '' ) {
+ $hasLabel = true;
}
} elseif ( is_array( $value ) ) {
$section = $this->displaySection( $value, $key );
@@ -789,27 +967,33 @@ class HTMLForm extends ContextSource {
}
}
- $classes = array();
+ if ( $displayFormat !== 'raw' ) {
+ $classes = array();
- if ( !$hasLeftColumn ) { // Avoid strange spacing when no labels exist
- $classes[] = 'mw-htmlform-nolabel';
- }
+ if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
+ $classes[] = 'mw-htmlform-nolabel';
+ }
- $attribs = array(
- 'class' => implode( ' ', $classes ),
- );
+ $attribs = array(
+ 'class' => implode( ' ', $classes ),
+ );
- if ( $sectionName ) {
- $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" );
- }
+ if ( $sectionName ) {
+ $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" );
+ }
- $tableHtml = Html::rawElement( 'table', $attribs,
- Html::rawElement( 'tbody', array(), "\n$tableHtml\n" ) ) . "\n";
+ if ( $displayFormat === 'table' ) {
+ $html = Html::rawElement( 'table', $attribs,
+ Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
+ } elseif ( $displayFormat === 'div' ) {
+ $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
+ }
+ }
if ( $this->mSubSectionBeforeFields ) {
- return $subsectionHtml . "\n" . $tableHtml;
+ return $subsectionHtml . "\n" . $html;
} else {
- return $tableHtml . "\n" . $subsectionHtml;
+ return $html . "\n" . $subsectionHtml;
}
}
@@ -842,9 +1026,11 @@ class HTMLForm extends ContextSource {
* Stop a reset button being shown for this form
* @param $suppressReset Bool set to false to re-enable the
* button again
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
function suppressReset( $suppressReset = true ) {
$this->mShowReset = !$suppressReset;
+ return $this;
}
/**
@@ -852,20 +1038,20 @@ class HTMLForm extends ContextSource {
* to the form as a whole, after it's submitted but before it's
* processed.
* @param $data
- * @return unknown_type
+ * @return
*/
function filterDataForSubmit( $data ) {
return $data;
}
/**
- * Get a string to go in the <legend> of a section fieldset. Override this if you
- * want something more complicated
+ * Get a string to go in the "<legend>" of a section fieldset.
+ * Override this if you want something more complicated.
* @param $key String
* @return String
*/
public function getLegend( $key ) {
- return wfMsg( "{$this->mMessagePrefix}-$key" );
+ return $this->msg( "{$this->mMessagePrefix}-$key" )->text();
}
/**
@@ -874,10 +1060,12 @@ class HTMLForm extends ContextSource {
*
* @since 1.19
*
- * @param string|false $action
+ * @param string|bool $action
+ * @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setAction( $action ) {
$this->mAction = $action;
+ return $this;
}
}
@@ -913,6 +1101,28 @@ abstract class HTMLFormField {
abstract function getInputHTML( $value );
/**
+ * Get a translated interface message
+ *
+ * This is a wrapper arround $this->mParent->msg() if $this->mParent is set
+ * and wfMessage() otherwise.
+ *
+ * Parameters are the same as wfMessage().
+ *
+ * @return Message object
+ */
+ function msg() {
+ $args = func_get_args();
+
+ if ( $this->mParent ) {
+ $callback = array( $this->mParent, 'msg' );
+ } else {
+ $callback = 'wfMessage';
+ }
+
+ return call_user_func_array( $callback, $args );
+ }
+
+ /**
* 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
@@ -921,8 +1131,8 @@ abstract class HTMLFormField {
* @return Mixed Bool true on success, or String error to display.
*/
function validate( $value, $alldata ) {
- if ( isset( $this->mParams['required'] ) && $value === '' ) {
- return wfMsgExt( 'htmlform-required', 'parseinline' );
+ if ( isset( $this->mParams['required'] ) && $this->mParams['required'] !== false && $value === '' ) {
+ return $this->msg( 'htmlform-required' )->parse();
}
if ( isset( $this->mValidationCallback ) ) {
@@ -982,7 +1192,7 @@ abstract class HTMLFormField {
$msgInfo = array();
}
- $this->mLabel = wfMsgExt( $msg, 'parseinline', $msgInfo );
+ $this->mLabel = wfMessage( $msg, $msgInfo )->parse();
} elseif ( isset( $params['label'] ) ) {
$this->mLabel = $params['label'];
}
@@ -1026,7 +1236,7 @@ abstract class HTMLFormField {
$this->mFilterCallback = $params['filter-callback'];
}
- if ( isset( $params['flatlist'] ) ){
+ if ( isset( $params['flatlist'] ) ) {
$this->mClass .= ' mw-htmlform-flatlist';
}
}
@@ -1038,35 +1248,27 @@ abstract class HTMLFormField {
* @return String complete HTML table row.
*/
function getTableRow( $value ) {
- # Check for invalid data.
-
- $errors = $this->validate( $value, $this->mParent->mFieldData );
-
+ list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
+ $inputHtml = $this->getInputHTML( $value );
+ $fieldType = get_class( $this );
+ $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
$cellAttributes = array();
- $verticalLabel = false;
- if ( !empty($this->mParams['vertical-label']) ) {
+ if ( !empty( $this->mParams['vertical-label'] ) ) {
$cellAttributes['colspan'] = 2;
$verticalLabel = true;
- }
-
- if ( $errors === true || ( !$this->mParent->getRequest()->wasPosted() && ( $this->mParent->getMethod() == 'post' ) ) ) {
- $errors = '';
- $errorClass = '';
} else {
- $errors = self::formatErrors( $errors );
- $errorClass = 'mw-htmlform-invalid-input';
+ $verticalLabel = false;
}
$label = $this->getLabelHtml( $cellAttributes );
+
$field = Html::rawElement(
'td',
array( 'class' => 'mw-input' ) + $cellAttributes,
- $this->getInputHTML( $value ) . "\n$errors"
+ $inputHtml . "\n$errors"
);
- $fieldType = get_class( $this );
-
if ( $verticalLabel ) {
$html = Html::rawElement( 'tr',
array( 'class' => 'mw-htmlform-vertical-label' ), $label );
@@ -1079,40 +1281,159 @@ abstract class HTMLFormField {
$label . $field );
}
+ return $html . $helptext;
+ }
+
+ /**
+ * 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.
+ * @return String complete HTML table row.
+ */
+ public function getDiv( $value ) {
+ list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
+ $inputHtml = $this->getInputHTML( $value );
+ $fieldType = get_class( $this );
+ $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
+ $cellAttributes = array();
+ $label = $this->getLabelHtml( $cellAttributes );
+
+ $field = Html::rawElement(
+ 'div',
+ array( 'class' => 'mw-input' ) + $cellAttributes,
+ $inputHtml . "\n$errors"
+ );
+ $html = Html::rawElement( 'div',
+ array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ),
+ $label . $field );
+ $html .= $helptext;
+ return $html;
+ }
+
+ /**
+ * 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.
+ * @return String complete HTML table row.
+ */
+ public function getRaw( $value ) {
+ list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
+ $inputHtml = $this->getInputHTML( $value );
+ $fieldType = get_class( $this );
+ $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() );
+ $cellAttributes = array();
+ $label = $this->getLabelHtml( $cellAttributes );
+
+ $html = "\n$errors";
+ $html .= $label;
+ $html .= $inputHtml;
+ $html .= $helptext;
+ return $html;
+ }
+
+ /**
+ * Generate help text HTML in table format
+ * @since 1.20
+ * @param $helptext String|null
+ * @return String
+ */
+ public function getHelpTextHtmlTable( $helptext ) {
+ if ( is_null( $helptext ) ) {
+ return '';
+ }
+
+ $row = Html::rawElement(
+ 'td',
+ array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
+ $helptext
+ );
+ $row = Html::rawElement( 'tr', array(), $row );
+ return $row;
+ }
+
+ /**
+ * Generate help text HTML in div format
+ * @since 1.20
+ * @param $helptext String|null
+ * @return String
+ */
+ public function getHelpTextHtmlDiv( $helptext ) {
+ if ( is_null( $helptext ) ) {
+ return '';
+ }
+
+ $div = Html::rawElement( 'div', array( 'class' => 'htmlform-tip' ), $helptext );
+ return $div;
+ }
+
+ /**
+ * Generate help text HTML formatted for raw output
+ * @since 1.20
+ * @param $helptext String|null
+ * @return String
+ */
+ public function getHelpTextHtmlRaw( $helptext ) {
+ return $this->getHelpTextHtmlDiv( $helptext );
+ }
+
+ /**
+ * Determine the help text to display
+ * @since 1.20
+ * @return String
+ */
+ public function getHelpText() {
$helptext = null;
if ( isset( $this->mParams['help-message'] ) ) {
- $msg = wfMessage( $this->mParams['help-message'] );
- if ( $msg->exists() ) {
- $helptext = $msg->parse();
- }
- } elseif ( isset( $this->mParams['help-messages'] ) ) {
- # help-message can be passed a message key (string) or an array containing
- # a message key and additional parameters. This makes it impossible to pass
- # an array of message key
- foreach( $this->mParams['help-messages'] as $name ) {
- $msg = wfMessage( $name );
- if( $msg->exists() ) {
- $helptext .= $msg->parse(); // append message
+ $this->mParams['help-messages'] = array( $this->mParams['help-message'] );
+ }
+
+ if ( isset( $this->mParams['help-messages'] ) ) {
+ foreach ( $this->mParams['help-messages'] as $name ) {
+ $helpMessage = (array)$name;
+ $msg = $this->msg( array_shift( $helpMessage ), $helpMessage );
+
+ if ( $msg->exists() ) {
+ if ( is_null( $helptext ) ) {
+ $helptext = '';
+ } else {
+ $helptext .= $this->msg( 'word-separator' )->escaped(); // some space
+ }
+ $helptext .= $msg->parse(); // Append message
}
}
- } elseif ( isset( $this->mParams['help'] ) ) {
+ }
+ elseif ( isset( $this->mParams['help'] ) ) {
$helptext = $this->mParams['help'];
}
+ return $helptext;
+ }
- if ( !is_null( $helptext ) ) {
- $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
- $helptext );
- $row = Html::rawElement( 'tr', array(), $row );
- $html .= "$row\n";
- }
+ /**
+ * Determine form errors to display and their classes
+ * @since 1.20
+ * @param $value String the value of the input
+ * @return Array
+ */
+ public function getErrorsAndErrorClass( $value ) {
+ $errors = $this->validate( $value, $this->mParent->mFieldData );
- return $html;
+ if ( $errors === true || ( !$this->mParent->getRequest()->wasPosted() && ( $this->mParent->getMethod() == 'post' ) ) ) {
+ $errors = '';
+ $errorClass = '';
+ } else {
+ $errors = self::formatErrors( $errors );
+ $errorClass = 'mw-htmlform-invalid-input';
+ }
+ return array( $errors, $errorClass );
}
function getLabel() {
return $this->mLabel;
}
+
function getLabelHtml( $cellAttributes = array() ) {
# Don't output a for= attribute for labels with no associated input.
# Kind of hacky here, possibly we don't want these to be <label>s at all.
@@ -1122,9 +1443,20 @@ abstract class HTMLFormField {
$for['for'] = $this->mID;
}
- return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes,
- Html::rawElement( 'label', $for, $this->getLabel() )
- );
+ $displayFormat = $this->mParent->getDisplayFormat();
+ $labelElement = Html::rawElement( 'label', $for, $this->getLabel() );
+
+ if ( $displayFormat == 'table' ) {
+ return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes,
+ Html::rawElement( 'label', $for, $this->getLabel() )
+ );
+ } elseif ( $displayFormat == 'div' ) {
+ return Html::rawElement( 'div', array( 'class' => 'mw-label' ) + $cellAttributes,
+ Html::rawElement( 'label', $for, $this->getLabel() )
+ );
+ } else {
+ return $labelElement;
+ }
}
function getDefault() {
@@ -1149,7 +1481,7 @@ abstract class HTMLFormField {
/**
* flatten an array of options to a single array, for instance,
- * a set of <options> inside <optgroups>.
+ * a set of "<options>" inside "<optgroups>".
* @param $options array Associative Array with values either Strings
* or Arrays
* @return Array flattened input
@@ -1216,10 +1548,6 @@ class HTMLTextField extends HTMLFormField {
if ( $this->mClass !== '' ) {
$attribs['class'] = $this->mClass;
}
-
- if ( isset( $this->mParams['maxlength'] ) ) {
- $attribs['maxlength'] = $this->mParams['maxlength'];
- }
if ( !empty( $this->mParams['disabled'] ) ) {
$attribs['disabled'] = 'disabled';
@@ -1227,8 +1555,9 @@ class HTMLTextField extends HTMLFormField {
# TODO: Enforce pattern, step, required, readonly on the server side as
# well
- foreach ( array( 'min', 'max', 'pattern', 'title', 'step',
- 'placeholder' ) as $param ) {
+ $allowedParams = array( 'min', 'max', 'pattern', 'title', 'step',
+ 'placeholder', 'list', 'maxlength' );
+ foreach ( $allowedParams as $param ) {
if ( isset( $this->mParams[$param] ) ) {
$attribs[$param] = $this->mParams[$param];
}
@@ -1290,7 +1619,7 @@ class HTMLTextAreaField extends HTMLFormField {
if ( $this->mClass !== '' ) {
$attribs['class'] = $this->mClass;
}
-
+
if ( !empty( $this->mParams['disabled'] ) ) {
$attribs['disabled'] = 'disabled';
}
@@ -1299,6 +1628,10 @@ class HTMLTextAreaField extends HTMLFormField {
$attribs['readonly'] = 'readonly';
}
+ if ( isset( $this->mParams['placeholder'] ) ) {
+ $attribs['placeholder'] = $this->mParams['placeholder'];
+ }
+
foreach ( array( 'required', 'autofocus' ) as $param ) {
if ( isset( $this->mParams[$param] ) ) {
$attribs[$param] = '';
@@ -1331,7 +1664,7 @@ class HTMLFloatField extends HTMLTextField {
# http://dev.w3.org/html5/spec/common-microsyntaxes.html#real-numbers
# with the addition that a leading '+' sign is ok.
if ( !preg_match( '/^((\+|\-)?\d+(\.\d+)?(E(\+|\-)?\d+)?)?$/i', $value ) ) {
- return wfMsgExt( 'htmlform-float-invalid', 'parse' );
+ return $this->msg( 'htmlform-float-invalid' )->parseAsBlock();
}
# The "int" part of these message names is rather confusing.
@@ -1340,7 +1673,7 @@ class HTMLFloatField extends HTMLTextField {
$min = $this->mParams['min'];
if ( $min > $value ) {
- return wfMsgExt( 'htmlform-int-toolow', 'parse', array( $min ) );
+ return $this->msg( 'htmlform-int-toolow', $min )->parseAsBlock();
}
}
@@ -1348,7 +1681,7 @@ class HTMLFloatField extends HTMLTextField {
$max = $this->mParams['max'];
if ( $max < $value ) {
- return wfMsgExt( 'htmlform-int-toohigh', 'parse', array( $max ) );
+ return $this->msg( 'htmlform-int-toohigh', $max )->parseAsBlock();
}
}
@@ -1375,7 +1708,7 @@ class HTMLIntField extends HTMLFloatField {
# value to, eg, save in the DB, clean it up with intval().
if ( !preg_match( '/^((\+|\-)?\d+)?$/', trim( $value ) )
) {
- return wfMsgExt( 'htmlform-int-invalid', 'parse' );
+ return $this->msg( 'htmlform-int-invalid' )->parseAsBlock();
}
return true;
@@ -1397,7 +1730,7 @@ class HTMLCheckField extends HTMLFormField {
if ( !empty( $this->mParams['disabled'] ) ) {
$attr['disabled'] = 'disabled';
}
-
+
if ( $this->mClass !== '' ) {
$attr['class'] = $this->mClass;
}
@@ -1429,7 +1762,7 @@ class HTMLCheckField extends HTMLFormField {
// Fetch the value in either one of the two following case:
// - we have a valid token (form got posted or GET forged by the user)
// - checkbox name has a value (false or true), ie is not null
- if ( $request->getCheck( 'wpEditToken' ) || $request->getVal( $this->mName )!== null ) {
+ if ( $request->getCheck( 'wpEditToken' ) || $request->getVal( $this->mName ) !== null ) {
// XOR has the following truth table, which is what we want
// INVERT VALUE | OUTPUT
// true true | false
@@ -1459,7 +1792,7 @@ class HTMLSelectField extends HTMLFormField {
if ( in_array( $value, $validOptions ) )
return true;
else
- return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
+ return $this->msg( 'htmlform-select-badoption' )->parse();
}
function getInputHTML( $value ) {
@@ -1468,8 +1801,8 @@ class HTMLSelectField extends HTMLFormField {
# If one of the options' 'name' is int(0), it is automatically selected.
# because PHP sucks and thinks int(0) == 'some string'.
# Working around this by forcing all of them to strings.
- foreach( $this->mParams['options'] as &$opt ){
- if( is_int( $opt ) ){
+ foreach ( $this->mParams['options'] as &$opt ) {
+ if ( is_int( $opt ) ) {
$opt = strval( $opt );
}
}
@@ -1478,7 +1811,7 @@ class HTMLSelectField extends HTMLFormField {
if ( !empty( $this->mParams['disabled'] ) ) {
$select->setAttribute( 'disabled', 'disabled' );
}
-
+
if ( $this->mClass !== '' ) {
$select->setAttribute( 'class', $this->mClass );
}
@@ -1497,7 +1830,9 @@ class HTMLSelectOrOtherField extends HTMLTextField {
function __construct( $params ) {
if ( !in_array( 'other', $params['options'], true ) ) {
- $msg = isset( $params['other'] ) ? $params['other'] : wfMsg( 'htmlform-selectorother-other' );
+ $msg = isset( $params['other'] ) ?
+ $params['other'] :
+ wfMessage( 'htmlform-selectorother-other' )->text();
$params['options'][$msg] = 'other';
}
@@ -1543,7 +1878,7 @@ class HTMLSelectOrOtherField extends HTMLTextField {
if ( isset( $this->mParams['maxlength'] ) ) {
$tbAttribs['maxlength'] = $this->mParams['maxlength'];
}
-
+
if ( $this->mClass !== '' ) {
$tbAttribs['class'] = $this->mClass;
}
@@ -1601,7 +1936,7 @@ class HTMLMultiSelectField extends HTMLFormField {
if ( count( $validValues ) == count( $value ) ) {
return true;
} else {
- return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
+ return $this->msg( 'htmlform-select-badoption' )->parse();
}
}
@@ -1646,7 +1981,7 @@ class HTMLMultiSelectField extends HTMLFormField {
*/
function loadDataFromRequest( $request ) {
if ( $this->mParent->getMethod() == 'post' ) {
- if( $request->wasPosted() ){
+ if ( $request->wasPosted() ) {
# Checkboxes are just 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() );
@@ -1684,7 +2019,7 @@ class HTMLMultiSelectField extends HTMLFormField {
* ** <option value>
* * New Optgroup header
* Plus a text field underneath for an additional reason. The 'value' of the field is
- * ""<select>: <extra reason>"", or "<extra reason>" if nothing has been selected in the
+ * "<select>: <extra reason>", or "<extra reason>" if nothing has been selected in the
* select dropdown.
* @todo FIXME: If made 'required', only the text field should be compulsory.
*/
@@ -1692,7 +2027,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
function __construct( $params ) {
if ( array_key_exists( 'other', $params ) ) {
- } elseif( array_key_exists( 'other-message', $params ) ){
+ } elseif ( array_key_exists( 'other-message', $params ) ) {
$params['other'] = wfMessage( $params['other-message'] )->plain();
} else {
$params['other'] = null;
@@ -1700,7 +2035,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
if ( array_key_exists( 'options', $params ) ) {
# Options array already specified
- } elseif( array_key_exists( 'options-message', $params ) ){
+ } elseif ( array_key_exists( 'options-message', $params ) ) {
# Generate options array from a system message
$params['options'] = self::parseMessage(
wfMessage( $params['options-message'] )->inContentLanguage()->plain(),
@@ -1722,8 +2057,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
* @return Array
* TODO: this is copied from Xml::listDropDown(), deprecate/avoid duplication?
*/
- public static function parseMessage( $string, $otherName=null ) {
- if( $otherName === null ){
+ public static function parseMessage( $string, $otherName = null ) {
+ if ( $otherName === null ) {
$otherName = wfMessage( 'htmlform-selectorother-other' )->plain();
}
@@ -1734,14 +2069,14 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
$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 ) );
$optgroup = $value;
- } elseif ( substr( $value, 0, 2) == '**' ) {
+ } elseif ( substr( $value, 0, 2 ) == '**' ) {
# groupmember
$opt = trim( substr( $value, 2 ) );
- if( $optgroup === false ){
+ if ( $optgroup === false ) {
$options[$opt] = $opt;
} else {
$options[$optgroup][$opt] = $opt;
@@ -1763,7 +2098,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
'id' => $this->mID . '-other',
'size' => $this->getSize(),
);
-
+
if ( $this->mClass !== '' ) {
$textAttribs['class'] = $this->mClass;
}
@@ -1786,7 +2121,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
/**
* @param $request WebRequest
- * @return Array( <overall message>, <select value>, <text field value> )
+ * @return Array("<overall message>","<select value>","<text field value>")
*/
function loadDataFromRequest( $request ) {
if ( $request->getCheck( $this->mName ) ) {
@@ -1796,14 +2131,14 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
if ( $list == 'other' ) {
$final = $text;
- } elseif( !in_array( $list, $this->mFlatOptions ) ){
+ } elseif ( !in_array( $list, $this->mFlatOptions ) ) {
# User has spoofed the select form to give an option which wasn't
# in the original offer. Sulk...
$final = $text;
- } elseif( $text == '' ) {
+ } elseif ( $text == '' ) {
$final = $list;
} else {
- $final = $list . wfMsgForContent( 'colon-separator' ) . $text;
+ $final = $list . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $text;
}
} else {
@@ -1812,8 +2147,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
$list = 'other';
$text = $final;
foreach ( $this->mFlatOptions as $option ) {
- $match = $option . wfMsgForContent( 'colon-separator' );
- if( strpos( $text, $match ) === 0 ) {
+ $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text();
+ if ( strpos( $text, $match ) === 0 ) {
$list = $option;
$text = substr( $text, strlen( $match ) );
break;
@@ -1839,8 +2174,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
return $p;
}
- if( isset( $this->mParams['required'] ) && $value[1] === '' ){
- return wfMsgExt( 'htmlform-required', 'parseinline' );
+ if ( isset( $this->mParams['required'] ) && $this->mParams['required'] !== false && $value[1] === '' ) {
+ return $this->msg( 'htmlform-required' )->parse();
}
return true;
@@ -1869,7 +2204,7 @@ class HTMLRadioField extends HTMLFormField {
if ( in_array( $value, $validOptions ) ) {
return true;
} else {
- return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
+ return $this->msg( 'htmlform-select-badoption' )->parse();
}
}
@@ -1925,17 +2260,17 @@ class HTMLRadioField extends HTMLFormField {
* An information field (text blob), not a proper input.
*/
class HTMLInfoField extends HTMLFormField {
- function __construct( $info ) {
+ public function __construct( $info ) {
$info['nodata'] = true;
parent::__construct( $info );
}
- function getInputHTML( $value ) {
+ public function getInputHTML( $value ) {
return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value );
}
- function getTableRow( $value ) {
+ public function getTableRow( $value ) {
if ( !empty( $this->mParams['rawrow'] ) ) {
return $value;
}
@@ -1943,6 +2278,28 @@ class HTMLInfoField extends HTMLFormField {
return parent::getTableRow( $value );
}
+ /**
+ * @since 1.20
+ */
+ public function getDiv( $value ) {
+ if ( !empty( $this->mParams['rawrow'] ) ) {
+ return $value;
+ }
+
+ return parent::getDiv( $value );
+ }
+
+ /**
+ * @since 1.20
+ */
+ public function getRaw( $value ) {
+ if ( !empty( $this->mParams['rawrow'] ) ) {
+ return $value;
+ }
+
+ return parent::getRaw( $value );
+ }
+
protected function needsLabel() {
return false;
}
@@ -1972,6 +2329,20 @@ class HTMLHiddenField extends HTMLFormField {
return '';
}
+ /**
+ * @since 1.20
+ */
+ public function getDiv( $value ) {
+ return $this->getTableRow( $value );
+ }
+
+ /**
+ * @since 1.20
+ */
+ public function getRaw( $value ) {
+ return $this->getTableRow( $value );
+ }
+
public function getInputHTML( $value ) { return ''; }
}
@@ -1981,12 +2352,12 @@ class HTMLHiddenField extends HTMLFormField {
*/
class HTMLSubmitField extends HTMLFormField {
- function __construct( $info ) {
+ public function __construct( $info ) {
$info['nodata'] = true;
parent::__construct( $info );
}
- function getInputHTML( $value ) {
+ public function getInputHTML( $value ) {
return Xml::submitButton(
$value,
array(
@@ -2007,7 +2378,7 @@ class HTMLSubmitField extends HTMLFormField {
* @param $alldata Array
* @return Bool
*/
- public function validate( $value, $alldata ){
+ public function validate( $value, $alldata ) {
return true;
}
}
@@ -2018,20 +2389,39 @@ class HTMLEditTools extends HTMLFormField {
}
public function getTableRow( $value ) {
+ $msg = $this->formatMsg();
+
+ return '<tr><td></td><td class="mw-input">'
+ . '<div class="mw-editTools">'
+ . $msg->parseAsBlock()
+ . "</div></td></tr>\n";
+ }
+
+ /**
+ * @since 1.20
+ */
+ public function getDiv( $value ) {
+ $msg = $this->formatMsg();
+ return '<div class="mw-editTools">' . $msg->parseAsBlock() . '</div>';
+ }
+
+ /**
+ * @since 1.20
+ */
+ public function getRaw( $value ) {
+ return $this->getDiv( $value );
+ }
+
+ protected function formatMsg() {
if ( empty( $this->mParams['message'] ) ) {
- $msg = wfMessage( 'edittools' );
+ $msg = $this->msg( 'edittools' );
} else {
- $msg = wfMessage( $this->mParams['message'] );
+ $msg = $this->msg( $this->mParams['message'] );
if ( $msg->isDisabled() ) {
- $msg = wfMessage( 'edittools' );
+ $msg = $this->msg( 'edittools' );
}
}
$msg->inContentLanguage();
-
-
- return '<tr><td></td><td class="mw-input">'
- . '<div class="mw-editTools">'
- . $msg->parseAsBlock()
- . "</div></td></tr>\n";
+ return $msg;
}
}
diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php
index f707b3f6..bb8ec5e3 100644
--- a/includes/HistoryBlob.php
+++ b/includes/HistoryBlob.php
@@ -1,5 +1,25 @@
<?php
-
+/**
+ * Efficient concatenated text storage.
+ *
+ * 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 class for general text storage via the "object" flag in old_flags, or
* two-part external storage URLs. Used for represent efficient concatenated
@@ -179,8 +199,8 @@ class HistoryBlobStub {
var $mOldId, $mHash, $mRef;
/**
- * @param $hash Strng: the content hash of the text
- * @param $oldid Integer: the old_id for the CGZ object
+ * @param $hash string the content hash of the text
+ * @param $oldid Integer the old_id for the CGZ object
*/
function __construct( $hash = '', $oldid = 0 ) {
$this->mHash = $hash;
@@ -298,7 +318,7 @@ class HistoryBlobCurStub {
}
/**
- * @return string|false
+ * @return string|bool
*/
function getText() {
$dbr = wfGetDB( DB_SLAVE );
@@ -515,13 +535,11 @@ class DiffHistoryBlob implements HistoryBlob {
$header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) );
- # Check the checksum if mhash is available
- if ( extension_loaded( 'mhash' ) ) {
- $ofp = mhash( MHASH_ADLER32, $base );
- if ( $ofp !== substr( $diff, 0, 4 ) ) {
- wfDebug( __METHOD__. ": incorrect base checksum\n" );
- return false;
- }
+ # Check the checksum if hash/mhash is available
+ $ofp = $this->xdiffAdler32( $base );
+ if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) {
+ wfDebug( __METHOD__. ": incorrect base checksum\n" );
+ return false;
}
if ( $header['csize'] != strlen( $base ) ) {
wfDebug( __METHOD__. ": incorrect base length\n" );
@@ -560,6 +578,30 @@ class DiffHistoryBlob implements HistoryBlob {
return $out;
}
+ /**
+ * 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
+ */
+ function xdiffAdler32( $s ) {
+ 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
+ // 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 );
+ }
+
function uncompress() {
if ( !$this->mDiffs ) {
return;
diff --git a/includes/Hooks.php b/includes/Hooks.php
index e1c1d50b..bc39f2fc 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -259,7 +259,7 @@ class Hooks {
/**
* This REALLY should be protected... but it's public for compatibility
*
- * @param $errno Unused
+ * @param $errno int Unused
* @param $errstr String: error message
* @return Boolean: false
*/
diff --git a/includes/Html.php b/includes/Html.php
index c61a1baf..83af24af 100644
--- a/includes/Html.php
+++ b/includes/Html.php
@@ -211,6 +211,23 @@ class Html {
'search',
);
+ 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'] );
@@ -286,6 +303,8 @@ class Html {
return $attribs;
}
+ # Whenever altering this array, please provide a covering test case
+ # in HtmlTest::provideElementsWithAttributesHavingDefaultValues
static $attribDefaults = array(
'area' => array( 'shape' => 'rect' ),
'button' => array(
@@ -306,7 +325,6 @@ class Html {
'input' => array(
'formaction' => 'GET',
'type' => 'text',
- 'value' => '',
),
'keygen' => array( 'keytype' => 'rsa' ),
'link' => array( 'media' => 'all' ),
@@ -325,7 +343,11 @@ class Html {
foreach ( $attribs as $attrib => $value ) {
$lcattrib = strtolower( $attrib );
- $value = strval( $value );
+ if( is_array( $value ) ) {
+ $value = implode( ' ', $value );
+ } else {
+ $value = strval( $value );
+ }
# Simple checks using $attribDefaults
if ( isset( $attribDefaults[$element][$lcattrib] ) &&
@@ -343,6 +365,29 @@ class Html {
&& strval( $attribs['type'] ) == 'text/css' ) {
unset( $attribs['type'] );
}
+ if ( $element === 'input' ) {
+ $type = isset( $attribs['type'] ) ? $attribs['type'] : null;
+ $value = isset( $attribs['value'] ) ? $attribs['value'] : null;
+ if ( $type === 'checkbox' || $type === 'radio' ) {
+ // The default value for checkboxes and radio buttons is 'on'
+ // not ''. By stripping value="" we break radio boxes that
+ // actually wants empty values.
+ if ( $value === 'on' ) {
+ unset( $attribs['value'] );
+ }
+ } elseif ( $type === 'submit' ) {
+ // The default value for submit appears to be "Submit" but
+ // let's not bother stripping out localized text that matches
+ // that.
+ } else {
+ // The default value for nearly every other field type is ''
+ // The 'range' and 'color' types use different defaults but
+ // stripping a value="" does not hurt them.
+ if ( $value === '' ) {
+ unset( $attribs['value'] );
+ }
+ }
+ }
if ( $element === 'select' && isset( $attribs['size'] ) ) {
if ( in_array( 'multiple', $attribs )
|| ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false )
@@ -548,9 +593,10 @@ class Html {
}
/**
- * Output a <script> tag with the given contents. TODO: do some useful
- * escaping as well, like if $contents contains literal '</script>' or (for
- * XML) literal "]]>".
+ * Output a "<script>" tag with the given contents.
+ *
+ * @todo do some useful escaping as well, like if $contents contains
+ * literal "</script>" or (for XML) literal "]]>".
*
* @param $contents string JavaScript
* @return string Raw HTML
@@ -572,8 +618,8 @@ class Html {
}
/**
- * Output a <script> tag linking to the given URL, e.g.,
- * <script src=foo.js></script>.
+ * Output a "<script>" tag linking to the given URL, e.g.,
+ * "<script src=foo.js></script>".
*
* @param $url string
* @return string Raw HTML
@@ -591,9 +637,9 @@ class Html {
}
/**
- * Output a <style> tag with the given contents for the given media type
+ * Output a "<style>" tag with the given contents for the given media type
* (if any). TODO: do some useful escaping as well, like if $contents
- * contains literal '</style>' (admittedly unlikely).
+ * contains literal "</style>" (admittedly unlikely).
*
* @param $contents string CSS
* @param $media mixed A media type string, like 'screen'
@@ -613,7 +659,7 @@ class Html {
}
/**
- * Output a <link rel=stylesheet> linking to the given URL for the given
+ * Output a "<link rel=stylesheet>" linking to the given URL for the given
* media type (if any).
*
* @param $url string
@@ -630,7 +676,7 @@ class Html {
}
/**
- * Convenience function to produce an <input> element. This supports the
+ * 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.
*
@@ -663,11 +709,12 @@ 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).
+ * Convenience function to produce an "<input>" element.
+ *
+ * This supports leaving out the cols= and rows= which Xml requires and are
+ * required by HTML4/XHTML but not required by HTML5 and will silently set
+ * cols="" and rows="" if $wgHtml5 is false and cols and rows are omitted
+ * (HTML4 validates present but empty cols="" and rows="" as valid).
*
* @param $name string name attribute
* @param $value string value attribute
@@ -706,8 +753,10 @@ class Html {
*
* @param $params array:
* - selected: [optional] Id of namespace which should be pre-selected
- * - all: [optional] Value of item for "all namespaces". If null or unset, no <option> is generated to select all namespaces
+ * - all: [optional] Value of item for "all namespaces". If null or unset, no "<option>" is generated to select all namespaces
* - 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.
* - id: [optional], default: 'namespace'
* - name: [optional], default: 'namespace'
@@ -716,11 +765,6 @@ class Html {
public static function namespaceSelector( Array $params = array(), Array $selectAttribs = array() ) {
global $wgContLang;
- // Default 'id' & 'name' <select> attributes
- $selectAttribs = $selectAttribs + array(
- 'id' => 'namespace',
- 'name' => 'namespace',
- );
ksort( $selectAttribs );
// Is a namespace selected?
@@ -737,39 +781,60 @@ class Html {
$params['selected'] = '';
}
- // Array holding the <option> elements
+ if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) {
+ $params['exclude'] = array();
+ }
+ if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) {
+ $params['disable'] = array();
+ }
+
+ // Associative array between option-values and option-labels
$options = array();
if ( isset( $params['all'] ) ) {
- // add an <option> that would let the user select all namespaces.
- // Value is provided by user, the name shown is localized.
- $options[$params['all']] = wfMsg( 'namespacesall' );
+ // add an option that would let the user select all namespaces.
+ // Value is provided by user, the name shown is localized for the user.
+ $options[$params['all']] = wfMessage( 'namespacesall' )->text();
}
- // Add defaults <option> according to content language
+ // Add all namespaces as options (in the content langauge)
$options += $wgContLang->getFormattedNamespaces();
- // Convert $options to HTML
+ // Convert $options to HTML and filter out namespaces below 0
$optionsHtml = array();
foreach ( $options as $nsId => $nsName ) {
- if ( $nsId < NS_MAIN ) {
+ if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) {
continue;
}
if ( $nsId === 0 ) {
- $nsName = wfMsg( 'blanknamespace' );
+ // 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)")
+ $nsName = wfMessage( 'blanknamespace' )->text();
}
- $optionsHtml[] = Xml::option( $nsName, $nsId, $nsId === $params['selected'] );
+ $optionsHtml[] = Html::element(
+ 'option', array(
+ 'disabled' => in_array( $nsId, $params['disable'] ),
+ 'value' => $nsId,
+ 'selected' => $nsId === $params['selected'],
+ ), $nsName
+ );
}
- // Forge a <select> element and returns it
$ret = '';
if ( isset( $params['label'] ) ) {
- $ret .= Xml::label( $params['label'], $selectAttribs['id'] ) . '&#160;';
+ $ret .= Html::element(
+ 'label', array(
+ 'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null,
+ ), $params['label']
+ ) . '&#160;';
}
+
+ // Wrap options in a <select>
$ret .= Html::openElement( 'select', $selectAttribs )
. "\n"
. implode( "\n", $optionsHtml )
. "\n"
. Html::closeElement( 'select' );
+
return $ret;
}
@@ -839,7 +904,7 @@ class Html {
/**
* Get HTML for an info box with an icon.
*
- * @param $text String: wikitext, get this with wfMsgNoTrans()
+ * @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
diff --git a/includes/HttpFunctions.old.php b/includes/HttpFunctions.old.php
index 479b4d23..feb9b93c 100644
--- a/includes/HttpFunctions.old.php
+++ b/includes/HttpFunctions.old.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Class alias kept for backward compatibility.
+ *
+ * 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 HTTP
+ */
/**
* HttpRequest was renamed to MWHttpRequest in order
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index 147823fe..8453e62c 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Various HTTP related functions.
+ *
+ * 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 HTTP
+ */
+
+/**
* @defgroup HTTP HTTP
*/
@@ -20,8 +42,9 @@ class Http {
* - timeout Timeout length in seconds
* - postData An array of key-value pairs or a url-encoded form data
* - proxy The proxy to use.
- * Will use $wgHTTPProxy (if set) otherwise.
- * - noProxy Override $wgHTTPProxy (if set) and don't use any proxy at all.
+ * 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
@@ -42,9 +65,6 @@ class Http {
}
$req = MWHttpRequest::factory( $url, $options );
- if( isset( $options['userAgent'] ) ) {
- $req->setUserAgent( $options['userAgent'] );
- }
$status = $req->execute();
if ( $status->isOK() ) {
@@ -136,7 +156,7 @@ class Http {
*
* file:// should not be allowed here for security purpose (r67684)
*
- * @fixme this is wildly inaccurate and fails to actually check most stuff
+ * @todo FIXME this is wildly inaccurate and fails to actually check most stuff
*
* @param $uri Mixed: URI to check for validity
* @return Boolean
@@ -192,7 +212,7 @@ class MWHttpRequest {
* @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())
*/
- function __construct( $url, $options = array() ) {
+ protected function __construct( $url, $options = array() ) {
global $wgHTTPTimeout;
$this->url = wfExpandUrl( $url, PROTO_HTTP );
@@ -209,15 +229,27 @@ class MWHttpRequest {
} else {
$this->timeout = $wgHTTPTimeout;
}
+ if( isset( $options['userAgent'] ) ) {
+ $this->setUserAgent( $options['userAgent'] );
+ }
$members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
"method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" );
foreach ( $members as $o ) {
if ( isset( $options[$o] ) ) {
+ // ensure that MWHttpRequest::method is always
+ // uppercased. Bug 36137
+ if ( $o == 'method' ) {
+ $options[$o] = strtoupper( $options[$o] );
+ }
$this->$o = $options[$o];
}
}
+
+ if ( $this->noProxy ) {
+ $this->proxy = ''; // noProxy takes precedence
+ }
}
/**
@@ -278,19 +310,18 @@ class MWHttpRequest {
}
/**
- * Take care of setting up the proxy
- * (override in subclass)
+ * Take care of setting up the proxy (do nothing if "noProxy" is set)
*
- * @return String
+ * @return void
*/
public function proxySetup() {
global $wgHTTPProxy;
- if ( $this->proxy ) {
+ if ( $this->proxy || !$this->noProxy ) {
return;
}
- if ( Http::isLocalURL( $this->url ) ) {
+ if ( Http::isLocalURL( $this->url ) || $this->noProxy ) {
$this->proxy = '';
} elseif ( $wgHTTPProxy ) {
$this->proxy = $wgHTTPProxy ;
@@ -376,6 +407,7 @@ class MWHttpRequest {
*
* @param $fh handle
* @param $content String
+ * @return int
*/
public function read( $fh, $content ) {
$this->content .= $content;
@@ -400,9 +432,7 @@ class MWHttpRequest {
$this->setReferer( wfExpandUrl( $wgTitle->getFullURL(), PROTO_CURRENT ) );
}
- if ( !$this->noProxy ) {
- $this->proxySetup();
- }
+ $this->proxySetup(); // set up any proxy as needed
if ( !$this->callback ) {
$this->setCallback( array( $this, 'read' ) );
@@ -417,8 +447,6 @@ class MWHttpRequest {
* Parses the headers, including the HTTP status code and any
* Set-Cookie headers. This function expectes the headers to be
* found in an array in the member variable headerList.
- *
- * @return nothing
*/
protected function parseHeader() {
$lastname = "";
@@ -446,8 +474,6 @@ class MWHttpRequest {
* RFC2616, section 10,
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a
* list of status codes.)
- *
- * @return nothing
*/
protected function setStatus() {
if ( !$this->respHeaders ) {
@@ -801,11 +827,13 @@ class PhpHttpRequest extends MWHttpRequest {
if ( $this->method == 'POST' ) {
// Required for HTTP 1.0 POSTs
$this->reqHeaders['Content-Length'] = strlen( $this->postData );
- $this->reqHeaders['Content-type'] = "application/x-www-form-urlencoded";
+ if( !isset( $this->reqHeaders['Content-Type'] ) ) {
+ $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
+ }
}
$options = array();
- if ( $this->proxy && !$this->noProxy ) {
+ if ( $this->proxy ) {
$options['proxy'] = $this->urlToTCP( $this->proxy );
$options['request_fulluri'] = true;
}
@@ -884,7 +912,7 @@ class PhpHttpRequest extends MWHttpRequest {
return $this->status;
}
- // If everything went OK, or we recieved some error code
+ // If everything went OK, or we received some error code
// get the response body content.
if ( $this->status->isOK()
|| (int)$this->respStatus >= 300) {
diff --git a/includes/IP.php b/includes/IP.php
index e3f61214..10c707e7 100644
--- a/includes/IP.php
+++ b/includes/IP.php
@@ -133,7 +133,7 @@ class IP {
}
/**
- * Convert an IP into a nice standard form.
+ * Convert an IP into a verbose, uppercase, normalized form.
* IPv6 addresses in octet notation are expanded to 8 words.
* IPv4 addresses are just trimmed.
*
@@ -186,6 +186,49 @@ class IP {
}
/**
+ * Prettify an IP for display to end users.
+ * This will make it more compact and lower-case.
+ *
+ * @param $ip string
+ * @return string
+ */
+ public static function prettifyIP( $ip ) {
+ $ip = self::sanitizeIP( $ip ); // normalize (removes '::')
+ if ( self::isIPv6( $ip ) ) {
+ // Split IP into an address and a CIDR
+ if ( strpos( $ip, '/' ) !== false ) {
+ list( $ip, $cidr ) = explode( '/', $ip, 2 );
+ } else {
+ list( $ip, $cidr ) = array( $ip, '' );
+ }
+ // Get the largest slice of words with multiple zeros
+ $offset = 0;
+ $longest = $longestPos = false;
+ while ( preg_match(
+ '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset
+ ) ) {
+ list( $match, $pos ) = $m[0]; // full match
+ if ( strlen( $match ) > strlen( $longest ) ) {
+ $longest = $match;
+ $longestPos = $pos;
+ }
+ $offset += ( $pos + strlen( $match ) ); // advance
+ }
+ if ( $longest !== false ) {
+ // Replace this portion of the string with the '::' abbreviation
+ $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) );
+ }
+ // Add any CIDR back on
+ if ( $cidr !== '' ) {
+ $ip = "{$ip}/{$cidr}";
+ }
+ // Convert to lower case to make it more readable
+ $ip = strtolower( $ip );
+ }
+ return $ip;
+ }
+
+ /**
* Given a host/port string, like one might find in the host part of a URL
* per RFC 2732, split the hostname part and the port part and return an
* array with an element for each. If there is no port part, the array will
@@ -198,7 +241,7 @@ class IP {
*
* A bare IPv6 address is accepted despite the lack of square brackets.
*
- * @param $both The string with the host and port
+ * @param $both string The string with the host and port
* @return array
*/
public static function splitHostAndPort( $both ) {
@@ -671,6 +714,7 @@ class IP {
* @return String: valid dotted quad IPv4 address or null
*/
public static function canonicalize( $addr ) {
+ $addr = preg_replace( '/\%.*/','', $addr ); // remove zone info (bug 35738)
if ( self::isValid( $addr ) ) {
return $addr;
}
diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php
deleted file mode 100644
index 4b90e24a..00000000
--- a/includes/ImageFunctions.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-/**
- * Global functions related to images
- *
- * @file
- */
-
-/**
- * Determine if an image exists on the 'bad image list'.
- *
- * The format of MediaWiki:Bad_image_list is as follows:
- * * Only list items (lines starting with "*") are considered
- * * The first link on a line must be a link to a bad image
- * * 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 $contextTitle Title|bool the page on which the image occurs, if known
- * @param $blacklist string wikitext of a file blacklist
- * @return bool
- */
-function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
- static $badImageCache = null; // based on bad_image_list msg
- wfProfileIn( __METHOD__ );
-
- # Handle redirects
- $redirectTitle = RepoGroup::singleton()->checkRedirect( Title::makeTitle( NS_FILE, $name ) );
- if( $redirectTitle ) {
- $name = $redirectTitle->getDbKey();
- }
-
- # Run the extension hook
- $bad = false;
- if( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) {
- wfProfileOut( __METHOD__ );
- return $bad;
- }
-
- $cacheable = ( $blacklist === null );
- if( $cacheable && $badImageCache !== null ) {
- $badImages = $badImageCache;
- } else { // cache miss
- if ( $blacklist === null ) {
- $blacklist = wfMsgForContentNoTrans( 'bad_image_list' ); // site list
- }
- # Build the list now
- $badImages = array();
- $lines = explode( "\n", $blacklist );
- foreach( $lines as $line ) {
- # List items only
- if ( substr( $line, 0, 1 ) !== '*' ) {
- continue;
- }
-
- # Find all links
- $m = array();
- if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) {
- continue;
- }
-
- $exceptions = array();
- $imageDBkey = false;
- foreach ( $m[1] as $i => $titleText ) {
- $title = Title::newFromText( $titleText );
- if ( !is_null( $title ) ) {
- if ( $i == 0 ) {
- $imageDBkey = $title->getDBkey();
- } else {
- $exceptions[$title->getPrefixedDBkey()] = true;
- }
- }
- }
-
- if ( $imageDBkey !== false ) {
- $badImages[$imageDBkey] = $exceptions;
- }
- }
- if ( $cacheable ) {
- $badImageCache = $badImages;
- }
- }
-
- $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false;
- $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] );
- wfProfileOut( __METHOD__ );
- return $bad;
-}
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
index 1106124a..91c3190f 100644
--- a/includes/ImageGallery.php
+++ b/includes/ImageGallery.php
@@ -1,6 +1,24 @@
<?php
-if ( ! defined( 'MEDIAWIKI' ) )
- die( 1 );
+/**
+ * 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
@@ -75,7 +93,7 @@ class ImageGallery {
/**
* Set the caption (as plain text)
*
- * @param $caption Caption
+ * @param $caption string Caption
*/
function setCaption( $caption ) {
$this->mCaption = htmlspecialchars( $caption );
@@ -141,23 +159,24 @@ class ImageGallery {
* @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 = '' ) {
+ function add( $title, $html = '', $alt = '', $link = '') {
if ( $title instanceof File ) {
// Old calling convention
$title = $title->getTitle();
}
- $this->mImages[] = array( $title, $html, $alt );
+ $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
- */
+ * 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
@@ -168,6 +187,7 @@ class ImageGallery {
/**
* isEmpty() returns true if the gallery contains no images
+ * @return bool
*/
function isEmpty() {
return empty( $this->mImages );
@@ -215,6 +235,7 @@ class ImageGallery {
* - the additional text provided when adding the image
* - the size of the image
*
+ * @return string
*/
function toHTML() {
global $wgLang;
@@ -243,6 +264,7 @@ class ImageGallery {
$nt = $pair[0];
$text = $pair[1]; # "text" means "caption" here
$alt = $pair[2];
+ $link = $pair[3];
$descQuery = false;
if ( $nt->getNamespace() == NS_FILE ) {
@@ -287,6 +309,7 @@ class ImageGallery {
'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 == '' ) {
@@ -316,7 +339,7 @@ class ImageGallery {
if( $img ) {
$fileSize = htmlspecialchars( $wgLang->formatSize( $img->getSize() ) );
} else {
- $fileSize = wfMsgHtml( 'filemissing' );
+ $fileSize = wfMessage( 'filemissing' )->escaped();
}
$fileSize = "$fileSize<br />\n";
} else {
@@ -344,9 +367,9 @@ class ImageGallery {
. '<div style="width: ' . ( $this->mWidths + self::THUMB_PADDING + self::GB_PADDING ) . 'px">'
. $thumbhtml
. "\n\t\t\t" . '<div class="gallerytext">' . "\n"
- . $textlink . $text . $fileSize
+ . $textlink . $text . $fileSize
. "\n\t\t\t</div>"
- . "\n\t\t</div></li>";
+ . "\n\t\t</div></li>";
}
$output .= "\n</ul>";
@@ -376,8 +399,8 @@ class ImageGallery {
*/
public function getContextTitle() {
return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title
- ? $this->contextTitle
- : false;
+ ? $this->contextTitle
+ : false;
}
} //class
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
index dcb09a41..6f1b1a15 100644
--- a/includes/ImagePage.php
+++ b/includes/ImagePage.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Special handling for file description 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
+ */
+
+/**
* Class for viewing MediaWiki file description pages
*
* @ingroup Media
@@ -30,6 +51,7 @@ class ImagePage extends Article {
/**
* Constructor from a page id
* @param $id Int article ID to load
+ * @return ImagePage|null
*/
public static function newFromID( $id ) {
$t = Title::newFromID( $id );
@@ -50,7 +72,7 @@ class ImagePage extends Article {
protected function loadFile() {
if ( $this->fileLoaded ) {
- return true;
+ return;
}
$this->fileLoaded = true;
@@ -74,19 +96,21 @@ class ImagePage extends Article {
* Include body text only; none of the image extras
*/
public function render() {
- global $wgOut;
- $wgOut->setArticleBodyOnly( true );
+ $this->getContext()->getOutput()->setArticleBodyOnly( true );
parent::view();
}
public function view() {
- global $wgOut, $wgShowEXIF, $wgRequest, $wgUser;
+ global $wgShowEXIF;
- $diff = $wgRequest->getVal( 'diff' );
- $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
+ $out = $this->getContext()->getOutput();
+ $request = $this->getContext()->getRequest();
+ $diff = $request->getVal( 'diff' );
+ $diffOnly = $request->getBool( 'diffonly', $this->getContext()->getUser()->getOption( 'diffonly' ) );
if ( $this->getTitle()->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) ) {
- return parent::view();
+ parent::view();
+ return;
}
$this->loadFile();
@@ -95,13 +119,14 @@ class ImagePage extends Article {
if ( $this->getTitle()->getDBkey() == $this->mPage->getFile()->getName() || isset( $diff ) ) {
// mTitle is the same as the redirect target so ask Article
// to perform the redirect for us.
- $wgRequest->setVal( 'diffonly', 'true' );
- return parent::view();
+ $request->setVal( 'diffonly', 'true' );
+ parent::view();
+ return;
} else {
// mTitle is not the same as the redirect target so it is
// probably the redirect page itself. Fake the redirect symbol
- $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() );
- $wgOut->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ),
+ $out->setPageTitle( $this->getTitle()->getPrefixedText() );
+ $out->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ),
/* $appendSubtitle */ true, /* $forceKnown */ true ) );
$this->mPage->doViewUpdates( $this->getContext()->getUser() );
return;
@@ -117,7 +142,7 @@ class ImagePage extends Article {
}
if ( !$diff && $this->displayImg->exists() ) {
- $wgOut->addHTML( $this->showTOC( $showmeta ) );
+ $out->addHTML( $this->showTOC( $showmeta ) );
}
if ( !$diff ) {
@@ -128,16 +153,16 @@ class ImagePage extends Article {
if ( $this->mPage->getID() ) {
# NS_FILE is in the user language, but this section (the actual wikitext)
# should be in page content language
- $pageLang = $this->getTitle()->getPageLanguage();
- $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
- 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
+ $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() ) ) );
parent::view();
- $wgOut->addHTML( Xml::closeElement( 'div' ) );
+ $out->addHTML( Xml::closeElement( 'div' ) );
} else {
# Just need to set the right headers
- $wgOut->setArticleFlag( true );
- $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() );
+ $out->setArticleFlag( true );
+ $out->setPageTitle( $this->getTitle()->getPrefixedText() );
$this->mPage->doViewUpdates( $this->getContext()->getUser() );
}
@@ -145,18 +170,18 @@ class ImagePage extends Article {
if ( $this->mExtraDescription ) {
$fol = wfMessage( 'shareddescriptionfollows' );
if ( !$fol->isDisabled() ) {
- $wgOut->addWikiText( $fol->plain() );
+ $out->addWikiText( $fol->plain() );
}
- $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" );
+ $out->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" );
}
$this->closeShowImage();
$this->imageHistory();
// TODO: Cleanup the following
- $wgOut->addHTML( Xml::element( 'h2',
+ $out->addHTML( Xml::element( 'h2',
array( 'id' => 'filelinks' ),
- wfMsg( 'imagelinks' ) ) . "\n" );
+ wfMessage( 'imagelinks' )->text() ) . "\n" );
$this->imageDupes();
# @todo FIXME: For some freaky reason, we can't redirect to foreign images.
# Yet we return metadata about the target. Definitely an issue in the FileRepo
@@ -166,24 +191,27 @@ class ImagePage extends Article {
$html = '';
wfRunHooks( 'ImagePageAfterImageLinks', array( $this, &$html ) );
if ( $html ) {
- $wgOut->addHTML( $html );
+ $out->addHTML( $html );
}
if ( $showmeta ) {
- $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ) . "\n" );
- $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
- $wgOut->addModules( array( 'mediawiki.action.view.metadata' ) );
+ $out->addHTML( Xml::element(
+ 'h2',
+ array( 'id' => 'metadata' ),
+ wfMessage( 'metadata' )->text() ) . "\n" );
+ $out->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
+ $out->addModules( array( 'mediawiki.action.view.metadata' ) );
}
// Add remote Filepage.css
if( !$this->repo->isLocal() ) {
$css = $this->repo->getDescriptionStylesheetUrl();
if ( $css ) {
- $wgOut->addStyle( $css );
+ $out->addStyle( $css );
}
}
// always show the local local Filepage.css, bug 29277
- $wgOut->addModuleStyles( 'filepage' );
+ $out->addModuleStyles( 'filepage' );
}
/**
@@ -202,12 +230,12 @@ class ImagePage extends Article {
*/
protected function showTOC( $metadata ) {
$r = array(
- '<li><a href="#file">' . wfMsgHtml( 'file-anchor-link' ) . '</a></li>',
- '<li><a href="#filehistory">' . wfMsgHtml( 'filehist' ) . '</a></li>',
- '<li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>',
+ '<li><a href="#file">' . wfMessage( 'file-anchor-link' )->escaped() . '</a></li>',
+ '<li><a href="#filehistory">' . wfMessage( 'filehist' )->escaped() . '</a></li>',
+ '<li><a href="#filelinks">' . wfMessage( 'imagelinks' )->escaped() . '</a></li>',
);
if ( $metadata ) {
- $r[] = '<li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>';
+ $r[] = '<li><a href="#metadata">' . wfMessage( 'metadata' )->escaped() . '</a></li>';
}
wfRunHooks( 'ImagePageShowTOC', array( $this, &$r ) );
@@ -225,7 +253,7 @@ class ImagePage extends Article {
*/
protected function makeMetadataTable( $metadata ) {
$r = "<div class=\"mw-imagepage-section-metadata\">";
- $r .= wfMsgNoTrans( 'metadata-help' );
+ $r .= wfMessage( 'metadata-help' )->plain();
$r .= "<table id=\"mw_metadata\" class=\"mw_metadata\">\n";
foreach ( $metadata as $type => $stuff ) {
foreach ( $stuff as $v ) {
@@ -259,12 +287,16 @@ class ImagePage extends Article {
}
protected function openShowImage() {
- global $wgOut, $wgUser, $wgImageLimits, $wgRequest,
- $wgLang, $wgEnableUploads, $wgSend404Code;
+ global $wgImageLimits, $wgEnableUploads, $wgSend404Code;
$this->loadFile();
+ $out = $this->getContext()->getOutput();
+ $user = $this->getContext()->getUser();
+ $lang = $this->getContext()->getLanguage();
+ $dirmark = $lang->getDirMarkEntity();
+ $request = $this->getContext()->getRequest();
- $sizeSel = intval( $wgUser->getOption( 'imagesize' ) );
+ $sizeSel = intval( $user->getOption( 'imagesize' ) );
if ( !isset( $wgImageLimits[$sizeSel] ) ) {
$sizeSel = User::getDefaultOption( 'imagesize' );
@@ -278,11 +310,10 @@ class ImagePage extends Article {
$max = $wgImageLimits[$sizeSel];
$maxWidth = $max[0];
$maxHeight = $max[1];
- $dirmark = $wgLang->getDirMark();
if ( $this->displayImg->exists() ) {
# image
- $page = $wgRequest->getIntOrNull( 'page' );
+ $page = $request->getIntOrNull( 'page' );
if ( is_null( $page ) ) {
$params = array();
$page = 1;
@@ -294,15 +325,15 @@ class ImagePage extends Article {
$height_orig = $this->displayImg->getHeight( $page );
$height = $height_orig;
- $longDesc = wfMsg( 'parentheses', $this->displayImg->getLongDesc() );
+ $longDesc = wfMessage( 'parentheses', $this->displayImg->getLongDesc() )->text();
- wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$wgOut ) );
+ wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$out ) );
if ( $this->displayImg->allowInlineDisplay() ) {
# image
# "Download high res version" link below the image
- # $msgsize = wfMsgHtml( 'file-info-size', $width_orig, $height_orig, Linker::formatSize( $this->displayImg->getSize() ), $mime );
+ # $msgsize = wfMessage( 'file-info-size', $width_orig, $height_orig, Linker::formatSize( $this->displayImg->getSize() ), $mime )->escaped();
# We'll show a thumbnail of this image
if ( $width > $maxWidth || $height > $maxHeight ) {
# Calculate the thumbnail size.
@@ -318,21 +349,33 @@ class ImagePage extends Article {
# Note that $height <= $maxHeight now, but might not be identical
# because of rounding.
}
- $msgbig = wfMsgHtml( 'show-big-image' );
+ $msgbig = 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' )] );
+ }
+ # Generate thumbnails or thumbnail links as needed...
$otherSizes = array();
- foreach ( $wgImageLimits as $size ) {
- if ( $size[0] < $width_orig && $size[1] < $height_orig &&
- $size[0] != $width && $size[1] != $height ) {
+ foreach ( $thumbSizes as $size ) {
+ if ( $size[0] < $width_orig && $size[1] < $height_orig
+ && $size[0] != $width && $size[1] != $height )
+ {
$otherSizes[] = $this->makeSizeLink( $params, $size[0], $size[1] );
}
}
$msgsmall = wfMessage( 'show-big-image-preview' )->
rawParams( $this->makeSizeLink( $params, $width, $height ) )->
parse();
- if ( count( $otherSizes ) && $this->displayImg->getRepo()->canTransformVia404() ) {
+ if ( count( $otherSizes ) ) {
$msgsmall .= ' ' .
Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ),
- wfMessage( 'show-big-image-other' )->rawParams( $wgLang->pipeList( $otherSizes ) )->
+ wfMessage( 'show-big-image-other' )->rawParams( $lang->pipeList( $otherSizes ) )->
params( count( $otherSizes ) )->parse()
);
}
@@ -340,6 +383,9 @@ class ImagePage extends Article {
# Some sort of audio file that doesn't have dimensions
# Don't output a no hi res message for such a file
$msgsmall = '';
+ } elseif ( $this->displayImg->isVectorized() ) {
+ # For vectorized images, full size is just the frame size
+ $msgsmall = '';
} else {
# Image is small enough to show full size on image page
$msgsmall = wfMessage( 'file-nohires' )->parse();
@@ -354,7 +400,7 @@ class ImagePage extends Article {
$isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1;
if ( $isMulti ) {
- $wgOut->addHTML( '<table class="multipageimage"><tr><td>' );
+ $out->addHTML( '<table class="multipageimage"><tr><td>' );
}
if ( $thumbnail ) {
@@ -362,7 +408,7 @@ class ImagePage extends Article {
'alt' => $this->displayImg->getTitle()->getPrefixedText(),
'file-link' => true,
);
- $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
+ $out->addHTML( '<div class="fullImageLink" id="file">' .
$thumbnail->toHtml( $options ) .
$anchorclose . "</div>\n" );
}
@@ -371,13 +417,12 @@ class ImagePage extends Article {
$count = $this->displayImg->pageCount();
if ( $page > 1 ) {
- $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false );
- $link = Linker::link(
+ $label = $out->parse( wfMessage( 'imgmultipageprev' )->text(), false );
+ $link = Linker::linkKnown(
$this->getTitle(),
$label,
array(),
- array( 'page' => $page - 1 ),
- array( 'known', 'noclasses' )
+ array( 'page' => $page - 1 )
);
$thumb1 = Linker::makeThumbLinkObj( $this->getTitle(), $this->displayImg, $link, $label, 'none',
array( 'page' => $page - 1 ) );
@@ -386,13 +431,12 @@ class ImagePage extends Article {
}
if ( $page < $count ) {
- $label = wfMsg( 'imgmultipagenext' );
- $link = Linker::link(
+ $label = wfMessage( 'imgmultipagenext' )->text();
+ $link = Linker::linkKnown(
$this->getTitle(),
$label,
array(),
- array( 'page' => $page + 1 ),
- array( 'known', 'noclasses' )
+ array( 'page' => $page + 1 )
);
$thumb2 = Linker::makeThumbLinkObj( $this->getTitle(), $this->displayImg, $link, $label, 'none',
array( 'page' => $page + 1 ) );
@@ -409,18 +453,18 @@ class ImagePage extends Article {
);
$options = array();
for ( $i = 1; $i <= $count; $i++ ) {
- $options[] = Xml::option( $wgLang->formatNum( $i ), $i, $i == $page );
+ $options[] = Xml::option( $lang->formatNum( $i ), $i, $i == $page );
}
$select = Xml::tags( 'select',
array( 'id' => 'pageselector', 'name' => 'page' ),
implode( "\n", $options ) );
- $wgOut->addHTML(
+ $out->addHTML(
'</td><td><div class="multipageimagenavbox">' .
Xml::openElement( 'form', $formParams ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
- wfMsgExt( 'imgmultigoto', array( 'parseinline', 'replaceafter' ), $select ) .
- Xml::submitButton( wfMsg( 'imgmultigo' ) ) .
+ wfMessage( 'imgmultigoto' )->rawParams( $select )->parse() .
+ Xml::submitButton( wfMessage( 'imgmultigo' )->text() ) .
Xml::closeElement( 'form' ) .
"<hr />$thumb1\n$thumb2<br style=\"clear: both\" /></div></td></tr></table>"
);
@@ -430,7 +474,7 @@ class ImagePage extends Article {
if ( $this->displayImg->isSafeFile() ) {
$icon = $this->displayImg->iconThumb();
- $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
+ $out->addHTML( '<div class="fullImageLink" id="file">' .
$icon->toHtml( array( 'file-link' => true ) ) .
"</div>\n" );
}
@@ -447,27 +491,69 @@ class ImagePage extends Article {
$medialink = "[[Media:$filename|$linktext]]";
if ( !$this->displayImg->isSafeFile() ) {
- $warning = wfMsgNoTrans( 'mediawarning' );
- $wgOut->addWikiText( <<<EOT
-<div class="fullMedia"><span class="dangerousLink">{$medialink}</span>$dirmark <span class="fileInfo">$longDesc</span></div>
+ $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 {
- $wgOut->addWikiText( <<<EOT
-<div class="fullMedia">{$medialink}{$dirmark} <span class="fileInfo">$longDesc</span>
+ $out->addWikiText( <<<EOT
+<div class="fullMedia">{$medialink} {$dirmark}<span class="fileInfo">$longDesc</span>
</div>
EOT
);
}
}
+ // Add cannot animate thumbnail warning
+ if ( !$this->displayImg->canAnimateThumbIfAppropriate() ) {
+ // Include the extension so wiki admins can
+ // customize it on a per file-type basis
+ // (aka say things like use format X instead).
+ // additionally have a specific message for
+ // file-no-thumb-animation-gif
+ $ext = $this->displayImg->getExtension();
+ $noAnimMesg = wfMessageFallback(
+ 'file-no-thumb-animation-' . $ext,
+ 'file-no-thumb-animation'
+ )->plain();
+
+ $out->addWikiText( <<<EOT
+<div class="mw-noanimatethumb">{$noAnimMesg}</div>
+EOT
+ );
+ }
+
if ( !$this->displayImg->isLocal() ) {
$this->printSharedImageText();
}
} else {
# Image does not exist
- if ( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) {
+ if ( !$this->getID() ) {
+ # No article exists either
+ # Show deletion log to be consistent with normal articles
+ LogEventsList::showLogExtract(
+ $out,
+ array( 'delete', 'move' ),
+ $this->getTitle()->getPrefixedText(),
+ '',
+ array( 'lim' => 10,
+ 'conds' => array( "log_action != 'revision'" ),
+ 'showIfEmpty' => false,
+ 'msgKey' => array( 'moveddeleted-notice' )
+ )
+ );
+ }
+
+ if ( $wgEnableUploads && $user->isAllowed( 'upload' ) ) {
// Only show an upload link if the user can upload
$uploadTitle = SpecialPage::getTitleFor( 'Upload' );
$nofile = array(
@@ -480,15 +566,15 @@ EOT
// Note, if there is an image description page, but
// no image, then this setRobotPolicy is overriden
// by Article::View().
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
+ $out->setRobotPolicy( 'noindex,nofollow' );
+ $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
if ( !$this->getID() && $wgSend404Code ) {
// If there is no image, no shared image, and no description page,
// output a 404, to be consistent with articles.
- $wgRequest->response()->header( 'HTTP/1.1 404 Not Found' );
+ $request->response()->header( 'HTTP/1.1 404 Not Found' );
}
}
- $wgOut->setFileVersion( $this->displayImg );
+ $out->setFileVersion( $this->displayImg );
}
/**
@@ -518,8 +604,7 @@ EOT
* Show a notice that the file is from a shared repository
*/
protected function printSharedImageText() {
- global $wgOut;
-
+ $out = $this->getContext()->getOutput();
$this->loadFile();
$descUrl = $this->mPage->getFile()->getDescriptionUrl();
@@ -527,18 +612,18 @@ EOT
/* Add canonical to head if there is no local page for this shared file */
if( $descUrl && $this->mPage->getID() == 0 ) {
- $wgOut->addLink( array( 'rel' => 'canonical', 'href' => $descUrl ) );
+ $out->addLink( array( 'rel' => 'canonical', 'href' => $descUrl ) );
}
$wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n";
$repo = $this->mPage->getFile()->getRepo()->getDisplayName();
- if ( $descUrl && $descText && wfMsgNoTrans( 'sharedupload-desc-here' ) !== '-' ) {
- $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) );
- } elseif ( $descUrl && wfMsgNoTrans( 'sharedupload-desc-there' ) !== '-' ) {
- $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) );
+ 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 ) );
} else {
- $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ );
+ $out->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ );
}
if ( $descText ) {
@@ -560,7 +645,7 @@ EOT
* external editing (and instructions link) etc.
*/
protected function uploadLinksBox() {
- global $wgUser, $wgOut, $wgEnableUploads, $wgUseExternalEditor;
+ global $wgEnableUploads, $wgUseExternalEditor;
if ( !$wgEnableUploads ) {
return;
@@ -571,35 +656,38 @@ EOT
return;
}
- $wgOut->addHTML( "<br /><ul>\n" );
+ $out = $this->getContext()->getOutput();
+ $out->addHTML( "<ul>\n" );
# "Upload a new version of this file" link
- if ( UploadBase::userCanReUpload( $wgUser, $this->mPage->getFile()->name ) ) {
- $ulink = Linker::makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) );
- $wgOut->addHTML( "<li id=\"mw-imagepage-reupload-link\"><div class=\"plainlinks\">{$ulink}</div></li>\n" );
+ $canUpload = $this->getTitle()->userCan( 'upload', $this->getContext()->getUser() );
+ if ( $canUpload && UploadBase::userCanReUpload( $this->getContext()->getUser(), $this->mPage->getFile()->name ) ) {
+ $ulink = Linker::makeExternalLink( $this->getUploadUrl(), wfMessage( 'uploadnewversion-linktext' )->text() );
+ $out->addHTML( "<li id=\"mw-imagepage-reupload-link\"><div class=\"plainlinks\">{$ulink}</div></li>\n" );
+ } else {
+ $out->addHTML( "<li id=\"mw-imagepage-upload-disallowed\">" . $this->getContext()->msg( 'upload-disallowed-here' )->escaped() . "</li>\n" );
}
# External editing link
if ( $wgUseExternalEditor ) {
- $elink = Linker::link(
+ $elink = Linker::linkKnown(
$this->getTitle(),
- wfMsgHtml( 'edit-externally' ),
+ wfMessage( 'edit-externally' )->escaped(),
array(),
array(
'action' => 'edit',
'externaledit' => 'true',
'mode' => 'file'
- ),
- array( 'known', 'noclasses' )
+ )
);
- $wgOut->addHTML(
+ $out->addHTML(
'<li id="mw-imagepage-edit-external">' . $elink . ' <small>' .
- wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) .
- "</small></li>\n"
+ wfMessage( 'edit-externally-help' )->parse() .
+ "</small></li>\n"
);
}
- $wgOut->addHTML( "</ul>\n" );
+ $out->addHTML( "</ul>\n" );
}
protected function closeShowImage() { } # For overloading
@@ -609,12 +697,11 @@ EOT
* we follow it with an upload history of the image and its usage.
*/
protected function imageHistory() {
- global $wgOut;
-
$this->loadFile();
+ $out = $this->getContext()->getOutput();
$pager = new ImageHistoryPseudoPager( $this );
- $wgOut->addHTML( $pager->getBody() );
- $wgOut->preventClickjacking( $pager->getPreventClickjacking() );
+ $out->addHTML( $pager->getBody() );
+ $out->preventClickjacking( $pager->getPreventClickjacking() );
$this->mPage->getFile()->resetHistory(); // free db resources
@@ -643,10 +730,9 @@ EOT
}
protected function imageLinks() {
- global $wgOut, $wgLang;
-
$limit = 100;
+ $out = $this->getContext()->getOutput();
$res = $this->queryImageLinks( $this->getTitle()->getDbKey(), $limit + 1);
$rows = array();
$redirects = array();
@@ -670,7 +756,7 @@ EOT
}
if ( $count == 0 ) {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
Html::rawElement( 'div',
array( 'id' => 'mw-imagepage-nolinkstoimage' ), "\n$1\n" ),
'nolinkstoimage'
@@ -678,18 +764,18 @@ EOT
return;
}
- $wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
+ $out->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
if ( !$hasMore ) {
- $wgOut->addWikiMsg( 'linkstoimage', $count );
+ $out->addWikiMsg( 'linkstoimage', $count );
} else {
// More links than the limit. Add a link to [[Special:Whatlinkshere]]
- $wgOut->addWikiMsg( 'linkstoimage-more',
- $wgLang->formatNum( $limit ),
+ $out->addWikiMsg( 'linkstoimage-more',
+ $this->getContext()->getLanguage()->formatNum( $limit ),
$this->getTitle()->getPrefixedDBkey()
);
}
- $wgOut->addHTML(
+ $out->addHTML(
Html::openElement( 'ul',
array( 'class' => 'mw-imagepage-linkstoimage' ) ) . "\n"
);
@@ -720,7 +806,7 @@ EOT
$link2 = Linker::linkKnown( Title::makeTitle( $row->page_namespace, $row->page_title ) );
$ul .= Html::rawElement(
'li',
- array( 'id' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
+ array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
$link2
) . "\n";
}
@@ -728,39 +814,38 @@ EOT
$liContents = wfMessage( 'linkstoimage-redirect' )->rawParams(
$link, $ul )->parse();
}
- $wgOut->addHTML( Html::rawElement(
+ $out->addHTML( Html::rawElement(
'li',
- array( 'id' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
+ array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
$liContents
) . "\n"
);
};
- $wgOut->addHTML( Html::closeElement( 'ul' ) . "\n" );
+ $out->addHTML( Html::closeElement( 'ul' ) . "\n" );
$res->free();
// Add a links to [[Special:Whatlinkshere]]
if ( $count > $limit ) {
- $wgOut->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() );
+ $out->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() );
}
- $wgOut->addHTML( Html::closeElement( 'div' ) . "\n" );
+ $out->addHTML( Html::closeElement( 'div' ) . "\n" );
}
protected function imageDupes() {
- global $wgOut, $wgLang;
-
$this->loadFile();
+ $out = $this->getContext()->getOutput();
$dupes = $this->mPage->getDuplicates();
if ( count( $dupes ) == 0 ) {
return;
}
- $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
- $wgOut->addWikiMsg( 'duplicatesoffile',
- $wgLang->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey()
+ $out->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
+ $out->addWikiMsg( 'duplicatesoffile',
+ $this->getContext()->getLanguage()->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey()
);
- $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
+ $out->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
/**
* @var $file File
@@ -768,21 +853,15 @@ EOT
foreach ( $dupes as $file ) {
$fromSrc = '';
if ( $file->isLocal() ) {
- $link = Linker::link(
- $file->getTitle(),
- null,
- array(),
- array(),
- array( 'known', 'noclasses' )
- );
+ $link = Linker::linkKnown( $file->getTitle() );
} else {
$link = Linker::makeExternalLink( $file->getDescriptionUrl(),
$file->getTitle()->getPrefixedText() );
- $fromSrc = wfMsg( 'shared-repo-from', $file->getRepo()->getDisplayName() );
+ $fromSrc = wfMessage( 'shared-repo-from', $file->getRepo()->getDisplayName() )->text();
}
- $wgOut->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
+ $out->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
}
- $wgOut->addHTML( "</ul></div>\n" );
+ $out->addHTML( "</ul></div>\n" );
}
/**
@@ -806,12 +885,12 @@ EOT
* @param $description String
*/
function showError( $description ) {
- global $wgOut;
- $wgOut->setPageTitle( wfMessage( 'internalerror' ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->setArticleRelated( false );
- $wgOut->enableClientCache( false );
- $wgOut->addWikiText( $description );
+ $out = $this->getContext()->getOutput();
+ $out->setPageTitle( wfMessage( 'internalerror' ) );
+ $out->setRobotPolicy( 'noindex,nofollow' );
+ $out->setArticleRelated( false );
+ $out->enableClientCache( false );
+ $out->addWikiText( $description );
}
/**
@@ -836,7 +915,7 @@ EOT
*
* @ingroup Media
*/
-class ImageHistoryList {
+class ImageHistoryList extends ContextSource {
/**
* @var Title
@@ -871,6 +950,7 @@ class ImageHistoryList {
$this->title = $imagePage->getTitle();
$this->imagePage = $imagePage;
$this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender();
+ $this->setContext( $imagePage->getContext() );
}
/**
@@ -892,19 +972,18 @@ class ImageHistoryList {
* @return string
*/
public function beginImageHistoryList( $navLinks = '' ) {
- global $wgOut, $wgUser;
- return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) . "\n"
+ return Xml::element( 'h2', array( 'id' => 'filehistory' ), $this->msg( 'filehist' )->text() ) . "\n"
. "<div id=\"mw-imagepage-section-filehistory\">\n"
- . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
+ . $this->msg( 'filehist-help' )->parseAsBlock()
. $navLinks . "\n"
. Xml::openElement( 'table', array( 'class' => 'wikitable filehistory' ) ) . "\n"
. '<tr><td></td>'
- . ( $this->current->isLocal() && ( $wgUser->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' )
- . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
- . ( $this->showThumb ? '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>' : '' )
- . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
- . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
- . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
+ . ( $this->current->isLocal() && ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' )
+ . '<th>' . $this->msg( 'filehist-datetime' )->escaped() . '</th>'
+ . ( $this->showThumb ? '<th>' . $this->msg( 'filehist-thumb' )->escaped() . '</th>' : '' )
+ . '<th>' . $this->msg( 'filehist-dimensions' )->escaped() . '</th>'
+ . '<th>' . $this->msg( 'filehist-user' )->escaped() . '</th>'
+ . '<th>' . $this->msg( 'filehist-comment' )->escaped() . '</th>'
. "</tr>\n";
}
@@ -922,43 +1001,45 @@ class ImageHistoryList {
* @return string
*/
public function imageHistoryLine( $iscur, $file ) {
- global $wgUser, $wgLang, $wgContLang;
+ global $wgContLang;
+ $user = $this->getUser();
+ $lang = $this->getLanguage();
$timestamp = wfTimestamp( TS_MW, $file->getTimestamp() );
$img = $iscur ? $file->getName() : $file->getArchiveName();
- $user = $file->getUser( 'id' );
- $usertext = $file->getUser( 'text' );
- $description = $file->getDescription();
+ $userId = $file->getUser( 'id' );
+ $userText = $file->getUser( 'text' );
+ $description = $file->getDescription( File::FOR_THIS_USER, $user );
$local = $this->current->isLocal();
$row = $selected = '';
// Deletion link
- if ( $local && ( $wgUser->isAllowedAny( 'delete', 'deletedhistory' ) ) ) {
+ if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) {
$row .= '<td>';
# Link to remove from history
- if ( $wgUser->isAllowed( 'delete' ) ) {
+ if ( $user->isAllowed( 'delete' ) ) {
$q = array( 'action' => 'delete' );
if ( !$iscur ) {
$q['oldimage'] = $img;
}
- $row .= Linker::link(
+ $row .= Linker::linkKnown(
$this->title,
- wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
- array(), $q, array( 'known' )
+ $this->msg( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' )->escaped(),
+ array(), $q
);
}
# Link to hide content. Don't show useless link to people who cannot hide revisions.
- $canHide = $wgUser->isAllowed( 'deleterevision' );
- if ( $canHide || ( $wgUser->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) {
- if ( $wgUser->isAllowed( 'delete' ) ) {
+ $canHide = $user->isAllowed( 'deleterevision' );
+ if ( $canHide || ( $user->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) {
+ if ( $user->isAllowed( 'delete' ) ) {
$row .= '<br />';
}
// If file is top revision or locked from this user, don't link
- if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED ) ) {
+ if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
$del = Linker::revDeleteLinkDisabled( $canHide );
} else {
- list( $ts, $name ) = explode( '!', $img, 2 );
+ list( $ts, ) = explode( '!', $img, 2 );
$query = array(
'type' => 'oldimage',
'target' => $this->title->getPrefixedText(),
@@ -975,21 +1056,22 @@ class ImageHistoryList {
// Reversion link/current indicator
$row .= '<td>';
if ( $iscur ) {
- $row .= wfMsgHtml( 'filehist-current' );
- } elseif ( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
+ $row .= $this->msg( 'filehist-current' )->escaped();
+ } elseif ( $local && $this->title->quickUserCan( 'edit', $user )
+ && $this->title->quickUserCan( 'upload', $user )
+ ) {
if ( $file->isDeleted( File::DELETED_FILE ) ) {
- $row .= wfMsgHtml( 'filehist-revert' );
+ $row .= $this->msg( 'filehist-revert' )->escaped();
} else {
- $row .= Linker::link(
+ $row .= Linker::linkKnown(
$this->title,
- wfMsgHtml( 'filehist-revert' ),
+ $this->msg( 'filehist-revert' )->escaped(),
array(),
array(
'action' => 'revert',
'oldimage' => $img,
- 'wpEditToken' => $wgUser->getEditToken( $img )
- ),
- array( 'known', 'noclasses' )
+ 'wpEditToken' => $user->getEditToken( $img )
+ )
);
}
}
@@ -1000,32 +1082,31 @@ class ImageHistoryList {
$selected = "class='filehistory-selected'";
}
$row .= "<td $selected style='white-space: nowrap;'>";
- if ( !$file->userCan( File::DELETED_FILE ) ) {
+ if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
# Don't link to unviewable files
- $row .= '<span class="history-deleted">' . $wgLang->timeanddate( $timestamp, true ) . '</span>';
+ $row .= '<span class="history-deleted">' . $lang->userTimeAndDate( $timestamp, $user ) . '</span>';
} elseif ( $file->isDeleted( File::DELETED_FILE ) ) {
if ( $local ) {
$this->preventClickjacking();
$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
# Make a link to review the image
- $url = Linker::link(
+ $url = Linker::linkKnown(
$revdel,
- $wgLang->timeanddate( $timestamp, true ),
+ $lang->userTimeAndDate( $timestamp, $user ),
array(),
array(
'target' => $this->title->getPrefixedText(),
'file' => $img,
- 'token' => $wgUser->getEditToken( $img )
- ),
- array( 'known', 'noclasses' )
+ 'token' => $user->getEditToken( $img )
+ )
);
} else {
- $url = $wgLang->timeanddate( $timestamp, true );
+ $url = $lang->userTimeAndDate( $timestamp, $user );
}
$row .= '<span class="history-deleted">' . $url . '</span>';
} else {
$url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img );
- $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeanddate( $timestamp, true ) );
+ $row .= Xml::element( 'a', array( 'href' => $url ), $lang->userTimeAndDate( $timestamp, $user ) );
}
$row .= "</td>";
@@ -1037,27 +1118,33 @@ class ImageHistoryList {
// Image dimensions + size
$row .= '<td>';
$row .= htmlspecialchars( $file->getDimensionsString() );
- $row .= ' <span style="white-space: nowrap;">(' . Linker::formatSize( $file->getSize() ) . ')</span>';
+ $row .= $this->msg( 'word-separator' )->plain();
+ $row .= '<span style="white-space: nowrap;">';
+ $row .= $this->msg( 'parentheses' )->rawParams( Linker::formatSize( $file->getSize() ) )->plain();
+ $row .= '</span>';
$row .= '</td>';
// Uploading user
$row .= '<td>';
// Hide deleted usernames
if ( $file->isDeleted( File::DELETED_USER ) ) {
- $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ $row .= '<span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
} else {
if ( $local ) {
- $row .= Linker::userLink( $user, $usertext ) . ' <span style="white-space: nowrap;">' .
- Linker::userToolLinks( $user, $usertext ) . '</span>';
+ $row .= Linker::userLink( $userId, $userText );
+ $row .= $this->msg( 'word-separator' )->plain();
+ $row .= '<span style="white-space: nowrap;">';
+ $row .= Linker::userToolLinks( $userId, $userText );
+ $row .= '</span>';
} else {
- $row .= htmlspecialchars( $usertext );
+ $row .= htmlspecialchars( $userText );
}
}
$row .= '</td>';
// Don't show deleted descriptions
if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
- $row .= '<td><span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></td>';
+ $row .= '<td><span class="history-deleted">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span></td>';
} else {
$row .= '<td dir="' . $wgContLang->getDir() . '">' . Linker::formatComment( $description, $this->title ) . '</td>';
}
@@ -1074,9 +1161,11 @@ class ImageHistoryList {
* @return string
*/
protected function getThumbForLine( $file ) {
- global $wgLang;
-
- if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) {
+ $lang = $this->getLanguage();
+ $user = $this->getUser();
+ if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE,$user )
+ && !$file->isDeleted( File::DELETED_FILE ) )
+ {
$params = array(
'width' => '120',
'height' => '120',
@@ -1085,20 +1174,20 @@ class ImageHistoryList {
$thumbnail = $file->transform( $params );
$options = array(
- 'alt' => wfMsg( 'filehist-thumbtext',
- $wgLang->timeanddate( $timestamp, true ),
- $wgLang->date( $timestamp, true ),
- $wgLang->time( $timestamp, true ) ),
+ 'alt' => $this->msg( 'filehist-thumbtext',
+ $lang->userTimeAndDate( $timestamp, $user ),
+ $lang->userDate( $timestamp, $user ),
+ $lang->userTime( $timestamp, $user ) )->text(),
'file-link' => true,
);
if ( !$thumbnail ) {
- return wfMsgHtml( 'filehist-nothumb' );
+ return $this->msg( 'filehist-nothumb' )->escaped();
}
return $thumbnail->toHtml( $options );
} else {
- return wfMsgHtml( 'filehist-nothumb' );
+ return $this->msg( 'filehist-nothumb' )->escaped();
}
}
@@ -1162,6 +1251,7 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager {
}
/**
+ * @param $row object
* @return string
*/
function formatRow( $row ) {
diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php
index f46974b2..f9f6ceed 100644
--- a/includes/ImageQueryPage.php
+++ b/includes/ImageQueryPage.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Variant of QueryPage which uses a gallery to output results.
+ *
+ * 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
+ */
/**
* Variant of QueryPage which uses a gallery to output results, thus
diff --git a/includes/Import.php b/includes/Import.php
index e906c7f0..11f37952 100644
--- a/includes/Import.php
+++ b/includes/Import.php
@@ -1,6 +1,6 @@
<?php
/**
- * MediaWiki page data importer
+ * MediaWiki page data importer.
*
* Copyright © 2003,2005 Brion Vibber <brion@pobox.com>
* http://www.mediawiki.org/
@@ -33,7 +33,7 @@
class WikiImporter {
private $reader = null;
private $mLogItemCallback, $mUploadCallback, $mRevisionCallback, $mPageCallback;
- private $mSiteInfoCallback, $mTargetNamespace, $mPageOutCallback;
+ private $mSiteInfoCallback, $mTargetNamespace, $mTargetRootPage, $mPageOutCallback;
private $mNoticeCallback, $mDebug;
private $mImportUploads, $mImageBasePath;
private $mNoUpdates = false;
@@ -200,6 +200,39 @@ class WikiImporter {
}
/**
+ * Set a target root page under which all pages are imported
+ * @param $rootpage
+ * @return status object
+ */
+ public function setTargetRootPage( $rootpage ) {
+ $status = Status::newGood();
+ if( is_null( $rootpage ) ) {
+ // No rootpage
+ $this->mTargetRootPage = null;
+ } elseif( $rootpage !== '' ) {
+ $rootpage = rtrim( $rootpage, '/' ); //avoid double slashes
+ $title = Title::newFromText( $rootpage, !is_null( $this->mTargetNamespace ) ? $this->mTargetNamespace : NS_MAIN );
+ if( !$title || $title->isExternal() ) {
+ $status->fatal( 'import-rootpage-invalid' );
+ } else {
+ if( !MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ global $wgContLang;
+
+ $displayNSText = $title->getNamespace() == NS_MAIN
+ ? wfMessage( 'blanknamespace' )->text()
+ : $wgContLang->getNsText( $title->getNamespace() );
+ $status->fatal( 'import-rootpage-nosubpage', $displayNSText );
+ } else {
+ // set namespace to 'all', so the namespace check in processTitle() can passed
+ $this->setTargetNamespace( null );
+ $this->mTargetRootPage = $title->getPrefixedDBKey();
+ }
+ }
+ }
+ return $status;
+ }
+
+ /**
* @param $dir
*/
public function setImageBasePath( $dir ) {
@@ -275,7 +308,7 @@ class WikiImporter {
}
/**
- * Notify the callback function when a new <page> is reached.
+ * Notify the callback function when a new "<page>" is reached.
* @param $title Title
*/
function pageCallback( $title ) {
@@ -285,7 +318,7 @@ class WikiImporter {
}
/**
- * Notify the callback function when a </page> is closed.
+ * Notify the callback function when a "</page>" is closed.
* @param $title Title
* @param $origTitle Title
* @param $revCount Integer
@@ -301,7 +334,8 @@ class WikiImporter {
/**
* Notify the callback function of a revision
- * @param $revision A WikiRevision object
+ * @param $revision WikiRevision object
+ * @return bool|mixed
*/
private function revisionCallback( $revision ) {
if ( isset( $this->mRevisionCallback ) ) {
@@ -314,7 +348,8 @@ class WikiImporter {
/**
* Notify the callback function of a new log item
- * @param $revision A WikiRevision object
+ * @param $revision WikiRevision object
+ * @return bool|mixed
*/
private function logItemCallback( $revision ) {
if ( isset( $this->mLogItemCallback ) ) {
@@ -394,6 +429,7 @@ class WikiImporter {
/**
* Primary entry point
+ * @return bool
*/
public function doImport() {
$this->reader->read();
@@ -783,9 +819,14 @@ class WikiImporter {
$origTitle = Title::newFromText( $workTitle );
if( !is_null( $this->mTargetNamespace ) && !is_null( $origTitle ) ) {
- $title = Title::makeTitle( $this->mTargetNamespace,
+ # 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 ) ) {
+ $workTitle = $this->mTargetRootPage . '/' . $workTitle;
+ }
$title = Title::newFromText( $workTitle );
}
@@ -826,7 +867,7 @@ class UploadSourceAdapter {
* @return string
*/
static function registerSource( $source ) {
- $id = wfGenerateToken();
+ $id = wfRandomString();
self::$sourceRegistrations[$id] = $source;
diff --git a/includes/Init.php b/includes/Init.php
index 72c10543..a8540f2c 100644
--- a/includes/Init.php
+++ b/includes/Init.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Some functions that are useful during startup.
+ *
+ * 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
+ */
/**
* Some functions that are useful during startup.
@@ -197,6 +217,7 @@ class MWInit {
* @param $methodName string
* @param $args array
*
+ * @return mixed
*/
static function callStaticMethod( $className, $methodName, $args ) {
$r = new ReflectionMethod( $className, $methodName );
diff --git a/includes/Licenses.php b/includes/Licenses.php
index 8a06c6fc..ba504a99 100644
--- a/includes/Licenses.php
+++ b/includes/Licenses.php
@@ -1,14 +1,32 @@
<?php
/**
- * A License class for use on Special:Upload
+ * License selector for use on Special:Upload.
*
- * @ingroup SpecialPage
+ * 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 Ævar Arnfjörð Bjarmason <avarab@gmail.com>
* @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
+/**
+ * A License class for use on Special:Upload
+ */
class Licenses extends HTMLFormField {
/**
* @var string
@@ -34,7 +52,7 @@ class Licenses extends HTMLFormField {
public function __construct( $params ) {
parent::__construct( $params );
- $this->msg = empty( $params['licenses'] ) ? wfMsgForContent( 'licenses' ) : $params['licenses'];
+ $this->msg = empty( $params['licenses'] ) ? wfMessage( 'licenses' )->inContentLanguage()->plain() : $params['licenses'];
$this->selected = null;
$this->makeLicenses();
@@ -102,7 +120,7 @@ class Licenses extends HTMLFormField {
foreach ( $tagset as $key => $val )
if ( is_array( $val ) ) {
$this->html .= $this->outputOption(
- $this->msg( $key ), '',
+ $key, '',
array(
'disabled' => 'disabled',
'style' => 'color: GrayText', // for MSIE
@@ -112,7 +130,7 @@ class Licenses extends HTMLFormField {
$this->makeHtml( $val, $depth + 1 );
} else {
$this->html .= $this->outputOption(
- $this->msg( $val->text ), $val->template,
+ $val->text, $val->template,
array( 'title' => '{{' . $val->template . '}}' ),
$depth
);
@@ -120,13 +138,15 @@ class Licenses extends HTMLFormField {
}
/**
- * @param $text
+ * @param $message
* @param $value
* @param $attribs null
* @param $depth int
* @return string
*/
- protected function outputOption( $text, $value, $attribs = null, $depth = 0 ) {
+ protected function outputOption( $message, $value, $attribs = null, $depth = 0 ) {
+ $msgObj = $this->msg( $message );
+ $text = $msgObj->exists() ? $msgObj->text() : $message;
$attribs['value'] = $value;
if ( $value === $this->selected )
$attribs['selected'] = 'selected';
@@ -134,15 +154,6 @@ class Licenses extends HTMLFormField {
return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n";
}
- /**
- * @param $str string
- * @return String
- */
- protected function msg( $str ) {
- $msg = wfMessage( $str );
- return $msg->exists() ? $msg->text() : $str;
- }
-
/**#@-*/
/**
@@ -164,7 +175,7 @@ class Licenses extends HTMLFormField {
public function getInputHTML( $value ) {
$this->selected = $value;
- $this->html = $this->outputOption( wfMsg( 'nolicense' ), '',
+ $this->html = $this->outputOption( wfMessage( 'nolicense' )->text(), '',
(bool)$this->selected ? null : array( 'selected' => 'selected' ) );
$this->makeHtml( $this->getLicenses() );
diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php
index af7680fb..214f4959 100644
--- a/includes/LinkFilter.php
+++ b/includes/LinkFilter.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Functions to help implement an external link filter for spam control.
+ *
+ * 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
+ */
+
/**
* Some functions to help implement an external link filter for spam control.
@@ -120,7 +141,7 @@ class LinkFilter {
* Filters an array returned by makeLikeArray(), removing everything past first pattern placeholder.
*
* @param $arr array: array to filter
- * @return filtered array
+ * @return array filtered array
*/
public static function keepOneWildcard( $arr ) {
if( !is_array( $arr ) ) {
diff --git a/includes/Linker.php b/includes/Linker.php
index 575f2841..56626bd7 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Methods to make links and related items.
+ *
+ * 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
+ */
+
+/**
* Some internal bits split of from Skin.php. These functions are used
* for primarily page content: links, embedded images, table of contents. Links
* are also used in the skin.
@@ -20,6 +41,7 @@ class Linker {
*
* @param $class String: 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
*/
static function getExternalLinkAttributes( $class = 'external' ) {
@@ -36,6 +58,7 @@ class Linker {
* @param $unused String: unused
* @param $class String: the contents of the class attribute; if an empty
* string is passed, which is the default value, defaults to 'external'.
+ * @return string
*/
static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) {
global $wgContLang;
@@ -57,6 +80,7 @@ class Linker {
* not HTML-escaped
* @param $unused String: unused
* @param $class String: the contents of the class attribute, default none
+ * @return string
*/
static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) {
$title = urldecode( $title );
@@ -73,6 +97,7 @@ class Linker {
* @param $class String: 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
*/
static function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) {
if ( $title === false ) {
@@ -114,9 +139,9 @@ class Linker {
if ( $t->isRedirect() ) {
# Page is a redirect
$colour = 'mw-redirect';
- } elseif ( $threshold > 0 &&
- $t->exists() && $t->getLength() < $threshold &&
- $t->isContentPage() ) {
+ } elseif ( $threshold > 0 && $t->isContentPage() &&
+ $t->exists() && $t->getLength() < $threshold
+ ) {
# Page is a stub
$colour = 'stub';
}
@@ -173,6 +198,12 @@ class Linker {
wfProfileOut( __METHOD__ );
return "<!-- ERROR -->$html";
}
+
+ 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 );
+ }
$options = (array)$options;
$dummy = new DummyLinker; // dummy linker instance for bc on the hooks
@@ -229,6 +260,7 @@ class Linker {
/**
* Identical to link(), except $options defaults to 'known'.
+ * @return string
*/
public static function linkKnown(
$target, $html = null, $customAttribs = array(),
@@ -243,6 +275,7 @@ class Linker {
* @param $target Title
* @param $query Array: query parameters
* @param $options Array
+ * @return String
*/
private static function linkUrl( $target, $query, $options ) {
wfProfileIn( __METHOD__ );
@@ -312,7 +345,7 @@ class Linker {
} elseif ( in_array( 'known', $options ) ) {
$defaults['title'] = $target->getPrefixedText();
} else {
- $defaults['title'] = wfMsg( 'red-link-title', $target->getPrefixedText() );
+ $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )->text();
}
# Finally, merge the custom attribs with the default ones, and iterate
@@ -380,6 +413,11 @@ 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]
+ *
*
* @return string
*/
@@ -392,6 +430,31 @@ class Linker {
}
/**
+ * Get a message saying that an invalid title was encountered.
+ * This should be called after a method like Title::makeTitleSafe() returned
+ * 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
+ */
+ public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
+ global $wgContLang;
+
+ // First we check whether the namespace exists or not.
+ if ( MWNamespace::exists( $namespace ) ) {
+ if ( $namespace == NS_MAIN ) {
+ $name = $context->msg( 'blanknamespace' )->text();
+ } else {
+ $name = $wgContLang->getFormattedNsText( $namespace );
+ }
+ return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
+ } else {
+ return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
+ }
+ }
+
+ /**
* @param $title Title
* @return Title
*/
@@ -456,7 +519,8 @@ class Linker {
* Given parameters derived from [[Image:Foo|options...]], generate the
* HTML that that syntax inserts in the page.
*
- * @param $title Title object
+ * @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.
* Boolean parameters are indicated by presence or absence, the value is arbitrary and
@@ -472,6 +536,7 @@ class Linker {
* valign Vertical alignment (baseline, sub, super, top, text-top, middle,
* bottom, text-bottom)
* alt Alternate text for image (i.e. alt attribute). Plain text.
+ * class HTML for image classes. Plain text.
* caption HTML for image caption.
* link-url URL to link to
* link-title Title object to link to
@@ -483,9 +548,10 @@ class Linker {
* @param $time String: timestamp of the file, set as false for current
* @param $query String: 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.
*/
- public static function makeImageLink2( Title $title, $file, $frameParams = array(),
+ public static function makeImageLink( /*Parser*/ $parser, Title $title, $file, $frameParams = array(),
$handlerParams = array(), $time = false, $query = "", $widthOption = null )
{
$res = null;
@@ -515,6 +581,9 @@ class Linker {
if ( !isset( $fp['title'] ) ) {
$fp['title'] = '';
}
+ if ( !isset( $fp['class'] ) ) {
+ $fp['class'] = '';
+ }
$prefix = $postfix = '';
@@ -558,16 +627,20 @@ class Linker {
}
if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
- global $wgContLang;
- # Create a thumbnail. Alignment depends on language
- # writing direction, # right aligned for left-to-right-
- # languages ("Western languages"), left-aligned
- # for right-to-left-languages ("Semitic languages")
+ # Create a thumbnail. Alignment depends on the writing direction of
+ # the page content language (right-aligned for LTR languages,
+ # left-aligned for RTL languages)
#
- # If thumbnail width has not been provided, it is set
+ # If a thumbnail width has not been provided, it is set
# to the default user option as specified in Language*.php
if ( $fp['align'] == '' ) {
- $fp['align'] = $wgContLang->alignEnd();
+ if( $parser instanceof Parser ) {
+ $fp['align'] = $parser->getTargetLanguage()->alignEnd();
+ } else {
+ # backwards compatibility, remove with makeImageLink2()
+ global $wgContLang;
+ $fp['align'] = $wgContLang->alignEnd();
+ }
}
return $prefix . self::makeThumbLink2( $title, $file, $fp, $hp, $time, $query ) . $postfix;
}
@@ -594,9 +667,12 @@ class Linker {
$params = array(
'alt' => $fp['alt'],
'title' => $fp['title'],
- 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false ,
- 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false );
- $params = self::getImageLinkMTOParams( $fp, $query ) + $params;
+ 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false,
+ 'img-class' => $fp['class'] );
+ if ( isset( $fp['border'] ) ) {
+ $params['img-class'] .= ( $params['img-class'] !== '' ) ? ' thumbborder' : 'thumbborder';
+ }
+ $params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params;
$s = $thumb->toHtml( $params );
}
@@ -607,18 +683,37 @@ class Linker {
}
/**
+ * See makeImageLink()
+ * When this function is removed, remove if( $parser instanceof Parser ) check there too
+ * @deprecated since 1.20
+ */
+ public static function makeImageLink2( Title $title, $file, $frameParams = array(),
+ $handlerParams = array(), $time = false, $query = "", $widthOption = null ) {
+ return self::makeImageLink( null, $title, $file, $frameParams,
+ $handlerParams, $time, $query, $widthOption );
+ }
+
+ /**
* Get the link parameters for MediaTransformOutput::toHtml() from given
* frame parameters supplied by the Parser.
- * @param $frameParams The frame parameters
- * @param $query An optional query string to add to description page links
+ * @param $frameParams array The frame parameters
+ * @param $query string An optional query string to add to description page links
+ * @return array
*/
- private static function getImageLinkMTOParams( $frameParams, $query = '' ) {
+ private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
$mtoParams = array();
if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
$mtoParams['custom-url-link'] = $frameParams['link-url'];
if ( isset( $frameParams['link-target'] ) ) {
$mtoParams['custom-target-link'] = $frameParams['link-target'];
}
+ if ( $parser ) {
+ $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
+ foreach ( $extLinkAttrs as $name => $val ) {
+ // Currently could include 'rel' and 'target'
+ $mtoParams['parser-extlink-'.$name] = $val;
+ }
+ }
} elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
$mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] );
} elseif ( !empty( $frameParams['no-link'] ) ) {
@@ -640,6 +735,7 @@ class Linker {
* @param $params Array
* @param $framed Boolean
* @param $manualthumb String
+ * @return mixed
*/
public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
$align = 'right', $params = array(), $framed = false , $manualthumb = "" )
@@ -736,13 +832,14 @@ class Linker {
$s .= self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
$zoomIcon = '';
} elseif ( !$thumb ) {
- $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) );
+ $s .= wfMessage( 'thumbnail_error', '' )->escaped();
$zoomIcon = '';
} else {
$params = array(
'alt' => $fp['alt'],
'title' => $fp['title'],
- 'img-class' => 'thumbimage' );
+ 'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== '' ) ? $fp['class'] . ' thumbimage' : 'thumbimage'
+ );
$params = self::getImageLinkMTOParams( $fp, $query ) + $params;
$s .= $thumb->toHtml( $params );
if ( isset( $fp['framed'] ) ) {
@@ -752,7 +849,7 @@ class Linker {
Html::rawElement( 'a', array(
'href' => $url,
'class' => 'internal',
- 'title' => wfMsg( 'thumbnail-more' ) ),
+ 'title' => wfMessage( 'thumbnail-more' )->text() ),
Html::element( 'img', array(
'src' => $wgStylePath . '/common/images/magnify-clip' . ( $wgContLang->isRTL() ? '-rtl' : '' ) . '.png',
'width' => 15,
@@ -848,7 +945,7 @@ class Linker {
* This will make a broken link if $file is false.
*
* @param $title Title object.
- * @param $file File|false mixed File object or false
+ * @param $file File|bool mixed File object or false
* @param $html String: pre-sanitized HTML
* @return String: HTML
*
@@ -882,7 +979,7 @@ class Linker {
$key = strtolower( $name );
}
- return self::linkKnown( SpecialPage::getTitleFor( $name ) , wfMsg( $key ) );
+ return self::linkKnown( SpecialPage::getTitleFor( $name ) , wfMessage( $key )->text() );
}
/**
@@ -892,6 +989,7 @@ class Linker {
* @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>
+ * @return string
*/
public static function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) {
$class = "external";
@@ -923,7 +1021,7 @@ class Linker {
* @param $userName String: user name in database.
* @param $altUserName String: text to display instead of the user name (optional)
* @return String: HTML fragment
- * @since 1.19 Method exists for a long time. $displayText was added in 1.19.
+ * @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 ) {
@@ -973,7 +1071,7 @@ class Linker {
}
$contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
- $items[] = self::link( $contribsPage, wfMsgHtml( 'contribslink' ), $attribs );
+ $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
}
if ( $blockable && $wgUser->isAllowed( 'block' ) ) {
$items[] = self::blockLink( $userId, $userText );
@@ -986,7 +1084,10 @@ class Linker {
wfRunHooks( 'UserToolLinksEdit', array( $userId, $userText, &$items ) );
if ( $items ) {
- return ' <span class="mw-usertoollinks">(' . $wgLang->pipeList( $items ) . ')</span>';
+ return wfMessage( 'word-separator' )->plain()
+ . '<span class="mw-usertoollinks">'
+ . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
+ . '</span>';
} else {
return '';
}
@@ -997,6 +1098,7 @@ class Linker {
* @param $userId Integer: user identifier
* @param $userText String: user name or IP address
* @param $edits Integer: user edit count (optional, for performance)
+ * @return String
*/
public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
return self::userToolLinks( $userId, $userText, true, 0, $edits );
@@ -1010,7 +1112,7 @@ class Linker {
*/
public static function userTalkLink( $userId, $userText ) {
$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
- $userTalkLink = self::link( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) );
+ $userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() );
return $userTalkLink;
}
@@ -1021,7 +1123,7 @@ class Linker {
*/
public static function blockLink( $userId, $userText ) {
$blockPage = SpecialPage::getTitleFor( 'Block', $userText );
- $blockLink = self::link( $blockPage, wfMsgHtml( 'blocklink' ) );
+ $blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() );
return $blockLink;
}
@@ -1032,7 +1134,7 @@ class Linker {
*/
public static function emailLink( $userId, $userText ) {
$emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
- $emailLink = self::link( $emailPage, wfMsgHtml( 'emaillink' ) );
+ $emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() );
return $emailLink;
}
@@ -1044,12 +1146,12 @@ class Linker {
*/
public static function revUserLink( $rev, $isPublic = false ) {
if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
- $link = wfMsgHtml( 'rev-deleted-user' );
+ $link = wfMessage( 'rev-deleted-user' )->escaped();
} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
$link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ),
$rev->getUserText( Revision::FOR_THIS_USER ) );
} else {
- $link = wfMsgHtml( 'rev-deleted-user' );
+ $link = wfMessage( 'rev-deleted-user' )->escaped();
}
if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
return '<span class="history-deleted">' . $link . '</span>';
@@ -1065,14 +1167,15 @@ class Linker {
*/
public static function revUserTools( $rev, $isPublic = false ) {
if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
- $link = wfMsgHtml( 'rev-deleted-user' );
+ $link = wfMessage( 'rev-deleted-user' )->escaped();
} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
$userId = $rev->getUser( Revision::FOR_THIS_USER );
$userText = $rev->getUserText( Revision::FOR_THIS_USER );
- $link = self::userLink( $userId, $userText ) .
- ' ' . self::userToolLinks( $userId, $userText );
+ $link = self::userLink( $userId, $userText )
+ . wfMessage( 'word-separator' )->plain()
+ . self::userToolLinks( $userId, $userText );
} else {
- $link = wfMsgHtml( 'rev-deleted-user' );
+ $link = wfMessage( 'rev-deleted-user' )->escaped();
}
if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
return ' <span class="history-deleted">' . $link . '</span>';
@@ -1095,6 +1198,7 @@ class Linker {
* @param $comment String
* @param $title Mixed: Title object (to generate link to the section in autocomment) or null
* @param $local Boolean: whether section links should refer to local page
+ * @return mixed|String
*/
public static function formatComment( $comment, $title = null, $local = false ) {
wfProfileIn( __METHOD__ );
@@ -1126,7 +1230,7 @@ class Linker {
* Called by Linker::formatComment.
*
* @param $comment String: comment text
- * @param $title An optional title object used to links to sections
+ * @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
*/
@@ -1155,41 +1259,45 @@ class Linker {
$pre = $match[1];
$auto = $match[2];
$post = $match[3];
- $link = '';
- if ( $title ) {
- $section = $auto;
-
- # Remove links that a user may have manually put in the autosummary
- # This could be improved by copying as much of Parser::stripSectionName as desired.
- $section = str_replace( '[[:', '', $section );
- $section = str_replace( '[[', '', $section );
- $section = str_replace( ']]', '', $section );
-
- $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
- if ( $local ) {
- $sectionTitle = Title::newFromText( '#' . $section );
- } else {
- $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
- $title->getDBkey(), $section );
+ $comment = null;
+ wfRunHooks( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) );
+ if ( $comment === null ) {
+ $link = '';
+ if ( $title ) {
+ $section = $auto;
+
+ # Remove links that a user may have manually put in the autosummary
+ # This could be improved by copying as much of Parser::stripSectionName as desired.
+ $section = str_replace( '[[:', '', $section );
+ $section = str_replace( '[[', '', $section );
+ $section = str_replace( ']]', '', $section );
+
+ $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
+ if ( $local ) {
+ $sectionTitle = Title::newFromText( '#' . $section );
+ } else {
+ $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
+ $title->getDBkey(), $section );
+ }
+ if ( $sectionTitle ) {
+ $link = self::link( $sectionTitle,
+ $wgLang->getArrow(), array(), array(),
+ 'noclasses' );
+ } else {
+ $link = '';
+ }
}
- if ( $sectionTitle ) {
- $link = self::link( $sectionTitle,
- $wgLang->getArrow(), array(), array(),
- 'noclasses' );
- } else {
- $link = '';
+ if ( $pre ) {
+ # written summary $presep autocomment (summary /* section */)
+ $pre .= wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
}
+ if ( $post ) {
+ # autocomment $postsep written summary (/* section */ summary)
+ $auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
+ }
+ $auto = '<span class="autocomment">' . $auto . '</span>';
+ $comment = $pre . $link . $wgLang->getDirMark() . '<span dir="auto">' . $auto . $post . '</span>';
}
- if ( $pre ) {
- # written summary $presep autocomment (summary /* section */)
- $pre .= wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) );
- }
- if ( $post ) {
- # autocomment $postsep written summary (/* section */ summary)
- $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) );
- }
- $auto = '<span class="autocomment">' . $auto . '</span>';
- $comment = $pre . $link . $wgLang->getDirMark() . '<span dir="auto">' . $auto . $post . '</span>';
return $comment;
}
@@ -1205,7 +1313,7 @@ class Linker {
*
* @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 $title An optional title object used to links to sections
+ * @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
*/
@@ -1399,7 +1507,8 @@ class Linker {
return '';
} else {
$formatted = self::formatComment( $comment, $title, $local );
- return " <span class=\"comment\" dir=\"auto\">($formatted)</span>";
+ $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
+ return " <span class=\"comment\">$formatted</span>";
}
}
@@ -1417,12 +1526,12 @@ class Linker {
return "";
}
if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
- $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
+ $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
} elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
$block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
$rev->getTitle(), $local );
} else {
- $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
+ $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
}
if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
return " <span class=\"history-deleted\">$block</span>";
@@ -1436,13 +1545,11 @@ class Linker {
*/
public static function formatRevisionSize( $size ) {
if ( $size == 0 ) {
- $stxt = wfMsgExt( 'historyempty', 'parsemag' );
+ $stxt = wfMessage( 'historyempty' )->escaped();
} else {
- global $wgLang;
- $stxt = wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $size ) );
- $stxt = "($stxt)";
+ $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
+ $stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
}
- $stxt = htmlspecialchars( $stxt );
return "<span class=\"history-size\">$stxt</span>";
}
@@ -1484,6 +1591,7 @@ class Linker {
* End a Table Of Contents line.
* tocUnindent() will be used instead if we're ending a line below
* the new level.
+ * @return string
*/
public static function tocLineEnd() {
return "</li>\n";
@@ -1493,11 +1601,13 @@ class Linker {
* Wraps the TOC in a table and provides the hide/collapse javascript.
*
* @param $toc String: html of the Table Of Contents
- * @param $lang mixed: Language code for the toc title
+ * @param $lang String|Language|false: Language for the toc title, defaults to user language
* @return String: full html of the TOC
*/
public static function tocList( $toc, $lang = false ) {
- $title = wfMsgExt( 'toc', array( 'language' => $lang, 'escape' ) );
+ $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"
@@ -1509,7 +1619,7 @@ class Linker {
* Generate a table of contents from a section tree
* Currently unused.
*
- * @param $tree Return value of ParserOutput::getSections()
+ * @param $tree array Return value of ParserOutput::getSections()
* @return String: HTML fragment
*/
public static function generateTOC( $tree ) {
@@ -1562,6 +1672,7 @@ class Linker {
/**
* Split a link trail, return the "inside" portion and the remainder of the trail
* as a two-element array
+ * @return array
*/
static function splitTrail( $trail ) {
global $wgContLang;
@@ -1589,38 +1700,102 @@ class Linker {
* other users.
*
* @param $rev Revision object
+ * @param $context IContextSource context to use or null for the main context.
+ * @return string
*/
- public static function generateRollback( $rev ) {
- return '<span class="mw-rollback-link">['
- . self::buildRollbackLink( $rev )
- . ']</span>';
+ public static function generateRollback( $rev, IContextSource $context = null ) {
+ if ( $context === null ) {
+ $context = RequestContext::getMain();
+ }
+
+ return '<span class="mw-rollback-link">'
+ . $context->msg( 'brackets' )->rawParams(
+ self::buildRollbackLink( $rev, $context ) )->plain()
+ . '</span>';
}
/**
* Build a raw rollback link, useful for collections of "tool" links
*
* @param $rev Revision object
+ * @param $context IContextSource context to use or null for the main context.
* @return String: HTML fragment
*/
- public static function buildRollbackLink( $rev ) {
- global $wgRequest, $wgUser;
+ public static function buildRollbackLink( $rev, IContextSource $context = null ) {
+ global $wgShowRollbackEditCount, $wgMiserMode;
+
+ // To config which pages are effected by miser mode
+ $disableRollbackEditCountSpecialPage = array( 'Recentchanges', 'Watchlist' );
+
+ if ( $context === null ) {
+ $context = RequestContext::getMain();
+ }
+
$title = $rev->getTitle();
$query = array(
'action' => 'rollback',
'from' => $rev->getUserText(),
- 'token' => $wgUser->getEditToken( array( $title->getPrefixedText(), $rev->getUserText() ) ),
+ 'token' => $context->getUser()->getEditToken( array( $title->getPrefixedText(), $rev->getUserText() ) ),
);
- if ( $wgRequest->getBool( 'bot' ) ) {
+ if ( $context->getRequest()->getBool( 'bot' ) ) {
$query['bot'] = '1';
$query['hidediff'] = '1'; // bug 15999
}
- return self::link(
- $title,
- wfMsgHtml( 'rollbacklink' ),
- array( 'title' => wfMsg( 'tooltip-rollback' ) ),
- $query,
- array( 'known', 'noclasses' )
- );
+
+ $disableRollbackEditCount = false;
+ 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( $editCount > $wgShowRollbackEditCount ) {
+ $editCount_output = $context->msg( 'rollbacklinkcount-morethan' )->numParams( $wgShowRollbackEditCount )->parse();
+ } else {
+ $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
+ }
+
+ return self::link(
+ $title,
+ $editCount_output,
+ array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
+ $query,
+ array( 'known', 'noclasses' )
+ );
+ } else {
+ return self::link(
+ $title,
+ $context->msg( 'rollbacklink' )->escaped(),
+ array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
+ $query,
+ array( 'known', 'noclasses' )
+ );
+ }
}
/**
@@ -1647,35 +1822,38 @@ class Linker {
# Construct the HTML
$outText = '<div class="mw-templatesUsedExplanation">';
if ( $preview ) {
- $outText .= wfMsgExt( 'templatesusedpreview', array( 'parse' ), count( $templates ) );
+ $outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) )
+ ->parseAsBlock();
} elseif ( $section ) {
- $outText .= wfMsgExt( 'templatesusedsection', array( 'parse' ), count( $templates ) );
+ $outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) )
+ ->parseAsBlock();
} else {
- $outText .= wfMsgExt( 'templatesused', array( 'parse' ), count( $templates ) );
+ $outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) )
+ ->parseAsBlock();
}
$outText .= "</div><ul>\n";
- usort( $templates, array( 'Title', 'compare' ) );
+ usort( $templates, 'Title::compare' );
foreach ( $templates as $titleObj ) {
$r = $titleObj->getRestrictions( 'edit' );
if ( in_array( 'sysop', $r ) ) {
- $protected = wfMsgExt( 'template-protected', array( 'parseinline' ) );
+ $protected = wfMessage( 'template-protected' )->parse();
} elseif ( in_array( 'autoconfirmed', $r ) ) {
- $protected = wfMsgExt( 'template-semiprotected', array( 'parseinline' ) );
+ $protected = wfMessage( 'template-semiprotected' )->parse();
} else {
$protected = '';
}
if ( $titleObj->quickUserCan( 'edit' ) ) {
$editLink = self::link(
$titleObj,
- wfMsg( 'editlink' ),
+ wfMessage( 'editlink' )->text(),
array(),
array( 'action' => 'edit' )
);
} else {
$editLink = self::link(
$titleObj,
- wfMsg( 'viewsourcelink' ),
+ wfMessage( 'viewsourcelink' )->text(),
array(),
array( 'action' => 'edit' )
);
@@ -1696,14 +1874,13 @@ class Linker {
* @return String: HTML output
*/
public static function formatHiddenCategories( $hiddencats ) {
- global $wgLang;
wfProfileIn( __METHOD__ );
$outText = '';
if ( count( $hiddencats ) > 0 ) {
# Construct the HTML
$outText = '<div class="mw-hiddenCategoriesExplanation">';
- $outText .= wfMsgExt( 'hiddencategories', array( 'parse' ), $wgLang->formatnum( count( $hiddencats ) ) );
+ $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
$outText .= "</div><ul>\n";
foreach ( $hiddencats as $titleObj ) {
@@ -1719,7 +1896,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 Size to format
+ * @param $size int Size to format
* @return String
*/
public static function formatSize( $size ) {
@@ -1855,18 +2032,19 @@ class Linker {
* Creates a (show/hide) link for deleting revisions/log entries
*
* @param $query Array: query parameters to be passed to link()
- * @param $restricted Boolean: set to true to use a <strong> instead of a <span>
+ * @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)
*
- * @return String: HTML <a> link to Special:Revisiondelete, wrapped in a
+ * @return String: HTML "<a>" link to Special:Revisiondelete, wrapped in a
* span to allow for customization of appearance with CSS
*/
public static function revDeleteLink( $query = array(), $restricted = false, $delete = true ) {
$sp = SpecialPage::getTitleFor( 'Revisiondelete' );
- $html = $delete ? wfMsgHtml( 'rev-delundel' ) : wfMsgHtml( 'rev-showdeleted' );
+ $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
+ $html = wfMessage( $msgKey )->escaped();
$tag = $restricted ? 'strong' : 'span';
$link = self::link( $sp, $html, array(), $query, array( 'known', 'noclasses' ) );
- return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), "($link)" );
+ return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), wfMessage( 'parentheses' )->rawParams( $link )->escaped() );
}
/**
@@ -1878,8 +2056,10 @@ class Linker {
* of appearance with CSS
*/
public static function revDeleteLinkDisabled( $delete = true ) {
- $html = $delete ? wfMsgHtml( 'rev-delundel' ) : wfMsgHtml( 'rev-showdeleted' );
- return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), "($html)" );
+ $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
+ $html = wfMessage( $msgKey )->escaped();
+ $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
+ return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), $htmlParentheses );
}
/* Deprecated methods */
@@ -1896,6 +2076,7 @@ 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.
+ * @return string
*/
static function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) {
wfDeprecated( __METHOD__, '1.16' );
@@ -1924,6 +2105,7 @@ class Linker {
* 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.
+ * @return string
*/
static function makeLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
# wfDeprecated( __METHOD__, '1.16' ); // See r105985 and it's revert. Somewhere still used.
@@ -1955,7 +2137,7 @@ class Linker {
* @param $prefix String: text before link text
* @param $aprops String: extra attributes to the a-element
* @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead
- * @return the a-element
+ * @return string the a-element
*/
static function makeKnownLinkObj(
$title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = ''
@@ -1993,6 +2175,7 @@ class Linker {
* be included in the link text. Other characters will be appended after
* the end of the link.
* @param $prefix String: Optional prefix
+ * @return string
*/
static function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
wfDeprecated( __METHOD__, '1.16' );
@@ -2024,6 +2207,7 @@ class Linker {
* be included in the link text. Other characters will be appended after
* the end of the link.
* @param $prefix String: Optional prefix
+ * @return string
*/
static function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) {
wfDeprecated( __METHOD__, '1.16' );
@@ -2038,6 +2222,7 @@ class Linker {
/**
* Returns the attributes for the tooltip and access key.
+ * @return array
*/
public static function tooltipAndAccesskeyAttribs( $name ) {
# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
@@ -2058,6 +2243,7 @@ class Linker {
/**
* Returns raw bits of HTML, use titleAttrib()
+ * @return null|string
*/
public static function tooltip( $name, $options = null ) {
# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
@@ -2084,6 +2270,7 @@ class DummyLinker {
*
* @param $fname String Name of called method
* @param $args Array Arguments to the method
+ * @return mixed
*/
public function __call( $fname, $args ) {
return call_user_func_array( array( 'Linker', $fname ), $args );
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index 27d1dfd2..87db4d60 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -1,6 +1,6 @@
<?php
/**
- * See docs/deferred.txt
+ * Updater for link tracking tables after a page edit.
*
* 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
@@ -17,14 +17,19 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
+ * @file
+ */
+
+/**
+ * See docs/deferred.txt
+ *
* @todo document (e.g. one-sentence top-level class description).
*/
-class LinksUpdate {
+class LinksUpdate extends SqlDataUpdate {
- /**@{{
- * @private
- */
- var $mId, //!< Page ID of the article linked from
+ // @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
$mParserOutput, //!< Parser output
$mLinks, //!< Map of title strings to IDs for the links in the document
@@ -37,7 +42,6 @@ class LinksUpdate {
$mDb, //!< Database connection reference
$mOptions, //!< SELECT options to be used (array)
$mRecursive; //!< Whether to queue jobs for recursive updates
- /**@}}*/
/**
* Constructor
@@ -47,22 +51,25 @@ class LinksUpdate {
* @param $recursive Boolean: queue jobs for recursive updates?
*/
function __construct( $title, $parserOutput, $recursive = true ) {
- global $wgAntiLockFlags;
-
- if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
- $this->mOptions = array();
- } else {
- $this->mOptions = array( 'FOR UPDATE' );
- }
- $this->mDb = wfGetDB( DB_MASTER );
+ parent::__construct( false ); // no implicit transaction
- if ( !is_object( $title ) ) {
+ if ( !( $title instanceof Title ) ) {
throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
"Please see Article::editUpdates() for an invocation example.\n" );
}
+
+ if ( !( $parserOutput instanceof ParserOutput ) ) {
+ throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
+ "Please see WikiPage::doEditUpdates() for an invocation example.\n" );
+ }
+
$this->mTitle = $title;
$this->mId = $title->getArticleID();
+ if ( !$this->mId ) {
+ throw new MWException( "The Title object did not provide an article ID. Perhaps the page doesn't exist?" );
+ }
+
$this->mParserOutput = $parserOutput;
$this->mLinks = $parserOutput->getLinks();
$this->mImages = $parserOutput->getImages();
@@ -254,51 +261,6 @@ class LinksUpdate {
}
/**
- * Invalidate the cache of a list of pages from a single namespace
- *
- * @param $namespace Integer
- * @param $dbkeys Array
- */
- function invalidatePages( $namespace, $dbkeys ) {
- if ( !count( $dbkeys ) ) {
- return;
- }
-
- /**
- * Determine which pages need to be updated
- * This is necessary to prevent the job queue from smashing the DB with
- * large numbers of concurrent invalidations of the same page
- */
- $now = $this->mDb->timestamp();
- $ids = array();
- $res = $this->mDb->select( 'page', array( 'page_id' ),
- array(
- 'page_namespace' => $namespace,
- 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
- 'page_touched < ' . $this->mDb->addQuotes( $now )
- ), __METHOD__
- );
- foreach ( $res as $row ) {
- $ids[] = $row->page_id;
- }
- if ( !count( $ids ) ) {
- return;
- }
-
- /**
- * Do the update
- * We still need the page_touched condition, in case the row has changed since
- * the non-locking select above.
- */
- $this->mDb->update( 'page', array( 'page_touched' => $now ),
- array(
- 'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
- 'page_touched < ' . $this->mDb->addQuotes( $now )
- ), __METHOD__
- );
- }
-
- /**
* @param $cats
*/
function invalidateCategories( $cats ) {
@@ -849,3 +811,72 @@ class LinksUpdate {
}
}
}
+
+/**
+ * Update object handling the cleanup of links tables after a page was deleted.
+ **/
+class LinksDeletionUpdate extends SqlDataUpdate {
+
+ protected $mPage; //!< WikiPage the wikipage that was deleted
+
+ /**
+ * Constructor
+ *
+ * @param $page WikiPage Page we are updating
+ */
+ function __construct( WikiPage $page ) {
+ parent::__construct( false ); // no implicit transaction
+
+ $this->mPage = $page;
+ }
+
+ /**
+ * Do some database updates after deletion
+ */
+ public function doUpdate() {
+ $title = $this->mPage->getTitle();
+ $id = $this->mPage->getId();
+
+ # Delete restrictions for it
+ $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;
+ }
+
+ $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__ );
+ $this->mDb->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
+ $this->mDb->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
+ $this->mDb->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
+ $this->mDb->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
+ $this->mDb->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
+ $this->mDb->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
+ $this->mDb->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
+ }
+
+ # If using cleanup triggers, we can skip some manual deletes
+ if ( !$this->mDb->cleanupTriggers() ) {
+ # Clean up recentchanges entries...
+ $this->mDb->delete( 'recentchanges',
+ array( 'rc_type != ' . RC_LOG,
+ 'rc_namespace' => $title->getNamespace(),
+ 'rc_title' => $title->getDBkey() ),
+ __METHOD__ );
+ $this->mDb->delete( 'recentchanges',
+ array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
+ __METHOD__ );
+ }
+ }
+}
diff --git a/includes/LocalisationCache.php b/includes/LocalisationCache.php
index 3b1f45cc..d8e5d3a3 100644
--- a/includes/LocalisationCache.php
+++ b/includes/LocalisationCache.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Cache of the contents of localisation files.
+ *
+ * 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
+ */
define( 'MW_LC_VERSION', 2 );
@@ -90,7 +110,7 @@ class LocalisationCache {
'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
- 'digitGroupingPattern'
+ 'digitGroupingPattern', 'pluralRules', 'compiledPluralRules',
);
/**
@@ -98,7 +118,7 @@ class LocalisationCache {
* by a fallback sequence.
*/
static public $mergeableMapKeys = array( 'messages', 'namespaceNames',
- 'dateFormats', 'imageFiles', 'preloadedMessages',
+ 'dateFormats', 'imageFiles', 'preloadedMessages'
);
/**
@@ -134,6 +154,12 @@ class LocalisationCache {
*/
static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
+ /**
+ * Associative array of cached plural rules. The key is the language code,
+ * the value is an array of plural rules for that language.
+ */
+ var $pluralRules = null;
+
var $mergeableKeys = null;
/**
@@ -214,9 +240,9 @@ class LocalisationCache {
*/
public function getItem( $code, $key ) {
if ( !isset( $this->loadedItems[$code][$key] ) ) {
- wfProfileIn( __METHOD__.'-load' );
+ wfProfileIn( __METHOD__ . '-load' );
$this->loadItem( $code, $key );
- wfProfileOut( __METHOD__.'-load' );
+ wfProfileOut( __METHOD__ . '-load' );
}
if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
@@ -236,9 +262,9 @@ class LocalisationCache {
public function getSubitem( $code, $key, $subkey ) {
if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
!isset( $this->loadedItems[$code][$key] ) ) {
- wfProfileIn( __METHOD__.'-load' );
+ wfProfileIn( __METHOD__ . '-load' );
$this->loadSubitem( $code, $key, $subkey );
- wfProfileOut( __METHOD__.'-load' );
+ wfProfileOut( __METHOD__ . '-load' );
}
if ( isset( $this->data[$code][$key][$subkey] ) ) {
@@ -343,10 +369,11 @@ class LocalisationCache {
/**
* Returns true if the cache identified by $code is missing or expired.
+ * @return bool
*/
public function isExpired( $code ) {
if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
- wfDebug( __METHOD__."($code): forced reload\n" );
+ wfDebug( __METHOD__ . "($code): forced reload\n" );
return true;
}
@@ -355,7 +382,7 @@ class LocalisationCache {
$preload = $this->store->get( $code, 'preload' );
// Different keys may expire separately, at least in LCStore_Accel
if ( $deps === null || $keys === null || $preload === null ) {
- wfDebug( __METHOD__."($code): cache missing, need to make one\n" );
+ wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
return true;
}
@@ -365,7 +392,7 @@ class LocalisationCache {
// anymore (e.g. uninstalled extensions)
// When this happens, always expire the cache
if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
- wfDebug( __METHOD__."($code): cache for $code expired due to " .
+ wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
get_class( $dep ) . "\n" );
return true;
}
@@ -460,9 +487,95 @@ class LocalisationCache {
} elseif ( $_fileType == 'aliases' ) {
$data = compact( 'aliases' );
} else {
- throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
+ throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
}
+ return $data;
+ }
+
+ /**
+ * Get the compiled plural rules for a given language from the XML files.
+ * @since 1.20
+ */
+ public function getCompiledPluralRules( $code ) {
+ $rules = $this->getPluralRules( $code );
+ if ( $rules === null ) {
+ return null;
+ }
+ try {
+ $compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
+ } catch( CLDRPluralRuleError $e ) {
+ wfDebugLog( 'l10n', $e->getMessage() . "\n" );
+ return array();
+ }
+ return $compiledRules;
+ }
+
+ /**
+ * Get the plural rules for a given language from the XML files.
+ * Cached.
+ * @since 1.20
+ */
+ 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 );
+ }
+ }
+ if ( !isset( $this->pluralRules[$code] ) ) {
+ return null;
+ } else {
+ return $this->pluralRules[$code];
+ }
+ }
+
+ /**
+ * Load a plural XML file with the given filename, compile the relevant
+ * rules, and save the compiled rules in a process-local cache.
+ */
+ protected function loadPluralFile( $fileName ) {
+ $doc = new DOMDocument;
+ $doc->load( $fileName );
+ $rulesets = $doc->getElementsByTagName( "pluralRules" );
+ foreach ( $rulesets as $ruleset ) {
+ $codes = $ruleset->getAttribute( 'locales' );
+ $rules = array();
+ $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
+ foreach ( $ruleElements as $elt ) {
+ $rules[] = $elt->nodeValue;
+ }
+ foreach ( explode( ' ', $codes ) as $code ) {
+ $this->pluralRules[$code] = $rules;
+ }
+ }
+ }
+
+ /**
+ * Read the data from the source files for a given language, and register
+ * the relevant dependencies in the $deps array. If the localisation
+ * exists, the data array is returned, otherwise false is returned.
+ */
+ protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
+ $fileName = Language::getMessagesFileName( $code );
+ if ( !file_exists( $fileName ) ) {
+ return false;
+ }
+
+ $deps[] = new FileDependency( $fileName );
+ $data = $this->readPHPFile( $fileName, 'core' );
+
+ # Load CLDR plural rules for JavaScript
+ $data['pluralRules'] = $this->getPluralRules( $code );
+ # And for PHP
+ $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
+
+ $deps['plurals'] = new FileDependency( __DIR__ . "/../languages/data/plurals.xml" );
+ $deps['plurals-mw'] = new FileDependency( __DIR__ . "/../languages/data/plurals-mediawiki.xml" );
return $data;
}
@@ -564,14 +677,12 @@ class LocalisationCache {
$deps = array();
# Load the primary localisation from the source file
- $fileName = Language::getMessagesFileName( $code );
- if ( !file_exists( $fileName ) ) {
- wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" );
+ $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
+ if ( $data === false ) {
+ wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
$coreData['fallback'] = 'en';
} else {
- $deps[] = new FileDependency( $fileName );
- $data = $this->readPHPFile( $fileName, 'core' );
- wfDebug( __METHOD__.": got localisation for $code from source\n" );
+ wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
# Merge primary localisation
foreach ( $data as $key => $value ) {
@@ -584,7 +695,6 @@ class LocalisationCache {
if ( is_null( $coreData['fallback'] ) ) {
$coreData['fallback'] = $code === 'en' ? false : 'en';
}
-
if ( $coreData['fallback'] === false ) {
$coreData['fallbackSequence'] = array();
} else {
@@ -600,15 +710,11 @@ class LocalisationCache {
foreach ( $coreData['fallbackSequence'] as $fbCode ) {
# Load the secondary localisation from the source file to
# avoid infinite cycles on cyclic fallbacks
- $fbFilename = Language::getMessagesFileName( $fbCode );
-
- if ( !file_exists( $fbFilename ) ) {
+ $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps );
+ if ( $fbData === false ) {
continue;
}
- $deps[] = new FileDependency( $fbFilename );
- $fbData = $this->readPHPFile( $fbFilename, 'core' );
-
foreach ( self::$allKeys as $key ) {
if ( !isset( $fbData[$key] ) ) {
continue;
@@ -633,7 +739,7 @@ class LocalisationCache {
$used = false;
foreach ( $data as $key => $item ) {
- if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
+ if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
$used = true;
}
}
@@ -663,19 +769,26 @@ class LocalisationCache {
$page = str_replace( ' ', '_', $page );
}
# Decouple the reference to prevent accidental damage
- unset($page);
+ unset( $page );
+
+ # If there were no plural rules, return an empty array
+ if ( $allData['pluralRules'] === null ) {
+ $allData['pluralRules'] = array();
+ }
+ if ( $allData['compiledPluralRules'] === null ) {
+ $allData['compiledPluralRules'] = array();
+ }
# Set the list keys
$allData['list'] = array();
foreach ( self::$splitKeys as $key ) {
$allData['list'][$key] = array_keys( $allData[$key] );
}
-
# Run hooks
wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
if ( is_null( $allData['namespaceNames'] ) ) {
- throw new MWException( __METHOD__.': Localisation data failed sanity check! ' .
+ throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
'Check that your languages/messages/MessagesEn.php file is intact.' );
}
@@ -793,8 +906,8 @@ class LocalisationCache {
interface LCStore {
/**
* Get a value.
- * @param $code Language code
- * @param $key Cache key
+ * @param $code string Language code
+ * @param $key string Cache key
*/
function get( $code, $key );
@@ -903,17 +1016,17 @@ class LCStore_DB implements LCStore {
}
if ( !$code ) {
- throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+ throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
}
$this->dbw = wfGetDB( DB_MASTER );
try {
- $this->dbw->begin();
+ $this->dbw->begin( __METHOD__ );
$this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
} catch ( DBQueryError $e ) {
if ( $this->dbw->wasReadOnlyError() ) {
$this->readOnly = true;
- $this->dbw->rollback();
+ $this->dbw->rollback( __METHOD__ );
$this->dbw->ignoreErrors( false );
return;
} else {
@@ -934,7 +1047,7 @@ class LCStore_DB implements LCStore {
$this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
}
- $this->dbw->commit();
+ $this->dbw->commit( __METHOD__ );
$this->currentLang = null;
$this->dbw = null;
$this->batch = array();
@@ -947,7 +1060,7 @@ class LCStore_DB implements LCStore {
}
if ( is_null( $this->currentLang ) ) {
- throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+ throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
}
$this->batch[] = array(
@@ -1019,7 +1132,7 @@ class LCStore_CDB implements LCStore {
}
// Close reader to stop permission errors on write
- if( !empty($this->readers[$code]) ) {
+ if ( !empty( $this->readers[$code] ) ) {
$this->readers[$code]->close();
}
@@ -1037,14 +1150,14 @@ class LCStore_CDB implements LCStore {
public function set( $key, $value ) {
if ( is_null( $this->writer ) ) {
- throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+ throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
}
$this->writer->set( $key, serialize( $value ) );
}
protected function getFileName( $code ) {
if ( !$code || strpos( $code, '/' ) !== false ) {
- throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+ throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
}
return "{$this->directory}/l10n_cache-$code.cdb";
}
@@ -1160,8 +1273,9 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
reset( $this->mruLangs );
$code = key( $this->mruLangs );
- wfDebug( __METHOD__.": unloading $code\n" );
+ wfDebug( __METHOD__ . ": unloading $code\n" );
$this->unload( $code );
}
}
-} \ No newline at end of file
+
+}
diff --git a/includes/MWFunction.php b/includes/MWFunction.php
index 0113f917..36fcc30b 100644
--- a/includes/MWFunction.php
+++ b/includes/MWFunction.php
@@ -1,6 +1,7 @@
<?php
-
/**
+ * Helper methods to call functions and instance 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
@@ -16,6 +17,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
+ * @file
*/
class MWFunction {
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
index 1ba46701..42791f57 100644
--- a/includes/MagicWord.php
+++ b/includes/MagicWord.php
@@ -1,15 +1,30 @@
<?php
/**
- * File for magic words
+ * File for magic words.
*
- * See docs/magicword.txt
+ * See docs/magicword.txt.
+ *
+ * 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
*/
/**
- * This class encapsulates "magic words" such as #redirect, __NOTOC__, etc.
+ * This class encapsulates "magic words" such as "#redirect", __NOTOC__, etc.
*
* @par Usage:
* @code
@@ -27,7 +42,7 @@
*
* To add magic words in an extension, use $magicWords in a file listed in
* $wgExtensionMessagesFiles[].
- *
+ *
* @par Example:
* @code
* $magicWords = array();
@@ -84,6 +99,7 @@ class MagicWord {
'numberoffiles',
'numberofedits',
'articlepath',
+ 'pageid',
'sitename',
'server',
'servername',
@@ -95,6 +111,7 @@ class MagicWord {
'fullpagenamee',
'namespace',
'namespacee',
+ 'namespacenumber',
'currentweek',
'currentdow',
'localweek',
@@ -282,6 +299,7 @@ class MagicWord {
* Initialises this object with an ID
*
* @param $id
+ * @throws MWException
*/
function load( $id ) {
global $wgContLang;
@@ -290,8 +308,8 @@ class MagicWord {
$wgContLang->getMagic( $this );
if ( !$this->mSynonyms ) {
$this->mSynonyms = array( 'dkjsagfjsgashfajsh' );
- #throw new MWException( "Error: invalid magic word '$id'" );
- wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" );
+ throw new MWException( "Error: invalid magic word '$id'" );
+ #wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" );
}
wfProfileOut( __METHOD__ );
}
@@ -628,6 +646,9 @@ class MagicWordArray {
var $baseRegex, $regex;
var $matches;
+ /**
+ * @param $names array
+ */
function __construct( $names = array() ) {
$this->names = $names;
}
@@ -756,12 +777,21 @@ class MagicWordArray {
}
/**
+ * @since 1.20
+ * @return array
+ */
+ public function getNames() {
+ return $this->names;
+ }
+
+ /**
* Parse a match array from preg_match
* Returns array(magic word ID, parameter value)
* If there is no parameter value, that element will be false.
*
* @param $m array
*
+ * @throws MWException
* @return array
*/
function parseMatch( $m ) {
@@ -798,7 +828,7 @@ class MagicWordArray {
$regexes = $this->getVariableStartToEndRegex();
foreach ( $regexes as $regex ) {
if ( $regex !== '' ) {
- $m = false;
+ $m = array();
if ( preg_match( $regex, $text, $m ) ) {
return $this->parseMatch( $m );
}
@@ -813,7 +843,7 @@ class MagicWordArray {
*
* @param $text string
*
- * @return string|false
+ * @return string|bool False on failure
*/
public function matchStartToEnd( $text ) {
$hash = $this->getHash();
@@ -861,7 +891,7 @@ class MagicWordArray {
*
* @param $text string
*
- * @return int|false
+ * @return int|bool False on failure
*/
public function matchStartAndRemove( &$text ) {
$regexes = $this->getRegexStart();
diff --git a/includes/Message.php b/includes/Message.php
index 10f9d3e1..b0147b9a 100644
--- a/includes/Message.php
+++ b/includes/Message.php
@@ -1,12 +1,34 @@
<?php
/**
+ * Fetching and processing of interface messages.
+ *
+ * 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
+ */
+
+/**
* The Message class provides methods which fullfil 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
* replace the old wfMsg* functions that over time grew unusable.
- * @see https://www.mediawiki.org/wiki/New_messages_API for equivalences
+ * @see https://www.mediawiki.org/wiki/Manual:Messages_API for equivalences
* between old and new functions.
*
* You should use the wfMessage() global function which acts as a wrapper for
@@ -90,8 +112,7 @@
* ->plain();
* @endcode
*
- * @note You cannot parse the text except in the content or interface
- * @note languages
+ * @note You can parse the text only in the content or interface languages
*
* @section message_compare_old Comparison with old wfMsg* functions:
*
@@ -134,7 +155,6 @@
* @see https://www.mediawiki.org/wiki/Localisation
*
* @since 1.17
- * @author Niklas Laxström
*/
class Message {
/**
@@ -189,6 +209,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
* @return Message: $this
@@ -204,6 +225,7 @@ class Message {
* Factory function that is just wrapper for the real constructor. It is
* intented 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 Varargs: parameters as Strings
* @return Message: $this
@@ -218,6 +240,7 @@ class Message {
* Factory function accepting multiple message keys and returning a message instance
* for the first message which is non-empty. If all messages are empty then an
* instance of the first message key is returned.
+ * @since 1.18
* @param Varargs: message keys (or first arg as an array of all the message keys)
* @return Message: $this
*/
@@ -237,6 +260,7 @@ class Message {
/**
* Adds parameters to the parameter list of this message.
+ * @since 1.17
* @param Varargs: parameters as Strings, or a single argument that is an array of Strings
* @return Message: $this
*/
@@ -255,6 +279,7 @@ class Message {
* In other words the parsing process cannot access the contents
* of this type of parameter, and you need to make sure it is
* sanitized beforehand. The parser will see "$n", instead.
+ * @since 1.17
* @param Varargs: raw parameters as Strings (or single argument that is an array of raw parameters)
* @return Message: $this
*/
@@ -272,6 +297,7 @@ class Message {
/**
* Add parameters that are numeric and will be passed through
* Language::formatNum before substitution
+ * @since 1.18
* @param Varargs: numeric parameters (or single argument that is array of numeric parameters)
* @return Message: $this
*/
@@ -288,13 +314,14 @@ class Message {
/**
* Set the language and the title from a context object
- *
+ * @since 1.19
* @param $context IContextSource
* @return Message: $this
*/
public function setContext( IContextSource $context ) {
$this->inLanguage( $context->getLanguage() );
$this->title( $context->getTitle() );
+ $this->interface = true;
return $this;
}
@@ -303,6 +330,7 @@ class Message {
* Request the message in any language that is supported.
* As a side effect interface message status is unconditionally
* turned off.
+ * @since 1.17
* @param $lang Mixed: language code or Language object.
* @return Message: $this
*/
@@ -326,6 +354,7 @@ class Message {
/**
* Request the message in the wiki's content language,
* unless it is disabled for this message.
+ * @since 1.17
* @see $wgForceUIMsgAsContentMsg
* @return Message: $this
*/
@@ -342,7 +371,20 @@ class Message {
}
/**
+ * Allows manipulating the interface message flag directly.
+ * Can be used to restore the flag after setting a language.
+ * @param $value bool
+ * @return Message: $this
+ * @since 1.20
+ */
+ public function setInterfaceMessageFlag( $value ) {
+ $this->interface = (bool) $value;
+ return $this;
+ }
+
+ /**
* Enable or disable database use.
+ * @since 1.17
* @param $value Boolean
* @return Message: $this
*/
@@ -353,7 +395,7 @@ class Message {
/**
* Set the Title object to use as context when transforming the message
- *
+ * @since 1.18
* @param $title Title object
* @return Message: $this
*/
@@ -364,10 +406,19 @@ class Message {
/**
* Returns the message parsed from wikitext to HTML.
+ * @since 1.17
* @return String: HTML
*/
public function toString() {
- $string = $this->getMessageText();
+ $string = $this->fetchMessage();
+
+ if ( $string === false ) {
+ $key = htmlspecialchars( is_array( $this->key ) ? $this->key[0] : $this->key );
+ if ( $this->format === 'plain' ) {
+ return '<' . $key . '>';
+ }
+ return '&lt;' . $key . '&gt;';
+ }
# Replace parameters before text parsing
$string = $this->replaceParameters( $string, 'before' );
@@ -398,6 +449,7 @@ class Message {
* Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg:
* $foo = Message::get($key);
* $string = "<abbr>$foo</abbr>";
+ * @since 1.18
* @return String
*/
public function __toString() {
@@ -406,6 +458,7 @@ class Message {
/**
* Fully parse the text from wikitext to HTML
+ * @since 1.17
* @return String parsed HTML
*/
public function parse() {
@@ -415,6 +468,7 @@ class Message {
/**
* Returns the message text. {{-transformation is done.
+ * @since 1.17
* @return String: Unescaped message text.
*/
public function text() {
@@ -424,6 +478,7 @@ class Message {
/**
* Returns the message text as-is, only parameters are subsituted.
+ * @since 1.17
* @return String: Unescaped untransformed message text.
*/
public function plain() {
@@ -433,6 +488,7 @@ class Message {
/**
* Returns the parsed message text which is always surrounded by a block element.
+ * @since 1.17
* @return String: HTML
*/
public function parseAsBlock() {
@@ -443,6 +499,7 @@ class Message {
/**
* Returns the message text. {{-transformation is done and the result
* is escaped excluding any raw parameters.
+ * @since 1.17
* @return String: Escaped message text.
*/
public function escaped() {
@@ -452,6 +509,7 @@ class Message {
/**
* Check whether a message key has been defined currently.
+ * @since 1.17
* @return Bool: true if it is and false if not.
*/
public function exists() {
@@ -460,6 +518,7 @@ class Message {
/**
* Check whether a message does not exist, or is an empty string
+ * @since 1.18
* @return Bool: true if is is and false if not
* @todo FIXME: Merge with isDisabled()?
*/
@@ -470,6 +529,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
*/
public function isDisabled() {
@@ -478,6 +538,7 @@ class Message {
}
/**
+ * @since 1.17
* @param $value
* @return array
*/
@@ -486,6 +547,7 @@ class Message {
}
/**
+ * @since 1.18
* @param $value
* @return array
*/
@@ -495,6 +557,7 @@ class Message {
/**
* Substitutes any paramaters into the message text.
+ * @since 1.17
* @param $message String: the message text
* @param $type String: either before or after
* @return String
@@ -513,6 +576,7 @@ 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.
* @return Tuple(type, value)
*/
@@ -536,6 +600,7 @@ class Message {
/**
* Wrapper for what ever method we use to parse wikitext.
+ * @since 1.17
* @param $string String: Wikitext message contents
* @return string Wikitext parsed into HTML
*/
@@ -545,6 +610,7 @@ class Message {
/**
* Wrapper for what ever method we use to {{-transform wikitext.
+ * @since 1.17
* @param $string String: Wikitext message contents
* @return string Wikitext with {{-constructs replaced with their values.
*/
@@ -553,21 +619,8 @@ class Message {
}
/**
- * Returns the textual value for the message.
- * @return Message contents or placeholder
- */
- protected function getMessageText() {
- $message = $this->fetchMessage();
- if ( $message === false ) {
- return '&lt;' . htmlspecialchars( is_array($this->key) ? $this->key[0] : $this->key ) . '&gt;';
- } else {
- return $message;
- }
- }
-
- /**
* Wrapper for what ever method we use to get message contents
- *
+ * @since 1.17
* @return string
*/
protected function fetchMessage() {
diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php
index be6b27c9..34014e1b 100644
--- a/includes/MessageBlobStore.php
+++ b/includes/MessageBlobStore.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource message blobs storage used by the resource loader.
+ *
* 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
@@ -15,6 +17,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
+ * @file
* @author Roan Kattouw
* @author Trevor Parscal
*/
@@ -296,7 +299,7 @@ class MessageBlobStore {
*/
private static function reencodeBlob( $blob, $key, $lang ) {
$decoded = FormatJson::decode( $blob, true );
- $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
+ $decoded[$key] = wfMessage( $key )->inLanguage( $lang )->plain();
return FormatJson::encode( (object)$decoded );
}
@@ -350,7 +353,7 @@ class MessageBlobStore {
$messages = array();
foreach ( $module->getMessages() as $key ) {
- $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
+ $messages[$key] = wfMessage( $key )->inLanguage( $lang )->plain();
}
return FormatJson::encode( (object)$messages );
diff --git a/includes/Metadata.php b/includes/Metadata.php
index e5e3296b..0ca15393 100644
--- a/includes/Metadata.php
+++ b/includes/Metadata.php
@@ -1,21 +1,23 @@
<?php
/**
+ * Base code to format metadata.
*
* Copyright 2004, Evan Prodromou <evan@wikitravel.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 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.
+ * 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
+ * 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
*
* @author Evan Prodromou <evan@wikitravel.org>
* @file
@@ -58,7 +60,7 @@ abstract class RdfMetaData {
global $wgLanguageCode, $wgSitename;
$this->element( 'title', $this->mArticle->getTitle()->getText() );
- $this->pageOrString( 'publisher', wfMsg( 'aboutpage' ), $wgSitename );
+ $this->pageOrString( 'publisher', wfMessage( 'aboutpage' )->text(), $wgSitename );
$this->element( 'language', $wgLanguageCode );
$this->element( 'type', 'Text' );
$this->element( 'format', 'text/html' );
@@ -115,14 +117,18 @@ abstract class RdfMetaData {
protected function person( $name, User $user ) {
if( $user->isAnon() ){
- $this->element( $name, wfMsgExt( 'anonymous', array( 'parsemag' ), 1 ) );
+ $this->element( $name, wfMessage( 'anonymous' )->numParams( 1 )->text() );
} else {
$real = $user->getRealName();
if( $real ) {
$this->element( $name, $real );
} else {
$userName = $user->getName();
- $this->pageOrString( $name, $user->getUserPage(), wfMsgExt( 'siteuser', 'parsemag', $userName, $userName ) );
+ $this->pageOrString(
+ $name,
+ $user->getUserPage(),
+ wfMessage( 'siteuser', $userName, $userName )->text()
+ );
}
}
}
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
index f86b6051..1873e7bf 100644
--- a/includes/MimeMagic.php
+++ b/includes/MimeMagic.php
@@ -2,6 +2,21 @@
/**
* Module defining helper functions for detecting and dealing with mime types.
*
+ * 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
*/
@@ -123,7 +138,7 @@ END_STRING
* Implements functions related to mime types such as detection and mapping to
* file extension.
*
- * Instances of this class are stateles, there only needs to be one global instance
+ * Instances of this class are stateless, there only needs to be one global instance
* of MimeMagic. Please use MimeMagic::singleton() to get that instance.
*/
class MimeMagic {
@@ -215,8 +230,6 @@ class MimeMagic {
continue;
}
- #print "processing MIME line $s<br>";
-
$mime = substr( $s, 0, $i );
$ext = trim( substr($s, $i+1 ) );
@@ -566,6 +579,7 @@ class MimeMagic {
*
* @param string $file
* @param mixed $ext
+ * @return bool|string
*/
private function doGuessMimeType( $file, $ext ) { // TODO: remove $ext param
// Read a chunk of the file
@@ -1030,6 +1044,7 @@ class MimeMagic {
*
* This funktion relies on the mapping defined by $this->mMediaTypes
* @access private
+ * @return int|string
*/
function findMediaType( $extMime ) {
if ( strpos( $extMime, '.' ) === 0 ) {
@@ -1067,6 +1082,7 @@ class MimeMagic {
* @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
+ * @return Array
*/
public function getIEMimeTypes( $fileName, $chunk, $proposed ) {
$ca = $this->getIEContentAnalyzer();
diff --git a/includes/Namespace.php b/includes/Namespace.php
index 292559d0..2e2b8d61 100644
--- a/includes/Namespace.php
+++ b/includes/Namespace.php
@@ -1,6 +1,22 @@
<?php
/**
- * Provide things related to namespaces
+ * Provide things related to namespaces.
+ *
+ * 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
*/
@@ -14,7 +30,6 @@
* Users and translators should not change them
*
*/
-
class MWNamespace {
/**
@@ -33,7 +48,7 @@ class MWNamespace {
* @param $index
* @param $method
*
- * @return true
+ * @return bool
*/
private static function isMethodValidFor( $index, $method ) {
if ( $index < NS_MAIN ) {
@@ -50,7 +65,15 @@ class MWNamespace {
*/
public static function isMovable( $index ) {
global $wgAllowImageMoving;
- return !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) || $index == NS_CATEGORY );
+
+ $result = !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) || $index == NS_CATEGORY );
+
+ /**
+ * @since 1.20
+ */
+ wfRunHooks( 'NamespaceIsMovable', array( $index, &$result ) );
+
+ return $result;
}
/**
@@ -67,6 +90,7 @@ class MWNamespace {
/**
* @see self::isSubject
* @deprecated Please use the more consistently named isSubject (since 1.19)
+ * @return bool
*/
public static function isMain( $index ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -185,7 +209,7 @@ class MWNamespace {
* Returns array of all defined namespaces with their canonical
* (English) names.
*
- * @return \array
+ * @return array
* @since 1.17
*/
public static function getCanonicalNamespaces() {
@@ -315,6 +339,33 @@ class MWNamespace {
return $wgContentNamespaces;
}
}
+
+ /**
+ * List all namespace indices which are considered subject, aka not a talk
+ * or special namespace. See also MWNamespace::isSubject
+ *
+ * @return array of namespace indices
+ */
+ public static function getSubjectNamespaces() {
+ return array_filter(
+ MWNamespace::getValidNamespaces(),
+ 'MWNamespace::isSubject'
+ );
+ }
+
+ /**
+ * List all namespace indices which are considered talks, aka not a subject
+ * or special namespace. See also MWNamespace::isTalk
+ *
+ * @return array of namespace indices
+ */
+ public static function getTalkNamespaces() {
+ return array_filter(
+ MWNamespace::getValidNamespaces(),
+ 'MWNamespace::isTalk'
+ );
+ }
+
/**
* Is the namespace first-letter capitalized?
*
@@ -353,4 +404,16 @@ class MWNamespace {
return $index == NS_USER || $index == NS_USER_TALK;
}
+ /**
+ * It is not possible to use pages from this namespace as template?
+ *
+ * @since 1.20
+ * @param $index int Index to check
+ * @return bool
+ */
+ public static function isNonincludable( $index ) {
+ global $wgNonincludableNamespaces;
+ return $wgNonincludableNamespaces && in_array( $index, $wgNonincludableNamespaces );
+ }
+
}
diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php
index 4112f8a2..46a43f63 100644
--- a/includes/OutputHandler.php
+++ b/includes/OutputHandler.php
@@ -1,6 +1,21 @@
<?php
/**
- * Functions to be used with PHP's output buffer
+ * Functions to be used with PHP's output buffer.
+ *
+ * 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
*/
@@ -76,7 +91,12 @@ function wfRequestExtension() {
* @return string
*/
function wfGzipHandler( $s ) {
- if( !function_exists( 'gzencode' ) || headers_sent() ) {
+ if( !function_exists( 'gzencode' ) ) {
+ wfDebug( __FUNCTION__ . "() skipping compression (gzencode unavaible)\n" );
+ return $s;
+ }
+ if( headers_sent() ) {
+ wfDebug( __FUNCTION__ . "() skipping compression (headers already sent)\n" );
return $s;
}
@@ -90,6 +110,7 @@ function wfGzipHandler( $s ) {
}
if( wfClientAcceptsGzip() ) {
+ wfDebug( __FUNCTION__ . "() is compressing output\n" );
header( 'Content-Encoding: gzip' );
$s = gzencode( $s, 6 );
}
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index a91d5465..b4a81bb1 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -1,7 +1,24 @@
<?php
-if ( !defined( 'MEDIAWIKI' ) ) {
- die( 1 );
-}
+/**
+ * Preparation for the final page rendering.
+ *
+ * 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 should be covered by a general architecture document which does
@@ -19,10 +36,10 @@ if ( !defined( 'MEDIAWIKI' ) ) {
* @todo document
*/
class OutputPage extends ContextSource {
- /// Should be private. Used with addMeta() which adds <meta>
+ /// Should be private. Used with addMeta() which adds "<meta>"
var $mMetatags = array();
- /// <meta keyworkds="stuff"> most of the time the first 10 links to an article
+ /// "<meta keywords='stuff'>" most of the time the first 10 links to an article
var $mKeywords = array();
var $mLinktags = array();
@@ -33,17 +50,17 @@ class OutputPage extends ContextSource {
/// Should be private - has getter and setter. Contains the HTML title
var $mPagetitle = '';
- /// Contains all of the <body> content. Should be private we got set/get accessors and the append() method.
+ /// Contains all of the "<body>" content. Should be private we got set/get accessors and the append() method.
var $mBodytext = '';
/**
* Holds the debug lines that will be output as comments in page source if
* $wgDebugComments is enabled. See also $wgShowDebug.
- * TODO: make a getter method for this
+ * @deprecated since 1.20; use MWDebug class instead.
*/
- public $mDebugtext = ''; // TODO: we might want to replace it by wfDebug() wfDebugLog()
+ public $mDebugtext = '';
- /// Should be private. Stores contents of <title> tag
+ /// Should be private. Stores contents of "<title>" tag
var $mHTMLtitle = '';
/// Should be private. Is the displayed content related to the source of the corresponding wiki article.
@@ -99,8 +116,8 @@ class OutputPage extends ContextSource {
/**
* Should be private. Used for JavaScript (pre resource loader)
* We should split js / css.
- * mScripts content is inserted as is in <head> by Skin. This might contains
- * either a link to a stylesheet or inline css.
+ * mScripts content is inserted as is in "<head>" by Skin. This might
+ * contains either a link to a stylesheet or inline css.
*/
var $mScripts = '';
@@ -118,7 +135,7 @@ class OutputPage extends ContextSource {
*/
var $mPageLinkTitle = '';
- /// Array of elements in <head>. Parser might add its own headers!
+ /// Array of elements in "<head>". Parser might add its own headers!
var $mHeadItems = array();
// @todo FIXME: Next variables probably comes from the resource loader
@@ -180,7 +197,7 @@ class OutputPage extends ContextSource {
/**
* Comes from the parser. This was probably made to load CSS/JS only
- * if we had <gallery>. Used directly in CategoryPage.php
+ * if we had "<gallery>". Used directly in CategoryPage.php
* Looks like resource loader can replace this.
*/
var $mNoGallery = false;
@@ -220,7 +237,6 @@ class OutputPage extends ContextSource {
private $mFollowPolicy = 'follow';
private $mVaryHeader = array(
'Accept-Encoding' => array( 'list-contains=gzip' ),
- 'Cookie' => null
);
/**
@@ -276,7 +292,7 @@ class OutputPage extends ContextSource {
}
/**
- * Add a new <meta> tag
+ * Add a new "<meta>" tag
* To add an http-equiv meta tag, precede the name with "http:"
*
* @param $name String tag name
@@ -389,7 +405,7 @@ class OutputPage extends ContextSource {
/**
* Add a self-contained script tag with the given contents
*
- * @param $script String: JavaScript text, no <script> tags
+ * @param $script String: JavaScript text, no "<script>" tags
*/
public function addInlineScript( $script ) {
$this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n";
@@ -631,24 +647,16 @@ class OutputPage extends ContextSource {
$maxModified = max( $modifiedTimes );
$this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
- if( empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
+ $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
+ if ( $clientHeader === false ) {
wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false );
return false;
}
- # Make debug info
- $info = '';
- foreach ( $modifiedTimes as $name => $value ) {
- if ( $info !== '' ) {
- $info .= ', ';
- }
- $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
- }
-
# IE sends sizes after the date like this:
# Wed, 20 Aug 2003 06:51:19 GMT; length=5202
# this breaks strtotime().
- $clientHeader = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
+ $clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
wfSuppressWarnings(); // E_STRICT system time bitching
$clientHeaderTime = strtotime( $clientHeader );
@@ -659,6 +667,15 @@ class OutputPage extends ContextSource {
}
$clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
+ # Make debug info
+ $info = '';
+ foreach ( $modifiedTimes as $name => $value ) {
+ if ( $info !== '' ) {
+ $info .= ', ';
+ }
+ $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
+ }
+
wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", false );
wfDebug( __METHOD__ . ": effective Last-Modified: " .
@@ -763,7 +780,7 @@ class OutputPage extends ContextSource {
}
/**
- * "HTML title" means the contents of <title>.
+ * "HTML title" means the contents of "<title>".
* It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
*
* @param $name string
@@ -777,7 +794,7 @@ class OutputPage extends ContextSource {
}
/**
- * Return the "HTML title", i.e. the content of the <title> tag.
+ * Return the "HTML title", i.e. the content of the "<title>" tag.
*
* @return String
*/
@@ -788,7 +805,7 @@ class OutputPage extends ContextSource {
/**
* Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
*
- * param @t Title
+ * @param $t Title
*/
public function setRedirectedFrom( $t ) {
$this->mRedirectedFrom = $t;
@@ -1075,7 +1092,7 @@ class OutputPage extends ContextSource {
/**
* Add new language links
*
- * @param $newLinkArray Associative array mapping language code to the page
+ * @param $newLinkArray array Associative array mapping language code to the page
* name
*/
public function addLanguageLinks( $newLinkArray ) {
@@ -1085,7 +1102,7 @@ class OutputPage extends ContextSource {
/**
* Reset the language links and add new language links
*
- * @param $newLinkArray Associative array mapping language code to the page
+ * @param $newLinkArray array Associative array mapping language code to the page
* name
*/
public function setLanguageLinks( $newLinkArray ) {
@@ -1298,18 +1315,9 @@ class OutputPage extends ContextSource {
}
/**
- * Add $text to the debug output
- *
- * @param $text String: debug text
- */
- public function debug( $text ) {
- $this->mDebugtext .= $text;
- }
-
- /**
* Get/set the ParserOptions object to use for wikitext parsing
*
- * @param $options either the ParserOption to use or null to only get the
+ * @param $options ParserOptions|null either the ParserOption to use or null to only get the
* current ParserOption object
* @return ParserOptions object
*/
@@ -1346,11 +1354,11 @@ class OutputPage extends ContextSource {
* Set the timestamp of the revision which will be displayed. This is used
* to avoid a extra DB call in Skin::lastModified().
*
- * @param $revid Mixed: string, or null
+ * @param $timestamp Mixed: string, or null
* @return Mixed: previous value
*/
- public function setRevisionTimestamp( $timestmap ) {
- return wfSetVar( $this->mRevisionTimestamp, $timestmap );
+ public function setRevisionTimestamp( $timestamp) {
+ return wfSetVar( $this->mRevisionTimestamp, $timestamp );
}
/**
@@ -1366,7 +1374,7 @@ class OutputPage extends ContextSource {
/**
* Set the displayed file version
*
- * @param $file File|false
+ * @param $file File|bool
* @return Mixed: previous value
*/
public function setFileVersion( $file ) {
@@ -1662,18 +1670,6 @@ class OutputPage extends ContextSource {
}
/**
- * Return whether this page is not cacheable because "useskin" or "uselang"
- * URL parameters were passed.
- *
- * @return Boolean
- */
- function uncacheableBecauseRequestVars() {
- $request = $this->getRequest();
- return $request->getText( 'useskin', false ) === false
- && $request->getText( 'uselang', false ) === false;
- }
-
- /**
* Check if the request has a cache-varying cookie header
* If it does, it's very important that we don't allow public caching
*
@@ -1718,6 +1714,16 @@ class OutputPage extends ContextSource {
}
/**
+ * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader,
+ * such as Accept-Encoding or Cookie
+ *
+ * @return String
+ */
+ public function getVaryHeader() {
+ return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) );
+ }
+
+ /**
* Get a complete X-Vary-Options header
*
* @return String
@@ -1734,7 +1740,7 @@ class OutputPage extends ContextSource {
$headers = array();
foreach( $this->mVaryHeader as $header => $option ) {
$newheader = $header;
- if( is_array( $option ) ) {
+ if ( is_array( $option ) && count( $option ) > 0 ) {
$newheader .= ';' . implode( ';', $option );
}
$headers[] = $newheader;
@@ -1829,18 +1835,19 @@ class OutputPage extends ContextSource {
$response->header( "ETag: $this->mETag" );
}
+ $this->addVaryHeader( 'Cookie' );
$this->addAcceptLanguage();
# don't serve compressed data to clients who can't handle it
# maintain different caches for logged-in users and non-logged in ones
- $response->header( 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ) );
+ $response->header( $this->getVaryHeader() );
if ( $wgUseXVO ) {
# Add an X-Vary-Options header for Squid with Wikimedia patches
$response->header( $this->getXVO() );
}
- if( !$this->uncacheableBecauseRequestVars() && $this->mEnableClientCache ) {
+ if( $this->mEnableClientCache ) {
if(
$wgUseSquid && session_id() == '' && !$this->isPrintable() &&
$this->mSquidMaxage != 0 && !$this->haveCacheVaryCookies()
@@ -1983,6 +1990,9 @@ class OutputPage extends ContextSource {
wfProfileOut( 'Output-skin' );
}
+ // This hook allows last minute changes to final overall output by modifying output buffer
+ wfRunHooks( 'AfterFinalPageOutput', array( $this ) );
+
$this->sendCacheControl();
ob_end_flush();
wfProfileOut( __METHOD__ );
@@ -2008,18 +2018,14 @@ class OutputPage extends ContextSource {
/**
* Prepare this object to display an error page; disable caching and
* indexing, clear the current text and redirect, set the page's title
- * and optionally an custom HTML title (content of the <title> tag).
+ * 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();
- * optional, if not passed the <title> attribute will be
+ * optional, if not passed the "<title>" attribute will be
* based on $pageTitle
*/
public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
- if ( $this->getTitle() ) {
- $this->mDebugtext .= 'Original title: ' . $this->getTitle()->getPrefixedText() . "\n";
- }
-
$this->setPageTitle( $pageTitle );
if ( $htmlTitle !== false ) {
$this->setHTMLTitle( $htmlTitle );
@@ -2037,13 +2043,18 @@ class OutputPage extends ContextSource {
*
* showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) );
* showErrorPage( 'titlemsg', $messageObject );
+ * showErrorPage( $titleMessageObj, $messageObject );
*
- * @param $title String: message key for page title
+ * @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
*/
public function showErrorPage( $title, $msg, $params = array() ) {
- $this->prepareErrorPage( $this->msg( $title ), $this->msg( 'errorpagetitle' ) );
+ if( !$title instanceof Message ) {
+ $title = $this->msg( $title );
+ }
+
+ $this->prepareErrorPage( $title );
if ( $msg instanceof Message ){
$this->addHTML( $msg->parse() );
@@ -2330,7 +2341,7 @@ $templates
* Add a "return to" link pointing to a specified title
*
* @param $title Title to link
- * @param $query String query string
+ * @param $query Array query string parameters
* @param $text String text of the link (input is not escaped)
*/
public function addReturnTo( $title, $query = array(), $text = null ) {
@@ -2344,7 +2355,7 @@ $templates
* Add a "return to" link pointing to a specified title,
* or the title indicated in the request, or else the main page
*
- * @param $unused No longer used
+ * @param $unused
* @param $returnto Title or String to return to
* @param $returntoquery String: query string for the return to link
*/
@@ -2370,13 +2381,13 @@ $templates
$titleObj = Title::newMainPage();
}
- $this->addReturnTo( $titleObj, $returntoquery );
+ $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) );
}
/**
* @param $sk Skin The given Skin
* @param $includeStyle Boolean: unused
- * @return String: The doctype, opening <html>, and head element.
+ * @return String: The doctype, opening "<html>", and head element.
*/
public function headElement( Skin $sk, $includeStyle = true ) {
global $wgContLang;
@@ -2440,7 +2451,7 @@ $templates
*/
private function addDefaultModules() {
global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax,
- $wgAjaxWatch, $wgEnableMWSuggest;
+ $wgAjaxWatch;
// Add base resources
$this->addModules( array(
@@ -2465,11 +2476,11 @@ $templates
wfRunHooks( 'AjaxAddScript', array( &$this ) );
if( $wgAjaxWatch && $this->getUser()->isLoggedIn() ) {
- $this->addModules( 'mediawiki.action.watch.ajax' );
+ $this->addModules( 'mediawiki.page.watch.ajax' );
}
- if ( $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) {
- $this->addModules( 'mediawiki.legacy.mwsuggest' );
+ if ( !$this->getUser()->getOption( 'disablesuggest', false ) ) {
+ $this->addModules( 'mediawiki.searchSuggest' );
}
}
@@ -2501,19 +2512,21 @@ $templates
* @param $only String ResourceLoaderModule TYPE_ class constant
* @param $useESI boolean
* @param $extraQuery Array 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
+ * @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;
+
if ( !count( $modules ) ) {
return '';
}
if ( count( $modules ) > 1 ) {
// Remove duplicate module requests
- $modules = array_unique( (array) $modules );
+ $modules = array_unique( $modules );
// Sort module names so requests are more uniform
sort( $modules );
@@ -2530,7 +2543,7 @@ $templates
// Create keyed-by-group list of module objects from modules list
$groups = array();
$resourceLoader = $this->getResourceLoader();
- foreach ( (array) $modules as $name ) {
+ foreach ( $modules as $name ) {
$module = $resourceLoader->getModule( $name );
# Check that we're allowed to include this module on this page
if ( !$module
@@ -2551,7 +2564,7 @@ $templates
}
$links = '';
- foreach ( $groups as $group => $modules ) {
+ foreach ( $groups as $group => $grpModules ) {
// Special handling for user-specific groups
$user = null;
if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) {
@@ -2573,14 +2586,30 @@ $templates
$extraQuery
);
$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
- // Drop modules that know they're empty
- foreach ( $modules as $key => $module ) {
+ // Extract modules that know they're empty
+ $emptyModules = array ();
+ foreach ( $grpModules as $key => $module ) {
if ( $module->isKnownEmpty( $context ) ) {
- unset( $modules[$key] );
+ $emptyModules[$key] = 'ready';
+ unset( $grpModules[$key] );
}
}
+ // Inline empty modules: since they're empty, just mark them as 'ready'
+ 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";
+ }
+
// If there are no modules left, skip this group
- if ( $modules === array() ) {
+ if ( count( $grpModules ) === 0 ) {
continue;
}
@@ -2591,12 +2620,12 @@ $templates
if ( $group === 'private' ) {
if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
$links .= Html::inlineStyle(
- $resourceLoader->makeModuleResponse( $context, $modules )
+ $resourceLoader->makeModuleResponse( $context, $grpModules )
);
} else {
$links .= Html::inlineScript(
ResourceLoader::makeLoaderConditionalScript(
- $resourceLoader->makeModuleResponse( $context, $modules )
+ $resourceLoader->makeModuleResponse( $context, $grpModules )
)
);
}
@@ -2612,7 +2641,7 @@ $templates
if ( $group === 'user' ) {
// Get the maximum timestamp
$timestamp = 1;
- foreach ( $modules as $module ) {
+ foreach ( $grpModules as $module ) {
$timestamp = max( $timestamp, $module->getModifiedTime( $context ) );
}
// Add a version parameter so cache will break when things change
@@ -2620,7 +2649,7 @@ $templates
}
$url = ResourceLoader::makeLoaderURL(
- array_keys( $modules ),
+ array_keys( $grpModules ),
$this->getLanguage()->getCode(),
$this->getSkin()->getSkinName(),
$user,
@@ -2642,7 +2671,7 @@ $templates
// Automatically select style/script elements
if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
$link = Html::linkedStyle( $url );
- } else if ( $loadCall ) {
+ } else if ( $loadCall ) {
$link = Html::inlineScript(
ResourceLoader::makeLoaderConditionalScript(
Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
@@ -2663,14 +2692,14 @@ $templates
}
/**
- * JS stuff to put in the <head>. This is the startup module, config
+ * JS stuff to put in the "<head>". This is the startup module, config
* vars and modules marked with position 'top'
*
* @return String: HTML fragment
*/
function getHeadScripts() {
global $wgResourceLoaderExperimentalAsyncLoading;
-
+
// Startup - this will immediately load jquery and mediawiki modules
$scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
@@ -2702,7 +2731,7 @@ $templates
)
);
}
-
+
if ( $wgResourceLoaderExperimentalAsyncLoading ) {
$scripts .= $this->getScriptsForBottomQueue( true );
}
@@ -2711,12 +2740,12 @@ $templates
}
/**
- * JS stuff to put at the 'bottom', which can either be the bottom of the <body>
- * or the bottom of the <head> depending on $wgResourceLoaderExperimentalAsyncLoading:
+ * JS stuff to put at the 'bottom', which can either be the bottom of the "<body>"
+ * or the bottom of the "<head>" depending on $wgResourceLoaderExperimentalAsyncLoading:
* modules marked with position 'bottom', legacy scripts ($this->mScripts),
* user preferences, site JS and user JS
*
- * @param $inHead boolean If true, this HTML goes into the <head>, if false it goes into the <body>
+ * @param $inHead boolean If true, this HTML goes into the "<head>", if false it goes into the "<body>"
* @return string
*/
function getScriptsForBottomQueue( $inHead ) {
@@ -2748,47 +2777,92 @@ $templates
// Legacy Scripts
$scripts .= "\n" . $this->mScripts;
- $userScripts = array();
+ $defaultModules = array();
// Add site JS if enabled
if ( $wgUseSiteJs ) {
$scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS,
/* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
);
- if( $this->getUser()->isLoggedIn() ){
- $userScripts[] = 'user.groups';
- }
+ $defaultModules['site'] = 'loading';
+ } else {
+ // The wiki is configured to not allow a site module.
+ $defaultModules['site'] = 'missing';
}
// Add user JS if enabled
- if ( $wgAllowUserJs && $this->getUser()->isLoggedIn() ) {
- 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)
- $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
- array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
- );
- // Load the previewed JS
- $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
+ if ( $wgAllowUserJs ) {
+ if ( $this->getUser()->isLoggedIn() ) {
+ 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)
+ $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
+ array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
+ );
+ // Load the previewed JS
+ $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
+ // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded
+ // asynchronously and may arrive *after* the inline script here. So the previewed code
+ // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js...
+ } else {
+ // Include the user module normally, i.e., raw to avoid it being wrapped in a closure.
+ $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+ /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+ );
+ }
+ $defaultModules['user'] = 'loading';
} else {
- // Include the user module normally
- // We can't do $userScripts[] = 'user'; because the user module would end up
- // being wrapped in a closure, so load it raw like 'site'
- $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+ // 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.
+ $defaultModules['user'] = 'ready';
+ }
+ } else {
+ // User JS disabled
+ $defaultModules['user'] = 'missing';
+ }
+
+ // Group JS is only enabled if site JS is enabled.
+ if ( $wgUseSiteJs ) {
+ if ( $this->getUser()->isLoggedIn() ) {
+ $scripts .= $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED,
/* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
);
+ $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.
+ $defaultModules['user.groups'] = 'ready';
}
+ } else {
+ // Site (and group JS) disabled
+ $defaultModules['user.groups'] = 'missing';
}
- $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED,
- /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
- );
- return $scripts;
+ $loaderInit = '';
+ if ( $inHead ) {
+ // We generate loader calls anyway, so no need to fix the client-side loader's state to 'loading'.
+ foreach ( $defaultModules as $m => $state ) {
+ if ( $state == 'loading' ) {
+ unset( $defaultModules[$m] );
+ }
+ }
+ }
+ if ( count( $defaultModules ) > 0 ) {
+ $loaderInit = Html::inlineScript(
+ ResourceLoader::makeLoaderConditionalScript(
+ ResourceLoader::makeLoaderStateScript( $defaultModules )
+ )
+ ) . "\n";
+ }
+ return $loaderInit . $scripts;
}
/**
- * JS stuff to put at the bottom of the <body>
+ * JS stuff to put at the bottom of the "<body>"
+ * @return string
*/
function getBottomScripts() {
global $wgResourceLoaderExperimentalAsyncLoading;
@@ -2802,7 +2876,7 @@ $templates
/**
* Add one or more variables to be set in mw.config in JavaScript.
*
- * @param $key {String|Array} Key or array of key/value pars.
+ * @param $keys {String|Array} Key or array of key/value pairs.
* @param $value {Mixed} [optional] Value of the configuration variable.
*/
public function addJsConfigVars( $keys, $value = null ) {
@@ -2830,7 +2904,7 @@ $templates
* @return array
*/
public function getJSVars() {
- global $wgUseAjax, $wgEnableMWSuggest;
+ global $wgUseAjax, $wgContLang;
$latestRevID = 0;
$pageID = 0;
@@ -2885,17 +2959,17 @@ $templates
'wgPageContentLanguage' => $lang->getCode(),
'wgSeparatorTransformTable' => $compactSeparatorTransTable,
'wgDigitTransformTable' => $compactDigitTransTable,
+ 'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
+ 'wgMonthNames' => $lang->getMonthNamesArray(),
+ 'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
'wgRelevantPageName' => $relevantTitle->getPrefixedDBKey(),
);
- if ( $lang->hasVariants() ) {
- $vars['wgUserVariant'] = $lang->getPreferredVariant();
+ if ( $wgContLang->hasVariants() ) {
+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
}
foreach ( $title->getRestrictionTypes() as $type ) {
$vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
}
- if ( $wgUseAjax && $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) {
- $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $this->getUser() );
- }
if ( $title->isMainPage() ) {
$vars['wgIsMainPage'] = true;
}
@@ -2938,12 +3012,11 @@ $templates
}
/**
- * @param $unused Unused
- * @param $addContentType bool
+ * @param $addContentType bool: Whether "<meta>" specifying content type should be returned
*
- * @return string HTML tag links to be put in the header.
+ * @return array in format "link name or number => 'link html'".
*/
- public function getHeadLinks( $unused = null, $addContentType = false ) {
+ public function getHeadLinksArray( $addContentType = false ) {
global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI,
$wgSitename, $wgVersion, $wgHtml5, $wgMimeType,
$wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes,
@@ -2956,20 +3029,20 @@ $templates
if ( $wgHtml5 ) {
# More succinct than <meta http-equiv=Content-Type>, has the
# same effect
- $tags[] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
+ $tags['meta-charset'] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
} else {
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-content-type'] = Html::element( 'meta', array(
'http-equiv' => 'Content-Type',
'content' => "$wgMimeType; charset=UTF-8"
) );
- $tags[] = Html::element( 'meta', array( // bug 15835
+ $tags['meta-content-style-type'] = Html::element( 'meta', array( // bug 15835
'http-equiv' => 'Content-Style-Type',
'content' => 'text/css'
) );
}
}
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-generator'] = Html::element( 'meta', array(
'name' => 'generator',
'content' => "MediaWiki $wgVersion",
) );
@@ -2978,7 +3051,7 @@ $templates
if( $p !== 'index,follow' ) {
// http://www.robotstxt.org/wc/meta-user.html
// Only show if it's different from the default robots policy
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-robots'] = Html::element( 'meta', array(
'name' => 'robots',
'content' => $p,
) );
@@ -2989,7 +3062,7 @@ $templates
"/<.*?" . ">/" => '',
"/_/" => ' '
);
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-keywords'] = Html::element( 'meta', array(
'name' => 'keywords',
'content' => preg_replace(
array_keys( $strip ),
@@ -3006,7 +3079,11 @@ $templates
} else {
$a = 'name';
}
- $tags[] = Html::element( 'meta',
+ $tagName = "meta-{$tag[0]}";
+ if ( isset( $tags[$tagName] ) ) {
+ $tagName .= $tag[1];
+ }
+ $tags[$tagName] = Html::element( 'meta',
array(
$a => $tag[0],
'content' => $tag[1]
@@ -3025,14 +3102,14 @@ $templates
&& ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) {
// Original UniversalEditButton
$msg = $this->msg( 'edit' )->text();
- $tags[] = Html::element( 'link', array(
+ $tags['universal-edit-button'] = Html::element( 'link', array(
'rel' => 'alternate',
'type' => 'application/x-wiki',
'title' => $msg,
'href' => $this->getTitle()->getLocalURL( 'action=edit' )
) );
// Alternate edit link
- $tags[] = Html::element( 'link', array(
+ $tags['alternative-edit'] = Html::element( 'link', array(
'rel' => 'edit',
'title' => $msg,
'href' => $this->getTitle()->getLocalURL( 'action=edit' )
@@ -3045,15 +3122,15 @@ $templates
# uses whichever one appears later in the HTML source. Make sure
# apple-touch-icon is specified first to avoid this.
if ( $wgAppleTouchIcon !== false ) {
- $tags[] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
+ $tags['apple-touch-icon'] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
}
if ( $wgFavicon !== false ) {
- $tags[] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
+ $tags['favicon'] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
}
# OpenSearch description link
- $tags[] = Html::element( 'link', array(
+ $tags['opensearch'] = Html::element( 'link', array(
'rel' => 'search',
'type' => 'application/opensearchdescription+xml',
'href' => wfScript( 'opensearch_desc' ),
@@ -3065,7 +3142,7 @@ $templates
# for the MediaWiki API (and potentially additional custom API
# support such as WordPress or Twitter-compatible APIs for a
# blogging extension, etc)
- $tags[] = Html::element( 'link', array(
+ $tags['rsd'] = Html::element( 'link', array(
'rel' => 'EditURI',
'type' => 'application/rsd+xml',
// Output a protocol-relative URL here if $wgServer is protocol-relative
@@ -3085,14 +3162,14 @@ $templates
if ( !$urlvar ) {
$variants = $lang->getVariants();
foreach ( $variants as $_v ) {
- $tags[] = Html::element( 'link', array(
+ $tags["variant-$_v"] = Html::element( 'link', array(
'rel' => 'alternate',
'hreflang' => $_v,
'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) )
);
}
} else {
- $tags[] = Html::element( 'link', array(
+ $tags['canonical'] = Html::element( 'link', array(
'rel' => 'canonical',
'href' => $this->getTitle()->getCanonicalUrl()
) );
@@ -3115,7 +3192,7 @@ $templates
}
if ( $copyright ) {
- $tags[] = Html::element( 'link', array(
+ $tags['copyright'] = Html::element( 'link', array(
'rel' => 'copyright',
'href' => $copyright )
);
@@ -3164,11 +3241,21 @@ $templates
}
}
}
- return implode( "\n", $tags );
+ 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 ) );
}
/**
- * Generate a <link rel/> for a feed.
+ * Generate a "<link rel/>" for a feed.
*
* @param $type String: feed type
* @param $url String: URL to the feed
@@ -3223,7 +3310,7 @@ $templates
}
/**
- * Build a set of <link>s for the stylesheets specified in the $this->styles array.
+ * Build a set of "<link>" elements for the stylesheets specified in the $this->styles array.
* These will be applied to various media & IE conditionals.
*
* @return string
@@ -3259,7 +3346,7 @@ $templates
$otherTags .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false,
array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
);
-
+
// Load the previewed CSS
// If needed, Janus it first. This is user-supplied CSS, so it's
// assumed to be right for the content language directionality.
@@ -3421,7 +3508,7 @@ $templates
* Add a wikitext-formatted message to the output.
* This is equivalent to:
*
- * $wgOut->addWikiText( wfMsgNoTrans( ... ) )
+ * $wgOut->addWikiText( wfMessage( ... )->plain() )
*/
public function addWikiMsg( /*...*/ ) {
$args = func_get_args();
@@ -3450,9 +3537,6 @@ $templates
* message names, or arrays, in which case the first element is the message name,
* and subsequent elements are the parameters to that message.
*
- * The special named parameter 'options' in a message specification array is passed
- * through to the $options parameter of wfMsgExt().
- *
* Don't use this for messages that are not in users interface language.
*
* For example:
@@ -3461,7 +3545,7 @@ $templates
*
* Is equivalent to:
*
- * $wgOut->addWikiText( "<div class='error'>\n" . wfMsgNoTrans( 'some-error' ) . "\n</div>" );
+ * $wgOut->addWikiText( "<div class='error'>\n" . wfMessage( 'some-error' )->plain() . "\n</div>" );
*
* The newline after opening div is needed in some wikitext. See bug 19226.
*
@@ -3478,14 +3562,17 @@ $templates
$args = $spec;
$name = array_shift( $args );
if ( isset( $args['options'] ) ) {
- $options = $args['options'];
unset( $args['options'] );
+ wfDeprecated(
+ 'Adding "options" to ' . __METHOD__ . ' is no longer supported',
+ '1.20'
+ );
}
} else {
$args = array();
$name = $spec;
}
- $s = str_replace( '$' . ( $n + 1 ), wfMsgExt( $name, $options, $args ), $s );
+ $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
}
$this->addWikiText( $s );
}
diff --git a/includes/PHPVersionError.php b/includes/PHPVersionError.php
index ec6490a8..dad71f82 100644
--- a/includes/PHPVersionError.php
+++ b/includes/PHPVersionError.php
@@ -1,6 +1,27 @@
<?php
/**
* Display something vaguely comprehensible in the event of a totally unrecoverable error.
+ *
+ * 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
+ */
+
+/**
+ * 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.
* Safe for PHP4 (and putting this here means that WebStart.php and GlobalSettings.php
* no longer need to be).
@@ -17,9 +38,9 @@
* version are hardcoded here
*/
function wfPHPVersionError( $type ){
- $mwVersion = '1.19';
+ $mwVersion = '1.20';
$phpVersion = PHP_VERSION;
- $message = "MediaWiki $mwVersion requires at least PHP version 5.2.3, you are using PHP $phpVersion.";
+ $message = "MediaWiki $mwVersion requires at least PHP version 5.3.2, you are using PHP $phpVersion.";
if( $type == 'index.php' ) {
$encLogo = htmlspecialchars(
str_replace( '//', '/', pathinfo( $_SERVER['SCRIPT_NAME'], PATHINFO_DIRNAME ) . '/'
diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php
index dc5e971d..01a2439f 100644
--- a/includes/PageQueryPage.php
+++ b/includes/PageQueryPage.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Variant of QueryPage which formats the result as a simple link to the page.
+ *
+ * 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
+ */
/**
* Variant of QueryPage which formats the result as a simple link to the page
@@ -16,11 +37,15 @@ abstract class PageQueryPage extends QueryPage {
*/
public function formatResult( $skin, $row ) {
global $wgContLang;
+
$title = Title::makeTitleSafe( $row->namespace, $row->title );
- $text = $row->title;
+
if ( $title instanceof Title ) {
$text = $wgContLang->convert( $title->getPrefixedText() );
+ return Linker::linkKnown( $title, htmlspecialchars( $text ) );
+ } else {
+ return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription( $this->getContext(), $row->namespace, $row->title ) );
}
- return Linker::linkKnown( $title, htmlspecialchars( $text ) );
}
}
diff --git a/includes/Pager.php b/includes/Pager.php
index faae3d2d..96ba446e 100644
--- a/includes/Pager.php
+++ b/includes/Pager.php
@@ -1,12 +1,31 @@
<?php
/**
- * @defgroup Pager Pager
+ * Efficient paging for SQL queries.
+ *
+ * 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 Pager
*/
/**
+ * @defgroup Pager Pager
+ */
+
+/**
* Basic pager interface.
* @ingroup Pager
*/
@@ -100,6 +119,11 @@ abstract class IndexPager extends ContextSource implements Pager {
protected $mLastShown, $mFirstShown, $mPastTheEndIndex, $mDefaultQuery, $mNavigationBar;
/**
+ * Whether to include the offset in the query
+ */
+ protected $mIncludeOffset = false;
+
+ /**
* Result object for the query. Warning: seek before use.
*
* @var ResultWrapper
@@ -120,7 +144,10 @@ abstract class IndexPager extends ContextSource implements Pager {
# Use consistent behavior for the limit options
$this->mDefaultLimit = intval( $this->getUser()->getOption( 'rclimit' ) );
- list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset();
+ 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 );
@@ -157,6 +184,15 @@ abstract class IndexPager extends ContextSource implements Pager {
}
/**
+ * Get the Database object in use
+ *
+ * @return DatabaseBase
+ */
+ public function getDatabase() {
+ return $this->mDb;
+ }
+
+ /**
* Do the query, using information from the object context. This function
* has been kept minimal to make it overridable if necessary, to allow for
* result sets formed from multiple DB queries.
@@ -175,6 +211,7 @@ abstract class IndexPager extends ContextSource implements Pager {
$queryLimit,
$descending
);
+
$this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult );
$this->mQueryDone = true;
@@ -202,10 +239,30 @@ abstract class IndexPager extends ContextSource implements Pager {
/**
* Set the limit from an other source than the request
*
+ * Verifies limit is between 1 and 5000
+ *
* @param $limit Int|String
*/
function setLimit( $limit ) {
- $this->mLimit = $limit;
+ $limit = (int) $limit;
+ // WebRequest::getLimitOffset() puts a cap of 5000, so do same here.
+ if ( $limit > 5000 ) {
+ $limit = 5000;
+ }
+ if ( $limit > 0 ) {
+ $this->mLimit = $limit;
+ }
+ }
+
+ /**
+ * Set whether a row matching exactly the offset should be also included
+ * in the result or not. By default this is not the case, but when the
+ * offset is user-supplied this might be wanted.
+ *
+ * @param $include bool
+ */
+ public function setIncludeOffset( $include ) {
+ $this->mIncludeOffset = $include;
}
/**
@@ -284,7 +341,20 @@ abstract class IndexPager extends ContextSource implements Pager {
* @param $descending Boolean: query direction, false for ascending, true for descending
* @return ResultWrapper
*/
- function reallyDoQuery( $offset, $limit, $descending ) {
+ public function reallyDoQuery( $offset, $limit, $descending ) {
+ list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $descending );
+ return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
+ }
+
+ /**
+ * Build variables to use by the database wrapper.
+ *
+ * @param $offset String: index offset, inclusive
+ * @param $limit Integer: exact query limit
+ * @param $descending Boolean: query direction, false for ascending, true for descending
+ * @return array
+ */
+ protected function buildQueryInfo( $offset, $limit, $descending ) {
$fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
$info = $this->getQueryInfo();
$tables = $info['tables'];
@@ -294,22 +364,21 @@ abstract class IndexPager extends ContextSource implements Pager {
$join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array();
$sortColumns = array_merge( array( $this->mIndexField ), $this->mExtraSortFields );
if ( $descending ) {
- $options['ORDER BY'] = implode( ',', $sortColumns );
- $operator = '>';
+ $options['ORDER BY'] = $sortColumns;
+ $operator = $this->mIncludeOffset ? '>=' : '>';
} else {
$orderBy = array();
foreach ( $sortColumns as $col ) {
$orderBy[] = $col . ' DESC';
}
- $options['ORDER BY'] = implode( ',', $orderBy );
- $operator = '<';
+ $options['ORDER BY'] = $orderBy;
+ $operator = $this->mIncludeOffset ? '<=' : '<';
}
if ( $offset != '' ) {
$conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
}
$options['LIMIT'] = intval( $limit );
- $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
- return new ResultWrapper( $this->mDb, $res );
+ return array( $tables, $fields, $conds, $fname, $options, $join_conds );
}
/**
@@ -366,9 +435,10 @@ abstract class IndexPager extends ContextSource implements Pager {
* @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
+ *
* @return String: HTML fragment
*/
- function makeLink($text, $query = null, $type=null) {
+ function makeLink( $text, array $query = null, $type = null ) {
if ( $query === null ) {
return $text;
}
@@ -382,6 +452,7 @@ abstract class IndexPager extends ContextSource implements Pager {
if( $type ) {
$attrs['class'] = "mw-{$type}link";
}
+
return Linker::linkKnown(
$this->getTitle(),
$text,
@@ -433,7 +504,7 @@ abstract class IndexPager extends ContextSource implements Pager {
* By default, all parameters passed in the URL are used, except for a
* short blacklist.
*
- * @return Associative array
+ * @return array Associative array
*/
function getDefaultQuery() {
if ( !isset( $this->mDefaultQuery ) ) {
@@ -526,6 +597,7 @@ abstract class IndexPager extends ContextSource implements Pager {
function getPagingLinks( $linkTexts, $disabledTexts = array() ) {
$queries = $this->getPagingQueries();
$links = array();
+
foreach ( $queries as $type => $query ) {
if ( $query !== false ) {
$links[$type] = $this->makeLink(
@@ -539,6 +611,7 @@ abstract class IndexPager extends ContextSource implements Pager {
$links[$type] = $linkTexts[$type];
}
}
+
return $links;
}
@@ -651,41 +724,32 @@ abstract class AlphabeticPager extends IndexPager {
* @return String HTML
*/
function getNavigationBar() {
- if ( !$this->isNavigationBarShown() ) return '';
+ if ( !$this->isNavigationBarShown() ) {
+ return '';
+ }
if( isset( $this->mNavigationBar ) ) {
return $this->mNavigationBar;
}
- $lang = $this->getLanguage();
-
- $opts = array( 'parsemag', 'escapenoentities' );
$linkTexts = array(
- 'prev' => wfMsgExt(
- 'prevn',
- $opts,
- $lang->formatNum( $this->mLimit )
- ),
- 'next' => wfMsgExt(
- 'nextn',
- $opts,
- $lang->formatNum($this->mLimit )
- ),
- 'first' => wfMsgExt( 'page_first', $opts ),
- 'last' => wfMsgExt( 'page_last', $opts )
+ 'prev' => $this->msg( 'prevn' )->numParams( $this->mLimit )->escaped(),
+ 'next' => $this->msg( 'nextn' )->numParams( $this->mLimit )->escaped(),
+ 'first' => $this->msg( 'page_first' )->escaped(),
+ 'last' => $this->msg( 'page_last' )->escaped()
);
+ $lang = $this->getLanguage();
+
$pagingLinks = $this->getPagingLinks( $linkTexts );
$limitLinks = $this->getLimitLinks();
$limits = $lang->pipeList( $limitLinks );
- $this->mNavigationBar =
- "(" . $lang->pipeList(
- array( $pagingLinks['first'],
- $pagingLinks['last'] )
- ) . ") " .
- wfMsgHtml( 'viewprevnext', $pagingLinks['prev'],
- $pagingLinks['next'], $limits );
+ $this->mNavigationBar = $this->msg( 'parentheses' )->rawParams(
+ $lang->pipeList( array( $pagingLinks['first'],
+ $pagingLinks['last'] ) ) )->escaped() . " " .
+ $this->msg( 'viewprevnext' )->rawParams( $pagingLinks['prev'],
+ $pagingLinks['next'], $limits )->escaped();
if( !is_array( $this->getIndexField() ) ) {
# Early return to avoid undue nesting
@@ -699,21 +763,22 @@ abstract class AlphabeticPager extends IndexPager {
if( $first ) {
$first = false;
} else {
- $extra .= wfMsgExt( 'pipe-separator' , 'escapenoentities' );
+ $extra .= $this->msg( 'pipe-separator' )->escaped();
}
if( $order == $this->mOrderType ) {
- $extra .= wfMsgHTML( $msgs[$order] );
+ $extra .= $this->msg( $msgs[$order] )->escaped();
} else {
$extra .= $this->makeLink(
- wfMsgHTML( $msgs[$order] ),
+ $this->msg( $msgs[$order] )->escaped(),
array( 'order' => $order )
);
}
}
if( $extra !== '' ) {
- $this->mNavigationBar .= " ($extra)";
+ $extra = ' ' . $this->msg( 'parentheses' )->rawParams( $extra )->escaped();
+ $this->mNavigationBar .= $extra;
}
return $this->mNavigationBar;
@@ -750,43 +815,35 @@ abstract class ReverseChronologicalPager extends IndexPager {
return $this->mNavigationBar;
}
- $nicenumber = $this->getLanguage()->formatNum( $this->mLimit );
$linkTexts = array(
- 'prev' => wfMsgExt(
- 'pager-newer-n',
- array( 'parsemag', 'escape' ),
- $nicenumber
- ),
- 'next' => wfMsgExt(
- 'pager-older-n',
- array( 'parsemag', 'escape' ),
- $nicenumber
- ),
- 'first' => wfMsgHtml( 'histlast' ),
- 'last' => wfMsgHtml( 'histfirst' )
+ 'prev' => $this->msg( 'pager-newer-n' )->numParams( $this->mLimit )->escaped(),
+ 'next' => $this->msg( 'pager-older-n' )->numParams( $this->mLimit )->escaped(),
+ 'first' => $this->msg( 'histlast' )->escaped(),
+ 'last' => $this->msg( 'histfirst' )->escaped()
);
$pagingLinks = $this->getPagingLinks( $linkTexts );
$limitLinks = $this->getLimitLinks();
$limits = $this->getLanguage()->pipeList( $limitLinks );
+ $firstLastLinks = $this->msg( 'parentheses' )->rawParams( "{$pagingLinks['first']}" .
+ $this->msg( 'pipe-separator' )->escaped() .
+ "{$pagingLinks['last']}" )->escaped();
+
+ $this->mNavigationBar = $firstLastLinks . ' ' .
+ $this->msg( 'viewprevnext' )->rawParams(
+ $pagingLinks['prev'], $pagingLinks['next'], $limits )->escaped();
- $this->mNavigationBar = "({$pagingLinks['first']}" .
- wfMsgExt( 'pipe-separator' , 'escapenoentities' ) .
- "{$pagingLinks['last']}) " .
- wfMsgHTML(
- 'viewprevnext',
- $pagingLinks['prev'], $pagingLinks['next'],
- $limits
- );
return $this->mNavigationBar;
}
function getDateCond( $year, $month ) {
$year = intval( $year );
$month = intval( $month );
+
// Basic validity checks
$this->mYear = $year > 0 ? $year : false;
$this->mMonth = ( $month > 0 && $month < 13 ) ? $month : false;
+
// Given an optional year and month, we need to generate a timestamp
// to use as "WHERE rev_timestamp <= result"
// Examples: year = 2006 equals < 20070101 (+000000)
@@ -795,6 +852,7 @@ abstract class ReverseChronologicalPager extends IndexPager {
if ( !$this->mYear && !$this->mMonth ) {
return;
}
+
if ( $this->mYear ) {
$year = $this->mYear;
} else {
@@ -805,6 +863,7 @@ abstract class ReverseChronologicalPager extends IndexPager {
$year--;
}
}
+
if ( $this->mMonth ) {
$month = $this->mMonth + 1;
// For December, we want January 1 of the next year
@@ -817,14 +876,18 @@ abstract class ReverseChronologicalPager extends IndexPager {
$month = 1;
$year++;
}
+
// Y2K38 bug
if ( $year > 2032 ) {
$year = 2032;
}
+
$ymd = (int)sprintf( "%04d%02d01", $year, $month );
+
if ( $ymd > 20320101 ) {
$ymd = 20320101;
}
+
$this->mOffset = $this->mDb->timestamp( "${ymd}000000" );
}
}
@@ -837,7 +900,7 @@ abstract class TablePager extends IndexPager {
var $mSort;
var $mCurrentRow;
- function __construct( IContextSource $context = null ) {
+ public function __construct( IContextSource $context = null ) {
if ( $context ) {
$this->setContext( $context );
}
@@ -855,12 +918,16 @@ abstract class TablePager extends IndexPager {
parent::__construct();
}
+ /**
+ * @protected
+ * @return string
+ */
function getStartBody() {
global $wgStylePath;
$tableClass = htmlspecialchars( $this->getTableClass() );
$sortClass = htmlspecialchars( $this->getSortHeaderClass() );
- $s = "<table style='border:1;' class=\"mw-datatable $tableClass\"><thead><tr>\n";
+ $s = "<table style='border:1px;' class=\"mw-datatable $tableClass\"><thead><tr>\n";
$fields = $this->getFieldNames();
# Make table header
@@ -877,18 +944,18 @@ abstract class TablePager extends IndexPager {
$image = 'Arr_d.png';
$query['asc'] = '1';
$query['desc'] = '';
- $alt = htmlspecialchars( wfMsg( 'descending_abbrev' ) );
+ $alt = $this->msg( 'descending_abbrev' )->escaped();
} else {
# Ascending
$image = 'Arr_u.png';
$query['asc'] = '';
$query['desc'] = '1';
- $alt = htmlspecialchars( wfMsg( 'ascending_abbrev' ) );
+ $alt = $this->msg( 'ascending_abbrev' )->escaped();
}
$image = htmlspecialchars( "$wgStylePath/common/images/$image" );
$link = $this->makeLink(
"<img width=\"12\" height=\"12\" alt=\"$alt\" src=\"$image\" />" .
- htmlspecialchars( $name ), $query );
+ htmlspecialchars( $name ), $query );
$s .= "<th class=\"$sortClass\">$link</th>\n";
} else {
$s .= '<th>' . $this->makeLink( htmlspecialchars( $name ), $query ) . "</th>\n";
@@ -901,39 +968,55 @@ abstract class TablePager extends IndexPager {
return $s;
}
+ /**
+ * @protected
+ * @return string
+ */
function getEndBody() {
return "</tbody></table>\n";
}
+ /**
+ * @protected
+ * @return string
+ */
function getEmptyBody() {
$colspan = count( $this->getFieldNames() );
- $msgEmpty = wfMsgHtml( 'table_pager_empty' );
+ $msgEmpty = $this->msg( 'table_pager_empty' )->escaped();
return "<tr><td colspan=\"$colspan\">$msgEmpty</td></tr>\n";
}
/**
- * @param $row Array
+ * @protected
+ * @param stdClass $row
* @return String HTML
*/
function formatRow( $row ) {
- $this->mCurrentRow = $row; # In case formatValue etc need to know
+ $this->mCurrentRow = $row; // In case formatValue etc need to know
$s = Xml::openElement( 'tr', $this->getRowAttrs( $row ) );
$fieldNames = $this->getFieldNames();
+
foreach ( $fieldNames as $field => $name ) {
$value = isset( $row->$field ) ? $row->$field : null;
$formatted = strval( $this->formatValue( $field, $value ) );
+
if ( $formatted == '' ) {
$formatted = '&#160;';
}
+
$s .= Xml::tags( 'td', $this->getCellAttrs( $field, $value ), $formatted );
}
+
$s .= "</tr>\n";
+
return $s;
}
/**
* Get a class name to be applied to the given row.
*
+ * @protected
+ *
* @param $row Object: the database result row
* @return String
*/
@@ -944,8 +1027,10 @@ abstract class TablePager extends IndexPager {
/**
* Get attributes to be applied to the given row.
*
+ * @protected
+ *
* @param $row Object: the database result row
- * @return Array of <attr> => <value>
+ * @return Array of attribute => value
*/
function getRowAttrs( $row ) {
$class = $this->getRowClass( $row );
@@ -962,6 +1047,8 @@ abstract class TablePager extends IndexPager {
* take this as an excuse to hardcode styles; use classes and
* CSS instead. Row context is available in $this->mCurrentRow
*
+ * @protected
+ *
* @param $field String The column
* @param $value String The cell contents
* @return Array of attr => value
@@ -970,18 +1057,34 @@ abstract class TablePager extends IndexPager {
return array( 'class' => 'TablePager_col_' . $field );
}
+ /**
+ * @protected
+ * @return string
+ */
function getIndexField() {
return $this->mSort;
}
+ /**
+ * @protected
+ * @return string
+ */
function getTableClass() {
return 'TablePager';
}
+ /**
+ * @protected
+ * @return string
+ */
function getNavClass() {
return 'TablePager_nav';
}
+ /**
+ * @protected
+ * @return string
+ */
function getSortHeaderClass() {
return 'TablePager_sort';
}
@@ -990,7 +1093,7 @@ abstract class TablePager extends IndexPager {
* A navigation bar with images
* @return String HTML
*/
- function getNavigationBar() {
+ public function getNavigationBar() {
global $wgStylePath;
if ( !$this->isNavigationBarShown() ) {
@@ -1025,7 +1128,7 @@ abstract class TablePager extends IndexPager {
$linkTexts = array();
$disabledTexts = array();
foreach ( $labels as $type => $label ) {
- $msgLabel = wfMsgHtml( $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";
}
@@ -1042,11 +1145,11 @@ abstract class TablePager extends IndexPager {
}
/**
- * Get a <select> element which has options for each of the allowed limits
+ * Get a "<select>" element which has options for each of the allowed limits
*
* @return String: HTML fragment
*/
- function getLimitSelect() {
+ public function getLimitSelect() {
# 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 ) ) {
@@ -1072,7 +1175,7 @@ abstract class TablePager extends IndexPager {
}
/**
- * Get <input type="hidden"> elements for use in a method="get" form.
+ * Get \<input type="hidden"\> elements for use in a method="get" form.
* Resubmits all defined elements of the query string, except for a
* blacklist, passed in the $blacklist parameter.
*
@@ -1117,9 +1220,10 @@ abstract class TablePager extends IndexPager {
*/
function getLimitDropdown() {
# Make the select with some explanatory text
- $msgSubmit = wfMsgHtml( 'table_pager_limit_submit' );
+ $msgSubmit = $this->msg( 'table_pager_limit_submit' )->escaped();
- return wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) .
+ return $this->msg( 'table_pager_limit' )
+ ->rawParams( $this->getLimitSelect() )->escaped() .
"\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" .
$this->getHiddenFields( array( 'limit' ) );
}
@@ -1139,13 +1243,19 @@ abstract class TablePager extends IndexPager {
* The current result row is available as $this->mCurrentRow, in case you
* need more context.
*
+ * @protected
+ *
* @param $name String: the database field name
* @param $value String: the value retrieved from the database
*/
abstract function formatValue( $name, $value );
/**
- * The database field name used as a default sort order
+ * The database field name used as a default sort order.
+ *
+ * @protected
+ *
+ * @return string
*/
abstract function getDefaultSort();
diff --git a/includes/PathRouter.php b/includes/PathRouter.php
index 3e298a58..2dbc7ec0 100644
--- a/includes/PathRouter.php
+++ b/includes/PathRouter.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Parser to extract query parameters out of REQUEST_URI paths.
+ *
+ * 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
+ */
+
+/**
* PathRouter class.
* This class can take patterns such as /wiki/$1 and use them to
* parse query parameters out of REQUEST_URI paths.
@@ -34,7 +55,7 @@
* the relevant contents
* - The default behavior is equivalent to `array( 'title' => '$1' )`,
* if you don't want the title parameter you can explicitly use `array( 'title' => false )`
- * - You can specify a value that won't have replacements in it
+ * - You can specify a value that won't have replacements in it
* using `'foo' => array( 'value' => 'bar' );`
*
* Options:
@@ -52,10 +73,19 @@
class PathRouter {
/**
+ * @var array
+ */
+ private $patterns = array();
+
+ /**
* Protected helper to do the actual bulk work of adding a single pattern.
* This is in a separate method so that add() can handle the difference between
* a single string $path and an array() $path that contains multiple path
* patterns each with an associated $key to pass on.
+ * @param $path string
+ * @param $params array
+ * @param $options array
+ * @param $key null|string
*/
protected function doAdd( $path, $params, $options, $key = null ) {
// Make sure all paths start with a /
@@ -123,9 +153,9 @@ class PathRouter {
/**
* Add a new path pattern to the path router
*
- * @param $path The path pattern to add
- * @param $params The params for this path pattern
- * @param $options The options for this path pattern
+ * @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
*/
public function add( $path, $params = array(), $options = array() ) {
if ( is_array( $path ) ) {
@@ -140,6 +170,9 @@ class PathRouter {
/**
* Add a new path pattern to the path router with the strict option on
* @see self::add
+ * @param $path string|array
+ * @param $params array
+ * @param $options array
*/
public function addStrict( $path, $params = array(), $options = array() ) {
$options['strict'] = true;
@@ -158,6 +191,10 @@ class PathRouter {
array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
}
+ /**
+ * @param $pattern object
+ * @return float|int
+ */
protected static function makeWeight( $pattern ) {
# Start with a weight of 0
$weight = 0;
@@ -195,14 +232,14 @@ class PathRouter {
/**
* Parse a path and return the query matches for the path
*
- * @param $path The path to parse
+ * @param $path string The path to parse
* @return Array The array of matches for the path
*/
public function parse( $path ) {
// Make sure our patterns are sorted by weight so the most specific
// matches are tested first
$this->sortByWeight();
-
+
$matches = null;
foreach ( $this->patterns as $pattern ) {
@@ -219,6 +256,11 @@ class PathRouter {
return is_null( $matches ) ? array() : $matches;
}
+ /**
+ * @param $path string
+ * @param $pattern string
+ * @return array|null
+ */
protected static function extractTitle( $path, $pattern ) {
// Convert the path pattern into a regexp we can match with
$regexp = preg_quote( $pattern->path, '#' );
@@ -321,6 +363,8 @@ class PathRouterPatternReplacer {
* We do this inside of a replacement callback because after replacement we can't tell the
* difference between a $1 that was not replaced and a $1 that was part of
* the content a $1 was replaced with.
+ * @param $value string
+ * @return string
*/
public function replace( $value ) {
$this->error = false;
@@ -331,6 +375,10 @@ class PathRouterPatternReplacer {
return $value;
}
+ /**
+ * @param $m array
+ * @return string
+ */
protected function callback( $m ) {
if ( $m[1] == "key" ) {
if ( is_null( $this->key ) ) {
@@ -348,4 +396,4 @@ class PathRouterPatternReplacer {
}
}
-} \ No newline at end of file
+}
diff --git a/includes/PoolCounter.php b/includes/PoolCounter.php
index 83ae0abe..452dbc54 100644
--- a/includes/PoolCounter.php
+++ b/includes/PoolCounter.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Provides of semaphore semantics for restricting the number
+ * of workers that may be concurrently performing the same task.
+ *
+ * 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
+ */
/**
* When you have many workers (threads/servers) giving service, and a
@@ -150,6 +171,7 @@ abstract class PoolCounterWork {
/**
* Do something with the error, like showing it to the user.
+ * @return bool
*/
function error( $status ) {
return false;
diff --git a/includes/Preferences.php b/includes/Preferences.php
index ea1efa18..216ba48c 100644
--- a/includes/Preferences.php
+++ b/includes/Preferences.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Form to edit user perferences.
+ *
+ * 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
+ */
+
+/**
* We're now using the HTMLForm object with some customisation to generate the
* Preferences form. This object handles generic submission, CSRF protection,
* layout and other logic in a reusable manner. We subclass it as a PreferencesForm
@@ -24,7 +45,6 @@
* Once fields have been retrieved and validated, submission logic is handed
* over to the tryUISubmit static method of this class.
*/
-
class Preferences {
static $defaultPreferences = null;
static $saveFilters = array(
@@ -84,9 +104,9 @@ class Preferences {
// Already set, no problem
continue;
} elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
- $field->validate( $prefFromUser, $user->mOptions ) === true ) {
+ $field->validate( $prefFromUser, $user->getOptions() ) === true ) {
$info['default'] = $prefFromUser;
- } elseif ( $field->validate( $globalDefault, $user->mOptions ) === true ) {
+ } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
$info['default'] = $globalDefault;
} else {
throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
@@ -252,7 +272,7 @@ class Preferences {
}
// Language
- $languages = Language::getLanguageNames( false );
+ $languages = Language::fetchLanguageNames( null, 'mw' );
if ( !array_key_exists( $wgLanguageCode, $languages ) ) {
$languages[$wgLanguageCode] = $wgLanguageCode;
}
@@ -351,9 +371,13 @@ class Preferences {
$emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
if ( $wgAuth->allowPropChange( 'emailaddress' ) ) {
- $emailAddress .= $emailAddress == '' ? $link : " ($link)";
+ $emailAddress .= $emailAddress == '' ? $link : (
+ $context->msg( 'word-separator' )->plain()
+ . $context->msg( 'parentheses' )->rawParams( $link )->plain()
+ );
}
+
$defaultPreferences['emailaddress'] = array(
'type' => 'info',
'raw' => true,
@@ -361,10 +385,12 @@ class Preferences {
'label-message' => 'youremail',
'section' => 'personal/email',
'help-messages' => $helpMessages,
+ # 'cssclass' chosen below
);
$disableEmailPrefs = false;
+ $emailauthenticationclass = 'mw-email-not-authenticated';
if ( $wgEmailAuthentication ) {
if ( $user->getEmail() ) {
if ( $user->getEmailAuthenticationTimestamp() ) {
@@ -379,6 +405,7 @@ class Preferences {
$emailauthenticated = $context->msg( 'emailauthenticated',
$time, $d, $t )->parse() . '<br />';
$disableEmailPrefs = false;
+ $emailauthenticationclass = 'mw-email-authenticated';
} else {
$disableEmailPrefs = true;
$emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
@@ -386,10 +413,12 @@ class Preferences {
SpecialPage::getTitleFor( 'Confirmemail' ),
$context->msg( 'emailconfirmlink' )->escaped()
) . '<br />';
+ $emailauthenticationclass="mw-email-not-authenticated";
}
} else {
$disableEmailPrefs = true;
$emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
+ $emailauthenticationclass = 'mw-email-none';
}
$defaultPreferences['emailauthentication'] = array(
@@ -398,9 +427,11 @@ class Preferences {
'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;
if ( $wgEnableUserEmail && $user->isAllowed( 'sendemail' ) ) {
$defaultPreferences['disablemail'] = array(
@@ -639,11 +670,6 @@ class Preferences {
);
if ( $wgAllowUserCssPrefs ) {
- $defaultPreferences['highlightbroken'] = array(
- 'type' => 'toggle',
- 'section' => 'rendering/advancedrendering',
- 'label' => $context->msg( 'tog-highlightbroken' )->text(), // Raw HTML
- );
$defaultPreferences['showtoc'] = array(
'type' => 'toggle',
'section' => 'rendering/advancedrendering',
@@ -913,6 +939,7 @@ 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(
@@ -951,7 +978,7 @@ class Preferences {
* @param $defaultPreferences Array
*/
static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- global $wgContLang, $wgEnableMWSuggest, $wgVectorUseSimpleSearch;
+ global $wgContLang, $wgVectorUseSimpleSearch;
## Search #####################################
$defaultPreferences['searchlimit'] = array(
@@ -961,22 +988,21 @@ class Preferences {
'min' => 0,
);
- if ( $wgEnableMWSuggest ) {
- $defaultPreferences['disablesuggest'] = array(
- 'type' => 'toggle',
- 'label-message' => 'mwsuggest-disable',
- 'section' => 'searchoptions/displaysearchoptions',
- );
- }
if ( $wgVectorUseSimpleSearch ) {
$defaultPreferences['vector-simplesearch'] = array(
'type' => 'toggle',
'label-message' => 'vector-simplesearch-preference',
- 'section' => 'searchoptions/displaysearchoptions'
+ 'section' => 'searchoptions/displaysearchoptions',
);
}
+ $defaultPreferences['disablesuggest'] = array(
+ 'type' => 'toggle',
+ 'label-message' => 'mwsuggest-disable',
+ 'section' => 'searchoptions/displaysearchoptions',
+ );
+
$defaultPreferences['searcheverything'] = array(
'type' => 'toggle',
'label-message' => 'searcheverything-enable',
@@ -1428,39 +1454,21 @@ class Preferences {
* Try to set a user's email address.
* This does *not* try to validate the address.
* Caller is responsible for checking $wgAuth.
+ *
+ * @deprecated in 1.20; use User::setEmailWithConfirmation() instead.
* @param $user User
* @param $newaddr string New email address
* @return Array (true on success or Status on failure, info string)
*/
public static function trySetUserEmail( User $user, $newaddr ) {
- global $wgEnableEmail, $wgEmailAuthentication;
- $info = ''; // none
+ wfDeprecated( __METHOD__, '1.20' );
- if ( $wgEnableEmail ) {
- $oldaddr = $user->getEmail();
- if ( ( $newaddr != '' ) && ( $newaddr != $oldaddr ) ) {
- # The user has supplied a new email address on the login page
- # new behaviour: set this new emailaddr from login-page into user database record
- $user->setEmail( $newaddr );
- if ( $wgEmailAuthentication ) {
- # Mail a temporary password to the dirty address.
- # User can come back through the confirmation URL to re-enable email.
- $type = $oldaddr != '' ? 'changed' : 'set';
- $result = $user->sendConfirmationMail( $type );
- if ( !$result->isGood() ) {
- return array( $result, 'mailerror' );
- }
- $info = 'eauth';
- }
- } elseif ( $newaddr != $oldaddr ) { // if the address is the same, don't change it
- $user->setEmail( $newaddr );
- }
- if ( $oldaddr != $newaddr ) {
- wfRunHooks( 'PrefsEmailAudit', array( $user, $oldaddr, $newaddr ) );
- }
+ $result = $user->setEmailWithConfirmation( $newaddr );
+ if ( $result->isGood() ) {
+ return array( true, $result->value );
+ } else {
+ return array( $result, 'mailerror' );
}
-
- return array( true, $info );
}
/**
@@ -1578,7 +1586,7 @@ class PreferencesForm extends HTMLForm {
}
/**
- * Get the <legend> for a given section key. Normally this is the
+ * Get the "<legend>" for a given section key. Normally this is the
* prefs-$key message but we'll allow extensions to override it.
* @param $key string
* @return string
diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php
index 0efe1bdd..5d4b35c1 100644
--- a/includes/PrefixSearch.php
+++ b/includes/PrefixSearch.php
@@ -1,11 +1,31 @@
<?php
/**
- * PrefixSearch - Handles searching prefixes of titles and finding any page
+ * Prefix search of page names.
+ *
+ * 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
+ */
+
+/**
+ * Handles searching prefixes of titles and finding any page
* names that match. Used largely by the OpenSearch implementation.
*
* @ingroup Search
*/
-
class PrefixSearch {
/**
* Do a prefix search of titles and return a list of matching page names.
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
index dbe06d49..ce0e36b0 100644
--- a/includes/ProtectionForm.php
+++ b/includes/ProtectionForm.php
@@ -74,17 +74,17 @@ class ProtectionForm {
$this->disabledAttrib = $this->disabled
? array( 'disabled' => 'disabled' )
: array();
-
+
$this->loadData();
}
-
+
/**
* Loads the current state of protection into the object.
*/
function loadData() {
global $wgRequest, $wgUser;
global $wgRestrictionLevels;
-
+
$this->mCascade = $this->mTitle->areRestrictionsCascading();
$this->mReason = $wgRequest->getText( 'mwProtect-reason' );
@@ -94,7 +94,7 @@ class ProtectionForm {
foreach( $this->mApplicableTypes as $action ) {
// @todo FIXME: This form currently requires individual selections,
// but the db allows multiples separated by commas.
-
+
// Pull the actual restriction from the DB
$this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
@@ -151,7 +151,7 @@ class ProtectionForm {
* Get the expiry time for a given action, by combining the relevant inputs.
*
* @param $action string
- *
+ *
* @return string 14-char timestamp or "infinity", or false if the input was invalid
*/
function getExpiry( $action ) {
@@ -265,7 +265,7 @@ class ProtectionForm {
$reasonstr = $this->mReasonSelection;
if ( $reasonstr != 'other' && $this->mReason != '' ) {
// Entry from drop down menu + additional comment
- $reasonstr .= wfMsgForContent( 'colon-separator' ) . $this->mReason;
+ $reasonstr .= wfMessage( 'colon-separator' )->text() . $this->mReason;
} elseif ( $reasonstr == 'other' ) {
$reasonstr = $this->mReason;
}
@@ -318,10 +318,12 @@ class ProtectionForm {
return false;
}
- if ( $wgRequest->getCheck( 'mwProtectWatch' ) && $wgUser->isLoggedIn() ) {
- WatchAction::doWatch( $this->mTitle, $wgUser );
- } elseif ( $this->mTitle->userIsWatching() ) {
- WatchAction::doUnwatch( $this->mTitle, $wgUser );
+ 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 );
+ }
}
return true;
}
@@ -334,8 +336,14 @@ class ProtectionForm {
function buildForm() {
global $wgUser, $wgLang, $wgOut;
- $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonSelection' );
- $mProtectreason = Xml::label( wfMsg( 'protect-otherreason' ), 'mwProtect-reason' );
+ $mProtectreasonother = Xml::label(
+ wfMessage( 'protectcomment' )->text(),
+ 'wpProtectReasonSelection'
+ );
+ $mProtectreason = Xml::label(
+ wfMessage( 'protect-otherreason' )->text(),
+ 'mwProtect-reason'
+ );
$out = '';
if( !$this->disabled ) {
@@ -346,7 +354,7 @@ class ProtectionForm {
}
$out .= Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'protect-legend' ) ) .
+ Xml::element( 'legend', null, wfMessage( 'protect-legend' )->text() ) .
Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) .
Xml::openElement( 'tbody' );
@@ -360,16 +368,22 @@ class ProtectionForm {
"<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>";
$reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection',
- wfMsgForContent( 'protect-dropdown' ),
- wfMsgForContent( 'protect-otherreason-op' ),
+ wfMessage( 'protect-dropdown' )->inContentLanguage()->text(),
+ wfMessage( 'protect-otherreason-op' )->inContentLanguage()->text(),
$this->mReasonSelection,
'mwProtect-reason', 4 );
- $scExpiryOptions = wfMsgForContent( 'protect-expiry-options' );
+ $scExpiryOptions = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text();
$showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled);
- $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpirySelection-$action" );
- $mProtectother = Xml::label( wfMsg( 'protect-othertime' ), "mwProtect-$action-expires" );
+ $mProtectexpiry = Xml::label(
+ wfMessage( 'protectexpiry' )->text(),
+ "mwProtectExpirySelection-$action"
+ );
+ $mProtectother = Xml::label(
+ wfMessage( 'protect-othertime' )->text(),
+ "mwProtect-$action-expires"
+ );
$expiryFormOptions = '';
if ( $this->mExistingExpiry[$action] && $this->mExistingExpiry[$action] != 'infinity' ) {
@@ -378,13 +392,16 @@ class ProtectionForm {
$t = $wgLang->time( $this->mExistingExpiry[$action], true );
$expiryFormOptions .=
Xml::option(
- wfMsg( 'protect-existing-expiry', $timestamp, $d, $t ),
+ wfMessage( 'protect-existing-expiry', $timestamp, $d, $t )->text(),
'existing',
$this->mExpirySelection[$action] == 'existing'
) . "\n";
}
- $expiryFormOptions .= Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" ) . "\n";
+ $expiryFormOptions .= Xml::option(
+ wfMessage( 'protect-othertime-op' )->text(),
+ "othertime"
+ ) . "\n";
foreach( explode(',', $scExpiryOptions) as $option ) {
if ( strpos($option, ":") === false ) {
$show = $value = $option;
@@ -442,8 +459,12 @@ class ProtectionForm {
$out .= '<tr>
<td></td>
<td class="mw-input">' .
- Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade',
- $this->mCascade, $this->disabledAttrib ) .
+ Xml::checkLabel(
+ wfMessage( 'protect-cascade' )->text(),
+ 'mwProtect-cascade',
+ 'mwProtect-cascade',
+ $this->mCascade, $this->disabledAttrib
+ ) .
"</td>
</tr>\n";
$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
@@ -480,7 +501,7 @@ class ProtectionForm {
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'watchthis' ),
+ Xml::checkLabel( wfMessage( 'watchthis' )->text(),
'mwProtectWatch', 'mwProtectWatch',
$this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) ) .
"</td>
@@ -490,7 +511,10 @@ class ProtectionForm {
<tr>
<td></td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'confirm' ), array( 'id' => 'mw-Protect-submit' ) ) .
+ Xml::submitButton(
+ wfMessage( 'confirm' )->text(),
+ array( 'id' => 'mw-Protect-submit' )
+ ) .
"</td>
</tr>\n";
$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
@@ -501,7 +525,7 @@ class ProtectionForm {
$title = Title::makeTitle( NS_MEDIAWIKI, 'Protect-dropdown' );
$link = Linker::link(
$title,
- wfMsgHtml( 'protect-edit-reasonlist' ),
+ wfMessage( 'protect-edit-reasonlist' )->escaped(),
array(),
array( 'action' => 'edit' )
);
@@ -565,23 +589,23 @@ class ProtectionForm {
*/
private function getOptionLabel( $permission ) {
if( $permission == '' ) {
- return wfMsg( 'protect-default' );
+ return wfMessage( 'protect-default' )->text();
} else {
$msg = wfMessage( "protect-level-{$permission}" );
if( $msg->exists() ) {
return $msg->text();
}
- return wfMsg( 'protect-fallback', $permission );
+ return wfMessage( 'protect-fallback', $permission )->text();
}
}
-
+
function buildCleanupScript() {
global $wgRestrictionLevels, $wgGroupPermissions, $wgOut;
$cascadeableLevels = array();
foreach( $wgRestrictionLevels as $key ) {
if ( ( isset( $wgGroupPermissions[$key]['protect'] ) && $wgGroupPermissions[$key]['protect'] )
- || $key == 'protect'
+ || $key == 'protect'
) {
$cascadeableLevels[] = $key;
}
@@ -606,7 +630,8 @@ class ProtectionForm {
*/
function showLogExtract( &$out ) {
# Show relevant lines from the protection log:
- $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'protect' ) ) );
+ $protectLogPage = new LogPage( 'protect' );
+ $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) );
diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php
index bdab3be2..349789fe 100644
--- a/includes/ProxyTools.php
+++ b/includes/ProxyTools.php
@@ -1,6 +1,21 @@
<?php
/**
- * Functions for dealing with proxies
+ * Functions for dealing with proxies.
+ *
+ * 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
*/
@@ -53,11 +68,20 @@ function wfGetIP() {
* @return bool
*/
function wfIsTrustedProxy( $ip ) {
- global $wgSquidServers, $wgSquidServersNoPurge;
+ $trusted = wfIsConfiguredProxy( $ip );
+ wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) );
+ return $trusted;
+}
+/**
+ * Checks if an IP matches a proxy we've configured.
+ * @param $ip String
+ * @return bool
+ */
+function wfIsConfiguredProxy( $ip ) {
+ global $wgSquidServers, $wgSquidServersNoPurge;
$trusted = in_array( $ip, $wgSquidServers ) ||
in_array( $ip, $wgSquidServersNoPurge );
- wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) );
return $trusted;
}
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
index 69912cbf..ac559dc5 100644
--- a/includes/QueryPage.php
+++ b/includes/QueryPage.php
@@ -1,8 +1,24 @@
<?php
/**
- * Contain a class for special pages
+ * Base code for "query" special 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 SpecialPages
+ * @ingroup SpecialPage
*/
/**
@@ -29,6 +45,7 @@ $wgQueryPages = array(
array( 'MIMEsearchPage', 'MIMEsearch' ),
array( 'MostcategoriesPage', 'Mostcategories' ),
array( 'MostimagesPage', 'Mostimages' ),
+ array( 'MostinterwikisPage', 'Mostinterwikis' ),
array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
array( 'MostlinkedtemplatesPage', 'Mostlinkedtemplates' ),
array( 'MostlinkedPage', 'Mostlinked' ),
@@ -259,6 +276,7 @@ abstract class QueryPage extends SpecialPage {
* Setting this to return true will ensure formatResult() is called
* one more time to make sure that the very last result is formatted
* as well.
+ * @return bool
*/
function tryLastResult() {
return false;
@@ -269,6 +287,7 @@ abstract class QueryPage extends SpecialPage {
*
* @param $limit Integer: limit for SQL statement
* @param $ignoreErrors Boolean: whether to ignore database errors
+ * @return bool|int
*/
function recache( $limit, $ignoreErrors = true ) {
if ( !$this->isCacheable() ) {
@@ -293,7 +312,7 @@ abstract class QueryPage extends SpecialPage {
$res = $this->reallyDoQuery( $limit, false );
$num = false;
if ( $res ) {
- $num = $dbr->numRows( $res );
+ $num = $res->numRows();
# Fetch results
$vals = array();
while ( $res && $row = $dbr->fetchObject( $res ) ) {
@@ -358,7 +377,7 @@ abstract class QueryPage extends SpecialPage {
$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'] = implode( ', ', $order );
+ $options['ORDER BY'] = $order;
}
if ( $limit !== false ) {
$options['LIMIT'] = intval( $limit );
@@ -382,6 +401,7 @@ abstract class QueryPage extends SpecialPage {
/**
* Somewhat deprecated, you probably want to be using execute()
+ * @return ResultWrapper
*/
function doQuery( $offset = false, $limit = false ) {
if ( $this->isCached() && $this->isCacheable() ) {
@@ -413,9 +433,9 @@ abstract class QueryPage extends SpecialPage {
$options['ORDER BY'] = 'qc_value ASC';
}
$res = $dbr->select( 'querycache', array( 'qc_type',
- 'qc_namespace AS namespace',
- 'qc_title AS title',
- 'qc_value AS value' ),
+ 'namespace' => 'qc_namespace',
+ 'title' => 'qc_title',
+ 'value' => 'qc_value' ),
array( 'qc_type' => $this->getName() ),
__METHOD__, $options
);
@@ -435,6 +455,7 @@ abstract class QueryPage extends SpecialPage {
/**
* This is the actual workhorse. It does everything needed to make a
* real, honest-to-gosh query page.
+ * @return int
*/
function execute( $par ) {
global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
@@ -463,10 +484,11 @@ abstract class QueryPage extends SpecialPage {
// TODO: Use doQuery()
if ( !$this->isCached() ) {
- $res = $this->reallyDoQuery( $this->limit, $this->offset );
+ # select one extra row for navigation
+ $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
} else {
- # Get the cached result
- $res = $this->fetchFromCache( $this->limit, $this->offset );
+ # Get the cached result, select one extra row for navigation
+ $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
if ( !$this->listoutput ) {
# Fetch the timestamp of this update
@@ -479,7 +501,7 @@ abstract class QueryPage extends SpecialPage {
$updateddate = $lang->userDate( $ts, $user );
$updatedtime = $lang->userTime( $ts, $user );
$out->addMeta( 'Data-Cache-Time', $ts );
- $out->addInlineScript( "var dataCacheTime = '$ts';" );
+ $out->addJsConfigVars( 'dataCacheTime', $ts );
$out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
} else {
$out->addWikiMsg( 'perfcached', $maxResults );
@@ -488,7 +510,7 @@ abstract class QueryPage extends SpecialPage {
# If updates on this page have been disabled, let the user know
# that the data set won't be refreshed for now
if ( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
- $out->addWikiMsg( 'querypage-no-updates' );
+ $out->wrapWikiMsg( "<div class=\"mw-querypage-no-updates\">\n$1\n</div>", 'querypage-no-updates' );
}
}
}
@@ -505,10 +527,11 @@ abstract class QueryPage extends SpecialPage {
$out->addHTML( $this->getPageHeader() );
if ( $this->numRows > 0 ) {
$out->addHTML( $this->msg( 'showingresults' )->numParams(
- $this->numRows, $this->offset + 1 )->parseAsBlock() );
+ min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
+ $this->offset + 1 )->parseAsBlock() );
# Disable the "next" link when we reach the end
$paging = $this->getLanguage()->viewPrevNext( $this->getTitle( $par ), $this->offset,
- $this->limit, $this->linkParameters(), ( $this->numRows < $this->limit ) );
+ $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
$out->addHTML( '<p>' . $paging . '</p>' );
} else {
# No results to show, so don't bother with "showing X of Y" etc.
@@ -526,7 +549,7 @@ abstract class QueryPage extends SpecialPage {
$this->getSkin(),
$dbr, # Should use a ResultWrapper for this
$res,
- $this->numRows,
+ min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
$this->offset );
# Repeat the paging links at the bottom
@@ -536,7 +559,7 @@ abstract class QueryPage extends SpecialPage {
$out->addHTML( Xml::closeElement( 'div' ) );
- return $this->numRows;
+ return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist
}
/**
@@ -621,6 +644,7 @@ abstract class QueryPage extends SpecialPage {
/**
* Similar to above, but packaging in a syndicated feed instead of a web page
+ * @return bool
*/
function doFeed( $class = '', $limit = 50 ) {
global $wgFeed, $wgFeedClasses;
@@ -660,12 +684,13 @@ abstract class QueryPage extends SpecialPage {
/**
* Override for custom handling. If the titles/links are ok, just do
* feedItemDesc()
+ * @return FeedItem|null
*/
function feedResult( $row ) {
if ( !isset( $row->title ) ) {
return null;
}
- $title = Title::MakeTitle( intval( $row->namespace ), $row->title );
+ $title = Title::makeTitle( intval( $row->namespace ), $row->title );
if ( $title ) {
$date = isset( $row->timestamp ) ? $row->timestamp : '';
$comments = '';
@@ -727,6 +752,10 @@ abstract class WantedQueryPage extends QueryPage {
* Cache page existence for performance
*/
function preprocessResults( $db, $res ) {
+ if ( !$res->numRows() ) {
+ return;
+ }
+
$batch = new LinkBatch;
foreach ( $res as $row ) {
$batch->add( $row->namespace, $row->title );
@@ -734,9 +763,7 @@ abstract class WantedQueryPage extends QueryPage {
$batch->execute();
// Back to start for display
- if ( $db->numRows( $res ) > 0 )
- // If there are no rows we get an error seeking.
- $db->dataSeek( $res, 0 );
+ $res->seek( 0 );
}
/**
@@ -745,6 +772,7 @@ abstract class WantedQueryPage extends QueryPage {
* kluge for Special:WantedFiles, which may contain false
* positives for files that exist e.g. in a shared repo (bug
* 6220).
+ * @return bool
*/
function forceExistenceCheck() {
return false;
diff --git a/includes/RecentChange.php b/includes/RecentChange.php
index ca0ed955..332d0390 100644
--- a/includes/RecentChange.php
+++ b/includes/RecentChange.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Utility class for creating and accessing recent change 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
+ */
/**
* Utility class for creating new RC entries
@@ -51,6 +71,11 @@ class RecentChange {
var $mTitle = false;
/**
+ * @var User
+ */
+ private $mPerformer = false;
+
+ /**
* @var Title
*/
var $mMovedToTitle = false;
@@ -88,14 +113,7 @@ class RecentChange {
* @return RecentChange
*/
public static function newFromId( $rcid ) {
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'recentchanges', '*', array( 'rc_id' => $rcid ), __METHOD__ );
- if( $res && $dbr->numRows( $res ) > 0 ) {
- $row = $dbr->fetchObject( $res );
- return self::newFromRow( $row );
- } else {
- return null;
- }
+ return self::newFromConds( array( 'rc_id' => $rcid ), __METHOD__ );
}
/**
@@ -107,18 +125,12 @@ class RecentChange {
*/
public static function newFromConds( $conds, $fname = __METHOD__ ) {
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select(
- 'recentchanges',
- '*',
- $conds,
- $fname
- );
- if( $res instanceof ResultWrapper && $res->numRows() > 0 ) {
- $row = $res->fetchObject();
- $res->free();
+ $row = $dbr->selectRow( 'recentchanges', '*', $conds, $fname );
+ if ( $row !== false ) {
return self::newFromRow( $row );
+ } else {
+ return null;
}
- return null;
}
# Accessors
@@ -151,7 +163,7 @@ class RecentChange {
}
/**
- * @return bool|\Title
+ * @return bool|Title
*/
public function getMovedToTitle() {
if( $this->mMovedToTitle === false ) {
@@ -162,11 +174,27 @@ class RecentChange {
}
/**
+ * Get the User object of the person who performed this change.
+ *
+ * @return User
+ */
+ public function getPerformer() {
+ if ( $this->mPerformer === false ) {
+ if ( $this->mAttribs['rc_user'] ) {
+ $this->mPerformer = User::newFromID( $this->mAttribs['rc_user'] );
+ } else {
+ $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
+ }
+ }
+ return $this->mPerformer;
+ }
+
+ /**
* Writes the data in this object to the database
* @param $noudp bool
*/
public function save( $noudp = false ) {
- global $wgLocalInterwiki, $wgPutIPinRC, $wgContLang;
+ global $wgLocalInterwiki, $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang;
$dbw = wfGetDB( DB_MASTER );
if( !is_array($this->mExtra) ) {
@@ -211,26 +239,19 @@ class RecentChange {
}
# E-mail notifications
- global $wgUseEnotif, $wgShowUpdatedMarker, $wgUser;
if( $wgUseEnotif || $wgShowUpdatedMarker ) {
- // Users
- if( $this->mAttribs['rc_user'] ) {
- $editor = ($wgUser->getId() == $this->mAttribs['rc_user']) ?
- $wgUser : User::newFromID( $this->mAttribs['rc_user'] );
- // Anons
- } else {
- $editor = ($wgUser->getName() == $this->mAttribs['rc_user_text']) ?
- $wgUser : User::newFromName( $this->mAttribs['rc_user_text'], false );
+ $editor = $this->getPerformer();
+ $title = $this->getTitle();
+
+ 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'] );
}
- $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
-
- # @todo FIXME: This would be better as an extension hook
- $enotif = new EmailNotification();
- $status = $enotif->notifyOnPageChange( $editor, $title,
- $this->mAttribs['rc_timestamp'],
- $this->mAttribs['rc_comment'],
- $this->mAttribs['rc_minor'],
- $this->mAttribs['rc_last_oldid'] );
}
}
@@ -339,7 +360,7 @@ class RecentChange {
// Actually set the 'patrolled' flag in RC
$this->reallyMarkPatrolled();
// Log this patrol event
- PatrolLog::record( $this, $auto );
+ PatrolLog::record( $this, $auto, $user );
wfRunHooks( 'MarkPatrolledComplete', array($this->getAttribute('rc_id'), &$user, false) );
return array();
}
@@ -383,13 +404,9 @@ class RecentChange {
*/
public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId,
$lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0 ) {
- global $wgRequest;
- if( !$ip ) {
- $ip = $wgRequest->getIP();
- if( !$ip ) $ip = '';
- }
-
$rc = new RecentChange;
+ $rc->mTitle = $title;
+ $rc->mPerformer = $user;
$rc->mAttribs = array(
'rc_timestamp' => $timestamp,
'rc_cur_time' => $timestamp,
@@ -406,7 +423,7 @@ class RecentChange {
'rc_bot' => $bot ? 1 : 0,
'rc_moved_to_ns' => 0,
'rc_moved_to_title' => '',
- 'rc_ip' => $ip,
+ 'rc_ip' => self::checkIPAddress( $ip ),
'rc_patrolled' => intval($patrol),
'rc_new' => 0, # obsolete
'rc_old_len' => $oldSize,
@@ -447,15 +464,9 @@ class RecentChange {
*/
public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot,
$ip='', $size=0, $newId=0, $patrol=0 ) {
- global $wgRequest;
- if( !$ip ) {
- $ip = $wgRequest->getIP();
- if( !$ip ) {
- $ip = '';
- }
- }
-
$rc = new RecentChange;
+ $rc->mTitle = $title;
+ $rc->mPerformer = $user;
$rc->mAttribs = array(
'rc_timestamp' => $timestamp,
'rc_cur_time' => $timestamp,
@@ -472,7 +483,7 @@ class RecentChange {
'rc_bot' => $bot ? 1 : 0,
'rc_moved_to_ns' => 0,
'rc_moved_to_title' => '',
- 'rc_ip' => $ip,
+ 'rc_ip' => self::checkIPAddress( $ip ),
'rc_patrolled' => intval($patrol),
'rc_new' => 1, # obsolete
'rc_old_len' => 0,
@@ -506,10 +517,11 @@ class RecentChange {
* @param $logComment
* @param $params
* @param $newId int
+ * @param $actionCommentIRC string
* @return bool
*/
- public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='', $type,
- $action, $target, $logComment, $params, $newId=0 )
+ public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
+ $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='' )
{
global $wgLogRestrictions;
# Don't add private logs to RC!
@@ -517,7 +529,7 @@ class RecentChange {
return false;
}
$rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
- $target, $logComment, $params, $newId );
+ $target, $logComment, $params, $newId, $actionCommentIRC );
$rc->save();
return true;
}
@@ -534,19 +546,16 @@ class RecentChange {
* @param $logComment
* @param $params
* @param $newId int
+ * @param $actionCommentIRC string
* @return RecentChange
*/
- public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip='',
- $type, $action, $target, $logComment, $params, $newId=0 ) {
+ public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
+ $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='' ) {
global $wgRequest;
- if( !$ip ) {
- $ip = $wgRequest->getIP();
- if( !$ip ) {
- $ip = '';
- }
- }
$rc = new RecentChange;
+ $rc->mTitle = $target;
+ $rc->mPerformer = $user;
$rc->mAttribs = array(
'rc_timestamp' => $timestamp,
'rc_cur_time' => $timestamp,
@@ -563,7 +572,7 @@ class RecentChange {
'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
'rc_moved_to_ns' => 0,
'rc_moved_to_title' => '',
- 'rc_ip' => $ip,
+ 'rc_ip' => self::checkIPAddress( $ip ),
'rc_patrolled' => 1,
'rc_new' => 0, # obsolete
'rc_old_len' => null,
@@ -574,10 +583,12 @@ class RecentChange {
'rc_log_action' => $action,
'rc_params' => $params
);
+
$rc->mExtra = array(
'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => 0,
'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
+ 'actionCommentIRC' => $actionCommentIRC
);
return $rc;
}
@@ -675,6 +686,8 @@ class RecentChange {
$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();
@@ -706,6 +719,7 @@ class RecentChange {
} elseif($szdiff >= 0) {
$szdiff = '+' . $szdiff ;
}
+ // @todo i18n with parentheses in content language?
$szdiff = '(' . $szdiff . ')' ;
} else {
$szdiff = '';
@@ -715,15 +729,15 @@ class RecentChange {
if ( $this->mAttribs['rc_type'] == RC_LOG ) {
$targetText = $this->getTitle()->getPrefixedText();
- $comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $this->mExtra['actionComment'] ) );
+ $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_new'] && $wgUseNPPatrol ) ) {
+ if ( !$this->mAttribs['rc_patrolled'] && ( $wgUseRCPatrol || $this->mAttribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
$flag .= '!';
}
- $flag .= ( $this->mAttribs['rc_new'] ? "N" : "" ) . ( $this->mAttribs['rc_minor'] ? "M" : "" ) . ( $this->mAttribs['rc_bot'] ? "B" : "" );
+ $flag .= ( $this->mAttribs['rc_type'] == RC_NEW ? "N" : "" ) . ( $this->mAttribs['rc_minor'] ? "M" : "" ) . ( $this->mAttribs['rc_bot'] ? "B" : "" );
}
if ( $wgRC2UDPInterwikiPrefix === true && $wgLocalInterwiki !== false ) {
@@ -766,4 +780,18 @@ class RecentChange {
}
return ChangesList::showCharacterDifference( $old, $new );
}
+
+ private static function checkIPAddress( $ip ) {
+ global $wgRequest;
+ if ( $ip ) {
+ if ( !IP::isIPAddress( $ip ) ) {
+ throw new MWException( "Attempt to write \"" . $ip . "\" as an IP address into recent changes" );
+ }
+ } else {
+ $ip = $wgRequest->getIP();
+ if( !$ip )
+ $ip = '';
+ }
+ return $ip;
+ }
}
diff --git a/includes/Revision.php b/includes/Revision.php
index 445211c9..20cc8f58 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -1,9 +1,29 @@
<?php
+/**
+ * Representation of a page version.
+ *
+ * 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 Revision {
+class Revision implements IDBAccessObject {
protected $mId;
protected $mPage;
protected $mUserText;
@@ -21,13 +41,14 @@ class Revision {
protected $mTitle;
protected $mCurrent;
+ // Revision deletion constants
const DELETED_TEXT = 1;
const DELETED_COMMENT = 2;
const DELETED_USER = 4;
const DELETED_RESTRICTED = 8;
- // Convenience field
- const SUPPRESSED_USER = 12;
- // Audience options for Revision::getText()
+ const SUPPRESSED_USER = 12; // convenience
+
+ // Audience options for accessors
const FOR_PUBLIC = 1;
const FOR_THIS_USER = 2;
const RAW = 3;
@@ -36,11 +57,16 @@ class Revision {
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
*
+ * $flags include:
+ * Revision::READ_LATEST : Select the data from the master
+ * Revision::READ_LOCKING : Select & lock the data from the master
+ *
* @param $id Integer
+ * @param $flags Integer (optional)
* @return Revision or null
*/
- public static function newFromId( $id ) {
- return Revision::newFromConds( array( 'rev_id' => intval( $id ) ) );
+ public static function newFromId( $id, $flags = 0 ) {
+ return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
}
/**
@@ -48,11 +74,16 @@ class Revision {
* that's attached to a given title. If not attached
* to that title, will return null.
*
+ * $flags include:
+ * Revision::READ_LATEST : Select the data from the master
+ * Revision::READ_LOCKING : Select & lock the data from the master
+ *
* @param $title Title
* @param $id Integer (optional)
+ * @param $flags Integer Bitfield (optional)
* @return Revision or null
*/
- public static function newFromTitle( $title, $id = 0 ) {
+ public static function newFromTitle( $title, $id = 0, $flags = null ) {
$conds = array(
'page_namespace' => $title->getNamespace(),
'page_title' => $title->getDBkey()
@@ -60,19 +91,13 @@ class Revision {
if ( $id ) {
// Use the specified ID
$conds['rev_id'] = $id;
- } elseif ( wfGetLB()->getServerCount() > 1 ) {
- // Get the latest revision ID from the master
- $dbw = wfGetDB( DB_MASTER );
- $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
- if ( $latest === false ) {
- return null; // page does not exist
- }
- $conds['rev_id'] = $latest;
} 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 Revision::newFromConds( $conds );
+ return self::newFromConds( $conds, (int)$flags );
}
/**
@@ -80,26 +105,26 @@ class Revision {
* that's attached to a given page ID.
* Returns null if no such revision can be found.
*
+ * $flags include:
+ * Revision::READ_LATEST : Select the data from the master
+ * Revision::READ_LOCKING : Select & lock the data from the master
+ *
* @param $revId Integer
* @param $pageId Integer (optional)
+ * @param $flags Integer Bitfield (optional)
* @return Revision or null
*/
- public static function newFromPageId( $pageId, $revId = 0 ) {
+ public static function newFromPageId( $pageId, $revId = 0, $flags = null ) {
$conds = array( 'page_id' => $pageId );
if ( $revId ) {
$conds['rev_id'] = $revId;
- } elseif ( wfGetLB()->getServerCount() > 1 ) {
- // Get the latest revision ID from the master
- $dbw = wfGetDB( DB_MASTER );
- $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
- if ( $latest === false ) {
- return null; // page does not exist
- }
- $conds['rev_id'] = $latest;
} 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 Revision::newFromConds( $conds );
+ return self::newFromConds( $conds, (int)$flags );
}
/**
@@ -155,7 +180,7 @@ class Revision {
* @return Revision or null
*/
public static function loadFromId( $db, $id ) {
- return Revision::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
+ return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
}
/**
@@ -175,7 +200,7 @@ class Revision {
} else {
$conds[] = 'rev_id=page_latest';
}
- return Revision::loadFromConds( $db, $conds );
+ return self::loadFromConds( $db, $conds );
}
/**
@@ -194,7 +219,7 @@ class Revision {
} else {
$matchId = 'page_latest';
}
- return Revision::loadFromConds( $db,
+ return self::loadFromConds( $db,
array( "rev_id=$matchId",
'page_namespace' => $title->getNamespace(),
'page_title' => $title->getDBkey() )
@@ -212,7 +237,7 @@ class Revision {
* @return Revision or null
*/
public static function loadFromTimestamp( $db, $title, $timestamp ) {
- return Revision::loadFromConds( $db,
+ return self::loadFromConds( $db,
array( 'rev_timestamp' => $db->timestamp( $timestamp ),
'page_namespace' => $title->getNamespace(),
'page_title' => $title->getDBkey() )
@@ -223,14 +248,17 @@ class Revision {
* Given a set of conditions, fetch a revision.
*
* @param $conditions Array
+ * @param $flags integer (optional)
* @return Revision or null
*/
- public static function newFromConds( $conditions ) {
- $db = wfGetDB( DB_SLAVE );
- $rev = Revision::loadFromConds( $db, $conditions );
- if( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
- $dbw = wfGetDB( DB_MASTER );
- $rev = Revision::loadFromConds( $dbw, $conditions );
+ private static function newFromConds( $conditions, $flags = 0 ) {
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
+ $rev = self::loadFromConds( $db, $conditions, $flags );
+ if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
+ if ( !( $flags & self::READ_LATEST ) ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $rev = self::loadFromConds( $dbw, $conditions, $flags );
+ }
}
return $rev;
}
@@ -241,10 +269,11 @@ class Revision {
*
* @param $db DatabaseBase
* @param $conditions Array
+ * @param $flags integer (optional)
* @return Revision or null
*/
- private static function loadFromConds( $db, $conditions ) {
- $res = Revision::fetchFromConds( $db, $conditions );
+ private static function loadFromConds( $db, $conditions, $flags = 0 ) {
+ $res = self::fetchFromConds( $db, $conditions, $flags );
if( $res ) {
$row = $res->fetchObject();
if( $row ) {
@@ -265,7 +294,7 @@ class Revision {
* @return ResultWrapper
*/
public static function fetchRevision( $title ) {
- return Revision::fetchFromConds(
+ return self::fetchFromConds(
wfGetDB( DB_SLAVE ),
array( 'rev_id=page_latest',
'page_namespace' => $title->getNamespace(),
@@ -280,20 +309,25 @@ class Revision {
*
* @param $db DatabaseBase
* @param $conditions Array
+ * @param $flags integer (optional)
* @return ResultWrapper
*/
- private static function fetchFromConds( $db, $conditions ) {
+ private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
$fields = array_merge(
self::selectFields(),
self::selectPageFields(),
self::selectUserFields()
);
+ $options = array( 'LIMIT' => 1 );
+ if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
+ $options[] = 'FOR UPDATE';
+ }
return $db->select(
array( 'revision', 'page', 'user' ),
$fields,
$conditions,
__METHOD__,
- array( 'LIMIT' => 1 ),
+ $options,
array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
);
}
@@ -321,6 +355,7 @@ class Revision {
/**
* Return the list of revision fields that should be selected to create
* a new revision.
+ * @return array
*/
public static function selectFields() {
return array(
@@ -342,6 +377,7 @@ class Revision {
/**
* Return the list of text fields that should be selected to read the
* revision text
+ * @return array
*/
public static function selectTextFields() {
return array(
@@ -352,24 +388,51 @@ class Revision {
/**
* Return the list of page fields that should be selected from page table
+ * @return array
*/
public static function selectPageFields() {
return array(
'page_namespace',
'page_title',
'page_id',
- 'page_latest'
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
);
}
/**
* Return the list of user fields that should be selected from user table
+ * @return array
*/
public static function selectUserFields() {
return array( 'user_name' );
}
/**
+ * Do a batched query to get the parent revision lengths
+ * @param $db DatabaseBase
+ * @param $revIds Array
+ * @return array
+ */
+ public static function getParentLengths( $db, array $revIds ) {
+ $revLens = array();
+ if ( !$revIds ) {
+ return $revLens; // empty
+ }
+ wfProfileIn( __METHOD__ );
+ $res = $db->select( 'revision',
+ array( 'rev_id', 'rev_len' ),
+ array( 'rev_id' => $revIds ),
+ __METHOD__ );
+ foreach ( $res as $row ) {
+ $revLens[$row->rev_id] = $row->rev_len;
+ }
+ wfProfileOut( __METHOD__ );
+ return $revLens;
+ }
+
+ /**
* Constructor
*
* @param $row Mixed: either a database row or an array
@@ -469,7 +532,7 @@ class Revision {
/**
* Get revision ID
*
- * @return Integer
+ * @return Integer|null
*/
public function getId() {
return $this->mId;
@@ -488,7 +551,7 @@ class Revision {
/**
* Get text row ID
*
- * @return Integer
+ * @return Integer|null
*/
public function getTextId() {
return $this->mTextId;
@@ -497,7 +560,7 @@ class Revision {
/**
* Get parent revision ID (the original previous page revision)
*
- * @return Integer
+ * @return Integer|null
*/
public function getParentId() {
return $this->mParentId;
@@ -506,7 +569,7 @@ class Revision {
/**
* Returns the length of the text in this revision, or null if unknown.
*
- * @return Integer
+ * @return Integer|null
*/
public function getSize() {
return $this->mSize;
@@ -515,30 +578,34 @@ class Revision {
/**
* Returns the base36 sha1 of the text in this revision, or null if unknown.
*
- * @return String
+ * @return String|null
*/
public function getSha1() {
return $this->mSha1;
}
/**
- * Returns the title of the page associated with this entry.
+ * Returns the title of the page associated with this entry or null.
+ *
+ * Will do a query, when title is not set and id is given.
*
- * @return Title
+ * @return Title|null
*/
public function getTitle() {
if( isset( $this->mTitle ) ) {
return $this->mTitle;
}
- $dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow(
- array( 'page', 'revision' ),
- self::selectPageFields(),
- array( 'page_id=rev_page',
- 'rev_id' => $this->mId ),
- __METHOD__ );
- if ( $row ) {
- $this->mTitle = Title::newFromRow( $row );
+ if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow(
+ array( 'page', 'revision' ),
+ self::selectPageFields(),
+ array( 'page_id=rev_page',
+ 'rev_id' => $this->mId ),
+ __METHOD__ );
+ if ( $row ) {
+ $this->mTitle = Title::newFromRow( $row );
+ }
}
return $this->mTitle;
}
@@ -555,7 +622,7 @@ class Revision {
/**
* Get the page ID
*
- * @return Integer
+ * @return Integer|null
*/
public function getPage() {
return $this->mPage;
@@ -568,7 +635,7 @@ class Revision {
*
* @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 ID regardless of permissions
* @param $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
@@ -600,7 +667,7 @@ class Revision {
*
* @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
@@ -640,7 +707,7 @@ class Revision {
*
* @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
@@ -718,7 +785,7 @@ class Revision {
*
* @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
@@ -781,7 +848,7 @@ class Revision {
if( $this->getTitle() ) {
$prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
if( $prev ) {
- return Revision::newFromTitle( $this->getTitle(), $prev );
+ return self::newFromTitle( $this->getTitle(), $prev );
}
}
return null;
@@ -796,7 +863,7 @@ class Revision {
if( $this->getTitle() ) {
$next = $this->getTitle()->getNextRevisionID( $this->getId() );
if ( $next ) {
- return Revision::newFromTitle( $this->getTitle(), $next );
+ return self::newFromTitle( $this->getTitle(), $next );
}
}
return null;
@@ -926,7 +993,7 @@ class Revision {
$text = gzdeflate( $text );
$flags[] = 'gzip';
} else {
- wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
+ wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
}
}
return implode( ',', $flags );
@@ -945,7 +1012,7 @@ class Revision {
wfProfileIn( __METHOD__ );
$data = $this->mText;
- $flags = Revision::compressRevisionText( $data );
+ $flags = self::compressRevisionText( $data );
# Write to external storage if required
if( $wgDefaultExternalStore ) {
@@ -995,7 +1062,7 @@ class Revision {
? $this->getPreviousRevisionId( $dbw )
: $this->mParentId,
'rev_sha1' => is_null( $this->mSha1 )
- ? Revision::base36Sha1( $this->mText )
+ ? self::base36Sha1( $this->mText )
: $this->mSha1
), __METHOD__
);
@@ -1096,7 +1163,8 @@ class Revision {
$current = $dbw->selectRow(
array( 'page', 'revision' ),
- array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' ),
+ array( 'page_latest', 'page_namespace', 'page_title',
+ 'rev_text_id', 'rev_len', 'rev_sha1' ),
array(
'page_id' => $pageId,
'page_latest=rev_id',
@@ -1113,6 +1181,7 @@ class Revision {
'len' => $current->rev_len,
'sha1' => $current->rev_sha1
) );
+ $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
} else {
$revision = null;
}
@@ -1181,7 +1250,7 @@ class Revision {
$id = 0;
}
$conds = array( 'rev_id' => $id );
- $conds['rev_page'] = $title->getArticleId();
+ $conds['rev_page'] = $title->getArticleID();
$timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
# Not in slave, try master
@@ -1199,7 +1268,7 @@ class Revision {
* @return Integer
*/
static function countByPageId( $db, $id ) {
- $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
+ $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ),
array( 'rev_page' => $id ), __METHOD__ );
if( $row ) {
return $row->revCount;
@@ -1215,10 +1284,48 @@ class Revision {
* @return Integer
*/
static function countByTitle( $db, $title ) {
- $id = $title->getArticleId();
+ $id = $title->getArticleID();
if( $id ) {
- return Revision::countByPageId( $db, $id );
+ return self::countByPageId( $db, $id );
}
return 0;
}
-}
+
+ /**
+ * Check if no edits were made by other users since
+ * the time a user started editing the page. Limit to
+ * 50 revisions for the sake of performance.
+ *
+ * @since 1.20
+ *
+ * @param DatabaseBase|int $db the Database to perform the check on. May be given as a Database object or
+ * a database identifier usable with wfGetDB.
+ * @param int $pageId the ID of the page in question
+ * @param int $userId the ID of the user in question
+ * @param string $since look at edits since this time
+ *
+ * @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 ( is_int( $db ) ) {
+ $db = wfGetDB( $db );
+ }
+
+ $res = $db->select( 'revision',
+ 'rev_user',
+ array(
+ 'rev_page' => $pageId,
+ 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
+ foreach ( $res as $row ) {
+ if ( $row->rev_user != $userId ) {
+ return false;
+ }
+ }
+ return true;
+ }
+} \ No newline at end of file
diff --git a/includes/RevisionList.php b/includes/RevisionList.php
index 814e2dfa..3c5cfa8e 100644
--- a/includes/RevisionList.php
+++ b/includes/RevisionList.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Holders of revision list for a single page
+ *
+ * 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
+ */
+
+/**
* List for revision table items for a single page
*/
abstract class RevisionListBase extends ContextSource {
@@ -31,6 +52,7 @@ abstract class RevisionListBase extends ContextSource {
/**
* Get the internal type name of this list. Equal to the table name.
* Override this function.
+ * @return null
*/
public function getType() {
return null;
@@ -80,6 +102,7 @@ abstract class RevisionListBase extends ContextSource {
/**
* Get the number of items in the list.
+ * @return int
*/
public function length() {
if( !$this->res ) {
@@ -124,6 +147,7 @@ abstract class RevisionItemBase {
/**
* Get the DB field name associated with the ID list.
* Override this function.
+ * @return null
*/
public function getIdField() {
return null;
@@ -132,6 +156,7 @@ abstract class RevisionItemBase {
/**
* Get the DB field name storing timestamps.
* Override this function.
+ * @return bool
*/
public function getTimestampField() {
return false;
@@ -140,6 +165,7 @@ abstract class RevisionItemBase {
/**
* Get the DB field name storing user ids.
* Override this function.
+ * @return bool
*/
public function getAuthorIdField() {
return false;
@@ -148,6 +174,7 @@ abstract class RevisionItemBase {
/**
* Get the DB field name storing user names.
* Override this function.
+ * @return bool
*/
public function getAuthorNameField() {
return false;
@@ -155,6 +182,7 @@ abstract class RevisionItemBase {
/**
* Get the ID, as it would appear in the ids URL parameter
+ * @return
*/
public function getId() {
$field = $this->getIdField();
@@ -163,6 +191,7 @@ abstract class RevisionItemBase {
/**
* Get the date, formatted in user's languae
+ * @return String
*/
public function formatDate() {
return $this->list->getLanguage()->userDate( $this->getTimestamp(),
@@ -171,6 +200,7 @@ abstract class RevisionItemBase {
/**
* Get the time, formatted in user's languae
+ * @return String
*/
public function formatTime() {
return $this->list->getLanguage()->userTime( $this->getTimestamp(),
@@ -179,6 +209,7 @@ abstract class RevisionItemBase {
/**
* Get the timestamp in MW 14-char form
+ * @return Mixed
*/
public function getTimestamp() {
$field = $this->getTimestampField();
@@ -187,6 +218,7 @@ abstract class RevisionItemBase {
/**
* Get the author user ID
+ * @return int
*/
public function getAuthorId() {
$field = $this->getAuthorIdField();
@@ -195,6 +227,7 @@ abstract class RevisionItemBase {
/**
* Get the author user name
+ * @return string
*/
public function getAuthorName() {
$field = $this->getAuthorNameField();
@@ -212,7 +245,7 @@ abstract class RevisionItemBase {
abstract public function canViewContent();
/**
- * Get the HTML of the list item. Should be include <li></li> tags.
+ * Get the HTML of the list item. Should be include "<li></li>" tags.
* This is used to show the list in HTML form, by the special page.
*/
abstract public function getHTML();
@@ -258,7 +291,7 @@ class RevisionItem extends RevisionItemBase {
public function __construct( $list, $row ) {
parent::__construct( $list, $row );
$this->revision = new Revision( $row );
- $this->context = $list->context;
+ $this->context = $list->getContext();
}
public function getIdField() {
@@ -292,6 +325,7 @@ class RevisionItem extends RevisionItemBase {
/**
* Get the HTML link to the revision text.
* Overridden by RevDel_ArchiveItem.
+ * @return string
*/
protected function getRevisionLink() {
$date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true );
@@ -312,15 +346,16 @@ class RevisionItem extends RevisionItemBase {
/**
* Get the HTML link to the diff.
* Overridden by RevDel_ArchiveItem
+ * @return string
*/
protected function getDiffLink() {
if ( $this->isDeleted() && !$this->canViewContent() ) {
- return wfMsgHtml('diff');
+ return $this->context->msg( 'diff' )->escaped();
} else {
return
Linker::link(
$this->list->title,
- wfMsgHtml('diff'),
+ $this->context->msg( 'diff' )->escaped(),
array(),
array(
'diff' => $this->revision->getId(),
@@ -336,13 +371,14 @@ class RevisionItem extends RevisionItemBase {
}
public function getHTML() {
- $difflink = $this->getDiffLink();
+ $difflink = $this->context->msg( 'parentheses' )
+ ->rawParams( $this->getDiffLink() )->escaped();
$revlink = $this->getRevisionLink();
$userlink = Linker::revUserLink( $this->revision );
$comment = Linker::revComment( $this->revision );
if ( $this->isDeleted() ) {
$revlink = "<span class=\"history-deleted\">$revlink</span>";
}
- return "<li>($difflink) $revlink $userlink $comment</li>";
+ return "<li>$difflink $revlink $userlink $comment</li>";
}
}
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
index 196abd9f..b443ce14 100644
--- a/includes/Sanitizer.php
+++ b/includes/Sanitizer.php
@@ -1,6 +1,6 @@
<?php
/**
- * XHTML sanitizer for MediaWiki
+ * XHTML sanitizer for %MediaWiki.
*
* Copyright © 2002-2005 Brion Vibber <brion@pobox.com> et al
* http://www.mediawiki.org/
@@ -374,7 +374,7 @@ class Sanitizer {
if ( !$staticInitialised ) {
$htmlpairsStatic = array( # Tags that must be closed
- 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
+ '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',
@@ -613,102 +613,6 @@ class Sanitizer {
}
/**
- * Take an array of attribute names and values and fix some deprecated values
- * for the given element type.
- * This does not validate properties, so you should ensure that you call
- * validateTagAttributes AFTER this to ensure that the resulting style rule
- * this may add is safe.
- *
- * - Converts most presentational attributes like align into inline css
- *
- * @param $attribs Array
- * @param $element String
- * @return Array
- */
- static function fixDeprecatedAttributes( $attribs, $element ) {
- global $wgHtml5, $wgCleanupPresentationalAttributes;
-
- // presentational attributes were removed from html5, we can leave them
- // in when html5 is turned off
- if ( !$wgHtml5 || !$wgCleanupPresentationalAttributes ) {
- return $attribs;
- }
-
- $table = array( 'table' );
- $cells = array( 'td', 'th' );
- $colls = array( 'col', 'colgroup' );
- $tblocks = array( 'tbody', 'tfoot', 'thead' );
- $h = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' );
-
- $presentationalAttribs = array(
- 'align' => array( 'text-align', array_merge( array( 'caption', 'hr', 'div', 'p', 'tr' ), $table, $cells, $colls, $tblocks, $h ) ),
- 'clear' => array( 'clear', array( 'br' ) ),
- 'height' => array( 'height', $cells ),
- 'nowrap' => array( 'white-space', $cells ),
- 'size' => array( 'height', array( 'hr' ) ),
- 'type' => array( 'list-style-type', array( 'li', 'ol', 'ul' ) ),
- 'valign' => array( 'vertical-align', array_merge( $cells, $colls, $tblocks ) ),
- 'width' => array( 'width', array_merge( array( 'hr', 'pre' ), $table, $cells, $colls ) ),
- );
-
- // Ensure that any upper case or mixed case attributes are converted to lowercase
- foreach ( $attribs as $attribute => $value ) {
- if ( $attribute !== strtolower( $attribute ) && array_key_exists( strtolower( $attribute ), $presentationalAttribs ) ) {
- $attribs[strtolower( $attribute )] = $value;
- unset( $attribs[$attribute] );
- }
- }
-
- $style = "";
- foreach ( $presentationalAttribs as $attribute => $info ) {
- list( $property, $elements ) = $info;
-
- // Skip if this attribute is not relevant to this element
- if ( !in_array( $element, $elements ) ) {
- continue;
- }
-
- // Skip if the attribute is not used
- if ( !array_key_exists( $attribute, $attribs ) ) {
- continue;
- }
-
- $value = $attribs[$attribute];
-
- // For nowrap the value should be nowrap instead of whatever text is in the value
- if ( $attribute === 'nowrap' ) {
- $value = 'nowrap';
- }
-
- // clear="all" is clear: both; in css
- if ( $attribute === 'clear' && strtolower( $value ) === 'all' ) {
- $value = 'both';
- }
-
- // Size based properties should have px applied to them if they have no unit
- if ( in_array( $attribute, array( 'height', 'width', 'size' ) ) ) {
- if ( preg_match( '/^[\d.]+$/', $value ) ) {
- $value = "{$value}px";
- }
- }
-
- $style .= " $property: $value;";
-
- unset( $attribs[$attribute] );
- }
-
- if ( $style ) {
- // Prepend our style rules so that they can be overridden by user css
- if ( isset($attribs['style']) ) {
- $style .= " " . $attribs['style'];
- }
- $attribs['style'] = trim($style);
- }
-
- return $attribs;
- }
-
- /**
* Take an array of attribute names and values and normalize or discard
* illegal values for the given element type.
*
@@ -956,7 +860,6 @@ class Sanitizer {
}
$decoded = Sanitizer::decodeTagAttributes( $text );
- $decoded = Sanitizer::fixDeprecatedAttributes( $decoded, $element );
$stripped = Sanitizer::validateTagAttributes( $decoded, $element );
$attribs = array();
@@ -1016,7 +919,7 @@ class Sanitizer {
# Stupid hack
$encValue = preg_replace_callback(
- '/(' . wfUrlProtocols() . ')/',
+ '/((?i)' . wfUrlProtocols() . ')/',
array( 'Sanitizer', 'armorLinksCallback' ),
$encValue );
return $encValue;
@@ -1243,7 +1146,7 @@ class Sanitizer {
* a. named char refs can only be &lt; &gt; &amp; &quot;, others are
* numericized (this way we're well-formed even without a DTD)
* b. any numeric char refs must be legal chars, not invalid or forbidden
- * c. use &#x, not &#X
+ * c. use lower cased "&#x", not "&#X"
* d. fix or reject non-valid attributes
*
* @param $text String
@@ -1411,7 +1314,7 @@ class Sanitizer {
/**
* If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
* return the UTF-8 encoding of that character. Otherwise, returns
- * pseudo-entity source (eg &foo;)
+ * pseudo-entity source (eg "&foo;")
*
* @param $name String
* @return String
@@ -1611,6 +1514,10 @@ class Sanitizer {
# 'title' may not be 100% valid here; it's XHTML
# http://www.w3.org/TR/REC-MathML/
'math' => array( 'class', 'style', 'id', 'title' ),
+
+ # HTML 5 section 4.6
+ 'bdi' => $common,
+
);
return $whitelist;
}
diff --git a/includes/ScopedPHPTimeout.php b/includes/ScopedPHPTimeout.php
new file mode 100644
index 00000000..d1493c30
--- /dev/null
+++ b/includes/ScopedPHPTimeout.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Expansion of the PHP execution time limit feature for a function call.
+ *
+ * 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 to expand PHP execution time for a function call.
+ * Use this when performing changes that should not be interrupted.
+ *
+ * On construction, set_time_limit() is called and set to $seconds.
+ * If the client aborts the connection, PHP will continue to run.
+ * When the object goes out of scope, the timer is restarted, with
+ * the original time limit minus the time the object existed.
+ */
+class ScopedPHPTimeout {
+ protected $startTime; // float; seconds
+ protected $oldTimeout; // integer; seconds
+ protected $oldIgnoreAbort; // boolean
+
+ protected static $stackDepth = 0; // integer
+ protected static $totalCalls = 0; // integer
+ protected static $totalElapsed = 0; // float; seconds
+
+ /* Prevent callers in infinite loops from running forever */
+ const MAX_TOTAL_CALLS = 1000000;
+ const MAX_TOTAL_TIME = 300; // seconds
+
+ /**
+ * @param $seconds integer
+ */
+ public function __construct( $seconds ) {
+ if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0
+ if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) {
+ trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." );
+ } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) {
+ trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." );
+ } elseif ( self::$stackDepth > 0 ) { // recursion guard
+ trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." );
+ } else {
+ $this->oldIgnoreAbort = ignore_user_abort( true );
+ $this->oldTimeout = ini_set( 'max_execution_time', $seconds );
+ $this->startTime = microtime( true );
+ ++self::$stackDepth;
+ ++self::$totalCalls; // proof against < 1us scopes
+ }
+ }
+ }
+
+ /**
+ * Restore the original timeout.
+ * This does not account for the timer value on __construct().
+ */
+ public function __destruct() {
+ if ( $this->oldTimeout ) {
+ $elapsed = microtime( true ) - $this->startTime;
+ // Note: a limit of 0 is treated as "forever"
+ set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) );
+ // If each scoped timeout is for less than one second, we end up
+ // restoring the original timeout without any decrease in value.
+ // Thus web scripts in an infinite loop can run forever unless we
+ // take some measures to prevent this. Track total time and calls.
+ self::$totalElapsed += $elapsed;
+ --self::$stackDepth;
+ ignore_user_abort( $this->oldIgnoreAbort );
+ }
+ }
+}
diff --git a/includes/SeleniumWebSettings.php b/includes/SeleniumWebSettings.php
index 34d829ca..7b98568d 100644
--- a/includes/SeleniumWebSettings.php
+++ b/includes/SeleniumWebSettings.php
@@ -1,9 +1,28 @@
<?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 );
}
diff --git a/includes/Setup.php b/includes/Setup.php
index 3955937c..924c3c07 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -1,6 +1,21 @@
<?php
/**
- * Include most things that's need to customize the site
+ * Include most things that's need to customize the 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
*/
@@ -49,7 +64,7 @@ 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
+ # but is rarely filled in $wgActionPaths
$wgActionPaths['view'] = $wgArticlePath ;
}
@@ -62,9 +77,6 @@ if ( $wgLogo === false ) $wgLogo = "$wgStylePath/common/images/wiki.png";
if ( $wgUploadPath === false ) $wgUploadPath = "$wgScriptPath/images";
if ( $wgUploadDirectory === false ) $wgUploadDirectory = "$IP/images";
-
-if ( $wgTmpDirectory === false ) $wgTmpDirectory = "{$wgUploadDirectory}/tmp";
-
if ( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR";
if ( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache";
if ( $wgDeletedDirectory === false ) $wgDeletedDirectory = "{$wgUploadDirectory}/deleted";
@@ -320,9 +332,6 @@ if ( !$wgEnotifMinorEdits ) {
foreach( $wgDisabledActions as $action ){
$wgActions[$action] = false;
}
-if( !$wgAllowPageInfo ){
- $wgActions['info'] = false;
-}
if ( !$wgHtml5Version && $wgHtml5 && $wgAllowRdfaAttributes ) {
# see http://www.w3.org/TR/rdfa-in-html/#document-conformance
@@ -353,12 +362,14 @@ if ( $wgNewUserLog ) {
$wgLogTypes[] = 'newusers';
$wgLogNames['newusers'] = 'newuserlogpage';
$wgLogHeaders['newusers'] = 'newuserlogpagetext';
- # newusers, create, create2, autocreate
- $wgLogActionsHandlers['newusers/*'] = 'NewUsersLogFormatter';
+ $wgLogActionsHandlers['newusers/newusers'] = 'NewUsersLogFormatter';
+ $wgLogActionsHandlers['newusers/create'] = 'NewUsersLogFormatter';
+ $wgLogActionsHandlers['newusers/create2'] = 'NewUsersLogFormatter';
+ $wgLogActionsHandlers['newusers/autocreate'] = 'NewUsersLogFormatter';
}
if ( $wgCookieSecure === 'detect' ) {
- $wgCookieSecure = ( substr( $wgServer, 0, 6 ) === 'https:' );
+ $wgCookieSecure = ( WebRequest::detectProtocol() === 'https:' );
}
// Disable MWDebug for command line mode, this prevents MWDebug from eating up
@@ -381,16 +392,30 @@ if ( !defined( 'MW_COMPILED' ) ) {
require_once( "$IP/includes/normal/UtfNormalUtil.php" );
require_once( "$IP/includes/GlobalFunctions.php" );
require_once( "$IP/includes/ProxyTools.php" );
- require_once( "$IP/includes/ImageFunctions.php" );
require_once( "$IP/includes/normal/UtfNormalDefines.php" );
wfProfileOut( $fname . '-includes' );
}
-# Now that GlobalFunctions is loaded, set the default for $wgCanonicalServer
+# Now that GlobalFunctions is loaded, set defaults that depend
+# on it.
+if ( $wgTmpDirectory === false ) {
+ $wgTmpDirectory = wfTempDir();
+}
+
if ( $wgCanonicalServer === false ) {
$wgCanonicalServer = wfExpandUrl( $wgServer, PROTO_HTTP );
}
+// Initialize $wgHTCPMulticastRouting from backwards-compatible settings
+if ( !$wgHTCPMulticastRouting && $wgHTCPMulticastAddress ) {
+ $wgHTCPMulticastRouting = array(
+ '' => array(
+ 'host' => $wgHTCPMulticastAddress,
+ 'port' => $wgHTCPPort,
+ )
+ );
+}
+
wfProfileIn( $fname . '-misc1' );
# Raise the memory limit if it's too low
@@ -421,16 +446,16 @@ if ( $wgCommandLineMode ) {
# Can't stub this one, it sets up $_GET and $_REQUEST in its constructor
$wgRequest = new WebRequest;
- $debug = "Start request\n\n{$_SERVER['REQUEST_METHOD']} {$wgRequest->getRequestURL()}";
+ $debug = "\n\nStart request {$wgRequest->getMethod()} {$wgRequest->getRequestURL()}\n";
if ( $wgDebugPrintHttpHeaders ) {
- $debug .= "\nHTTP HEADERS:\n";
+ $debug .= "HTTP HEADERS:\n";
foreach ( $wgRequest->getAllHeaders() as $name => $value ) {
$debug .= "$name: $value\n";
}
}
- wfDebug( "$debug\n" );
+ wfDebug( $debug );
}
wfProfileOut( $fname . '-misc1' );
@@ -439,6 +464,7 @@ wfProfileIn( $fname . '-memcached' );
$wgMemc = wfGetMainCache();
$messageMemc = wfGetMessageCacheStorage();
$parserMemc = wfGetParserCacheStorage();
+$wgLangConvMemc = wfGetLangConverterCacheStorage();
wfDebug( 'CACHES: ' . get_class( $wgMemc ) . '[main] ' .
get_class( $messageMemc ) . '[message] ' .
@@ -458,11 +484,9 @@ if ( !wfIniGetBool( 'session.auto_start' ) ) {
if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
if ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix . 'Token'] ) ) {
- wfIncrStats( 'request_with_session' );
wfSetupSession();
$wgSessionStarted = true;
} else {
- wfIncrStats( 'request_without_session' );
$wgSessionStarted = false;
}
}
@@ -479,7 +503,7 @@ $wgRequest->interpolateTitle();
$wgUser = RequestContext::getMain()->getUser(); # BackCompat
/**
- * @var Language
+ * @var $wgLang Language
*/
$wgLang = new StubUserLang;
@@ -489,7 +513,7 @@ $wgLang = new StubUserLang;
$wgOut = RequestContext::getMain()->getOutput(); # BackCompat
/**
- * @var Parser
+ * @var $wgParser Parser
*/
$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index 8a977fb3..6a861d8e 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -1,6 +1,118 @@
<?php
/**
- * This is a class used to hold configuration settings, particularly for multi-wiki sites.
+ * Configuration holder, particularly for multi-wiki sites.
+ *
+ * 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 is a class for holding configuration settings, particularly for
+ * multi-wiki sites.
+ *
+ * A basic synopsis:
+ *
+ * Consider a wikifarm having three sites: two production sites, one in English
+ * and one in German, and one testing site. You can assign them easy-to-remember
+ * identifiers - ISO 639 codes 'en' and 'de' for language wikis, and 'beta' for
+ * the testing wiki.
+ *
+ * You would thus initialize the site configuration by specifying the wiki
+ * identifiers:
+ *
+ * @code
+ * $conf = new SiteConfiguration;
+ * $conf->wikis = array( 'de', 'en', 'beta' );
+ * @endcode
+ *
+ * When configuring the MediaWiki global settings (the $wg variables),
+ * the identifiers will be available to specify settings on a per wiki basis.
+ *
+ * @code
+ * $conf->settings = array(
+ * 'wgSomeSetting' => array(
+ *
+ * # production:
+ * 'de' => false,
+ * 'en' => false,
+ *
+ * # test:
+ * 'beta => true,
+ * ),
+ * );
+ * @endcode
+ *
+ * With three wikis, that is easy to manage. But what about a farm with
+ * hundreds of wikis? Site configuration provides a special keyword named
+ * 'default' which is the value used when a wiki is not found. Hence
+ * the above code could be written:
+ *
+ * @code
+ * $conf->settings = array(
+ * 'wgSomeSetting' => array(
+ *
+ * 'default' => false,
+ *
+ * # Enable feature on test
+ * 'beta' => true,
+ * ),
+ * );
+ * @endcode
+ *
+ *
+ * Since settings can contain arrays, site configuration provides a way
+ * to merge an array with the default. This is very useful to avoid
+ * repeating settings again and again while still maintaining specific changes
+ * on a per wiki basis.
+ *
+ * @code
+ * $conf->settings = array(
+ * 'wgMergeSetting' = array(
+ * # Value that will be shared among all wikis:
+ * 'default' => array( NS_USER => true ),
+ *
+ * # Leading '+' means merging the array of value with the defaults
+ * '+beta' => array( NS_HELP => true ),
+ * ),
+ * );
+ *
+ * # Get configuration for the German site:
+ * $conf->get( 'wgMergeSetting', 'de' );
+ * // --> array( NS_USER => true );
+ *
+ * # Get configuration for the testing site:
+ * $conf->get( 'wgMergeSetting', 'beta' );
+ * // --> array( NS_USER => true, NS_HELP => true );
+ * @endcode
+ *
+ * Finally, to load all configuration settings, extract them in global context:
+ *
+ * @code
+ * # Name / identifier of the wiki as set in $conf->wikis
+ * $wikiID = 'beta';
+ * $globals = $conf->getAll( $wikiID );
+ * extract( $globals );
+ * @endcode
+ *
+ * TODO: give examples for,
+ * suffixes:
+ * $conf->suffixes = array( 'wiki' );
+ * localVHosts
+ * callbacks!
*/
class SiteConfiguration {
@@ -26,6 +138,7 @@ class SiteConfiguration {
/**
* Optional callback to load full configuration data.
+ * @var string|array
*/
public $fullLoadCallback = null;
@@ -43,6 +156,8 @@ class SiteConfiguration {
* argument and the wiki in the second one.
* if suffix and lang are passed they will be used for the return value of
* self::siteFromDB() and self::$suffixes will be ignored
+ *
+ * @var string|array
*/
public $siteParamsCallback = null;
@@ -77,7 +192,7 @@ class SiteConfiguration {
if( array_key_exists( $wiki, $thisSetting ) ) {
$retval = $thisSetting[$wiki];
break;
- } elseif( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) {
+ } elseif ( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) {
$retval = $thisSetting["+$wiki"];
}
@@ -91,8 +206,9 @@ class SiteConfiguration {
}
break 2;
} elseif( array_key_exists( "+$tag", $thisSetting ) && is_array($thisSetting["+$tag"]) ) {
- if( !isset( $retval ) )
+ if( !isset( $retval ) ) {
$retval = array();
+ }
$retval = self::arrayMerge( $retval, $thisSetting["+$tag"] );
}
}
@@ -106,9 +222,10 @@ class SiteConfiguration {
$retval = $thisSetting[$suffix];
}
break;
- } elseif( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) {
- if (!isset($retval))
+ } elseif ( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) {
+ if ( !isset( $retval ) ) {
$retval = array();
+ }
$retval = self::arrayMerge( $retval, $thisSetting["+$suffix"] );
}
}
@@ -175,8 +292,9 @@ class SiteConfiguration {
}
$value = $this->getSetting( $varname, $wiki, $params );
- if ( $append && is_array( $value ) && is_array( $GLOBALS[$var] ) )
+ if ( $append && is_array( $value ) && is_array( $GLOBALS[$var] ) ) {
$value = self::arrayMerge( $value, $GLOBALS[$var] );
+ }
if ( !is_null( $value ) ) {
$localSettings[$var] = $value;
}
@@ -210,7 +328,7 @@ class SiteConfiguration {
* @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 Reference The variable to insert the value into.
+ * @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.
*/
@@ -296,8 +414,9 @@ class SiteConfiguration {
}
foreach( $default as $name => $def ){
- if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) )
+ if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) ) {
$ret[$name] = $default[$name];
+ }
}
return $ret;
@@ -318,18 +437,21 @@ class SiteConfiguration {
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;
+ }
$ret['tags'] = array_unique( array_merge( $ret['tags'], $wikiTags ) );
$ret['params'] += $params;
// Automatically fill that ones if needed
- if( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) )
+ 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'];
+ }
return $ret;
}
@@ -343,8 +465,9 @@ 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;
@@ -401,7 +524,7 @@ class SiteConfiguration {
}
public function loadFullData() {
- if ($this->fullLoadCallback && !$this->fullLoadDone) {
+ if ( $this->fullLoadCallback && !$this->fullLoadDone ) {
call_user_func( $this->fullLoadCallback, $this );
$this->fullLoadDone = true;
}
diff --git a/includes/SiteStats.php b/includes/SiteStats.php
index abb11306..1c2c454d 100644
--- a/includes/SiteStats.php
+++ b/includes/SiteStats.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Accessors and mutators for the site-wide statistics.
+ *
+ * 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
+ */
/**
* Static accessor class for site_stats and related things
@@ -223,53 +243,91 @@ class SiteStats {
* Class for handling updates to the site_stats table
*/
class SiteStatsUpdate implements DeferrableUpdate {
-
- var $mViews, $mEdits, $mGood, $mPages, $mUsers;
-
+ protected $views = 0;
+ protected $edits = 0;
+ protected $pages = 0;
+ protected $articles = 0;
+ protected $users = 0;
+ protected $images = 0;
+
+ // @TODO: deprecate this constructor
function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) {
- $this->mViews = $views;
- $this->mEdits = $edits;
- $this->mGood = $good;
- $this->mPages = $pages;
- $this->mUsers = $users;
+ $this->views = $views;
+ $this->edits = $edits;
+ $this->articles = $good;
+ $this->pages = $pages;
+ $this->users = $users;
}
/**
- * @param $sql
- * @param $field
- * @param $delta
+ * @param $deltas Array
+ * @return SiteStatsUpdate
*/
- function appendUpdate( &$sql, $field, $delta ) {
- if ( $delta ) {
- if ( $sql ) {
- $sql .= ',';
- }
- if ( $delta < 0 ) {
- $sql .= "$field=$field-1";
- } else {
- $sql .= "$field=$field+1";
+ public static function factory( array $deltas ) {
+ $update = new self( 0, 0, 0 );
+
+ $fields = array( 'views', 'edits', 'pages', 'articles', 'users', 'images' );
+ foreach ( $fields as $field ) {
+ if ( isset( $deltas[$field] ) && $deltas[$field] ) {
+ $update->$field = $deltas[$field];
}
}
+
+ return $update;
}
- function doUpdate() {
- $dbw = wfGetDB( DB_MASTER );
+ public function doUpdate() {
+ global $wgSiteStatsAsyncFactor;
+
+ $rate = $wgSiteStatsAsyncFactor; // convenience
+ // If set to do so, only do actual DB updates 1 every $rate times.
+ // The other times, just update "pending delta" values in memcached.
+ 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']['-'] );
+ }
- $updates = '';
+ // Need a separate transaction because this a global lock
+ $dbw->begin( __METHOD__ );
- $this->appendUpdate( $updates, 'ss_total_views', $this->mViews );
- $this->appendUpdate( $updates, 'ss_total_edits', $this->mEdits );
- $this->appendUpdate( $updates, 'ss_good_articles', $this->mGood );
- $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages );
- $this->appendUpdate( $updates, 'ss_users', $this->mUsers );
+ // 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__ );
+ }
- if ( $updates ) {
- $site_stats = $dbw->tableName( 'site_stats' );
- $sql = "UPDATE $site_stats SET $updates";
+ 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__ );
+ }
- # Need a separate transaction because this a global lock
- $dbw->begin( __METHOD__ );
- $dbw->query( $sql, __METHOD__ );
$dbw->commit( __METHOD__ );
}
}
@@ -289,8 +347,8 @@ class SiteStatsUpdate implements DeferrableUpdate {
array(
'rc_user != 0',
'rc_bot' => 0,
- "rc_log_type != 'newusers' OR rc_log_type IS NULL",
- "rc_timestamp >= '{$dbw->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 )}'",
+ 'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL',
+ 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ) ),
),
__METHOD__
);
@@ -302,6 +360,102 @@ class SiteStatsUpdate implements DeferrableUpdate {
);
return $activeUsers;
}
+
+ protected function doUpdatePendingDeltas() {
+ $this->adjustPending( 'ss_total_views', $this->views );
+ $this->adjustPending( 'ss_total_edits', $this->edits );
+ $this->adjustPending( 'ss_good_articles', $this->articles );
+ $this->adjustPending( 'ss_total_pages', $this->pages );
+ $this->adjustPending( 'ss_users', $this->users );
+ $this->adjustPending( 'ss_images', $this->images );
+ }
+
+ /**
+ * @param $sql string
+ * @param $field string
+ * @param $delta integer
+ */
+ protected function appendUpdate( &$sql, $field, $delta ) {
+ if ( $delta ) {
+ if ( $sql ) {
+ $sql .= ',';
+ }
+ if ( $delta < 0 ) {
+ $sql .= "$field=$field-" . abs( $delta );
+ } else {
+ $sql .= "$field=$field+" . abs( $delta );
+ }
+ }
+ }
+
+ /**
+ * @param $type string
+ * @param $sign string ('+' or '-')
+ * @return string
+ */
+ private function getTypeCacheKey( $type, $sign ) {
+ return wfMemcKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
+ }
+
+ /**
+ * Adjust the pending deltas for a stat type.
+ * Each stat type has two pending counters, one for increments and decrements
+ * @param $type string
+ * @param $delta integer Delta (positive or negative)
+ * @return void
+ */
+ protected function adjustPending( $type, $delta ) {
+ global $wgMemc;
+
+ if ( $delta < 0 ) { // decrement
+ $key = $this->getTypeCacheKey( $type, '-' );
+ } else { // increment
+ $key = $this->getTypeCacheKey( $type, '+' );
+ }
+
+ $magnitude = abs( $delta );
+ if ( !$wgMemc->incr( $key, $magnitude ) ) { // not there?
+ if ( !$wgMemc->add( $key, $magnitude ) ) { // race?
+ $wgMemc->incr( $key, $magnitude );
+ }
+ }
+ }
+
+ /**
+ * Get pending delta counters for each stat type
+ * @return Array Positive and negative deltas for each type
+ * @return void
+ */
+ protected function getPendingDeltas() {
+ global $wgMemc;
+
+ $pending = array();
+ foreach ( array( 'ss_total_views', 'ss_total_edits',
+ 'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ) as $type )
+ {
+ // Get pending increments and pending decrements
+ $pending[$type]['+'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '+' ) );
+ $pending[$type]['-'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '-' ) );
+ }
+
+ return $pending;
+ }
+
+ /**
+ * Reduce pending delta counters after updates have been applied
+ * @param Array $pd Result of getPendingDeltas(), used for DB update
+ * @return void
+ */
+ protected function removePendingDeltas( array $pd ) {
+ global $wgMemc;
+
+ foreach ( $pd as $type => $deltas ) {
+ foreach ( $deltas as $sign => $magnitude ) {
+ // Lower the pending counter now that we applied these changes
+ $wgMemc->decr( $this->getTypeCacheKey( $type, $sign ), $magnitude );
+ }
+ }
+ }
}
/**
diff --git a/includes/Skin.php b/includes/Skin.php
index 430537d4..968f215e 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -1,11 +1,28 @@
<?php
/**
- * @defgroup Skins Skins
+ * Base class for all 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
*/
-if ( !defined( 'MEDIAWIKI' ) ) {
- die( 1 );
-}
+/**
+ * @defgroup Skins Skins
+ */
/**
* The main skin class that provide methods and properties for all other skins.
@@ -22,7 +39,7 @@ abstract class Skin extends ContextSource {
/**
* Fetch the set of available skins.
- * @return associative array of strings
+ * @return array associative array of strings
*/
static function getSkinNames() {
global $wgValidSkinNames;
@@ -55,7 +72,7 @@ abstract class Skin extends ContextSource {
}
return $wgValidSkinNames;
}
-
+
/**
* Fetch the skinname messages for available skins.
* @return array of strings
@@ -98,7 +115,7 @@ abstract class Skin extends ContextSource {
$skinNames = Skin::getSkinNames();
- if ( $key == '' ) {
+ if ( $key == '' || $key == 'default' ) {
// Don't return the default immediately;
// in a misconfiguration we need to fall back.
$key = $wgDefaultSkin;
@@ -147,11 +164,6 @@ abstract class Skin extends ContextSource {
if ( !MWInit::classExists( $className ) ) {
if ( !defined( 'MW_COMPILED' ) ) {
- // Preload base classes to work around APC/PHP5 bug
- $deps = "{$wgStyleDirectory}/{$skinName}.deps.php";
- if ( file_exists( $deps ) ) {
- include_once( $deps );
- }
require_once( "{$wgStyleDirectory}/{$skinName}.php" );
}
@@ -314,10 +326,10 @@ abstract class Skin extends ContextSource {
}
/**
- * Make a <script> tag containing global variables
+ * Make a "<script>" tag containing global variables
*
* @deprecated in 1.19
- * @param $unused Unused
+ * @param $unused
* @return string HTML fragment
*/
public static function makeGlobalVariablesScript( $unused ) {
@@ -351,7 +363,7 @@ abstract class Skin extends ContextSource {
* inside ->getOutput() is deprecated. The $out arg is kept
* for compatibility purposes with skins.
* @param $out OutputPage
- * @delete
+ * @todo delete
*/
abstract function setupSkinUserCss( OutputPage $out );
@@ -385,7 +397,7 @@ abstract class Skin extends ContextSource {
/**
* This will be called by OutputPage::headElement when it is creating the
- * <body> tag, skins can override it if they have a need to add in any
+ * "<body>" tag, skins can override it if they have a need to add in any
* body attributes or classes of their own.
* @param $out OutputPage
* @param $bodyAttrs Array
@@ -425,7 +437,7 @@ abstract class Skin extends ContextSource {
if ( !empty( $allCats['normal'] ) ) {
$t = $embed . implode( "{$pop}{$embed}" , $allCats['normal'] ) . $pop;
- $msg = $this->msg( 'pagecategories', count( $allCats['normal'] ) )->escaped();
+ $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped();
$linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text();
$s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' .
Linker::link( Title::newFromText( $linkPage ), $msg )
@@ -443,7 +455,7 @@ abstract class Skin extends ContextSource {
}
$s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" .
- $this->msg( 'hidden-categories', count( $allCats['hidden'] ) )->escaped() .
+ $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() .
$colon . '<ul>' . $embed . implode( "{$pop}{$embed}" , $allCats['hidden'] ) . $pop . '</ul>' .
'</div>';
}
@@ -556,77 +568,13 @@ abstract class Skin extends ContextSource {
* @return String HTML containing debug data, if enabled (otherwise empty).
*/
protected function generateDebugHTML() {
- global $wgShowDebug;
-
- $html = MWDebug::getDebugHTML( $this->getContext() );
-
- if ( $wgShowDebug ) {
- $listInternals = $this->formatDebugHTML( $this->getOutput()->mDebugtext );
- $html .= "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">" .
- $listInternals . "</ul>\n";
- }
-
- return $html;
- }
-
- /**
- * @param $debugText string
- * @return string
- */
- private function formatDebugHTML( $debugText ) {
- global $wgDebugTimestamps;
-
- $lines = explode( "\n", $debugText );
- $curIdent = 0;
- $ret = '<li>';
-
- foreach ( $lines as $line ) {
- $pre = '';
- if ( $wgDebugTimestamps ) {
- $matches = array();
- if ( preg_match( '/^(\d+\.\d+ {1,3}\d+.\dM\s{2})/', $line, $matches ) ) {
- $pre = $matches[1];
- $line = substr( $line, strlen( $pre ) );
- }
- }
- $display = ltrim( $line );
- $ident = strlen( $line ) - strlen( $display );
- $diff = $ident - $curIdent;
-
- $display = $pre . $display;
- if ( $display == '' ) {
- $display = "\xc2\xa0";
- }
-
- if ( !$ident && $diff < 0 && substr( $display, 0, 9 ) != 'Entering ' && substr( $display, 0, 8 ) != 'Exiting ' ) {
- $ident = $curIdent;
- $diff = 0;
- $display = '<span style="background:yellow;">' . htmlspecialchars( $display ) . '</span>';
- } else {
- $display = htmlspecialchars( $display );
- }
-
- if ( $diff < 0 ) {
- $ret .= str_repeat( "</li></ul>\n", -$diff ) . "</li><li>\n";
- } elseif ( $diff == 0 ) {
- $ret .= "</li><li>\n";
- } else {
- $ret .= str_repeat( "<ul><li>\n", $diff );
- }
- $ret .= "<tt>$display</tt>\n";
-
- $curIdent = $ident;
- }
-
- $ret .= str_repeat( '</li></ul>', $curIdent ) . '</li>';
-
- return $ret;
+ return MWDebug::getHTMLDebugLog();
}
/**
- * This gets called shortly before the </body> tag.
+ * This gets called shortly before the "</body>" tag.
*
- * @return String HTML-wrapped JS code to be put before </body>
+ * @return String HTML-wrapped JS code to be put before "</body>"
*/
function bottomScripts() {
// TODO and the suckage continues. This function is really just a wrapper around
@@ -647,10 +595,10 @@ abstract class Skin extends ContextSource {
function printSource() {
$oldid = $this->getRevisionId();
if ( $oldid ) {
- $url = htmlspecialchars( $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid ) );
+ $url = htmlspecialchars( wfExpandIRI( $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid ) ) );
} else {
// oldid not available for non existing pages
- $url = htmlspecialchars( $this->getTitle()->getCanonicalURL() );
+ $url = htmlspecialchars( wfExpandIRI( $this->getTitle()->getCanonicalURL() ) );
}
return $this->msg( 'retrievedfrom', '<a href="' . $url . '">' . $url . '</a>' )->text();
}
@@ -662,7 +610,7 @@ abstract class Skin extends ContextSource {
$action = $this->getRequest()->getVal( 'action', 'view' );
if ( $this->getUser()->isAllowed( 'deletedhistory' ) &&
- ( $this->getTitle()->getArticleId() == 0 || $action == 'history' ) ) {
+ ( $this->getTitle()->getArticleID() == 0 || $action == 'history' ) ) {
$n = $this->getTitle()->isDeleted();
@@ -688,6 +636,7 @@ abstract class Skin extends ContextSource {
* @return string
*/
function subPageSubtitle() {
+ global $wgLang;
$out = $this->getOutput();
$subpages = '';
@@ -709,7 +658,7 @@ abstract class Skin extends ContextSource {
$display .= $link;
$linkObj = Title::newFromText( $growinglink );
- if ( is_object( $linkObj ) && $linkObj->exists() ) {
+ if ( is_object( $linkObj ) && $linkObj->isKnown() ) {
$getlink = Linker::linkKnown(
$linkObj,
htmlspecialchars( $display )
@@ -718,7 +667,7 @@ abstract class Skin extends ContextSource {
$c++;
if ( $c > 1 ) {
- $subpages .= $this->msg( 'pipe-separator' )->escaped();
+ $subpages .= $wgLang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped();
} else {
$subpages .= '&lt; ';
}
@@ -886,7 +835,7 @@ abstract class Skin extends ContextSource {
*/
function logoText( $align = '' ) {
if ( $align != '' ) {
- $a = " align='{$align}'";
+ $a = " style='float: {$align};'";
} else {
$a = '';
}
@@ -1054,13 +1003,23 @@ abstract class Skin extends ContextSource {
}
/**
- * @param $name string
- * @param $urlaction string
+ * Make a URL for a Special Page using the given query and protocol.
+ *
+ * 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 $proto Protocol to use or null for a local URL
* @return String
*/
- static function makeSpecialUrl( $name, $urlaction = '' ) {
+ static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) {
$title = SpecialPage::getSafeTitleFor( $name );
- return $title->getLocalURL( $urlaction );
+ if( is_null( $proto ) ) {
+ return $title->getLocalURL( $urlaction );
+ } else {
+ return $title->getFullURL( $urlaction, false, $proto );
+ }
}
/**
@@ -1080,7 +1039,7 @@ abstract class Skin extends ContextSource {
* @return String
*/
static function makeI18nUrl( $name, $urlaction = '' ) {
- $title = Title::newFromText( wfMsgForContent( $name ) );
+ $title = Title::newFromText( wfMessage( $name )->inContentLanguage()->text() );
self::checkTitle( $title, $name );
return $title->getLocalURL( $urlaction );
}
@@ -1104,7 +1063,7 @@ abstract class Skin extends ContextSource {
* @return String URL
*/
static function makeInternalOrExternalUrl( $name ) {
- if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $name ) ) {
+ if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) {
return $name;
} else {
return self::makeUrl( $name );
@@ -1212,7 +1171,7 @@ abstract class Skin extends ContextSource {
* @param $message String
*/
function addToSidebar( &$bar, $message ) {
- $this->addToSidebarPlain( $bar, wfMsgForContentNoTrans( $message ) );
+ $this->addToSidebarPlain( $bar, wfMessage( $message )->inContentLanguage()->plain() );
}
/**
@@ -1268,7 +1227,7 @@ abstract class Skin extends ContextSource {
$text = $line[1];
}
- if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $link ) ) {
+ if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) {
$href = $link;
// Parser::getExternalLinkAttribs won't work here because of the Namespace things
@@ -1329,29 +1288,59 @@ abstract class Skin extends ContextSource {
$ntl = '';
if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) {
- $userTitle = $this->getUser()->getUserPage();
- $userTalkTitle = $userTitle->getTalkPage();
-
- if ( !$userTalkTitle->equals( $out->getTitle() ) ) {
+ $uTalkTitle = $this->getUser()->getTalkPage();
+
+ if ( !$uTalkTitle->equals( $out->getTitle() ) ) {
+ $lastSeenRev = isset( $newtalks[0]['rev'] ) ? $newtalks[0]['rev'] : null;
+ $nofAuthors = 0;
+ if ( $lastSeenRev !== null ) {
+ $plural = true; // Default if we have a last seen revision: if unknown, use plural
+ $latestRev = Revision::newFromTitle( $uTalkTitle, false, Revision::READ_NORMAL );
+ if ( $latestRev !== null ) {
+ // Singular if only 1 unseen revision, plural if several unseen revisions.
+ $plural = $latestRev->getParentId() !== $lastSeenRev->getId();
+ $nofAuthors = $uTalkTitle->countAuthorsBetween(
+ $lastSeenRev, $latestRev, 10, 'include_new' );
+ }
+ } else {
+ // Singular if no revision -> diff link will show latest change only in any case
+ $plural = false;
+ }
+ $plural = $plural ? 2 : 1;
+ // 2 signifies "more than one revision". We don't know how many, and even if we did,
+ // the number of revisions or authors is not necessarily the same as the number of
+ // "messages".
$newMessagesLink = Linker::linkKnown(
- $userTalkTitle,
- $this->msg( 'newmessageslink' )->escaped(),
+ $uTalkTitle,
+ $this->msg( 'newmessageslinkplural' )->params( $plural )->escaped(),
array(),
array( 'redirect' => 'no' )
);
$newMessagesDiffLink = Linker::linkKnown(
- $userTalkTitle,
- $this->msg( 'newmessagesdifflink' )->escaped(),
+ $uTalkTitle,
+ $this->msg( 'newmessagesdifflinkplural' )->params( $plural )->escaped(),
array(),
- array( 'diff' => 'cur' )
+ $lastSeenRev !== null
+ ? array( 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' )
+ : array( 'diff' => 'cur' )
);
- $ntl = $this->msg(
- 'youhavenewmessages',
- $newMessagesLink,
- $newMessagesDiffLink
- )->text();
+ if ( $nofAuthors >= 1 && $nofAuthors <= 10 ) {
+ $ntl = $this->msg(
+ 'youhavenewmessagesfromusers',
+ $newMessagesLink,
+ $newMessagesDiffLink
+ )->numParams( $nofAuthors );
+ } else {
+ // $nofAuthors === 11 signifies "11 or more" ("more than 10")
+ $ntl = $this->msg(
+ $nofAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages',
+ $newMessagesLink,
+ $newMessagesDiffLink
+ );
+ }
+ $ntl = $ntl->text();
# Disable Squid cache
$out->setSquidMaxage( 0 );
}
@@ -1495,13 +1484,17 @@ abstract class Skin extends ContextSource {
public function doEditSectionLink( Title $nt, $section, $tooltip = null, $lang = false ) {
// HTML generated here should probably have userlangattributes
// added to it for LTR text on RTL pages
+
+ $lang = wfGetLangObj( $lang );
+
$attribs = array();
if ( !is_null( $tooltip ) ) {
# Bug 25462: undo double-escaping.
$tooltip = Sanitizer::decodeCharReferences( $tooltip );
- $attribs['title'] = wfMsgExt( 'editsectionhint', array( 'language' => $lang, 'parsemag', 'replaceafter' ), $tooltip );
+ $attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip )
+ ->inLanguage( $lang )->text();
}
- $link = Linker::link( $nt, wfMsgExt( 'editsection', array( 'language' => $lang ) ),
+ $link = Linker::link( $nt, wfMessage( 'editsection' )->inLanguage( $lang )->text(),
$attribs,
array( 'action' => 'edit', 'section' => $section ),
array( 'noclasses', 'known' )
@@ -1511,7 +1504,8 @@ abstract class Skin extends ContextSource {
# we can rid of it someday.
$attribs = '';
if ( $tooltip ) {
- $attribs = wfMsgExt( 'editsectionhint', array( 'language' => $lang, 'parsemag', 'escape', 'replaceafter' ), $tooltip );
+ $attribs = wfMessage( 'editsectionhint' )->rawParams( $tooltip )
+ ->inLanguage( $lang )->escaped();
$attribs = " title=\"$attribs\"";
}
$result = null;
@@ -1521,13 +1515,15 @@ abstract class Skin extends ContextSource {
# 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 = wfMsgExt( 'editsection-brackets', array( 'escape', 'replaceafter', 'language' => $lang ), $result );
+ $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 = wfMsgExt( 'editsection-brackets', array( 'escape', 'replaceafter', 'language' => $lang ), $link );
+ $result = wfMessage( 'editsection-brackets' )->rawParams( $link )
+ ->inLanguage( $lang )->escaped();
$result = "<span class=\"editsection\">$result</span>";
wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result, $lang ) );
@@ -1540,6 +1536,7 @@ abstract class Skin extends ContextSource {
*
* @param $fname String Name of called method
* @param $args Array Arguments to the method
+ * @return mixed
*/
function __call( $fname, $args ) {
$realFunction = array( 'Linker', $fname );
diff --git a/includes/SkinLegacy.php b/includes/SkinLegacy.php
index 77c85a88..e695ba6c 100644
--- a/includes/SkinLegacy.php
+++ b/includes/SkinLegacy.php
@@ -1,12 +1,25 @@
<?php
/**
- * @defgroup Skins Skins
+ * 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
*/
-if ( !defined( 'MEDIAWIKI' ) ) {
- die( 1 );
-}
-
class SkinLegacy extends SkinTemplate {
var $useHeadElement = true;
protected $mWatchLinkNum = 0; // Appended to end of watch link id's
@@ -82,8 +95,9 @@ class LegacyTemplate extends BaseTemplate {
}
/**
- * This will be called immediately after the <body> tag. Split into
+ * 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();
@@ -106,21 +120,21 @@ class LegacyTemplate extends BaseTemplate {
}
$s .= "\n<div id='content'>\n<div id='topbar'>\n" .
- "<table border='0' cellspacing='0' width='100%'>\n<tr>\n";
+ "<table cellspacing='0' width='100%'>\n<tr>\n";
if ( $this->getSkin()->qbSetting() == 0 ) {
- $s .= "<td class='top' align='left' valign='top' rowspan='{$rows}'>\n" .
+ $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} align='$l' valign='top'>\n";
+ $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} valign='top' align='$r' nowrap='nowrap'>";
+ $s .= "</td>\n<td {$borderhack} style='text-align: $r; vertical-align: top;' nowrap='nowrap'>";
$s .= $this->nameAndLogin();
$s .= "\n<br />" . $this->searchForm() . '</td>';
@@ -145,14 +159,16 @@ class LegacyTemplate extends BaseTemplate {
}
/**
- * This gets called shortly before the </body> tag.
- * @return String HTML to be put before </body>
+ * 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 */
+ /** overloaded by derived classes
+ * @return string
+ */
function doAfterContent() {
return '</div></div>';
}
@@ -166,12 +182,12 @@ class LegacyTemplate extends BaseTemplate {
. $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="' . wfMsg( 'searcharticle' ) . '" />';
+ . '<input type="submit" name="go" value="' . wfMessage( 'searcharticle' )->text() . '" />';
if ( $wgUseTwoButtonsSearchForm ) {
- $s .= '&#160;<input type="submit" name="fulltext" value="' . wfMsg( 'searchbutton' ) . "\" />\n";
+ $s .= '&#160;<input type="submit" name="fulltext" value="' . wfMessage( 'searchbutton' )->text() . "\" />\n";
} else {
- $s .= ' <a href="' . $this->getSkin()->escapeSearchLink() . '" rel="search">' . wfMsg( 'powersearch-legend' ) . "</a>\n";
+ $s .= ' <a href="' . $this->getSkin()->escapeSearchLink() . '" rel="search">' . wfMessage( 'powersearch-legend' )->text() . "</a>\n";
}
$s .= '</form>';
@@ -220,7 +236,7 @@ class LegacyTemplate extends BaseTemplate {
}
// @todo FIXME: Is using Language::pipeList impossible here? Do not quite understand the use of the newline
- return implode( $s, wfMsgExt( 'pipe-separator', 'escapenoentities' ) . "\n" );
+ return implode( $s, wfMessage( 'pipe-separator' )->escaped() . "\n" );
}
/**
@@ -247,7 +263,7 @@ class LegacyTemplate extends BaseTemplate {
}
$s = $wgLang->pipeList( array(
$s,
- '<a href="' . htmlspecialchars( $title->getLocalURL( 'variant=' . $code ) ) . '">' . htmlspecialchars( $varname ) . '</a>'
+ '<a href="' . htmlspecialchars( $title->getLocalURL( 'variant=' . $code ) ) . '" lang="' . $code . '" hreflang="' . $code . '">' . htmlspecialchars( $varname ) . '</a>'
) );
}
}
@@ -276,7 +292,7 @@ class LegacyTemplate extends BaseTemplate {
if ( count( $s ) ) {
global $wgLang;
- $out = wfMsgExt( 'pipe-separator' , 'escapenoentities' );
+ $out = wfMessage( 'pipe-separator' )->escaped();
$out .= $wgLang->pipeList( $s );
}
@@ -285,7 +301,7 @@ class LegacyTemplate extends BaseTemplate {
function bottomLinks() {
global $wgOut, $wgUser;
- $sep = wfMsgExt( 'pipe-separator', 'escapenoentities' ) . "\n";
+ $sep = wfMessage( 'pipe-separator' )->escaped() . "\n";
$s = '';
if ( $wgOut->isArticleRelated() ) {
@@ -321,7 +337,7 @@ class LegacyTemplate extends BaseTemplate {
$s = implode( $element, $sep );
- if ( $title->getArticleId() ) {
+ if ( $title->getArticleID() ) {
$s .= "\n<br />";
// Delete/protect/move links for privileged users
@@ -345,7 +361,7 @@ class LegacyTemplate extends BaseTemplate {
}
function otherLanguages() {
- global $wgOut, $wgLang, $wgContLang, $wgHideInterlanguageLinks;
+ global $wgOut, $wgLang, $wgHideInterlanguageLinks;
if ( $wgHideInterlanguageLinks ) {
return '';
@@ -357,7 +373,7 @@ class LegacyTemplate extends BaseTemplate {
return '';
}
- $s = wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' );
+ $s = wfMessage( 'otherlanguages' )->text() . wfMessage( 'colon-separator' )->text();
$first = true;
if ( $wgLang->isRTL() ) {
@@ -366,13 +382,13 @@ class LegacyTemplate extends BaseTemplate {
foreach ( $a as $l ) {
if ( !$first ) {
- $s .= wfMsgExt( 'pipe-separator', 'escapenoentities' );
+ $s .= wfMessage( 'pipe-separator' )->escaped();
}
$first = false;
$nt = Title::newFromText( $l );
- $text = $wgContLang->getLanguageName( $nt->getInterwiki() );
+ $text = Language::fetchLanguageName( $nt->getInterwiki() );
$s .= Html::element( 'a',
array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ),
@@ -388,6 +404,7 @@ class LegacyTemplate extends BaseTemplate {
/**
* Show a drop-down box of special pages
+ * @return string
*/
function specialPagesList() {
global $wgScript;
@@ -400,8 +417,9 @@ class LegacyTemplate extends BaseTemplate {
$obj->getTitle()->getPrefixedDBkey() );
}
- return Html::rawElement( 'form', array( 'id' => 'specialpages', 'method' => 'get',
- 'action' => $wgScript ), $select->getHTML() . Xml::submitButton( wfMsg( 'go' ) ) );
+ return Html::rawElement( 'form',
+ array( 'id' => 'specialpages', 'method' => 'get', 'action' => $wgScript ),
+ $select->getHTML() . Xml::submitButton( wfMessage( 'go' )->text() ) );
}
function pageTitleLinks() {
@@ -429,21 +447,21 @@ class LegacyTemplate extends BaseTemplate {
if ( $wgOut->isArticleRelated() ) {
if ( $title->getNamespace() == NS_FILE ) {
- $name = $title->getDBkey();
$image = wfFindFile( $title );
if ( $image ) {
- $link = htmlspecialchars( $image->getURL() );
- $style = Linker::getInternalLinkAttributes( $link, $name );
- $s[] = "<a href=\"{$link}\"{$style}>{$name}</a>";
+ $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,
- wfMsg( 'currentrev' )
+ $title,
+ wfMessage( 'currentrev' )->text()
);
}
@@ -453,18 +471,18 @@ class LegacyTemplate extends BaseTemplate {
if ( !$title->equals( $wgUser->getTalkPage() ) ) {
$tl = Linker::linkKnown(
$wgUser->getTalkPage(),
- wfMsgHtml( 'newmessageslink' ),
+ wfMessage( 'newmessageslink' )->escaped(),
array(),
array( 'redirect' => 'no' )
);
$dl = Linker::linkKnown(
$wgUser->getTalkPage(),
- wfMsgHtml( 'newmessagesdifflink' ),
+ wfMessage( 'newmessagesdifflink' )->escaped(),
array(),
array( 'diff' => 'cur' )
);
- $s[] = '<strong>' . wfMsg( 'youhavenewmessages', $tl, $dl ) . '</strong>';
+ $s[] = '<strong>' . wfMessage( 'youhavenewmessages', $tl, $dl )->text() . '</strong>';
# disable caching
$wgOut->setSquidMaxage( 0 );
$wgOut->enableClientCache( false );
@@ -497,7 +515,7 @@ class LegacyTemplate extends BaseTemplate {
if ( $sub == '' ) {
global $wgExtraSubtitle;
- $sub = wfMsgExt( 'tagline', 'parsemag' ) . $wgExtraSubtitle;
+ $sub = wfMessage( 'tagline' )->parse() . $wgExtraSubtitle;
}
$subpages = $this->getSkin()->subPageSubtitle();
@@ -515,14 +533,15 @@ class LegacyTemplate extends BaseTemplate {
if ( !$wgOut->isPrintable() ) {
$printurl = htmlspecialchars( $this->getSkin()->getTitle()->getLocalUrl(
$wgRequest->appendQueryValue( 'printable', 'yes', true ) ) );
- $s[] = "<a href=\"$printurl\" rel=\"alternate\">" . wfMsg( 'printableversion' ) . '</a>';
+ $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\">" . wfMsgHtml( "feed-$format" ) . "</a>";
+ . " class=\"feedlink\">" . wfMessage( "feed-$format" )->escaped() . "</a>";
}
}
return $wgLang->pipeList( $s );
@@ -530,6 +549,7 @@ class LegacyTemplate extends BaseTemplate {
/**
* @deprecated in 1.19
+ * @return string
*/
function getQuickbarCompensator( $rows = 1 ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -540,15 +560,15 @@ class LegacyTemplate extends BaseTemplate {
global $wgOut;
if ( !$wgOut->isArticleRelated() ) {
- $s = wfMsg( 'protectedpage' );
+ $s = wfMessage( 'protectedpage' )->text();
} else {
$title = $this->getSkin()->getTitle();
if ( $title->quickUserCan( 'edit' ) && $title->exists() ) {
- $t = wfMsg( 'editthispage' );
+ $t = wfMessage( 'editthispage' )->text();
} elseif ( $title->quickUserCan( 'create' ) && !$title->exists() ) {
- $t = wfMsg( 'create-this-page' );
+ $t = wfMessage( 'create-this-page' )->text();
} else {
- $t = wfMsg( 'viewsource' );
+ $t = wfMessage( 'viewsource' )->text();
}
$s = Linker::linkKnown(
@@ -568,8 +588,8 @@ class LegacyTemplate extends BaseTemplate {
$diff = $wgRequest->getVal( 'diff' );
$title = $this->getSkin()->getTitle();
- if ( $title->getArticleId() && ( !$diff ) && $wgUser->isAllowed( 'delete' ) ) {
- $t = wfMsg( 'deletethispage' );
+ if ( $title->getArticleID() && ( !$diff ) && $wgUser->isAllowed( 'delete' ) ) {
+ $t = wfMessage( 'deletethispage' )->text();
$s = Linker::linkKnown(
$title,
@@ -590,12 +610,12 @@ class LegacyTemplate extends BaseTemplate {
$diff = $wgRequest->getVal( 'diff' );
$title = $this->getSkin()->getTitle();
- if ( $title->getArticleId() && ( ! $diff ) && $wgUser->isAllowed( 'protect' ) ) {
+ if ( $title->getArticleID() && ( ! $diff ) && $wgUser->isAllowed( 'protect' ) ) {
if ( $title->isProtected() ) {
- $text = wfMsg( 'unprotectthispage' );
+ $text = wfMessage( 'unprotectthispage' )->text();
$query = array( 'action' => 'unprotect' );
} else {
- $text = wfMsg( 'protectthispage' );
+ $text = wfMessage( 'protectthispage' )->text();
$query = array( 'action' => 'protect' );
}
@@ -620,15 +640,15 @@ class LegacyTemplate extends BaseTemplate {
$title = $this->getSkin()->getTitle();
if ( $wgOut->isArticleRelated() ) {
- if ( $title->userIsWatching() ) {
- $text = wfMsg( 'unwatchthispage' );
+ 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 = wfMsg( 'watchthispage' );
+ $text = wfMessage( 'watchthispage' )->text();
$query = array(
'action' => 'watch',
'token' => WatchAction::getWatchToken( $title, $wgUser ),
@@ -643,7 +663,7 @@ class LegacyTemplate extends BaseTemplate {
$query
);
} else {
- $s = wfMsg( 'notanarticle' );
+ $s = wfMessage( 'notanarticle' )->text();
}
return $s;
@@ -653,7 +673,7 @@ class LegacyTemplate extends BaseTemplate {
if ( $this->getSkin()->getTitle()->quickUserCan( 'move' ) ) {
return Linker::linkKnown(
SpecialPage::getTitleFor( 'Movepage' ),
- wfMsg( 'movethispage' ),
+ wfMessage( 'movethispage' )->text(),
array(),
array( 'target' => $this->getSkin()->getTitle()->getPrefixedDBkey() )
);
@@ -666,7 +686,7 @@ class LegacyTemplate extends BaseTemplate {
function historyLink() {
return Linker::link(
$this->getSkin()->getTitle(),
- wfMsgHtml( 'history' ),
+ wfMessage( 'history' )->escaped(),
array( 'rel' => 'archives' ),
array( 'action' => 'history' )
);
@@ -675,21 +695,21 @@ class LegacyTemplate extends BaseTemplate {
function whatLinksHere() {
return Linker::linkKnown(
SpecialPage::getTitleFor( 'Whatlinkshere', $this->getSkin()->getTitle()->getPrefixedDBkey() ),
- wfMsgHtml( 'whatlinkshere' )
+ wfMessage( 'whatlinkshere' )->escaped()
);
}
function userContribsLink() {
return Linker::linkKnown(
SpecialPage::getTitleFor( 'Contributions', $this->getSkin()->getTitle()->getDBkey() ),
- wfMsgHtml( 'contributions' )
+ wfMessage( 'contributions' )->escaped()
);
}
function emailUserLink() {
return Linker::linkKnown(
SpecialPage::getTitleFor( 'Emailuser', $this->getSkin()->getTitle()->getDBkey() ),
- wfMsgHtml( 'emailuser' )
+ wfMessage( 'emailuser' )->escaped()
);
}
@@ -697,11 +717,11 @@ class LegacyTemplate extends BaseTemplate {
global $wgOut;
if ( !$wgOut->isArticleRelated() ) {
- return '(' . wfMsg( 'notanarticle' ) . ')';
+ return wfMessage( 'parentheses', wfMessage( 'notanarticle' )->text() )->escaped();
} else {
return Linker::linkKnown(
SpecialPage::getTitleFor( 'Recentchangeslinked', $this->getSkin()->getTitle()->getPrefixedDBkey() ),
- wfMsgHtml( 'recentchangeslinked-toolbox' )
+ wfMessage( 'recentchangeslinked-toolbox' )->escaped()
);
}
}
@@ -719,41 +739,41 @@ class LegacyTemplate extends BaseTemplate {
$link = $title->getSubjectPage();
switch( $link->getNamespace() ) {
case NS_MAIN:
- $text = wfMsg( 'articlepage' );
+ $text = wfMessage( 'articlepage' );
break;
case NS_USER:
- $text = wfMsg( 'userpage' );
+ $text = wfMessage( 'userpage' );
break;
case NS_PROJECT:
- $text = wfMsg( 'projectpage' );
+ $text = wfMessage( 'projectpage' );
break;
case NS_FILE:
- $text = wfMsg( 'imagepage' );
+ $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 = wfMsg( 'mediawikipage' );
+ $text = wfMessage( 'mediawikipage' );
break;
case NS_TEMPLATE:
- $text = wfMsg( 'templatepage' );
+ $text = wfMessage( 'templatepage' );
break;
case NS_HELP:
- $text = wfMsg( 'viewhelppage' );
+ $text = wfMessage( 'viewhelppage' );
break;
case NS_CATEGORY:
- $text = wfMsg( 'categorypage' );
+ $text = wfMessage( 'categorypage' );
break;
default:
- $text = wfMsg( 'articlepage' );
+ $text = wfMessage( 'articlepage' );
}
} else {
$link = $title->getTalkPage();
- $text = wfMsg( 'talkpage' );
+ $text = wfMessage( 'talkpage' );
}
- $s = Linker::link( $link, $text, array(), array(), $linkOptions );
+ $s = Linker::link( $link, $text->text(), array(), array(), $linkOptions );
return $s;
}
@@ -775,7 +795,7 @@ class LegacyTemplate extends BaseTemplate {
return Linker::linkKnown(
$title,
- wfMsg( 'postcomment' ),
+ wfMessage( 'postcomment' )->text(),
array(),
array(
'action' => 'edit',
@@ -789,11 +809,13 @@ class LegacyTemplate extends BaseTemplate {
if ( $wgUploadNavigationUrl ) {
# Using an empty class attribute to avoid automatic setting of "external" class
- return Linker::makeExternalLink( $wgUploadNavigationUrl, wfMsgHtml( 'upload' ), false, null, array( 'class' => '' ) );
+ return Linker::makeExternalLink( $wgUploadNavigationUrl,
+ wfMessage( 'upload' )->escaped(),
+ false, null, array( 'class' => '' ) );
} else {
return Linker::linkKnown(
SpecialPage::getTitleFor( 'Upload' ),
- wfMsgHtml( 'upload' )
+ wfMessage( 'upload' )->escaped()
);
}
}
@@ -810,10 +832,11 @@ class LegacyTemplate extends BaseTemplate {
$talkLink = Linker::link( $wgUser->getTalkPage(),
$wgLang->getNsText( NS_TALK ) );
+ $talkLink = wfMessage( 'parentheses' )->rawParams( $talkLink )->escaped();
- $ret .= "$name ($talkLink)";
+ $ret .= "$name $talkLink";
} else {
- $ret .= wfMsg( 'notloggedin' );
+ $ret .= wfMessage( 'notloggedin' )->text();
}
$query = array();
@@ -827,18 +850,19 @@ class LegacyTemplate extends BaseTemplate {
: 'login';
$ret .= "\n<br />" . Linker::link(
SpecialPage::getTitleFor( 'Userlogin' ),
- wfMsg( $loginlink ), array(), $query
+ 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 .= " $talkLink<br />";
$ret .= $wgLang->pipeList( array(
Linker::link(
- SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ),
+ SpecialPage::getTitleFor( 'Userlogout' ), wfMessage( 'logout' )->text(),
array(), array( 'returnto' => $returnTo->getPrefixedDBkey() )
),
Linker::specialLink( 'Preferences' ),
@@ -848,13 +872,11 @@ class LegacyTemplate extends BaseTemplate {
$ret = $wgLang->pipeList( array(
$ret,
Linker::link(
- Title::newFromText( wfMsgForContent( 'helppage' ) ),
- wfMsg( 'help' )
+ Title::newFromText( wfMessage( 'helppage' )->inContentLanguage()->text() ),
+ wfMessage( 'help' )->text()
),
) );
return $ret;
}
-
}
-
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
index 2dd00980..bda43957 100644
--- a/includes/SkinTemplate.php
+++ b/includes/SkinTemplate.php
@@ -1,6 +1,6 @@
<?php
/**
- * Base class for template-based skins
+ * Base class for template-based 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
@@ -20,10 +20,6 @@
* @file
*/
-if ( !defined( 'MEDIAWIKI' ) ) {
- die( 1 );
-}
-
/**
* Wrapper object for MediaWiki's localization functions,
* to be passed to the template engine.
@@ -44,7 +40,7 @@ class MediaWiki_I18N {
// Hack for i18n:attributes in PHPTAL 1.0.0 dev version as of 2004-10-23
$value = preg_replace( '/^string:/', '', $value );
- $value = wfMsg( $value );
+ $value = wfMessage( $value )->text();
// interpolate variables
$m = array();
while( preg_match( '/\$([0-9]*?)/sm', $value, $m ) ) {
@@ -95,7 +91,7 @@ class SkinTemplate extends Skin {
var $template = 'QuickTemplate';
/**
- * Whether this skin use OutputPage::headElement() to generate the <head>
+ * Whether this skin use OutputPage::headElement() to generate the "<head>"
* tag
*/
var $useHeadElement = false;
@@ -139,7 +135,6 @@ class SkinTemplate extends Skin {
global $wgDisableCounters, $wgSitename, $wgLogo, $wgHideInterlanguageLinks;
global $wgMaxCredits, $wgShowCreditsIfMax;
global $wgPageShowWatchingUsers;
- global $wgDebugComments;
global $wgArticlePath, $wgScriptPath, $wgServer;
wfProfileIn( __METHOD__ );
@@ -216,7 +211,7 @@ class SkinTemplate extends Skin {
$tpl->setRef( 'thispage', $this->thispage );
$tpl->setRef( 'titleprefixeddbkey', $this->thispage );
$tpl->set( 'titletext', $title->getText() );
- $tpl->set( 'articleid', $title->getArticleId() );
+ $tpl->set( 'articleid', $title->getArticleID() );
$tpl->set( 'isarticle', $out->isArticle() );
@@ -262,7 +257,7 @@ class SkinTemplate extends Skin {
/* XXX currently unused, might get useful later
$tpl->set( 'editable', ( !$title->isSpecialPage() ) );
$tpl->set( 'exists', $title->getArticleID() != 0 );
- $tpl->set( 'watch', $title->userIsWatching() ? 'unwatch' : 'watch' );
+ $tpl->set( 'watch', $user->isWatched( $title ) ? 'unwatch' : 'watch' );
$tpl->set( 'protect', count( $title->isProtected() ) ? 'unprotect' : 'protect' );
$tpl->set( 'helppage', $this->msg( 'helppage' )->text() );
*/
@@ -276,20 +271,20 @@ class SkinTemplate extends Skin {
$tpl->setRef( 'logopath', $wgLogo );
$tpl->setRef( 'sitename', $wgSitename );
- $lang = $this->getLanguage();
- $userlang = $lang->getHtmlCode();
- $userdir = $lang->getDir();
+ $userLang = $this->getLanguage();
+ $userLangCode = $userLang->getHtmlCode();
+ $userLangDir = $userLang->getDir();
- $tpl->set( 'lang', $userlang );
- $tpl->set( 'dir', $userdir );
- $tpl->set( 'rtl', $lang->isRTL() );
+ $tpl->set( 'lang', $userLangCode );
+ $tpl->set( 'dir', $userLangDir );
+ $tpl->set( 'rtl', $userLang->isRTL() );
- $tpl->set( 'capitalizeallnouns', $lang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' );
+ $tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' );
$tpl->set( 'showjumplinks', $user->getOption( 'showjumplinks' ) );
$tpl->set( 'username', $this->loggedin ? $this->username : null );
$tpl->setRef( 'userpage', $this->userpage );
$tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] );
- $tpl->set( 'userlang', $userlang );
+ $tpl->set( 'userlang', $userLangCode );
// Users can have their language set differently than the
// content of the wiki. For these users, tell the web browser
@@ -297,9 +292,9 @@ class SkinTemplate extends Skin {
$tpl->set( 'userlangattributes', '' );
$tpl->set( 'specialpageattributes', '' ); # obsolete
- if ( $userlang !== $wgContLang->getHtmlCode() || $userdir !== $wgContLang->getDir() ) {
- $escUserlang = htmlspecialchars( $userlang );
- $escUserdir = htmlspecialchars( $userdir );
+ if ( $userLangCode !== $wgContLang->getHtmlCode() || $userLangDir !== $wgContLang->getDir() ) {
+ $escUserlang = htmlspecialchars( $userLangCode );
+ $escUserdir = htmlspecialchars( $userLangDir );
// Attributes must be in double quotes because htmlspecialchars() doesn't
// escape single quotes
$attrs = " lang=\"$escUserlang\" dir=\"$escUserdir\"";
@@ -326,13 +321,13 @@ class SkinTemplate extends Skin {
}
}
- if( $wgPageShowWatchingUsers ) {
+ if ( $wgPageShowWatchingUsers ) {
$dbr = wfGetDB( DB_SLAVE );
$num = $dbr->selectField( 'watchlist', 'COUNT(*)',
array( 'wl_title' => $title->getDBkey(), 'wl_namespace' => $title->getNamespace() ),
__METHOD__
);
- if( $num > 0 ) {
+ if ( $num > 0 ) {
$tpl->set( 'numberofwatchingusers',
$this->msg( 'number_of_watching_users_pageview' )->numParams( $num )->parse()
);
@@ -391,12 +386,6 @@ class SkinTemplate extends Skin {
}
}
- if ( $wgDebugComments ) {
- $tpl->setRef( 'debug', $out->mDebugtext );
- } else {
- $tpl->set( 'debug', '' );
- }
-
$tpl->set( 'sitenotice', $this->getSiteNotice() );
$tpl->set( 'bottomscripts', $this->bottomScripts() );
$tpl->set( 'printfooter', $this->printSource() );
@@ -408,10 +397,10 @@ class SkinTemplate extends Skin {
# when the content is different from the UI language, i.e.:
# 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 ) ) &&
+ if ( !in_array( $title->getNamespace(), array( NS_SPECIAL, NS_FILE ) ) &&
in_array( $request->getVal( 'action', 'view' ), array( 'view', 'historysubmit' ) ) &&
( $title->exists() || $title->getNamespace() == NS_MEDIAWIKI ) ) {
- $pageLang = $title->getPageLanguage();
+ $pageLang = $title->getPageViewLanguage();
$realBodyAttribs['lang'] = $pageLang->getHtmlCode();
$realBodyAttribs['dir'] = $pageLang->getDir();
$realBodyAttribs['class'] = 'mw-content-'.$pageLang->getDir();
@@ -430,10 +419,15 @@ class SkinTemplate extends Skin {
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' => ( $wgContLang->getLanguageName( $nt->getInterwiki() ) != '' ?
- $wgContLang->getLanguageName( $nt->getInterwiki() ) : $l ),
+ 'text' => $ilLangName,
'title' => $nt->getText(),
'class' => $class,
'lang' => $nt->getInterwiki(),
@@ -442,7 +436,7 @@ class SkinTemplate extends Skin {
}
}
}
- if( count( $language_urls ) ) {
+ if ( count( $language_urls ) ) {
$tpl->setRef( 'language_urls', $language_urls );
} else {
$tpl->set( 'language_urls', false );
@@ -467,6 +461,7 @@ class SkinTemplate extends Skin {
$tpl->set( 'headscripts', $out->getHeadScripts() . $out->getHeadItems() );
}
+ $tpl->set( 'debug', '' );
$tpl->set( 'debughtml', $this->generateDebugHTML() );
$tpl->set( 'reporttime', wfReportTime() );
@@ -522,6 +517,7 @@ class SkinTemplate extends Skin {
* This is setup as a method so that like with $wgLogo and getLogo() a skin
* can override this setting and always output one or the other if it has
* a reason it can't output one of the two modes.
+ * @return bool
*/
function useCombinedLoginLink() {
global $wgUseCombinedLoginLink;
@@ -561,7 +557,8 @@ class SkinTemplate extends Skin {
'text' => $this->username,
'href' => &$this->userpageUrlDetails['href'],
'class' => $this->userpageUrlDetails['exists'] ? false : 'new',
- 'active' => ( $this->userpageUrlDetails['href'] == $pageurl )
+ 'active' => ( $this->userpageUrlDetails['href'] == $pageurl ),
+ 'dir' => 'auto'
);
$usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage );
$personal_urls['mytalk'] = array(
@@ -584,10 +581,12 @@ class SkinTemplate extends Skin {
);
# We need to do an explicit check for Special:Contributions, as we
- # have to match both the title, and the target (which could come
- # from request values or be specified in "sub page" form. The plot
+ # have to match both the title, and the target, which could come
+ # from request values (Special:Contributions?target=Jimbo_Wales)
+ # or be specified in "sub page" form
+ # (Special:Contributions/Jimbo_Wales). The plot
# thickens, because the Title object is altered for special pages,
- # so doesn't contain the original alias-with-subpage.
+ # so it doesn't contain the original alias-with-subpage.
$origTitle = Title::newFromText( $request->getText( 'title' ) );
if( $origTitle instanceof Title && $origTitle->isSpecialPage() ) {
list( $spName, $spPar ) = SpecialPageFactory::resolveAlias( $origTitle->getText() );
@@ -618,37 +617,25 @@ class SkinTemplate extends Skin {
$loginlink = $this->getUser()->isAllowed( 'createaccount' ) && $useCombinedLoginLink
? 'nav-login-createaccount'
: 'login';
- $is_signup = $request->getText('type') == "signup";
+ $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 ),
- 'active' => $title->isSpecial( 'Userlogin' ) && ( $loginlink == "nav-login-createaccount" || !$is_signup )
+ 'href' => self::makeSpecialUrl( 'Userlogin', $returnto, $proto ),
+ '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 ),
+ 'active' => $title->isSpecial( 'Userlogin' ) && $is_signup,
+ 'class' => $wgSecureLogin ? 'link-https' : ''
);
- if ( $this->getUser()->isAllowed( 'createaccount' ) && !$useCombinedLoginLink ) {
- $createaccount_url = array(
- 'text' => $this->msg( 'createaccount' )->text(),
- 'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ),
- 'active' => $title->isSpecial( 'Userlogin' ) && $is_signup
- );
- }
- global $wgServer, $wgSecureLogin;
- if( substr( $wgServer, 0, 5 ) === 'http:' && $wgSecureLogin ) {
- $title = SpecialPage::getTitleFor( 'Userlogin' );
- $https_url = preg_replace( '/^http:/', 'https:', $title->getFullURL() );
- $login_url['href'] = $https_url;
- # @todo FIXME: Class depends on skin
- $login_url['class'] = 'link-https';
- if ( isset($createaccount_url) ) {
- $https_url = preg_replace( '/^http:/', 'https:',
- $title->getFullURL("type=signup") );
- $createaccount_url['href'] = $https_url;
- # @todo FIXME: Class depends on skin
- $createaccount_url['class'] = 'link-https';
- }
- }
-
if( $this->showIPinHeader() ) {
$href = &$this->userpageUrlDetails['href'];
@@ -666,13 +653,13 @@ class SkinTemplate extends Skin {
'class' => $usertalkUrlDetails['exists'] ? false : 'new',
'active' => ( $pageurl == $href )
);
- $personal_urls['anonlogin'] = $login_url;
- } else {
- $personal_urls['login'] = $login_url;
}
- if ( isset($createaccount_url) ) {
+
+ if ( $this->getUser()->isAllowed( 'createaccount' ) && !$useCombinedLoginLink ) {
$personal_urls['createaccount'] = $createaccount_url;
}
+
+ $personal_urls[$login_id] = $login_url;
}
wfRunHooks( 'PersonalUrls', array( &$personal_urls, &$title ) );
@@ -702,9 +689,9 @@ class SkinTemplate extends Skin {
// wfMessageFallback will nicely accept $message as an array of fallbacks
// or just a single key
$msg = wfMessageFallback( $message )->setContext( $this->getContext() );
- if ( is_array($message) ) {
+ if ( is_array( $message ) ) {
// for hook compatibility just keep the last message name
- $message = end($message);
+ $message = end( $message );
}
if ( $msg->exists() ) {
$text = $msg->text();
@@ -789,8 +776,9 @@ class SkinTemplate extends Skin {
wfProfileIn( __METHOD__ );
- $title = $this->getRelevantTitle(); // Display tabs for the relevant title rather than always the title itself
- $onPage = $title->equals($this->getTitle());
+ // Display tabs for the relevant title rather than always the title itself
+ $title = $this->getRelevantTitle();
+ $onPage = $title->equals( $this->getTitle() );
$out = $this->getOutput();
$request = $this->getRequest();
@@ -834,7 +822,7 @@ class SkinTemplate extends Skin {
// Adds namespace links
$subjectMsg = array( "nstab-$subjectId" );
if ( $subjectPage->isMainPage() ) {
- array_unshift($subjectMsg, 'mainpage-nstab');
+ array_unshift( $subjectMsg, 'mainpage-nstab' );
}
$content_navigation['namespaces'][$subjectId] = $this->tabAction(
$subjectPage, $subjectMsg, !$isTalk && !$preventActiveTabs, '', $userCanRead
@@ -851,9 +839,10 @@ class SkinTemplate extends Skin {
$content_navigation['views']['view'] = $this->tabAction(
$isTalk ? $talkPage : $subjectPage,
array( "$skname-view-view", 'view' ),
- ( $onPage && ($action == 'view' || $action == 'purge' ) ), '', true
+ ( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true
);
- $content_navigation['views']['view']['redundant'] = true; // signal to hide this from simple content_actions
+ // signal to hide this from simple content_actions
+ $content_navigation['views']['view']['redundant'] = true;
}
wfProfileIn( __METHOD__ . '-edit' );
@@ -871,14 +860,14 @@ class SkinTemplate extends Skin {
$section = $request->getVal( 'section' );
$msgKey = $title->exists() || ( $title->getNamespace() == NS_MEDIAWIKI && $title->getDefaultMessageText() !== false ) ?
- "edit" : "create";
+ 'edit' : 'create';
$content_navigation['views']['edit'] = array(
'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection ) ? 'selected' : '' ) . $isTalkClass,
'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey )->setContext( $this->getContext() )->text(),
'href' => $title->getLocalURL( $this->editUrlOptions() ),
'primary' => true, // don't collapse this in vector
);
-
+
// section link
if ( $showNewSection ) {
// Adds new section link
@@ -932,7 +921,7 @@ class SkinTemplate extends Skin {
// article doesn't exist or is deleted
if ( $user->isAllowed( 'deletedhistory' ) ) {
$n = $title->isDeleted();
- if( $n ) {
+ if ( $n ) {
$undelTitle = SpecialPage::getTitleFor( 'Undelete' );
// If the user can't undelete but can view deleted history show them a "View .. deleted" tab instead
$msgKey = $user->isAllowed( 'undelete' ) ? 'undelete' : 'viewdeleted';
@@ -968,11 +957,12 @@ class SkinTemplate extends Skin {
* a change to that procedure these messages will have to remain as
* the global versions.
*/
- $mode = $title->userIsWatching() ? 'unwatch' : 'watch';
+ $mode = $user->isWatched( $title ) ? 'unwatch' : 'watch';
$token = WatchAction::getWatchToken( $title, $user, $mode );
$content_navigation['actions'][$mode] = array(
'class' => $onPage && ( $action == 'watch' || $action == 'unwatch' ) ? 'selected' : false,
- 'text' => $this->msg( $mode )->text(), // uses 'watch' or 'unwatch' message
+ // uses 'watch' or 'unwatch' message
+ 'text' => $this->msg( $mode )->text(),
'href' => $title->getLocalURL( array( 'action' => $mode, 'token' => $token ) )
);
}
@@ -986,8 +976,8 @@ class SkinTemplate extends Skin {
$variants = $pageLang->getVariants();
// Checks that language conversion is enabled and variants exist
// And if it is not in the special namespace
- if( count( $variants ) > 1 ) {
- // Gets preferred variant (note that user preference is
+ if ( count( $variants ) > 1 ) {
+ // Gets preferred variant (note that user preference is
// only possible for wiki content language variant)
$preferred = $pageLang->getPreferredVariant();
// Loops over each variant
@@ -1003,7 +993,9 @@ class SkinTemplate extends Skin {
$content_navigation['variants'][] = array(
'class' => ( $code == $preferred ) ? 'selected' : false,
'text' => $varname,
- 'href' => $title->getLocalURL( array( 'variant' => $code ) )
+ 'href' => $title->getLocalURL( array( 'variant' => $code ) ),
+ 'lang' => $code,
+ 'hreflang' => $code
);
}
}
@@ -1013,7 +1005,7 @@ class SkinTemplate extends Skin {
$content_navigation['namespaces']['special'] = array(
'class' => 'selected',
'text' => $this->msg( 'nstab-special' )->text(),
- 'href' => $request->getRequestURL(), // @bug 2457, 2510
+ 'href' => $request->getRequestURL(), // @see: bug 2457, bug 2510
'context' => 'subject'
);
@@ -1032,7 +1024,7 @@ class SkinTemplate extends Skin {
$xmlID = 'ca-nstab-' . $xmlID;
} elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) {
$xmlID = 'ca-talk';
- } elseif ( $section == "variants" ) {
+ } elseif ( $section == 'variants' ) {
$xmlID = 'ca-varlang-' . $xmlID;
} else {
$xmlID = 'ca-' . $xmlID;
@@ -1047,14 +1039,14 @@ class SkinTemplate extends Skin {
# give the edit tab an accesskey, because that's fairly su-
# perfluous 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']) ) {
+ if ( in_array( $action, array( 'edit', 'submit' ) ) ) {
+ if ( isset( $content_navigation['views']['edit'] ) ) {
$content_navigation['views']['edit']['tooltiponly'] = true;
}
- if ( isset($content_navigation['actions']['watch']) ) {
+ if ( isset( $content_navigation['actions']['watch'] ) ) {
$content_navigation['actions']['watch']['tooltiponly'] = true;
}
- if ( isset($content_navigation['actions']['unwatch']) ) {
+ if ( isset( $content_navigation['actions']['unwatch'] ) ) {
$content_navigation['actions']['unwatch']['tooltiponly'] = true;
}
}
@@ -1083,7 +1075,7 @@ class SkinTemplate extends Skin {
foreach ( $links as $key => $value ) {
- if ( isset($value["redundant"]) && $value["redundant"] ) {
+ if ( isset( $value['redundant'] ) && $value['redundant'] ) {
// Redundant tabs are dropped from content_actions
continue;
}
@@ -1092,11 +1084,11 @@ class SkinTemplate extends Skin {
// so the xmlID based id is much closer to the actual $key that we want
// for that reason we'll just strip out the ca- if present and use
// the latter potion of the "id" as the $key
- if ( isset($value["id"]) && substr($value["id"], 0, 3) == "ca-" ) {
- $key = substr($value["id"], 3);
+ if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) {
+ $key = substr( $value['id'], 3 );
}
- if ( isset($content_actions[$key]) ) {
+ if ( isset( $content_actions[$key] ) ) {
wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening content_navigation into content_actions." );
continue;
}
@@ -1147,7 +1139,7 @@ class SkinTemplate extends Skin {
// A print stylesheet is attached to all pages, but nobody ever
// figures that out. :) Add a link...
- if( $out->isArticle() ) {
+ if ( $out->isArticle() ) {
if ( !$out->isPrintable() ) {
$nav_urls['print'] = array(
'text' => $this->msg( 'printableversion' )->text(),
@@ -1161,7 +1153,7 @@ class SkinTemplate extends Skin {
if ( $revid ) {
$nav_urls['permalink'] = array(
'text' => $this->msg( 'permalink' )->text(),
- 'href' => $out->getTitle()->getLocalURL( "oldid=$revid" )
+ 'href' => $this->getTitle()->getLocalURL( "oldid=$revid" )
);
}
@@ -1174,7 +1166,7 @@ class SkinTemplate extends Skin {
$nav_urls['whatlinkshere'] = array(
'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage )->getLocalUrl()
);
- if ( $this->getTitle()->getArticleId() ) {
+ if ( $this->getTitle()->getArticleID() ) {
$nav_urls['recentchangeslinked'] = array(
'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalUrl()
);
@@ -1189,12 +1181,9 @@ class SkinTemplate extends Skin {
'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser )
);
- if ( $user->isLoggedIn() ) {
- $logPage = SpecialPage::getTitleFor( 'Log' );
- $nav_urls['log'] = array(
- 'href' => $logPage->getLocalUrl( array( 'user' => $rootUser ) )
- );
- }
+ $nav_urls['log'] = array(
+ 'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser )
+ );
if ( $this->getUser()->isAllowed( 'block' ) ) {
$nav_urls['blockip'] = array(
@@ -1319,6 +1308,7 @@ abstract class QuickTemplate {
/**
* @private
+ * @return bool
*/
function haveData( $str ) {
return isset( $this->data[$str] );
@@ -1354,7 +1344,7 @@ abstract class BaseTemplate extends QuickTemplate {
/**
* Get a Message object with its context set
*
- * @param $name Str message name
+ * @param $name string message name
* @return Message
*/
public function getMsg( $name ) {
@@ -1378,6 +1368,7 @@ abstract class BaseTemplate extends QuickTemplate {
* stored by SkinTemplate.
* The resulting array is built acording to a format intended to be passed
* through makeListItem to generate the html.
+ * @return array
*/
function getToolbox() {
wfProfileIn( __METHOD__ );
@@ -1411,12 +1402,13 @@ abstract class BaseTemplate extends QuickTemplate {
}
if ( isset( $this->data['nav_urls']['print'] ) && $this->data['nav_urls']['print'] ) {
$toolbox['print'] = $this->data['nav_urls']['print'];
+ $toolbox['print']['id'] = 't-print';
$toolbox['print']['rel'] = 'alternate';
$toolbox['print']['msg'] = 'printableversion';
}
if ( isset( $this->data['nav_urls']['permalink'] ) && $this->data['nav_urls']['permalink'] ) {
$toolbox['permalink'] = $this->data['nav_urls']['permalink'];
- if( $toolbox['permalink']['href'] === '' ) {
+ if ( $toolbox['permalink']['href'] === '' ) {
unset( $toolbox['permalink']['href'] );
$toolbox['ispermalink']['tooltiponly'] = true;
$toolbox['ispermalink']['id'] = 't-ispermalink';
@@ -1438,23 +1430,28 @@ abstract class BaseTemplate extends QuickTemplate {
* 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
* to makeListItem instead of hardcoding the element creation boilerplate.
+ * @return array
*/
function getPersonalTools() {
$personal_tools = array();
- foreach( $this->data['personal_urls'] as $key => $ptool ) {
+ foreach ( $this->data['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
- $personal_tools[$key] = array();
- $personal_tools[$key]["links"][] = array();
- $personal_tools[$key]["links"][0]["single-id"] = $personal_tools[$key]["id"] = "pt-$key";
- if ( isset($ptool["active"]) ) {
- $personal_tools[$key]["active"] = $ptool["active"];
+ # of using most of the personal_url's keys directly.
+ $ptool = array(
+ 'links' => array(
+ array( 'single-id' => "pt-$key" ),
+ ),
+ 'id' => "pt-$key",
+ );
+ if ( isset( $plink['active'] ) ) {
+ $ptool['active'] = $plink['active'];
}
- foreach ( array("href", "class", "text") as $k ) {
- if ( isset($ptool[$k]) )
- $personal_tools[$key]["links"][0][$k] = $ptool[$k];
+ foreach ( array( 'href', 'class', 'text' ) as $k ) {
+ if ( isset( $plink[$k] ) )
+ $ptool['links'][0][$k] = $plink[$k];
}
+ $personal_tools[$key] = $ptool;
}
return $personal_tools;
}
@@ -1471,7 +1468,7 @@ abstract class BaseTemplate extends QuickTemplate {
if ( !isset( $sidebar['LANGUAGES'] ) ) {
$sidebar['LANGUAGES'] = true;
}
-
+
if ( !isset( $options['search'] ) || $options['search'] !== true ) {
unset( $sidebar['SEARCH'] );
}
@@ -1481,7 +1478,7 @@ abstract class BaseTemplate extends QuickTemplate {
if ( isset( $options['languages'] ) && $options['languages'] === false ) {
unset( $sidebar['LANGUAGES'] );
}
-
+
$boxes = array();
foreach ( $sidebar as $boxName => $content ) {
if ( $content === false ) {
@@ -1491,7 +1488,7 @@ abstract class BaseTemplate extends QuickTemplate {
case 'SEARCH':
// Search is a special case, skins should custom implement this
$boxes[$boxName] = array(
- 'id' => "p-search",
+ 'id' => 'p-search',
'header' => $this->getMsg( 'search' )->text(),
'generated' => false,
'content' => true,
@@ -1500,7 +1497,7 @@ abstract class BaseTemplate extends QuickTemplate {
case 'TOOLBOX':
$msgObj = $this->getMsg( 'toolbox' );
$boxes[$boxName] = array(
- 'id' => "p-tb",
+ 'id' => 'p-tb',
'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
'generated' => false,
'content' => $this->getToolbox(),
@@ -1510,12 +1507,12 @@ abstract class BaseTemplate extends QuickTemplate {
if ( $this->data['language_urls'] ) {
$msgObj = $this->getMsg( 'otherlanguages' );
$boxes[$boxName] = array(
- 'id' => "p-lang",
+ 'id' => 'p-lang',
'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
'generated' => false,
'content' => $this->data['language_urls'],
);
- }
+ }
break;
default:
$msgObj = $this->getMsg( $boxName );
@@ -1528,7 +1525,7 @@ abstract class BaseTemplate extends QuickTemplate {
break;
}
}
-
+
// HACK: Compatibility with extensions still using SkinTemplateToolboxEnd
$hookContents = null;
if ( isset( $boxes['TOOLBOX'] ) ) {
@@ -1543,17 +1540,17 @@ abstract class BaseTemplate extends QuickTemplate {
}
}
// END hack
-
+
if ( isset( $options['htmlOnly'] ) && $options['htmlOnly'] === true ) {
foreach ( $boxes as $boxName => $box ) {
if ( is_array( $box['content'] ) ) {
- $content = "<ul>";
+ $content = '<ul>';
foreach ( $box['content'] as $key => $val ) {
$content .= "\n " . $this->makeListItem( $key, $val );
}
// HACK, shove the toolbox end onto the toolbox if we're rendering itself
if ( $hookContents ) {
- $content .= "\n $hookContents";
+ $content .= "\n $hookContents";
}
// END hack
$content .= "\n</ul>\n";
@@ -1563,7 +1560,7 @@ abstract class BaseTemplate extends QuickTemplate {
} else {
if ( $hookContents ) {
$boxes['TOOLBOXEND'] = array(
- 'id' => "p-toolboxend",
+ 'id' => 'p-toolboxend',
'header' => $boxes['TOOLBOX']['header'],
'generated' => false,
'content' => "<ul>{$hookContents}</ul>",
@@ -1583,7 +1580,7 @@ abstract class BaseTemplate extends QuickTemplate {
// END hack
}
}
-
+
return $boxes;
}
@@ -1591,26 +1588,40 @@ 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...
*
- * $key is a string, usually a key from the list you are generating this link from
- * $item is an array containing 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 that name will
- * be used, and if neither of those are set the $key will be used as a message name.
+ * @param $key string usually a key from the list you are generating this
+ * link from.
+ * @param $item array 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
+ * that name will be used, and if neither of those are set the $key will be
+ * used as a message name.
+ *
* If a "href" key is not present makeLink will just output htmlescaped text.
- * The href, id, class, rel, and type keys are used as attributes for the link if present.
- * If an "id" or "single-id" (if you don't want the actual id to be output on the link)
- * is present it will be used to generate a tooltip and accesskey for the link.
+ * The "href", "id", "class", "rel", and "type" keys are used as attributes
+ * for the link if present.
+ *
+ * If an "id" or "single-id" (if you don't want the actual id to be output
+ * on the link) is present it will be used to generate a tooltip and
+ * accesskey for the link.
+ *
* If you don't want an accesskey, set $item['tooltiponly'] = true;
- * $options can be used to affect the output of a link:
- * You can use a 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
- * optionally an 'attributes' key. If you only have one element you don't need
- * to wrap it in another array. eg: To use <a><span>...</span></a> in all links
- * use array( 'text-wrapper' => array( 'tag' => 'span' ) ) for your options.
- * A link-class key can be used to specify additional classes to apply to all links.
- * A link-fallback can be used to specify a tag to use instead of <a> if there is
- * no link. eg: If you specify 'link-fallback' => 'span' than any non-link will
- * output a <span> instead of just text.
+ *
+ * @param $options array 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
+ * optionally an 'attributes' key. If you only have one element you don't
+ * need to wrap it in another array. eg: To use <a><span>...</span></a>
+ * in all links use array( 'text-wrapper' => array( 'tag' => 'span' ) )
+ * for your options.
+ * - 'link-class' key can be used to specify additional classes to apply
+ * to all links.
+ * - 'link-fallback' can be used to specify a tag to use instead of "<a>"
+ * if there is no link. eg: If you specify 'link-fallback' => 'span' than
+ * any non-link will output a "<span>" instead of just text.
+ *
+ * @return string
*/
function makeLink( $key, $item, $options = array() ) {
if ( isset( $item['text'] ) ) {
@@ -1671,17 +1682,22 @@ abstract class BaseTemplate extends QuickTemplate {
}
/**
- * Generates a list item for a navigation, portlet, portal, sidebar... etc list
- * $key is a string, usually a key from the list you are generating this link from
- * $item is an array of list item data containing some of a specific set of keys.
+ * Generates a list item for a navigation, portlet, portal, sidebar... list
+ *
+ * @param $key string, usually a key from the list you are generating this link from.
+ * @param $item array, of list item data containing some of a specific set of keys.
* The "id" and "class" keys will be used as attributes for the list item,
* if "active" contains a value of true a "active" class will also be appended to class.
- * If you want something other than a <li> you can pass a tag name such as
+ *
+ * @param $options array
+ *
+ * If you want something other than a "<li>" you can pass a tag name such as
* "tag" => "span" in the $options array to change the tag used.
* link/content data for the list item may come in one of two forms
* A "links" key may be used, in which case it should contain an array with
- * a list of links to include inside the list item, see makeLink for the format
- * of individual links array items.
+ * a list of links to include inside the list item, see makeLink for the
+ * format of individual links array items.
+ *
* Otherwise the relevant keys from the list item $item array will be passed
* to makeLink instead. Note however that "id" and "class" are used by the
* list item directly so they will not be passed to makeLink
@@ -1689,6 +1705,8 @@ abstract class BaseTemplate extends QuickTemplate {
* If you need an id or class on a single link you should include a "links"
* array with just one link item inside of it.
* $options is also passed on to makeLink calls
+ *
+ * @return string
*/
function makeListItem( $key, $item, $options = array() ) {
if ( isset( $item['links'] ) ) {
@@ -1783,6 +1801,7 @@ abstract class BaseTemplate extends QuickTemplate {
* If you pass "flat" as an option then the returned array will be a flat array
* of footer icons instead of a key/value array of footerlinks arrays broken
* up into categories.
+ * @return array|mixed
*/
function getFooterLinks( $option = null ) {
$footerlinks = $this->data['footerlinks'];
@@ -1821,6 +1840,7 @@ abstract class BaseTemplate extends QuickTemplate {
* in the list of footer icons. This is mostly useful for skins which only
* display the text from footericons instead of the images and don't want a
* duplicate copyright statement because footerlinks already rendered one.
+ * @return
*/
function getFooterIcons( $option = null ) {
// Generate additional footer icons
@@ -1857,15 +1877,9 @@ abstract class BaseTemplate extends QuickTemplate {
* body and html tags.
*/
function printTrail() { ?>
-<?php $this->html('bottomscripts'); /* JS call to runBodyOnloadHook */ ?>
-<?php $this->html('reporttime') ?>
-<?php if ( $this->data['debug'] ): ?>
-<!-- Debug output:
-<?php $this->text( 'debug' ); ?>
-
--->
-<?php endif;
+<?php $this->html( 'bottomscripts' ); /* JS call to runBodyOnloadHook */ ?>
+<?php $this->html( 'reporttime' ) ?>
+<?php echo MWDebug::getDebugHTML( $this->getSkin()->getContext() );
}
}
-
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
index 17a36ecf..2e5e02b0 100644
--- a/includes/SpecialPage.php
+++ b/includes/SpecialPage.php
@@ -1,25 +1,24 @@
<?php
/**
- * SpecialPage: handling special pages and lists thereof.
+ * Parent class for all special pages.
*
- * To add a special page in an extension, add to $wgSpecialPages either
- * an object instance or an array containing the name and constructor
- * parameters. The latter is preferred for performance reasons.
+ * 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.
*
- * The object instantiated must be either an instance of SpecialPage or a
- * sub-class thereof. It must have an execute() method, which sends the HTML
- * for the special page to $wgOut. The parent class has an execute() method
- * which distributes the call to the historical global functions. Additionally,
- * execute() also checks if the user has the necessary access privileges
- * and bails out if not.
+ * 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.
*
- * To add a core special page, use the similar static list in
- * SpecialPage::$mList. To remove a core static special page at runtime, use
- * a SpecialPage_initList hook.
+ * 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
- * @defgroup SpecialPage SpecialPage
*/
/**
@@ -124,19 +123,18 @@ class SpecialPage {
*
* @param $page Mixed: SpecialPage or string
* @param $group String
- * @return null
* @deprecated since 1.18 call SpecialPageFactory method directly
*/
static function setGroup( $page, $group ) {
wfDeprecated( __METHOD__, '1.18' );
- return SpecialPageFactory::setGroup( $page, $group );
+ SpecialPageFactory::setGroup( $page, $group );
}
/**
* Get the group that the special page belongs in on Special:SpecialPage
*
* @param $page SpecialPage
- * @return null
+ * @return string
* @deprecated since 1.18 call SpecialPageFactory method directly
*/
static function getGroup( &$page ) {
@@ -200,7 +198,7 @@ class SpecialPage {
*
* @param $user User object to check permissions, $wgUser will be used
* if not provided
- * @return Associative array mapping page's name to its SpecialPage object
+ * @return array Associative array mapping page's name to its SpecialPage object
* @deprecated since 1.18 call SpecialPageFactory method directly
*/
static function getUsablePages( User $user = null ) {
@@ -211,7 +209,7 @@ class SpecialPage {
/**
* Return categorised listable special pages for all users
*
- * @return Associative array mapping page's name to its SpecialPage object
+ * @return array Associative array mapping page's name to its SpecialPage object
* @deprecated since 1.18 call SpecialPageFactory method directly
*/
static function getRegularPages() {
@@ -223,7 +221,7 @@ class SpecialPage {
* Return categorised listable special pages which are available
* for the current user, but not for everyone
*
- * @return Associative array mapping page's name to its SpecialPage object
+ * @return array Associative array mapping page's name to its SpecialPage object
* @deprecated since 1.18 call SpecialPageFactory method directly
*/
static function getRestrictedPages() {
@@ -353,7 +351,7 @@ class SpecialPage {
$this->mFunction = $function;
}
if ( $file === 'default' ) {
- $this->mFile = dirname( __FILE__ ) . "/specials/Special$name.php";
+ $this->mFile = __DIR__ . "/specials/Special$name.php";
} else {
$this->mFile = $file;
}
@@ -592,14 +590,69 @@ class SpecialPage {
}
/**
+ * Entry point.
+ *
+ * @since 1.20
+ *
+ * @param $subPage string|null
+ */
+ public final function run( $subPage ) {
+ /**
+ * Gets called before @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param $special SpecialPage
+ * @param $subPage string|null
+ */
+ wfRunHooks( 'SpecialPageBeforeExecute', array( $this, $subPage ) );
+
+ $this->beforeExecute( $subPage );
+ $this->execute( $subPage );
+ $this->afterExecute( $subPage );
+
+ /**
+ * Gets called after @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param $special SpecialPage
+ * @param $subPage string|null
+ */
+ wfRunHooks( 'SpecialPageAfterExecute', array( $this, $subPage ) );
+ }
+
+ /**
+ * Gets called before @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param $subPage string|null
+ */
+ protected function beforeExecute( $subPage ) {
+ // No-op
+ }
+
+ /**
+ * Gets called after @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param $subPage string|null
+ */
+ protected function afterExecute( $subPage ) {
+ // No-op
+ }
+
+ /**
* Default execute method
* Checks user permissions, calls the function given in mFunction
*
* This must be overridden by subclasses; it will be made abstract in a future version
*
- * @param $par String subpage string, if one was specified
+ * @param $subPage string|null
*/
- function execute( $par ) {
+ public function execute( $subPage ) {
$this->setHeaders();
$this->checkPermissions();
@@ -609,7 +662,7 @@ class SpecialPage {
require_once( $this->mFile );
}
$this->outputHeader();
- call_user_func( $func, $par, $this );
+ call_user_func( $func, $subPage, $this );
}
/**
@@ -628,7 +681,7 @@ class SpecialPage {
} else {
$msg = $summaryMessageKey;
}
- if ( !$this->msg( $msg )->isBlank() && !$this->including() ) {
+ if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) {
$this->getOutput()->wrapWikiMsg(
"<div class='mw-specialpage-summary'>\n$1\n</div>", $msg );
}
@@ -768,7 +821,15 @@ class SpecialPage {
// Works fine as the first parameter, which appears elsewhere in the
// code base. Sighhhh.
$args = func_get_args();
- return call_user_func_array( array( $this->getContext(), 'msg' ), $args );
+ $message = call_user_func_array( array( $this->getContext(), 'msg' ), $args );
+ // 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.
+ if ( $this->including() ) {
+ $message->setInterfaceMessageFlag( false );
+ }
+ return $message;
}
/**
@@ -891,7 +952,7 @@ abstract class FormSpecialPage extends SpecialPage {
$this->checkPermissions();
if ( $this->requiresUnblock() && $user->isBlocked() ) {
- $block = $user->mBlock;
+ $block = $user->getBlock();
throw new UserBlockedError( $block );
}
@@ -988,7 +1049,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
* False otherwise.
*
* @param $par String Subpage string
- * @return Title|false
+ * @return Title|bool
*/
abstract public function getRedirect( $par );
@@ -1076,16 +1137,102 @@ class SpecialCreateAccount extends SpecialRedirectToSpecial {
*/
/**
+ * Superclass for any RedirectSpecialPage which redirects the user
+ * to a particular article (as opposed to user contributions, logs, etc.).
+ *
+ * For security reasons these special pages are restricted to pass on
+ * the following subset of GET parameters to the target page while
+ * removing all others:
+ *
+ * - useskin, uselang, printable: to alter the appearance of the resulting page
+ *
+ * - redirect: allows viewing one's user page or talk page even if it is a
+ * redirect.
+ *
+ * - rdfrom: allows redirecting to one's user page or talk page from an
+ * external wiki with the "Redirect from..." notice.
+ *
+ * - 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
+ *
+ * - feed: would allow linking to the current user's RSS feed for their user
+ * talk page:
+ * http://en.wikipedia.org/w/index.php?title=Special:MyTalk&action=history&feed=rss
+ *
+ * - preloadtitle: Can be used to provide a default section title for a
+ * preloaded new comment on one's own talk page.
+ *
+ * - summary : Can be used to provide a default edit summary for a preloaded
+ * edit to one's own user page or talk page.
+ *
+ * - preview: Allows showing/hiding preview on first edit regardless of user
+ * preference, useful for preloaded edits where you know preview wouldn't be
+ * useful.
+ *
+ * - internaledit, externaledit, mode: Allows forcing the use of the
+ * internal/external editor, e.g. to force the internal editor for
+ * short/simple preloaded edits.
+ *
+ * - redlink: Affects the message the user sees if their talk page/user talk
+ * page does not currently exist. Avoids confusion for newbies with no user
+ * pages over why they got a "permission error" following this link:
+ * http://en.wikipedia.org/w/index.php?title=Special:MyPage&redlink=1
+ *
+ * - debug: determines whether the debug parameter is passed to load.php,
+ * which disables reformatting and allows scripts to be debugged. Useful
+ * when debugging scripts that manipulate one's own user page or talk page.
+ *
+ * @par Hook extension:
+ * Extensions can add to the redirect parameters list by using the hook
+ * RedirectSpecialArticleRedirectParams
+ *
+ * This hook allows extensions which add GET parameters like FlaggedRevs to
+ * retain those parameters when redirecting using special pages.
+ *
+ * @par Hook extension example:
+ * @code
+ * $wgHooks['RedirectSpecialArticleRedirectParams'][] =
+ * 'MyExtensionHooks::onRedirectSpecialArticleRedirectParams';
+ * public static function onRedirectSpecialArticleRedirectParams( &$redirectParams ) {
+ * $redirectParams[] = 'stable';
+ * return true;
+ * }
+ * @endcode
+ * @ingroup SpecialPage
+ */
+abstract class RedirectSpecialArticle extends RedirectSpecialPage {
+ function __construct( $name ) {
+ parent::__construct( $name );
+ $redirectParams = array(
+ 'action',
+ 'redirect', 'rdfrom',
+ # Options for preloaded edits
+ 'preload', 'editintro', 'preloadtitle', 'summary',
+ # Options for overriding user settings
+ 'preview', 'internaledit', 'externaledit', 'mode',
+ # Options for history/diffs
+ 'section', 'oldid', 'diff', 'dir',
+ 'limit', 'offset', 'feed',
+ # Misc options
+ 'redlink', 'debug',
+ # Options for action=raw; missing ctype can break JS or CSS in some browsers
+ 'ctype', 'maxage', 'smaxage',
+ );
+
+ wfRunHooks( "RedirectSpecialArticleRedirectParams", array(&$redirectParams) );
+ $this->mAllowedRedirectParams = $redirectParams;
+ }
+}
+
+/**
* Shortcut to construct a special page pointing to current user user's page.
* @ingroup SpecialPage
*/
-class SpecialMypage extends RedirectSpecialPage {
+class SpecialMypage extends RedirectSpecialArticle {
function __construct() {
parent::__construct( 'Mypage' );
- $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro',
- 'section', 'oldid', 'diff', 'dir',
- // Options for action=raw; missing ctype can break JS or CSS in some browsers
- 'ctype', 'maxage', 'smaxage' );
}
function getRedirect( $subpage ) {
@@ -1101,11 +1248,9 @@ class SpecialMypage extends RedirectSpecialPage {
* Shortcut to construct a special page pointing to current user talk page.
* @ingroup SpecialPage
*/
-class SpecialMytalk extends RedirectSpecialPage {
+class SpecialMytalk extends RedirectSpecialArticle {
function __construct() {
parent::__construct( 'Mytalk' );
- $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro',
- 'section', 'oldid', 'diff', 'dir' );
}
function getRedirect( $subpage ) {
diff --git a/includes/SpecialPageFactory.php b/includes/SpecialPageFactory.php
index 0a1631b0..95f75a8e 100644
--- a/includes/SpecialPageFactory.php
+++ b/includes/SpecialPageFactory.php
@@ -1,6 +1,29 @@
<?php
/**
- * SpecialPage: handling special pages and lists thereof.
+ * Factory for handling the special page list and generating SpecialPage 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 SpecialPage
+ * @defgroup SpecialPage SpecialPage
+ */
+
+/**
+ * Factory for handling the special page list and generating SpecialPage objects.
*
* To add a special page in an extension, add to $wgSpecialPages either
* an object instance or an array containing the name and constructor
@@ -17,13 +40,6 @@
* SpecialPage::$mList. To remove a core static special page at runtime, use
* a SpecialPage_initList hook.
*
- * @file
- * @ingroup SpecialPage
- * @defgroup SpecialPage SpecialPage
- */
-
-/**
- * Factory for handling the special page list and generating SpecialPage objects
* @ingroup SpecialPage
* @since 1.17
*/
@@ -118,6 +134,7 @@ class SpecialPageFactory {
// High use pages
'Mostlinkedcategories' => 'MostlinkedCategoriesPage',
'Mostimages' => 'MostimagesPage',
+ 'Mostinterwikis' => 'MostinterwikisPage',
'Mostlinked' => 'MostlinkedPage',
'Mostlinkedtemplates' => 'MostlinkedTemplatesPage',
'Mostcategories' => 'MostcategoriesPage',
@@ -276,6 +293,7 @@ class SpecialPageFactory {
* Get the group that the special page belongs in on Special:SpecialPage
*
* @param $page SpecialPage
+ * @return String
*/
public static function getGroup( &$page ) {
$name = $page->getName();
@@ -473,7 +491,7 @@ class SpecialPageFactory {
// Execute special page
$profName = 'Special:' . $page->getName();
wfProfileIn( $profName );
- $page->execute( $par );
+ $page->run( $par );
wfProfileOut( $profName );
wfProfileOut( __METHOD__ );
return true;
diff --git a/includes/SqlDataUpdate.php b/includes/SqlDataUpdate.php
new file mode 100644
index 00000000..52c9be00
--- /dev/null
+++ b/includes/SqlDataUpdate.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * Base code for update jobs that put some secondary data extracted
+ * from article content into the 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
+ */
+
+/**
+ * Abstract base class for update jobs that put some secondary data extracted
+ * from article content into the database.
+ *
+ * @note: subclasses should NOT start or commit transactions in their doUpdate() method,
+ * a transaction will automatically be wrapped around the update. Starting another
+ * one would break the outer transaction bracket. If need be, subclasses can override
+ * the beginTransaction() and commitTransaction() methods.
+ */
+abstract class SqlDataUpdate extends DataUpdate {
+
+ protected $mDb; //!< Database connection reference
+ protected $mOptions; //!< SELECT options to be used (array)
+
+ private $mHasTransaction; //!< bool whether a transaction is open on this object (internal use only!)
+ protected $mUseTransaction; //!< bool whether this update should be wrapped in a transaction
+
+ /**
+ * Constructor
+ *
+ * @param bool $withTransaction whether this update should be wrapped in a transaction (default: true).
+ * A transaction is only started if no transaction is already in progress,
+ * see beginTransaction() for details.
+ **/
+ public function __construct( $withTransaction = true ) {
+ global $wgAntiLockFlags;
+
+ parent::__construct( );
+
+ if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
+ $this->mOptions = array();
+ } else {
+ $this->mOptions = array( 'FOR UPDATE' );
+ }
+
+ // @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;
+ $this->mHasTransaction = false;
+ }
+
+ /**
+ * Begin a database transaction, if $withTransaction was given as true in the constructor for this SqlDataUpdate.
+ *
+ * Because nested transactions are not supported by the Database class, this implementation
+ * checks Database::trxLevel() and only opens a transaction if none is already active.
+ */
+ public function beginTransaction() {
+ if ( !$this->mWithTransaction ) {
+ return;
+ }
+
+ // 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->mHasTransaction = true;
+ }
+ }
+
+ /**
+ * Commit the database transaction started via beginTransaction (if any).
+ */
+ public function commitTransaction() {
+ if ( $this->mHasTransaction ) {
+ $this->mDb->commit( get_class( $this ) . '::commitTransaction' );
+ $this->mHasTransaction = false;
+ }
+ }
+
+ /**
+ * Abort the database transaction started via beginTransaction (if any).
+ */
+ public function abortTransaction() {
+ if ( $this->mHasTransaction ) {
+ $this->mDb->rollback( get_class( $this ) . '::abortTransaction' );
+ $this->mHasTransaction = false;
+ }
+ }
+
+ /**
+ * Invalidate the cache of a list of pages from a single namespace.
+ * This is intended for use by subclasses.
+ *
+ * @param $namespace Integer
+ * @param $dbkeys Array
+ */
+ protected function invalidatePages( $namespace, Array $dbkeys ) {
+ if ( !count( $dbkeys ) ) {
+ return;
+ }
+
+ /**
+ * Determine which pages need to be updated
+ * This is necessary to prevent the job queue from smashing the DB with
+ * large numbers of concurrent invalidations of the same page
+ */
+ $now = $this->mDb->timestamp();
+ $ids = array();
+ $res = $this->mDb->select( 'page', array( 'page_id' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title' => $dbkeys,
+ 'page_touched < ' . $this->mDb->addQuotes( $now )
+ ), __METHOD__
+ );
+ foreach ( $res as $row ) {
+ $ids[] = $row->page_id;
+ }
+ if ( !count( $ids ) ) {
+ return;
+ }
+
+ /**
+ * Do the update
+ * We still need the page_touched condition, in case the row has changed since
+ * the non-locking select above.
+ */
+ $this->mDb->update( 'page', array( 'page_touched' => $now ),
+ array(
+ 'page_id' => $ids,
+ 'page_touched < ' . $this->mDb->addQuotes( $now )
+ ), __METHOD__
+ );
+ }
+
+}
diff --git a/includes/SquidPurgeClient.php b/includes/SquidPurgeClient.php
index 506ada96..8eb0f6bf 100644
--- a/includes/SquidPurgeClient.php
+++ b/includes/SquidPurgeClient.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Squid and Varnish 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
+ * 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
+ */
+
+/**
* 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.
@@ -23,7 +44,15 @@ class SquidPurgeClient {
* The socket resource, or null for unconnected, or false for disabled due to error
*/
var $socket;
-
+
+ var $readBuffer;
+
+ var $bodyRemaining;
+
+ /**
+ * @param $server string
+ * @param $options array
+ */
public function __construct( $server, $options = array() ) {
$parts = explode( ':', $server, 2 );
$this->host = $parts[0];
@@ -34,7 +63,7 @@ class SquidPurgeClient {
* Open a socket if there isn't one open already, return it.
* Returns false on error.
*
- * @return false|resource
+ * @return bool|resource
*/
protected function getSocket() {
if ( $this->socket !== null ) {
@@ -319,6 +348,9 @@ class SquidPurgeClient {
$this->bodyRemaining = null;
}
+ /**
+ * @param $msg string
+ */
protected function log( $msg ) {
wfDebugLog( 'squid', __CLASS__." ($this->host): $msg\n" );
}
@@ -332,6 +364,9 @@ class SquidPurgeClientPool {
var $clients = array();
var $timeout = 5;
+ /**
+ * @param $options array
+ */
function __construct( $options = array() ) {
if ( isset( $options['timeout'] ) ) {
$this->timeout = $options['timeout'];
@@ -351,6 +386,9 @@ class SquidPurgeClientPool {
$startTime = microtime( true );
while ( !$done ) {
$readSockets = $writeSockets = array();
+ /**
+ * @var $client SquidPurgeClient
+ */
foreach ( $this->clients as $clientIndex => $client ) {
$sockets = $client->getReadSocketsForSelect();
foreach ( $sockets as $i => $socket ) {
diff --git a/includes/Status.php b/includes/Status.php
index e9f3fb91..10dfb516 100644
--- a/includes/Status.php
+++ b/includes/Status.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Generic operation result.
+ *
+ * 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
+ */
/**
* Generic operation result class
@@ -28,6 +48,7 @@ class Status {
* Factory function for fatal errors
*
* @param $message String: message name
+ * @return Status
*/
static function newFatal( $message /*, parameters...*/ ) {
$params = func_get_args();
@@ -41,6 +62,7 @@ class Status {
* Factory function for good results
*
* @param $value Mixed
+ * @return Status
*/
static function newGood( $value = null ) {
$result = new self;
@@ -143,35 +165,6 @@ class Status {
}
/**
- * @param $item
- * @return string
- */
- protected function getItemXML( $item ) {
- $params = $this->cleanParams( $item['params'] );
- $xml = "<{$item['type']}>\n" .
- Xml::element( 'message', null, $item['message'] ) . "\n" .
- Xml::element( 'text', null, wfMsg( $item['message'], $params ) ) ."\n";
- foreach ( $params as $param ) {
- $xml .= Xml::element( 'param', null, $param );
- }
- $xml .= "</{$item['type']}>\n";
- return $xml;
- }
-
- /**
- * Get the error list as XML
- * @return string
- */
- function getXML() {
- $xml = "<errors>\n";
- foreach ( $this->errors as $error ) {
- $xml .= $this->getItemXML( $error );
- }
- $xml .= "</errors>\n";
- return $xml;
- }
-
- /**
* Get the error list as a wikitext formatted list
*
* @param $shortContext String: a short enclosing context message name, to
@@ -192,17 +185,17 @@ class Status {
if ( count( $this->errors ) == 1 ) {
$s = $this->getWikiTextForError( $this->errors[0], $this->errors[0] );
if ( $shortContext ) {
- $s = wfMsgNoTrans( $shortContext, $s );
+ $s = wfMessage( $shortContext, $s )->plain();
} elseif ( $longContext ) {
- $s = wfMsgNoTrans( $longContext, "* $s\n" );
+ $s = wfMessage( $longContext, "* $s\n" )->plain();
}
} else {
$s = '* '. implode("\n* ",
$this->getWikiTextArray( $this->errors ) ) . "\n";
if ( $longContext ) {
- $s = wfMsgNoTrans( $longContext, $s );
+ $s = wfMessage( $longContext, $s )->plain();
} elseif ( $shortContext ) {
- $s = wfMsgNoTrans( $shortContext, "\n$s\n" );
+ $s = wfMessage( $shortContext, "\n$s\n" )->plain();
}
}
return $s;
@@ -220,15 +213,15 @@ class Status {
protected function getWikiTextForError( $error ) {
if ( is_array( $error ) ) {
if ( isset( $error['message'] ) && isset( $error['params'] ) ) {
- return wfMsgNoTrans( $error['message'],
- array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) );
+ return wfMessage( $error['message'],
+ array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) )->plain();
} else {
$message = array_shift($error);
- return wfMsgNoTrans( $message,
- array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) );
+ return wfMessage( $message,
+ array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) )->plain();
}
} else {
- return wfMsgNoTrans( $error );
+ return wfMessage( $error )->plain();
}
}
@@ -355,4 +348,11 @@ class Status {
public function getMessage() {
return $this->getWikiText();
}
+
+ /**
+ * @return mixed
+ */
+ public function getValue() {
+ return $this->value;
+ }
}
diff --git a/includes/StreamFile.php b/includes/StreamFile.php
index 0de03c83..95c69a20 100644
--- a/includes/StreamFile.php
+++ b/includes/StreamFile.php
@@ -1,9 +1,28 @@
<?php
/**
- * Functions related to the output of file content
+ * Functions related to the output of file content.
+ *
+ * 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
*/
+
+/**
+ * Functions related to the output of file content
+ */
class StreamFile {
const READY_STREAM = 1;
const NOT_MODIFIED = 2;
@@ -12,25 +31,36 @@ class StreamFile {
* Stream a file to the browser, adding all the headings and fun stuff.
* 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)
* @return bool Success
*/
public static function stream( $fname, $headers = array(), $sendErrors = true ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( FileBackend::isStoragePath( $fname ) ) { // sanity
+ throw new MWException( __FUNCTION__ . " given storage path '$fname'." );
+ }
+
wfSuppressWarnings();
$stat = stat( $fname );
wfRestoreWarnings();
$res = self::prepareForStream( $fname, $stat, $headers, $sendErrors );
if ( $res == self::NOT_MODIFIED ) {
- return true; // use client cache
+ $ok = true; // use client cache
} elseif ( $res == self::READY_STREAM ) {
- return readfile( $fname );
+ wfProfileIn( __METHOD__ . '-send' );
+ $ok = readfile( $fname );
+ wfProfileOut( __METHOD__ . '-send' );
} else {
- return false; // failed
+ $ok = false; // failed
}
+
+ wfProfileOut( __METHOD__ );
+ return $ok;
}
/**
@@ -41,16 +71,14 @@ class StreamFile {
* (c) sends Content-Length header based on HTTP_IF_MODIFIED_SINCE check
*
* @param $path string Storage path or file system path
- * @param $info Array|false File stat info with 'mtime' and 'size' fields
+ * @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)
- * @return int|false READY_STREAM, NOT_MODIFIED, or false on failure
+ * @return int|bool READY_STREAM, NOT_MODIFIED, or false on failure
*/
public static function prepareForStream(
$path, $info, $headers = array(), $sendErrors = true
) {
- global $wgLanguageCode;
-
if ( !is_array( $info ) ) {
if ( $sendErrors ) {
header( 'HTTP/1.0 404 Not Found' );
@@ -91,9 +119,6 @@ class StreamFile {
return false;
}
- header( "Content-Disposition: inline;filename*=utf-8'$wgLanguageCode'" .
- urlencode( basename( $path ) ) );
-
// Send additional headers
foreach ( $headers as $header ) {
header( $header );
@@ -116,7 +141,7 @@ 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
* @return null|string
diff --git a/includes/StringUtils.php b/includes/StringUtils.php
index f405e616..43275a66 100644
--- a/includes/StringUtils.php
+++ b/includes/StringUtils.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Methods to play with strings.
+ *
+ * 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
+ */
+
+/**
* A collection of static methods to play with strings.
*/
class StringUtils {
@@ -54,6 +75,7 @@ class StringUtils {
* @param $callback Callback: function to call on each match
* @param $subject String
* @param $flags String: regular expression flags
+ * @throws MWException
* @return string
*/
static function delimiterReplaceCallback( $startDelim, $endDelim, $callback, $subject, $flags = '' ) {
@@ -191,7 +213,7 @@ class StringUtils {
* Returns an Iterator
* @param $separator
* @param $subject
- * @return \ArrayIterator|\ExplodeIterator
+ * @return ArrayIterator|ExplodeIterator
*/
static function explode( $separator, $subject ) {
if ( substr_count( $subject, $separator ) > 1000 ) {
@@ -207,6 +229,10 @@ class StringUtils {
* StringUtils::delimiterReplaceCallback()
*/
class Replacer {
+
+ /**
+ * @return array
+ */
function cb() {
return array( &$this, 'replace' );
}
@@ -217,10 +243,18 @@ class Replacer {
*/
class RegexlikeReplacer extends Replacer {
var $r;
+
+ /**
+ * @param $r string
+ */
function __construct( $r ) {
$this->r = $r;
}
+ /**
+ * @param $matches array
+ * @return string
+ */
function replace( $matches ) {
$pairs = array();
foreach ( $matches as $i => $match ) {
@@ -235,12 +269,22 @@ class RegexlikeReplacer extends Replacer {
* Class to perform secondary replacement within each replacement string
*/
class DoubleReplacer extends Replacer {
+
+ /**
+ * @param $from
+ * @param $to
+ * @param $index int
+ */
function __construct( $from, $to, $index = 0 ) {
$this->from = $from;
$this->to = $to;
$this->index = $index;
}
+ /**
+ * @param $matches array
+ * @return mixed
+ */
function replace( $matches ) {
return str_replace( $this->from, $this->to, $matches[$this->index] );
}
@@ -252,11 +296,19 @@ class DoubleReplacer extends Replacer {
class HashtableReplacer extends Replacer {
var $table, $index;
+ /**
+ * @param $table
+ * @param $index int
+ */
function __construct( $table, $index = 0 ) {
$this->table = $table;
$this->index = $index;
}
+ /**
+ * @param $matches array
+ * @return mixed
+ */
function replace( $matches ) {
return $this->table[$matches[$this->index]];
}
@@ -273,11 +325,15 @@ class ReplacementArray {
/**
* Create an object with the specified replacement array
* The array should have the same form as the replacement array for strtr()
+ * @param array $data
*/
function __construct( $data = array() ) {
$this->data = $data;
}
+ /**
+ * @return array
+ */
function __sleep() {
return array( 'data' );
}
@@ -294,39 +350,61 @@ class ReplacementArray {
$this->fss = false;
}
+ /**
+ * @return array|bool
+ */
function getArray() {
return $this->data;
}
/**
* Set an element of the replacement array
+ * @param $from string
+ * @param $to stromg
*/
function setPair( $from, $to ) {
$this->data[$from] = $to;
$this->fss = false;
}
+ /**
+ * @param $data array
+ */
function mergeArray( $data ) {
$this->data = array_merge( $this->data, $data );
$this->fss = false;
}
+ /**
+ * @param $other
+ */
function merge( $other ) {
$this->data = array_merge( $this->data, $other->data );
$this->fss = false;
}
+ /**
+ * @param $from string
+ */
function removePair( $from ) {
unset($this->data[$from]);
$this->fss = false;
}
+ /**
+ * @param $data array
+ */
function removeArray( $data ) {
- foreach( $data as $from => $to )
+ foreach( $data as $from => $to ) {
$this->removePair( $from );
+ }
$this->fss = false;
}
+ /**
+ * @param $subject string
+ * @return string
+ */
function replace( $subject ) {
if ( function_exists( 'fss_prep_replace' ) ) {
wfProfileIn( __METHOD__.'-fss' );
@@ -369,8 +447,10 @@ class ExplodeIterator implements Iterator {
// The current token
var $current;
- /**
+ /**
* Construct a DelimIterator
+ * @param $delim string
+ * @param $s string
*/
function __construct( $delim, $s ) {
$this->subject = $s;
@@ -389,7 +469,6 @@ class ExplodeIterator implements Iterator {
$this->refreshCurrent();
}
-
function refreshCurrent() {
if ( $this->curPos === false ) {
$this->current = false;
@@ -410,6 +489,9 @@ class ExplodeIterator implements Iterator {
return $this->curPos;
}
+ /**
+ * @return string
+ */
function next() {
if ( $this->endPos === false ) {
$this->curPos = false;
@@ -425,8 +507,10 @@ class ExplodeIterator implements Iterator {
return $this->current;
}
+ /**
+ * @return bool
+ */
function valid() {
return $this->curPos !== false;
}
}
-
diff --git a/includes/StubObject.php b/includes/StubObject.php
index 647ad929..615bcb5f 100644
--- a/includes/StubObject.php
+++ b/includes/StubObject.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Delayed loading of global 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
+ */
/**
* Class to implement stub globals, which are globals that delay loading the
@@ -52,6 +72,7 @@ class StubObject {
*
* @param $name String: name of the function called
* @param $args Array: arguments
+ * @return mixed
*/
function _call( $name, $args ) {
$this->_unstub( $name, 5 );
@@ -72,6 +93,7 @@ class StubObject {
*
* @param $name String: name of the function called
* @param $args Array: arguments
+ * @return mixed
*/
function __call( $name, $args ) {
return $this->_call( $name, $args );
diff --git a/includes/Timestamp.php b/includes/Timestamp.php
new file mode 100644
index 00000000..c9ba8d91
--- /dev/null
+++ b/includes/Timestamp.php
@@ -0,0 +1,229 @@
+<?php
+/**
+ * Creation and parsing of MW-style timestamps.
+ *
+ * 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.20
+ * @author Tyler Romeo, 2012
+ */
+
+/**
+ * Library for creating and parsing MW-style timestamps. Based on the JS
+ * library that does the same thing.
+ *
+ * @since 1.20
+ */
+class MWTimestamp {
+ /**
+ * Standard gmdate() formats for the different timestamp types.
+ */
+ private static $formats = array(
+ TS_UNIX => 'U',
+ TS_MW => 'YmdHis',
+ TS_DB => 'Y-m-d H:i:s',
+ TS_ISO_8601 => 'Y-m-d\TH:i:s\Z',
+ TS_ISO_8601_BASIC => 'Ymd\THis\Z',
+ TS_EXIF => 'Y:m:d H:i:s', // This shouldn't ever be used, but is included for completeness
+ 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
+ */
+ 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;
+
+ /**
+ * 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
+ */
+ public function __construct( $timestamp = false ) {
+ $this->setTimestamp( $timestamp );
+ }
+
+ /**
+ * Set the timestamp to the specified time, or the current time if unspecified.
+ *
+ * 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
+ * @throws TimestampException
+ */
+ public function setTimestamp( $ts = false ) {
+ $da = array();
+ $strtime = '';
+
+ if ( !$ts || $ts === "\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ) { // We want to catch 0, '', null... but not date strings starting with a letter.
+ $uts = time();
+ $strtime = "@$uts";
+ } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
+ # TS_DB
+ } elseif ( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
+ # TS_EXIF
+ } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) {
+ # TS_MW
+ } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) {
+ # TS_UNIX
+ $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php
+ } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) {
+ # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6
+ $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3",
+ str_replace( '+00:00', 'UTC', $ts ) );
+ } elseif ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) {
+ # TS_ISO_8601
+ } elseif ( preg_match( '/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) {
+ #TS_ISO_8601_BASIC
+ } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/', $ts, $da ) ) {
+ # TS_POSTGRES
+ } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\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
+ # TS_RFC2822, accepting a trailing comment. See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171
+ # The regex is a superset of rfc2822 for readability
+ $strtime = strtok( $ts, ';' );
+ } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) {
+ # TS_RFC850
+ $strtime = $ts;
+ } elseif ( preg_match( '/^[A-Z][a-z]{2} [A-Z][a-z]{2} +\d{1,2} \d\d:\d\d:\d\d \d{4}/', $ts ) ) {
+ # asctime
+ $strtime = $ts;
+ } else {
+ throw new TimestampException( __METHOD__ . " : Invalid timestamp - $ts" );
+ }
+
+ 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 );
+ }
+
+ if( $final === false ) {
+ throw new TimestampException( __METHOD__ . ' Invalid timestamp format.' );
+ }
+ $this->timestamp = $final;
+ }
+
+ /**
+ * Get the timestamp represented by this object in a certain form.
+ *
+ * Convert the internal timestamp to the specified format and then
+ * return it.
+ *
+ * @param $style int 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( 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 );
+ }
+
+ if ( ( $style == TS_RFC2822 ) || ( $style == TS_POSTGRES ) ) {
+ $output .= ' GMT';
+ }
+
+ return $output;
+ }
+
+ /**
+ * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
+ *
+ * Determine the difference between the timestamp and the current time, and
+ * generate a readable timestamp by returning "<N> <units> ago", where the
+ * largest possible unit is used.
+ *
+ * @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;
+ } else {
+ $timeago = $next;
+ $message = array( $unit, floor( $timeago ) );
+ }
+ }
+
+ if( $message ) {
+ $initial = call_user_func_array( 'wfMessage', $message );
+ return wfMessage( 'ago', $initial );
+ } else {
+ return wfMessage( 'just-now' );
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString() {
+ return $this->getTimestamp();
+ }
+}
+
+class TimestampException extends MWException {}
diff --git a/includes/Title.php b/includes/Title.php
index f3cf79d4..1b5e21d2 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Representation a title within %MediaWiki.
+ *
* See title.txt
*
* This program is free software; you can redistribute it and/or modify
@@ -82,7 +84,7 @@ class Title {
var $mLength = -1; // /< The page length, 0 for special pages
var $mRedirect = null; // /< Is the article at this title a redirect?
var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false
- var $mBacklinkCache = null; // /< Cache of links to this title
+ var $mHasSubpage; // /< Whether a page has any subpages
// @}
@@ -119,7 +121,8 @@ class Title {
* fied by a prefix. If you want to force a specific namespace even if
* $text might begin with a namespace prefix, use makeTitle() or
* makeTitleSafe().
- * @return Title, or null on an error.
+ * @throws MWException
+ * @return Title|null - Title or null on an error.
*/
public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
if ( is_object( $text ) ) {
@@ -179,13 +182,12 @@ class Title {
* @return Title the new object, or NULL on an error
*/
public static function newFromURL( $url ) {
- global $wgLegalTitleChars;
$t = new Title();
# For compatibility with old buggy URLs. "+" is usually not valid in titles,
# but some URLs used it as a space replacement and they still come
# from some external search tools.
- if ( strpos( $wgLegalTitleChars, '+' ) === false ) {
+ if ( strpos( self::legalChars(), '+' ) === false ) {
$url = str_replace( '+', ' ', $url );
}
@@ -206,7 +208,15 @@ class Title {
*/
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_id' => $id ), __METHOD__ );
+ $row = $db->selectRow(
+ 'page',
+ array(
+ 'page_namespace', 'page_title', 'page_id',
+ 'page_len', 'page_is_redirect', 'page_latest',
+ ),
+ array( 'page_id' => $id ),
+ __METHOD__
+ );
if ( $row !== false ) {
$title = Title::newFromRow( $row );
} else {
@@ -260,8 +270,7 @@ class Title {
* Load Title object fields from a DB row.
* If false is given, the title will be treated as non-existing.
*
- * @param $row Object|false database row
- * @return void
+ * @param $row Object|bool database row
*/
public function loadFromRow( $row ) {
if ( $row ) { // page found
@@ -318,6 +327,10 @@ class Title {
* @return Title the new object, or NULL on an error
*/
public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
+ if ( !MWNamespace::exists( $ns ) ) {
+ return null;
+ }
+
$t = new Title();
$t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki );
if ( $t->secureAndSplit() ) {
@@ -333,7 +346,7 @@ class Title {
* @return Title the new object
*/
public static function newMainPage() {
- $title = Title::newFromText( wfMsgForContent( 'mainpage' ) );
+ $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
// Don't give fatal errors if the message is broken
if ( !$title ) {
$title = Title::newFromText( 'Main Page' );
@@ -708,17 +721,9 @@ class Title {
}
}
- // Strip off subpages
- $pagename = $this->getText();
- if ( strpos( $pagename, '/' ) !== false ) {
- list( $username , ) = explode( '/', $pagename, 2 );
- } else {
- $username = $pagename;
- }
-
if ( $wgContLang->needsGenderDistinction() &&
MWNamespace::hasGenderDistinction( $this->mNamespace ) ) {
- $gender = GenderCache::singleton()->getGenderOf( $username, __METHOD__ );
+ $gender = GenderCache::singleton()->getGenderOf( $this->getText(), __METHOD__ );
return $wgContLang->getGenderNsText( $this->mNamespace, $gender );
}
@@ -819,7 +824,7 @@ class Title {
/**
* Returns true if the title is inside the specified namespace.
- *
+ *
* 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.
@@ -863,6 +868,8 @@ class Title {
* This is MUCH simpler than individually testing for equivilance
* against both NS_USER and NS_USER_TALK, and is also forward compatible.
* @since 1.19
+ * @param $ns int
+ * @return bool
*/
public function hasSubjectNamespace( $ns ) {
return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
@@ -928,7 +935,7 @@ class Title {
*/
public function isConversionTable() {
return $this->getNamespace() == NS_MEDIAWIKI &&
- strpos( $this->getText(), 'Conversiontable' ) !== false;
+ strpos( $this->getText(), 'Conversiontable/' ) === 0;
}
/**
@@ -1226,6 +1233,9 @@ class Title {
* andthe wfArrayToCGI moved to getLocalURL();
*
* @since 1.19 (r105919)
+ * @param $query
+ * @param $query2 bool
+ * @return String
*/
private static function fixUrlQueryArgs( $query, $query2 = false ) {
if( $query2 !== false ) {
@@ -1259,9 +1269,11 @@ class Title {
* See getLocalURL for the arguments.
*
* @see self::getLocalURL
+ * @see wfExpandUrl
+ * @param $proto Protocol type to use in URL
* @return String the URL
*/
- public function getFullURL( $query = '', $query2 = false ) {
+ public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
$query = self::fixUrlQueryArgs( $query, $query2 );
# Hand off all the decisions on urls to getLocalURL
@@ -1270,7 +1282,7 @@ class Title {
# Expand the url to make it a full url. Note that getLocalURL has the
# potential to output full urls for a variety of reasons, so we use
# wfExpandUrl instead of simply prepending $wgServer
- $url = wfExpandUrl( $url, PROTO_RELATIVE );
+ $url = wfExpandUrl( $url, $proto );
# Finally, add the fragment.
$url .= $this->getFragmentForURL();
@@ -1284,7 +1296,7 @@ class Title {
* with action=render, $wgServer is prepended.
*
- * @param $query \twotypes{\string,\array} an optional query string,
+ * @param $query string|array an optional query string,
* not used for interwiki links. Can be specified as an associative array as well,
* e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
* Some query patterns will trigger various shorturl path replacements.
@@ -1407,6 +1419,8 @@ class Title {
* See getLocalURL for the arguments.
*
* @see self::getLocalURL
+ * @param $query string
+ * @param $query2 bool|string
* @return String the URL
*/
public function escapeLocalURL( $query = '', $query2 = false ) {
@@ -1478,6 +1492,7 @@ class Title {
*
* @see self::getLocalURL
* @since 1.18
+ * @return string
*/
public function escapeCanonicalURL( $query = '', $query2 = false ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -1502,6 +1517,7 @@ class Title {
/**
* Is $wgUser watching this page?
*
+ * @deprecated in 1.20; use User::isWatched() instead.
* @return Bool
*/
public function userIsWatching() {
@@ -1577,7 +1593,7 @@ class Title {
* queries by skipping checks for cascading protections and user blocks.
* @param $ignoreErrors Array of Strings Set this to a list of message keys
* whose corresponding errors may be ignored.
- * @return Array of arguments to wfMsg to explain permissions problems.
+ * @return Array of arguments to wfMessage to explain permissions problems.
*/
public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
@@ -1734,7 +1750,7 @@ class Title {
# Check $wgNamespaceProtection for restricted namespaces
if ( $this->isNamespaceProtected( $user ) ) {
$ns = $this->mNamespace == NS_MAIN ?
- wfMsg( 'nstab-main' ) : $this->getNsText();
+ wfMessage( 'nstab-main' )->text() : $this->getNsText();
$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
array( 'protectedinterface' ) : array( 'namespaceprotected', $ns );
}
@@ -1867,7 +1883,7 @@ class Title {
$title_protection['pt_create_perm'] = 'protect'; // B/C
}
if( $title_protection['pt_create_perm'] == '' ||
- !$user->isAllowed( $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'] );
}
@@ -1925,7 +1941,7 @@ class Title {
// 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->mBlock;
+ $block = $user->getBlock();
// This is from OutputPage::blockedPage
// Copied at r23888 by werdna
@@ -1933,7 +1949,7 @@ class Title {
$id = $user->blockedBy();
$reason = $user->blockedFor();
if ( $reason == '' ) {
- $reason = wfMsg( 'blockednoreason' );
+ $reason = wfMessage( 'blockednoreason' )->text();
}
$ip = $user->getRequest()->getIP();
@@ -1945,15 +1961,15 @@ class Title {
$link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
$blockid = $block->getId();
- $blockExpiry = $user->mBlock->mExpiry;
- $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true );
+ $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( $user->mBlock->getTarget() );
+ $intended = strval( $block->getTarget() );
$errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name,
$blockid, $blockExpiry, $intended, $blockTimestamp );
@@ -2020,9 +2036,8 @@ class Title {
$name = $this->getPrefixedText();
$dbName = $this->getPrefixedDBKey();
- // Check with and without underscores
+ // Check for explicit whitelisting with and without underscores
if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
- # Check for explicit whitelisting
$whitelisted = true;
} elseif ( $this->getNamespace() == NS_MAIN ) {
# Old settings might have the title prefixed with
@@ -2092,7 +2107,7 @@ class Title {
* @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.
- * @return Array of arrays of the arguments to wfMsg to explain permissions problems.
+ * @return Array of arrays of the arguments to wfMessage to explain permissions problems.
*/
protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) {
wfProfileIn( __METHOD__ );
@@ -2471,8 +2486,9 @@ class Title {
/**
* Get the expiry time for the restriction against a given action
*
+ * @param $action
* @return String|Bool 14-char timestamp, or 'infinity' if the page is protected forever
- * or not protected at all, or false if the action is not recognised.
+ * or not protected at all, or false if the action is not recognised.
*/
public function getRestrictionExpiry( $action ) {
if ( !$this->mRestrictionsLoaded ) {
@@ -2537,7 +2553,7 @@ class Title {
if ( $oldFashionedRestrictions === null ) {
$oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
- array( 'page_id' => $this->getArticleId() ), __METHOD__ );
+ array( 'page_id' => $this->getArticleID() ), __METHOD__ );
}
if ( $oldFashionedRestrictions != '' ) {
@@ -2549,7 +2565,10 @@ class Title {
$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
} else {
- $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
+ $restriction = trim( $temp[1] );
+ if( $restriction != '' ) { //some old entries are empty
+ $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
+ }
}
}
@@ -2608,7 +2627,7 @@ class Title {
$res = $dbr->select(
'page_restrictions',
'*',
- array( 'pr_page' => $this->getArticleId() ),
+ array( 'pr_page' => $this->getArticleID() ),
__METHOD__
);
@@ -2841,7 +2860,7 @@ class Title {
* @return Int or 0 if the page doesn't exist
*/
public function getLatestRevID( $flags = 0 ) {
- if ( $this->mLatestID !== false ) {
+ if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
return intval( $this->mLatestID );
}
# Calling getArticleID() loads the field from cache as needed
@@ -2860,7 +2879,7 @@ class Title {
*
* - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
* loading of the new page_id. It's also called from
- * WikiPage::doDeleteArticle()
+ * WikiPage::doDeleteArticleReal()
*
* @param $newid Int the new Article ID
*/
@@ -3166,7 +3185,7 @@ class Title {
* @return Array of Title objects linking here
*/
public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
- $id = $this->getArticleId();
+ $id = $this->getArticleID();
# If the page doesn't exist; there can't be any link from this page
if ( !$id ) {
@@ -3230,7 +3249,7 @@ class Title {
* @return Array of Title the Title objects
*/
public function getBrokenLinksFrom() {
- if ( $this->getArticleId() == 0 ) {
+ if ( $this->getArticleID() == 0 ) {
# All links from article ID 0 are false positives
return array();
}
@@ -3240,7 +3259,7 @@ class Title {
array( 'page', 'pagelinks' ),
array( 'pl_namespace', 'pl_title' ),
array(
- 'pl_from' => $this->getArticleId(),
+ 'pl_from' => $this->getArticleID(),
'page_namespace IS NULL'
),
__METHOD__, array(),
@@ -3267,16 +3286,14 @@ class Title {
* @return Array of String the URLs
*/
public function getSquidURLs() {
- global $wgContLang;
-
$urls = array(
$this->getInternalURL(),
$this->getInternalURL( 'action=history' )
);
- // purge variant urls as well
- if ( $wgContLang->hasVariants() ) {
- $variants = $wgContLang->getVariants();
+ $pageLang = $this->getPageLanguage();
+ if ( $pageLang->hasVariants() ) {
+ $variants = $pageLang->getVariants();
foreach ( $variants as $vCode ) {
$urls[] = $this->getInternalURL( '', $vCode );
}
@@ -3458,6 +3475,10 @@ class Title {
$wgUser->spreadAnyEditBlock();
return $err;
}
+ // Check suppressredirect permission
+ if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
+ $createRedirect = true;
+ }
// If it is a file, move it first.
// It is done before all other moving stuff is done because it's hard to revert.
@@ -3475,17 +3496,12 @@ class Title {
RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
}
- $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own.
+ $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own.
$pageid = $this->getArticleID( self::GAID_FOR_UPDATE );
$protected = $this->isProtected();
// Do the actual move
- $err = $this->moveToInternal( $nt, $reason, $createRedirect );
- if ( is_array( $err ) ) {
- # @todo FIXME: What about the File we have already moved?
- $dbw->rollback();
- return $err;
- }
+ $this->moveToInternal( $nt, $reason, $createRedirect );
// Refresh the sortkey for this row. Be careful to avoid resetting
// cl_timestamp, which may disturb time-based lists on some sites.
@@ -3529,9 +3545,13 @@ class Title {
);
# Update the protection log
$log = new LogPage( 'protect' );
- $comment = wfMsgForContent( 'prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
+ $comment = wfMessage(
+ 'prot_1movedto2',
+ $this->getPrefixedText(),
+ $nt->getPrefixedText()
+ )->inContentLanguage()->text();
if ( $reason ) {
- $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
+ $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
}
// @todo FIXME: $params?
$log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) );
@@ -3547,7 +3567,7 @@ class Title {
WatchedItem::duplicateEntries( $this, $nt );
}
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
return true;
@@ -3559,8 +3579,9 @@ class Title {
*
* @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. Ignored
- * if the user doesn't have the suppressredirect right
+ * @param $createRedirect Bool Whether to leave a redirect at the old title. Does not check
+ * if the user has the suppressredirect right
+ * @throws MWException
*/
private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
global $wgUser, $wgContLang;
@@ -3573,7 +3594,7 @@ class Title {
$logType = 'move';
}
- $redirectSuppressed = !$createRedirect && $wgUser->isAllowed( 'suppressredirect' );
+ $redirectSuppressed = !$createRedirect;
$logEntry = new ManualLogEntry( 'move', $logType );
$logEntry->setPerformer( $wgUser );
@@ -3588,13 +3609,12 @@ class Title {
$formatter->setContext( RequestContext::newExtraneousContext( $this ) );
$comment = $formatter->getPlainActionText();
if ( $reason ) {
- $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
+ $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
}
# Truncate for whole multibyte characters.
$comment = $wgContLang->truncate( $comment, 255 );
$oldid = $this->getArticleID();
- $latest = $this->getLatestRevID();
$dbw = wfGetDB( DB_MASTER );
@@ -3635,7 +3655,7 @@ class Title {
$newpage->updateRevisionOn( $dbw, $nullRevision );
wfRunHooks( 'NewRevisionFromEditComplete',
- array( $newpage, $nullRevision, $latest, $wgUser ) );
+ array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) );
$newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) );
@@ -3714,8 +3734,8 @@ class Title {
// We don't know whether this function was called before
// or after moving the root page, so check both
// $this and $nt
- if ( $oldSubpage->getArticleId() == $this->getArticleId() ||
- $oldSubpage->getArticleID() == $nt->getArticleId() )
+ if ( $oldSubpage->getArticleID() == $this->getArticleID() ||
+ $oldSubpage->getArticleID() == $nt->getArticleID() )
{
// When moving a page to a subpage of itself,
// don't move it twice
@@ -3803,7 +3823,7 @@ class Title {
return false;
}
# Get the article text
- $rev = Revision::newFromTitle( $nt );
+ $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
if( !is_object( $rev ) ){
return false;
}
@@ -3839,7 +3859,7 @@ class Title {
$data = array();
- $titleKey = $this->getArticleId();
+ $titleKey = $this->getArticleID();
if ( $titleKey === 0 ) {
return $data;
@@ -3915,14 +3935,20 @@ class Title {
*/
public function getPreviousRevisionID( $revId, $flags = 0 ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
- return $db->selectField( 'revision', 'rev_id',
+ $revId = $db->selectField( 'revision', 'rev_id',
array(
- 'rev_page' => $this->getArticleId( $flags ),
+ 'rev_page' => $this->getArticleID( $flags ),
'rev_id < ' . intval( $revId )
),
__METHOD__,
array( 'ORDER BY' => 'rev_id DESC' )
);
+
+ if ( $revId === false ) {
+ return false;
+ } else {
+ return intval( $revId );
+ }
}
/**
@@ -3934,14 +3960,20 @@ class Title {
*/
public function getNextRevisionID( $revId, $flags = 0 ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
- return $db->selectField( 'revision', 'rev_id',
+ $revId = $db->selectField( 'revision', 'rev_id',
array(
- 'rev_page' => $this->getArticleId( $flags ),
+ 'rev_page' => $this->getArticleID( $flags ),
'rev_id > ' . intval( $revId )
),
__METHOD__,
array( 'ORDER BY' => 'rev_id' )
);
+
+ if ( $revId === false ) {
+ return false;
+ } else {
+ return intval( $revId );
+ }
}
/**
@@ -3951,10 +3983,10 @@ class Title {
* @return Revision|Null if page doesn't exist
*/
public function getFirstRevision( $flags = 0 ) {
- $pageId = $this->getArticleId( $flags );
+ $pageId = $this->getArticleID( $flags );
if ( $pageId ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
- $row = $db->selectRow( 'revision', '*',
+ $row = $db->selectRow( 'revision', Revision::selectFields(),
array( 'rev_page' => $pageId ),
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
@@ -4016,7 +4048,7 @@ class Title {
if ( $this->mEstimateRevisions === null ) {
$dbr = wfGetDB( DB_SLAVE );
$this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
- array( 'rev_page' => $this->getArticleId() ), __METHOD__ );
+ array( 'rev_page' => $this->getArticleID() ), __METHOD__ );
}
return $this->mEstimateRevisions;
@@ -4043,7 +4075,7 @@ class Title {
$dbr = wfGetDB( DB_SLAVE );
return (int)$dbr->selectField( 'revision', 'count(*)',
array(
- 'rev_page' => $this->getArticleId(),
+ 'rev_page' => $this->getArticleID(),
'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
),
@@ -4052,30 +4084,60 @@ class Title {
}
/**
- * Get the number of authors between the given revision IDs.
+ * 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)
- * @param $new int|Revision New revision or rev ID (first after range)
- * @param $limit Int Maximum number of authors
- * @return Int Number of revision authors between these revisions.
- */
- public function countAuthorsBetween( $old, $new, $limit ) {
+ * @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:
+ * '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.
+ * Unknown option values are ignored.
+ * @return int Number of revision authors in the range; zero if not both revisions exist
+ */
+ public function countAuthorsBetween( $old, $new, $limit, $options = array() ) {
if ( !( $old instanceof Revision ) ) {
$old = Revision::newFromTitle( $this, (int)$old );
}
if ( !( $new instanceof Revision ) ) {
$new = Revision::newFromTitle( $this, (int)$new );
}
+ // XXX: what if Revision objects are passed in, but they don't refer to this title?
+ // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
+ // in the sanity check below?
if ( !$old || !$new ) {
return 0; // nothing to compare
}
+ $old_cmp = '>';
+ $new_cmp = '<';
+ $options = (array) $options;
+ if ( in_array( 'include_old', $options ) ) {
+ $old_cmp = '>=';
+ }
+ if ( in_array( 'include_new', $options ) ) {
+ $new_cmp = '<=';
+ }
+ if ( in_array( 'include_both', $options ) ) {
+ $old_cmp = '>=';
+ $new_cmp = '<=';
+ }
+ // 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() ) {
+ if ( $old_cmp === '>' || $new_cmp === '<' ) {
+ return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1;
+ }
+ return ( $old->getRawUserText() === $new->getRawUserText() ) ? 1 : 2;
+ }
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
array(
'rev_page' => $this->getArticleID(),
- 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
- 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
+ "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
+ "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
), __METHOD__,
array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated
);
@@ -4117,7 +4179,7 @@ class Title {
* @return Bool
*/
public function exists() {
- return $this->getArticleId() != 0;
+ return $this->getArticleID() != 0;
}
/**
@@ -4137,9 +4199,28 @@ class Title {
* @return Bool
*/
public function isAlwaysKnown() {
+ $isKnown = null;
+
+ /**
+ * Allows overriding default behaviour 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.
+ *
+ * @since 1.20
+ *
+ * @param Title $title
+ * @param boolean|null $isKnown
+ */
+ wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) );
+
+ if ( !is_null( $isKnown ) ) {
+ return $isKnown;
+ }
+
if ( $this->mInterwiki != '' ) {
return true; // any interwiki link might be viewable, for all we know
}
+
switch( $this->mNamespace ) {
case NS_MEDIA:
case NS_FILE:
@@ -4164,6 +4245,9 @@ class Title {
* viewed? In particular, this function may be used to determine if
* links to the title should be rendered as "bluelinks" (as opposed to
* "redlinks" to non-existent pages).
+ * Adding something else to this function will cause inconsistency
+ * since LinkHolderArray calls isAlwaysKnown() and does its own
+ * page existence check.
*
* @return Bool
*/
@@ -4292,9 +4376,10 @@ class Title {
$dbr = wfGetDB( DB_SLAVE );
$this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
'wl_notificationtimestamp',
- array( 'wl_namespace' => $this->getNamespace(),
+ array(
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => $this->getNamespace(),
'wl_title' => $this->getDBkey(),
- 'wl_user' => $user->getId()
),
__METHOD__
);
@@ -4347,6 +4432,11 @@ class Title {
'rd_title' => $this->getDBkey(),
'rd_from = page_id'
);
+ if ( $this->isExternal() ) {
+ $where['rd_interwiki'] = $this->getInterwiki();
+ } else {
+ $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
+ }
if ( !is_null( $ns ) ) {
$where['page_namespace'] = $ns;
}
@@ -4391,11 +4481,8 @@ class Title {
*
* @return BacklinkCache
*/
- function getBacklinkCache() {
- if ( is_null( $this->mBacklinkCache ) ) {
- $this->mBacklinkCache = new BacklinkCache( $this );
- }
- return $this->mBacklinkCache;
+ public function getBacklinkCache() {
+ return BacklinkCache::get( $this );
}
/**
@@ -4444,19 +4531,19 @@ class Title {
}
/**
- * Get the language in which the content of this page is written.
- * Defaults to $wgContLang, but in certain cases it can be e.g.
- * $wgLang (such as special pages, which are in the user language).
+ * Get the language in which the content of this page is written in
+ * wikitext. Defaults to $wgContLang, but in certain cases it can be
+ * e.g. $wgLang (such as special pages, which are in the user language).
*
* @since 1.18
- * @return object Language
+ * @return Language
*/
public function getPageLanguage() {
global $wgLang;
if ( $this->isSpecialPage() ) {
// special pages are in the user language
return $wgLang;
- } elseif ( $this->isCssOrJsPage() ) {
+ } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
// css/js should always be LTR and is, in fact, English
return wfGetLangObj( 'en' );
} elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
@@ -4471,4 +4558,29 @@ class Title {
wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
return wfGetLangObj( $pageLang );
}
+
+ /**
+ * Get the language in which the content of this page is written when
+ * viewed by user. Defaults to $wgContLang, but in certain cases it can be
+ * e.g. $wgLang (such as special pages, which are in the user language).
+ *
+ * @since 1.20
+ * @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
+ ) {
+ // 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;
+ }
}
diff --git a/includes/TitleArray.php b/includes/TitleArray.php
index 96960089..5cdec16d 100644
--- a/includes/TitleArray.php
+++ b/includes/TitleArray.php
@@ -1,8 +1,27 @@
<?php
/**
+ * 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.
+ *
+ * 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
*/
/**
diff --git a/includes/User.php b/includes/User.php
index 1529da1e..0a3db4c0 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -66,6 +66,11 @@ class User {
const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
/**
+ * Maximum items in $mWatchedItems
+ */
+ const MAX_WATCHED_ITEMS_CACHE = 100;
+
+ /**
* Array of Strings List of member variables which are saved to the
* shared cache (memcached). Any operation which changes the
* corresponding database fields must call a cache-clearing function.
@@ -114,9 +119,11 @@ class User {
'delete',
'deletedhistory',
'deletedtext',
+ 'deletelogentry',
'deleterevision',
'edit',
'editinterface',
+ 'editprotected',
'editusercssjs', #deprecated
'editusercss',
'edituserjs',
@@ -134,12 +141,15 @@ class User {
'nominornewtalk',
'noratelimit',
'override-export-depth',
+ 'passwordreset',
'patrol',
+ 'patrolmarks',
'protect',
'proxyunbannable',
'purge',
'read',
'reupload',
+ 'reupload-own',
'reupload-shared',
'rollback',
'sendemail',
@@ -165,8 +175,8 @@ class User {
//@{
var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
$mEmail, $mTouched, $mToken, $mEmailAuthenticated,
- $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides,
- $mCookiePassword, $mEditCount, $mAllowUsertalk;
+ $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount,
+ $mGroups, $mOptionOverrides;
//@}
/**
@@ -210,10 +220,20 @@ class User {
var $mBlock;
/**
+ * @var bool
+ */
+ var $mAllowUsertalk;
+
+ /**
* @var Block
*/
private $mBlockedFromCreateAccount = false;
+ /**
+ * @var Array
+ */
+ private $mWatchedItems = array();
+
static $idCacheByName = array();
/**
@@ -445,22 +465,20 @@ class User {
/**
* Get the username corresponding to a given user ID
* @param $id Int User ID
- * @return String|false The corresponding username
+ * @return String|bool The corresponding username
*/
public static function whoIs( $id ) {
- $dbr = wfGetDB( DB_SLAVE );
- return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), __METHOD__ );
+ return UserCache::singleton()->getProp( $id, 'name' );
}
/**
* Get the real name of a user given their user ID
*
* @param $id Int User ID
- * @return String|false The corresponding user's real name
+ * @return String|bool The corresponding user's real name
*/
public static function whoIsReal( $id ) {
- $dbr = wfGetDB( DB_SLAVE );
- return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
+ return UserCache::singleton()->getProp( $id, 'real_name' );
}
/**
@@ -512,7 +530,7 @@ class User {
* as 300.300.300.300 will return true because it looks like an IP
* address, despite not being strictly valid.
*
- * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
+ * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
* address because the usemod software would "cloak" anonymous IP
* addresses like this, if we allowed accounts like this to be created
* new users could get the old edits of these anonymous users.
@@ -606,7 +624,7 @@ class User {
// Certain names may be reserved for batch processes.
foreach ( $reservedUsernames as $reserved ) {
if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
- $reserved = wfMsgForContent( substr( $reserved, 4 ) );
+ $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
}
if ( $reserved == $name ) {
return false;
@@ -1023,7 +1041,7 @@ class User {
}
$dbr = wfGetDB( DB_MASTER );
- $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
+ $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ );
wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
@@ -1278,22 +1296,22 @@ class User {
}
# User/IP blocking
- $block = Block::newFromTarget( $this->getName(), $ip, !$bFromSlave );
+ $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
# Proxy blocking
if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
- && !in_array( $ip, $wgProxyWhitelist ) )
+ && !in_array( $ip, $wgProxyWhitelist ) )
{
# Local list
if ( self::isLocallyBlockedProxy( $ip ) ) {
$block = new Block;
- $block->setBlocker( wfMsg( 'proxyblocker' ) );
- $block->mReason = wfMsg( 'proxyblockreason' );
+ $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
+ $block->mReason = wfMessage( 'proxyblockreason' )->text();
$block->setTarget( $ip );
} elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
$block = new Block;
- $block->setBlocker( wfMsg( 'sorbs' ) );
- $block->mReason = wfMsg( 'sorbsreason' );
+ $block->setBlocker( wfMessage( 'sorbs' )->text() );
+ $block->mReason = wfMessage( 'sorbsreason' )->text();
$block->setTarget( $ip );
}
}
@@ -1372,11 +1390,11 @@ class User {
$ipList = gethostbynamel( $host );
if( $ipList ) {
- wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
+ wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
$found = true;
break;
} else {
- wfDebug( "Requested $host, not found in $base.\n" );
+ wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" );
}
}
}
@@ -1512,7 +1530,7 @@ class User {
$count = $wgMemc->get( $key );
// Already pinged?
if( $count ) {
- if( $count > $max ) {
+ if( $count >= $max ) {
wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
if( $wgRateLimitLog ) {
wfSuppressWarnings();
@@ -1748,16 +1766,22 @@ class User {
# Check memcached separately for anons, who have no
# entire User object stored in there.
if( !$this->mId ) {
- global $wgMemc;
- $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
- $newtalk = $wgMemc->get( $key );
- if( strval( $newtalk ) !== '' ) {
- $this->mNewtalk = (bool)$newtalk;
+ global $wgDisableAnonTalk;
+ if( $wgDisableAnonTalk ) {
+ // Anon newtalk disabled by configuration.
+ $this->mNewtalk = false;
} else {
- // Since we are caching this, make sure it is up to date by getting it
- // from the master
- $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
- $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
+ global $wgMemc;
+ $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
+ $newtalk = $wgMemc->get( $key );
+ if( strval( $newtalk ) !== '' ) {
+ $this->mNewtalk = (bool)$newtalk;
+ } else {
+ // Since we are caching this, make sure it is up to date by getting it
+ // from the master
+ $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
+ $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
+ }
}
} else {
$this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
@@ -1773,14 +1797,20 @@ class User {
*/
public function getNewMessageLinks() {
$talks = array();
- if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) )
+ if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
return $talks;
-
- if( !$this->getNewtalk() )
+ } elseif( !$this->getNewtalk() ) {
return array();
- $up = $this->getUserPage();
- $utp = $up->getTalkPage();
- return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) );
+ }
+ $utp = $this->getTalkPage();
+ $dbr = wfGetDB( DB_SLAVE );
+ // Get the "last viewed rev" timestamp from the oldest message notification
+ $timestamp = $dbr->selectField( 'user_newtalk',
+ 'MIN(user_last_timestamp)',
+ $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
+ __METHOD__ );
+ $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
+ return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
}
/**
@@ -1807,12 +1837,17 @@ 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 $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null.
* @return Bool True if successful, false otherwise
*/
- protected function updateNewtalk( $field, $id ) {
+ protected function updateNewtalk( $field, $id, $curRev = null ) {
+ // Get timestamp of the talk page revision prior to the current one
+ $prevRev = $curRev ? $curRev->getPrevious() : false;
+ $ts = $prevRev ? $prevRev->getTimestamp() : null;
+ // Mark the user as having new messages since this revision
$dbw = wfGetDB( DB_MASTER );
$dbw->insert( 'user_newtalk',
- array( $field => $id ),
+ array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
__METHOD__,
'IGNORE' );
if ( $dbw->affectedRows() ) {
@@ -1847,8 +1882,9 @@ class User {
/**
* Update the 'You have new messages!' status.
* @param $val Bool 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 ) {
+ public function setNewtalk( $val, $curRev = null ) {
if( wfReadOnly() ) {
return;
}
@@ -1866,7 +1902,7 @@ class User {
global $wgMemc;
if( $val ) {
- $changed = $this->updateNewtalk( $field, $id );
+ $changed = $this->updateNewtalk( $field, $id, $curRev );
} else {
$changed = $this->deleteNewtalk( $field, $id );
}
@@ -1921,10 +1957,19 @@ class User {
$this->mTouched = self::newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
- $dbw->update( 'user',
- array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
- array( 'user_id' => $this->mId ),
- __METHOD__ );
+
+ // 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__
+ );
+ }
$this->clearSharedCache();
}
@@ -1971,7 +2016,7 @@ class User {
if( $str !== null ) {
if( !$wgAuth->allowPasswordChange() ) {
- throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
+ throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
}
if( !$this->isValidPassword( $str ) ) {
@@ -1984,12 +2029,12 @@ class User {
$message = $valid;
$params = array( $wgMinimalPasswordLength );
}
- throw new PasswordError( wfMsgExt( $message, array( 'parsemag' ), $params ) );
+ throw new PasswordError( wfMessage( $message, $params )->text() );
}
}
if( !$wgAuth->setPassword( $this, $str ) ) {
- throw new PasswordError( wfMsg( 'externaldberror' ) );
+ throw new PasswordError( wfMessage( 'externaldberror' )->text() );
}
$this->setInternalPassword( $str );
@@ -2036,7 +2081,6 @@ class User {
* @param $token String|bool If specified, set the token to this value
*/
public function setToken( $token = false ) {
- global $wgSecretKey, $wgProxyKey;
$this->load();
if ( !$token ) {
$this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH );
@@ -2046,16 +2090,6 @@ class User {
}
/**
- * Set the cookie password
- *
- * @param $str String New cookie password
- */
- private function setCookiePassword( $str ) {
- $this->load();
- $this->mCookiePassword = md5( $str );
- }
-
- /**
* Set the password for a password reminder or new account email
*
* @param $str String New password to set
@@ -2119,6 +2153,42 @@ 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
+ * @return Status
+ */
+ public function setEmailWithConfirmation( $str ) {
+ global $wgEnableEmail, $wgEmailAuthentication;
+
+ if ( !$wgEnableEmail ) {
+ return Status::newFatal( 'emaildisabled' );
+ }
+
+ $oldaddr = $this->getEmail();
+ if ( $str === $oldaddr ) {
+ return Status::newGood( true );
+ }
+
+ $this->setEmail( $str );
+
+ if ( $str !== '' && $wgEmailAuthentication ) {
+ # 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
+ $result->value = 'eauth';
+ }
+ } else {
+ $result = Status::newGood( true );
+ }
+
+ return $result;
+ }
+
+ /**
* Get the user's real name
* @return String User's real name
*/
@@ -2239,9 +2309,11 @@ class User {
$this->loadOptions();
// Explicitly NULL values should refer to defaults
- global $wgDefaultUserOptions;
- if( is_null( $val ) && isset( $wgDefaultUserOptions[$oname] ) ) {
- $val = $wgDefaultUserOptions[$oname];
+ if( is_null( $val ) ) {
+ $defaultOption = self::getDefaultOption( $oname );
+ if( !is_null( $defaultOption ) ) {
+ $val = $defaultOption;
+ }
}
$this->mOptions[$oname] = $val;
@@ -2251,7 +2323,10 @@ class User {
* Reset all options to the site defaults
*/
public function resetOptions() {
+ $this->load();
+
$this->mOptions = self::getDefaultOptions();
+ $this->mOptionsLoaded = true;
}
/**
@@ -2572,13 +2647,33 @@ class User {
}
/**
+ * Get a WatchedItem for this user and $title.
+ *
+ * @param $title Title
+ * @return WatchedItem
+ */
+ public function getWatchedItem( $title ) {
+ $key = $title->getNamespace() . ':' . $title->getDBkey();
+
+ if ( isset( $this->mWatchedItems[$key] ) ) {
+ return $this->mWatchedItems[$key];
+ }
+
+ if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
+ $this->mWatchedItems = array();
+ }
+
+ $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title );
+ return $this->mWatchedItems[$key];
+ }
+
+ /**
* Check the watched status of an article.
* @param $title Title of the article to look at
* @return Bool
*/
public function isWatched( $title ) {
- $wl = WatchedItem::fromUserTitle( $this, $title );
- return $wl->isWatched();
+ return $this->getWatchedItem( $title )->isWatched();
}
/**
@@ -2586,8 +2681,7 @@ class User {
* @param $title Title of the article to look at
*/
public function addWatch( $title ) {
- $wl = WatchedItem::fromUserTitle( $this, $title );
- $wl->addWatch();
+ $this->getWatchedItem( $title )->addWatch();
$this->invalidateCache();
}
@@ -2596,8 +2690,7 @@ class User {
* @param $title Title of the article to look at
*/
public function removeWatch( $title ) {
- $wl = WatchedItem::fromUserTitle( $this, $title );
- $wl->removeWatch();
+ $this->getWatchedItem( $title )->removeWatch();
$this->invalidateCache();
}
@@ -2635,28 +2728,14 @@ class User {
// The query to find out if it is watched is cached both in memcached and per-invocation,
// and when it does have to be executed, it can be on a slave
// If this is the user's newtalk page, we always update the timestamp
- if( $title->getNamespace() == NS_USER_TALK &&
+ $force = '';
+ if ( $title->getNamespace() == NS_USER_TALK &&
$title->getText() == $this->getName() )
{
- $watched = true;
- } else {
- $watched = $this->isWatched( $title );
+ $force = 'force';
}
- // If the page is watched by the user (or may be watched), update the timestamp on any
- // any matching rows
- if ( $watched ) {
- $dbw = wfGetDB( DB_MASTER );
- $dbw->update( 'watchlist',
- array( /* SET */
- 'wl_notificationtimestamp' => null
- ), array( /* WHERE */
- 'wl_title' => $title->getDBkey(),
- 'wl_namespace' => $title->getNamespace(),
- 'wl_user' => $this->getID()
- ), __METHOD__
- );
- }
+ $this->getWatchedItem( $title )->resetNotificationTimestamp( $force );
}
/**
@@ -2903,6 +2982,7 @@ class User {
'user_token' => strval( $user->mToken ),
'user_registration' => $dbw->timestamp( $user->mRegistration ),
'user_editcount' => 0,
+ 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
);
foreach ( $params as $name => $value ) {
$fields["user_$name"] = $value;
@@ -2921,6 +3001,9 @@ class User {
*/
public function addToDatabase() {
$this->load();
+
+ $this->mTouched = self::newTouchedTimestamp();
+
$dbw = wfGetDB( DB_MASTER );
$seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
$dbw->insert( 'user',
@@ -2936,6 +3019,7 @@ class User {
'user_token' => strval( $this->mToken ),
'user_registration' => $dbw->timestamp( $this->mRegistration ),
'user_editcount' => 0,
+ 'user_touched' => $dbw->timestamp( $this->mTouched ),
), __METHOD__
);
$this->mId = $dbw->insertId();
@@ -2994,7 +3078,7 @@ class User {
*/
public function getPageRenderingHash() {
wfDeprecated( __METHOD__, '1.17' );
-
+
global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
if( $this->mHash ){
return $this->mHash;
@@ -3212,6 +3296,7 @@ class User {
*
* @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
*/
public static function generateToken( $salt = '' ) {
return MWCryptRand::generateHex( 32 );
@@ -3273,15 +3358,15 @@ class User {
$message = 'confirmemail_body_' . $type;
}
- return $this->sendMail( wfMsg( 'confirmemail_subject' ),
- wfMsg( $message,
+ return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
+ wfMessage( $message,
$this->getRequest()->getIP(),
$this->getName(),
$url,
$wgLang->timeanddate( $expiration, false ),
$invalidateURL,
$wgLang->date( $expiration, false ),
- $wgLang->time( $expiration, false ) ) );
+ $wgLang->time( $expiration, false ) )->text() );
}
/**
@@ -3344,7 +3429,7 @@ class User {
* @return String New token URL
*/
private function invalidationTokenUrl( $token ) {
- return $this->getTokenUrl( 'Invalidateemail', $token );
+ return $this->getTokenUrl( 'InvalidateEmail', $token );
}
/**
@@ -3372,7 +3457,7 @@ class User {
*
* @note Call saveSettings() after calling this function to commit the change.
*
- * @return true
+ * @return bool
*/
public function confirmEmail() {
$this->setEmailAuthenticationTimestamp( wfTimestampNow() );
@@ -3385,7 +3470,7 @@ class User {
* address if it was already confirmed.
*
* @note Call saveSettings() after calling this function to commit the change.
- * @return true
+ * @return bool Returns true
*/
function invalidateEmail() {
$this->load();
@@ -3933,10 +4018,10 @@ class User {
$action = 'create2';
if ( $byEmail ) {
if ( $reason === '' ) {
- $reason = wfMsgForContent( 'newuserlog-byemail' );
+ $reason = wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text();
} else {
$reason = $wgContLang->commaList( array(
- $reason, wfMsgForContent( 'newuserlog-byemail' ) ) );
+ $reason, wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text() ) );
}
}
}
@@ -3953,7 +4038,7 @@ class User {
* Add an autocreate newuser log entry for this user
* Used by things like CentralAuth and perhaps other authplugins.
*
- * @return true
+ * @return bool
*/
public function addNewUserLogEntryAutoCreate() {
global $wgNewUserLog;
@@ -3961,7 +4046,7 @@ class User {
return true; // disabled
}
$log = new LogPage( 'newusers', false );
- $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
+ $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ), $this );
return true;
}
@@ -3988,7 +4073,7 @@ class User {
$res = $dbr->select(
'user_properties',
- '*',
+ array( 'up_property', 'up_value' ),
array( 'up_user' => $this->getId() ),
__METHOD__
);
@@ -4011,13 +4096,9 @@ class User {
protected function saveOptions() {
global $wgAllowPrefChange;
- $extuser = ExternalUser::newFromUser( $this );
-
$this->loadOptions();
- $dbw = wfGetDB( DB_MASTER );
-
- $insert_rows = array();
+ // Not using getOptions(), to keep hidden preferences in database
$saveOptions = $this->mOptions;
// Allow hooks to abort, for instance to save to a global profile.
@@ -4026,13 +4107,17 @@ class User {
return;
}
+ $extuser = ExternalUser::newFromUser( $this );
+ $userId = $this->getId();
+ $insert_rows = array();
foreach( $saveOptions as $key => $value ) {
# Don't bother storing default values
- if ( ( is_null( self::getDefaultOption( $key ) ) &&
- !( $value === false || is_null($value) ) ) ||
- $value != self::getDefaultOption( $key ) ) {
+ $defaultOption = self::getDefaultOption( $key );
+ if ( ( is_null( $defaultOption ) &&
+ !( $value === false || is_null( $value ) ) ) ||
+ $value != $defaultOption ) {
$insert_rows[] = array(
- 'up_user' => $this->getId(),
+ 'up_user' => $userId,
'up_property' => $key,
'up_value' => $value,
);
@@ -4049,7 +4134,8 @@ class User {
}
}
- $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ );
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
$dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
}
@@ -4104,11 +4190,35 @@ class User {
/*
if ( $wgMinimalPasswordLength > 1 ) {
$ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
- $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag',
- $wgMinimalPasswordLength );
+ $ret['title'] = wfMessage( 'passwordtooshort' )
+ ->numParams( $wgMinimalPasswordLength )->text();
}
*/
return $ret;
}
+
+ /**
+ * Return the list of user fields that should be selected to create
+ * a new user object.
+ * @return array
+ */
+ public static function selectFields() {
+ return array(
+ 'user_id',
+ 'user_name',
+ 'user_real_name',
+ 'user_password',
+ 'user_newpassword',
+ 'user_newpass_time',
+ 'user_email',
+ 'user_touched',
+ 'user_token',
+ 'user_email_authenticated',
+ 'user_email_token',
+ 'user_email_token_expires',
+ 'user_registration',
+ 'user_editcount',
+ );
+ }
}
diff --git a/includes/UserArray.php b/includes/UserArray.php
index c5ba0b2b..3b8f5c10 100644
--- a/includes/UserArray.php
+++ b/includes/UserArray.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Classes to walk into a list of User 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
+ */
abstract class UserArray implements Iterator {
/**
diff --git a/includes/UserMailer.php b/includes/UserMailer.php
index 5d98d9d2..01e7132d 100644
--- a/includes/UserMailer.php
+++ b/includes/UserMailer.php
@@ -109,16 +109,17 @@ class UserMailer {
/**
* Creates a single string from an associative array
*
- * @param $headers Associative Array: keys are header field names,
+ * @param $headers array Associative Array: keys are header field names,
* values are ... values.
* @param $endl String: The end of line character. Defaults to "\n"
* @return String
*/
static function arrayToHeaderString( $headers, $endl = "\n" ) {
+ $strings = array();
foreach( $headers as $name => $value ) {
- $string[] = "$name: $value";
+ $strings[] = "$name: $value";
}
- return implode( $endl, $string );
+ return implode( $endl, $strings );
}
/**
@@ -345,6 +346,7 @@ class UserMailer {
/**
* Converts a string into quoted-printable format
* @since 1.17
+ * @return string
*/
public static function quotedPrintable( $string, $charset = '' ) {
# Probably incomplete; see RFC 2045
@@ -434,9 +436,9 @@ class EmailNotification {
$res = $dbw->select( array( 'watchlist' ),
array( 'wl_user' ),
array(
- 'wl_title' => $title->getDBkey(),
- 'wl_namespace' => $title->getNamespace(),
'wl_user != ' . intval( $editor->getID() ),
+ 'wl_namespace' => $title->getNamespace(),
+ 'wl_title' => $title->getDBkey(),
'wl_notificationtimestamp IS NULL',
), __METHOD__
);
@@ -446,17 +448,17 @@ class EmailNotification {
if ( $watchers ) {
// Update wl_notificationtimestamp for all watching users except
// the editor
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$dbw->update( 'watchlist',
array( /* SET */
'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
), array( /* WHERE */
- 'wl_title' => $title->getDBkey(),
+ 'wl_user' => $watchers,
'wl_namespace' => $title->getNamespace(),
- 'wl_user' => $watchers
+ 'wl_title' => $title->getDBkey(),
), __METHOD__
);
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
}
}
@@ -620,32 +622,37 @@ class EmailNotification {
$postTransformKeys = array();
if ( $this->oldid ) {
- if ( $wgEnotifImpersonal ) {
- // For impersonal mail, show a diff link to the last revision.
- $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_lastdiff',
- $this->title->getCanonicalUrl( 'diff=next&oldid=' . $this->oldid ) );
- } else {
- $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_lastvisited',
- $this->title->getCanonicalUrl( 'diff=0&oldid=' . $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 ) )
+ ->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 ) )
+ ->inContentLanguage()->text();
}
$keys['$OLDID'] = $this->oldid;
- $keys['$CHANGEDORCREATED'] = wfMsgForContent( 'changed' );
+ $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
} else {
- $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_newpagetext' );
+ $keys['$NEWPAGE'] = wfMessage( 'enotif_newpagetext' )->inContentLanguage()->text();
# clear $OLDID placeholder in the message template
$keys['$OLDID'] = '';
- $keys['$CHANGEDORCREATED'] = wfMsgForContent( 'created' );
+ $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
}
$keys['$PAGETITLE'] = $this->title->getPrefixedText();
$keys['$PAGETITLE_URL'] = $this->title->getCanonicalUrl();
- $keys['$PAGEMINOREDIT'] = $this->minorEdit ? wfMsgForContent( 'minoredit' ) : '';
+ $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
+ wfMessage( 'minoredit' )->inContentLanguage()->text() : '';
$keys['$UNWATCHURL'] = $this->title->getCanonicalUrl( 'action=unwatch' );
if ( $this->editor->isAnon() ) {
# real anon (user:xxx.xxx.xxx.xxx)
- $keys['$PAGEEDITOR'] = wfMsgForContent( 'enotif_anon_editor', $this->editor->getName() );
- $keys['$PAGEEDITOR_EMAIL'] = wfMsgForContent( 'noemailtitle' );
+ $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() );
@@ -659,12 +666,12 @@ class EmailNotification {
# Now build message's subject and body
- $subject = wfMsgExt( 'enotif_subject', 'content' );
+ $subject = wfMessage( 'enotif_subject' )->inContentLanguage()->plain();
$subject = strtr( $subject, $keys );
$subject = MessageCache::singleton()->transform( $subject, false, null, $this->title );
$this->subject = strtr( $subject, $postTransformKeys );
- $body = wfMsgExt( 'enotif_body', 'content' );
+ $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
$body = strtr( $body, $keys );
$body = MessageCache::singleton()->transform( $body, false, null, $this->title );
$this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
@@ -754,6 +761,7 @@ class EmailNotification {
/**
* Same as sendPersonalised but does impersonal mail suitable for bulk
* mailing. Takes an array of MailAddress objects.
+ * @return Status
*/
function sendImpersonal( $addresses ) {
global $wgContLang;
@@ -765,7 +773,7 @@ class EmailNotification {
array( '$WATCHINGUSERNAME',
'$PAGEEDITDATE',
'$PAGEEDITTIME' ),
- array( wfMsgForContent( 'enotif_impersonal_salutation' ),
+ array( wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
$wgContLang->date( $this->timestamp, false, false ),
$wgContLang->time( $this->timestamp, false, false ) ),
$this->body );
diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php
index dfce8adf..26ac3dcd 100644
--- a/includes/UserRightsProxy.php
+++ b/includes/UserRightsProxy.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Representation of an user on a other locally-hosted wiki.
+ *
+ * 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
+ */
/**
* Cut-down copy of User interface for local-interwiki-database
@@ -163,6 +183,7 @@ class UserRightsProxy {
/**
* Replaces User::getUserGroups()
+ * @return array
*/
function getGroups() {
$res = $this->db->select( 'user_groups',
diff --git a/includes/ViewCountUpdate.php b/includes/ViewCountUpdate.php
index a30b0f79..28ba3414 100644
--- a/includes/ViewCountUpdate.php
+++ b/includes/ViewCountUpdate.php
@@ -48,8 +48,7 @@ class ViewCountUpdate implements DeferrableUpdate {
$dbw = wfGetDB( DB_MASTER );
if ( $wgHitcounterUpdateFreq <= 1 || $dbw->getType() == 'sqlite' ) {
- $pageTable = $dbw->tableName( 'page' );
- $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = {$this->id}" );
+ $dbw->update( 'page', array( 'page_counter = page_counter + 1' ), array( 'page_id' => $this->id ), __METHOD__ );
return;
}
@@ -71,10 +70,7 @@ class ViewCountUpdate implements DeferrableUpdate {
$dbw = wfGetDB( DB_MASTER );
- $hitcounterTable = $dbw->tableName( 'hitcounter' );
- $res = $dbw->query( "SELECT COUNT(*) as n FROM $hitcounterTable" );
- $row = $dbw->fetchObject( $res );
- $rown = intval( $row->n );
+ $rown = $dbw->selectField( 'hitcounter', 'COUNT(*)', array(), __METHOD__ );
if ( $rown < $wgHitcounterUpdateFreq ) {
return;
@@ -87,6 +83,7 @@ class ViewCountUpdate implements DeferrableUpdate {
$dbType = $dbw->getType();
$tabletype = $dbType == 'mysql' ? "ENGINE=HEAP " : '';
+ $hitcounterTable = $dbw->tableName( 'hitcounter' );
$acchitsTable = $dbw->tableName( 'acchits' );
$pageTable = $dbw->tableName( 'page' );
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
index 031b2b48..932af169 100644
--- a/includes/WatchedItem.php
+++ b/includes/WatchedItem.php
@@ -1,14 +1,34 @@
<?php
/**
+ * Accessor and mutator for watchlist 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
* @ingroup Watchlist
*/
/**
+ * Representation of a pair of user and title for watchlist entries.
+ *
* @ingroup Watchlist
*/
class WatchedItem {
var $mTitle, $mUser, $id, $ns, $ti;
+ private $loaded = false, $watched, $timestamp;
/**
* Create a WatchedItem object with the given user and title
@@ -32,18 +52,83 @@ class WatchedItem {
}
/**
- * Is mTitle being watched by mUser?
- * @return bool
+ * Return an array of conditions to select or update the appropriate database
+ * row.
+ *
+ * @return array
*/
- public function isWatched() {
+ private function dbCond() {
+ return array( 'wl_user' => $this->id, 'wl_namespace' => $this->ns, 'wl_title' => $this->ti );
+ }
+
+ /**
+ * Load the object from the database
+ */
+ private function load() {
+ if ( $this->loaded ) {
+ return;
+ }
+ $this->loaded = true;
+
# Pages and their talk pages are considered equivalent for watching;
# remember that talk namespaces are numbered as page namespace+1.
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'watchlist', 1, array( 'wl_user' => $this->id, 'wl_namespace' => $this->ns,
- 'wl_title' => $this->ti ), __METHOD__ );
- $iswatched = ($dbr->numRows( $res ) > 0) ? 1 : 0;
- return $iswatched;
+ $row = $dbr->selectRow( 'watchlist', 'wl_notificationtimestamp',
+ $this->dbCond(), __METHOD__ );
+
+ if ( $row === false ) {
+ $this->watched = false;
+ } else {
+ $this->watched = true;
+ $this->timestamp = $row->wl_notificationtimestamp;
+ }
+ }
+
+ /**
+ * Is mTitle being watched by mUser?
+ * @return bool
+ */
+ public function isWatched() {
+ $this->load();
+ return $this->watched;
+ }
+
+ /**
+ * Get the notification timestamp of this entry.
+ *
+ * @return false|null|string: false if the page is not watched, the value of
+ * the wl_notificationtimestamp field otherwise
+ */
+ public function getNotificationTimestamp() {
+ $this->load();
+ if ( $this->watched ) {
+ return $this->timestamp;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Reset the notification timestamp of this entry
+ *
+ * @param $force Whether to force the write query to be executed even if the
+ * page is not watched or the notification timestamp is already NULL.
+ */
+ public function resetNotificationTimestamp( $force = '' ) {
+ if ( $force != 'force' ) {
+ $this->load();
+ if ( !$this->watched || $this->timestamp === null ) {
+ return;
+ }
+ }
+
+ // If the page is watched by the user (or may be watched), update the timestamp on any
+ // any matching rows
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => null ),
+ $this->dbCond(), __METHOD__ );
+ $this->timestamp = null;
}
/**
@@ -75,6 +160,8 @@ class WatchedItem {
'wl_notificationtimestamp' => null
), __METHOD__, 'IGNORE' );
+ $this->watched = true;
+
wfProfileOut( __METHOD__ );
return true;
}
@@ -115,6 +202,8 @@ class WatchedItem {
$success = true;
}
+ $this->watched = false;
+
wfProfileOut( __METHOD__ );
return $success;
}
@@ -139,7 +228,7 @@ class WatchedItem {
*
* @return bool
*/
- private static function doDuplicateEntries( $ot, $nt ) {
+ private static function doDuplicateEntries( $ot, $nt ) {
$oldnamespace = $ot->getNamespace();
$newnamespace = $nt->getNamespace();
$oldtitle = $ot->getDBkey();
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
index 39f9cb8e..2cc6338b 100644
--- a/includes/WebRequest.php
+++ b/includes/WebRequest.php
@@ -77,6 +77,7 @@ class WebRequest {
* @return Array: Any query arguments found in path matches.
*/
static public 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.
// So we will use REQUEST_URI if possible.
@@ -87,7 +88,9 @@ class WebRequest {
if ( !preg_match( '!^https?://!', $url ) ) {
$url = 'http://unused' . $url;
}
+ wfSuppressWarnings();
$a = parse_url( $url );
+ wfRestoreWarnings();
if( $a ) {
$path = isset( $a['path'] ) ? $a['path'] : '';
@@ -134,15 +137,17 @@ class WebRequest {
$matches = $router->parse( $path );
}
- } elseif ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
- // Mangled PATH_INFO
- // http://bugs.php.net/bug.php?id=31892
- // Also reported when ini_get('cgi.fix_pathinfo')==false
- $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
-
- } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) {
- // Regular old PATH_INFO yay
- $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+ } elseif ( $wgUsePathInfo ) {
+ if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
+ // Mangled PATH_INFO
+ // http://bugs.php.net/bug.php?id=31892
+ // Also reported when ini_get('cgi.fix_pathinfo')==false
+ $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
+
+ } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) {
+ // Regular old PATH_INFO yay
+ $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+ }
}
return $matches;
@@ -206,18 +211,14 @@ class WebRequest {
* available variant URLs.
*/
public function interpolateTitle() {
- global $wgUsePathInfo;
-
// bug 16019: title interpolation on API queries is useless and sometimes harmful
if ( defined( 'MW_API' ) ) {
return;
}
- if ( $wgUsePathInfo ) {
- $matches = self::getPathInfo( 'title' );
- foreach( $matches as $key => $val) {
- $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
- }
+ $matches = self::getPathInfo( 'title' );
+ foreach( $matches as $key => $val) {
+ $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
}
}
@@ -298,8 +299,8 @@ class WebRequest {
/**
* Recursively normalizes UTF-8 strings in the given array.
*
- * @param $data string or array
- * @return cleaned-up version of the given
+ * @param $data string|array
+ * @return array|string cleaned-up version of the given
* @private
*/
function normalizeUnicode( $data ) {
@@ -379,6 +380,23 @@ class WebRequest {
return $ret;
}
+
+ /**
+ * Unset an arbitrary value from our get/post data.
+ *
+ * @param $key String: key name to use
+ * @return Mixed: old value if one was present, null otherwise
+ */
+ public function unsetVal( $key ) {
+ if ( !isset( $this->data[$key] ) ) {
+ $ret = null;
+ } else {
+ $ret = $this->data[$key];
+ unset( $this->data[$key] );
+ }
+ return $ret;
+ }
+
/**
* Fetch an array from the input or return $default if it's not set.
* If source was scalar, will return an array with a single element.
@@ -480,17 +498,16 @@ class WebRequest {
public function getCheck( $name ) {
# Checkboxes and buttons are only present when clicked
# Presence connotes truth, abscense false
- $val = $this->getVal( $name, null );
- return isset( $val );
+ return $this->getVal( $name, null ) !== null;
}
/**
* Fetch a text string from the given array or return $default if it's not
* set. Carriage returns are stripped from the text, and with some language
* modules there is an input transliteration applied. This should generally
- * be used for form <textarea> and <input> fields. Used for user-supplied
- * freeform text input (for which input transformations may be required - e.g.
- * Esperanto x-coding).
+ * be used for form "<textarea>" and "<input>" fields. Used for
+ * user-supplied freeform text input (for which input transformations may
+ * be required - e.g. Esperanto x-coding).
*
* @param $name String
* @param $default String: optional
@@ -518,7 +535,7 @@ class WebRequest {
$retVal = array();
foreach ( $names as $name ) {
- $value = $this->getVal( $name );
+ $value = $this->getGPCVal( $this->data, $name, null );
if ( !is_null( $value ) ) {
$retVal[$name] = $value;
}
@@ -547,6 +564,15 @@ class WebRequest {
}
/**
+ * Get the HTTP method used for this request.
+ *
+ * @return String
+ */
+ public function getMethod() {
+ return isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : 'GET';
+ }
+
+ /**
* Returns true if the present request was reached by a POST operation,
* false otherwise (GET, HEAD, or command-line).
*
@@ -556,7 +582,7 @@ class WebRequest {
* @return Boolean
*/
public function wasPosted() {
- return isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] == 'POST';
+ return $this->getMethod() == 'POST';
}
/**
@@ -655,6 +681,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 '?'
* @return String
@@ -838,7 +865,7 @@ class WebRequest {
* Get a request header, or false if it isn't set
* @param $name String: case-insensitive header name
*
- * @return string|false
+ * @return string|bool False on failure
*/
public function getHeader( $name ) {
$this->initHeaders();
@@ -964,9 +991,11 @@ HTML;
/**
* Parse the Accept-Language header sent by the client into an array
- * @return array array( languageCode => q-value ) sorted by q-value in descending order
+ * @return array array( languageCode => q-value ) sorted by q-value in descending order then
+ * appearing time in the header in ascending order.
* May contain the "language" '*', which applies to languages other than those explicitly listed.
* This is aligned with rfc2616 section 14.4
+ * Preference for earlier languages appears in rfc3282 as an extension to HTTP/1.1.
*/
public function getAcceptLang() {
// Modified version of code found at http://www.thefutureoftheweb.com/blog/use-accept-language-header
@@ -987,19 +1016,25 @@ HTML;
return array();
}
- // Create a list like "en" => 0.8
- $langs = array_combine( $lang_parse[1], $lang_parse[4] );
+ $langcodes = $lang_parse[1];
+ $qvalues = $lang_parse[4];
+ $indices = range( 0, count( $lang_parse[1] ) - 1 );
+
// Set default q factor to 1
- foreach ( $langs as $lang => $val ) {
- if ( $val === '' ) {
- $langs[$lang] = 1;
- } elseif ( $val == 0 ) {
- unset($langs[$lang]);
+ foreach ( $indices as $index ) {
+ if ( $qvalues[$index] === '' ) {
+ $qvalues[$index] = 1;
+ } elseif ( $qvalues[$index] == 0 ) {
+ unset( $langcodes[$index], $qvalues[$index], $indices[$index] );
}
}
- // Sort list
- arsort( $langs, SORT_NUMERIC );
+ // Sort list. First by $qvalues, then by order. Reorder $langcodes the same way
+ array_multisort( $qvalues, SORT_DESC, SORT_NUMERIC, $indices, $langcodes );
+
+ // Create a list like "en" => 0.8
+ $langs = array_combine( $langcodes, $qvalues );
+
return $langs;
}
@@ -1251,6 +1286,10 @@ class FauxRequest extends WebRequest {
}
}
+ public function getMethod() {
+ return $this->wasPosted ? 'POST' : 'GET';
+ }
+
/**
* @return bool
*/
@@ -1336,6 +1375,7 @@ class FauxRequest extends WebRequest {
* (cookies, session and headers).
*
* @ingroup HTTP
+ * @since 1.19
*/
class DerivativeRequest extends FauxRequest {
private $base;
@@ -1366,7 +1406,7 @@ class DerivativeRequest extends FauxRequest {
}
public function setSessionData( $key, $data ) {
- return $this->base->setSessionData( $key, $data );
+ $this->base->setSessionData( $key, $data );
}
public function getAcceptLang() {
diff --git a/includes/WebStart.php b/includes/WebStart.php
index 17f8216b..01c5eea8 100644
--- a/includes/WebStart.php
+++ b/includes/WebStart.php
@@ -81,7 +81,7 @@ define( 'MEDIAWIKI', true );
# Full path to working directory.
# Makes it possible to for example to have effective exclude path in apc.
# Also doesn't break installations using symlinked includes, like
-# dirname( __FILE__ ) would do.
+# __DIR__ would do.
$IP = getenv( 'MW_INSTALL_PATH' );
if ( $IP === false ) {
$IP = realpath( '.' );
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 6ead57c4..a4a89032 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -62,7 +62,6 @@ class MediaWiki {
}
$this->context = $context;
- $this->context->setTitle( $this->parseTitle() );
}
/**
@@ -134,6 +133,34 @@ class MediaWiki {
}
/**
+ * Returns the name of the action that will be executed.
+ *
+ * @return string: action
+ */
+ public function getAction() {
+ static $action = null;
+
+ if ( $action === null ) {
+ $action = Action::getActionName( $this->context );
+ }
+
+ return $action;
+ }
+
+ /**
+ * Create an Article object of the appropriate class for the given page.
+ *
+ * @deprecated in 1.18; use Article::newFromTitle() instead
+ * @param $title Title
+ * @param $context IContextSource
+ * @return Article object
+ */
+ public static function articleFromTitle( $title, IContextSource $context ) {
+ wfDeprecated( __METHOD__, '1.18' );
+ return Article::newFromTitle( $title, $context );
+ }
+
+ /**
* Performs the request.
* - bad titles
* - read restriction
@@ -269,11 +296,10 @@ class MediaWiki {
$pageView = true;
/**
* $wgArticle is deprecated, do not use it.
- * This will be removed entirely in 1.20.
* @deprecated since 1.18
*/
global $wgArticle;
- $wgArticle = $article;
+ $wgArticle = new DeprecatedGlobal( 'wgArticle', $article, '1.18' );
$this->performAction( $article );
} elseif ( is_string( $article ) ) {
@@ -293,34 +319,6 @@ class MediaWiki {
}
/**
- * Create an Article object of the appropriate class for the given page.
- *
- * @deprecated in 1.18; use Article::newFromTitle() instead
- * @param $title Title
- * @param $context IContextSource
- * @return Article object
- */
- public static function articleFromTitle( $title, IContextSource $context ) {
- wfDeprecated( __METHOD__, '1.18' );
- return Article::newFromTitle( $title, $context );
- }
-
- /**
- * Returns the name of the action that will be executed.
- *
- * @return string: action
- */
- public function getAction() {
- static $action = null;
-
- if ( $action === null ) {
- $action = Action::getActionName( $this->context );
- }
-
- return $action;
- }
-
- /**
* Initialize the main Article object for "standard" actions (view, etc)
* Create an Article object for the page, following redirects if needed.
*
@@ -394,75 +392,13 @@ class MediaWiki {
}
/**
- * Cleaning up request by doing deferred updates, DB transaction, and the output
- */
- public function finalCleanup() {
- wfProfileIn( __METHOD__ );
- // Now commit any transactions, so that unreported errors after
- // output() don't roll back the whole DB transaction
- $factory = wfGetLBFactory();
- $factory->commitMasterChanges();
- // Output everything!
- $this->context->getOutput()->output();
- // Do any deferred jobs
- DeferredUpdates::doUpdates( 'commit' );
- $this->doJobs();
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Do a job from the job queue
- */
- private function doJobs() {
- global $wgJobRunRate;
-
- if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
- return;
- }
- if ( $wgJobRunRate < 1 ) {
- $max = mt_getrandmax();
- if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
- return;
- }
- $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";
- }
- wfDebugLog( 'jobqueue', $output );
- }
- }
-
- /**
- * Ends this task peacefully
- */
- public function restInPeace() {
- MessageCache::logMessages();
- wfLogProfilingData();
- // Commit and close up!
- $factory = wfGetLBFactory();
- $factory->commitMasterChanges();
- $factory->shutdown();
- wfDebug( "Request ended normally\n" );
- }
-
- /**
* Perform one of the "standard" actions
*
* @param $page Page
*/
private function performAction( Page $page ) {
+ global $wgUseSquid, $wgSquidMaxage;
+
wfProfileIn( __METHOD__ );
$request = $this->context->getRequest();
@@ -481,6 +417,13 @@ class MediaWiki {
$action = Action::factory( $act, $page );
if ( $action instanceof Action ) {
+ # Let Squid cache things if we can purge them.
+ if ( $wgUseSquid &&
+ in_array( $request->getFullRequestURL(), $title->getSquidURLs() )
+ ) {
+ $output->setSquidMaxage( $wgSquidMaxage );
+ }
+
$action->show();
wfProfileOut( __METHOD__ );
return;
@@ -591,8 +534,72 @@ class MediaWiki {
}
$this->performRequest();
- $this->finalCleanup();
+
+ // Now commit any transactions, so that unreported errors after
+ // output() don't roll back the whole DB transaction
+ wfGetLBFactory()->commitMasterChanges();
+
+ // Output everything!
+ $this->context->getOutput()->output();
wfProfileOut( __METHOD__ );
}
+
+ /**
+ * Ends this task peacefully
+ */
+ public function restInPeace() {
+ // Do any deferred jobs
+ DeferredUpdates::doUpdates( 'commit' );
+
+ // 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();
+
+ // Commit and close up!
+ $factory = wfGetLBFactory();
+ $factory->commitMasterChanges();
+ $factory->shutdown();
+
+ wfDebug( "Request ended normally\n" );
+ }
+
+ /**
+ * Do a job from the job queue
+ */
+ private function doJobs() {
+ global $wgJobRunRate;
+
+ if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
+ return;
+ }
+ if ( $wgJobRunRate < 1 ) {
+ $max = mt_getrandmax();
+ if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
+ return;
+ }
+ $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";
+ }
+ wfDebugLog( 'jobqueue', $output );
+ }
+ }
}
diff --git a/includes/WikiCategoryPage.php b/includes/WikiCategoryPage.php
index 01938cd9..d3820016 100644
--- a/includes/WikiCategoryPage.php
+++ b/includes/WikiCategoryPage.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Special handling for category 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
+ */
+
+/**
* Special handling for category pages
*/
class WikiCategoryPage extends WikiPage {
diff --git a/includes/WikiError.php b/includes/WikiError.php
index 7c167f61..45ee20c9 100644
--- a/includes/WikiError.php
+++ b/includes/WikiError.php
@@ -91,22 +91,22 @@ class WikiErrorMsg extends WikiError {
wfDeprecated( __METHOD__, '1.17' );
$args = func_get_args();
array_shift( $args );
- $this->mMessage = wfMsgReal( $message, $args, true );
+ $this->mMessage = wfMessage( $message )->rawParams( $args )->text();
$this->mMsgKey = $message;
$this->mMsgArgs = $args;
}
-
+
function getMessageKey() {
return $this->mMsgKey;
}
-
+
function getMessageArgs() {
return $this->mMsgArgs;
}
}
/**
- * Error class designed to handle errors involved with
+ * Error class designed to handle errors involved with
* XML parsing
* @ingroup Exception
*/
@@ -134,12 +134,12 @@ class WikiXmlError extends WikiError {
/** @return string */
function getMessage() {
// '$1 at line $2, col $3 (byte $4): $5',
- return wfMsgHtml( 'xml-error-string',
+ return wfMessage( 'xml-error-string',
$this->mMessage,
$this->mLine,
$this->mColumn,
$this->mByte . $this->mContext,
- xml_error_string( $this->mXmlError ) );
+ xml_error_string( $this->mXmlError ) )->escaped();
}
function _extractContext( $context, $offset ) {
diff --git a/includes/WikiFilePage.php b/includes/WikiFilePage.php
index 8aeaa243..9fb1522d 100644
--- a/includes/WikiFilePage.php
+++ b/includes/WikiFilePage.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Special handling for file 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
+ */
+
+/**
* Special handling for file pages
*
* @ingroup Media
@@ -40,12 +61,9 @@ class WikiFilePage extends WikiPage {
}
$this->mFileLoaded = true;
- $this->mFile = false;
+ $this->mFile = wfFindFile( $this->mTitle );
if ( !$this->mFile ) {
- $this->mFile = wfFindFile( $this->mTitle );
- if ( !$this->mFile ) {
- $this->mFile = wfLocalFile( $this->mTitle ); // always a File
- }
+ $this->mFile = wfLocalFile( $this->mTitle ); // always a File
}
$this->mRepo = $this->mFile->getRepo();
return true;
@@ -148,6 +166,7 @@ class WikiFilePage extends WikiPage {
/**
* Override handling of action=purge
+ * @return bool
*/
public function doPurge() {
$this->loadFile();
diff --git a/includes/WikiMap.php b/includes/WikiMap.php
index 6c7f23b5..4a5e2bcf 100644
--- a/includes/WikiMap.php
+++ b/includes/WikiMap.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Tools for dealing with other locally-hosted wikis.
+ *
+ * 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
+ */
/**
* Helper tools for dealing with other locally-hosted wikis
@@ -34,7 +54,7 @@ class WikiMap {
*
* @todo We can give more info than just the wiki id!
* @param $wikiID String: wiki'd id (generally database name)
- * @return Wiki's name or $wiki_id if the wiki was not found
+ * @return string|int Wiki's name or $wiki_id if the wiki was not found
*/
public static function getWikiName( $wikiID ) {
$wiki = WikiMap::getWiki( $wikiID );
@@ -89,7 +109,7 @@ class WikiMap {
$wiki = WikiMap::getWiki( $wikiID );
if ( $wiki ) {
- return $wiki->getUrl( $page );
+ return $wiki->getFullUrl( $page );
}
return false;
@@ -106,6 +126,13 @@ class WikiReference {
private $mServer; ///< server URL, may be protocol-relative, e.g. '//www.mediawiki.org'
private $mPath; ///< path, '/wiki/$1'
+ /**
+ * @param $major string
+ * @param $minor string
+ * @param $canonicalServer string
+ * @param $path string
+ * @param $server null|string
+ */
public function __construct( $major, $minor, $canonicalServer, $path, $server = null ) {
$this->mMajor = $major;
$this->mMinor = $minor;
@@ -167,7 +194,16 @@ class WikiReference {
}
/**
+ * Get a canonical server URL
+ * @return string
+ */
+ public function getCanonicalServer() {
+ return $this->mCanonicalServer;
+ }
+
+ /**
* Alias for getCanonicalUrl(), for backwards compatibility.
+ * @param $page string
* @return String
*/
public function getUrl( $page ) {
diff --git a/includes/WikiPage.php b/includes/WikiPage.php
index acc9831a..bc8766de 100644
--- a/includes/WikiPage.php
+++ b/includes/WikiPage.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Base representation for a MediaWiki page.
+ *
+ * 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
+ */
+
+/**
* Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
*/
abstract class Page {}
@@ -12,30 +33,8 @@ abstract class Page {}
*
* @internal documentation reviewed 15 Mar 2010
*/
-class WikiPage extends Page {
- // doDeleteArticleReal() return values. Values less than zero indicate fatal errors,
- // values greater than zero indicate that there were problems not resulting in page
- // not being deleted
-
- /**
- * Delete operation aborted by hook
- */
- const DELETE_HOOK_ABORTED = -1;
-
- /**
- * Deletion successful
- */
- const DELETE_SUCCESS = 0;
-
- /**
- * Page not found
- */
- const DELETE_NO_PAGE = 1;
-
- /**
- * No revisions found to delete
- */
- const DELETE_NO_REVISIONS = 2;
+class WikiPage extends Page implements IDBAccessObject {
+ // Constants for $mDataLoadedFrom and related
/**
* @var Title
@@ -52,6 +51,11 @@ class WikiPage extends Page {
/**@}}*/
/**
+ * @var int; one of the READ_* constants
+ */
+ protected $mDataLoadedFrom = self::READ_NONE;
+
+ /**
* @var Title
*/
protected $mRedirectTarget = null;
@@ -88,6 +92,7 @@ class WikiPage extends Page {
* Create a WikiPage object of the appropriate class for the given title.
*
* @param $title Title
+ * @throws MWException
* @return WikiPage object of the appropriate type
*/
public static function factory( Title $title ) {
@@ -117,15 +122,58 @@ class WikiPage extends Page {
* Constructor from a page id
*
* @param $id Int article ID to load
+ * @param $from string|int 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
*
+ * @return WikiPage|null
+ */
+ public static function newFromID( $id, $from = 'fromdb' ) {
+ $from = self::convertSelectType( $from );
+ $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
+ $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
+ if ( !$row ) {
+ return null;
+ }
+ return self::newFromRow( $row, $from );
+ }
+
+ /**
+ * Constructor from a database row
+ *
+ * @since 1.20
+ * @param $row object: database row containing at least fields returned
+ * by selectFields().
+ * @param $from string|int: 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
* @return WikiPage
*/
- public static function newFromID( $id ) {
- $t = Title::newFromID( $id );
- if ( $t ) {
- return self::factory( $t );
+ public static function newFromRow( $row, $from = 'fromdb' ) {
+ $page = self::factory( Title::newFromRow( $row ) );
+ $page->loadFromRow( $row, $from );
+ return $page;
+ }
+
+ /**
+ * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
+ *
+ * @param $type object|string|int
+ * @return mixed
+ */
+ private static function convertSelectType( $type ) {
+ switch ( $type ) {
+ case 'fromdb':
+ return self::READ_NORMAL;
+ case 'fromdbmaster':
+ return self::READ_LATEST;
+ case 'forupdate':
+ return self::READ_LOCKING;
+ default:
+ // It may already be an integer or whatever else
+ return $type;
}
- return null;
}
/**
@@ -152,10 +200,20 @@ class WikiPage extends Page {
/**
* Clear the object
+ * @return void
*/
public function clear() {
$this->mDataLoaded = false;
+ $this->mDataLoadedFrom = self::READ_NONE;
+
+ $this->clearCacheFields();
+ }
+ /**
+ * Clear the object cache fields
+ * @return void
+ */
+ protected function clearCacheFields() {
$this->mCounter = null;
$this->mRedirectTarget = null; # Title object if set
$this->mLastRevision = null; # Latest revision
@@ -192,14 +250,15 @@ class WikiPage extends Page {
* Fetch a page record with the given conditions
* @param $dbr DatabaseBase object
* @param $conditions Array
+ * @param $options Array
* @return mixed Database result resource, or false on failure
*/
- protected function pageData( $dbr, $conditions ) {
+ protected function pageData( $dbr, $conditions, $options = array() ) {
$fields = self::selectFields();
wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) );
- $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__ );
+ $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) );
@@ -212,12 +271,13 @@ class WikiPage extends Page {
*
* @param $dbr DatabaseBase object
* @param $title Title object
+ * @param $options Array
* @return mixed Database result resource, or false on failure
*/
- public function pageDataFromTitle( $dbr, $title ) {
+ public function pageDataFromTitle( $dbr, $title, $options = array() ) {
return $this->pageData( $dbr, array(
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey() ) );
+ 'page_title' => $title->getDBkey() ), $options );
}
/**
@@ -225,37 +285,69 @@ class WikiPage extends Page {
*
* @param $dbr DatabaseBase
* @param $id Integer
+ * @param $options Array
* @return mixed Database result resource, or false on failure
*/
- public function pageDataFromId( $dbr, $id ) {
- return $this->pageData( $dbr, array( 'page_id' => $id ) );
+ public function pageDataFromId( $dbr, $id, $options = array() ) {
+ return $this->pageData( $dbr, array( 'page_id' => $id ), $options );
}
/**
* Set the general counter, title etc data loaded from
* some source.
*
- * @param $data Object|String One of the following:
- * A DB query result object or...
- * "fromdb" to get from a slave DB or...
- * "fromdbmaster" to get from the master DB
+ * @param $from object|string|int One of the following:
+ * - A DB query result object
+ * - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB
+ * - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB
+ * - "forupdate" or WikiPage::READ_LOCKING to get from the master DB using SELECT FOR UPDATE
+ *
* @return void
*/
- public function loadPageData( $data = 'fromdb' ) {
- if ( $data === 'fromdbmaster' ) {
+ public function loadPageData( $from = 'fromdb' ) {
+ $from = self::convertSelectType( $from );
+ if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
+ // We already have the data from the correct location, no need to load it twice.
+ return;
+ }
+
+ if ( $from === self::READ_LOCKING ) {
+ $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) );
+ } elseif ( $from === self::READ_LATEST ) {
$data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
- } elseif ( $data === 'fromdb' ) { // slave
+ } 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.
$touched = $this->getCachedLastEditTime();
if ( $touched ) { // key set
if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
+ $from = self::READ_LATEST;
$data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
}
}
+ } else {
+ // No idea from where the caller got this data, assume slave database.
+ $data = $from;
+ $from = self::READ_NORMAL;
}
+ $this->loadFromRow( $data, $from );
+ }
+
+ /**
+ * Load the object from a database row
+ *
+ * @since 1.20
+ * @param $data object: database row containing at least fields returned
+ * by selectFields()
+ * @param $from string|int 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
+ * the master DB using SELECT FOR UPDATE
+ */
+ public function loadFromRow( $data, $from ) {
$lc = LinkCache::singleton();
if ( $data ) {
@@ -270,13 +362,22 @@ class WikiPage extends Page {
$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 ) {
+ $this->mLastRevision = null;
+ $this->mTimestamp = '';
+ }
} else {
$lc->addBadLinkObj( $this->mTitle );
$this->mTitle->loadFromRow( false );
+
+ $this->clearCacheFields();
}
$this->mDataLoaded = true;
+ $this->mDataLoadedFrom = self::convertSelectType( $from );
}
/**
@@ -368,6 +469,45 @@ class WikiPage extends Page {
}
/**
+ * Get the Revision object of the oldest revision
+ * @return Revision|null
+ */
+ public function getOldestRevision() {
+ wfProfileIn( __METHOD__ );
+
+ // Try using the slave database first, then try the master
+ $continue = 2;
+ $db = wfGetDB( DB_SLAVE );
+ $revSelectFields = Revision::selectFields();
+
+ while ( $continue ) {
+ $row = $db->selectRow(
+ array( 'page', 'revision' ),
+ $revSelectFields,
+ array(
+ 'page_namespace' => $this->mTitle->getNamespace(),
+ 'page_title' => $this->mTitle->getDBkey(),
+ 'rev_page = page_id'
+ ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'rev_timestamp ASC'
+ )
+ );
+
+ if ( $row ) {
+ $continue = 0;
+ } else {
+ $db = wfGetDB( DB_MASTER );
+ $continue--;
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $row ? Revision::newFromRow( $row ) : null;
+ }
+
+ /**
* Loads everything except the text
* This isn't necessary for all uses, so it's only done if needed.
*/
@@ -381,7 +521,14 @@ class WikiPage extends Page {
return; // page doesn't exist or is missing page_latest info
}
- $revision = Revision::newFromPageId( $this->getId(), $latest );
+ // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the
+ // latest changes committed. This is true even within REPEATABLE-READ transactions, where
+ // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to
+ // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row
+ // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT.
+ // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read.
+ $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0;
+ $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
if ( $revision ) { // sanity
$this->setLastEdit( $revision );
}
@@ -414,7 +561,7 @@ class WikiPage extends Page {
* 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|false The text of the current revision
+ * @return String|bool The text of the current revision. False on failure
*/
public function getText( $audience = Revision::FOR_PUBLIC ) {
$this->loadLastEdit();
@@ -427,7 +574,7 @@ class WikiPage extends Page {
/**
* Get the text of the current revision. No side-effects...
*
- * @return String|false The text of the current revision
+ * @return String|bool The text of the current revision. False on failure
*/
public function getRawText() {
$this->loadLastEdit();
@@ -445,6 +592,7 @@ class WikiPage extends Page {
if ( !$this->mTimestamp ) {
$this->loadLastEdit();
}
+
return wfTimestamp( TS_MW, $this->mTimestamp );
}
@@ -474,6 +622,24 @@ class WikiPage extends Page {
}
/**
+ * 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::RAW get the text regardless of permissions
+ * @return User|null
+ */
+ public function getCreator( $audience = Revision::FOR_PUBLIC ) {
+ $revision = $this->getOldestRevision();
+ if ( $revision ) {
+ $userName = $revision->getUserText( $audience );
+ return User::newFromName( $userName, false );
+ } else {
+ return null;
+ }
+ }
+
+ /**
* @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to $wgUser
@@ -546,7 +712,7 @@ class WikiPage extends Page {
* Determine whether a page would be suitable for being counted as an
* article in the site_stats table based on the title & its content
*
- * @param $editInfo Object or false: object returned by prepareTextForEdit(),
+ * @param $editInfo Object|bool (false): object returned by prepareTextForEdit(),
* if false, the current database state will be used
* @return Boolean
*/
@@ -726,10 +892,10 @@ class WikiPage extends Page {
$tables = array( 'revision', 'user' );
$fields = array(
- 'rev_user as user_id',
- 'rev_user_text AS user_name',
+ 'user_id' => 'rev_user',
+ 'user_name' => 'rev_user_text',
$realNameField,
- 'MAX(rev_timestamp) AS timestamp',
+ 'timestamp' => 'MAX(rev_timestamp)',
);
$conds = array( 'rev_page' => $this->getId() );
@@ -888,6 +1054,7 @@ class WikiPage extends Page {
/**
* Perform the actions of a page purging
+ * @return bool
*/
public function doPurge() {
global $wgUseSquid;
@@ -903,7 +1070,7 @@ class WikiPage extends Page {
if ( $wgUseSquid ) {
// Commit the transaction before the purge is sent
$dbw = wfGetDB( DB_MASTER );
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
// Send purge
$update = SquidUpdate::newSimplePurge( $this->mTitle );
@@ -1002,7 +1169,7 @@ class WikiPage extends Page {
$conditions,
__METHOD__ );
- $result = $dbw->affectedRows() != 0;
+ $result = $dbw->affectedRows() > 0;
if ( $result ) {
$this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
$this->setLastEdit( $revision );
@@ -1023,7 +1190,7 @@ class WikiPage extends Page {
* @param $dbw DatabaseBase
* @param $redirectTitle Title object pointing to the redirect target,
* or NULL if this is not a redirect
- * @param $lastRevIsRedirect If given, will optimize adding and
+ * @param $lastRevIsRedirect null|bool If given, will optimize adding and
* removing rows in redirect table.
* @return bool true on success, false on failure
* @private
@@ -1059,7 +1226,7 @@ class WikiPage extends Page {
* If the given revision is newer than the currently set page_latest,
* update the page record. Otherwise, do nothing.
*
- * @param $dbw Database object
+ * @param $dbw DatabaseBase object
* @param $revision Revision object
* @return mixed
*/
@@ -1124,7 +1291,7 @@ class WikiPage extends Page {
}
/**
- * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...)
+ * @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
@@ -1160,7 +1327,8 @@ class WikiPage extends Page {
if ( $section == 'new' ) {
# Inserting a new section
- $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
+ $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}"
@@ -1223,9 +1391,10 @@ class WikiPage extends Page {
* edit-already-exists error will be returned. These two conditions are also possible with
* auto-detection due to MediaWiki's performance-optimised locking strategy.
*
- * @param $baseRevId the revision ID this edit was based off, if any
+ * @param bool|int $baseRevId int the revision ID this edit was based off, if any
* @param $user User the user doing the edit
*
+ * @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
@@ -1242,7 +1411,7 @@ class WikiPage extends Page {
* Compatibility note: this function previously returned a boolean value indicating success/failure
*/
public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
- global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
+ global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
# Low-level sanity check
if ( $this->mTitle->getText() === '' ) {
@@ -1254,7 +1423,9 @@ class WikiPage extends Page {
$user = is_null( $user ) ? $wgUser : $user;
$status = Status::newGood( array() );
- # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already
+ // Load the data from the master database if needed.
+ // The caller may already loaded it from the master or even loaded it using
+ // SELECT FOR UPDATE, so do not override that using clear().
$this->loadPageData( 'fromdbmaster' );
$flags = $this->checkFlags( $flags );
@@ -1306,11 +1477,10 @@ class WikiPage extends Page {
wfProfileOut( __METHOD__ );
return $status;
- }
-
- # Make sure the revision is either completely inserted or not inserted at all
- if ( !$wgDBtransactions ) {
- $userAbort = ignore_user_abort( true );
+ } elseif ( $oldtext === false ) {
+ # Sanity check for bug 37225
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Could not find text for current revision {$oldid}." );
}
$revision = new Revision( array(
@@ -1323,11 +1493,14 @@ class WikiPage extends Page {
'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
$changed = ( strcmp( $text, $oldtext ) != 0 );
if ( $changed ) {
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$revisionId = $revision->insertOn( $dbw );
# Update page
@@ -1340,54 +1513,40 @@ class WikiPage extends Page {
$ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect );
if ( !$ok ) {
- /* Belated edit conflict! Run away!! */
+ # Belated edit conflict! Run away!!
$status->fatal( 'edit-conflict' );
- # Delete the invalid revision if the DB is not transactional
- if ( !$wgDBtransactions ) {
- $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ );
- }
+ $dbw->rollback( __METHOD__ );
- $revisionId = 0;
- $dbw->rollback();
- } else {
- global $wgUseRCPatrol;
- wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
- # Update recentchanges
- if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
- # Mark as patrolled if the user can do so
- $patrolled = $wgUseRCPatrol && !count(
- $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
- # 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
- if ( $patrolled ) {
- PatrolLog::record( $rc, true );
- }
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
+ # Update recentchanges
+ if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
+ # Mark as patrolled if the user can do so
+ $patrolled = $wgUseRCPatrol && !count(
+ $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
+ # 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
+ if ( $patrolled ) {
+ PatrolLog::record( $rc, true, $user );
}
- $user->incEditCount();
- $dbw->commit();
}
+ $user->incEditCount();
+ $dbw->commit( __METHOD__ );
} else {
// Bug 32948: revision ID must be set to page {{REVISIONID}} and
// related variables correctly
$revision->setId( $this->getLatest() );
}
- if ( !$wgDBtransactions ) {
- ignore_user_abort( $userAbort );
- }
-
- // Now that ignore_user_abort is restored, we can respond to fatal errors
- if ( !$status->isOK() ) {
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
# Update links tables, site stats, etc.
$this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
'oldcountable' => $oldcountable ) );
@@ -1403,14 +1562,14 @@ class WikiPage extends Page {
# Create new article
$status->value['new'] = true;
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
# 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 ) {
- $dbw->rollback();
+ $dbw->rollback( __METHOD__ );
$status->fatal( 'edit-already-exists' );
wfProfileOut( __METHOD__ );
@@ -1429,6 +1588,9 @@ class WikiPage extends Page {
) );
$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
+
# Update the page record with revision data
$this->updateRevisionOn( $dbw, $revision, 0 );
@@ -1436,8 +1598,6 @@ class WikiPage extends Page {
# Update recentchanges
if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
- global $wgUseRCPatrol, $wgUseNPPatrol;
-
# Mark as patrolled if the user can do so
$patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
@@ -1447,11 +1607,11 @@ class WikiPage extends Page {
# Log auto-patrolled edits
if ( $patrolled ) {
- PatrolLog::record( $rc, true );
+ PatrolLog::record( $rc, true, $user );
}
}
$user->incEditCount();
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
# Update links, etc.
$this->doEditUpdates( $revision, $user, array( 'created' => true ) );
@@ -1480,24 +1640,41 @@ class WikiPage extends Page {
/**
* Get parser options suitable for rendering the primary article wikitext
- * @param User|string $user User object or 'canonical'
+ *
+ * @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).
* @return ParserOptions
*/
- public function makeParserOptions( $user ) {
+ public function makeParserOptions( $context ) {
global $wgContLang;
- if ( $user instanceof User ) { // settings per user (even anons)
- $options = ParserOptions::newFromUser( $user );
+
+ 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 );
}
+
+ if ( $this->getTitle()->isConversionTable() ) {
+ $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
*/
public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
global $wgParser, $wgContLang, $wgUser;
@@ -1568,9 +1745,9 @@ class WikiPage extends Page {
$parserCache->save( $editInfo->output, $this, $editInfo->popts );
}
- # Update the links tables
- $u = new LinksUpdate( $this->mTitle, $editInfo->output );
- $u->doUpdate();
+ # Update the links tables and other secondary data
+ $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
+ DataUpdate::runUpdates( $updates );
wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
@@ -1630,9 +1807,9 @@ class WikiPage extends Page {
wfDebug( __METHOD__ . ": invalid username\n" );
} elseif ( User::isIP( $shortTitle ) ) {
// An anonymous user
- $other->setNewtalk( true );
+ $other->setNewtalk( true, $revision );
} elseif ( $other->isLoggedIn() ) {
- $other->setNewtalk( true );
+ $other->setNewtalk( true, $revision );
} else {
wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
}
@@ -1689,7 +1866,7 @@ class WikiPage extends Page {
* @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
- * @return bool true on success
+ * @return Status
*/
public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) {
global $wgContLang;
@@ -1772,12 +1949,15 @@ class WikiPage extends Page {
if ( $restrictions != '' ) {
$protectDescription .= $wgContLang->getDirMark() . "[$action=$restrictions] (";
if ( $encodedExpiry[$action] != 'infinity' ) {
- $protectDescription .= wfMsgForContent( 'protect-expiring',
+ $protectDescription .= wfMessage(
+ 'protect-expiring',
$wgContLang->timeanddate( $expiry[$action], false, false ) ,
$wgContLang->date( $expiry[$action], false, false ) ,
- $wgContLang->time( $expiry[$action], false, false ) );
+ $wgContLang->time( $expiry[$action], false, false )
+ )->inContentLanguage()->text();
} else {
- $protectDescription .= wfMsgForContent( 'protect-expiry-indefinite' );
+ $protectDescription .= wfMessage( 'protect-expiry-indefinite' )
+ ->inContentLanguage()->text();
}
$protectDescription .= ') ';
@@ -1818,7 +1998,12 @@ class WikiPage extends Page {
}
# Prepare a null revision to be added to the history
- $editComment = $wgContLang->ucfirst( wfMsgForContent( $revCommentMsg, $this->mTitle->getPrefixedText() ) );
+ $editComment = $wgContLang->ucfirst(
+ wfMessage(
+ $revCommentMsg,
+ $this->mTitle->getPrefixedText()
+ )->inContentLanguage()->text()
+ );
if ( $reason ) {
$editComment .= ": $reason";
}
@@ -1826,7 +2011,9 @@ class WikiPage extends Page {
$editComment .= " ($protectDescription)";
}
if ( $cascade ) {
- $editComment .= ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']';
+ // FIXME: Should use 'brackets' message.
+ $editComment .= ' [' . wfMessage( 'protect-summary-cascade' )
+ ->inContentLanguage()->text() . ']';
}
# Insert a null revision
@@ -1893,6 +2080,7 @@ class WikiPage extends Page {
* Take an array of page restrictions and flatten it to a string
* suitable for insertion into the page_restrictions field.
* @param $limit Array
+ * @throws MWException
* @return String
*/
protected static function flattenRestrictions( $limit ) {
@@ -1913,15 +2101,15 @@ class WikiPage extends Page {
}
/**
- * Same as doDeleteArticleReal(), but returns more detailed success/failure status
+ * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
+ * backwards compatibility, if you care about error reporting you should use
+ * doDeleteArticleReal() instead.
+ *
* Deletes the article with database consistency, writes logs, purges caches
*
* @param $reason string delete reason for deletion log
- * @param $suppress bitfield
- * Revision::DELETED_TEXT
- * Revision::DELETED_COMMENT
- * Revision::DELETED_USER
- * Revision::DELETED_RESTRICTED
+ * @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 $commit boolean defaults to true, triggers transaction end
* @param &$error Array of errors to append to
@@ -1931,43 +2119,56 @@ class WikiPage extends Page {
public function doDeleteArticle(
$reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
) {
- return $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user )
- == WikiPage::DELETE_SUCCESS;
+ $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user );
+ return $status->isGood();
}
/**
* Back-end article deletion
* Deletes the article with database consistency, writes logs, purges caches
*
+ * @since 1.19
+ *
* @param $reason string delete reason for deletion log
- * @param $suppress bitfield
- * Revision::DELETED_TEXT
- * Revision::DELETED_COMMENT
- * Revision::DELETED_USER
- * Revision::DELETED_RESTRICTED
- * @param $id int article ID
+ * @param $suppress boolean suppress all revisions and log the deletion in
+ * the suppression log instead of the deletion log
* @param $commit boolean defaults to true, triggers transaction end
* @param &$error Array of errors to append to
* @param $user User The deleting user
- * @return int: One of WikiPage::DELETE_* constants
+ * @return Status: Status object; if successful, $status->value is the log_id of the
+ * deletion log entry. If the page couldn't be deleted because it wasn't
+ * found, $status is a non-fatal 'cannotdelete' error
*/
public function doDeleteArticleReal(
$reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
) {
global $wgUser;
- $user = is_null( $user ) ? $wgUser : $user;
wfDebug( __METHOD__ . "\n" );
- if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error ) ) ) {
- return WikiPage::DELETE_HOOK_ABORTED;
+ $status = Status::newGood();
+
+ if ( $this->mTitle->getDBkey() === '' ) {
+ $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+ return $status;
}
- $dbw = wfGetDB( DB_MASTER );
- $t = $this->mTitle->getDBkey();
- $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
- if ( $t === '' || $id == 0 ) {
- return WikiPage::DELETE_NO_PAGE;
+ $user = is_null( $user ) ? $wgUser : $user;
+ if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) {
+ if ( $status->isOK() ) {
+ // Hook aborted but didn't set a fatal status
+ $status->fatal( 'delete-hook-aborted' );
+ }
+ return $status;
+ }
+
+ if ( $id == 0 ) {
+ $this->loadPageData( 'forupdate' );
+ $id = $this->getID();
+ if ( $id == 0 ) {
+ $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+ return $status;
+ }
}
// Bitfields to further suppress the content
@@ -1982,7 +2183,8 @@ class WikiPage extends Page {
$bitfield = 'rev_deleted';
}
- $dbw->begin();
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->begin( __METHOD__ );
// For now, shunt the revision data into the archive table.
// Text is *not* removed from the text table; bulk storage
// is left intact to avoid breaking block-compression or
@@ -2019,11 +2221,12 @@ class WikiPage extends Page {
# 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 ); // getArticleID() uses slave, could be laggy
if ( !$ok ) {
- $dbw->rollback();
- return WikiPage::DELETE_NO_REVISIONS;
+ $dbw->rollback( __METHOD__ );
+ $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+ return $status;
}
$this->doDeleteUpdates( $id );
@@ -2039,79 +2242,54 @@ class WikiPage extends Page {
$logEntry->publish( $logid );
if ( $commit ) {
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
}
wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) );
- return WikiPage::DELETE_SUCCESS;
+ $status->value = $logid;
+ return $status;
}
/**
* Do some database updates after deletion
*
- * @param $id Int: page_id value of the page being deleted
+ * @param $id Int: page_id value of the page being deleted (B/C, currently unused)
*/
public function doDeleteUpdates( $id ) {
+ # update site status
DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
- $dbw = wfGetDB( DB_MASTER );
-
- # Delete restrictions for it
- $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
-
- # Fix category table counts
- $cats = array();
- $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
-
- foreach ( $res as $row ) {
- $cats [] = $row->cl_to;
- }
-
- $this->updateCategoryCounts( array(), $cats );
-
- # If using cascading deletes, we can skip some explicit deletes
- if ( !$dbw->cascadingDeletes() ) {
- $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
-
- # Delete outgoing links
- $dbw->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
- $dbw->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
- $dbw->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
- $dbw->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
- $dbw->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
- $dbw->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
- $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
- $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
- $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
- }
-
- # If using cleanup triggers, we can skip some manual deletes
- if ( !$dbw->cleanupTriggers() ) {
- # Clean up recentchanges entries...
- $dbw->delete( 'recentchanges',
- array( 'rc_type != ' . RC_LOG,
- 'rc_namespace' => $this->mTitle->getNamespace(),
- 'rc_title' => $this->mTitle->getDBkey() ),
- __METHOD__ );
- $dbw->delete( 'recentchanges',
- array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
- __METHOD__ );
- }
+ # remove secondary indexes, etc
+ $updates = $this->getDeletionUpdates( );
+ DataUpdate::runUpdates( $updates );
# Clear caches
- self::onArticleDelete( $this->mTitle );
+ WikiPage::onArticleDelete( $this->mTitle );
+
+ # Reset this object
+ $this->clear();
# 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;
+ }
+
/**
* Roll back the most recent consecutive set of edits to a page
* from the same user; fails if there are no eligible edits to
* roll back to, e.g. user is the sole contributor. This function
* performs permissions checks on $user, then calls commitRollback()
* to do the dirty work
- *
+ *
* @todo: seperate the business/permission stuff out from backend code
*
* @param $fromP String: Name of the user whose edits to rollback.
@@ -2169,6 +2347,7 @@ class WikiPage extends Page {
*
* @param $resultDetails Array: contains result-specific array of additional values
* @param $guser User The user performing the rollback
+ * @return array
*/
public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) {
global $wgUseRCPatrol, $wgContLang;
@@ -2234,7 +2413,7 @@ class WikiPage extends Page {
array( /* WHERE */
'rc_cur_id' => $current->getPage(),
'rc_user_text' => $current->getUserText(),
- "rc_timestamp > '{$s->rev_timestamp}'",
+ 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
), __METHOD__
);
}
@@ -2243,9 +2422,9 @@ class WikiPage extends Page {
$target = Revision::newFromId( $s->rev_id );
if ( empty( $summary ) ) {
if ( $from == '' ) { // no public user name
- $summary = wfMsgForContent( 'revertpage-nouser' );
+ $summary = wfMessage( 'revertpage-nouser' );
} else {
- $summary = wfMsgForContent( 'revertpage' );
+ $summary = wfMessage( 'revertpage' );
}
}
@@ -2255,7 +2434,14 @@ class WikiPage extends Page {
$wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
$current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
);
- $summary = wfMsgReplaceArgs( $summary, $args );
+ if( $summary instanceof Message ) {
+ $summary = $summary->params( $args )->inContentLanguage()->text();
+ } else {
+ $summary = wfMsgReplaceArgs( $summary, $args );
+ }
+
+ # Truncate for whole multibyte characters.
+ $summary = $wgContLang->truncate( $summary, 255 );
# Save
$flags = EDIT_UPDATE;
@@ -2432,11 +2618,12 @@ class WikiPage extends Page {
if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
$truncatedtext = $wgContLang->truncate(
str_replace( "\n", ' ', $newtext ),
- max( 0, 250
- - strlen( wfMsgForContent( 'autoredircomment' ) )
+ max( 0, 255
+ - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
- strlen( $rt->getFullText() )
) );
- return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
+ return wfMessage( 'autoredircomment', $rt->getFullText() )
+ ->rawParams( $truncatedtext )->inContentLanguage()->text();
}
# New page autosummaries
@@ -2445,22 +2632,24 @@ class WikiPage extends Page {
$truncatedtext = $wgContLang->truncate(
str_replace( "\n", ' ', $newtext ),
- max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
+ max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) );
- return wfMsgForContent( 'autosumm-new', $truncatedtext );
+ return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
}
# Blanking autosummaries
if ( $oldtext != '' && $newtext == '' ) {
- return wfMsgForContent( 'autosumm-blank' );
+ 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( wfMsgForContent( 'autosumm-replace' ) ) ) );
+ max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) );
- return wfMsgForContent( 'autosumm-replace', $truncatedtext );
+ return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
}
# If we reach this point, there's no applicable autosummary for our case, so our
@@ -2535,12 +2724,16 @@ class WikiPage extends Page {
if ( $blank ) {
// The current revision is blank and the one before is also
// blank. It's just not our lucky day
- $reason = wfMsgForContent( 'exbeforeblank', '$1' );
+ $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
} else {
if ( $onlyAuthor ) {
- $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
+ $reason = wfMessage(
+ 'excontentauthor',
+ '$1',
+ $onlyAuthor
+ )->inContentLanguage()->text();
} else {
- $reason = wfMsgForContent( 'excontent', '$1' );
+ $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
}
}
@@ -2672,6 +2865,7 @@ class WikiPage extends Page {
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.
$u = new LinksUpdate( $this->mTitle, $parserOutput, false );
$u->doUpdate();
}
@@ -2779,7 +2973,7 @@ class WikiPage extends Page {
public function quickEdit( $text, $comment = '', $minor = 0 ) {
wfDeprecated( __METHOD__, '1.18' );
global $wgUser;
- return $this->doQuickEdit( $text, $wgUser, $comment, $minor );
+ $this->doQuickEdit( $text, $wgUser, $comment, $minor );
}
/**
@@ -2793,6 +2987,8 @@ class WikiPage extends Page {
/**
* @deprecated since 1.18
+ * @param $oldid int
+ * @return bool
*/
public function useParserCache( $oldid ) {
wfDeprecated( __METHOD__, '1.18' );
@@ -2829,7 +3025,7 @@ class PoolWorkArticleView extends PoolCounterWork {
private $text;
/**
- * @var ParserOutput|false
+ * @var ParserOutput|bool
*/
private $parserOutput = false;
@@ -2839,7 +3035,7 @@ class PoolWorkArticleView extends PoolCounterWork {
private $isDirty = false;
/**
- * @var Status|false
+ * @var Status|bool
*/
private $error = false;
@@ -2883,7 +3079,7 @@ class PoolWorkArticleView extends PoolCounterWork {
/**
* Get a Status object in case of error or false otherwise
*
- * @return Status|false
+ * @return Status|bool
*/
public function getError() {
return $this->error;
@@ -2973,6 +3169,7 @@ class PoolWorkArticleView extends PoolCounterWork {
/**
* @param $status Status
+ * @return bool
*/
function error( $status ) {
$this->error = $status;
diff --git a/includes/Xml.php b/includes/Xml.php
index 7e5b3cdb..120312dd 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -1,9 +1,28 @@
<?php
+/**
+ * Methods to generate XML.
+ *
+ * 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
+ */
/**
* Module of static functions for generating XML
*/
-
class Xml {
/**
* Format an XML element with given attributes and, optionally, text content.
@@ -40,6 +59,7 @@ class Xml {
* The values are passed to Sanitizer::encodeAttribute.
* Return null if no attributes given.
* @param $attribs Array of attributes for an XML element
+ * @return null|string
*/
public static function expandAttributes( $attribs ) {
$out = '';
@@ -146,7 +166,7 @@ class Xml {
if( is_null( $selected ) )
$selected = '';
if( !is_null( $allmonths ) )
- $options[] = self::option( wfMsg( 'monthsall' ), $allmonths, $selected === $allmonths );
+ $options[] = self::option( wfMessage( 'monthsall' )->text(), $allmonths, $selected === $allmonths );
for( $i = 1; $i < 13; $i++ )
$options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i );
return self::openElement( 'select', array( 'id' => $id, 'name' => 'month', 'class' => 'mw-month-selector' ) )
@@ -178,9 +198,9 @@ class Xml {
} else {
$encYear = '';
}
- return Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
+ return Xml::label( wfMessage( 'year' )->text(), 'year' ) . ' '.
Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . ' '.
- Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
+ Xml::label( wfMessage( 'month' )->text(), 'month' ) . ' '.
Xml::monthSelector( $encMonth, -1 );
}
@@ -189,41 +209,29 @@ class Xml {
*
* @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 $language The ISO code of the language to display the select list in (optional)
+ * @param string $inLanguage The ISO code of the language to display the select list in (optional)
+ * @param array $overrideAttrs Override the attributes of the select tag (since 1.20)
+ * @param Message|null $msg Label message key (since 1.20)
* @return array containing 2 items: label HTML and select list HTML
*/
- public static function languageSelector( $selected, $customisedOnly = true, $language = null ) {
+ public static function languageSelector( $selected, $customisedOnly = true, $inLanguage = null, $overrideAttrs = array(), Message $msg = null ) {
global $wgLanguageCode;
- // If a specific language was requested and CLDR is installed, use it
- if ( $language && is_callable( array( 'LanguageNames', 'getNames' ) ) ) {
- if ( $customisedOnly ) {
- $listType = LanguageNames::LIST_MW_SUPPORTED; // Only pull names that have localisation in MediaWiki
- } else {
- $listType = LanguageNames::LIST_MW; // Pull all languages that are in Names.php
- }
- // Retrieve the list of languages in the requested language (via CLDR)
- $languages = LanguageNames::getNames(
- $language, // Code of the requested language
- LanguageNames::FALLBACK_NORMAL, // Use fallback chain
- $listType
- );
- } else {
- $languages = Language::getLanguageNames( $customisedOnly );
- }
-
- // Make sure the site language is in the list; a custom language code might not have a
- // defined name...
+ $include = $customisedOnly ? 'mwfile' : 'mw';
+ $languages = Language::fetchLanguageNames( $inLanguage, $include );
+
+ // 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 ) ) {
$languages[$wgLanguageCode] = $wgLanguageCode;
}
-
+
ksort( $languages );
/**
* If a bogus value is set, default to the content language.
* Otherwise, no default is selected and the user ends up
- * with an Afrikaans interface since it's first in the list.
+ * with Afrikaans since it's first in the list.
*/
$selected = isset( $languages[$selected] ) ? $selected : $wgLanguageCode;
$options = "\n";
@@ -231,12 +239,15 @@ class Xml {
$options .= Xml::option( "$code - $name", $code, ($code == $selected) ) . "\n";
}
+ $attrs = array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' );
+ $attrs = array_merge( $attrs, $overrideAttrs );
+
+ if( $msg === null ) {
+ $msg = wfMessage( 'yourlanguage' );
+ }
return array(
- Xml::label( wfMsg('yourlanguage'), 'wpUserLanguage' ),
- Xml::tags( 'select',
- array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ),
- $options
- )
+ Xml::label( $msg->text(), $attrs['id'] ),
+ Xml::tags( 'select', $attrs, $options )
);
}
@@ -254,8 +265,8 @@ class Xml {
/**
* Shortcut to make a specific element with a class attribute
- * @param $text content of the element, will be escaped
- * @param $class class name of the span element
+ * @param $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
* @return string
@@ -529,8 +540,8 @@ class Xml {
/**
* Shortcut for creating fieldsets.
*
- * @param $legend Legend of the fieldset. If evaluates to false, legend is not added.
- * @param $content Pre-escaped content for the fieldset. If false, only open fieldset is returned.
+ * @param $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.
*
* @return string
@@ -761,7 +772,7 @@ class Xml {
foreach( $fields as $labelmsg => $input ) {
$id = "mw-$labelmsg";
$form .= Xml::openElement( 'tr', array( 'id' => $id ) );
- $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMsgExt( $labelmsg, array('parseinline') ) );
+ $form .= Xml::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' );
}
@@ -769,7 +780,7 @@ class Xml {
if( $submitLabel ) {
$form .= Xml::openElement( 'tr' );
$form .= Xml::tags( 'td', array(), '' );
- $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMsg( $submitLabel ) ) . Xml::closeElement( 'td' );
+ $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMessage( $submitLabel )->text() ) . Xml::closeElement( 'td' );
$form .= Xml::closeElement( 'tr' );
}
diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php
index be286f8e..b95dd6a5 100644
--- a/includes/XmlTypeCheck.php
+++ b/includes/XmlTypeCheck.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * XML syntax and type checker.
+ *
+ * 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 XmlTypeCheck {
/**
diff --git a/includes/ZhClient.php b/includes/ZhClient.php
index d3d79165..4299841b 100644
--- a/includes/ZhClient.php
+++ b/includes/ZhClient.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Client for querying zhdaemon.
+ *
+ * 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
+ */
/**
* Client for querying zhdaemon
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
index 58bc98c9..247b1939 100644
--- a/includes/ZhConversion.php
+++ b/includes/ZhConversion.php
@@ -2570,7 +2570,6 @@ $zh2Hant = array(
'龚' => '龔',
'龛' => '龕',
'龟' => '龜',
-'' => '棡',
'𠮶' => '嗰',
'𡒄' => '壈',
'𦈖' => '䌈',
@@ -3273,8 +3272,8 @@ $zh2Hant = array(
'于伟国' => '于偉國',
'于偉國' => '于偉國',
'于光新' => '于光新',
-'于光远' => '于光遠',
'于光遠' => '于光遠',
+'于光远' => '于光遠',
'于克-蘭多縣' => '于克-蘭多縣',
'于克-兰多县' => '于克-蘭多縣',
'于克勒' => '于克勒',
@@ -3444,8 +3443,8 @@ $zh2Hant = array(
'于风政' => '于風政',
'于風政' => '于風政',
'于飞' => '于飛',
-'于飛島' => '于飛島',
'于飞岛' => '于飛島',
+'于飛島' => '于飛島',
'于余曲折' => '于餘曲折',
'于鬯' => '于鬯',
'于魁智' => '于魁智',
@@ -6283,8 +6282,8 @@ $zh2Hant = array(
'有只用' => '有只用',
'有够赞' => '有夠讚',
'有征伐' => '有征伐',
-'有征戰' => '有征戰',
'有征战' => '有征戰',
+'有征戰' => '有征戰',
'有征服' => '有征服',
'有征讨' => '有征討',
'有征討' => '有征討',
@@ -6610,6 +6609,8 @@ $zh2Hant = array(
'浮松' => '浮鬆',
'海上布雷' => '海上佈雷',
'海干' => '海乾',
+'海淀山后' => '海淀山後',
+'海淀山後' => '海淀山後',
'海湾布雷' => '海灣佈雷',
'涂善妮' => '涂善妮',
'涂坤' => '涂坤',
@@ -7060,6 +7061,7 @@ $zh2Hant = array(
'皇庄' => '皇莊',
'皓发' => '皓髮',
'皮制服' => '皮制服',
+'皮肤' => '皮膚',
'皮里春秋' => '皮裡春秋',
'皮里阳秋' => '皮裡陽秋',
'皮制' => '皮製',
@@ -8408,6 +8410,7 @@ $zh2Hant = array(
'跌扑' => '跌扑',
'跌荡' => '跌蕩',
'路签' => '路籤',
+'路面' => '路面',
'跳梁小丑' => '跳樑小丑',
'跳荡' => '跳蕩',
'跳表' => '跳錶',
@@ -10387,7 +10390,6 @@ $zh2Hans = array(
'棖' => '枨',
'棗' => '枣',
'棟' => '栋',
-'棡' => '',
'棧' => '栈',
'棲' => '栖',
'棶' => '梾',
@@ -15595,8 +15597,8 @@ $zh2TW = array(
'卡塔尔' => '卡達',
'打印機' => '印表機',
'打印机' => '印表機',
-'厄立特里亞' => '厄利垂亞',
'厄立特里亚' => '厄利垂亞',
+'厄立特里亞' => '厄利垂亞',
'厄瓜多尔' => '厄瓜多',
'厄瓜多爾' => '厄瓜多',
'斯威士兰' => '史瓦濟蘭',
@@ -15800,6 +15802,7 @@ $zh2TW = array(
'彩线' => '綵線',
'彩船' => '綵船',
'彩衣' => '綵衣',
+'綫' => '線',
'缉凶' => '緝凶',
'緝兇' => '緝凶',
'緝凶' => '緝凶',
@@ -15935,6 +15938,30 @@ $zh2TW = array(
);
$zh2HK = array(
+'505線' => '505綫',
+'505线' => '505綫',
+'507線' => '507綫',
+'507线' => '507綫',
+'610線' => '610綫',
+'610线' => '610綫',
+'614P線' => '614P綫',
+'614P线' => '614P綫',
+'614线' => '614綫',
+'614線' => '614綫',
+'615P線' => '615P綫',
+'615P线' => '615P綫',
+'615线' => '615綫',
+'615線' => '615綫',
+'705线' => '705綫',
+'705線' => '705綫',
+'706线' => '706綫',
+'706線' => '706綫',
+'751P線' => '751P綫',
+'751P线' => '751P綫',
+'751線' => '751綫',
+'751线' => '751綫',
+'761P线' => '761P綫',
+'761P線' => '761P綫',
'“' => '「',
'”' => '」',
'‘' => '『',
@@ -16171,6 +16198,8 @@ $zh2HK = array(
'動著者' => '動著者',
'動著述' => '動著述',
'動著錄' => '動著錄',
+'北环线' => '北環綫',
+'北環線' => '北環綫',
'医院里' => '医院裏',
'波札那' => '博茨瓦納',
'珍妮弗·卡普里亚蒂' => '卡佩雅蒂',
@@ -16418,6 +16447,8 @@ $zh2HK = array(
'寫著者' => '寫著者',
'寫著述' => '寫著述',
'寫著錄' => '寫著錄',
+'将军澳线' => '將軍澳綫',
+'將軍澳線' => '將軍澳綫',
'专辑里' => '專輯裏',
'專輯裡' => '專輯裏',
'尋著' => '尋着',
@@ -16948,6 +16979,10 @@ $zh2HK = array(
'本著錄' => '本著錄',
'村子里' => '村子裏',
'村子裡' => '村子裏',
+'东涌线' => '東涌綫',
+'東涌線' => '東涌綫',
+'東鐵線' => '東鐵綫',
+'东铁线' => '東鐵綫',
'枕著' => '枕着',
'枕著作' => '枕著作',
'枕著名' => '枕著名',
@@ -16981,6 +17016,8 @@ $zh2HK = array(
'樂著錄' => '樂著錄',
'寶獅' => '標致',
'標誌著' => '標誌着',
+'機場快線' => '機場快綫',
+'机场快线' => '機場快綫',
'機器人' => '機械人',
'机器人' => '機械人',
'历史里' => '歷史裏',
@@ -17013,8 +17050,12 @@ $zh2HK = array(
'沉著者' => '沉著者',
'沉著述' => '沉著述',
'沉著錄' => '沉著錄',
+'沙中线' => '沙中綫',
+'沙中線' => '沙中綫',
'沙地阿拉伯' => '沙特阿拉伯',
'沙烏地阿拉伯' => '沙特阿拉伯',
+'沙田至中環線' => '沙田至中環綫',
+'沙田至中环线' => '沙田至中環綫',
'马拉特·萨芬' => '沙芬',
'沿著' => '沿着',
'沿著作' => '沿著作',
@@ -17072,6 +17113,8 @@ $zh2HK = array(
'涼著錄' => '涼著錄',
'深淵裡' => '深淵裏',
'深渊里' => '深渊裏',
+'港岛线' => '港島綫',
+'港島線' => '港島綫',
'渴著' => '渴着',
'渴著作' => '渴著作',
'渴著名' => '渴著名',
@@ -17112,6 +17155,14 @@ $zh2HK = array(
'潤著者' => '潤著者',
'潤著述' => '潤著述',
'潤著錄' => '潤著錄',
+'無線劇集' => '無綫劇集',
+'无线剧集' => '無綫劇集',
+'無線收費' => '無綫收費',
+'无线收费' => '無綫收費',
+'无线节目' => '無綫節目',
+'無線節目' => '無綫節目',
+'无线电视' => '無綫電視',
+'無線電視' => '無綫電視',
'菸' => '煙',
'照著' => '照着',
'照著作' => '照著作',
@@ -17545,6 +17596,8 @@ $zh2HK = array(
'苦著錄' => '苦著錄',
'苦里' => '苦裏',
'苦裡' => '苦裏',
+'荃湾线' => '荃灣綫',
+'荃灣線' => '荃灣綫',
'莫三比克' => '莫桑比克',
'賴索托' => '萊索托',
'馬自達' => '萬事得',
@@ -17628,6 +17681,8 @@ $zh2HK = array(
'裹著者' => '裹著者',
'裹著述' => '裹著述',
'裹著錄' => '裹著錄',
+'西铁线' => '西鐵綫',
+'西鐵線' => '西鐵綫',
'見著' => '見着',
'見著作' => '見著作',
'見著名' => '見著名',
@@ -17636,6 +17691,8 @@ $zh2HK = array(
'見著者' => '見著者',
'見著述' => '見著述',
'見著錄' => '見著錄',
+'觀塘線' => '觀塘綫',
+'观塘线' => '觀塘綫',
'記著' => '記着',
'記著作' => '記著作',
'記著名' => '記著名',
@@ -17819,6 +17876,8 @@ $zh2HK = array(
'辦著錄' => '辦著錄',
'近角聪信' => '近角聰信',
'近角聰信' => '近角聰信',
+'迪士尼线' => '迪士尼綫',
+'迪士尼線' => '迪士尼綫',
'迫著' => '迫着',
'追著' => '追着',
'追著作' => '追著作',
@@ -18078,6 +18137,8 @@ $zh2HK = array(
'馬爾地夫' => '馬爾代夫',
'馬利共和國' => '馬里共和國',
'土豆' => '馬鈴薯',
+'馬鞍山線' => '馬鞍山綫',
+'马鞍山线' => '馬鞍山綫',
'駕著' => '駕着',
'駕著作' => '駕著作',
'駕著名' => '駕著名',
@@ -18192,7 +18253,6 @@ $zh2CN = array(
'攜帶型' => '便携式',
'資訊理論' => '信息论',
'母音' => '元音',
-'游標' => '光标',
'光碟' => '光盘',
'光碟機' => '光驱',
'柯林頓' => '克林顿',
@@ -18450,8 +18510,8 @@ $zh2SG = array(
'方便面' => '快速面',
'零钱' => '散钱',
'散紙' => '散钱',
-'榴蓮' => '榴梿',
'榴莲' => '榴梿',
+'榴蓮' => '榴梿',
'笨豬跳' => '绑紧跳',
'蹦极跳' => '绑紧跳',
'笑星' => '谐星',
diff --git a/includes/ZipDirectoryReader.php b/includes/ZipDirectoryReader.php
index 37934aea..0e84583f 100644
--- a/includes/ZipDirectoryReader.php
+++ b/includes/ZipDirectoryReader.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * ZIP file directories reader, for the purposes of upload verification.
+ *
+ * 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
+ */
/**
* A class for reading ZIP file directories, for the purposes of upload
@@ -297,7 +317,7 @@ class ZipDirectoryReader {
* Find the location of the central directory, as would be seen by a
* ZIP64-compliant reader.
*
- * @return List containing offset, size and end position.
+ * @return array List containing offset, size and end position.
*/
function findZip64CentralDirectory() {
// The spec is ambiguous about the exact rules of precedence between the
@@ -426,6 +446,7 @@ class ZipDirectoryReader {
/**
* Interpret ZIP64 "extra field" data and return an associative array.
+ * @return array|bool
*/
function unpackZip64Extra( $extraField ) {
$extraHeaderInfo = array(
@@ -473,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 The byte offset of the start of the block.
- * @param $length The number of bytes to return. If omitted, the remainder
+ * @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
* of the file will be returned.
*
* @return string
@@ -520,6 +541,7 @@ class ZipDirectoryReader {
* If there are not enough bytes in the file to satsify 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
*/
function getSegment( $segIndex ) {
if ( !isset( $this->buffer[$segIndex] ) ) {
@@ -542,6 +564,7 @@ class ZipDirectoryReader {
/**
* Get the size of a structure in bytes. See unpack() for the format of $struct.
+ * @return int
*/
function getStructSize( $struct ) {
$size = 0;
@@ -560,9 +583,9 @@ class ZipDirectoryReader {
* Unpack a binary structure. This is like the built-in unpack() function
* except nicer.
*
- * @param $string The binary data input
+ * @param $string string The binary data input
*
- * @param $struct An associative array giving structure members and their
+ * @param $struct array 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
@@ -571,9 +594,9 @@ class ZipDirectoryReader {
* - "string": The second array element gives the length of string.
* Not null terminated.
*
- * @param $offset The offset into the string at which to start unpacking.
+ * @param $offset int The offset into the string at which to start unpacking.
*
- * @return Unpacked associative array. Note that large integers in the input
+ * @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.
*/
@@ -628,7 +651,8 @@ class ZipDirectoryReader {
* boolean.
*
* @param $value integer
- * @param $bitIndex The index of the bit, where 0 is the LSB.
+ * @param $bitIndex int The index of the bit, where 0 is the LSB.
+ * @return bool
*/
function testBit( $value, $bitIndex ) {
return (bool)( ( $value >> $bitIndex ) & 1 );
@@ -672,10 +696,10 @@ class ZipDirectoryReader {
* Internal exception class. Will be caught by private code.
*/
class ZipDirectoryReaderError extends Exception {
- var $code;
+ var $errorCode;
function __construct( $code ) {
- $this->code = $code;
+ $this->errorCode = $code;
parent::__construct( "ZipDirectoryReader error: $code" );
}
@@ -683,6 +707,6 @@ class ZipDirectoryReaderError extends Exception {
* @return mixed
*/
function getErrorCode() {
- return $this->code;
+ return $this->errorCode;
}
}
diff --git a/includes/actions/CachedAction.php b/includes/actions/CachedAction.php
new file mode 100644
index 00000000..d21f9aeb
--- /dev/null
+++ b/includes/actions/CachedAction.php
@@ -0,0 +1,182 @@
+<?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
+ * (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 Action
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @since 1.20
+ */
+abstract class CachedAction extends FormlessAction implements ICacheHelper {
+
+ /**
+ * CacheHelper object to which we forward the non-SpecialPage specific caching work.
+ * Initialized in startCache.
+ *
+ * @since 1.20
+ * @var CacheHelper
+ */
+ protected $cacheHelper;
+
+ /**
+ * If the cache is enabled or not.
+ *
+ * @since 1.20
+ * @var boolean
+ */
+ protected $cacheEnabled = true;
+
+ /**
+ * Sets if the cache should be enabled or not.
+ *
+ * @since 1.20
+ * @param boolean $cacheEnabled
+ */
+ public function setCacheEnabled( $cacheEnabled ) {
+ $this->cacheHelper->setCacheEnabled( $cacheEnabled );
+ }
+
+ /**
+ * Initializes the caching.
+ * Should be called before the first time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ *
+ * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp.
+ * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not.
+ */
+ public function startCache( $cacheExpiry = null, $cacheEnabled = null ) {
+ $this->cacheHelper = new CacheHelper();
+
+ $this->cacheHelper->setCacheEnabled( $this->cacheEnabled );
+ $this->cacheHelper->setOnInitializedHandler( array( $this, 'onCacheInitialized' ) );
+
+ $keyArgs = $this->getCacheKey();
+
+ if ( array_key_exists( 'action', $keyArgs ) && $keyArgs['action'] === 'purge' ) {
+ unset( $keyArgs['action'] );
+ }
+
+ $this->cacheHelper->setCacheKey( $keyArgs );
+
+ if ( $this->getRequest()->getText( 'action' ) === 'purge' ) {
+ $this->cacheHelper->rebuildOnDemand();
+ }
+
+ $this->cacheHelper->startCache( $cacheExpiry, $cacheEnabled );
+ }
+
+ /**
+ * Get a cached value if available or compute it if not and then cache it if possible.
+ * The provided $computeFunction is only called when the computation needs to happen
+ * and should return a result value. $args are arguments that will be passed to the
+ * compute function when called.
+ *
+ * @since 1.20
+ *
+ * @param {function} $computeFunction
+ * @param array|mixed $args
+ * @param string|null $key
+ *
+ * @return mixed
+ */
+ public function getCachedValue( $computeFunction, $args = array(), $key = null ) {
+ return $this->cacheHelper->getCachedValue( $computeFunction, $args, $key );
+ }
+
+ /**
+ * Add some HTML to be cached.
+ * This is done by providing a callback function that should
+ * return the HTML to be added. It will only be called if the
+ * item is not in the cache yet or when the cache has been invalidated.
+ *
+ * @since 1.20
+ *
+ * @param {function} $computeFunction
+ * @param array $args
+ * @param string|null $key
+ */
+ public function addCachedHTML( $computeFunction, $args = array(), $key = null ) {
+ $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ) );
+ }
+
+ /**
+ * Saves the HTML to the cache in case it got recomputed.
+ * Should be called after the last time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ */
+ public function saveCache() {
+ $this->cacheHelper->saveCache();
+ }
+
+ /**
+ * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry.
+ *
+ * @since 1.20
+ *
+ * @param integer $cacheExpiry
+ */
+ public function setExpiry( $cacheExpiry ) {
+ $this->cacheHelper->setExpiry( $cacheExpiry );
+ }
+
+ /**
+ * Returns the variables used to constructed the cache key in an array.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ protected function getCacheKey() {
+ return array(
+ get_class( $this->page ),
+ $this->getName(),
+ $this->getLanguage()->getCode()
+ );
+ }
+
+ /**
+ * Gets called after the cache got initialized.
+ *
+ * @since 1.20
+ *
+ * @param boolean $hasCached
+ */
+ public function onCacheInitialized( $hasCached ) {
+ if ( $hasCached ) {
+ $this->getOutput()->setSubtitle( $this->cacheHelper->getCachedNotice( $this->getContext() ) );
+ }
+ }
+
+}
diff --git a/includes/actions/CreditsAction.php b/includes/actions/CreditsAction.php
index cd083c30..f7152297 100644
--- a/includes/actions/CreditsAction.php
+++ b/includes/actions/CreditsAction.php
@@ -30,7 +30,7 @@ class CreditsAction extends FormlessAction {
}
protected function getDescription() {
- return wfMsgHtml( 'creditspage' );
+ return $this->msg( 'creditspage' )->escaped();
}
/**
diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php
index 457f67ff..dcd6fe55 100644
--- a/includes/actions/HistoryAction.php
+++ b/includes/actions/HistoryAction.php
@@ -3,6 +3,22 @@
* Page history
*
* Split off from Article.php and Skin.php, 2003-12-22
+ *
+ * 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
*/
@@ -69,10 +85,9 @@ class HistoryAction extends FormlessAction {
/**
* Print the history page for an article.
- * @return nothing
*/
function onView() {
- global $wgScript, $wgUseFileCache, $wgSquidMaxage;
+ global $wgScript, $wgUseFileCache;
$out = $this->getOutput();
$request = $this->getRequest();
@@ -86,10 +101,6 @@ class HistoryAction extends FormlessAction {
wfProfileIn( __METHOD__ );
- if ( $request->getFullRequestURL() == $this->getTitle()->getInternalURL( 'action=history' ) ) {
- $out->setSquidMaxage( $wgSquidMaxage );
- }
-
$this->preCacheMessages();
# Fill in the file cache if not set already
@@ -107,8 +118,9 @@ class HistoryAction extends FormlessAction {
// Handle atom/RSS feeds.
$feedType = $request->getVal( 'feed' );
if ( $feedType ) {
+ $this->feed( $feedType );
wfProfileOut( __METHOD__ );
- return $this->feed( $feedType );
+ return;
}
// Fail nicely if article doesn't exist.
@@ -192,6 +204,11 @@ class HistoryAction extends FormlessAction {
* @return ResultWrapper
*/
function fetchRevisions( $limit, $offset, $direction ) {
+ // Fail if article doesn't exist.
+ if( !$this->getTitle()->exists() ) {
+ return new FakeResultWrapper( array() );
+ }
+
$dbr = wfGetDB( DB_SLAVE );
if ( $direction == HistoryPage::DIR_PREV ) {
@@ -231,8 +248,8 @@ class HistoryAction extends FormlessAction {
$feed = new $wgFeedClasses[$type](
$this->getTitle()->getPrefixedText() . ' - ' .
- wfMsgForContent( 'history-feed-title' ),
- wfMsgForContent( 'history-feed-description' ),
+ $this->msg( 'history-feed-title' )->inContentLanguage()->text(),
+ $this->msg( 'history-feed-description' )->inContentLanguage()->text(),
$this->getTitle()->getFullUrl( 'action=history' )
);
@@ -258,8 +275,8 @@ class HistoryAction extends FormlessAction {
function feedEmpty() {
return new FeedItem(
- wfMsgForContent( 'nohistory' ),
- $this->getOutput()->parse( wfMsgForContent( 'history-feed-empty' ) ),
+ $this->msg( 'nohistory' )->inContentLanguage()->text(),
+ $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
$this->getTitle()->getFullUrl(),
wfTimestamp( TS_MW ),
'',
@@ -287,15 +304,14 @@ class HistoryAction extends FormlessAction {
);
if ( $rev->getComment() == '' ) {
global $wgContLang;
- $title = wfMsgForContent( 'history-feed-item-nocomment',
+ $title = $this->msg( 'history-feed-item-nocomment',
$rev->getUserText(),
$wgContLang->timeanddate( $rev->getTimestamp() ),
$wgContLang->date( $rev->getTimestamp() ),
- $wgContLang->time( $rev->getTimestamp() )
- );
+ $wgContLang->time( $rev->getTimestamp() ) )->inContentLanguage()->text();
} else {
$title = $rev->getUserText() .
- wfMsgForContent( 'colon-separator' ) .
+ $this->msg( 'colon-separator' )->inContentLanguage()->text() .
FeedItem::stripComment( $rev->getComment() );
}
return new FeedItem(
@@ -316,6 +332,10 @@ class HistoryPager extends ReverseChronologicalPager {
public $lastRow = false, $counter, $historyPage, $buttons, $conds;
protected $oldIdChecked;
protected $preventClickjacking = false;
+ /**
+ * @var array
+ */
+ protected $parentLens;
function __construct( $historyPage, $year = '', $month = '', $tagFilter = '', $conds = array() ) {
parent::__construct( $historyPage->getContext() );
@@ -384,7 +404,11 @@ class HistoryPager extends ReverseChronologicalPager {
# Do a link batch query
$this->mResult->seek( 0 );
$batch = new LinkBatch();
+ $revIds = array();
foreach ( $this->mResult as $row ) {
+ if( $row->rev_parent_id ) {
+ $revIds[] = $row->rev_parent_id;
+ }
if( !is_null( $row->user_name ) ) {
$batch->add( NS_USER, $row->user_name );
$batch->add( NS_USER_TALK, $row->user_name );
@@ -393,6 +417,7 @@ class HistoryPager extends ReverseChronologicalPager {
$batch->add( NS_USER_TALK, $row->rev_user_text );
}
}
+ $this->parentLens = Revision::getParentLengths( $this->mDb, $revIds );
$batch->execute();
$this->mResult->seek( 0 );
}
@@ -523,7 +548,7 @@ class HistoryPager extends ReverseChronologicalPager {
$histLinks = Html::rawElement(
'span',
array( 'class' => 'mw-history-histlinks' ),
- '(' . $curlink . $this->historyPage->message['pipe-separator'] . $lastlink . ') '
+ $this->msg( 'parentheses' )->rawParams( $curlink . $this->historyPage->message['pipe-separator'] . $lastlink )->escaped()
);
$s = $histLinks . $diffButtons;
@@ -574,26 +599,29 @@ class HistoryPager extends ReverseChronologicalPager {
}
# Size is always public data
- $prevSize = $prevRev ? $prevRev->getSize() : 0;
+ $prevSize = isset( $this->parentLens[$row->rev_parent_id] )
+ ? $this->parentLens[$row->rev_parent_id]
+ : 0;
$sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() );
- $s .= ' . . ' . $sDiff . ' . . ';
+ $fSize = Linker::formatRevisionSize($rev->getSize());
+ $s .= ' <span class="mw-changeslist-separator">. .</span> ' . "$fSize $sDiff";
- $s .= Linker::revComment( $rev, false, true );
+ # Text following the character difference is added just before running hooks
+ $s2 = Linker::revComment( $rev, false, true );
if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
- $s .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
+ $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
+ $classes[] = 'mw-history-line-updated';
}
$tools = array();
# Rollback and undo links
- if ( $prevRev &&
- !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) ) )
- {
- if ( $latest && !count( $this->getTitle()->getUserPermissionsErrors( 'rollback', $this->getUser() ) ) ) {
+ 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 ) . '</span>';
+ Linker::buildRollbackLink( $rev, $this->getContext() ) . '</span>';
}
if ( !$rev->isDeleted( Revision::DELETED_TEXT )
@@ -618,13 +646,20 @@ class HistoryPager extends ReverseChronologicalPager {
}
if ( $tools ) {
- $s .= ' (' . $lang->pipeList( $tools ) . ')';
+ $s2 .= ' '. $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped();
}
# Tags
list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'history' );
$classes = array_merge( $classes, $newClasses );
- $s .= " $tagSummary";
+ if ( $tagSummary !== '' ) {
+ $s2 .= " $tagSummary";
+ }
+
+ # Include separator between character difference and following text
+ if ( $s2 !== '' ) {
+ $s .= ' <span class="mw-changeslist-separator">. .</span> ' . $s2;
+ }
wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s, &$classes ) );
@@ -649,7 +684,7 @@ class HistoryPager extends ReverseChronologicalPager {
$link = Linker::linkKnown(
$this->getTitle(),
$date,
- array(),
+ array( 'class' => 'mw-changeslist-date' ),
array( 'oldid' => $rev->getId() )
);
} else {
@@ -784,6 +819,7 @@ class HistoryPager extends ReverseChronologicalPager {
/**
* Get the "prevent clickjacking" flag
+ * @return bool
*/
function getPreventClickjacking() {
return $this->preventClickjacking;
diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php
index 70edabce..ae550391 100644
--- a/includes/actions/InfoAction.php
+++ b/includes/actions/InfoAction.php
@@ -1,7 +1,6 @@
<?php
/**
- * Display informations about a page.
- * Very inefficient for the moment.
+ * Displays information about a page.
*
* Copyright © 2011 Alexandre Emsenhuber
*
@@ -24,124 +23,592 @@
*/
class InfoAction extends FormlessAction {
-
+ /**
+ * Returns the name of the action this object responds to.
+ *
+ * @return string lowercase
+ */
public function getName() {
return 'info';
}
- protected function getDescription() {
- return '';
+ /**
+ * Whether this action can still be executed by a blocked user.
+ *
+ * @return bool
+ */
+ public function requiresUnblock() {
+ return false;
}
+ /**
+ * Whether this action requires the wiki not to be locked.
+ *
+ * @return bool
+ */
public function requiresWrite() {
return false;
}
- public function requiresUnblock() {
- return false;
+ /**
+ * Shows page information on GET request.
+ *
+ * @return string Page information that will be added to the output
+ */
+ public function onView() {
+ $content = '';
+
+ // Validate revision
+ $oldid = $this->page->getOldID();
+ if ( $oldid ) {
+ $revision = $this->page->getRevisionFetched();
+
+ // Revision is missing
+ if ( $revision === null ) {
+ return $this->msg( 'missing-revision', $oldid )->parse();
+ }
+
+ // Revision is not current
+ if ( !$revision->isCurrent() ) {
+ return $this->msg( 'pageinfo-not-current' )->plain();
+ }
+ }
+
+ // Page header
+ if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) {
+ $content .= $this->msg( 'pageinfo-header' )->parse();
+ }
+
+ // Hide "This page is a member of # hidden categories" explanation
+ $content .= Html::element( 'style', array(),
+ '.mw-hiddenCategoriesExplanation { display: none; }' );
+
+ // Hide "Templates used on this page" explanation
+ $content .= Html::element( 'style', array(),
+ '.mw-templatesUsedExplanation { display: none; }' );
+
+ // Get page information
+ $pageInfo = $this->pageInfo();
+
+ // Allow extensions to add additional information
+ wfRunHooks( 'InfoAction', array( $this->getContext(), &$pageInfo ) );
+
+ // Render page information
+ foreach ( $pageInfo as $header => $infoTable ) {
+ $content .= $this->makeHeader( $this->msg( "pageinfo-${header}" )->escaped() );
+ $table = '';
+ 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 );
+ }
+ $content = $this->addTable( $content, $table );
+ }
+
+ // Page footer
+ if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
+ $content .= $this->msg( 'pageinfo-footer' )->parse();
+ }
+
+ // Page credits
+ /*if ( $this->page->exists() ) {
+ $content .= Html::rawElement( 'div', array( 'id' => 'mw-credits' ), $this->getContributors() );
+ }*/
+
+ return $content;
}
- protected function getPageTitle() {
- return $this->msg( 'pageinfo-title', $this->getTitle()->getSubjectPage()->getPrefixedText() )->text();
+ /**
+ * Creates a header that can be added to the output.
+ *
+ * @param $header The header text.
+ * @return string The HTML.
+ */
+ protected function makeHeader( $header ) {
+ global $wgParser;
+ $spanAttribs = array( 'class' => 'mw-headline', 'id' => $wgParser->guessSectionNameFromWikiText( $header ) );
+ return Html::rawElement( 'h2', array(), Html::element( 'span', $spanAttribs, $header ) );
}
- public function onView() {
- global $wgDisableCounters;
-
- $title = $this->getTitle()->getSubjectPage();
-
- $pageInfo = self::pageCountInfo( $title );
- $talkInfo = self::pageCountInfo( $title->getTalkPage() );
-
- return Html::rawElement( 'table', array( 'class' => 'wikitable mw-page-info' ),
- Html::rawElement( 'tr', array(),
- Html::element( 'th', array(), '' ) .
- Html::element( 'th', array(), $this->msg( 'pageinfo-subjectpage' )->text() ) .
- Html::element( 'th', array(), $this->msg( 'pageinfo-talkpage' )->text() )
- ) .
- Html::rawElement( 'tr', array(),
- Html::element( 'th', array( 'colspan' => 3 ), $this->msg( 'pageinfo-header-edits' )->text() )
- ) .
- Html::rawElement( 'tr', array(),
- Html::element( 'td', array(), $this->msg( 'pageinfo-edits' )->text() ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( $pageInfo['edits'] ) ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( $talkInfo['edits'] ) )
- ) .
- Html::rawElement( 'tr', array(),
- Html::element( 'td', array(), $this->msg( 'pageinfo-authors' )->text() ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( $pageInfo['authors'] ) ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( $talkInfo['authors'] ) )
- ) .
- ( !$this->getUser()->isAllowed( 'unwatchedpages' ) ? '' :
- Html::rawElement( 'tr', array(),
- Html::element( 'th', array( 'colspan' => 3 ), $this->msg( 'pageinfo-header-watchlist' )->text() )
- ) .
- Html::rawElement( 'tr', array(),
- Html::element( 'td', array(), $this->msg( 'pageinfo-watchers' )->text() ) .
- Html::element( 'td', array( 'colspan' => 2 ), $this->getLanguage()->formatNum( $pageInfo['watchers'] ) )
- )
- ).
- ( $wgDisableCounters ? '' :
- Html::rawElement( 'tr', array(),
- Html::element( 'th', array( 'colspan' => 3 ), $this->msg( 'pageinfo-header-views' )->text() )
- ) .
- Html::rawElement( 'tr', array(),
- Html::element( 'td', array(), $this->msg( 'pageinfo-views' )->text() ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( $pageInfo['views'] ) ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( $talkInfo['views'] ) )
- ) .
- Html::rawElement( 'tr', array(),
- Html::element( 'td', array(), $this->msg( 'pageinfo-viewsperedit' )->text() ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( sprintf( '%.2f', $pageInfo['edits'] ? $pageInfo['views'] / $pageInfo['edits'] : 0 ) ) ) .
- Html::element( 'td', array(), $this->getLanguage()->formatNum( sprintf( '%.2f', $talkInfo['edits'] ? $talkInfo['views'] / $talkInfo['edits'] : 0 ) ) )
- )
- )
+ /**
+ * 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
+ * @return string The table with the row added
+ */
+ protected function addRow( $table, $name, $value ) {
+ return $table . Html::rawElement( 'tr', array(),
+ Html::rawElement( 'td', array( 'style' => 'vertical-align: top;' ), $name ) .
+ Html::rawElement( 'td', array(), $value )
);
}
/**
- * Return the total number of edits and number of unique editors
- * on a given page. If page does not exist, returns false.
+ * Adds a table to the content that will be added to the output.
*
- * @param $title Title object
- * @return mixed array or boolean false
+ * @param $content string The content that will be added to the output
+ * @param $table string The table
+ * @return string The content with the table added
+ */
+ protected function addTable( $content, $table ) {
+ return $content . Html::rawElement( 'table', array( 'class' => 'wikitable mw-page-info' ),
+ $table );
+ }
+
+ /**
+ * 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.
*/
- public static function pageCountInfo( $title ) {
- $id = $title->getArticleId();
+ protected function pageInfo() {
+ global $wgContLang, $wgRCMaxAge;
+
+ $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 );
+
+ // Get page properties
$dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->select(
+ 'page_props',
+ array( 'pp_propname', 'pp_value' ),
+ array( 'pp_page' => $id ),
+ __METHOD__
+ );
- $watchers = (int)$dbr->selectField(
- 'watchlist',
- 'COUNT(*)',
- array(
- 'wl_title' => $title->getDBkey(),
- 'wl_namespace' => $title->getNamespace()
+ $pageProperties = array();
+ foreach ( $result as $row ) {
+ $pageProperties[$row->pp_propname] = $row->pp_value;
+ }
+
+ // Basic information
+ $pageInfo = array();
+ $pageInfo['header-basic'] = array();
+
+ // Display title
+ $displayTitle = $title->getPrefixedText();
+ if ( !empty( $pageProperties['displaytitle'] ) ) {
+ $displayTitle = $pageProperties['displaytitle'];
+ }
+
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-display-title' ), $displayTitle
+ );
+
+ // Default sort key
+ $sortKey = $title->getCategorySortKey();
+ if ( !empty( $pageProperties['defaultsort'] ) ) {
+ $sortKey = $pageProperties['defaultsort'];
+ }
+
+ $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-default-sort' ), $sortKey );
+
+ // Page length (in bytes)
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() )
+ );
+
+ // Page ID (number not localised, as it's a database ID)
+ $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-article-id' ), $id );
+
+ // Search engine status
+ $pOutput = new ParserOutput();
+ if ( isset( $pageProperties['noindex'] ) ) {
+ $pOutput->setIndexPolicy( 'noindex' );
+ }
+
+ // Use robot policy logic
+ $policy = $this->page->getRobotPolicy( 'view', $pOutput );
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-robot-policy' ), $this->msg( "pageinfo-robot-${policy['index']}" )
+ );
+
+ if ( isset( $pageCounts['views'] ) ) {
+ // Number of views
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-views' ), $lang->formatNum( $pageCounts['views'] )
+ );
+ }
+
+ if ( isset( $pageCounts['watchers'] ) ) {
+ // Number of page watchers
+ $pageInfo['header-basic'][] = array(
+ $this->msg( 'pageinfo-watchers' ), $lang->formatNum( $pageCounts['watchers'] )
+ );
+ }
+
+ // Redirects to this page
+ $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
+ $pageInfo['header-basic'][] = array(
+ Linker::link(
+ $whatLinksHere,
+ $this->msg( 'pageinfo-redirects-name' )->escaped(),
+ array(),
+ array( 'hidelinks' => 1, 'hidetrans' => 1 )
),
- __METHOD__
+ $this->msg( 'pageinfo-redirects-value' )
+ ->numParams( count( $title->getRedirectsHere() ) )
);
- $edits = (int)$dbr->selectField(
+ // Subpages of this page, if subpages are enabled for the current NS
+ if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ $prefixIndex = SpecialPage::getTitleFor( 'Prefixindex', $title->getPrefixedText() . '/' );
+ $pageInfo['header-basic'][] = array(
+ Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ),
+ $this->msg( 'pageinfo-subpages-value' )
+ ->numParams(
+ $pageCounts['subpages']['total'],
+ $pageCounts['subpages']['redirects'],
+ $pageCounts['subpages']['nonredirects'] )
+ );
+ }
+
+ // Page protection
+ $pageInfo['header-restrictions'] = array();
+
+ // Page protection
+ foreach ( $title->getRestrictionTypes() as $restrictionType ) {
+ $protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) );
+
+ if ( $protectionLevel == '' ) {
+ // Allow all users
+ $message = $this->msg( 'protect-default' )->escaped();
+ } else {
+ // Administrators only
+ $message = $this->msg( "protect-level-$protectionLevel" );
+ if ( $message->isDisabled() ) {
+ // Require "$1" permission
+ $message = $this->msg( "protect-fallback", $protectionLevel )->parse();
+ } else {
+ $message = $message->escaped();
+ }
+ }
+
+ $pageInfo['header-restrictions'][] = array(
+ $this->msg( "restriction-$restrictionType" ), $message
+ );
+ }
+
+ if ( !$this->page->exists() ) {
+ return $pageInfo;
+ }
+
+ // Edit history
+ $pageInfo['header-edits'] = array();
+
+ $firstRev = $this->page->getOldestRevision();
+
+ // Page creator
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-firstuser' ),
+ Linker::revUserTools( $firstRev )
+ );
+
+ // 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() )
+ )
+ );
+
+ // Latest editor
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-lastuser' ),
+ Linker::revUserTools( $this->page->getRevision() )
+ );
+
+ // 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(
+ $this->msg( 'pageinfo-edits' ), $lang->formatNum( $pageCounts['edits'] )
+ );
+
+ // Total number of distinct authors
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-authors' ), $lang->formatNum( $pageCounts['authors'] )
+ );
+
+ // Recent number of edits (within past 30 days)
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $wgRCMaxAge ) ),
+ $lang->formatNum( $pageCounts['recent_edits'] )
+ );
+
+ // Recent number of distinct authors
+ $pageInfo['header-edits'][] = array(
+ $this->msg( 'pageinfo-recent-authors' ), $lang->formatNum( $pageCounts['recent_authors'] )
+ );
+
+ // Array of MagicWord objects
+ $magicWords = MagicWord::getDoubleUnderscoreArray();
+
+ // Array of magic word IDs
+ $wordIDs = $magicWords->names;
+
+ // Array of IDs => localized magic words
+ $localizedWords = $wgContLang->getMagicWords();
+
+ $listItems = array();
+ foreach ( $pageProperties as $property => $value ) {
+ if ( in_array( $property, $wordIDs ) ) {
+ $listItems[] = Html::element( 'li', array(), $localizedWords[$property][1] );
+ }
+ }
+
+ $localizedList = Html::rawElement( 'ul', array(), implode( '', $listItems ) );
+ $hiddenCategories = $this->page->getHiddenCategories();
+ $transcludedTemplates = $title->getTemplateLinksFrom();
+
+ if ( count( $listItems ) > 0
+ || count( $hiddenCategories ) > 0
+ || count( $transcludedTemplates ) > 0 ) {
+ // Page properties
+ $pageInfo['header-properties'] = array();
+
+ // Magic words
+ if ( count( $listItems ) > 0 ) {
+ $pageInfo['header-properties'][] = array(
+ $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
+ $localizedList
+ );
+ }
+
+ // Hidden categories
+ if ( count( $hiddenCategories ) > 0 ) {
+ $pageInfo['header-properties'][] = array(
+ $this->msg( 'pageinfo-hidden-categories' )
+ ->numParams( count( $hiddenCategories ) ),
+ Linker::formatHiddenCategories( $hiddenCategories )
+ );
+ }
+
+ // Transcluded templates
+ if ( count( $transcludedTemplates ) > 0 ) {
+ $pageInfo['header-properties'][] = array(
+ $this->msg( 'pageinfo-templates' )
+ ->numParams( count( $transcludedTemplates ) ),
+ Linker::formatTemplates( $transcludedTemplates )
+ );
+ }
+ }
+
+ return $pageInfo;
+ }
+
+ /**
+ * Returns page counts that would be too "expensive" to retrieve by normal means.
+ *
+ * @param $title Title object
+ * @param $user User object
+ * @return array
+ */
+ protected static function pageCounts( $title, $user ) {
+ global $wgRCMaxAge, $wgDisableCounters;
+
+ wfProfileIn( __METHOD__ );
+ $id = $title->getArticleID();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = array();
+
+ if ( !$wgDisableCounters ) {
+ // Number of views
+ $views = (int) $dbr->selectField(
+ 'page',
+ 'page_counter',
+ array( 'page_id' => $id ),
+ __METHOD__
+ );
+ $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;
+ }
+
+ // Total number of edits
+ $edits = (int) $dbr->selectField(
'revision',
'COUNT(rev_page)',
array( 'rev_page' => $id ),
__METHOD__
);
+ $result['edits'] = $edits;
- $authors = (int)$dbr->selectField(
+ // Total number of distinct authors
+ $authors = (int) $dbr->selectField(
'revision',
'COUNT(DISTINCT rev_user_text)',
array( 'rev_page' => $id ),
__METHOD__
);
+ $result['authors'] = $authors;
+
+ // "Recent" threshold defined by $wgRCMaxAge
+ $threshold = $dbr->timestamp( time() - $wgRCMaxAge );
+
+ // Recent number of edits
+ $edits = (int) $dbr->selectField(
+ 'revision',
+ 'COUNT(rev_page)',
+ array(
+ 'rev_page' => $id ,
+ "rev_timestamp >= $threshold"
+ ),
+ __METHOD__
+ );
+ $result['recent_edits'] = $edits;
- $views = (int)$dbr->selectField(
- 'page',
- 'page_counter',
- array( 'page_id' => $id ),
+ // Recent number of distinct authors
+ $authors = (int) $dbr->selectField(
+ 'revision',
+ 'COUNT(DISTINCT rev_user_text)',
+ array(
+ 'rev_page' => $id,
+ "rev_timestamp >= $threshold"
+ ),
__METHOD__
);
+ $result['recent_authors'] = $authors;
+
+ // Subpages (if enabled)
+ if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ $conds = array( 'page_namespace' => $title->getNamespace() );
+ $conds[] = 'page_title ' . $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
- return array( 'watchers' => $watchers, 'edits' => $edits,
- 'authors' => $authors, 'views' => $views );
+ // Subpages of this page (redirects)
+ $conds['page_is_redirect'] = 1;
+ $result['subpages']['redirects'] = (int) $dbr->selectField(
+ 'page',
+ 'COUNT(page_id)',
+ $conds,
+ __METHOD__ );
+
+ // Subpages of this page (non-redirects)
+ $conds['page_is_redirect'] = 0;
+ $result['subpages']['nonredirects'] = (int) $dbr->selectField(
+ 'page',
+ 'COUNT(page_id)',
+ $conds,
+ __METHOD__
+ );
+
+ // Subpages of this page (total)
+ $result['subpages']['total'] = $result['subpages']['redirects']
+ + $result['subpages']['nonredirects'];
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ /**
+ * Returns the name that goes in the <h1> page title.
+ *
+ * @return string
+ */
+ protected function getPageTitle() {
+ return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
+ }
+
+ /**
+ * Get a list of contributors of $article
+ * @return string: html
+ */
+ protected function getContributors() {
+ global $wgHiddenPrefs;
+
+ $contributors = $this->page->getContributors();
+ $real_names = array();
+ $user_names = array();
+ $anon_ips = array();
+
+ # Sift for real versus user names
+ foreach ( $contributors as $user ) {
+ $page = $user->isAnon()
+ ? SpecialPage::getTitleFor( 'Contributions', $user->getName() )
+ : $user->getUserPage();
+
+ if ( $user->getID() == 0 ) {
+ $anon_ips[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
+ } elseif ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) {
+ $real_names[] = Linker::link( $page, htmlspecialchars( $user->getRealName() ) );
+ } else {
+ $user_names[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
+ }
+ }
+
+ $lang = $this->getLanguage();
+
+ $real = $lang->listToText( $real_names );
+
+ # "ThisSite user(s) A, B and C"
+ if ( count( $user_names ) ) {
+ $user = $this->msg( 'siteusers' )->rawParams( $lang->listToText( $user_names ) )->params(
+ count( $user_names ) )->escaped();
+ } else {
+ $user = false;
+ }
+
+ if ( count( $anon_ips ) ) {
+ $anon = $this->msg( 'anonusers' )->rawParams( $lang->listToText( $anon_ips ) )->params(
+ count( $anon_ips ) )->escaped();
+ } else {
+ $anon = false;
+ }
+
+ # This is the big list, all mooshed together. We sift for blank strings
+ $fulllist = array();
+ foreach ( array( $real, $user, $anon ) as $s ) {
+ if ( $s !== '' ) {
+ array_push( $fulllist, $s );
+ }
+ }
+
+ $count = count( $fulllist );
+ # "Based on work by ..."
+ return $count
+ ? $this->msg( 'othercontribs' )->rawParams(
+ $lang->listToText( $fulllist ) )->params( $count )->escaped()
+ : '';
+ }
+
+ /**
+ * Returns the description that goes below the <h1> tag.
+ *
+ * @return string
+ */
+ protected function getDescription() {
+ return '';
}
}
diff --git a/includes/actions/PurgeAction.php b/includes/actions/PurgeAction.php
index 21a6d904..cd58889d 100644
--- a/includes/actions/PurgeAction.php
+++ b/includes/actions/PurgeAction.php
@@ -79,15 +79,15 @@ class PurgeAction extends FormAction {
}
protected function alterForm( HTMLForm $form ) {
- $form->setSubmitText( wfMsg( 'confirm_purge_button' ) );
+ $form->setSubmitTextMsg( 'confirm_purge_button' );
}
protected function preText() {
- return wfMessage( 'confirm-purge-top' )->parse();
+ return $this->msg( 'confirm-purge-top' )->parse();
}
protected function postText() {
- return wfMessage( 'confirm-purge-bottom' )->parse();
+ return $this->msg( 'confirm-purge-bottom' )->parse();
}
public function onSuccess() {
diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php
index e4c6b3e0..174ca3f8 100644
--- a/includes/actions/RawAction.php
+++ b/includes/actions/RawAction.php
@@ -7,7 +7,20 @@
*
* Based on HistoryPage and SpecialExport
*
- * License: GPL (http://www.gnu.org/copyleft/gpl.html)
+ * 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
*
* @author Gabriel Wicke <wicke@wikidev.net>
* @file
@@ -120,10 +133,13 @@ class RawAction extends FormlessAction {
// If it's a MediaWiki message we can just hit the message cache
if ( $request->getBool( 'usemsgcache' ) && $title->getNamespace() == NS_MEDIAWIKI ) {
- $key = $title->getDBkey();
- $msg = wfMessage( $key )->inContentLanguage();
- # If the message doesn't exist, return a blank
- $text = !$msg->exists() ? '' : $msg->plain();
+ // The first "true" is to use the database, the second is to use the content langue
+ // and the last one is to specify the message key already contains the language in it ("/de", etc.)
+ $text = MessageCache::singleton()->get( $title->getDBkey(), true, true, true );
+ // If the message doesn't exist, return a blank
+ if ( $text === false ) {
+ $text = '';
+ }
} else {
// Get it from the DB
$rev = Revision::newFromTitle( $title, $this->getOldId() );
diff --git a/includes/actions/RevertAction.php b/includes/actions/RevertAction.php
index f9497f4b..77434384 100644
--- a/includes/actions/RevertAction.php
+++ b/includes/actions/RevertAction.php
@@ -75,8 +75,8 @@ class RevertFileAction extends FormAction {
}
protected function alterForm( HTMLForm $form ) {
- $form->setWrapperLegend( wfMsgHtml( 'filerevert-legend' ) );
- $form->setSubmitText( wfMsg( 'filerevert-submit' ) );
+ $form->setWrapperLegendMsg( 'filerevert-legend' );
+ $form->setSubmitTextMsg( 'filerevert-submit' );
$form->addHiddenField( 'oldimage', $this->getRequest()->getText( 'oldimage' ) );
}
@@ -85,22 +85,28 @@ class RevertFileAction extends FormAction {
$timestamp = $this->oldFile->getTimestamp();
+ $user = $this->getUser();
+ $lang = $this->getLanguage();
+ $userDate = $lang->userDate( $timestamp, $user );
+ $userTime = $lang->userTime( $timestamp, $user );
+ $siteDate = $wgContLang->date( $timestamp, false, false );
+ $siteTime = $wgContLang->time( $timestamp, false, false );
+
return array(
'intro' => array(
'type' => 'info',
'vertical-label' => true,
'raw' => true,
- 'default' => wfMsgExt( 'filerevert-intro', 'parse', $this->getTitle()->getText(),
- $this->getLanguage()->date( $timestamp, true ), $this->getLanguage()->time( $timestamp, true ),
+ 'default' => $this->msg( 'filerevert-intro',
+ $this->getTitle()->getText(), $userDate, $userTime,
wfExpandUrl( $this->page->getFile()->getArchiveUrl( $this->getRequest()->getText( 'oldimage' ) ),
- PROTO_CURRENT
- ) )
+ PROTO_CURRENT ) )->parseAsBlock()
),
'comment' => array(
'type' => 'text',
'label-message' => 'filerevert-comment',
- 'default' => wfMsgForContent( 'filerevert-defaultcomment',
- $wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ),
+ 'default' => $this->msg( 'filerevert-defaultcomment', $siteDate, $siteTime
+ )->inContentLanguage()->text()
)
);
}
@@ -114,17 +120,21 @@ class RevertFileAction extends FormAction {
public function onSuccess() {
$timestamp = $this->oldFile->getTimestamp();
- $this->getOutput()->addHTML( wfMsgExt( 'filerevert-success', 'parse', $this->getTitle()->getText(),
- $this->getLanguage()->date( $timestamp, true ),
- $this->getLanguage()->time( $timestamp, true ),
+ $user = $this->getUser();
+ $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' ) ),
PROTO_CURRENT
- ) ) );
+ ) );
$this->getOutput()->returnToMain( false, $this->getTitle() );
}
protected function getPageTitle() {
- return wfMsg( 'filerevert', $this->getTitle()->getText() );
+ return $this->msg( 'filerevert', $this->getTitle()->getText() );
}
protected function getDescription() {
diff --git a/includes/actions/RevisiondeleteAction.php b/includes/actions/RevisiondeleteAction.php
index f07e493d..14da2fcf 100644
--- a/includes/actions/RevisiondeleteAction.php
+++ b/includes/actions/RevisiondeleteAction.php
@@ -44,6 +44,6 @@ class RevisiondeleteAction extends FormlessAction {
public function show() {
$special = SpecialPageFactory::getPage( 'Revisiondelete' );
$special->setContext( $this->getContext() );
- $special->execute( '' );
+ $special->run( '' );
}
}
diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php
index ebb34c78..0d9a9027 100644
--- a/includes/actions/RollbackAction.php
+++ b/includes/actions/RollbackAction.php
@@ -63,7 +63,7 @@ class RollbackAction extends FormlessAction {
$current = $details['current'];
if ( $current->getComment() != '' ) {
- $this->getOutput()->addHTML( wfMessage( 'editcomment' )->rawParams(
+ $this->getOutput()->addHTML( $this->msg( 'editcomment' )->rawParams(
Linker::formatComment( $current->getComment() ) )->parse() );
}
}
@@ -97,7 +97,7 @@ class RollbackAction extends FormlessAction {
$this->getOutput()->setRobotPolicy( 'noindex,nofollow' );
if ( $current->getUserText() === '' ) {
- $old = wfMsg( 'rev-deleted-user' );
+ $old = $this->msg( 'rev-deleted-user' )->escaped();
} else {
$old = Linker::userLink( $current->getUser(), $current->getUserText() )
. Linker::userToolLinks( $current->getUser(), $current->getUserText() );
@@ -105,7 +105,7 @@ class RollbackAction extends FormlessAction {
$new = Linker::userLink( $target->getUser(), $target->getUserText() )
. Linker::userToolLinks( $target->getUser(), $target->getUserText() );
- $this->getOutput()->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
+ $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 ) ) {
diff --git a/includes/actions/ViewAction.php b/includes/actions/ViewAction.php
index 4e37381b..d57585ee 100644
--- a/includes/actions/ViewAction.php
+++ b/includes/actions/ViewAction.php
@@ -34,9 +34,6 @@ class ViewAction extends FormlessAction {
}
public function show(){
- global $wgSquidMaxage;
-
- $this->getOutput()->setSquidMaxage( $wgSquidMaxage );
$this->page->view();
}
diff --git a/includes/actions/WatchAction.php b/includes/actions/WatchAction.php
index 63d9b151..e2636452 100644
--- a/includes/actions/WatchAction.php
+++ b/includes/actions/WatchAction.php
@@ -31,7 +31,7 @@ class WatchAction extends FormAction {
}
protected function getDescription() {
- return wfMsgHtml( 'addwatch' );
+ return $this->msg( 'addwatch' )->escaped();
}
/**
@@ -136,11 +136,11 @@ class WatchAction extends FormAction {
}
protected function alterForm( HTMLForm $form ) {
- $form->setSubmitText( wfMsg( 'confirm-watch-button' ) );
+ $form->setSubmitTextMsg( 'confirm-watch-button' );
}
protected function preText() {
- return wfMessage( 'confirm-watch-top' )->parse();
+ return $this->msg( 'confirm-watch-top' )->parse();
}
public function onSuccess() {
@@ -155,7 +155,7 @@ class UnwatchAction extends WatchAction {
}
protected function getDescription() {
- return wfMsg( 'removewatch' );
+ return $this->msg( 'removewatch' )->escaped();
}
public function onSubmit( $data ) {
@@ -166,11 +166,11 @@ class UnwatchAction extends WatchAction {
}
protected function alterForm( HTMLForm $form ) {
- $form->setSubmitText( wfMsg( 'confirm-unwatch-button' ) );
+ $form->setSubmitTextMsg( 'confirm-unwatch-button' );
}
protected function preText() {
- return wfMessage( 'confirm-unwatch-top' )->parse();
+ return $this->msg( 'confirm-unwatch-top' )->parse();
}
public function onSuccess() {
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index a586f688..875a3814 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -4,7 +4,7 @@
*
* Created on Sep 5, 2006
*
- * Copyright © 2006, 2010 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright © 2006, 2010 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -51,9 +51,16 @@ abstract class ApiBase extends ContextSource {
const PARAM_MIN = 5; // Lowest value allowed for a parameter. Only applies if TYPE='integer'
const PARAM_ALLOW_DUPLICATES = 6; // Boolean, do we allow the same value to be set more than once when ISMULTI=true
const PARAM_DEPRECATED = 7; // Boolean, is the parameter deprecated (will show a warning)
+ /// @since 1.17
const PARAM_REQUIRED = 8; // Boolean, is the parameter required?
+ /// @since 1.17
const PARAM_RANGE_ENFORCE = 9; // Boolean, if MIN/MAX are set, enforce (die) these? Only applies if TYPE='integer' Use with extreme caution
+ const PROP_ROOT = 'ROOT'; // Name of property group that is on the root element of the result, i.e. not part of a list
+ const PROP_LIST = 'LIST'; // Boolean, is the result multiple items? Defaults to true for query modules, to false for other modules
+ const PROP_TYPE = 0; // Type of the property, uses same format as PARAM_TYPE
+ const PROP_NULLABLE = 1; // Boolean, can the property be not included in the result? Defaults to false
+
const LIMIT_BIG1 = 500; // Fast query, std user limit
const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit
const LIMIT_SML1 = 50; // Slow query, std user limit
@@ -127,7 +134,7 @@ abstract class ApiBase extends ContextSource {
/**
* Get the name of the module as shown in the profiler log
*
- * @param $db DatabaseBase
+ * @param $db DatabaseBase|bool
*
* @return string
*/
@@ -280,12 +287,12 @@ abstract class ApiBase extends ContextSource {
if ( is_numeric( $k ) ) {
$msg .= " $v\n";
} else {
- $v .= ":";
if ( is_array( $v ) ) {
$msgExample = implode( "\n", array_map( array( $this, 'indentExampleText' ), $v ) );
} else {
$msgExample = " $v";
}
+ $msgExample .= ":";
$msg .= wordwrap( $msgExample, 100, "\n" ) . "\n $k\n";
}
}
@@ -365,27 +372,38 @@ abstract class ApiBase extends ContextSource {
$desc = implode( $paramPrefix, $desc );
}
+ //handle shorthand
if ( !is_array( $paramSettings ) ) {
$paramSettings = array(
self::PARAM_DFLT => $paramSettings,
);
}
- $deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] ) ?
- $paramSettings[self::PARAM_DEPRECATED] : false;
- if ( $deprecated ) {
+ //handle missing type
+ if ( !isset( $paramSettings[ApiBase::PARAM_TYPE] ) ) {
+ $dflt = isset( $paramSettings[ApiBase::PARAM_DFLT] ) ? $paramSettings[ApiBase::PARAM_DFLT] : null;
+ if ( is_bool( $dflt ) ) {
+ $paramSettings[ApiBase::PARAM_TYPE] = 'boolean';
+ } elseif ( is_string( $dflt ) || is_null( $dflt ) ) {
+ $paramSettings[ApiBase::PARAM_TYPE] = 'string';
+ } elseif ( is_int( $dflt ) ) {
+ $paramSettings[ApiBase::PARAM_TYPE] = 'integer';
+ }
+ }
+
+ if ( isset( $paramSettings[self::PARAM_DEPRECATED] ) && $paramSettings[self::PARAM_DEPRECATED] ) {
$desc = "DEPRECATED! $desc";
}
- $required = isset( $paramSettings[self::PARAM_REQUIRED] ) ?
- $paramSettings[self::PARAM_REQUIRED] : false;
- if ( $required ) {
+ if ( isset( $paramSettings[self::PARAM_REQUIRED] ) && $paramSettings[self::PARAM_REQUIRED] ) {
$desc .= $paramPrefix . "This parameter is required";
}
$type = isset( $paramSettings[self::PARAM_TYPE] ) ? $paramSettings[self::PARAM_TYPE] : null;
if ( isset( $type ) ) {
- if ( isset( $paramSettings[self::PARAM_ISMULTI] ) && $paramSettings[self::PARAM_ISMULTI] ) {
+ $hintPipeSeparated = true;
+ $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) ? $paramSettings[self::PARAM_ISMULTI] : false;
+ if ( $multi ) {
$prompt = 'Values (separate with \'|\'): ';
} else {
$prompt = 'One value: ';
@@ -393,7 +411,7 @@ abstract class ApiBase extends ContextSource {
if ( is_array( $type ) ) {
$choices = array();
- $nothingPrompt = false;
+ $nothingPrompt = '';
foreach ( $type as $t ) {
if ( $t === '' ) {
$nothingPrompt = 'Can be empty, or ';
@@ -404,6 +422,7 @@ abstract class ApiBase extends ContextSource {
$desc .= $paramPrefix . $nothingPrompt . $prompt;
$choicesstring = implode( ', ', $choices );
$desc .= wordwrap( $choicesstring, 100, $descWordwrap );
+ $hintPipeSeparated = false;
} else {
switch ( $type ) {
case 'namespace':
@@ -411,6 +430,7 @@ abstract class ApiBase extends ContextSource {
$desc .= $paramPrefix . $prompt;
$desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ),
100, $descWordwrap );
+ $hintPipeSeparated = false;
break;
case 'limit':
$desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]}";
@@ -420,37 +440,39 @@ abstract class ApiBase extends ContextSource {
$desc .= ' allowed';
break;
case 'integer':
+ $s = $multi ? 's' : '';
$hasMin = isset( $paramSettings[self::PARAM_MIN] );
$hasMax = isset( $paramSettings[self::PARAM_MAX] );
if ( $hasMin || $hasMax ) {
if ( !$hasMax ) {
- $intRangeStr = "The value must be no less than {$paramSettings[self::PARAM_MIN]}";
+ $intRangeStr = "The value$s must be no less than {$paramSettings[self::PARAM_MIN]}";
} elseif ( !$hasMin ) {
- $intRangeStr = "The value must be no more than {$paramSettings[self::PARAM_MAX]}";
+ $intRangeStr = "The value$s must be no more than {$paramSettings[self::PARAM_MAX]}";
} else {
- $intRangeStr = "The value must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}";
+ $intRangeStr = "The value$s must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}";
}
$desc .= $paramPrefix . $intRangeStr;
}
break;
}
+ }
- if ( isset( $paramSettings[self::PARAM_ISMULTI] ) ) {
- $isArray = is_array( $paramSettings[self::PARAM_TYPE] );
+ if ( $multi ) {
+ if ( $hintPipeSeparated ) {
+ $desc .= $paramPrefix . "Separate values with '|'";
+ }
- if ( !$isArray
- || $isArray && count( $paramSettings[self::PARAM_TYPE] ) > self::LIMIT_SML1 ) {
- $desc .= $paramPrefix . "Maximum number of values " .
- self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)";
- }
+ $isArray = is_array( $type );
+ if ( !$isArray
+ || $isArray && count( $type ) > self::LIMIT_SML1 ) {
+ $desc .= $paramPrefix . "Maximum number of values " .
+ self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)";
}
}
}
- $default = is_array( $paramSettings )
- ? ( isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null )
- : $paramSettings;
+ $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null;
if ( !is_null( $default ) && $default !== false ) {
$desc .= $paramPrefix . "Default: $default";
}
@@ -512,7 +534,7 @@ abstract class ApiBase extends ContextSource {
/**
* Returns usage examples for this module. Return false if no examples are available.
- * @return false|string|array
+ * @return bool|string|array
*/
protected function getExamples() {
return false;
@@ -523,7 +545,7 @@ 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.
- * @return array or false
+ * @return array|bool
*/
protected function getAllowedParams() {
return false;
@@ -533,7 +555,7 @@ abstract class ApiBase extends ContextSource {
* Returns an array of parameter descriptions.
* Don't call this functon directly: use getFinalParamDescription() to
* allow hooks to modify descriptions as needed.
- * @return array or false
+ * @return array|bool False on no parameter descriptions
*/
protected function getParamDescription() {
return false;
@@ -543,7 +565,7 @@ abstract class ApiBase extends ContextSource {
* Get final list of parameters, after hooks have had a chance to
* tweak it as needed.
*
- * @return array or false
+ * @return array|Bool False on no parameters
*/
public function getFinalParams() {
$params = $this->getAllowedParams();
@@ -555,7 +577,7 @@ abstract class ApiBase extends ContextSource {
* Get final parameter descriptions, after hooks have had a chance to tweak it as
* needed.
*
- * @return array
+ * @return array|bool False on no parameter descriptions
*/
public function getFinalParamDescription() {
$desc = $this->getParamDescription();
@@ -564,11 +586,56 @@ abstract class ApiBase extends ContextSource {
}
/**
- * Get final module description, after hooks have had a chance to tweak it as
+ * Returns possible properties in the result, grouped by the value of the prop parameter
+ * that shows them.
+ *
+ * Properties that are shown always are in a group with empty string as a key.
+ * Properties that can be shown by several values of prop are included multiple times.
+ * If some properties are part of a list and some are on the root object (see ApiQueryQueryPage),
+ * those on the root object are under the key PROP_ROOT.
+ * 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
+ * allow hooks to modify descriptions as needed.
+ *
+ * @return array|bool False on no properties
+ */
+ protected function getResultProperties() {
+ return false;
+ }
+
+ /**
+ * Get final possible result properties, after hooks have had a chance to tweak it as
* needed.
*
* @return array
*/
+ public function getFinalResultProperties() {
+ $properties = $this->getResultProperties();
+ wfRunHooks( 'APIGetResultProperties', array( $this, &$properties ) );
+ return $properties;
+ }
+
+ /**
+ * Add token properties to the array used by getResultProperties,
+ * based on a token functions mapping.
+ */
+ protected static function addTokenProperties( &$props, $tokenFunctions ) {
+ foreach ( array_keys( $tokenFunctions ) as $token ) {
+ $props[''][$token . 'token'] = array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ );
+ }
+ }
+
+ /**
+ * Get final module description, after hooks have had a chance to tweak it as
+ * needed.
+ *
+ * @return array|bool False on no parameters
+ */
public function getFinalDescription() {
$desc = $this->getDescription();
wfRunHooks( 'APIGetDescription', array( &$this, &$desc ) );
@@ -630,14 +697,15 @@ abstract class ApiBase extends ContextSource {
public function requireOnlyOneParameter( $params ) {
$required = func_get_args();
array_shift( $required );
+ $p = $this->getModulePrefix();
$intersection = array_intersect( array_keys( array_filter( $params,
array( $this, "parameterNotEmpty" ) ) ), $required );
if ( count( $intersection ) > 1 ) {
- $this->dieUsage( 'The parameters ' . implode( ', ', $intersection ) . ' can not be used together', 'invalidparammix' );
+ $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" );
} elseif ( count( $intersection ) == 0 ) {
- $this->dieUsage( 'One of the parameters ' . implode( ', ', $required ) . ' is required', 'missingparam' );
+ $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" );
}
}
@@ -665,12 +733,13 @@ abstract class ApiBase extends ContextSource {
public function requireMaxOneParameter( $params ) {
$required = func_get_args();
array_shift( $required );
+ $p = $this->getModulePrefix();
$intersection = array_intersect( array_keys( array_filter( $params,
array( $this, "parameterNotEmpty" ) ) ), $required );
if ( count( $intersection ) > 1 ) {
- $this->dieUsage( 'The parameters ' . implode( ', ', $intersection ) . ' can not be used together', 'invalidparammix' );
+ $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" );
}
}
@@ -690,6 +759,53 @@ abstract class ApiBase extends ContextSource {
}
/**
+ * @param $params array
+ * @param $load bool|string 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
+ * @return WikiPage
+ */
+ public function getTitleOrPageId( $params, $load = false ) {
+ $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
+
+ $pageObj = null;
+ if ( isset( $params['title'] ) ) {
+ $titleObj = Title::newFromText( $params['title'] );
+ if ( !$titleObj ) {
+ $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
+ }
+ $pageObj = WikiPage::factory( $titleObj );
+ if ( $load !== false ) {
+ $pageObj->loadPageData( $load );
+ }
+ } elseif ( isset( $params['pageid'] ) ) {
+ if ( $load === false ) {
+ $load = 'fromdb';
+ }
+ $pageObj = WikiPage::newFromID( $params['pageid'], $load );
+ if ( !$pageObj ) {
+ $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
+ }
+ }
+
+ return $pageObj;
+ }
+
+ /**
+ * @return array
+ */
+ public function getTitleOrPageIdErrorMessage() {
+ return array_merge(
+ $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ),
+ array(
+ array( 'invalidtitle', 'title' ),
+ array( 'nosuchpageid', 'pageid' ),
+ )
+ );
+ }
+
+ /**
* Callback function used in requireOnlyOneParameter to check whether reequired parameters are set
*
* @param $x object Parameter to check is not null/false
@@ -719,7 +835,7 @@ abstract class ApiBase extends ContextSource {
*/
protected function getWatchlistValue ( $watchlist, $titleObj, $userOption = null ) {
- $userWatching = $titleObj->userIsWatching();
+ $userWatching = $this->getUser()->isWatched( $titleObj );
switch ( $watchlist ) {
case 'watch':
@@ -773,7 +889,7 @@ abstract class ApiBase extends ContextSource {
* Using the settings determine the value for the given parameter
*
* @param $paramName String: parameter name
- * @param $paramSettings Mixed: default value or an array of settings
+ * @param $paramSettings array|mixed default value or an array of settings
* using PARAM_* constants.
* @param $parseLimit Boolean: parse limit?
* @return mixed Parameter value
@@ -809,8 +925,8 @@ abstract class ApiBase extends ContextSource {
if ( $type == 'boolean' ) {
if ( isset( $default ) && $default !== false ) {
- // Having a default value of anything other than 'false' is pointless
- ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'" );
+ // Having a default value of anything other than 'false' is not allowed
+ ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'. Boolean parameters must default to false." );
}
$value = $this->getRequest()->getCheck( $encParamName );
@@ -1078,7 +1194,8 @@ abstract class ApiBase extends ContextSource {
* @param $errorCode string Brief, arbitrary, stable string to allow easy
* automated identification of the error, e.g., 'unknown_action'
* @param $httpRespCode int HTTP response code
- * @param $extradata array Data to add to the <error> element; array in ApiResult format
+ * @param $extradata array Data to add to the "<error>" element; array in ApiResult format
+ * @throws UsageException
*/
public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
Profiler::instance()->close();
@@ -1155,6 +1272,8 @@ abstract class ApiBase extends ContextSource {
'nouserspecified' => array( 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ),
'noname' => array( 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ),
'summaryrequired' => array( 'code' => 'summaryrequired', 'info' => 'Summary required' ),
+ 'import-rootpage-invalid' => array( 'code' => 'import-rootpage-invalid', 'info' => 'Root page is an invalid title' ),
+ 'import-rootpage-nosubpage' => array( 'code' => 'import-rootpage-nosubpage', 'info' => 'Namespace "$1" of the root page does not allow subpages' ),
// API-specific messages
'readrequired' => array( 'code' => 'readapidenied', 'info' => "You need read permission to use this module" ),
@@ -1186,7 +1305,6 @@ abstract class ApiBase extends ContextSource {
'toofewexpiries' => array( 'code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed" ),
'cantimport' => array( 'code' => 'cantimport', 'info' => "You don't have permission to import pages" ),
'cantimport-upload' => array( 'code' => 'cantimport-upload', 'info' => "You don't have permission to import uploaded pages" ),
- 'nouploadmodule' => array( 'code' => 'nomodule', 'info' => 'No upload module set' ),
'importnofile' => array( 'code' => 'nofile', 'info' => "You didn't upload a file" ),
'importuploaderrorsize' => array( 'code' => 'filetoobig', 'info' => 'The file you uploaded is bigger than the maximum upload size' ),
'importuploaderrorpartial' => array( 'code' => 'partialupload', 'info' => 'The file was only partially uploaded' ),
@@ -1202,12 +1320,14 @@ abstract class ApiBase extends ContextSource {
'specialpage-cantexecute' => array( 'code' => 'specialpage-cantexecute', 'info' => "You don't have permission to view the results of this special page" ),
'invalidoldimage' => array( 'code' => 'invalidoldimage', 'info' => 'The oldimage parameter has invalid format' ),
'nodeleteablefile' => array( 'code' => 'nodeleteablefile', 'info' => 'No such old version of the file' ),
+ 'fileexists-forbidden' => array( 'code' => 'fileexists-forbidden', 'info' => 'A file with name "$1" already exists, and cannot be overwritten.' ),
+ 'fileexists-shared-forbidden' => array( 'code' => 'fileexists-shared-forbidden', 'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.' ),
+ 'filerevert-badversion' => array( 'code' => 'filerevert-badversion', 'info' => 'There is no previous local version of this file with the provided timestamp.' ),
// ApiEditPage messages
'noimageredirect-anon' => array( 'code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects" ),
'noimageredirect-logged' => array( 'code' => 'noimageredirect', 'info' => "You don't have permission to create image redirects" ),
'spamdetected' => array( 'code' => 'spamdetected', 'info' => "Your edit was refused because it contained a spam fragment: \"\$1\"" ),
- 'filtered' => array( 'code' => 'filtered', 'info' => "The filter callback function refused your edit" ),
'contenttoobig' => array( 'code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes" ),
'noedit-anon' => array( 'code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages" ),
'noedit' => array( 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ),
@@ -1227,10 +1347,11 @@ abstract class ApiBase extends ContextSource {
'edit-already-exists' => array( 'code' => 'edit-already-exists', 'info' => "It seems the page you tried to create already exist" ),
// uploadMsgs
- 'invalid-session-key' => array( 'code' => 'invalid-session-key', 'info' => 'Not a valid session key' ),
+ '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.' ),
+ 'copyuploadbaddomain' => array( 'code' => 'copyuploadbaddomain', 'info' => 'Uploads by URL are not allowed from this domain.' ),
'filename-tooshort' => array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ),
'filename-toolong' => array( 'code' => 'filename-toolong', 'info' => 'The filename is too long' ),
@@ -1280,10 +1401,9 @@ abstract class ApiBase extends ContextSource {
}
if ( isset( self::$messageMap[$key] ) ) {
- return array( 'code' =>
- wfMsgReplaceArgs( self::$messageMap[$key]['code'], $error ),
- 'info' =>
- wfMsgReplaceArgs( self::$messageMap[$key]['info'], $error )
+ return array(
+ 'code' => wfMsgReplaceArgs( self::$messageMap[$key]['code'], $error ),
+ 'info' => wfMsgReplaceArgs( self::$messageMap[$key]['info'], $error )
);
}
@@ -1332,7 +1452,9 @@ abstract class ApiBase extends ContextSource {
}
/**
- * Returns whether this module requires a Token to execute
+ * Returns whether this module requires a token to execute
+ * It is used to show possible errors in action=paraminfo
+ * see bug 25248
* @return bool
*/
public function needsToken() {
@@ -1340,8 +1462,12 @@ abstract class ApiBase extends ContextSource {
}
/**
- * Returns the token salt if there is one, '' if the module doesn't require a salt, else false if the module doesn't need a token
- * @return bool|string
+ * Returns the token salt if there is one,
+ * '' if the module doesn't require a salt,
+ * else false if the module doesn't need a token
+ * You have also to override needsToken()
+ * Value is passed to User::getEditToken
+ * @return bool|string|array
*/
public function getTokenSalt() {
return false;
@@ -1373,7 +1499,7 @@ abstract class ApiBase extends ContextSource {
}
/**
- * @return false|string|array Returns a false if the module has no help url, else returns a (array of) string
+ * @return bool|string|array Returns a false if the module has no help url, else returns a (array of) string
*/
public function getHelpUrls() {
return false;
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
index 351ac6b7..c879b35d 100644
--- a/includes/api/ApiBlock.php
+++ b/includes/api/ApiBlock.php
@@ -4,7 +4,7 @@
*
* Created on Sep 4, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -47,7 +47,7 @@ class ApiBlock extends ApiBase {
$params = $this->extractRequestParams();
if ( $params['gettoken'] ) {
- $res['blocktoken'] = $user->getEditToken( '', $this->getMain()->getRequest() );
+ $res['blocktoken'] = $user->getEditToken();
$this->getResult()->addValue( null, $this->getModuleName(), $res );
return;
}
@@ -72,9 +72,9 @@ class ApiBlock extends ApiBase {
$data = array(
'Target' => $params['user'],
'Reason' => array(
- is_null( $params['reason'] ) ? '' : $params['reason'],
+ $params['reason'],
'other',
- is_null( $params['reason'] ) ? '' : $params['reason']
+ $params['reason']
),
'Expiry' => $params['expiry'] == 'never' ? 'infinite' : $params['expiry'],
'HardBlock' => !$params['anononly'],
@@ -100,12 +100,14 @@ class ApiBlock extends ApiBase {
$block = Block::newFromTarget( $target );
if( $block instanceof Block ){
- $res['expiry'] = $block->mExpiry == wfGetDB( DB_SLAVE )->getInfinity()
+ $res['expiry'] = $block->mExpiry == $this->getDB()->getInfinity()
? 'infinite'
: wfTimestamp( TS_ISO_8601, $block->mExpiry );
+ $res['id'] = $block->getId();
} else {
# should be unreachable
$res['expiry'] = '';
+ $res['id'] = '';
}
$res['reason'] = $params['reason'];
@@ -149,9 +151,12 @@ class ApiBlock extends ApiBase {
ApiBase::PARAM_REQUIRED => true
),
'token' => null,
- 'gettoken' => false,
+ 'gettoken' => array(
+ ApiBase::PARAM_DFLT => false,
+ ApiBase::PARAM_DEPRECATED => true,
+ ),
'expiry' => 'never',
- 'reason' => null,
+ 'reason' => '',
'anononly' => false,
'nocreate' => false,
'autoblock' => false,
@@ -166,10 +171,10 @@ class ApiBlock extends ApiBase {
public function getParamDescription() {
return array(
'user' => 'Username, IP address or IP range you want to block',
- 'token' => 'A block token previously obtained through the gettoken parameter or prop=info',
+ '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 (optional)',
+ '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',
@@ -181,6 +186,44 @@ 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
+ ),
+ 'userID' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'expiry' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'id' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'reason' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'anononly' => 'boolean',
+ 'nocreate' => 'boolean',
+ 'autoblock' => 'boolean',
+ 'noemail' => 'boolean',
+ 'hidename' => 'boolean',
+ 'allowusertalk' => 'boolean',
+ 'watchuser' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
return 'Block a user';
}
diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php
index 4bb94c4a..ed72b29b 100644
--- a/includes/api/ApiComparePages.php
+++ b/includes/api/ApiComparePages.php
@@ -32,8 +32,8 @@ class ApiComparePages extends ApiBase {
public function execute() {
$params = $this->extractRequestParams();
- $rev1 = $this->revisionOrTitle( $params['fromrev'], $params['fromtitle'] );
- $rev2 = $this->revisionOrTitle( $params['torev'], $params['totitle'] );
+ $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] );
+ $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] );
$de = new DifferenceEngine( $this->getContext(),
$rev1,
@@ -46,10 +46,16 @@ class ApiComparePages extends ApiBase {
if ( isset( $params['fromtitle'] ) ) {
$vals['fromtitle'] = $params['fromtitle'];
}
+ if ( isset( $params['fromid'] ) ) {
+ $vals['fromid'] = $params['fromid'];
+ }
$vals['fromrevid'] = $rev1;
if ( isset( $params['totitle'] ) ) {
$vals['totitle'] = $params['totitle'];
}
+ if ( isset( $params['toid'] ) ) {
+ $vals['toid'] = $params['toid'];
+ }
$vals['torevid'] = $rev2;
$difftext = $de->getDiffBody();
@@ -67,9 +73,10 @@ class ApiComparePages extends ApiBase {
/**
* @param $revision int
* @param $titleText string
+ * @param $titleId int
* @return int
*/
- private function revisionOrTitle( $revision, $titleText ) {
+ private function revisionOrTitleOrId( $revision, $titleText, $titleId ) {
if( $revision ){
return $revision;
} elseif( $titleText ) {
@@ -78,17 +85,29 @@ class ApiComparePages extends ApiBase {
$this->dieUsageMsg( array( 'invalidtitle', $titleText ) );
}
return $title->getLatestRevID();
+ } elseif ( $titleId ) {
+ $title = Title::newFromID( $titleId );
+ if( !$title ) {
+ $this->dieUsageMsg( array( 'nosuchpageid', $titleId ) );
+ }
+ return $title->getLatestRevID();
}
- $this->dieUsage( 'inputneeded', 'A title or a revision number is needed for both the from and the to parameters' );
+ $this->dieUsage( 'inputneeded', 'A title, a page ID, or a revision number is needed for both the from and the to parameters' );
}
public function getAllowedParams() {
return array(
'fromtitle' => null,
+ 'fromid' => array(
+ ApiBase::PARAM_TYPE => 'integer'
+ ),
'fromrev' => array(
ApiBase::PARAM_TYPE => 'integer'
),
'totitle' => null,
+ 'toid' => array(
+ ApiBase::PARAM_TYPE => 'integer'
+ ),
'torev' => array(
ApiBase::PARAM_TYPE => 'integer'
),
@@ -98,15 +117,36 @@ class ApiComparePages extends ApiBase {
public function getParamDescription() {
return array(
'fromtitle' => 'First title to compare',
+ 'fromid' => 'First page ID to compare',
'fromrev' => 'First revision to compare',
'totitle' => 'Second title to compare',
+ 'toid' => 'Second page ID to compare',
'torev' => 'Second revision to compare',
);
}
+
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'fromtitle' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'fromrevid' => 'integer',
+ 'totitle' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'torevid' => 'integer',
+ '*' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return array(
'Get the difference between 2 pages',
- 'You must pass a revision number or a page title for each part (1 and 2)'
+ 'You must pass a revision number or a page title or a page ID id for each part (1 and 2)'
);
}
@@ -114,6 +154,7 @@ class ApiComparePages extends ApiBase {
return array_merge( parent::getPossibleErrors(), array(
array( 'code' => 'inputneeded', 'info' => 'A title or a revision is needed' ),
array( 'invalidtitle', 'title' ),
+ array( 'nosuchpageid', 'pageid' ),
array( 'code' => 'baddiff', 'info' => 'The diff cannot be retrieved. Maybe one or both revisions do not exist or you do not have permission to view them.' ),
) );
}
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
index cfaf6cc1..2d36f19a 100644
--- a/includes/api/ApiDelete.php
+++ b/includes/api/ApiDelete.php
@@ -4,7 +4,7 @@
*
* Created on Jun 30, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -46,35 +46,24 @@ class ApiDelete extends ApiBase {
public function execute() {
$params = $this->extractRequestParams();
- $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
-
- if ( isset( $params['title'] ) ) {
- $titleObj = Title::newFromText( $params['title'] );
- if ( !$titleObj ) {
- $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
- }
- } elseif ( isset( $params['pageid'] ) ) {
- $titleObj = Title::newFromID( $params['pageid'] );
- if ( !$titleObj ) {
- $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
- }
- }
- if ( !$titleObj->exists() ) {
+ $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
+ if ( !$pageObj->exists() ) {
$this->dieUsageMsg( 'notanarticle' );
}
- $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
- $pageObj = WikiPage::factory( $titleObj );
+ $titleObj = $pageObj->getTitle();
+ $reason = $params['reason'];
$user = $this->getUser();
if ( $titleObj->getNamespace() == NS_FILE ) {
- $retval = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false );
+ $status = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false );
} else {
- $retval = self::delete( $pageObj, $user, $params['token'], $reason );
+ $status = self::delete( $pageObj, $user, $params['token'], $reason );
}
- if ( count( $retval ) ) {
- $this->dieUsageMsg( reset( $retval ) ); // We don't care about multiple errors, just report one of them
+ if ( !$status->isGood() ) {
+ $errors = $status->getErrorsArray();
+ $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them
}
// Deprecated parameters
@@ -87,7 +76,11 @@ class ApiDelete extends ApiBase {
}
$this->setWatch( $watch, $titleObj, 'watchdeletion' );
- $r = array( 'title' => $titleObj->getPrefixedText(), 'reason' => $reason );
+ $r = array(
+ 'title' => $titleObj->getPrefixedText(),
+ 'reason' => $reason,
+ 'logid' => $status->value
+ );
$this->getResult()->addValue( null, $this->getModuleName(), $r );
}
@@ -109,7 +102,7 @@ class ApiDelete extends ApiBase {
* @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 Title::getUserPermissionsErrors()-like array
+ * @return Status
*/
public static function delete( Page $page, User $user, $token, &$reason = null ) {
$title = $page->getTitle();
@@ -131,11 +124,7 @@ class ApiDelete extends ApiBase {
$error = '';
// Luckily, Article.php provides a reusable delete function that does the hard work for us
- if ( $page->doDeleteArticle( $reason, false, 0, true, $error ) ) {
- return array();
- } else {
- return array( array( 'cannotdelete', $title->getPrefixedText() ) );
- }
+ return $page->doDeleteArticleReal( $reason, false, 0, true, $error );
}
/**
@@ -145,7 +134,7 @@ class ApiDelete extends ApiBase {
* @param $oldimage
* @param $reason
* @param $suppress bool
- * @return \type|array|Title
+ * @return Status
*/
public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) {
$title = $page->getTitle();
@@ -167,19 +156,12 @@ class ApiDelete extends ApiBase {
if ( !$oldfile->exists() || !$oldfile->isLocal() || $oldfile->getRedirected() ) {
return array( array( 'nodeleteablefile' ) );
}
- } else {
- $oldfile = false;
}
if ( is_null( $reason ) ) { // Log and RC don't like null reasons
$reason = '';
}
- $status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
- if ( !$status->isGood() ) {
- return array( array( 'cannotdelete', $title->getPrefixedText() ) );
- }
-
- return array();
+ return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
}
public function mustBePosted() {
@@ -196,7 +178,10 @@ class ApiDelete extends ApiBase {
'pageid' => array(
ApiBase::PARAM_TYPE => 'integer'
),
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'reason' => null,
'watch' => array(
ApiBase::PARAM_DFLT => false,
@@ -233,16 +218,24 @@ class ApiDelete extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'title' => 'string',
+ 'reason' => 'string',
+ 'logid' => 'integer'
+ )
+ );
+ }
+
public function getDescription() {
return 'Delete a page';
}
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(),
- $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ),
+ $this->getTitleOrPageIdErrorMessage(),
array(
- array( 'invalidtitle', 'title' ),
- array( 'nosuchpageid', 'pageid' ),
array( 'notanarticle' ),
array( 'hookaborted', 'error' ),
array( 'delete-toobig', 'limit' ),
diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php
index 55754896..13975aec 100644
--- a/includes/api/ApiDisabled.php
+++ b/includes/api/ApiDisabled.php
@@ -4,7 +4,7 @@
*
* Created on Sep 25, 2008
*
- * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2008 Roan Kattouw "<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
diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php
index 9ed6d08d..0963fe7c 100644
--- a/includes/api/ApiEditPage.php
+++ b/includes/api/ApiEditPage.php
@@ -4,7 +4,7 @@
*
* Created on August 16, 2007
*
- * Copyright © 2007 Iker Labarga <Firstname><Lastname>@gmail.com
+ * Copyright © 2007 Iker Labarga "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -48,8 +48,9 @@ class ApiEditPage extends ApiBase {
$this->dieUsageMsg( 'missingtext' );
}
- $titleObj = Title::newFromText( $params['title'] );
- if ( !$titleObj || $titleObj->isExternal() ) {
+ $pageObj = $this->getTitleOrPageId( $params );
+ $titleObj = $pageObj->getTitle();
+ if ( $titleObj->isExternal() ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
@@ -59,7 +60,11 @@ class ApiEditPage extends ApiBase {
if ( $titleObj->isRedirect() ) {
$oldTitle = $titleObj;
- $titles = Title::newFromRedirectArray( Revision::newFromTitle( $oldTitle )->getText( Revision::FOR_THIS_USER ) );
+ $titles = Title::newFromRedirectArray(
+ Revision::newFromTitle(
+ $oldTitle, false, Revision::READ_LATEST
+ )->getText( Revision::FOR_THIS_USER )
+ );
// array_shift( $titles );
$redirValues = array();
@@ -161,7 +166,7 @@ class ApiEditPage extends ApiBase {
// If no summary was given and we only undid one rev,
// use an autosummary
if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] ) {
- $params['summary'] = wfMsgForContent( 'undo-summary', $params['undo'], $undoRev->getUserText() );
+ $params['summary'] = wfMessage( 'undo-summary', $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text();
}
}
@@ -181,7 +186,7 @@ class ApiEditPage extends ApiBase {
if ( !is_null( $params['summary'] ) ) {
$requestArray['wpSummary'] = $params['summary'];
}
-
+
if ( !is_null( $params['sectiontitle'] ) ) {
$requestArray['wpSectionTitle'] = $params['sectiontitle'];
}
@@ -282,9 +287,6 @@ class ApiEditPage extends ApiBase {
case EditPage::AS_SPAM_ERROR:
$this->dieUsageMsg( array( 'spamdetected', $result['spam'] ) );
- case EditPage::AS_FILTERING:
- $this->dieUsageMsg( 'filtered' );
-
case EditPage::AS_BLOCKED_PAGE_FOR_USER:
$this->dieUsageMsg( 'blockedtext' );
@@ -342,16 +344,11 @@ class ApiEditPage extends ApiBase {
$this->dieUsageMsg( 'summaryrequired' );
case EditPage::AS_END:
+ default:
// $status came from WikiPage::doEdit()
$errors = $status->getErrorsArray();
$this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map
break;
- default:
- if ( is_string( $status->value ) && strlen( $status->value ) ) {
- $this->dieUsage( "An unknown return value was returned by Editpage. The code returned was \"{$status->value}\"" , $status->value );
- } else {
- $this->dieUsageMsg( array( 'unknownerror', $status->value ) );
- }
}
$apiResult->addValue( null, $this->getModuleName(), $r );
}
@@ -371,45 +368,48 @@ class ApiEditPage extends ApiBase {
public function getPossibleErrors() {
global $wgMaxArticleSize;
- return array_merge( parent::getPossibleErrors(), array(
- array( 'missingtext' ),
- array( 'invalidtitle', 'title' ),
- array( 'createonly-exists' ),
- array( 'nocreate-missing' ),
- array( 'nosuchrevid', 'undo' ),
- array( 'nosuchrevid', 'undoafter' ),
- array( 'revwrongpage', 'id', 'text' ),
- array( 'undo-failure' ),
- array( 'hashcheckfailed' ),
- array( 'hookaborted' ),
- array( 'noimageredirect-anon' ),
- array( 'noimageredirect-logged' ),
- array( 'spamdetected', 'spam' ),
- array( 'summaryrequired' ),
- array( 'filtered' ),
- array( 'blockedtext' ),
- array( 'contenttoobig', $wgMaxArticleSize ),
- array( 'noedit-anon' ),
- array( 'noedit' ),
- array( 'actionthrottledtext' ),
- array( 'wasdeleted' ),
- array( 'nocreate-loggedin' ),
- array( 'blankpage' ),
- array( 'editconflict' ),
- array( 'emptynewsection' ),
- array( 'unknownerror', 'retval' ),
- array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ),
- array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ),
- array( 'customcssprotected' ),
- array( 'customjsprotected' ),
- ) );
+ return array_merge( parent::getPossibleErrors(),
+ $this->getTitleOrPageIdErrorMessage(),
+ array(
+ array( 'missingtext' ),
+ array( 'createonly-exists' ),
+ array( 'nocreate-missing' ),
+ array( 'nosuchrevid', 'undo' ),
+ array( 'nosuchrevid', 'undoafter' ),
+ array( 'revwrongpage', 'id', 'text' ),
+ array( 'undo-failure' ),
+ array( 'hashcheckfailed' ),
+ array( 'hookaborted' ),
+ array( 'noimageredirect-anon' ),
+ array( 'noimageredirect-logged' ),
+ array( 'spamdetected', 'spam' ),
+ array( 'summaryrequired' ),
+ array( 'blockedtext' ),
+ array( 'contenttoobig', $wgMaxArticleSize ),
+ array( 'noedit-anon' ),
+ array( 'noedit' ),
+ array( 'actionthrottledtext' ),
+ array( 'wasdeleted' ),
+ array( 'nocreate-loggedin' ),
+ array( 'blankpage' ),
+ array( 'editconflict' ),
+ array( 'emptynewsection' ),
+ array( 'unknownerror', 'retval' ),
+ array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ),
+ array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ),
+ array( 'customcssprotected' ),
+ array( 'customjsprotected' ),
+ )
+ );
}
public function getAllowedParams() {
return array(
'title' => array(
ApiBase::PARAM_TYPE => 'string',
- ApiBase::PARAM_REQUIRED => true
+ ),
+ 'pageid' => array(
+ ApiBase::PARAM_TYPE => 'integer',
),
'section' => null,
'sectiontitle' => array(
@@ -417,7 +417,10 @@ class ApiEditPage extends ApiBase {
ApiBase::PARAM_REQUIRED => false,
),
'text' => null,
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'summary' => null,
'minor' => false,
'notminor' => false,
@@ -463,19 +466,20 @@ class ApiEditPage extends ApiBase {
public function getParamDescription() {
$p = $this->getModulePrefix();
return array(
- 'title' => 'Page title',
+ 'title' => "Title of the page you want to edit. Cannot be used together with {$p}pageid",
+ 'pageid' => "Page ID of the page you want to edit. Cannot be used together with {$p}title",
'section' => 'Section number. 0 for the top section, \'new\' for a new section',
'sectiontitle' => 'The title for a new section',
'text' => 'Page content',
'token' => array( 'Edit token. You can get one of these through prop=info.',
- 'The token should always be sent as the last parameter, or at least, after the text parameter'
+ "The token should always be sent as the last parameter, or at least, after the {$p}text parameter"
),
- 'summary' => 'Edit summary. Also section title when section=new',
+ 'summary' => "Edit summary. Also section title when {$p}section=new and {$p}sectiontitle is not set",
'minor' => 'Minor edit',
'notminor' => 'Non-minor edit',
'bot' => 'Mark this edit as bot',
'basetimestamp' => array( 'Timestamp of the base revision (obtained through prop=revisions&rvprop=timestamp).',
- 'Used to detect edit conflicts; leave unset to ignore conflicts.'
+ 'Used to detect edit conflicts; leave unset to ignore conflicts'
),
'starttimestamp' => array( 'Timestamp when you obtained the edit token.',
'Used to detect edit conflicts; leave unset to ignore conflicts'
@@ -489,13 +493,49 @@ class ApiEditPage extends ApiBase {
'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' => "Add this text to the end of the page. Overrides {$p}text",
+ 'appendtext' => array( "Add this text to the end of the page. Overrides {$p}text.",
+ "Use {$p}section=new to append a new section" ),
'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',
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'new' => 'boolean',
+ 'result' => array(
+ ApiBase::PROP_TYPE => array(
+ 'Success',
+ 'Failure'
+ ),
+ ),
+ 'pageid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'title' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'nochange' => 'boolean',
+ 'oldrevid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'newrevid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'newtimestamp' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function needsToken() {
return true;
}
diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php
index d9eed60c..4fa03434 100644
--- a/includes/api/ApiEmailUser.php
+++ b/includes/api/ApiEmailUser.php
@@ -55,7 +55,7 @@ class ApiEmailUser extends ApiBase {
'Subject' => $params['subject'],
'CCMe' => $params['ccme'],
);
- $retval = SpecialEmailUser::submit( $data );
+ $retval = SpecialEmailUser::submit( $data, $this->getContext() );
if ( $retval instanceof Status ) {
// SpecialEmailUser sometimes returns a status
@@ -98,7 +98,10 @@ class ApiEmailUser extends ApiBase {
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
),
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'ccme' => false,
);
}
@@ -113,6 +116,23 @@ class ApiEmailUser extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'result' => array(
+ ApiBase::PROP_TYPE => array(
+ 'Success',
+ 'Failure'
+ ),
+ ),
+ 'message' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'Email a user.';
}
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
index d570534d..160f5b91 100644
--- a/includes/api/ApiExpandTemplates.php
+++ b/includes/api/ApiExpandTemplates.php
@@ -4,7 +4,7 @@
*
* Created on Oct 05, 2007
*
- * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright © 2007 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
@@ -103,6 +103,14 @@ class ApiExpandTemplates extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ '*' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Expands all templates in wikitext';
}
diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php
index 4e70bde2..1cf760ae 100644
--- a/includes/api/ApiFeedContributions.php
+++ b/includes/api/ApiFeedContributions.php
@@ -60,7 +60,7 @@ class ApiFeedContributions extends ApiBase {
$this->dieUsage( 'Size difference is disabled in Miser Mode', 'sizediffdisabled' );
}
- $msg = wfMsgForContent( 'Contributions' );
+ $msg = wfMessage( 'Contributions' )->inContentLanguage()->text();
$feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']';
$feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL();
@@ -96,7 +96,7 @@ class ApiFeedContributions extends ApiBase {
}
protected function feedItem( $row ) {
- $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title );
+ $title = Title::makeTitle( intval( $row->page_namespace ), $row->page_title );
if( $title ) {
$date = $row->rev_timestamp;
$comments = $title->getTalkPage()->getFullURL();
@@ -129,7 +129,8 @@ class ApiFeedContributions extends ApiBase {
*/
protected function feedItemDesc( $revision ) {
if( $revision ) {
- return '<p>' . htmlspecialchars( $revision->getUserText() ) . wfMsgForContent( 'colon-separator' ) .
+ $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text();
+ return '<p>' . htmlspecialchars( $revision->getUserText() ) . $msg .
htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
"</p>\n<hr />\n<div>" .
nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
@@ -150,8 +151,7 @@ class ApiFeedContributions extends ApiBase {
ApiBase::PARAM_REQUIRED => true,
),
'namespace' => array(
- ApiBase::PARAM_TYPE => 'namespace',
- ApiBase::PARAM_ISMULTI => true
+ ApiBase::PARAM_TYPE => 'namespace'
),
'year' => array(
ApiBase::PARAM_TYPE => 'integer'
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index eee8fa19..6ccb02fe 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -4,7 +4,7 @@
*
* Created on Oct 13, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -117,7 +117,7 @@ class ApiFeedWatchlist extends ApiBase {
$feedItems[] = $this->createFeedItem( $info );
}
- $msg = wfMsgForContent( 'watchlist' );
+ $msg = wfMessage( 'watchlist' )->inContentLanguage()->text();
$feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']';
$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
@@ -131,11 +131,12 @@ class ApiFeedWatchlist extends ApiBase {
// Error results should not be cached
$this->getMain()->setCacheMaxAge( 0 );
- $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent( 'watchlist' ) . ' [' . $wgLanguageCode . ']';
+ $feedTitle = $wgSitename . ' - Error - ' . wfMessage( 'watchlist' )->inContentLanguage()->text() . ' [' . $wgLanguageCode . ']';
$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
$feedFormat = isset( $params['feedformat'] ) ? $params['feedformat'] : 'rss';
- $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, htmlspecialchars( wfMsgForContent( 'watchlist' ) ), $feedUrl );
+ $msg = wfMessage( 'watchlist' )->inContentLanguage()->escaped();
+ $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl );
if ( $e instanceof UsageException ) {
$errorCode = $e->getCodeString();
diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php
index 7ef1da0a..83d078d2 100644
--- a/includes/api/ApiFileRevert.php
+++ b/includes/api/ApiFileRevert.php
@@ -71,9 +71,10 @@ class ApiFileRevert extends ApiBase {
* @param $user User The user to check.
*/
protected function checkPermissions( $user ) {
+ $title = $this->file->getTitle();
$permissionErrors = array_merge(
- $this->file->getTitle()->getUserPermissionsErrors( 'edit' , $user ),
- $this->file->getTitle()->getUserPermissionsErrors( 'upload' , $user )
+ $title->getUserPermissionsErrors( 'edit' , $user ),
+ $title->getUserPermissionsErrors( 'upload' , $user )
);
if ( $permissionErrors ) {
@@ -91,15 +92,17 @@ class ApiFileRevert extends ApiBase {
if ( is_null( $title ) ) {
$this->dieUsageMsg( array( 'invalidtitle', $this->params['filename'] ) );
}
+ $localRepo = RepoGroup::singleton()->getLocalRepo();
+
// Check if the file really exists
- $this->file = wfLocalFile( $title );
+ $this->file = $localRepo->newFile( $title );
if ( !$this->file->exists() ) {
$this->dieUsageMsg( 'notanarticle' );
}
// Check if the archivename is valid for this file
$this->archiveName = $this->params['archivename'];
- $oldFile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $this->archiveName );
+ $oldFile = $localRepo->newFromArchiveName( $title, $this->archiveName );
if ( !$oldFile->exists() ) {
$this->dieUsageMsg( 'filerevert-badversion' );
}
@@ -126,21 +129,38 @@ class ApiFileRevert extends ApiBase {
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true,
),
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
);
}
public function getParamDescription() {
- $params = array(
- 'filename' => 'Target filename',
+ return array(
+ 'filename' => 'Target filename without the File: prefix',
'token' => 'Edit token. You can get one of these through prop=info',
'comment' => 'Upload comment',
'archivename' => 'Archive name of the revision to revert to',
);
+ }
- return $params;
-
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'result' => array(
+ ApiBase::PROP_TYPE => array(
+ 'Success',
+ 'Failure'
+ )
+ ),
+ 'errors' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
}
public function getDescription() {
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index 1eee717a..8ad9b8ca 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -4,7 +4,7 @@
*
* Created on Sep 19, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -169,8 +169,10 @@ abstract class ApiFormatBase extends ApiBase {
<br />
<small>
You are looking at the HTML representation of the <?php echo( $this->mFormat ); ?> format.<br />
-HTML is good for debugging, but probably is not suitable for your application.<br />
-See <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or
+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 />
+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.
</small>
<?php
@@ -264,11 +266,12 @@ See <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or
$text = htmlspecialchars( $text );
// encode all comments or tags as safe blue strings
- $text = preg_replace( '/\&lt;(!--.*?--|.*?)\&gt;/', '<span style="color:blue;">&lt;\1&gt;</span>', $text );
+ $text = 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( "#(($protos).*?)(&quot;)?([ \\'\"<>\n]|&lt;|&gt;|&quot;)#", '<a href="\\1">\\1</a>\\3\\4', $text );
+ $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 ) {
@@ -329,7 +332,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
*/
public static function setResult( $result, $feed, $feedItems ) {
// Store output in the Result data.
- // This way we can check during execution if any error has occured
+ // This way we can check during execution if any error has occurred
// Disable size checking for this because we can't continue
// cleanly; size checking would cause more problems than it'd
// solve
@@ -374,7 +377,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
}
$feed->outFooter();
} else {
- // Error has occured, print something useful
+ // Error has occurred, print something useful
ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' );
}
}
diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php
index 92619f76..3d2a39ca 100644
--- a/includes/api/ApiFormatDbg.php
+++ b/includes/api/ApiFormatDbg.php
@@ -4,7 +4,7 @@
*
* Created on Oct 22, 2006
*
- * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2008 Roan Kattouw "<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
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index e728d057..acbc7d3b 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -4,7 +4,7 @@
*
* Created on Sep 19, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php
index 60552c40..fac2ca58 100644
--- a/includes/api/ApiFormatPhp.php
+++ b/includes/api/ApiFormatPhp.php
@@ -4,7 +4,7 @@
*
* Created on Oct 22, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php
index db81aacd..184f0a34 100644
--- a/includes/api/ApiFormatRaw.php
+++ b/includes/api/ApiFormatRaw.php
@@ -4,7 +4,7 @@
*
* Created on Feb 2, 2009
*
- * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2009 Roan Kattouw "<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
diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php
index e26b82b0..71414593 100644
--- a/includes/api/ApiFormatTxt.php
+++ b/includes/api/ApiFormatTxt.php
@@ -4,7 +4,7 @@
*
* Created on Oct 22, 2006
*
- * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2008 Roan Kattouw "<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
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index 1bc9d025..65056e44 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -4,7 +4,7 @@
*
* Created on Oct 22, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index 8f4abc15..5ccf1859 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -4,7 +4,7 @@
*
* Created on Sep 19, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -83,16 +83,40 @@ class ApiFormatXml extends ApiFormatBase {
/**
* This method takes an array and converts it to XML.
+ *
* There are several noteworthy cases:
*
- * If array contains a key '_element', then the code assumes that ALL other keys are not important and replaces them with the value['_element'].
- * Example: name='root', value = array( '_element'=>'page', 'x', 'y', 'z') creates <root> <page>x</page> <page>y</page> <page>z</page> </root>
+ * If array contains a key '_element', then the code assumes that ALL
+ * other keys are not important and replaces them with the
+ * value['_element'].
+ *
+ * @par Example:
+ * @verbatim
+ * name='root', value = array( '_element'=>'page', 'x', 'y', 'z')
+ * @endverbatim
+ * creates:
+ * @verbatim
+ * <root> <page>x</page> <page>y</page> <page>z</page> </root>
+ * @endverbatim
+ *
+ * If any of the array's element key is '*', then the code treats all
+ * other key->value pairs as attributes, and the value['*'] as the
+ * element's content.
+ *
+ * @par Example:
+ * @verbatim
+ * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10)
+ * @endverbatim
+ * creates:
+ * @verbatim
+ * <root lang='en' id='10'>text</root>
+ * @endverbatim
*
- * If any of the array's element key is '*', then the code treats all other key->value pairs as attributes, and the value['*'] as the element's content.
- * Example: name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) creates <root lang='en' id='10'>text</root>
+ * Finally neither key is found, all keys become element names, and values
+ * become element content.
*
- * If neither key is found, all keys become element names, and values become element content.
- * The method is recursive, so the same rules apply to any sub-arrays.
+ * @note The method is recursive, so the same rules apply to any
+ * sub-arrays.
*
* @param $elemName
* @param $elemValue
@@ -215,7 +239,8 @@ 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',
+ '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'
);
}
diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php
index dbcdb21c..730ad8ea 100644
--- a/includes/api/ApiFormatYaml.php
+++ b/includes/api/ApiFormatYaml.php
@@ -4,7 +4,7 @@
*
* Created on Sep 19, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php
index 97da786b..2b5de21a 100644
--- a/includes/api/ApiHelp.php
+++ b/includes/api/ApiHelp.php
@@ -4,7 +4,7 @@
*
* Created on Sep 6, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php
index ade9f1f3..637c1fff 100644
--- a/includes/api/ApiImport.php
+++ b/includes/api/ApiImport.php
@@ -4,7 +4,7 @@
*
* Created on Feb 4, 2009
*
- * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2009 Roan Kattouw "<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
@@ -68,6 +68,12 @@ class ApiImport extends ApiBase {
if ( isset( $params['namespace'] ) ) {
$importer->setTargetNamespace( $params['namespace'] );
}
+ if ( isset( $params['rootpage'] ) ) {
+ $statusRootPage = $importer->setTargetRootPage( $params['rootpage'] );
+ if( !$statusRootPage->isGood() ) {
+ $this->dieUsageMsg( $statusRootPage->getErrorsArray() );
+ }
+ }
$reporter = new ApiImportReporter(
$importer,
$isUpload,
@@ -98,7 +104,10 @@ class ApiImport extends ApiBase {
public function getAllowedParams() {
global $wgImportSources;
return array(
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'summary' => null,
'xml' => null,
'interwikisource' => array(
@@ -109,7 +118,8 @@ class ApiImport extends ApiBase {
'templates' => false,
'namespace' => array(
ApiBase::PARAM_TYPE => 'namespace'
- )
+ ),
+ 'rootpage' => null,
);
}
@@ -123,6 +133,18 @@ class ApiImport extends ApiBase {
'fullhistory' => 'For interwiki imports: import the full history, not just the current version',
'templates' => 'For interwiki imports: import all included templates as well',
'namespace' => 'For interwiki imports: import to this namespace',
+ 'rootpage' => 'Import as subpage of this page',
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ ApiBase::PROP_LIST => true,
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string',
+ 'revisions' => 'integer'
+ )
);
}
@@ -141,6 +163,8 @@ class ApiImport extends ApiBase {
array( 'cantimport-upload' ),
array( 'import-unknownerror', 'source' ),
array( 'import-unknownerror', 'result' ),
+ array( 'import-rootpage-nosubpage', 'namespace' ),
+ array( 'import-rootpage-invalid' ),
) );
}
@@ -186,8 +210,16 @@ class ApiImportReporter extends ImportReporter {
function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) {
// Add a result entry
$r = array();
- ApiQueryBase::addTitleInfo( $r, $title );
- $r['revisions'] = intval( $successCount );
+
+ if ( $title === null ) {
+ # Invalid or non-importable title
+ $r['title'] = $pageInfo['title'];
+ $r['invalid'] = '';
+ } else {
+ ApiQueryBase::addTitleInfo( $r, $title );
+ $r['revisions'] = intval( $successCount );
+ }
+
$this->mResultArr[] = $r;
// Piggyback on the parent to do the logging
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index aa570cbc..1f91fe92 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -4,7 +4,7 @@
*
* Created on Sep 19, 2006
*
- * Copyright © 2006-2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com,
+ * Copyright © 2006-2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
* Daniel Cannon (cannon dot danielc at gmail dot com)
*
* This program is free software; you can redistribute it and/or modify
@@ -79,6 +79,8 @@ class ApiLogin extends ApiBase {
$user->setOption( 'rememberpassword', 1 );
$user->setCookies( $this->getRequest() );
+ ApiQueryInfo::resetTokenCache();
+
// Run hooks.
// @todo FIXME: Split back and frontend from this hook.
// @todo FIXME: This hook should be placed in the backend
@@ -181,6 +183,66 @@ class ApiLogin extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'result' => array(
+ ApiBase::PROP_TYPE => array(
+ 'Success',
+ 'NeedToken',
+ 'WrongToken',
+ 'NoName',
+ 'Illegal',
+ 'WrongPluginPass',
+ 'NotExists',
+ 'WrongPass',
+ 'EmptyPass',
+ 'CreateBlocked',
+ 'Throttled',
+ 'Blocked',
+ 'Aborted'
+ )
+ ),
+ 'lguserid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'lgusername' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'lgtoken' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'cookieprefix' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'sessionid' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'token' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'details' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'wait' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'reason' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return array(
'Log in and get the authentication tokens. ',
diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php
index 81a054a6..b2f634d0 100644
--- a/includes/api/ApiLogout.php
+++ b/includes/api/ApiLogout.php
@@ -4,7 +4,7 @@
*
* Created on Jan 4, 2008
*
- * Copyright © 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com,
+ * Copyright © 2008 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -54,6 +54,10 @@ class ApiLogout extends ApiBase {
return array();
}
+ public function getResultProperties() {
+ return array();
+ }
+
public function getParamDescription() {
return array();
}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index fa95cfca..35febd95 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -4,7 +4,7 @@
*
* Created on Sep 4, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -61,9 +61,11 @@ class ApiMain extends ApiBase {
'paraminfo' => 'ApiParamInfo',
'rsd' => 'ApiRsd',
'compare' => 'ApiComparePages',
+ 'tokens' => 'ApiTokens',
// Write modules
'purge' => 'ApiPurge',
+ 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
'rollback' => 'ApiRollback',
'delete' => 'ApiDelete',
'undelete' => 'ApiUndelete',
@@ -79,6 +81,7 @@ class ApiMain extends ApiBase {
'patrol' => 'ApiPatrol',
'import' => 'ApiImport',
'userrights' => 'ApiUserrights',
+ 'options' => 'ApiOptions',
);
/**
@@ -352,6 +355,12 @@ class ApiMain extends ApiBase {
* have been accumulated, and replace it with an error message and a help screen.
*/
protected function executeActionWithErrorHandling() {
+ // Verify the CORS header before executing the action
+ if ( !$this->handleCORS() ) {
+ // handleCORS() has sent a 403, abort
+ return;
+ }
+
// In case an error occurs during data output,
// clear the output buffer and print just the error information
ob_start();
@@ -359,8 +368,11 @@ class ApiMain extends ApiBase {
try {
$this->executeAction();
} catch ( Exception $e ) {
+ // Allow extra cleanup and logging
+ wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
+
// Log it
- if ( $e instanceof MWException ) {
+ if ( !( $e instanceof UsageException ) ) {
wfDebugLog( 'exception', $e->getLogMessage() );
}
@@ -384,7 +396,7 @@ class ApiMain extends ApiBase {
// Reset and print just the error message
ob_clean();
- // If the error occured during printing, do a printer->profileOut()
+ // If the error occurred during printing, do a printer->profileOut()
$this->mPrinter->safeProfileOut();
$this->printResult( true );
}
@@ -400,9 +412,101 @@ class ApiMain extends ApiBase {
ob_end_flush();
}
+ /**
+ * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
+ *
+ * If no origin parameter is present, nothing happens.
+ * If an origin parameter is present but doesn't match the Origin header, a 403 status code
+ * is set and false is returned.
+ * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
+ * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
+ * headers are set.
+ *
+ * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
+ */
+ protected function handleCORS() {
+ global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions;
+
+ $originParam = $this->getParameter( 'origin' ); // defaults to null
+ if ( $originParam === null ) {
+ // No origin parameter, nothing to do
+ return true;
+ }
+
+ $request = $this->getRequest();
+ $response = $request->response();
+ // Origin: header is a space-separated list of origins, check all of them
+ $originHeader = $request->getHeader( 'Origin' );
+ if ( $originHeader === false ) {
+ $origins = array();
+ } else {
+ $origins = explode( ' ', $originHeader );
+ }
+ if ( !in_array( $originParam, $origins ) ) {
+ // origin parameter set but incorrect
+ // Send a 403 response
+ $message = HttpStatus::getMessage( 403 );
+ $response->header( "HTTP/1.1 403 $message", true, 403 );
+ $response->header( 'Cache-Control: no-cache' );
+ echo "'origin' parameter does not match Origin header\n";
+ return false;
+ }
+ if ( self::matchOrigin( $originParam, $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions ) ) {
+ $response->header( "Access-Control-Allow-Origin: $originParam" );
+ $response->header( 'Access-Control-Allow-Credentials: true' );
+ $this->getOutput()->addVaryHeader( 'Origin' );
+ }
+ return true;
+ }
+
+ /**
+ * 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
+ * @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 ) {
+ foreach ( $rules as $rule ) {
+ if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
+ // Rule matches, check exceptions
+ foreach ( $exceptions as $exc ) {
+ if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Helper function to convert wildcard string into a regex
+ * '*' => '.*?'
+ * '?' => '.'
+ *
+ * @param $wildcard string String with wildcards
+ * @return string Regular expression
+ */
+ protected static function wildcardToRegex( $wildcard ) {
+ $wildcard = preg_quote( $wildcard, '/' );
+ $wildcard = str_replace(
+ array( '\*', '\?' ),
+ array( '.*?', '.' ),
+ $wildcard
+ );
+ return "/https?:\/\/$wildcard/";
+ }
+
protected function sendCacheHeaders() {
global $wgUseXVO, $wgVaryOnXFP;
$response = $this->getRequest()->response();
+ $out = $this->getOutput();
+
+ if ( $wgVaryOnXFP ) {
+ $out->addVaryHeader( 'X-Forwarded-Proto' );
+ }
if ( $this->mCacheMode == 'private' ) {
$response->header( 'Cache-Control: private' );
@@ -410,13 +514,9 @@ class ApiMain extends ApiBase {
}
if ( $this->mCacheMode == 'anon-public-user-private' ) {
- $xfp = $wgVaryOnXFP ? ', X-Forwarded-Proto' : '';
- $response->header( 'Vary: Accept-Encoding, Cookie' . $xfp );
+ $out->addVaryHeader( 'Cookie' );
+ $response->header( $out->getVaryHeader() );
if ( $wgUseXVO ) {
- $out = $this->getOutput();
- if ( $wgVaryOnXFP ) {
- $out->addVaryHeader( 'X-Forwarded-Proto' );
- }
$response->header( $out->getXVO() );
if ( $out->haveCacheVaryCookies() ) {
// Logged in, mark this request private
@@ -433,12 +533,9 @@ class ApiMain extends ApiBase {
}
// Send public headers
- if ( $wgVaryOnXFP ) {
- $response->header( 'Vary: Accept-Encoding, X-Forwarded-Proto' );
- if ( $wgUseXVO ) {
- // Bleeeeegh. Our header setting system sucks
- $response->header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip, X-Forwarded-Proto' );
- }
+ $response->header( $out->getVaryHeader() );
+ if ( $wgUseXVO ) {
+ $response->header( $out->getXVO() );
}
// If nobody called setCacheMaxAge(), use the (s)maxage parameters
@@ -605,7 +702,7 @@ class ApiMain extends ApiBase {
if ( !isset( $moduleParams['token'] ) ) {
$this->dieUsageMsg( array( 'missingparam', 'token' ) );
} else {
- if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getRequest() ) ) {
+ if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getContext()->getRequest() ) ) {
$this->dieUsageMsg( 'sessionfailure' );
}
}
@@ -664,6 +761,12 @@ class ApiMain extends ApiBase {
$this->dieReadOnly();
}
}
+
+ // Allow extensions to stop execution for arbitrary reasons.
+ $message = false;
+ if( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
+ $this->dieUsageMsg( $message );
+ }
}
/**
@@ -713,6 +816,9 @@ class ApiMain extends ApiBase {
$module->profileOut();
if ( !$this->mInternalMode ) {
+ //append Debug information
+ MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
+
// Print result data
$this->printResult( false );
}
@@ -779,6 +885,7 @@ class ApiMain extends ApiBase {
),
'requestid' => null,
'servedby' => false,
+ 'origin' => null,
);
}
@@ -804,6 +911,12 @@ class ApiMain extends ApiBase {
'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
'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 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.',
+ ),
);
}
@@ -871,11 +984,11 @@ class ApiMain extends ApiBase {
protected function getCredits() {
return array(
'API developers:',
- ' Roan Kattouw <Firstname>.<Lastname>@gmail.com (lead developer Sep 2007-present)',
+ ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-present)',
' Victor Vasiliev - vasilvv at gee mail dot com',
' Bryan Tong Minh - bryan . tongminh @ gmail . com',
' Sam Reed - sam @ reedyboy . net',
- ' Yuri Astrakhan <Firstname><Lastname>@gmail.com (creator, lead developer Sep 2006-Sep 2007)',
+ ' Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007)',
'',
'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org',
'or file a bug report at https://bugzilla.wikimedia.org/'
@@ -1061,11 +1174,21 @@ class ApiMain extends ApiBase {
*
* @ingroup API
*/
-class UsageException extends Exception {
+class UsageException extends MWException {
private $mCodestr;
+
+ /**
+ * @var null|array
+ */
private $mExtraData;
+ /**
+ * @param $message string
+ * @param $codestr string
+ * @param $code int
+ * @param $extradata array|null
+ */
public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
parent::__construct( $message, $code );
$this->mCodestr = $codestr;
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
index f0a25e4a..9d73562b 100644
--- a/includes/api/ApiMove.php
+++ b/includes/api/ApiMove.php
@@ -4,7 +4,7 @@
*
* Created on Oct 31, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -37,9 +37,6 @@ class ApiMove extends ApiBase {
public function execute() {
$user = $this->getUser();
$params = $this->extractRequestParams();
- if ( is_null( $params['reason'] ) ) {
- $params['reason'] = '';
- }
$this->requireOnlyOneParameter( $params, 'from', 'fromid' );
@@ -78,6 +75,7 @@ class ApiMove extends ApiBase {
}
// Move the page
+ $toTitleExists = $toTitle->exists();
$retval = $fromTitle->moveTo( $toTitle, true, $params['reason'], !$params['noredirect'] );
if ( $retval !== true ) {
$this->dieUsageMsg( reset( $retval ) );
@@ -87,13 +85,20 @@ class ApiMove extends ApiBase {
if ( !$params['noredirect'] || !$user->isAllowed( 'suppressredirect' ) ) {
$r['redirectcreated'] = '';
}
+ if( $toTitleExists ) {
+ $r['moveoverredirect'] = '';
+ }
// Move the talk page
if ( $params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage() ) {
+ $toTalkExists = $toTalk->exists();
$retval = $fromTalk->moveTo( $toTalk, true, $params['reason'], !$params['noredirect'] );
if ( $retval === true ) {
$r['talkfrom'] = $fromTalk->getPrefixedText();
$r['talkto'] = $toTalk->getPrefixedText();
+ if( $toTalkExists ) {
+ $r['talkmoveoverredirect'] = '';
+ }
} else {
// We're not gonna dieUsage() on failure, since we already changed something
$parsed = $this->parseMsg( reset( $retval ) );
@@ -180,8 +185,11 @@ class ApiMove extends ApiBase {
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
),
- 'token' => null,
- 'reason' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
+ 'reason' => '',
'movetalk' => false,
'movesubpages' => false,
'noredirect' => false,
@@ -213,7 +221,7 @@ class ApiMove extends ApiBase {
'fromid' => "Page ID of the page you want to move. Cannot be used together with {$p}from",
'to' => 'Title you want to rename the page to',
'token' => 'A move token previously retrieved through prop=info',
- 'reason' => 'Reason for the move (optional)',
+ 'reason' => 'Reason for the move',
'movetalk' => 'Move the talk page, if it exists',
'movesubpages' => 'Move subpages, if applicable',
'noredirect' => 'Don\'t create a redirect',
@@ -224,6 +232,35 @@ class ApiMove extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'from' => 'string',
+ 'to' => 'string',
+ 'reason' => 'string',
+ 'redirectcreated' => 'boolean',
+ 'moveoverredirect' => 'boolean',
+ 'talkfrom' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'talkto' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'talkmoveoverredirect' => 'boolean',
+ 'talkmove-error-code' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'talkmove-error-info' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'Move a page';
}
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index 0727cffd..ef562741 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -4,7 +4,7 @@
*
* Created on Oct 13, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -45,7 +45,7 @@ class ApiOpenSearch extends ApiBase {
$namespaces = $params['namespace'];
$suggest = $params['suggest'];
- // MWSuggest or similar hit
+ // Some script that was loaded regardless of wgEnableOpenSearchSuggest, likely cached.
if ( $suggest && !$wgEnableOpenSearchSuggest ) {
$searches = array();
} else {
diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php
new file mode 100644
index 00000000..265c2ccb
--- /dev/null
+++ b/includes/api/ApiOptions.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ *
+ *
+ * Created on Apr 15, 2012
+ *
+ * Copyright © 2012 Szymon Świerkosz beau@adres.pl
+ *
+ * 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
+ */
+
+/**
+* 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.
+ */
+ public function execute() {
+ $user = $this->getUser();
+
+ if ( $user->isAnon() ) {
+ $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' );
+ }
+
+ $params = $this->extractRequestParams();
+ $changed = false;
+
+ if ( isset( $params['optionvalue'] ) && !isset( $params['optionname'] ) ) {
+ $this->dieUsageMsg( array( 'missingparam', 'optionname' ) );
+ }
+
+ if ( $params['reset'] ) {
+ $user->resetOptions();
+ $changed = true;
+ }
+
+ $changes = array();
+ if ( count( $params['change'] ) ) {
+ foreach ( $params['change'] as $entry ) {
+ $array = explode( '=', $entry, 2 );
+ $changes[$array[0]] = isset( $array[1] ) ? $array[1] : null;
+ }
+ }
+ if ( isset( $params['optionname'] ) ) {
+ $newValue = isset( $params['optionvalue'] ) ? $params['optionvalue'] : null;
+ $changes[$params['optionname']] = $newValue;
+ }
+ if ( !$changed && !count( $changes ) ) {
+ $this->dieUsage( 'No changes were requested', 'nochanges' );
+ }
+
+ $prefs = Preferences::getPreferences( $user, $this->getContext() );
+ foreach ( $changes as $key => $value ) {
+ if ( !isset( $prefs[$key] ) ) {
+ $this->setWarning( "Not a valid preference: $key" );
+ continue;
+ }
+ $field = HTMLForm::loadInputFromParameters( $key, $prefs[$key] );
+ $validation = $field->validate( $value, $user->getOptions() );
+ if ( $validation === true ) {
+ $user->setOption( $key, $value );
+ $changed = true;
+ } else {
+ $this->setWarning( "Validation error for '$key': $validation" );
+ }
+ }
+
+ if ( $changed ) {
+ // Commit changes
+ $user->saveSettings();
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(), 'success' );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
+ 'reset' => false,
+ 'change' => array(
+ ApiBase::PARAM_ISMULTI => true,
+ ),
+ 'optionname' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ),
+ 'optionvalue' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ),
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ '*' => array(
+ ApiBase::PROP_TYPE => array(
+ 'success'
+ )
+ )
+ )
+ );
+ }
+
+ 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',
+ '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';
+ }
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot change preferences' ),
+ array( 'code' => 'nochanges', 'info' => 'No changes were requested' ),
+ ) );
+ }
+
+ public function needsToken() {
+ return true;
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Options';
+ }
+
+ public function getExamples() {
+ return array(
+ 'api.php?action=options&reset=&token=123ABC',
+ 'api.php?action=options&change=skin=vector|hideminor=1&token=123ABC',
+ '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 7b84c473..0f5be6b2 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 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
@@ -52,7 +52,7 @@ class ApiPageSet extends ApiQueryBase {
/**
* Constructor
- * @param $query ApiQueryBase
+ * @param $query ApiBase
* @param $resolveRedirects bool Whether redirects should be resolved
* @param $convertTitles bool
*/
@@ -266,8 +266,8 @@ class ApiPageSet extends ApiQueryBase {
}
/**
- * Returns the number of revisions (requested with revids= parameter)\
- * @return int
+ * Returns the number of revisions (requested with revids= parameter).
+ * @return int Number of revisions.
*/
public function getRevisionCount() {
return count( $this->getRevisionIDs() );
@@ -342,7 +342,7 @@ class ApiPageSet extends ApiQueryBase {
/**
* Populate this PageSet from a rowset returned from the database
- * @param $db Database object
+ * @param $db DatabaseBase object
* @param $queryResult ResultWrapper Query result object
*/
public function populateFromQueryResult( $db, $queryResult ) {
@@ -367,7 +367,7 @@ class ApiPageSet extends ApiQueryBase {
*/
public function processDbRow( $row ) {
// Store Title object in various data structures
- $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $title = Title::newFromRow( $row );
$pageId = intval( $row->page_id );
$this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
@@ -481,6 +481,7 @@ class ApiPageSet extends ApiQueryBase {
ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' );
}
+ $usernames = array();
if ( $res ) {
foreach ( $res as $row ) {
$pageId = intval( $row->page_id );
@@ -496,6 +497,11 @@ class ApiPageSet extends ApiQueryBase {
// Store any extra fields requested by modules
$this->processDbRow( $row );
+
+ // Need gender information
+ if( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) {
+ $usernames[] = $row->page_title;
+ }
}
}
@@ -510,6 +516,11 @@ class ApiPageSet extends ApiQueryBase {
$this->mMissingTitles[$this->mFakePageId] = $title;
$this->mFakePageId--;
$this->mTitles[] = $title;
+
+ // need gender information
+ if( MWNamespace::hasGenderDistinction( $ns ) ) {
+ $usernames[] = $dbkey;
+ }
}
}
} else {
@@ -521,6 +532,10 @@ class ApiPageSet extends ApiQueryBase {
}
}
}
+
+ // Get gender information
+ $genderCache = GenderCache::singleton();
+ $genderCache->doQuery( $usernames, __METHOD__ );
}
/**
@@ -664,6 +679,9 @@ class ApiPageSet extends ApiQueryBase {
* @return LinkBatch
*/
private function processTitlesArray( $titles ) {
+ $genderCache = GenderCache::singleton();
+ $genderCache->doTitlesArray( $titles, __METHOD__ );
+
$linkBatch = new LinkBatch();
foreach ( $titles as $title ) {
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
index f2263476..343a2625 100644
--- a/includes/api/ApiParamInfo.php
+++ b/includes/api/ApiParamInfo.php
@@ -4,7 +4,7 @@
*
* Created on Dec 01, 2007
*
- * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2008 Roan Kattouw "<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
@@ -251,6 +251,62 @@ class ApiParamInfo extends ApiBase {
}
$result->setIndexedTagName( $retval['parameters'], 'param' );
+ $props = $obj->getFinalResultProperties();
+ $listResult = null;
+ if ( $props !== false ) {
+ $retval['props'] = array();
+
+ foreach ( $props as $prop => $properties ) {
+ $propResult = array();
+ if ( $prop == ApiBase::PROP_LIST ) {
+ $listResult = $properties;
+ continue;
+ }
+ if ( $prop != ApiBase::PROP_ROOT ) {
+ $propResult['name'] = $prop;
+ }
+ $propResult['properties'] = array();
+
+ foreach ( $properties as $name => $p ) {
+ $propertyResult = array();
+
+ $propertyResult['name'] = $name;
+
+ if ( !is_array( $p ) ) {
+ $p = array( ApiBase::PROP_TYPE => $p );
+ }
+
+ $propertyResult['type'] = $p[ApiBase::PROP_TYPE];
+
+ if ( is_array( $propertyResult['type'] ) ) {
+ $propertyResult['type'] = array_values( $propertyResult['type'] );
+ $result->setIndexedTagName( $propertyResult['type'], 't' );
+ }
+
+ $nullable = null;
+ if ( isset( $p[ApiBase::PROP_NULLABLE] ) ) {
+ $nullable = $p[ApiBase::PROP_NULLABLE];
+ }
+
+ if ( $nullable === true ) {
+ $propertyResult['nullable'] = '';
+ }
+
+ $propResult['properties'][] = $propertyResult;
+ }
+
+ $result->setIndexedTagName( $propResult['properties'], 'property' );
+ $retval['props'][] = $propResult;
+ }
+
+ // default is true for query modules, false for other modules, overriden by ApiBase::PROP_LIST
+ if ( $listResult === true || ( $listResult !== false && $obj instanceof ApiQueryBase ) ) {
+ $retval['listresult'] = '';
+ }
+
+ $result->setIndexedTagName( $retval['props'], 'prop' );
+ }
+
// Errors
$retval['errors'] = $this->parseErrors( $obj->getPossibleErrors() );
$result->setIndexedTagName( $retval['errors'], 'error' );
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index 893491b9..db6e2bb8 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -2,7 +2,7 @@
/**
* Created on Dec 01, 2007
*
- * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright © 2007 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
@@ -59,19 +59,15 @@ class ApiParse extends ApiBase {
// The parser needs $wgTitle to be set, apparently the
// $title parameter in Parser::parse isn't enough *sigh*
// TODO: Does this still need $wgTitle?
- global $wgParser, $wgTitle, $wgLang;
+ global $wgParser, $wgTitle;
// Currently unnecessary, code to act as a safeguard against any change in current behaviour of uselang breaks
$oldLang = null;
- if ( isset( $params['uselang'] ) && $params['uselang'] != $wgLang->getCode() ) {
- $oldLang = $wgLang; // Backup wgLang
- $wgLang = Language::factory( $params['uselang'] );
+ if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) {
+ $oldLang = $this->getContext()->getLanguage(); // Backup language
+ $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) );
}
- $popts = ParserOptions::newFromContext( $this->getContext() );
- $popts->setTidy( true );
- $popts->enableLimitReport( !$params['disablepp'] );
-
$redirValues = null;
// Return result
@@ -89,13 +85,15 @@ class ApiParse extends ApiBase {
}
$titleObj = $rev->getTitle();
-
$wgTitle = $titleObj;
+ $pageObj = WikiPage::factory( $titleObj );
+ $popts = $pageObj->makeParserOptions( $this->getContext() );
+ $popts->enableLimitReport( !$params['disablepp'] );
// If for some reason the "oldid" is actually the current revision, it may be cached
if ( $titleObj->getLatestRevID() === intval( $oldid ) ) {
// May get from/save to parser cache
- $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid,
+ $p_result = $this->getParsedSectionOrText( $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() );
@@ -129,32 +127,26 @@ class ApiParse extends ApiBase {
foreach ( (array)$redirValues as $r ) {
$to = $r['to'];
}
- $titleObj = Title::newFromText( $to );
- } else {
- if ( !is_null ( $pageid ) ) {
- $reqParams['pageids'] = $pageid;
- $titleObj = Title::newFromID( $pageid );
- } else { // $page
- $to = $page;
- $titleObj = Title::newFromText( $to );
- }
- }
- if ( !is_null ( $pageid ) ) {
- if ( !$titleObj ) {
- // Still throw nosuchpageid error if pageid was provided
- $this->dieUsageMsg( array( 'nosuchpageid', $pageid ) );
- }
- } elseif ( !$titleObj || !$titleObj->exists() ) {
- $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
+ $pageParams = array( 'title' => $to );
+ } elseif ( !is_null( $pageid ) ) {
+ $pageParams = array( 'pageid' => $pageid );
+ } else { // $page
+ $pageParams = array( 'title' => $page );
}
+
+ $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
+ $titleObj = $pageObj->getTitle();
$wgTitle = $titleObj;
if ( isset( $prop['revid'] ) ) {
- $oldid = $titleObj->getLatestRevID();
+ $oldid = $pageObj->getLatest();
}
+ $popts = $pageObj->makeParserOptions( $this->getContext() );
+ $popts->enableLimitReport( !$params['disablepp'] );
+
// Potentially cached
- $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid,
+ $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid,
isset( $prop['wikitext'] ) ) ;
}
} else { // Not $oldid, $pageid, $page. Hence based on $text
@@ -168,6 +160,10 @@ class ApiParse extends ApiBase {
$this->dieUsageMsg( array( 'invalidtitle', $title ) );
}
$wgTitle = $titleObj;
+ $pageObj = WikiPage::factory( $titleObj );
+
+ $popts = $pageObj->makeParserOptions( $this->getContext() );
+ $popts->enableLimitReport( !$params['disablepp'] );
if ( $this->section !== false ) {
$this->text = $this->getSectionText( $this->text, $titleObj->getText() );
@@ -285,6 +281,21 @@ class ApiParse extends ApiBase {
$result->setContent( $result_array['psttext'], $this->pstText );
}
}
+ if ( isset( $prop['properties'] ) ) {
+ $result_array['properties'] = $this->formatProperties( $p_result->getProperties() );
+ }
+
+ if ( $params['generatexml'] ) {
+ $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS );
+ $dom = $wgParser->preprocessToDom( $this->text );
+ if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+ $xml = $dom->saveXML();
+ } else {
+ $xml = $dom->__toString();
+ }
+ $result_array['parsetree'] = array();
+ $result->setContent( $result_array['parsetree'], $xml );
+ }
$result_mapping = array(
'redirects' => 'r',
@@ -297,37 +308,39 @@ class ApiParse extends ApiBase {
'iwlinks' => 'iw',
'sections' => 's',
'headitems' => 'hi',
+ 'properties' => 'pp',
);
$this->setIndexedTagNames( $result_array, $result_mapping );
$result->addValue( null, $this->getModuleName(), $result_array );
if ( !is_null( $oldLang ) ) {
- $wgLang = $oldLang; // Reset $wgLang to $oldLang
+ $this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang
}
}
/**
- * @param $titleObj Title
+ * @param $page WikiPage
* @param $popts ParserOptions
* @param $pageId Int
* @param $getWikitext Bool
* @return ParserOutput
*/
- private function getParsedSectionOrText( $titleObj, $popts, $pageId = null, $getWikitext = false ) {
+ private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) {
global $wgParser;
- $page = WikiPage::factory( $titleObj );
-
if ( $this->section !== false ) {
$this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
- ? 'page id ' . $pageId : $titleObj->getText() );
+ ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() );
// Not cached (save or load)
- return $wgParser->parse( $this->text, $titleObj, $popts );
+ return $wgParser->parse( $this->text, $page->getTitle(), $popts );
} else {
// Try the parser cache first
// getParserOutput will save to Parser cache if able
$pout = $page->getParserOutput( $popts );
+ if ( !$pout ) {
+ $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
+ }
if ( $getWikitext ) {
$this->text = $page->getRawText();
}
@@ -394,19 +407,19 @@ class ApiParse extends ApiBase {
return '';
}
- $s = htmlspecialchars( wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' ) );
+ $s = htmlspecialchars( wfMessage( 'otherlanguages' )->text() . wfMessage( 'colon-separator' )->text() );
$langs = array();
foreach ( $languages as $l ) {
$nt = Title::newFromText( $l );
- $text = $wgContLang->getLanguageName( $nt->getInterwiki() );
+ $text = Language::fetchLanguageName( $nt->getInterwiki() );
$langs[] = Html::element( 'a',
array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ),
$text == '' ? $l : $text );
}
- $s .= implode( htmlspecialchars( wfMsgExt( 'pipe-separator', 'escapenoentities' ) ), $langs );
+ $s .= implode( wfMessage( 'pipe-separator' )->escaped(), $langs );
if ( $wgContLang->isRTL() ) {
$s = Html::rawElement( 'span', array( 'dir' => "LTR" ), $s );
@@ -461,6 +474,17 @@ class ApiParse extends ApiBase {
return $result;
}
+ private function formatProperties( $properties ) {
+ $result = array();
+ foreach ( $properties as $name => $value ) {
+ $entry = array();
+ $entry['name'] = $name;
+ $this->getResult()->setContent( $entry, $value );
+ $result[] = $entry;
+ }
+ return $result;
+ }
+
private function formatCss( $css ) {
$result = array();
foreach ( $css as $file => $link ) {
@@ -496,7 +520,7 @@ class ApiParse extends ApiBase {
ApiBase::PARAM_TYPE => 'integer',
),
'prop' => array(
- ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle',
+ ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle|iwlinks|properties',
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => array(
'text',
@@ -515,6 +539,7 @@ class ApiParse extends ApiBase {
'headhtml',
'iwlinks',
'wikitext',
+ 'properties',
)
),
'pst' => false,
@@ -522,6 +547,7 @@ class ApiParse extends ApiBase {
'uselang' => null,
'section' => null,
'disablepp' => false,
+ 'generatexml' => false,
);
}
@@ -553,6 +579,7 @@ class ApiParse extends ApiBase {
' headhtml - Gives parsed <head> of the page',
' iwlinks - Gives interwiki links in the parsed wikitext',
' wikitext - Gives the original wikitext that was parsed',
+ ' properties - Gives various properties defined in the parsed wikitext',
),
'pst' => array(
'Do a pre-save transform on the input before parsing it',
@@ -565,11 +592,15 @@ class ApiParse extends ApiBase {
'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',
);
}
public function getDescription() {
- return 'Parses wikitext and returns parser output';
+ return array(
+ 'Parses wikitext and returns parser output',
+ 'See the various prop-Modules of action=query to get information from the current version of a page',
+ );
}
public function getPossibleErrors() {
diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php
index 1332f263..cb5e081a 100644
--- a/includes/api/ApiPatrol.php
+++ b/includes/api/ApiPatrol.php
@@ -65,7 +65,10 @@ class ApiPatrol extends ApiBase {
public function getAllowedParams() {
return array(
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'rcid' => array(
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_REQUIRED => true
@@ -80,6 +83,16 @@ class ApiPatrol extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'rcid' => 'integer',
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Patrol a page or revision';
}
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
index fb225d86..b3ca67e6 100644
--- a/includes/api/ApiProtect.php
+++ b/includes/api/ApiProtect.php
@@ -4,7 +4,7 @@
*
* Created on Sep 1, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -37,10 +37,8 @@ class ApiProtect extends ApiBase {
global $wgRestrictionLevels;
$params = $this->extractRequestParams();
- $titleObj = Title::newFromText( $params['title'] );
- if ( !$titleObj ) {
- $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
- }
+ $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
+ $titleObj = $pageObj->getTitle();
$errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() );
if ( $errors ) {
@@ -58,7 +56,7 @@ class ApiProtect extends ApiBase {
}
$restrictionTypes = $titleObj->getRestrictionTypes();
- $dbr = wfGetDB( DB_SLAVE );
+ $db = $this->getDB();
$protections = array();
$expiryarray = array();
@@ -82,7 +80,7 @@ class ApiProtect extends ApiBase {
}
if ( in_array( $expiry[$i], array( 'infinite', 'indefinite', 'never' ) ) ) {
- $expiryarray[$p[0]] = $dbr->getInfinity();
+ $expiryarray[$p[0]] = $db->getInfinity();
} else {
$exp = strtotime( $expiry[$i] );
if ( $exp < 0 || !$exp ) {
@@ -96,7 +94,7 @@ class ApiProtect extends ApiBase {
$expiryarray[$p[0]] = $exp;
}
$resultProtections[] = array( $p[0] => $protections[$p[0]],
- 'expiry' => ( $expiryarray[$p[0]] == $dbr->getInfinity() ?
+ 'expiry' => ( $expiryarray[$p[0]] == $db->getInfinity() ?
'infinite' :
wfTimestamp( TS_ISO_8601, $expiryarray[$p[0]] ) ) );
}
@@ -106,7 +104,6 @@ class ApiProtect extends ApiBase {
$watch = $params['watch'] ? 'watch' : $params['watchlist'];
$this->setWatch( $watch, $titleObj );
- $pageObj = WikiPage::factory( $titleObj );
$status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() );
if ( !$status->isOK() ) {
@@ -138,9 +135,14 @@ class ApiProtect extends ApiBase {
return array(
'title' => array(
ApiBase::PARAM_TYPE => 'string',
+ ),
+ 'pageid' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ),
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
),
- 'token' => null,
'protections' => array(
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_REQUIRED => true,
@@ -169,13 +171,15 @@ class ApiProtect extends ApiBase {
}
public function getParamDescription() {
+ $p = $this->getModulePrefix();
return array(
- 'title' => 'Title of the page you want to (un)protect',
+ 'title' => "Title of the page you want to (un)protect. Cannot be used together with {$p}pageid",
+ 'pageid' => "ID of the page you want to (un)protect. Cannot be used together with {$p}title",
'token' => 'A protect token previously retrieved through prop=info',
- 'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)',
+ '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.' ),
- 'reason' => 'Reason for (un)protecting (optional)',
+ '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\'' ),
'watch' => 'If set, add the page being (un)protected to your watchlist',
@@ -183,21 +187,33 @@ class ApiProtect extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'title' => 'string',
+ 'reason' => 'string',
+ 'cascade' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
return 'Change the protection level of a page';
}
public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
- array( 'invalidtitle', 'title' ),
- array( 'toofewexpiries', 'noofexpiries', 'noofprotections' ),
- array( 'create-titleexists' ),
- array( 'missingtitle-createonly' ),
- array( 'protect-invalidaction', 'action' ),
- array( 'protect-invalidlevel', 'level' ),
- array( 'invalidexpiry', 'expiry' ),
- array( 'pastexpiry', 'expiry' ),
- ) );
+ return array_merge( parent::getPossibleErrors(),
+ $this->getTitleOrPageIdErrorMessage(),
+ array(
+ array( 'toofewexpiries', 'noofexpiries', 'noofprotections' ),
+ array( 'create-titleexists' ),
+ array( 'missingtitle-createonly' ),
+ array( 'protect-invalidaction', 'action' ),
+ array( 'protect-invalidlevel', 'level' ),
+ array( 'invalidexpiry', 'expiry' ),
+ array( 'pastexpiry', 'expiry' ),
+ )
+ );
}
public function needsToken() {
diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php
index 9e9320fb..9fedaf1b 100644
--- a/includes/api/ApiPurge.php
+++ b/includes/api/ApiPurge.php
@@ -88,13 +88,13 @@ class ApiPurge extends ApiBase {
if ( !$user->pingLimiter() ) {
global $wgParser, $wgEnableParserCache;
- $popts = ParserOptions::newFromContext( $this->getContext() );
+ $popts = $page->makeParserOptions( 'canonical' );
$p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
true, true, $page->getLatest() );
# Update the links tables
- $u = new LinksUpdate( $title, $p_result );
- $u->doUpdate();
+ $updates = $p_result->getSecondaryDataUpdates( $title );
+ DataUpdate::runUpdates( $updates );
$r['linkupdate'] = '';
@@ -103,7 +103,8 @@ class ApiPurge extends ApiBase {
$pcache->save( $p_result, $page, $popts );
}
} else {
- $this->setWarning( $this->parseMsg( array( 'actionthrottledtext' ) ) );
+ $error = $this->parseMsg( array( 'actionthrottledtext' ) );
+ $this->setWarning( $error['info'] );
$forceLinkUpdate = false;
}
}
@@ -133,6 +134,34 @@ class ApiPurge extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ ApiBase::PROP_LIST => true,
+ '' => array(
+ 'ns' => array(
+ ApiBase::PROP_TYPE => 'namespace',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'title' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'pageid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'revid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'invalid' => 'boolean',
+ 'missing' => 'boolean',
+ 'purged' => 'boolean',
+ 'linkupdate' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
return array( 'Purge the cache for the given titles.',
'Requires a POST request if the user is not logged in.'
@@ -143,7 +172,6 @@ class ApiPurge extends ApiBase {
$psModule = new ApiPageSet( $this );
return array_merge(
parent::getPossibleErrors(),
- array( array( 'cantpurge' ), ),
$psModule->getPossibleErrors()
);
}
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index cd54a7da..554aae5a 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -4,7 +4,7 @@
*
* Created on Sep 7, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -47,55 +47,55 @@ class ApiQuery extends ApiBase {
private $params, $redirects, $convertTitles, $iwUrl;
private $mQueryPropModules = array(
+ 'categories' => 'ApiQueryCategories',
+ 'categoryinfo' => 'ApiQueryCategoryInfo',
+ 'duplicatefiles' => 'ApiQueryDuplicateFiles',
+ 'extlinks' => 'ApiQueryExternalLinks',
+ 'images' => 'ApiQueryImages',
+ 'imageinfo' => 'ApiQueryImageInfo',
'info' => 'ApiQueryInfo',
- 'revisions' => 'ApiQueryRevisions',
'links' => 'ApiQueryLinks',
'iwlinks' => 'ApiQueryIWLinks',
'langlinks' => 'ApiQueryLangLinks',
- 'images' => 'ApiQueryImages',
- 'imageinfo' => 'ApiQueryImageInfo',
+ 'pageprops' => 'ApiQueryPageProps',
+ 'revisions' => 'ApiQueryRevisions',
'stashimageinfo' => 'ApiQueryStashImageInfo',
'templates' => 'ApiQueryLinks',
- 'categories' => 'ApiQueryCategories',
- 'extlinks' => 'ApiQueryExternalLinks',
- 'categoryinfo' => 'ApiQueryCategoryInfo',
- 'duplicatefiles' => 'ApiQueryDuplicateFiles',
- 'pageprops' => 'ApiQueryPageProps',
);
private $mQueryListModules = array(
- 'allimages' => 'ApiQueryAllimages',
- 'allpages' => 'ApiQueryAllpages',
- 'alllinks' => 'ApiQueryAllLinks',
'allcategories' => 'ApiQueryAllCategories',
+ 'allimages' => 'ApiQueryAllImages',
+ 'alllinks' => 'ApiQueryAllLinks',
+ 'allpages' => 'ApiQueryAllPages',
'allusers' => 'ApiQueryAllUsers',
'backlinks' => 'ApiQueryBacklinks',
'blocks' => 'ApiQueryBlocks',
'categorymembers' => 'ApiQueryCategoryMembers',
'deletedrevs' => 'ApiQueryDeletedrevs',
'embeddedin' => 'ApiQueryBacklinks',
+ 'exturlusage' => 'ApiQueryExtLinksUsage',
'filearchive' => 'ApiQueryFilearchive',
'imageusage' => 'ApiQueryBacklinks',
'iwbacklinks' => 'ApiQueryIWBacklinks',
'langbacklinks' => 'ApiQueryLangBacklinks',
'logevents' => 'ApiQueryLogEvents',
+ 'protectedtitles' => 'ApiQueryProtectedTitles',
+ 'querypage' => 'ApiQueryQueryPage',
+ 'random' => 'ApiQueryRandom',
'recentchanges' => 'ApiQueryRecentChanges',
'search' => 'ApiQuerySearch',
'tags' => 'ApiQueryTags',
'usercontribs' => 'ApiQueryContributions',
+ 'users' => 'ApiQueryUsers',
'watchlist' => 'ApiQueryWatchlist',
'watchlistraw' => 'ApiQueryWatchlistRaw',
- 'exturlusage' => 'ApiQueryExtLinksUsage',
- 'users' => 'ApiQueryUsers',
- 'random' => 'ApiQueryRandom',
- 'protectedtitles' => 'ApiQueryProtectedTitles',
- 'querypage' => 'ApiQueryQueryPage',
);
private $mQueryMetaModules = array(
+ 'allmessages' => 'ApiQueryAllMessages',
'siteinfo' => 'ApiQuerySiteinfo',
'userinfo' => 'ApiQueryUserInfo',
- 'allmessages' => 'ApiQueryAllmessages',
);
private $mSlaveDB = null;
@@ -103,11 +103,16 @@ class ApiQuery extends ApiBase {
protected $mAllowedGenerators = array();
+ /**
+ * @param $main ApiMain
+ * @param $action string
+ */
public function __construct( $main, $action ) {
parent::__construct( $main, $action );
// Allow custom modules to be added in LocalSettings.php
- global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules;
+ global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules,
+ $wgMemc, $wgAPICacheHelpTimeout;
self::appendUserModules( $this->mQueryPropModules, $wgAPIPropModules );
self::appendUserModules( $this->mQueryListModules, $wgAPIListModules );
self::appendUserModules( $this->mQueryMetaModules, $wgAPIMetaModules );
@@ -116,8 +121,22 @@ class ApiQuery extends ApiBase {
$this->mListModuleNames = array_keys( $this->mQueryListModules );
$this->mMetaModuleNames = array_keys( $this->mQueryMetaModules );
- $this->makeHelpMsgHelper( $this->mQueryPropModules, 'prop' );
- $this->makeHelpMsgHelper( $this->mQueryListModules, 'list' );
+ // 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 );
+
+ if ( $wgAPICacheHelpTimeout > 0 ) {
+ $wgMemc->set( $key, $this->mAllowedGenerators, $wgAPICacheHelpTimeout );
+ }
}
/**
@@ -135,7 +154,7 @@ class ApiQuery extends ApiBase {
/**
* Gets a default slave database connection object
- * @return Database
+ * @return DatabaseBase
*/
public function getDB() {
if ( !isset( $this->mSlaveDB ) ) {
@@ -154,7 +173,7 @@ class ApiQuery extends ApiBase {
* @param $name string Name to assign to the database connection
* @param $db int One of the DB_* constants
* @param $groups array Query groups
- * @return Database
+ * @return DatabaseBase
*/
public function getNamedDB( $name, $db, $groups ) {
if ( !array_key_exists( $name, $this->mNamedDB ) ) {
@@ -202,6 +221,9 @@ class ApiQuery extends ApiBase {
return null;
}
+ /**
+ * @return ApiFormatRaw|null
+ */
public function getCustomPrinter() {
// If &exportnowrap is set, use the raw formatter
if ( $this->getParameter( 'export' ) &&
@@ -258,6 +280,9 @@ class ApiQuery extends ApiBase {
$this->outputGeneralPageInfo();
// Execute all requested modules.
+ /**
+ * @var $module ApiQueryBase
+ */
foreach ( $modules as $module ) {
$params = $module->extractRequestParams();
$cacheMode = $this->mergeCacheMode(
@@ -303,6 +328,9 @@ class ApiQuery extends ApiBase {
*/
private function addCustomFldsToPageSet( $modules, $pageSet ) {
// Query all requested modules.
+ /**
+ * @var $module ApiQueryBase
+ */
foreach ( $modules as $module ) {
$module->requestExtraData( $pageSet );
}
@@ -384,6 +412,9 @@ class ApiQuery extends ApiBase {
// Show redirect information
$redirValues = array();
+ /**
+ * @var $titleTo Title
+ */
foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleTo ) {
$r = array(
'from' => strval( $titleStrFrom ),
@@ -602,7 +633,6 @@ class ApiQuery extends ApiBase {
// Make sure the internal object is empty
// (just in case a sub-module decides to optimize during instantiation)
$this->mPageSet = null;
- $this->mAllowedGenerators = array(); // Will be repopulated
$querySeparator = str_repeat( '--- ', 12 );
$moduleSeparator = str_repeat( '*** ', 14 );
@@ -614,8 +644,6 @@ class ApiQuery extends ApiBase {
$msg .= $this->makeHelpMsgHelper( $this->mQueryMetaModules, 'meta' );
$msg .= "\n\n$moduleSeparator Modules: continuation $moduleSeparator\n\n";
- // Perform the base call last because the $this->mAllowedGenerators
- // will be updated inside makeHelpMsgHelper()
// Use parent to make default message for the query module
$msg = parent::makeHelpMsg() . $msg;
@@ -643,7 +671,6 @@ class ApiQuery extends ApiBase {
$msg .= $msg2;
}
if ( $module instanceof ApiQueryGeneratorBase ) {
- $this->mAllowedGenerators[] = $moduleName;
$msg .= "Generator:\n This module may be used as a generator\n";
}
$moduleDescriptions[] = $msg;
@@ -653,6 +680,19 @@ class ApiQuery extends ApiBase {
}
/**
+ * 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
*/
@@ -674,7 +714,7 @@ class ApiQuery extends ApiBase {
'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 gan, iu, kk, ku, shi, sr, tg, zh' ),
+ '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',
diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php
index 78367a45..4f4c77f0 100644
--- a/includes/api/ApiQueryAllCategories.php
+++ b/includes/api/ApiQueryAllCategories.php
@@ -4,7 +4,7 @@
*
* Created on December 12, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -58,6 +58,17 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
$this->addTables( 'category' );
$this->addFields( 'cat_title' );
+ 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 = $params['dir'] == 'descending' ? '<' : '>';
+ $cont_from = $db->addQuotes( $cont[0] );
+ $this->addWhere( "cat_title $op= $cont_from" );
+ }
+
$dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
$from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
$to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) );
@@ -65,14 +76,20 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
$min = $params['min'];
$max = $params['max'];
- $this->addWhereRange( 'cat_pages', $dir, $min, $max );
+ if ( $dir == 'newer' ) {
+ $this->addWhereRange( 'cat_pages', 'newer', $min, $max );
+ } else {
+ $this->addWhereRange( 'cat_pages', 'older', $max, $min);
+ }
+
if ( isset( $params['prefix'] ) ) {
$this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
}
$this->addOption( 'LIMIT', $params['limit'] + 1 );
- $this->addOption( 'ORDER BY', 'cat_title' . ( $params['dir'] == 'descending' ? ' DESC' : '' ) );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ $this->addOption( 'ORDER BY', 'cat_title' . $sort );
$prop = array_flip( $params['prop'] );
$this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset( $prop['size'] ) );
@@ -86,7 +103,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
'pp_page=page_id',
'pp_propname' => 'hiddencat' ) ),
) );
- $this->addFields( 'pp_propname AS cat_hidden' );
+ $this->addFields( array( 'cat_hidden' => 'pp_propname' ) );
}
$res = $this->select( __METHOD__ );
@@ -98,15 +115,14 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
foreach ( $res as $row ) {
if ( ++ $count > $params['limit'] ) {
// We've reached the one extra which shows that there are additional cats to be had. Stop here...
- // TODO: Security issue - if the user has no right to view next title, it will still be shown
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->cat_title );
break;
}
// Normalize titles
$titleObj = Title::makeTitle( NS_CATEGORY, $row->cat_title );
if ( !is_null( $resultPageSet ) ) {
- $pages[] = $titleObj->getPrefixedText();
+ $pages[] = $titleObj;
} else {
$item = array();
$result->setContent( $item, $titleObj->getText() );
@@ -121,7 +137,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
}
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $item );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->cat_title );
break;
}
}
@@ -137,6 +153,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
public function getAllowedParams() {
return array(
'from' => null,
+ 'continue' => null,
'to' => null,
'prefix' => null,
'dir' => array(
@@ -172,6 +189,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
public function getParamDescription() {
return array(
'from' => 'The category to start enumerating from',
+ 'continue' => 'When more results are available, use this to continue',
'to' => 'The category to stop enumerating at',
'prefix' => 'Search for all category titles that begin with this value',
'dir' => 'Direction to sort in',
@@ -186,10 +204,33 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ '*' => 'string'
+ ),
+ 'size' => array(
+ 'size' => 'integer',
+ 'pages' => 'integer',
+ 'files' => 'integer',
+ 'subcats' => 'integer'
+ ),
+ 'hidden' => array(
+ 'hidden' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
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',
diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php
new file mode 100644
index 00000000..b562da8e
--- /dev/null
+++ b/includes/api/ApiQueryAllImages.php
@@ -0,0 +1,409 @@
+<?php
+
+/**
+ * API for MediaWiki 1.12+
+ *
+ * Created on Mar 16, 2008
+ *
+ * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com,
+ * based on ApiQueryAllPages.php
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Query module to enumerate all available pages.
+ *
+ * @ingroup API
+ */
+class ApiQueryAllImages extends ApiQueryGeneratorBase {
+
+ protected $mRepo;
+
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'ai' );
+ $this->mRepo = RepoGroup::singleton()->getLocalRepo();
+ }
+
+ /**
+ * 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.
+ *
+ * TODO: allow querying non-local repos.
+ * @return DatabaseBase
+ */
+ protected function getDB() {
+ return $this->mRepo->getSlaveDB();
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ /**
+ * @param $resultPageSet ApiPageSet
+ * @return void
+ */
+ public function executeGenerator( $resultPageSet ) {
+ if ( $resultPageSet->isResolvingRedirects() ) {
+ $this->dieUsage( 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params' );
+ }
+
+ $this->run( $resultPageSet );
+ }
+
+ /**
+ * @param $resultPageSet ApiPageSet
+ * @return void
+ */
+ private function run( $resultPageSet = null ) {
+ $repo = $this->mRepo;
+ if ( !$repo instanceof LocalRepo ) {
+ $this->dieUsage( 'Local file repository does not support querying all images', 'unsupportedrepo' );
+ }
+
+ $prefix = $this->getModulePrefix();
+
+ $db = $this->getDB();
+
+ $params = $this->extractRequestParams();
+
+ // Table and return fields
+ $this->addTables( 'image' );
+
+ $prop = array_flip( $params['prop'] );
+ $this->addFields( LocalFile::selectFields() );
+
+ $dir = ( in_array( $params['dir'], array( 'descending', 'older' ) ) ? 'older' : 'newer' );
+
+ if ( $params['sort'] == 'name' ) {
+ // Check mutually exclusive params
+ $disallowed = array( 'start', 'end', 'user' );
+ foreach ( $disallowed as $pname ) {
+ if ( isset( $params[$pname] ) ) {
+ $this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=timestamp", 'badparams' );
+ }
+ }
+ if ( $params['filterbots'] != 'all' ) {
+ $this->dieUsage( "Parameter '{$prefix}filterbots' can only be used with {$prefix}sort=timestamp", 'badparams' );
+ }
+
+ // 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" );
+ }
+
+ // 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 );
+
+ if ( isset( $params['prefix'] ) ) {
+ $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
+ }
+ } else {
+ // Check mutually exclusive params
+ $disallowed = array( 'from', 'to', 'prefix' );
+ foreach ( $disallowed as $pname ) {
+ if ( isset( $params[$pname] ) ) {
+ $this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name", 'badparams' );
+ }
+ }
+ 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' );
+ }
+
+ // Pagination
+ $this->addTimestampWhereRange( 'img_timestamp', $dir, $params['start'], $params['end'] );
+
+ // Image filters
+ if ( !is_null( $params['user'] ) ) {
+ $this->addWhereFld( 'img_user_text', $params['user'] );
+ }
+ 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(
+ 'ug_group' => User::getGroupsWithPermission( 'bot' ),
+ 'ug_user = img_user'
+ )
+ ) ) );
+ }
+ }
+
+ // Filters not depending on sort
+ if ( isset( $params['minsize'] ) ) {
+ $this->addWhere( 'img_size>=' . intval( $params['minsize'] ) );
+ }
+
+ if ( isset( $params['maxsize'] ) ) {
+ $this->addWhere( 'img_size<=' . intval( $params['maxsize'] ) );
+ }
+
+ $sha1 = false;
+ if ( isset( $params['sha1'] ) ) {
+ if ( !$this->validateSha1Hash( $params['sha1'] ) ) {
+ $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' );
+ }
+ $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 );
+ } elseif ( isset( $params['sha1base36'] ) ) {
+ $sha1 = $params['sha1base36'];
+ if ( !$this->validateSha1Base36Hash( $sha1 ) ) {
+ $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' );
+ }
+ }
+ if ( $sha1 ) {
+ $this->addWhereFld( 'img_sha1', $sha1 );
+ }
+
+ if ( !is_null( $params['mime'] ) ) {
+ global $wgMiserMode;
+ if ( $wgMiserMode ) {
+ $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' );
+ }
+
+ list( $major, $minor ) = File::splitMime( $params['mime'] );
+
+ $this->addWhereFld( 'img_major_mime', $major );
+ $this->addWhereFld( 'img_minor_mime', $minor );
+ }
+
+ $limit = $params['limit'];
+ $this->addOption( 'LIMIT', $limit + 1 );
+ $sort = ( $dir == 'older' ? ' 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( 'USE INDEX', array( 'image' => 'img_usertext_timestamp' ) );
+ }
+ } else {
+ $this->addOption( 'ORDER BY', 'img_name' . $sort );
+ }
+
+ $res = $this->select( __METHOD__ );
+
+ $titles = array();
+ $count = 0;
+ $result = $this->getResult();
+ foreach ( $res as $row ) {
+ if ( ++ $count > $limit ) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ if ( $params['sort'] == 'name' ) {
+ $this->setContinueEnumParameter( 'continue', $row->img_name );
+ } else {
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) );
+ }
+ break;
+ }
+
+ if ( is_null( $resultPageSet ) ) {
+ $file = $repo->newFileFromRow( $row );
+ $info = array_merge( array( 'name' => $row->img_name ),
+ ApiQueryImageInfo::getInfo( $file, $prop, $result ) );
+ self::addTitleInfo( $info, $file->getTitle() );
+
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info );
+ if ( !$fit ) {
+ if ( $params['sort'] == 'name' ) {
+ $this->setContinueEnumParameter( 'continue', $row->img_name );
+ } else {
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) );
+ }
+ break;
+ }
+ } else {
+ $titles[] = Title::makeTitle( NS_FILE, $row->img_name );
+ }
+ }
+
+ if ( is_null( $resultPageSet ) ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' );
+ } else {
+ $resultPageSet->populateFromTitles( $titles );
+ }
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'sort' => array(
+ ApiBase::PARAM_DFLT => 'name',
+ ApiBase::PARAM_TYPE => array(
+ 'name',
+ 'timestamp'
+ )
+ ),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ // sort=name
+ 'ascending',
+ 'descending',
+ // sort=timestamp
+ 'newer',
+ 'older',
+ )
+ ),
+ 'from' => null,
+ 'to' => null,
+ 'continue' => null,
+ 'start' => array(
+ ApiBase::PARAM_TYPE => 'timestamp'
+ ),
+ 'end' => array(
+ ApiBase::PARAM_TYPE => 'timestamp'
+ ),
+ 'prop' => array(
+ ApiBase::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( $this->propertyFilter ),
+ ApiBase::PARAM_DFLT => 'timestamp|url',
+ ApiBase::PARAM_ISMULTI => true
+ ),
+ 'prefix' => null,
+ 'minsize' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ),
+ 'maxsize' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ),
+ 'sha1' => null,
+ 'sha1base36' => null,
+ 'user' => array(
+ ApiBase::PARAM_TYPE => 'user'
+ ),
+ 'filterbots' => array(
+ ApiBase::PARAM_DFLT => 'all',
+ ApiBase::PARAM_TYPE => array(
+ 'all',
+ 'bots',
+ 'nobots'
+ )
+ ),
+ 'mime' => null,
+ 'limit' => array(
+ ApiBase::PARAM_DFLT => 10,
+ ApiBase::PARAM_TYPE => 'limit',
+ ApiBase::PARAM_MIN => 1,
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ $p = $this->getModulePrefix();
+ return array(
+ 'sort' => 'Property to sort by',
+ 'dir' => 'The direction in which to list',
+ 'from' => "The image title to start enumerating from. Can only be used with {$p}sort=name",
+ 'to' => "The image title to stop enumerating at. Can only be used with {$p}sort=name",
+ 'continue' => 'When more results are available, use this to continue',
+ 'start' => "The timestamp to start enumerating from. Can only be used with {$p}sort=timestamp",
+ 'end' => "The timestamp to end enumerating. Can only be used with {$p}sort=timestamp",
+ 'prop' => ApiQueryImageInfo::getPropertyDescriptions( $this->propertyFilter ),
+ 'prefix' => "Search for all image titles that begin with this value. Can only be used with {$p}sort=name",
+ 'minsize' => 'Limit to images with at least this many bytes',
+ 'maxsize' => 'Limit to images with at most this many bytes',
+ 'sha1' => "SHA1 hash of image. Overrides {$p}sha1base36",
+ 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)',
+ 'user' => "Only return files uploaded by this user. Can only be used with {$p}sort=timestamp. Cannot be used together with {$p}filterbots",
+ 'filterbots' => "How to filter files uploaded by bots. Can only be used with {$p}sort=timestamp. Cannot be used together with {$p}user",
+ 'mime' => 'What MIME type to search for. e.g. image/jpeg. Disabled in Miser Mode',
+ 'limit' => 'How many images in total to return',
+ );
+ }
+
+ private $propertyFilter = array( 'archivename', 'thumbmime' );
+
+ public function getResultProperties() {
+ return array_merge(
+ array(
+ '' => array(
+ 'name' => 'string',
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
+ ),
+ ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter )
+ );
+ }
+
+ public function getDescription() {
+ return 'Enumerate all images sequentially';
+ }
+
+ public function getPossibleErrors() {
+ $p = $this->getModulePrefix();
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'params', 'info' => 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator' ),
+ array( 'code' => 'badparams', 'info' => "Parameter'{$p}start' can only be used with {$p}sort=timestamp" ),
+ array( 'code' => 'badparams', 'info' => "Parameter'{$p}end' can only be used with {$p}sort=timestamp" ),
+ array( 'code' => 'badparams', 'info' => "Parameter'{$p}user' can only be used with {$p}sort=timestamp" ),
+ array( 'code' => 'badparams', 'info' => "Parameter'{$p}filterbots' can only be used with {$p}sort=timestamp" ),
+ 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' => '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' ),
+ ) );
+ }
+
+ public function getExamples() {
+ return array(
+ 'api.php?action=query&list=allimages&aifrom=B' => array(
+ 'Simple Use',
+ 'Show a list of files starting at the letter "B"',
+ ),
+ 'api.php?action=query&list=allimages&aiprop=user|timestamp|url&aisort=timestamp&aidir=older' => array(
+ 'Simple Use',
+ 'Show a list of recently uploaded files similar to Special:NewFiles',
+ ),
+ 'api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo' => array(
+ 'Using as Generator',
+ 'Show info about 4 files starting at the letter "T"',
+ ),
+ );
+ }
+
+ 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 903f144f..da4840f0 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -4,7 +4,7 @@
*
* Created on July 7, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -76,17 +76,26 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$this->dieUsage( 'alcontinue and alfrom cannot be used together', 'params' );
}
if ( !is_null( $params['continue'] ) ) {
- $arr = explode( '|', $params['continue'] );
- if ( count( $arr ) != 2 ) {
- $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
+ $continueArr = explode( '|', $params['continue'] );
+ $op = $params['dir'] == 'descending' ? '<' : '>';
+ if ( $params['unique'] ) {
+ if ( count( $continueArr ) != 1 ) {
+ $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
+ }
+ $continueTitle = $db->addQuotes( $continueArr[0] );
+ $this->addWhere( "pl_title $op= $continueTitle" );
+ } else {
+ if ( count( $continueArr ) != 2 ) {
+ $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
+ }
+ $continueTitle = $db->addQuotes( $continueArr[0] );
+ $continueFrom = intval( $continueArr[1] );
+ $this->addWhere(
+ "pl_title $op $continueTitle OR " .
+ "(pl_title = $continueTitle AND " .
+ "pl_from $op= $continueFrom)"
+ );
}
- $from = $this->getDB()->strencode( $this->titleToKey( $arr[0] ) );
- $id = intval( $arr[1] );
- $this->addWhere(
- "pl_title > '$from' OR " .
- "(pl_title = '$from' AND " .
- "pl_from > $id)"
- );
}
$from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
@@ -104,9 +113,13 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$limit = $params['limit'];
$this->addOption( 'LIMIT', $limit + 1 );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ $orderBy = array();
+ $orderBy[] = 'pl_title' . $sort;
if ( !$params['unique'] ) {
- $this->addOption( 'ORDER BY', 'pl_title, pl_from' );
+ $orderBy[] = 'pl_from' . $sort;
}
+ $this->addOption( 'ORDER BY', $orderBy );
$res = $this->select( __METHOD__ );
@@ -116,11 +129,10 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
foreach ( $res as $row ) {
if ( ++ $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- // TODO: Security issue - if the user has no right to view next title, it will still be shown
if ( $params['unique'] ) {
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->pl_title );
} else {
- $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from );
+ $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from );
}
break;
}
@@ -137,9 +149,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
if ( !$fit ) {
if ( $params['unique'] ) {
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->pl_title );
} else {
- $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from );
+ $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from );
}
break;
}
@@ -180,7 +192,14 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
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'
+ )
+ ),
);
}
@@ -199,6 +218,19 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
'namespace' => 'The namespace to enumerate',
'limit' => 'How many total links to return',
'continue' => 'When more results are available, use this to continue',
+ 'dir' => 'The direction in which to list',
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ 'ids' => array(
+ 'fromid' => 'integer'
+ ),
+ 'title' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
);
}
diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllMessages.php
index 44774927..f5e1146b 100644
--- a/includes/api/ApiQueryAllmessages.php
+++ b/includes/api/ApiQueryAllMessages.php
@@ -4,7 +4,7 @@
*
* Created on Dec 1, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -29,7 +29,7 @@
*
* @ingroup API
*/
-class ApiQueryAllmessages extends ApiQueryBase {
+class ApiQueryAllMessages extends ApiQueryBase {
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'am' );
@@ -256,6 +256,27 @@ class ApiQueryAllmessages extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'name' => 'string',
+ 'customised' => 'boolean',
+ 'missing' => 'boolean',
+ '*' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'default' => array(
+ 'defaultmissing' => 'boolean',
+ 'default' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'Return messages from this site';
}
diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllPages.php
index e003ee91..16cc31d2 100644
--- a/includes/api/ApiQueryAllpages.php
+++ b/includes/api/ApiQueryAllPages.php
@@ -4,7 +4,7 @@
*
* Created on Sep 25, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -29,7 +29,7 @@
*
* @ingroup API
*/
-class ApiQueryAllpages extends ApiQueryGeneratorBase {
+class ApiQueryAllPages extends ApiQueryGeneratorBase {
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'ap' );
@@ -67,6 +67,17 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
// Page filters
$this->addTables( 'page' );
+ 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 = $params['dir'] == 'descending' ? '<' : '>';
+ $cont_from = $db->addQuotes( $cont[0] );
+ $this->addWhere( "page_title $op= $cont_from" );
+ }
+
if ( $params['filterredir'] == 'redirects' ) {
$this->addWhereFld( 'page_is_redirect', 1 );
} elseif ( $params['filterredir'] == 'nonredirects' ) {
@@ -153,7 +164,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$this->addOption( 'STRAIGHT_JOIN' );
// We have to GROUP BY all selected fields to stop
// PostgreSQL from whining
- $this->addOption( 'GROUP BY', implode( ', ', $selectFields ) );
+ $this->addOption( 'GROUP BY', $selectFields );
$forceNameTitleIndex = false;
}
@@ -165,13 +176,22 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$this->addOption( 'LIMIT', $limit + 1 );
$res = $this->select( __METHOD__ );
+ //Get gender information
+ if( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) {
+ $users = array();
+ foreach ( $res as $row ) {
+ $users[] = $row->page_title;
+ }
+ GenderCache::singleton()->doQuery( $users, __METHOD__ );
+ $res->rewind(); //reset
+ }
+
$count = 0;
$result = $this->getResult();
foreach ( $res as $row ) {
if ( ++ $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- // TODO: Security issue - if the user has no right to view next title, it will still be shown
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->page_title );
break;
}
@@ -184,7 +204,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
);
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->page_title );
break;
}
} else {
@@ -202,6 +222,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
return array(
'from' => null,
+ 'continue' => null,
'to' => null,
'prefix' => null,
'namespace' => array(
@@ -275,6 +296,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$p = $this->getModulePrefix();
return array(
'from' => 'The page title to start enumerating from',
+ 'continue' => 'When more results are available, use this to continue',
'to' => 'The page title to stop enumerating at',
'prefix' => 'Search for all page titles that begin with this value',
'namespace' => 'The namespace to enumerate',
@@ -296,6 +318,16 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'pageid' => 'integer',
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Enumerate all pages sequentially in a given namespace';
}
@@ -304,6 +336,7 @@ 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' ),
) );
}
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index ac112ef9..7f50cbad 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -4,7 +4,7 @@
*
* Created on July 7, 2007
*
- * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright © 2007 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
@@ -34,6 +34,16 @@ class ApiQueryAllUsers extends ApiQueryBase {
parent::__construct( $query, $moduleName, 'au' );
}
+ /**
+ * This function converts the user name to a canonical form
+ * which is stored in the database.
+ * @param String $name
+ * @return String
+ */
+ private function getCanonicalUserName( $name ) {
+ return str_replace( '_', ' ', $name );
+ }
+
public function execute() {
$db = $this->getDB();
$params = $this->extractRequestParams();
@@ -57,8 +67,8 @@ class ApiQueryAllUsers extends ApiQueryBase {
$useIndex = true;
$dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
- $from = is_null( $params['from'] ) ? null : $this->keyToTitle( $params['from'] );
- $to = is_null( $params['to'] ) ? null : $this->keyToTitle( $params['to'] );
+ $from = is_null( $params['from'] ) ? null : $this->getCanonicalUserName( $params['from'] );
+ $to = is_null( $params['to'] ) ? null : $this->getCanonicalUserName( $params['to'] );
# MySQL doesn't seem to use 'equality propagation' here, so like the
# ActiveUsers special page, we have to use rc_user_text for some cases.
@@ -68,7 +78,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
if ( !is_null( $params['prefix'] ) ) {
$this->addWhere( $userFieldToSort .
- $db->buildLike( $this->keyToTitle( $params['prefix'] ), $db->anyString() ) );
+ $db->buildLike( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) );
}
if ( !is_null( $params['rights'] ) ) {
@@ -142,11 +152,11 @@ class ApiQueryAllUsers extends ApiQueryBase {
'INNER JOIN', 'rc_user_text=user_name'
) ) );
- $this->addFields( 'COUNT(*) AS recentedits' );
+ $this->addFields( array( 'recentedits' => 'COUNT(*)' ) );
- $this->addWhere( "rc_log_type IS NULL OR rc_log_type != 'newusers'" );
+ $this->addWhere( 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ) );
$timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 );
- $this->addWhere( "rc_timestamp >= {$db->addQuotes( $timestamp )}" );
+ $this->addWhere( 'rc_timestamp >= ' . $db->addQuotes( $timestamp ) );
$this->addOption( 'GROUP BY', $userFieldToSort );
}
@@ -190,15 +200,14 @@ class ApiQueryAllUsers extends ApiQueryBase {
$lastUserData = null;
if ( !$fit ) {
- $this->setContinueEnumParameter( 'from',
- $this->keyToTitle( $lastUserData['name'] ) );
+ $this->setContinueEnumParameter( 'from', $lastUserData['name'] );
break;
}
}
if ( $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->user_name ) );
+ $this->setContinueEnumParameter( 'from', $row->user_name );
break;
}
@@ -209,7 +218,9 @@ class ApiQueryAllUsers extends ApiQueryBase {
'name' => $lastUser,
);
if ( $fld_blockinfo && !is_null( $row->ipb_by_text ) ) {
+ $lastUserData['blockid'] = $row->ipb_id;
$lastUserData['blockedby'] = $row->ipb_by_text;
+ $lastUserData['blockedbyid'] = $row->ipb_by;
$lastUserData['blockreason'] = $row->ipb_reason;
$lastUserData['blockexpiry'] = $row->ipb_expiry;
}
@@ -235,32 +246,45 @@ class ApiQueryAllUsers extends ApiQueryBase {
'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function' );
}
- $lastUserObj = User::newFromName( $lastUser );
+ $lastUserObj = User::newFromId( $row->user_id );
// Add user's group info
if ( $fld_groups ) {
- if ( !isset( $lastUserData['groups'] ) && $lastUserObj ) {
- $lastUserData['groups'] = ApiQueryUsers::getAutoGroups( $lastUserObj );
+ if ( !isset( $lastUserData['groups'] ) ) {
+ if ( $lastUserObj ) {
+ $lastUserData['groups'] = $lastUserObj->getAutomaticGroups();
+ } else {
+ // This should not normally happen
+ $lastUserData['groups'] = array();
+ }
}
if ( !is_null( $row->ug_group2 ) ) {
$lastUserData['groups'][] = $row->ug_group2;
}
+
$result->setIndexedTagName( $lastUserData['groups'], 'g' );
}
if ( $fld_implicitgroups && !isset( $lastUserData['implicitgroups'] ) && $lastUserObj ) {
- $lastUserData['implicitgroups'] = ApiQueryUsers::getAutoGroups( $lastUserObj );
+ $lastUserData['implicitgroups'] = $lastUserObj->getAutomaticGroups();
$result->setIndexedTagName( $lastUserData['implicitgroups'], 'g' );
}
if ( $fld_rights ) {
- if ( !isset( $lastUserData['rights'] ) && $lastUserObj ) {
- $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
+ if ( !isset( $lastUserData['rights'] ) ) {
+ if ( $lastUserObj ) {
+ $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
+ } else {
+ // This should not normally happen
+ $lastUserData['rights'] = array();
+ }
}
+
if ( !is_null( $row->ug_group2 ) ) {
$lastUserData['rights'] = array_unique( array_merge( $lastUserData['rights'],
User::getGroupPermissions( array( $row->ug_group2 ) ) ) );
}
+
$result->setIndexedTagName( $lastUserData['rights'], 'r' );
}
}
@@ -269,8 +293,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
$fit = $result->addValue( array( 'query', $this->getModuleName() ),
null, $lastUserData );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'from',
- $this->keyToTitle( $lastUserData['name'] ) );
+ $this->setContinueEnumParameter( 'from', $lastUserData['name'] );
}
}
@@ -338,7 +361,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
'dir' => 'Direction to sort in',
'group' => 'Limit users to given group name(s)',
'excludegroup' => 'Exclude users in given group name(s)',
- 'rights' => 'Limit users to given right(s)',
+ 'rights' => 'Limit users to given right(s) (does not include rights granted by implicit or auto-promoted groups like *, user, or autoconfirmed)',
'prop' => array(
'What pieces of information to include.',
' blockinfo - Adds the information about a current block on the user',
@@ -354,6 +377,48 @@ class ApiQueryAllUsers extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'userid' => 'integer',
+ 'name' => 'string',
+ 'recenteditcount' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'blockinfo' => array(
+ 'blockid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedby' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedbyid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedreason' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedexpiry' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'hidden' => 'boolean'
+ ),
+ 'editcount' => array(
+ 'editcount' => 'integer'
+ ),
+ 'registration' => array(
+ 'registration' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Enumerate all registered users';
}
diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php
deleted file mode 100644
index ca344f73..00000000
--- a/includes/api/ApiQueryAllimages.php
+++ /dev/null
@@ -1,267 +0,0 @@
-<?php
-
-/**
- * API for MediaWiki 1.12+
- *
- * Created on Mar 16, 2008
- *
- * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com,
- * based on ApiQueryAllpages.php
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * Query module to enumerate all available pages.
- *
- * @ingroup API
- */
-class ApiQueryAllimages extends ApiQueryGeneratorBase {
-
- protected $mRepo;
-
- public function __construct( $query, $moduleName ) {
- parent::__construct( $query, $moduleName, 'ai' );
- $this->mRepo = RepoGroup::singleton()->getLocalRepo();
- }
-
- /**
- * 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.
- *
- * TODO: allow querying non-local repos.
- * @return DatabaseBase
- */
- protected function getDB() {
- return $this->mRepo->getSlaveDB();
- }
-
- public function execute() {
- $this->run();
- }
-
- public function getCacheMode( $params ) {
- return 'public';
- }
-
- /**
- * @param $resultPageSet ApiPageSet
- * @return void
- */
- public function executeGenerator( $resultPageSet ) {
- if ( $resultPageSet->isResolvingRedirects() ) {
- $this->dieUsage( 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params' );
- }
-
- $this->run( $resultPageSet );
- }
-
- /**
- * @param $resultPageSet ApiPageSet
- * @return void
- */
- private function run( $resultPageSet = null ) {
- $repo = $this->mRepo;
- if ( !$repo instanceof LocalRepo ) {
- $this->dieUsage( 'Local file repository does not support querying all images', 'unsupportedrepo' );
- }
-
- $db = $this->getDB();
-
- $params = $this->extractRequestParams();
-
- // Image filters
- $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
- $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
- $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) );
- $this->addWhereRange( 'img_name', $dir, $from, $to );
-
- if ( isset( $params['prefix'] ) )
- $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
-
- if ( isset( $params['minsize'] ) ) {
- $this->addWhere( 'img_size>=' . intval( $params['minsize'] ) );
- }
-
- if ( isset( $params['maxsize'] ) ) {
- $this->addWhere( 'img_size<=' . intval( $params['maxsize'] ) );
- }
-
- $sha1 = false;
- if ( isset( $params['sha1'] ) ) {
- if ( !$this->validateSha1Hash( $params['sha1'] ) ) {
- $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' );
- }
- $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 );
- } elseif ( isset( $params['sha1base36'] ) ) {
- $sha1 = $params['sha1base36'];
- if ( !$this->validateSha1Base36Hash( $sha1 ) ) {
- $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' );
- }
- }
- if ( $sha1 ) {
- $this->addWhereFld( 'img_sha1', $sha1 );
- }
-
- if ( !is_null( $params['mime'] ) ) {
- global $wgMiserMode;
- if ( $wgMiserMode ) {
- $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' );
- }
-
- list( $major, $minor ) = File::splitMime( $params['mime'] );
-
- $this->addWhereFld( 'img_major_mime', $major );
- $this->addWhereFld( 'img_minor_mime', $minor );
- }
-
- $this->addTables( 'image' );
-
- $prop = array_flip( $params['prop'] );
- $this->addFields( LocalFile::selectFields() );
-
- $limit = $params['limit'];
- $this->addOption( 'LIMIT', $limit + 1 );
- $this->addOption( 'ORDER BY', 'img_name' .
- ( $params['dir'] == 'descending' ? ' DESC' : '' ) );
-
- $res = $this->select( __METHOD__ );
-
- $titles = array();
- $count = 0;
- $result = $this->getResult();
- foreach ( $res as $row ) {
- if ( ++ $count > $limit ) {
- // We've reached the one extra which shows that there are additional pages to be had. Stop here...
- // TODO: Security issue - if the user has no right to view next title, it will still be shown
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
- break;
- }
-
- if ( is_null( $resultPageSet ) ) {
- $file = $repo->newFileFromRow( $row );
- $info = array_merge( array( 'name' => $row->img_name ),
- ApiQueryImageInfo::getInfo( $file, $prop, $result ) );
- self::addTitleInfo( $info, $file->getTitle() );
-
- $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info );
- if ( !$fit ) {
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
- break;
- }
- } else {
- $titles[] = Title::makeTitle( NS_IMAGE, $row->img_name );
- }
- }
-
- if ( is_null( $resultPageSet ) ) {
- $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' );
- } else {
- $resultPageSet->populateFromTitles( $titles );
- }
- }
-
- public function getAllowedParams() {
- return array (
- 'from' => null,
- 'to' => null,
- 'prefix' => null,
- 'minsize' => array(
- ApiBase::PARAM_TYPE => 'integer',
- ),
- 'maxsize' => array(
- ApiBase::PARAM_TYPE => 'integer',
- ),
- 'limit' => array(
- ApiBase::PARAM_DFLT => 10,
- ApiBase::PARAM_TYPE => 'limit',
- ApiBase::PARAM_MIN => 1,
- ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
- ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
- ),
- 'dir' => array(
- ApiBase::PARAM_DFLT => 'ascending',
- ApiBase::PARAM_TYPE => array(
- 'ascending',
- 'descending'
- )
- ),
- 'sha1' => null,
- 'sha1base36' => null,
- 'prop' => array(
- ApiBase::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( $this->propertyFilter ),
- ApiBase::PARAM_DFLT => 'timestamp|url',
- ApiBase::PARAM_ISMULTI => true
- ),
- 'mime' => null,
- );
- }
-
- public function getParamDescription() {
- return array(
- 'from' => 'The image title to start enumerating from',
- 'to' => 'The image title to stop enumerating at',
- 'prefix' => 'Search for all image titles that begin with this value',
- 'dir' => 'The direction in which to list',
- 'minsize' => 'Limit to images with at least this many bytes',
- 'maxsize' => 'Limit to images with at most this many bytes',
- 'limit' => 'How many images in total to return',
- 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36",
- 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)',
- 'prop' => ApiQueryImageInfo::getPropertyDescriptions( $this->propertyFilter ),
- 'mime' => 'What MIME type to search for. e.g. image/jpeg. Disabled in Miser Mode',
- );
- }
-
- private $propertyFilter = array( 'archivename' );
-
- public function getDescription() {
- return 'Enumerate all images sequentially';
- }
-
- public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
- array( 'code' => 'params', 'info' => 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator' ),
- array( 'code' => 'unsupportedrepo', 'info' => 'Local file repository does not support querying all images' ),
- 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' ),
- ) );
- }
-
- public function getExamples() {
- return array(
- 'api.php?action=query&list=allimages&aifrom=B' => array(
- 'Simple Use',
- 'Show a list of images starting at the letter "B"',
- ),
- 'api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo' => array(
- 'Using as Generator',
- 'Show info about 4 images starting at the letter "T"',
- ),
- );
- }
-
- public function getHelpUrls() {
- return 'https://www.mediawiki.org/wiki/API:Allimages';
- }
-
- public function getVersion() {
- return __CLASS__ . ': $Id$';
- }
-}
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index 381ef550..06db87bf 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -4,7 +4,7 @@
*
* Created on Oct 16, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -40,7 +40,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
private $rootTitle;
private $params, $contID, $redirID, $redirect;
- private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_sort, $bl_fields, $hasNS;
+ private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS;
/**
* Maps ns and title to pageid
@@ -91,14 +91,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->hasNS = $moduleName !== 'imageusage';
if ( $this->hasNS ) {
$this->bl_title = $prefix . '_title';
- $this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}";
$this->bl_fields = array(
$this->bl_ns,
$this->bl_title
);
} else {
$this->bl_title = $prefix . '_to';
- $this->bl_sort = "{$this->bl_title}, {$this->bl_from}";
$this->bl_fields = array(
$this->bl_title
);
@@ -144,7 +142,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
if ( !is_null( $this->contID ) ) {
- $this->addWhere( "{$this->bl_from}>={$this->contID}" );
+ $op = $this->params['dir'] == 'descending' ? '<' : '>';
+ $this->addWhere( "{$this->bl_from}$op={$this->contID}" );
}
if ( $this->params['filterredir'] == 'redirects' ) {
@@ -155,7 +154,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
- $this->addOption( 'ORDER BY', $this->bl_from );
+ $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
+ $this->addOption( 'ORDER BY', $this->bl_from . $sort );
$this->addOption( 'STRAIGHT_JOIN' );
}
@@ -186,28 +186,35 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
// We can't use LinkBatch here because $this->hasNS may be false
$titleWhere = array();
+ $allRedirNs = array();
+ $allRedirDBkey = array();
foreach ( $this->redirTitles as $t ) {
- $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $t->getDBkey() ) .
- ( $this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : '' );
+ $redirNs = $t->getNamespace();
+ $redirDBkey = $t->getDBkey();
+ $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) .
+ ( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' );
+ $allRedirNs[] = $redirNs;
+ $allRedirDBkey[] = $redirDBkey;
}
$this->addWhere( $db->makeList( $titleWhere, LIST_OR ) );
$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
if ( !is_null( $this->redirID ) ) {
+ $op = $this->params['dir'] == 'descending' ? '<' : '>';
$first = $this->redirTitles[0];
- $title = $db->strencode( $first->getDBkey() );
+ $title = $db->addQuotes( $first->getDBkey() );
$ns = $first->getNamespace();
$from = $this->redirID;
if ( $this->hasNS ) {
- $this->addWhere( "{$this->bl_ns} > $ns OR " .
+ $this->addWhere( "{$this->bl_ns} $op $ns OR " .
"({$this->bl_ns} = $ns AND " .
- "({$this->bl_title} > '$title' OR " .
- "({$this->bl_title} = '$title' AND " .
- "{$this->bl_from} >= $from)))" );
+ "({$this->bl_title} $op $title OR " .
+ "({$this->bl_title} = $title AND " .
+ "{$this->bl_from} $op= $from)))" );
} else {
- $this->addWhere( "{$this->bl_title} > '$title' OR " .
- "({$this->bl_title} = '$title' AND " .
- "{$this->bl_from} >= $from)" );
+ $this->addWhere( "{$this->bl_title} $op $title OR " .
+ "({$this->bl_title} = $title AND " .
+ "{$this->bl_from} $op= $from)" );
}
}
if ( $this->params['filterredir'] == 'redirects' ) {
@@ -217,7 +224,17 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
- $this->addOption( 'ORDER BY', $this->bl_sort );
+ $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 ) {
+ $orderBy[] = $this->bl_ns . $sort;
+ }
+ if( count( array_unique( $allRedirDBkey ) ) != 1 ) {
+ $orderBy[] = $this->bl_title . $sort;
+ }
+ $orderBy[] = $this->bl_from . $sort;
+ $this->addOption( 'ORDER BY', $orderBy );
$this->addOption( 'USE INDEX', array( 'page' => 'PRIMARY' ) );
}
@@ -277,7 +294,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if ( $this->hasNS ) {
$parentID = $this->pageMap[$row-> { $this->bl_ns } ][$row-> { $this->bl_title } ];
} else {
- $parentID = $this->pageMap[NS_IMAGE][$row-> { $this->bl_title } ];
+ $parentID = $this->pageMap[NS_FILE][$row-> { $this->bl_title } ];
}
$this->continueStr = $this->getContinueRedirStr( $parentID, $row->page_id );
break;
@@ -369,14 +386,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if ( !is_null( $this->params['continue'] ) ) {
$this->parseContinueParam();
} else {
- if ( $this->params['title'] !== '' ) {
- $title = Title::newFromText( $this->params['title'] );
- if ( !$title ) {
- $this->dieUsageMsg( array( 'invalidtitle', $this->params['title'] ) );
- } else {
- $this->rootTitle = $title;
- }
- }
+ $this->rootTitle = $this->getTitleOrPageId( $this->params )->getTitle();
}
// only image titles are allowed for the root in imageinfo mode
@@ -436,13 +446,22 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$retval = array(
'title' => array(
ApiBase::PARAM_TYPE => 'string',
- ApiBase::PARAM_REQUIRED => true
+ ),
+ 'pageid' => array(
+ ApiBase::PARAM_TYPE => 'integer',
),
'continue' => null,
'namespace' => array(
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace'
),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
+ ),
'filterredir' => array(
ApiBase::PARAM_DFLT => 'all',
ApiBase::PARAM_TYPE => array(
@@ -468,9 +487,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
public function getParamDescription() {
$retval = array(
- 'title' => 'Title to search',
+ 'title' => "Title to search. Cannot be used together with {$this->bl_code}pageid",
+ 'pageid' => "Pageid to search. Cannot be used together with {$this->bl_code}title",
'continue' => 'When more results are available, use this to continue',
'namespace' => 'The namespace to enumerate',
+ 'dir' => 'The direction in which to list',
);
if ( $this->getModuleName() != 'embeddedin' ) {
return array_merge( $retval, array(
@@ -485,6 +506,17 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
) );
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'pageid' => 'integer',
+ 'ns' => 'namespace',
+ 'title' => 'string',
+ 'redirect' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
switch ( $this->getModuleName() ) {
case 'backlinks':
@@ -499,11 +531,13 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
- array( 'invalidtitle', 'title' ),
- array( 'code' => 'bad_image_title', 'info' => "The title for {$this->getModuleName()} query must be an image" ),
- array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
- ) );
+ return array_merge( parent::getPossibleErrors(),
+ $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' ),
+ )
+ );
}
public function getExamples() {
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index 4fe82de0..2c48aca0 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -4,7 +4,7 @@
*
* Created on Sep 7, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -233,7 +233,7 @@ abstract class ApiQueryBase extends ApiBase {
*/
protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
$db = $this->getDb();
- return $this->addWhereRange( $field, $dir,
+ $this->addWhereRange( $field, $dir,
$db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
}
@@ -392,7 +392,7 @@ abstract class ApiQueryBase extends ApiBase {
* @param $name string Name to assign to the database connection
* @param $db int One of the DB_* constants
* @param $groups array Query groups
- * @return Database
+ * @return DatabaseBase
*/
public function selectNamedDB( $name, $db, $groups ) {
$this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups );
@@ -519,7 +519,7 @@ abstract class ApiQueryBase extends ApiBase {
$this->addFields( 'ipb_deleted' );
if ( $showBlockInfo ) {
- $this->addFields( array( 'ipb_reason', 'ipb_by_text', 'ipb_expiry' ) );
+ $this->addFields( array( 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_reason', 'ipb_expiry' ) );
}
// Don't show hidden names
@@ -571,6 +571,11 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
private $mIsGenerator;
+ /**
+ * @param $query ApiBase
+ * @param $moduleName string
+ * @param $paramPrefix string
+ */
public function __construct( $query, $moduleName, $paramPrefix = '' ) {
parent::__construct( $query, $moduleName, $paramPrefix );
$this->mIsGenerator = false;
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
index bebb5a7d..96b86962 100644
--- a/includes/api/ApiQueryBlocks.php
+++ b/includes/api/ApiQueryBlocks.php
@@ -4,7 +4,7 @@
*
* Created on Sep 10, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -77,6 +77,9 @@ class ApiQueryBlocks extends ApiQueryBase {
$this->addOption( 'LIMIT', $params['limit'] + 1 );
$this->addTimestampWhereRange( 'ipb_timestamp', $params['dir'], $params['start'], $params['end'] );
+
+ $db = $this->getDB();
+
if ( isset( $params['ids'] ) ) {
$this->addWhereFld( 'ipb_id', $params['ids'] );
}
@@ -87,7 +90,6 @@ class ApiQueryBlocks extends ApiQueryBase {
$this->addWhereFld( 'ipb_address', $this->usernames );
$this->addWhereFld( 'ipb_auto', 0 );
}
- $db = $this->getDB();
if ( isset( $params['ip'] ) ) {
list( $ip, $range ) = IP::parseCIDR( $params['ip'] );
if ( $ip && $range ) {
@@ -101,10 +103,15 @@ class ApiQueryBlocks extends ApiQueryBase {
}
$prefix = substr( $lower, 0, 4 );
+ # Fairly hard to make a malicious SQL statement out of hex characters,
+ # but it is good practice to add quotes
+ $lower = $db->addQuotes( $lower );
+ $upper = $db->addQuotes( $upper );
+
$this->addWhere( array(
'ipb_range_start' . $db->buildLike( $prefix, $db->anyString() ),
- "ipb_range_start <= '$lower'",
- "ipb_range_end >= '$upper'",
+ 'ipb_range_start <= ' . $lower,
+ 'ipb_range_end >= ' . $upper,
'ipb_auto' => 0
) );
}
@@ -292,8 +299,8 @@ class ApiQueryBlocks extends ApiQueryBase {
'start' => 'The timestamp to start enumerating from',
'end' => 'The timestamp to stop enumerating at',
'dir' => $this->getDirectionDescription( $p ),
- 'ids' => 'Pipe-separated list of block IDs to list (optional)',
- 'users' => 'Pipe-separated list of users to search for (optional)',
+ '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' ),
'limit' => 'The maximum amount of blocks to list',
@@ -317,18 +324,74 @@ class ApiQueryBlocks extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ 'id' => array(
+ 'id' => 'integer'
+ ),
+ 'user' => array(
+ 'user' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'userid' => array(
+ 'userid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'by' => array(
+ 'by' => 'string'
+ ),
+ 'byid' => array(
+ 'byid' => 'integer'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'expiry' => array(
+ 'expiry' => 'timestamp'
+ ),
+ 'reason' => array(
+ 'reason' => 'string'
+ ),
+ 'range' => array(
+ 'rangestart' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'rangeend' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'flags' => array(
+ 'automatic' => 'boolean',
+ 'anononly' => 'boolean',
+ 'nocreate' => 'boolean',
+ 'autoblock' => 'boolean',
+ 'noemail' => 'boolean',
+ 'hidden' => 'boolean',
+ 'allowusertalk' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
return 'List all blocked users and IP addresses';
}
public function getPossibleErrors() {
- return array_merge( parent::getPossibleErrors(), array(
+ return array_merge( parent::getPossibleErrors(),
$this->getRequireOnlyOneParameterErrorMessages( array( 'users', 'ip' ) ),
- array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ),
- array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ),
- array( 'code' => 'param_user', 'info' => 'User name user is not valid' ),
- array( 'show' ),
- ) );
+ array(
+ array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ),
+ array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ),
+ array( 'code' => 'param_user', 'info' => 'User name user is not valid' ),
+ array( 'show' ),
+ )
+ );
}
public function getExamples() {
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 1c1f1550..309c2ce9 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -4,7 +4,7 @@
*
* Created on May 13, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -89,12 +89,13 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$this->dieUsage( "Invalid continue param. You should pass the " .
"original value returned by the previous query", "_badcontinue" );
}
+ $op = $params['dir'] == 'descending' ? '<' : '>';
$clfrom = intval( $cont[0] );
- $clto = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $clto = $this->getDB()->addQuotes( $cont[1] );
$this->addWhere(
- "cl_from > $clfrom OR " .
+ "cl_from $op $clfrom OR " .
"(cl_from = $clfrom AND " .
- "cl_to >= '$clto')"
+ "cl_to $op= $clto)"
);
}
@@ -123,14 +124,14 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$this->addOption( 'USE INDEX', array( 'categorylinks' => 'cl_from' ) );
- $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
// Don't order by cl_from if it's constant in the WHERE clause
if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) {
- $this->addOption( 'ORDER BY', 'cl_to' . $dir );
+ $this->addOption( 'ORDER BY', 'cl_to' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'cl_from' . $dir,
- 'cl_to' . $dir
+ 'cl_from' . $sort,
+ 'cl_to' . $sort
));
}
@@ -142,8 +143,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'continue', $row->cl_from .
- '|' . $this->keyToTitle( $row->cl_to ) );
+ $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to );
break;
}
@@ -163,8 +163,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$fit = $this->addPageSubItem( $row->cl_from, $vals );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'continue', $row->cl_from .
- '|' . $this->keyToTitle( $row->cl_to ) );
+ $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to );
break;
}
}
@@ -174,8 +173,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'continue', $row->cl_from .
- '|' . $this->keyToTitle( $row->cl_to ) );
+ $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to );
break;
}
@@ -239,6 +237,25 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'sortkey' => array(
+ 'sortkey' => 'string',
+ 'sortkeyprefix' => 'string'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'hidden' => array(
+ 'hidden' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
return 'List all categories the page(s) belong to';
}
diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php
index c5070e87..31517fab 100644
--- a/includes/api/ApiQueryCategoryInfo.php
+++ b/includes/api/ApiQueryCategoryInfo.php
@@ -4,7 +4,7 @@
*
* Created on May 13, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -25,7 +25,8 @@
*/
/**
- * This query adds the <categories> subelement to all pages with the list of categories the page is in
+ * This query adds the "<categories>" subelement to all pages with the list of
+ * categories the page is in.
*
* @ingroup API
*/
@@ -61,7 +62,7 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
'pp_propname' => 'hiddencat' ) ),
) );
- $this->addFields( array( 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden' ) );
+ $this->addFields( array( 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'cat_hidden' => 'pp_propname' ) );
$this->addWhere( array( 'cat_title' => $cattitles ) );
if ( !is_null( $params['continue'] ) ) {
@@ -106,6 +107,34 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ ApiBase::PROP_LIST => false,
+ '' => array(
+ 'size' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => false
+ ),
+ 'pages' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => false
+ ),
+ 'files' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => false
+ ),
+ 'subcats' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => false
+ ),
+ 'hidden' => array(
+ ApiBase::PROP_TYPE => 'boolean',
+ ApiBase::PROP_NULLABLE => false
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'Returns information about the given categories';
}
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 4b19b7e8..55ce0234 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -4,7 +4,7 @@
*
* Created on June 14, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -54,22 +54,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
private function run( $resultPageSet = null ) {
$params = $this->extractRequestParams();
- $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
-
- if ( isset( $params['title'] ) ) {
- $categoryTitle = Title::newFromText( $params['title'] );
-
- if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY ) {
- $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' );
- }
- } elseif( isset( $params['pageid'] ) ) {
- $categoryTitle = Title::newFromID( $params['pageid'] );
-
- if ( !$categoryTitle ) {
- $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
- } elseif ( $categoryTitle->getNamespace() != NS_CATEGORY ) {
- $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' );
- }
+ $categoryTitle = $this->getTitleOrPageId( $params )->getTitle();
+ if ( $categoryTitle->getNamespace() != NS_CATEGORY ) {
+ $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' );
}
$prop = array_flip( $params['prop'] );
@@ -107,10 +94,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$this->addWhereFld( 'page_namespace', $params['namespace'] );
}
- $dir = $params['dir'] == 'asc' ? 'newer' : 'older';
+ $dir = in_array( $params['dir'], array( 'asc', 'ascending', 'newer' ) ) ? 'newer' : 'older';
if ( $params['sort'] == 'timestamp' ) {
- $this->addWhereRange( 'cl_timestamp',
+ $this->addTimestampWhereRange( 'cl_timestamp',
$dir,
$params['start'],
$params['end'] );
@@ -313,10 +300,15 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
)
),
'dir' => array(
- ApiBase::PARAM_DFLT => 'asc',
+ ApiBase::PARAM_DFLT => 'ascending',
ApiBase::PARAM_TYPE => array(
'asc',
- 'desc'
+ 'desc',
+ // Normalising with other modules
+ 'ascending',
+ 'descending',
+ 'newer',
+ 'older',
)
),
'start' => array(
@@ -357,7 +349,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
'endsortkey' => "Sortkey to end listing at. Must be given in binary format. Can only be used with {$p}sort=sortkey",
'startsortkeyprefix' => "Sortkey prefix to start listing from. Can only be used with {$p}sort=sortkey. Overrides {$p}startsortkey",
'endsortkeyprefix' => "Sortkey prefix to end listing BEFORE (not at, if this value occurs it will not be included!). Can only be used with {$p}sort=sortkey. Overrides {$p}endsortkey",
- 'continue' => 'For large categories, give the value retured from previous query',
+ 'continue' => 'For large categories, give the value returned from previous query',
'limit' => 'The maximum number of pages to return.',
);
@@ -372,17 +364,46 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
return $desc;
}
+ public function getResultProperties() {
+ return array(
+ 'ids' => array(
+ 'pageid' => 'integer'
+ ),
+ 'title' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'sortkey' => array(
+ 'sortkey' => 'string'
+ ),
+ 'sortkeyprefix' => array(
+ 'sortkeyprefix' => 'string'
+ ),
+ 'type' => array(
+ 'type' => array(
+ ApiBase::PROP_TYPE => array(
+ 'page',
+ 'subcat',
+ 'file'
+ )
+ )
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ )
+ );
+ }
+
public function getDescription() {
return 'List all pages in a given category';
}
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(),
- $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ),
+ $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' ),
- array( 'nosuchpageid', 'pageid' ),
)
);
}
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
index 0a0cc93d..e69ccbd6 100644
--- a/includes/api/ApiQueryDeletedrevs.php
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -4,7 +4,7 @@
*
* Created on Jul 2, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -155,7 +155,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$this->addWhereFld( 'ar_user_text', $params['user'] );
} elseif ( !is_null( $params['excludeuser'] ) ) {
$this->addWhere( 'ar_user_text != ' .
- $this->getDB()->addQuotes( $params['excludeuser'] ) );
+ $db->addQuotes( $params['excludeuser'] ) );
}
if ( !is_null( $params['continue'] ) && ( $mode == 'all' || $mode == 'revs' ) ) {
@@ -164,14 +164,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', 'badcontinue' );
}
$ns = intval( $cont[0] );
- $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
- $ts = $this->getDB()->strencode( $cont[2] );
+ $title = $db->addQuotes( $cont[1] );
+ $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
$op = ( $dir == 'newer' ? '>' : '<' );
$this->addWhere( "ar_namespace $op $ns OR " .
"(ar_namespace = $ns AND " .
- "(ar_title $op '$title' OR " .
- "(ar_title = '$title' AND " .
- "ar_timestamp $op= '$ts')))" );
+ "(ar_title $op $title OR " .
+ "(ar_title = $title AND " .
+ "ar_timestamp $op= $ts)))" );
}
$this->addOption( 'LIMIT', $limit + 1 );
@@ -180,7 +180,11 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
if ( $params['unique'] ) {
$this->addOption( 'GROUP BY', 'ar_title' );
} else {
- $this->addOption( 'ORDER BY', 'ar_title, ar_timestamp' );
+ $sort = ( $dir == 'newer' ? '' : ' DESC' );
+ $this->addOption( 'ORDER BY', array(
+ 'ar_title' . $sort,
+ 'ar_timestamp' . $sort
+ ));
}
} else {
if ( $mode == 'revs' ) {
@@ -199,7 +203,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
// We've had enough
if ( $mode == 'all' || $mode == 'revs' ) {
$this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' .
- $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp );
+ $row->ar_title . '|' . $row->ar_timestamp );
} else {
$this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) );
}
@@ -265,7 +269,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
if ( !$fit ) {
if ( $mode == 'all' || $mode == 'revs' ) {
$this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' .
- $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp );
+ $row->ar_title . '|' . $row->ar_timestamp );
} else {
$this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) );
}
@@ -334,8 +338,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
public function getParamDescription() {
return array(
- 'start' => 'The timestamp to start enumerating from (1,2)',
- 'end' => 'The timestamp to stop enumerating at (1,2)',
+ 'start' => 'The timestamp to start enumerating from (1, 2)',
+ 'end' => 'The timestamp to stop enumerating at (1, 2)',
'dir' => $this->getDirectionDescription( $this->getModulePrefix(), ' (1, 3)' ),
'from' => 'Start listing at this title (3)',
'to' => 'Stop listing at this title (3)',
@@ -363,6 +367,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'token' => array(
+ 'token' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
$p = $this->getModulePrefix();
return array(
diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php
index d68480c3..6715969a 100644
--- a/includes/api/ApiQueryDisabled.php
+++ b/includes/api/ApiQueryDisabled.php
@@ -4,7 +4,7 @@
*
* Created on Sep 25, 2008
*
- * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2008 Roan Kattouw "<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
diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php
index beca5879..8f0fd3be 100644
--- a/includes/api/ApiQueryDuplicateFiles.php
+++ b/includes/api/ApiQueryDuplicateFiles.php
@@ -4,7 +4,7 @@
*
* Created on Sep 27, 2008
*
- * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2008 Roan Kattouw "<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
@@ -59,67 +59,99 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
}
$images = $namespaces[NS_FILE];
- $this->addTables( 'image', 'i1' );
- $this->addTables( 'image', 'i2' );
- $this->addFields( array(
- 'i1.img_name AS orig_name',
- 'i2.img_name AS dup_name',
- 'i2.img_user_text AS dup_user_text',
- 'i2.img_timestamp AS dup_timestamp'
- ) );
-
- $this->addWhere( array(
- 'i1.img_name' => array_keys( $images ),
- 'i1.img_sha1 = i2.img_sha1',
- 'i1.img_name != i2.img_name',
- ) );
+ if( $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' );
}
- $orig = $this->getDB()->strencode( $this->titleTokey( $cont[0] ) );
- $dup = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
- $this->addWhere(
- "i1.img_name > '$orig' OR " .
- "(i1.img_name = '$orig' AND " .
- "i2.img_name >= '$dup')"
- );
+ $fromImage = $cont[0];
+ $skipUntilThisDup = $cont[1];
+ // Filter out any images before $fromImage
+ foreach ( $images as $image => $pageId ) {
+ if ( $image < $fromImage ) {
+ unset( $images[$image] );
+ } else {
+ break;
+ }
+ }
}
- $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' );
- $this->addOption( 'ORDER BY', 'i1.img_name' . $dir );
- $this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $filesToFind = array_keys( $images );
+ if( $params['localonly'] ) {
+ $files = RepoGroup::singleton()->getLocalRepo()->findFiles( $filesToFind );
+ } else {
+ $files = RepoGroup::singleton()->findFiles( $filesToFind );
+ }
- $res = $this->select( __METHOD__ );
+ $fit = true;
$count = 0;
$titles = array();
- 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( 'continue',
- $this->keyToTitle( $row->orig_name ) . '|' .
- $this->keyToTitle( $row->dup_name ) );
- break;
+
+ $sha1s = array();
+ foreach ( $files as $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'] ) {
+ $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] ) ) {
+ continue; //file does not exist
+ }
+ $sha1 = $sha1s[$image];
+ $dupFiles = $filesBySha1s[$sha1];
+ if( $params['dir'] == 'descending' ) {
+ $dupFiles = array_reverse( $dupFiles );
}
- if ( !is_null( $resultPageSet ) ) {
- $titles[] = Title::makeTitle( NS_FILE, $row->dup_name );
- } else {
- $r = array(
- 'name' => $row->dup_name,
- 'user' => $row->dup_user_text,
- 'timestamp' => wfTimestamp( TS_ISO_8601, $row->dup_timestamp )
- );
- $fit = $this->addPageSubItem( $images[$row->orig_name], $r );
- if ( !$fit ) {
- $this->setContinueEnumParameter( 'continue',
- $this->keyToTitle( $row->orig_name ) . '|' .
- $this->keyToTitle( $row->dup_name ) );
+ foreach ( $dupFiles as $dupFile ) {
+ $dupName = $dupFile->getName();
+ if( $image == $dupName && $dupFile->isLocal() ) {
+ continue; //ignore the local file itself
+ }
+ if( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) {
+ continue; //skip to pos after the image from continue param
+ }
+ $skipUntilThisDup = false;
+ if ( ++$count > $params['limit'] ) {
+ $fit = false; //break outer loop
+ // We're one over limit which shows that
+ // there are additional images to be had. Stop here...
+ $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName );
break;
}
+ if ( !is_null( $resultPageSet ) ) {
+ $titles[] = $file->getTitle();
+ } else {
+ $r = array(
+ 'name' => $dupName,
+ 'user' => $dupFile->getUser( 'text' ),
+ 'timestamp' => wfTimestamp( TS_ISO_8601, $dupFile->getTimestamp() )
+ );
+ if( !$dupFile->isLocal() ) {
+ $r['shared'] = '';
+ }
+ $fit = $this->addPageSubItem( $pageId, $r );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName );
+ break;
+ }
+ }
+ }
+ if( !$fit ) {
+ break;
}
}
if ( !is_null( $resultPageSet ) ) {
@@ -144,19 +176,32 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
'descending'
)
),
+ 'localonly' => false,
);
}
public function getParamDescription() {
return array(
- 'limit' => 'How many files to return',
+ 'limit' => 'How many duplicate files to return',
'continue' => 'When more results are available, use this to continue',
'dir' => 'The direction in which to list',
+ 'localonly' => 'Look only for files in the local repository',
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'name' => 'string',
+ 'user' => 'string',
+ 'timestamp' => 'timestamp',
+ 'shared' => 'boolean',
+ )
);
}
public function getDescription() {
- return 'List all files that are duplicates of the given file(s)';
+ return 'List all files that are duplicates of the given file(s) based on hash values';
}
public function getPossibleErrors() {
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
index 93c71e2f..42b398ba 100644
--- a/includes/api/ApiQueryExtLinksUsage.php
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -4,7 +4,7 @@
*
* Created on July 7, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -232,6 +232,21 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
return $desc;
}
+ public function getResultProperties() {
+ return array(
+ 'ids' => array(
+ 'pageid' => 'integer'
+ ),
+ 'title' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'url' => array(
+ 'url' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Enumerate pages that contain a given URL';
}
diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php
index a9fbc839..9365a9b8 100644
--- a/includes/api/ApiQueryExternalLinks.php
+++ b/includes/api/ApiQueryExternalLinks.php
@@ -4,7 +4,7 @@
*
* Created on May 13, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -133,6 +133,14 @@ class ApiQueryExternalLinks extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ '*' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Returns all external urls (not interwikies) from the given page(s)';
}
diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php
index be995f30..a5486ef4 100644
--- a/includes/api/ApiQueryFilearchive.php
+++ b/includes/api/ApiQueryFilearchive.php
@@ -6,7 +6,7 @@
*
* Copyright © 2010 Sam Reed
* Copyright © 2008 Vasiliev Victor vasilvv@gmail.com,
- * based on ApiQueryAllpages.php
+ * based on ApiQueryAllPages.php
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -56,8 +56,10 @@ class ApiQueryFilearchive extends ApiQueryBase {
$fld_dimensions = isset( $prop['dimensions'] );
$fld_description = isset( $prop['description'] ) || isset( $prop['parseddescription'] );
$fld_mime = isset( $prop['mime'] );
+ $fld_mediatype = isset( $prop['mediatype'] );
$fld_metadata = isset( $prop['metadata'] );
$fld_bitdepth = isset( $prop['bitdepth'] );
+ $fld_archivename = isset( $prop['archivename'] );
$this->addTables( 'filearchive' );
@@ -68,12 +70,28 @@ class ApiQueryFilearchive extends ApiQueryBase {
$this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size );
$this->addFieldsIf( 'fa_description', $fld_description );
$this->addFieldsIf( array( 'fa_major_mime', 'fa_minor_mime' ), $fld_mime );
+ $this->addFieldsIf( 'fa_media_type', $fld_mediatype );
$this->addFieldsIf( 'fa_metadata', $fld_metadata );
$this->addFieldsIf( 'fa_bits', $fld_bitdepth );
+ $this->addFieldsIf( 'fa_archive_name', $fld_archivename );
+
+ 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 = $params['dir'] == 'descending' ? '<' : '>';
+ $cont_from = $db->addQuotes( $cont[0] );
+ $this->addWhere( "fa_name $op= $cont_from" );
+ }
// Image filters
$dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
$from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
+ if ( !is_null( $params['continue'] ) ) {
+ $from = $params['continue'];
+ }
$to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) );
$this->addWhereRange( 'fa_name', $dir, $from, $to );
if ( isset( $params['prefix'] ) ) {
@@ -117,8 +135,8 @@ class ApiQueryFilearchive extends ApiQueryBase {
$limit = $params['limit'];
$this->addOption( 'LIMIT', $limit + 1 );
- $this->addOption( 'ORDER BY', 'fa_name' .
- ( $params['dir'] == 'descending' ? ' DESC' : '' ) );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ $this->addOption( 'ORDER BY', 'fa_name' . $sort );
$res = $this->select( __METHOD__ );
@@ -127,8 +145,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
foreach ( $res as $row ) {
if ( ++$count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- // TODO: Security issue - if the user has no right to view next title, it will still be shown
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) );
+ $this->setContinueEnumParameter( 'continue', $row->fa_name );
break;
}
@@ -165,6 +182,9 @@ class ApiQueryFilearchive extends ApiQueryBase {
$row->fa_description, $title );
}
}
+ if ( $fld_mediatype ) {
+ $file['mediatype'] = $row->fa_media_type;
+ }
if ( $fld_metadata ) {
$file['metadata'] = $row->fa_metadata
? ApiQueryImageInfo::processMetaData( unserialize( $row->fa_metadata ), $result )
@@ -176,6 +196,9 @@ class ApiQueryFilearchive extends ApiQueryBase {
if ( $fld_mime ) {
$file['mime'] = "$row->fa_major_mime/$row->fa_minor_mime";
}
+ if ( $fld_archivename && !is_null( $row->fa_archive_name ) ) {
+ $file['archivename'] = $row->fa_archive_name;
+ }
if ( $row->fa_deleted & File::DELETED_FILE ) {
$file['filehidden'] = '';
@@ -194,7 +217,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
$fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) );
+ $this->setContinueEnumParameter( 'continue', $row->fa_name );
break;
}
}
@@ -205,6 +228,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
public function getAllowedParams() {
return array (
'from' => null,
+ 'continue' => null,
'to' => null,
'prefix' => null,
'limit' => array(
@@ -235,8 +259,10 @@ class ApiQueryFilearchive extends ApiQueryBase {
'description',
'parseddescription',
'mime',
+ 'mediatype',
'metadata',
- 'bitdepth'
+ 'bitdepth',
+ 'archivename',
),
),
);
@@ -245,6 +271,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
public function getParamDescription() {
return array(
'from' => 'The image title to start enumerating from',
+ 'continue' => 'When more results are available, use this to continue',
'to' => 'The image title to stop enumerating at',
'prefix' => 'Search for all image titles that begin with this value',
'dir' => 'The direction in which to list',
@@ -261,9 +288,75 @@ class ApiQueryFilearchive extends ApiQueryBase {
' description - Adds description the image version',
' 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',
' bitdepth - Adds the bit depth of the version',
- ),
+ ' archivename - Adds the file name of the archive version for non-latest versions'
+ ),
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'name' => 'string',
+ 'ns' => 'namespace',
+ 'title' => 'string',
+ 'filehidden' => 'boolean',
+ 'commenthidden' => 'boolean',
+ 'userhidden' => 'boolean',
+ 'suppressed' => 'boolean'
+ ),
+ 'sha1' => array(
+ 'sha1' => 'string'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'user' => array(
+ 'userid' => 'integer',
+ 'user' => 'string'
+ ),
+ 'size' => array(
+ 'size' => 'integer',
+ 'pagecount' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'height' => 'integer',
+ 'width' => 'integer'
+ ),
+ 'dimensions' => array(
+ 'size' => 'integer',
+ 'pagecount' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'height' => 'integer',
+ 'width' => 'integer'
+ ),
+ 'description' => array(
+ 'description' => 'string'
+ ),
+ 'parseddescription' => array(
+ 'description' => 'string',
+ 'parseddescription' => 'string'
+ ),
+ 'metadata' => array(
+ 'metadata' => 'string'
+ ),
+ 'bitdepth' => array(
+ 'bitdepth' => 'integer'
+ ),
+ 'mime' => array(
+ 'mime' => 'string'
+ ),
+ 'mediatype' => array(
+ 'mediatype' => 'string'
+ ),
+ 'archivename' => array(
+ 'archivename' => 'string'
+ ),
);
}
@@ -277,6 +370,7 @@ 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' ),
) );
}
diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php
index feda1779..c5012f08 100644
--- a/includes/api/ApiQueryIWBacklinks.php
+++ b/includes/api/ApiQueryIWBacklinks.php
@@ -5,7 +5,7 @@
* Created on May 14, 2010
*
* Copyright © 2010 Sam Reed
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -61,15 +61,17 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
'original value returned by the previous query', '_badcontinue' );
}
- $prefix = $this->getDB()->strencode( $cont[0] );
- $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $db = $this->getDB();
+ $op = $params['dir'] == 'descending' ? '<' : '>';
+ $prefix = $db->addQuotes( $cont[0] );
+ $title = $db->addQuotes( $cont[1] );
$from = intval( $cont[2] );
$this->addWhere(
- "iwl_prefix > '$prefix' OR " .
- "(iwl_prefix = '$prefix' AND " .
- "(iwl_title > '$title' OR " .
- "(iwl_title = '$title' AND " .
- "iwl_from >= $from)))"
+ "iwl_prefix $op $prefix OR " .
+ "(iwl_prefix = $prefix AND " .
+ "(iwl_title $op $title OR " .
+ "(iwl_title = $title AND " .
+ "iwl_from $op= $from)))"
);
}
@@ -83,16 +85,24 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
$this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect',
'iwl_from', 'iwl_prefix', 'iwl_title' ) );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
if ( isset( $params['prefix'] ) ) {
$this->addWhereFld( 'iwl_prefix', $params['prefix'] );
if ( isset( $params['title'] ) ) {
$this->addWhereFld( 'iwl_title', $params['title'] );
- $this->addOption( 'ORDER BY', 'iwl_from' );
+ $this->addOption( 'ORDER BY', 'iwl_from' . $sort );
} else {
- $this->addOption( 'ORDER BY', 'iwl_title, iwl_from' );
+ $this->addOption( 'ORDER BY', array(
+ 'iwl_title' . $sort,
+ 'iwl_from' . $sort
+ ));
}
} else {
- $this->addOption( 'ORDER BY', 'iwl_prefix, iwl_title, iwl_from' );
+ $this->addOption( 'ORDER BY', array(
+ 'iwl_prefix' . $sort,
+ 'iwl_title' . $sort,
+ 'iwl_from' . $sort
+ ));
}
$this->addOption( 'LIMIT', $params['limit'] + 1 );
@@ -170,6 +180,13 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
'iwtitle',
),
),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
+ ),
);
}
@@ -184,6 +201,24 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
' iwtitle - Adds the title of the interwiki',
),
'limit' => 'How many total pages to return',
+ 'dir' => 'The direction in which to list',
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'pageid' => 'integer',
+ 'ns' => 'namespace',
+ 'title' => 'string',
+ 'redirect' => 'boolean'
+ ),
+ 'iwprefix' => array(
+ 'iwprefix' => 'string'
+ ),
+ 'iwtitle' => array(
+ 'iwtitle' => 'string'
+ )
);
}
@@ -205,7 +240,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
public function getExamples() {
return array(
'api.php?action=query&list=iwbacklinks&iwbltitle=Test&iwblprefix=wikibooks',
- 'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&iwblprefix=wikibooks&prop=info'
+ 'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&giwblprefix=wikibooks&prop=info'
);
}
diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php
index 13256ad8..30c7f5a8 100644
--- a/includes/api/ApiQueryIWLinks.php
+++ b/includes/api/ApiQueryIWLinks.php
@@ -5,7 +5,7 @@
* Created on May 14, 2010
*
* Copyright © 2010 Sam Reed
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -62,38 +62,40 @@ class ApiQueryIWLinks extends ApiQueryBase {
$this->dieUsage( 'Invalid continue param. You should pass the ' .
'original value returned by the previous query', '_badcontinue' );
}
+ $op = $params['dir'] == 'descending' ? '<' : '>';
+ $db = $this->getDB();
$iwlfrom = intval( $cont[0] );
- $iwlprefix = $this->getDB()->strencode( $cont[1] );
- $iwltitle = $this->getDB()->strencode( $this->titleToKey( $cont[2] ) );
+ $iwlprefix = $db->addQuotes( $cont[1] );
+ $iwltitle = $db->addQuotes( $cont[2] );
$this->addWhere(
- "iwl_from > $iwlfrom OR " .
+ "iwl_from $op $iwlfrom OR " .
"(iwl_from = $iwlfrom AND " .
- "(iwl_prefix > '$iwlprefix' OR " .
- "(iwl_prefix = '$iwlprefix' AND " .
- "iwl_title >= '$iwltitle')))"
+ "(iwl_prefix $op $iwlprefix OR " .
+ "(iwl_prefix = $iwlprefix AND " .
+ "iwl_title $op= $iwltitle)))"
);
}
- $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
if ( isset( $params['prefix'] ) ) {
$this->addWhereFld( 'iwl_prefix', $params['prefix'] );
if ( isset( $params['title'] ) ) {
$this->addWhereFld( 'iwl_title', $params['title'] );
- $this->addOption( 'ORDER BY', 'iwl_from' . $dir );
+ $this->addOption( 'ORDER BY', 'iwl_from' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'iwl_title' . $dir,
- 'iwl_from' . $dir
+ 'iwl_title' . $sort,
+ 'iwl_from' . $sort
));
}
} else {
// Don't order by iwl_from if it's constant in the WHERE clause
if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) {
- $this->addOption( 'ORDER BY', 'iwl_prefix' . $dir );
+ $this->addOption( 'ORDER BY', 'iwl_prefix' . $sort );
} else {
$this->addOption( 'ORDER BY', array (
- 'iwl_from' . $dir,
- 'iwl_prefix' . $dir
+ 'iwl_from' . $sort,
+ 'iwl_prefix' . $sort
));
}
}
@@ -165,6 +167,19 @@ class ApiQueryIWLinks extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'prefix' => 'string',
+ 'url' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ '*' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Returns all interwiki links from the given page(s)';
}
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index 03a24821..d822eed5 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -4,7 +4,7 @@
*
* Created on July 6, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -73,7 +73,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
$result = $this->getResult();
- $images = RepoGroup::singleton()->findFiles( $titles );
+ //search only inside the local repo
+ 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() ) {
@@ -81,14 +86,14 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
$start = $skip ? $fromTimestamp : $params['start'];
- $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ];
+ $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ];
$fit = $result->addValue(
array( 'query', 'pages', intval( $pageId ) ),
'imagerepository', $img->getRepoName()
);
if ( !$fit ) {
- if ( count( $pageIds[NS_IMAGE] ) == 1 ) {
+ if ( count( $pageIds[NS_FILE] ) == 1 ) {
// The user is screwed. imageinfo can't be solely
// responsible for exceeding the limit in this case,
// so set a query-continue that just returns the same
@@ -119,7 +124,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
self::getInfo( $img, $prop, $result,
$finalThumbParams, $params['metadataversion'] ) );
if ( !$fit ) {
- if ( count( $pageIds[NS_IMAGE] ) == 1 ) {
+ if ( count( $pageIds[NS_FILE] ) == 1 ) {
// See the 'the user is screwed' comment above
$this->setContinueEnumParameter( 'start',
wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) );
@@ -149,7 +154,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
self::getInfo( $oldie, $prop, $result,
$finalThumbParams, $params['metadataversion'] ) );
if ( !$fit ) {
- if ( count( $pageIds[NS_IMAGE] ) == 1 ) {
+ if ( count( $pageIds[NS_FILE] ) == 1 ) {
$this->setContinueEnumParameter( 'start',
wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) );
} else {
@@ -356,8 +361,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) {
list( $ext, $mime ) = $file->getHandler()->getThumbType(
- substr( $mto->getPath(), strrpos( $mto->getPath(), '.' ) + 1 ),
- $file->getMimeType(), $thumbParams );
+ $mto->getExtension(), $file->getMimeType(), $thumbParams );
$vals['thumbmime'] = $mime;
}
} elseif ( $mto && $mto->isError() ) {
@@ -430,7 +434,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
* @param $img File
* @return string
*/
- private function getContinueStr( $img ) {
+ protected function getContinueStr( $img ) {
return $img->getOriginalTitle()->getText() .
'|' . $img->getTimestamp();
}
@@ -472,6 +476,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
ApiBase::PARAM_TYPE => 'string',
),
'continue' => null,
+ 'localonly' => false,
);
}
@@ -491,7 +496,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
*
* @return array
*/
- private static function getProperties() {
+ private static function getProperties( $modulePrefix = '' ) {
return array(
'timestamp' => ' timestamp - Adds timestamp for the uploaded version',
'user' => ' user - Adds the user who uploaded the image version',
@@ -503,7 +508,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
'dimensions' => ' dimensions - Alias for size', // For backwards compatibility with Allimages
'sha1' => ' sha1 - Adds SHA-1 hash for the image',
'mime' => ' mime - Adds MIME type of the image',
- 'thumbmime' => ' thumbmime - Adds MIME type of the image thumbnail (requires url)',
+ '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',
'archivename' => ' archivename - Adds the file name of the archive version for non-latest versions',
@@ -518,10 +524,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
*
* @return array
*/
- public static function getPropertyDescriptions( $filter = array() ) {
+ public static function getPropertyDescriptions( $filter = array(), $modulePrefix = '' ) {
return array_merge(
array( 'What image information to get:' ),
- array_values( array_diff_key( self::getProperties(), array_flip( $filter ) ) )
+ array_values( array_diff_key( self::getProperties( $modulePrefix ), array_flip( $filter ) ) )
);
}
@@ -532,7 +538,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
public function getParamDescription() {
$p = $this->getModulePrefix();
return array(
- 'prop' => self::getPropertyDescriptions(),
+ '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",
@@ -543,10 +549,119 @@ class ApiQueryImageInfo extends ApiQueryBase {
'end' => 'Timestamp to stop listing at',
'metadataversion' => array( "Version of metadata to use. if 'latest' is specified, use latest version.",
"Defaults to '1' for backwards compatibility" ),
- 'continue' => 'If the query response includes a continue value, use it here to get another page of results'
+ 'continue' => 'If the query response includes a continue value, use it here to get another page of results',
+ 'localonly' => 'Look only for files in the local repository',
);
}
+ public static function getResultPropertiesFiltered( $filter = array() ) {
+ $props = array(
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'user' => array(
+ 'userhidden' => 'boolean',
+ 'user' => 'string',
+ 'anon' => 'boolean'
+ ),
+ 'userid' => array(
+ 'userhidden' => 'boolean',
+ 'userid' => 'integer',
+ 'anon' => 'boolean'
+ ),
+ 'size' => array(
+ 'size' => 'integer',
+ 'width' => 'integer',
+ 'height' => 'integer',
+ 'pagecount' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'comment' => array(
+ 'commenthidden' => 'boolean',
+ 'comment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'parsedcomment' => array(
+ 'commenthidden' => 'boolean',
+ 'parsedcomment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'url' => array(
+ 'filehidden' => 'boolean',
+ 'thumburl' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'thumbwidth' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'thumbheight' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'thumberror' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'url' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'descriptionurl' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'sha1' => array(
+ 'filehidden' => 'boolean',
+ 'sha1' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'mime' => array(
+ 'filehidden' => 'boolean',
+ 'mime' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'mediatype' => array(
+ 'filehidden' => 'boolean',
+ 'mediatype' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'archivename' => array(
+ 'filehidden' => 'boolean',
+ 'archivename' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'bitdepth' => array(
+ 'filehidden' => 'boolean',
+ 'bitdepth' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ );
+ return array_diff_key( $props, array_flip( $filter ) );
+ }
+
+ public function getResultProperties() {
+ return self::getResultPropertiesFiltered();
+ }
+
public function getDescription() {
return 'Returns image information and upload history';
}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
index f03b2874..6052a75f 100644
--- a/includes/api/ApiQueryImages.php
+++ b/includes/api/ApiQueryImages.php
@@ -4,7 +4,7 @@
*
* Created on May 13, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -25,7 +25,8 @@
*/
/**
- * This query adds an <images> subelement to all pages with the list of images embedded into those pages.
+ * This query adds an "<images>" subelement to all pages with the list of
+ * images embedded into those pages.
*
* @ingroup API
*/
@@ -65,23 +66,24 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$this->dieUsage( 'Invalid continue param. You should pass the ' .
'original value returned by the previous query', '_badcontinue' );
}
+ $op = $params['dir'] == 'descending' ? '<' : '>';
$ilfrom = intval( $cont[0] );
- $ilto = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $ilto = $this->getDB()->addQuotes( $cont[1] );
$this->addWhere(
- "il_from > $ilfrom OR " .
+ "il_from $op $ilfrom OR " .
"(il_from = $ilfrom AND " .
- "il_to >= '$ilto')"
+ "il_to $op= $ilto)"
);
}
- $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
// Don't order by il_from if it's constant in the WHERE clause
if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) {
- $this->addOption( 'ORDER BY', 'il_to' . $dir );
+ $this->addOption( 'ORDER BY', 'il_to' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'il_from' . $dir,
- 'il_to' . $dir
+ 'il_from' . $sort,
+ 'il_to' . $sort
));
}
$this->addOption( 'LIMIT', $params['limit'] + 1 );
@@ -107,16 +109,14 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'continue', $row->il_from .
- '|' . $this->keyToTitle( $row->il_to ) );
+ $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to );
break;
}
$vals = array();
ApiQueryBase::addTitleInfo( $vals, Title::makeTitle( NS_FILE, $row->il_to ) );
$fit = $this->addPageSubItem( $row->il_from, $vals );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'continue', $row->il_from .
- '|' . $this->keyToTitle( $row->il_to ) );
+ $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to );
break;
}
}
@@ -127,8 +127,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'continue', $row->il_from .
- '|' . $this->keyToTitle( $row->il_to ) );
+ $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to );
break;
}
$titles[] = Title::makeTitle( NS_FILE, $row->il_to );
@@ -173,6 +172,15 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Returns all images contained on the given page(s)';
}
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index f0d0faa3..5d4f0346 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -4,7 +4,7 @@
*
* Created on Sep 25, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -33,7 +33,7 @@ class ApiQueryInfo extends ApiQueryBase {
private $fld_protection = false, $fld_talkid = false,
$fld_subjectid = false, $fld_url = false,
- $fld_readable = false, $fld_watched = false,
+ $fld_readable = false, $fld_watched = false, $fld_notificationtimestamp = false,
$fld_preload = false, $fld_displaytitle = false;
private $params, $titles, $missing, $everything, $pageCounter;
@@ -41,7 +41,7 @@ class ApiQueryInfo extends ApiQueryBase {
private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched,
$pageLatest, $pageLength;
- private $protections, $watched, $talkids, $subjectids, $displaytitles;
+ private $protections, $watched, $notificationtimestamps, $talkids, $subjectids, $displaytitles;
private $tokenFunctions;
@@ -57,7 +57,10 @@ class ApiQueryInfo extends ApiQueryBase {
global $wgDisableCounters;
$pageSet->requestField( 'page_restrictions' );
- $pageSet->requestField( 'page_is_redirect' );
+ // when resolving redirects, no page will have this field
+ if( !$pageSet->isResolvingRedirects() ) {
+ $pageSet->requestField( 'page_is_redirect' );
+ }
$pageSet->requestField( 'page_is_new' );
if ( !$wgDisableCounters ) {
$pageSet->requestField( 'page_counter' );
@@ -99,6 +102,12 @@ class ApiQueryInfo extends ApiQueryBase {
return $this->tokenFunctions;
}
+ static $cachedTokens = array();
+
+ public static function resetTokenCache() {
+ ApiQueryInfo::$cachedTokens = array();
+ }
+
public static function getEditToken( $pageid, $title ) {
// We could check for $title->userCan('edit') here,
// but that's too expensive for this purpose
@@ -108,14 +117,12 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- // The edit token is always the same, let's exploit that
- static $cachedEditToken = null;
- if ( !is_null( $cachedEditToken ) ) {
- return $cachedEditToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'edit' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'edit' ] = $wgUser->getEditToken();
}
- $cachedEditToken = $wgUser->getEditToken();
- return $cachedEditToken;
+ return ApiQueryInfo::$cachedTokens[ 'edit' ];
}
public static function getDeleteToken( $pageid, $title ) {
@@ -124,13 +131,12 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- static $cachedDeleteToken = null;
- if ( !is_null( $cachedDeleteToken ) ) {
- return $cachedDeleteToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'delete' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'delete' ] = $wgUser->getEditToken();
}
- $cachedDeleteToken = $wgUser->getEditToken();
- return $cachedDeleteToken;
+ return ApiQueryInfo::$cachedTokens[ 'delete' ];
}
public static function getProtectToken( $pageid, $title ) {
@@ -139,13 +145,12 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- static $cachedProtectToken = null;
- if ( !is_null( $cachedProtectToken ) ) {
- return $cachedProtectToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'protect' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'protect' ] = $wgUser->getEditToken();
}
- $cachedProtectToken = $wgUser->getEditToken();
- return $cachedProtectToken;
+ return ApiQueryInfo::$cachedTokens[ 'protect' ];
}
public static function getMoveToken( $pageid, $title ) {
@@ -154,13 +159,12 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- static $cachedMoveToken = null;
- if ( !is_null( $cachedMoveToken ) ) {
- return $cachedMoveToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'move' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'move' ] = $wgUser->getEditToken();
}
- $cachedMoveToken = $wgUser->getEditToken();
- return $cachedMoveToken;
+ return ApiQueryInfo::$cachedTokens[ 'move' ];
}
public static function getBlockToken( $pageid, $title ) {
@@ -169,13 +173,12 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- static $cachedBlockToken = null;
- if ( !is_null( $cachedBlockToken ) ) {
- return $cachedBlockToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'block' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'block' ] = $wgUser->getEditToken();
}
- $cachedBlockToken = $wgUser->getEditToken();
- return $cachedBlockToken;
+ return ApiQueryInfo::$cachedTokens[ 'block' ];
}
public static function getUnblockToken( $pageid, $title ) {
@@ -189,13 +192,12 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- static $cachedEmailToken = null;
- if ( !is_null( $cachedEmailToken ) ) {
- return $cachedEmailToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'email' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'email' ] = $wgUser->getEditToken();
}
- $cachedEmailToken = $wgUser->getEditToken();
- return $cachedEmailToken;
+ return ApiQueryInfo::$cachedTokens[ 'email' ];
}
public static function getImportToken( $pageid, $title ) {
@@ -204,13 +206,12 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- static $cachedImportToken = null;
- if ( !is_null( $cachedImportToken ) ) {
- return $cachedImportToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'import' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'import' ] = $wgUser->getEditToken();
}
- $cachedImportToken = $wgUser->getEditToken();
- return $cachedImportToken;
+ return ApiQueryInfo::$cachedTokens[ 'import' ];
}
public static function getWatchToken( $pageid, $title ) {
@@ -219,13 +220,26 @@ class ApiQueryInfo extends ApiQueryBase {
return false;
}
- static $cachedWatchToken = null;
- if ( !is_null( $cachedWatchToken ) ) {
- return $cachedWatchToken;
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'watch' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'watch' ] = $wgUser->getEditToken( 'watch' );
}
- $cachedWatchToken = $wgUser->getEditToken( 'watch' );
- return $cachedWatchToken;
+ return ApiQueryInfo::$cachedTokens[ 'watch' ];
+ }
+
+ public static function getOptionsToken( $pageid, $title ) {
+ global $wgUser;
+ if ( !$wgUser->isLoggedIn() ) {
+ return false;
+ }
+
+ // The token is always the same, let's exploit that
+ if ( !isset( ApiQueryInfo::$cachedTokens[ 'options' ] ) ) {
+ ApiQueryInfo::$cachedTokens[ 'options' ] = $wgUser->getEditToken();
+ }
+
+ return ApiQueryInfo::$cachedTokens[ 'options' ];
}
public function execute() {
@@ -234,6 +248,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_notificationtimestamp = isset( $prop['notificationtimestamp'] );
$this->fld_talkid = isset( $prop['talkid'] );
$this->fld_subjectid = isset( $prop['subjectid'] );
$this->fld_url = isset( $prop['url'] );
@@ -269,7 +284,10 @@ class ApiQueryInfo extends ApiQueryBase {
}
$this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
- $this->pageIsRedir = $pageSet->getCustomField( 'page_is_redirect' );
+ // when resolving redirects, no page will have this field
+ $this->pageIsRedir = !$pageSet->isResolvingRedirects()
+ ? $pageSet->getCustomField( 'page_is_redirect' )
+ : array();
$this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
global $wgDisableCounters;
@@ -286,7 +304,7 @@ class ApiQueryInfo extends ApiQueryBase {
$this->getProtectionInfo();
}
- if ( $this->fld_watched ) {
+ if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
$this->getWatchedInfo();
}
@@ -322,7 +340,10 @@ class ApiQueryInfo extends ApiQueryBase {
*/
private function extractPageInfo( $pageid, $title ) {
$pageInfo = array();
- if ( $title->exists() ) {
+ $titleExists = $pageid > 0; //$title->exists() needs pageid, which is not set for all title objects
+ $ns = $title->getNamespace();
+ $dbkey = $title->getDBkey();
+ if ( $titleExists ) {
global $wgDisableCounters;
$pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
@@ -332,7 +353,7 @@ class ApiQueryInfo extends ApiQueryBase {
: intval( $this->pageCounter[$pageid] );
$pageInfo['length'] = intval( $this->pageLength[$pageid] );
- if ( $this->pageIsRedir[$pageid] ) {
+ if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
$pageInfo['redirect'] = '';
}
if ( $this->pageIsNew[$pageid] ) {
@@ -355,23 +376,30 @@ class ApiQueryInfo extends ApiQueryBase {
if ( $this->fld_protection ) {
$pageInfo['protection'] = array();
- if ( isset( $this->protections[$title->getNamespace()][$title->getDBkey()] ) ) {
+ if ( isset( $this->protections[$ns][$dbkey] ) ) {
$pageInfo['protection'] =
- $this->protections[$title->getNamespace()][$title->getDBkey()];
+ $this->protections[$ns][$dbkey];
}
$this->getResult()->setIndexedTagName( $pageInfo['protection'], 'pr' );
}
- if ( $this->fld_watched && isset( $this->watched[$title->getNamespace()][$title->getDBkey()] ) ) {
+ if ( $this->fld_watched && isset( $this->watched[$ns][$dbkey] ) ) {
$pageInfo['watched'] = '';
}
- if ( $this->fld_talkid && isset( $this->talkids[$title->getNamespace()][$title->getDBkey()] ) ) {
- $pageInfo['talkid'] = $this->talkids[$title->getNamespace()][$title->getDBkey()];
+ if ( $this->fld_notificationtimestamp ) {
+ $pageInfo['notificationtimestamp'] = '';
+ if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) {
+ $pageInfo['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
+ }
+ }
+
+ if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
+ $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
}
- if ( $this->fld_subjectid && isset( $this->subjectids[$title->getNamespace()][$title->getDBkey()] ) ) {
- $pageInfo['subjectid'] = $this->subjectids[$title->getNamespace()][$title->getDBkey()];
+ if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
+ $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
}
if ( $this->fld_url ) {
@@ -383,7 +411,7 @@ class ApiQueryInfo extends ApiQueryBase {
}
if ( $this->fld_preload ) {
- if ( $title->exists() ) {
+ if ( $titleExists ) {
$pageInfo['preload'] = '';
} else {
$text = null;
@@ -394,8 +422,8 @@ class ApiQueryInfo extends ApiQueryBase {
}
if ( $this->fld_displaytitle ) {
- if ( isset( $this->displaytitles[$title->getArticleId()] ) ) {
- $pageInfo['displaytitle'] = $this->displaytitles[$title->getArticleId()];
+ if ( isset( $this->displaytitles[$pageid] ) ) {
+ $pageInfo['displaytitle'] = $this->displaytitles[$pageid];
} else {
$pageInfo['displaytitle'] = $title->getPrefixedText();
}
@@ -415,15 +443,14 @@ class ApiQueryInfo extends ApiQueryBase {
// Get normal protections for existing titles
if ( count( $this->titles ) ) {
$this->resetQueryParams();
- $this->addTables( array( 'page_restrictions', 'page' ) );
- $this->addWhere( 'page_id=pr_page' );
+ $this->addTables( 'page_restrictions' );
$this->addFields( array( 'pr_page', 'pr_type', 'pr_level',
- 'pr_expiry', 'pr_cascade', 'page_namespace',
- 'page_title' ) );
+ 'pr_expiry', 'pr_cascade' ) );
$this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
$res = $this->select( __METHOD__ );
foreach ( $res as $row ) {
+ $title = $this->titles[$row->pr_page];
$a = array(
'type' => $row->pr_type,
'level' => $row->pr_level,
@@ -432,11 +459,14 @@ class ApiQueryInfo extends ApiQueryBase {
if ( $row->pr_cascade ) {
$a['cascade'] = '';
}
- $this->protections[$row->page_namespace][$row->page_title][] = $a;
-
- // Also check old restrictions
- if ( $this->pageRestrictions[$row->pr_page] ) {
- $restrictions = explode( ':', trim( $this->pageRestrictions[$row->pr_page] ) );
+ $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
+ }
+ // Also check old restrictions
+ foreach( $this->titles as $pageId => $title ) {
+ if ( $this->pageRestrictions[$pageId] ) {
+ $namespace = $title->getNamespace();
+ $dbKey = $title->getDBkey();
+ $restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) );
foreach ( $restrictions as $restrict ) {
$temp = explode( '=', trim( $restrict ) );
if ( count( $temp ) == 1 ) {
@@ -446,12 +476,12 @@ class ApiQueryInfo extends ApiQueryBase {
if ( $restriction == '' ) {
continue;
}
- $this->protections[$row->page_namespace][$row->page_title][] = array(
+ $this->protections[$namespace][$dbKey][] = array(
'type' => 'edit',
'level' => $restriction,
'expiry' => 'infinity',
);
- $this->protections[$row->page_namespace][$row->page_title][] = array(
+ $this->protections[$namespace][$dbKey][] = array(
'type' => 'move',
'level' => $restriction,
'expiry' => 'infinity',
@@ -461,7 +491,7 @@ class ApiQueryInfo extends ApiQueryBase {
if ( $restriction == '' ) {
continue;
}
- $this->protections[$row->page_namespace][$row->page_title][] = array(
+ $this->protections[$namespace][$dbKey][] = array(
'type' => $temp[0],
'level' => $restriction,
'expiry' => 'infinity',
@@ -612,6 +642,7 @@ class ApiQueryInfo extends ApiQueryBase {
/**
* Get information about watched status and put it in $this->watched
+ * and $this->notificationtimestamps
*/
private function getWatchedInfo() {
$user = $this->getUser();
@@ -621,6 +652,7 @@ class ApiQueryInfo extends ApiQueryBase {
}
$this->watched = array();
+ $this->notificationtimestamps = array();
$db = $this->getDB();
$lb = new LinkBatch( $this->everything );
@@ -628,6 +660,7 @@ class ApiQueryInfo extends ApiQueryBase {
$this->resetQueryParams();
$this->addTables( array( 'watchlist' ) );
$this->addFields( array( 'wl_title', 'wl_namespace' ) );
+ $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp );
$this->addWhere( array(
$lb->constructSet( 'wl', $db ),
'wl_user' => $user->getID()
@@ -636,7 +669,12 @@ class ApiQueryInfo extends ApiQueryBase {
$res = $this->select( __METHOD__ );
foreach ( $res as $row ) {
- $this->watched[$row->wl_namespace][$row->wl_title] = true;
+ if ( $this->fld_watched ) {
+ $this->watched[$row->wl_namespace][$row->wl_title] = true;
+ }
+ if ( $this->fld_notificationtimestamp ) {
+ $this->notificationtimestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp;
+ }
}
}
@@ -671,6 +709,7 @@ class ApiQueryInfo extends ApiQueryBase {
'protection',
'talkid',
'watched', # private
+ 'notificationtimestamp', # private
'subjectid',
'url',
'readable', # private
@@ -692,20 +731,80 @@ class ApiQueryInfo extends ApiQueryBase {
return array(
'prop' => array(
'Which additional properties to get:',
- ' protection - List the protection level of each page',
- ' talkid - The page ID of the talk page for each non-talk page',
- ' watched - List the watched status of each page',
- ' subjectid - The page ID of the parent page for each talk page',
- ' url - Gives a full URL to the page, and also an edit URL',
- ' readable - Whether the user can read this page',
- ' preload - Gives the text returned by EditFormPreloadText',
- ' displaytitle - Gives the way the page title is actually displayed',
+ ' 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',
+ ' 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',
+ ' readable - Whether the user can read this page',
+ ' preload - Gives the text returned by EditFormPreloadText',
+ ' displaytitle - Gives the way the page title is actually displayed',
),
'token' => 'Request a token to perform a data-modifying action on a page',
'continue' => 'When more results are available, use this to continue',
);
}
+ public function getResultProperties() {
+ $props = array(
+ ApiBase::PROP_LIST => false,
+ '' => array(
+ 'touched' => 'timestamp',
+ 'lastrevid' => 'integer',
+ 'counter' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'length' => 'integer',
+ 'redirect' => 'boolean',
+ 'new' => 'boolean',
+ 'starttimestamp' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'watched' => array(
+ 'watched' => 'boolean'
+ ),
+ 'notificationtimestamp' => array(
+ 'notificationtimestamp' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'talkid' => array(
+ 'talkid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'subjectid' => array(
+ 'subjectid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'url' => array(
+ 'fullurl' => 'string',
+ 'editurl' => 'string'
+ ),
+ 'readable' => array(
+ 'readable' => 'boolean'
+ ),
+ 'preload' => array(
+ 'preload' => 'string'
+ ),
+ 'displaytitle' => array(
+ 'displaytitle' => 'string'
+ )
+ );
+
+ self::addTokenProperties( $props, $this->getTokenFunctions() );
+
+ return $props;
+ }
+
public function getDescription() {
return 'Get basic page information such as namespace, title, last touched date, ...';
}
diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php
index 15734944..3920407b 100644
--- a/includes/api/ApiQueryLangBacklinks.php
+++ b/includes/api/ApiQueryLangBacklinks.php
@@ -5,7 +5,7 @@
* Created on May 14, 2011
*
* Copyright © 2011 Sam Reed
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -61,15 +61,17 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
'original value returned by the previous query', '_badcontinue' );
}
- $prefix = $this->getDB()->strencode( $cont[0] );
- $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $db = $this->getDB();
+ $op = $params['dir'] == 'descending' ? '<' : '>';
+ $prefix = $db->addQuotes( $cont[0] );
+ $title = $db->addQuotes( $cont[1] );
$from = intval( $cont[2] );
$this->addWhere(
- "ll_lang > '$prefix' OR " .
- "(ll_lang = '$prefix' AND " .
- "(ll_title > '$title' OR " .
- "(ll_title = '$title' AND " .
- "ll_from >= $from)))"
+ "ll_lang $op $prefix OR " .
+ "(ll_lang = $prefix AND " .
+ "(ll_title $op $title OR " .
+ "(ll_title = $title AND " .
+ "ll_from $op= $from)))"
);
}
@@ -83,16 +85,24 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
$this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect',
'll_from', 'll_lang', 'll_title' ) );
+ $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' );
+ $this->addOption( 'ORDER BY', 'll_from' . $sort );
} else {
- $this->addOption( 'ORDER BY', 'll_title, ll_from' );
+ $this->addOption( 'ORDER BY', array(
+ 'll_title' . $sort,
+ 'll_from' . $sort
+ ));
}
} else {
- $this->addOption( 'ORDER BY', 'll_lang, ll_title, ll_from' );
+ $this->addOption( 'ORDER BY', array(
+ 'll_lang' . $sort,
+ 'll_title' . $sort,
+ 'll_from' . $sort
+ ));
}
$this->addOption( 'LIMIT', $params['limit'] + 1 );
@@ -170,6 +180,13 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
'lltitle',
),
),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
+ ),
);
}
@@ -184,6 +201,24 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
' lltitle - Adds the title of the language ink',
),
'limit' => 'How many total pages to return',
+ 'dir' => 'The direction in which to list',
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'pageid' => 'integer',
+ 'ns' => 'namespace',
+ 'title' => 'string',
+ 'redirect' => 'boolean'
+ ),
+ 'lllang' => array(
+ 'lllang' => 'string'
+ ),
+ 'lltitle' => array(
+ 'lltitle' => 'string'
+ )
);
}
@@ -205,7 +240,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
public function getExamples() {
return array(
'api.php?action=query&list=langbacklinks&lbltitle=Test&lbllang=fr',
- 'api.php?action=query&generator=langbacklinks&glbltitle=Test&lbllang=fr&prop=info'
+ 'api.php?action=query&generator=langbacklinks&glbltitle=Test&glbllang=fr&prop=info'
);
}
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index fdba8465..3109a090 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -4,7 +4,7 @@
*
* Created on May 13, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -60,35 +60,36 @@ class ApiQueryLangLinks extends ApiQueryBase {
$this->dieUsage( 'Invalid continue param. You should pass the ' .
'original value returned by the previous query', '_badcontinue' );
}
+ $op = $params['dir'] == 'descending' ? '<' : '>';
$llfrom = intval( $cont[0] );
- $lllang = $this->getDB()->strencode( $cont[1] );
+ $lllang = $this->getDB()->addQuotes( $cont[1] );
$this->addWhere(
- "ll_from > $llfrom OR " .
+ "ll_from $op $llfrom OR " .
"(ll_from = $llfrom AND " .
- "ll_lang >= '$lllang')"
+ "ll_lang $op= $lllang)"
);
}
- $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' );
- if ( isset( $params['lang'] ) ) {
+ $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' . $dir );
+ $this->addOption( 'ORDER BY', 'll_from' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'll_title' . $dir,
- 'll_from' . $dir
+ 'll_title' . $sort,
+ 'll_from' . $sort
));
}
} else {
// Don't order by ll_from if it's constant in the WHERE clause
if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) {
- $this->addOption( 'ORDER BY', 'll_lang' . $dir );
+ $this->addOption( 'ORDER BY', 'll_lang' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'll_from' . $dir,
- 'll_lang' . $dir
+ 'll_from' . $sort,
+ 'll_lang' . $sort
));
}
}
@@ -158,6 +159,19 @@ class ApiQueryLangLinks extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'lang' => 'string',
+ 'url' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ '*' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Returns all interlanguage links from the given page(s)';
}
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
index 0377eddb..9e4b7ebb 100644
--- a/includes/api/ApiQueryLinks.php
+++ b/includes/api/ApiQueryLinks.php
@@ -4,7 +4,7 @@
*
* Created on May 12, 2007
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -85,9 +85,9 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
$this->addFields( array(
- $this->prefix . '_from AS pl_from',
- $this->prefix . '_namespace AS pl_namespace',
- $this->prefix . '_title AS pl_title'
+ 'pl_from' => $this->prefix . '_from',
+ 'pl_namespace' => $this->prefix . '_namespace',
+ 'pl_title' => $this->prefix . '_title'
) );
$this->addTables( $this->table );
@@ -116,19 +116,20 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$this->dieUsage( 'Invalid continue param. You should pass the ' .
'original value returned by the previous query', '_badcontinue' );
}
+ $op = $params['dir'] == 'descending' ? '<' : '>';
$plfrom = intval( $cont[0] );
$plns = intval( $cont[1] );
- $pltitle = $this->getDB()->strencode( $this->titleToKey( $cont[2] ) );
+ $pltitle = $this->getDB()->addQuotes( $cont[2] );
$this->addWhere(
- "{$this->prefix}_from > $plfrom OR " .
+ "{$this->prefix}_from $op $plfrom OR " .
"({$this->prefix}_from = $plfrom AND " .
- "({$this->prefix}_namespace > $plns OR " .
+ "({$this->prefix}_namespace $op $plns OR " .
"({$this->prefix}_namespace = $plns AND " .
- "{$this->prefix}_title >= '$pltitle')))"
+ "{$this->prefix}_title $op= $pltitle)))"
);
}
- $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
// Here's some MySQL craziness going on: if you use WHERE foo='bar'
// and later ORDER BY foo MySQL doesn't notice the ORDER BY is pointless
// but instead goes and filesorts, because the index for foo was used
@@ -136,13 +137,13 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
// clause from the ORDER BY clause
$order = array();
if ( count( $this->getPageSet()->getGoodTitles() ) != 1 ) {
- $order[] = $this->prefix . '_from' . $dir;
+ $order[] = $this->prefix . '_from' . $sort;
}
if ( count( $params['namespace'] ) != 1 ) {
- $order[] = $this->prefix . '_namespace' . $dir;
+ $order[] = $this->prefix . '_namespace' . $sort;
}
- $order[] = $this->prefix . "_title" . $dir;
+ $order[] = $this->prefix . '_title' . $sort;
$this->addOption( 'ORDER BY', $order );
$this->addOption( 'USE INDEX', $this->prefix . '_from' );
$this->addOption( 'LIMIT', $params['limit'] + 1 );
@@ -156,8 +157,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
$this->setContinueEnumParameter( 'continue',
- "{$row->pl_from}|{$row->pl_namespace}|" .
- $this->keyToTitle( $row->pl_title ) );
+ "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
break;
}
$vals = array();
@@ -165,8 +165,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$fit = $this->addPageSubItem( $row->pl_from, $vals );
if ( !$fit ) {
$this->setContinueEnumParameter( 'continue',
- "{$row->pl_from}|{$row->pl_namespace}|" .
- $this->keyToTitle( $row->pl_title ) );
+ "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
break;
}
}
@@ -178,8 +177,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
$this->setContinueEnumParameter( 'continue',
- "{$row->pl_from}|{$row->pl_namespace}|" .
- $this->keyToTitle( $row->pl_title ) );
+ "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
break;
}
$titles[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
@@ -226,6 +224,15 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return "Returns all {$this->description}s from the given page(s)";
}
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index 0d07a254..5d85c221 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -4,7 +4,7 @@
*
* Created on Oct 16, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -195,6 +195,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
* @param $type string
* @param $action string
* @param $ts
+ * @param $legacy bool
* @return array
*/
public static function addLogParams( $result, &$vals, $params, $type, $action, $ts, $legacy = false ) {
@@ -262,6 +263,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
if ( !is_null( $params ) ) {
$result->setIndexedTagName( $params, 'param' );
+ $result->setIndexedTagName_recursive( $params, 'param' );
$vals = array_merge( $vals, $params );
}
return $vals;
@@ -358,7 +360,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
public function getCacheMode( $params ) {
if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
- // formatComment() calls wfMsg() among other things
+ // formatComment() calls wfMessage() among other things
return 'anon-public-user-private';
} else {
return 'public';
@@ -366,7 +368,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function getAllowedParams() {
- global $wgLogTypes, $wgLogActions;
+ global $wgLogTypes, $wgLogActions, $wgLogActionsHandlers;
return array(
'prop' => array(
ApiBase::PARAM_ISMULTI => true,
@@ -388,7 +390,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
ApiBase::PARAM_TYPE => $wgLogTypes
),
'action' => array(
- ApiBase::PARAM_TYPE => array_keys( $wgLogActions )
+ ApiBase::PARAM_TYPE => array_keys( array_merge( $wgLogActions, $wgLogActionsHandlers ) )
),
'start' => array(
ApiBase::PARAM_TYPE => 'timestamp'
@@ -446,6 +448,62 @@ class ApiQueryLogEvents extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ global $wgLogTypes;
+ return array(
+ 'ids' => array(
+ 'logid' => 'integer',
+ 'pageid' => 'integer'
+ ),
+ 'title' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'type' => array(
+ 'type' => array(
+ ApiBase::PROP_TYPE => $wgLogTypes
+ ),
+ 'action' => 'string'
+ ),
+ 'details' => array(
+ 'actionhidden' => 'boolean'
+ ),
+ 'user' => array(
+ 'userhidden' => 'boolean',
+ 'user' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'anon' => 'boolean'
+ ),
+ 'userid' => array(
+ 'userhidden' => 'boolean',
+ 'userid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'anon' => 'boolean'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'comment' => array(
+ 'commenthidden' => 'boolean',
+ 'comment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'parsedcomment' => array(
+ 'commenthidden' => 'boolean',
+ 'parsedcomment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'Get events from logs';
}
diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php
index 44cc1d32..14aed28d 100644
--- a/includes/api/ApiQueryProtectedTitles.php
+++ b/includes/api/ApiQueryProtectedTitles.php
@@ -4,7 +4,7 @@
*
* Created on Feb 13, 2009
*
- * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2009 Roan Kattouw "<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
@@ -139,7 +139,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
public function getCacheMode( $params ) {
if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
- // formatComment() calls wfMsg() among other things
+ // formatComment() calls wfMessage() among other things
return 'anon-public-user-private';
} else {
return 'public';
@@ -214,6 +214,40 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ global $wgRestrictionLevels;
+ return array(
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'user' => array(
+ 'user' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'userid' => 'integer'
+ ),
+ 'comment' => array(
+ 'comment' => 'string'
+ ),
+ 'parsedcomment' => array(
+ 'parsedcomment' => 'string'
+ ),
+ 'expiry' => array(
+ 'expiry' => 'timestamp'
+ ),
+ 'level' => array(
+ 'level' => array(
+ ApiBase::PROP_TYPE => array_diff( $wgRestrictionLevels, array( '' ) )
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'List all titles protected from creation';
}
diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php
index 5eba0de6..a8be26d3 100644
--- a/includes/api/ApiQueryQueryPage.php
+++ b/includes/api/ApiQueryQueryPage.php
@@ -4,7 +4,7 @@
*
* Created on Dec 22, 2010
*
- * Copyright © 2010 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2010 Roan Kattouw "<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
@@ -70,6 +70,8 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
* @param $resultPageSet ApiPageSet
*/
public function run( $resultPageSet = null ) {
+ global $wgQueryCacheLimit;
+
$params = $this->extractRequestParams();
$result = $this->getResult();
@@ -88,6 +90,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
if ( $ts ) {
$r['cachedtimestamp'] = wfTimestamp( TS_ISO_8601, $ts );
}
+ $r['maxresults'] = $wgQueryCacheLimit;
}
}
$result->addValue( array( 'query' ), $this->getModuleName(), $r );
@@ -170,6 +173,38 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ ApiBase::PROP_ROOT => array(
+ 'name' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => false
+ ),
+ 'disabled' => array(
+ ApiBase::PROP_TYPE => 'boolean',
+ ApiBase::PROP_NULLABLE => false
+ ),
+ 'cached' => array(
+ ApiBase::PROP_TYPE => 'boolean',
+ ApiBase::PROP_NULLABLE => false
+ ),
+ 'cachedtimestamp' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ '' => array(
+ 'value' => 'string',
+ 'timestamp' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Get a list provided by a QueryPage-based special page';
}
diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php
index 2e9e2dd5..ddf5841b 100644
--- a/includes/api/ApiQueryRandom.php
+++ b/includes/api/ApiQueryRandom.php
@@ -161,6 +161,16 @@ class ApiQueryRandom extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'id' => 'integer',
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return array(
'Get a set of random pages',
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index bf5bbd9b..7ae4f371 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -4,7 +4,7 @@
*
* Created on Oct 19, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -70,24 +70,37 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/**
* @param $pageid
* @param $title
- * @param $rc RecentChange
+ * @param $rc RecentChange (optional)
* @return bool|String
*/
- public static function getPatrolToken( $pageid, $title, $rc ) {
+ public static function getPatrolToken( $pageid, $title, $rc = null ) {
global $wgUser;
- if ( !$wgUser->useRCPatrol() && ( !$wgUser->useNPPatrol() ||
- $rc->getAttribute( 'rc_type' ) != RC_NEW ) )
- {
- return false;
+
+ $validTokenUser = false;
+
+ if ( $rc ) {
+ if ( ( $wgUser->useRCPatrol() && $rc->getAttribute( 'rc_type' ) == RC_EDIT ) ||
+ ( $wgUser->useNPPatrol() && $rc->getAttribute( 'rc_type' ) == RC_NEW ) )
+ {
+ $validTokenUser = true;
+ }
+ } else {
+ if ( $wgUser->useRCPatrol() || $wgUser->useNPPatrol() ) {
+ $validTokenUser = true;
+ }
}
- // The patrol token is always the same, let's exploit that
- static $cachedPatrolToken = null;
- if ( is_null( $cachedPatrolToken ) ) {
- $cachedPatrolToken = $wgUser->getEditToken( 'patrol' );
+ if ( $validTokenUser ) {
+ // The patrol token is always the same, let's exploit that
+ static $cachedPatrolToken = null;
+ if ( is_null( $cachedPatrolToken ) ) {
+ $cachedPatrolToken = $wgUser->getEditToken( 'patrol' );
+ }
+ return $cachedPatrolToken;
+ } else {
+ return false;
}
- return $cachedPatrolToken;
}
/**
@@ -131,7 +144,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/* Build our basic query. Namely, something along the lines of:
* SELECT * FROM recentchanges WHERE rc_timestamp > $start
* AND rc_timestamp < $end AND rc_namespace = $namespace
- * AND rc_deleted = '0'
+ * AND rc_deleted = 0
*/
$this->addTables( 'recentchanges' );
$index = array( 'recentchanges' => 'rc_timestamp' ); // May change
@@ -223,7 +236,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$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_new', '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 );
@@ -304,7 +317,7 @@ 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.
- * @return An array mapping strings (descriptors) to their respective string values.
+ * @return array An array mapping strings (descriptors) to their respective string values.
* @access public
*/
public function extractRowInfo( $row ) {
@@ -380,7 +393,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
if ( $row->rc_bot ) {
$vals['bot'] = '';
}
- if ( $row->rc_new ) {
+ if ( $row->rc_type == RC_NEW ) {
$vals['new'] = '';
}
if ( $row->rc_minor ) {
@@ -423,13 +436,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$vals['logid'] = intval( $row->rc_logid );
$vals['logtype'] = $row->rc_log_type;
$vals['logaction'] = $row->rc_log_action;
+ $logEntry = DatabaseLogEntry::newFromRow( (array)$row );
ApiQueryLogEvents::addLogParams(
$this->getResult(),
$vals,
- $row->rc_params,
- $row->rc_log_action,
- $row->rc_log_type,
- $row->rc_timestamp
+ $logEntry->getParameters(),
+ $logEntry->getType(),
+ $logEntry->getSubtype(),
+ $logEntry->getTimestamp()
);
}
@@ -489,7 +503,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
return 'private';
}
if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
- // formatComment() calls wfMsg() among other things
+ // formatComment() calls wfMessage() among other things
return 'anon-public-user-private';
}
return 'public';
@@ -615,6 +629,97 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ global $wgLogTypes;
+ $props = array(
+ '' => array(
+ 'type' => array(
+ ApiBase::PROP_TYPE => array(
+ 'edit',
+ 'new',
+ 'move',
+ 'log',
+ 'move over redirect'
+ )
+ )
+ ),
+ 'title' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string',
+ 'new_ns' => array(
+ ApiBase::PROP_TYPE => 'namespace',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'new_title' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'ids' => array(
+ 'rcid' => 'integer',
+ 'pageid' => 'integer',
+ 'revid' => 'integer',
+ 'old_revid' => 'integer'
+ ),
+ 'user' => array(
+ 'user' => 'string',
+ 'anon' => 'boolean'
+ ),
+ 'userid' => array(
+ 'userid' => 'integer',
+ 'anon' => 'boolean'
+ ),
+ 'flags' => array(
+ 'bot' => 'boolean',
+ 'new' => 'boolean',
+ 'minor' => 'boolean'
+ ),
+ 'sizes' => array(
+ 'oldlen' => 'integer',
+ 'newlen' => 'integer'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'comment' => array(
+ 'comment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'parsedcomment' => array(
+ 'parsedcomment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'redirect' => array(
+ 'redirect' => 'boolean'
+ ),
+ 'patrolled' => array(
+ 'patrolled' => 'boolean'
+ ),
+ 'loginfo' => array(
+ 'logid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'logtype' => array(
+ ApiBase::PROP_TYPE => $wgLogTypes,
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'logaction' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+
+ self::addTokenProperties( $props, $this->getTokenFunctions() );
+
+ return $props;
+ }
+
public function getDescription() {
return 'Enumerate recent changes';
}
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index fa58bdf0..b89a8ea9 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -4,7 +4,7 @@
*
* Created on Sep 7, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -224,6 +224,13 @@ class ApiQueryRevisions extends ApiQueryBase {
}
}
+ // add user name, if needed
+ if ( $this->fld_user ) {
+ $this->addTables( 'user' );
+ $this->addJoinConds( array( 'user' => Revision::userJoinCond() ) );
+ $this->addFields( Revision::selectUserFields() );
+ }
+
// Bug 24166 - API error when using rvprop=tags
$this->addTables( 'revision' );
@@ -241,6 +248,16 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
}
+ // Continuing effectively uses startid. But we can't use rvstartid
+ // directly, because there is no way to tell the client to ''not''
+ // send rvstart if it sent it in the original query. So instead we
+ // send the continuation startid as rvcontinue, and ignore both
+ // rvstart and rvstartid when that is supplied.
+ if ( !is_null( $params['continue'] ) ) {
+ $params['startid'] = $params['continue'];
+ unset( $params['start'] );
+ }
+
// This code makes an assumption that sorting by rev_id and rev_timestamp produces
// the same result. This way users may request revisions starting at a given time,
// but to page through results use the rev_id returned after each page.
@@ -290,7 +307,7 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->addWhereFld( 'rev_id', array_keys( $revs ) );
if ( !is_null( $params['continue'] ) ) {
- $this->addWhere( "rev_id >= '" . intval( $params['continue'] ) . "'" );
+ $this->addWhere( 'rev_id >= ' . intval( $params['continue'] ) );
}
$this->addOption( 'ORDER BY', 'rev_id' );
@@ -322,12 +339,15 @@ class ApiQueryRevisions extends ApiQueryBase {
$pageid = intval( $cont[0] );
$revid = intval( $cont[1] );
$this->addWhere(
- "rev_page > '$pageid' OR " .
- "(rev_page = '$pageid' AND " .
- "rev_id >= '$revid')"
+ "rev_page > $pageid OR " .
+ "(rev_page = $pageid AND " .
+ "rev_id >= $revid)"
);
}
- $this->addOption( 'ORDER BY', 'rev_page, rev_id' );
+ $this->addOption( 'ORDER BY', array(
+ 'rev_page',
+ 'rev_id'
+ ));
// assumption testing -- we should never get more then $pageCount rows.
$limit = $pageCount;
@@ -347,14 +367,14 @@ class ApiQueryRevisions extends ApiQueryBase {
if ( !$enumRevMode ) {
ApiBase::dieDebug( __METHOD__, 'Got more rows then expected' ); // bug report
}
- $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) );
+ $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
break;
}
$fit = $this->addPageSubItem( $row->rev_page, $this->extractRowInfo( $row ), 'rev' );
if ( !$fit ) {
if ( $enumRevMode ) {
- $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) );
+ $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
} elseif ( $revCount > 0 ) {
$this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
} else {
@@ -528,7 +548,7 @@ class ApiQueryRevisions extends ApiQueryBase {
return 'private';
}
if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
- // formatComment() calls wfMsg() among other things
+ // formatComment() calls wfMessage() among other things
return 'anon-public-user-private';
}
return 'public';
@@ -638,6 +658,66 @@ class ApiQueryRevisions extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ $props = array(
+ '' => array(),
+ 'ids' => array(
+ 'revid' => 'integer',
+ 'parentid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'flags' => array(
+ 'minor' => 'boolean'
+ ),
+ 'user' => array(
+ 'userhidden' => 'boolean',
+ 'user' => 'string',
+ 'anon' => 'boolean'
+ ),
+ 'userid' => array(
+ 'userhidden' => 'boolean',
+ 'userid' => 'integer',
+ 'anon' => 'boolean'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'size' => array(
+ 'size' => 'integer'
+ ),
+ 'sha1' => array(
+ 'sha1' => 'string'
+ ),
+ 'comment' => array(
+ 'commenthidden' => 'boolean',
+ 'comment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'parsedcomment' => array(
+ 'commenthidden' => 'boolean',
+ 'parsedcomment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'content' => array(
+ '*' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'texthidden' => 'boolean'
+ )
+ );
+
+ self::addTokenProperties( $props, $this->getTokenFunctions() );
+
+ return $props;
+ }
+
public function getDescription() {
return array(
'Get revision information',
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index 40aac050..364433d5 100644
--- a/includes/api/ApiQuerySearch.php
+++ b/includes/api/ApiQuerySearch.php
@@ -4,7 +4,7 @@
*
* Created on July 30, 2007
*
- * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright © 2007 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
@@ -280,6 +280,63 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'snippet' => array(
+ 'snippet' => 'string'
+ ),
+ 'size' => array(
+ 'size' => 'integer'
+ ),
+ 'wordcount' => array(
+ 'wordcount' => 'integer'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'score' => array(
+ 'score' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'titlesnippet' => array(
+ 'titlesnippet' => 'string'
+ ),
+ 'redirecttitle' => array(
+ 'redirecttitle' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'redirectsnippet' => array(
+ 'redirectsnippet' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'sectiontitle' => array(
+ 'sectiontitle' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'sectionsnippet' => array(
+ 'sectionsnippet' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'hasrelated' => array(
+ 'hasrelated' => 'boolean'
+ )
+ );
+ }
+
public function getDescription() {
return 'Perform a full text search';
}
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index e2580ac6..ec503d64 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -4,7 +4,7 @@
*
* Created on Sep 25, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -93,6 +93,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
case 'showhooks':
$fit = $this->appendSubscribedHooks( $p );
break;
+ case 'variables':
+ $fit = $this->appendVariables( $p );
+ break;
default:
ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" );
}
@@ -121,9 +124,14 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['dbtype'] = $GLOBALS['wgDBtype'];
$data['dbversion'] = $this->getDB()->getServerVersion();
- $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] );
- if ( $svn ) {
- $data['rev'] = $svn;
+ $git = SpecialVersion::getGitHeadSha1( $GLOBALS['IP'] );
+ if ( $git ) {
+ $data['git-hash'] = $git;
+ } else {
+ $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] );
+ if ( $svn ) {
+ $data['rev'] = $svn;
+ }
}
// 'case-insensitive' option is reserved for future
@@ -142,6 +150,15 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['fallback'] = $fallbacks;
$this->getResult()->setIndexedTagName( $data['fallback'], 'lang' );
+ if( $wgContLang->hasVariants() ) {
+ $variants = array();
+ foreach( $wgContLang->getVariants() as $code ) {
+ $variants[] = array( 'code' => $code );
+ }
+ $data['variants'] = $variants;
+ $this->getResult()->setIndexedTagName( $data['variants'], 'lang' );
+ }
+
if ( $wgContLang->isRTL() ) {
$data['rtl'] = '';
}
@@ -177,6 +194,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['misermode'] = '';
}
+ $data['maxuploadsize'] = UploadBase::getMaxUploadSize();
+
wfRunHooks( 'APIQuerySiteInfoGeneralInfo', array( $this, &$data ) );
return $this->getResult()->addValue( 'query', $property, $data );
@@ -204,6 +223,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
if ( MWNamespace::isContent( $ns ) ) {
$data[$ns]['content'] = '';
}
+
+ if ( MWNamespace::isNonincludable( $ns ) ) {
+ $data[$ns]['nonincludable'] = '';
+ }
}
$this->getResult()->setIndexedTagName( $data, 'ns' );
@@ -234,10 +257,13 @@ class ApiQuerySiteinfo extends ApiQueryBase {
protected function appendSpecialPageAliases( $property ) {
global $wgContLang;
$data = array();
- foreach ( $wgContLang->getSpecialPageAliases() as $specialpage => $aliases ) {
- $arr = array( 'realname' => $specialpage, 'aliases' => $aliases );
- $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
- $data[] = $arr;
+ $aliases = $wgContLang->getSpecialPageAliases();
+ foreach ( SpecialPageFactory::getList() as $specialpage => $stuff ) {
+ if ( isset( $aliases[$specialpage] ) ) {
+ $arr = array( 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] );
+ $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
+ $data[] = $arr;
+ }
}
$this->getResult()->setIndexedTagName( $data, 'specialpage' );
return $this->getResult()->addValue( 'query', $property, $data );
@@ -271,12 +297,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$params = $this->extractRequestParams();
$langCode = isset( $params['inlanguagecode'] ) ? $params['inlanguagecode'] : '';
-
- if( $langCode ) {
- $langNames = Language::getTranslatedLanguageNames( $langCode );
- } else {
- $langNames = Language::getLanguageNames();
- }
+ $langNames = Language::fetchLanguageNames( $langCode );
$getPrefixes = Interwiki::getAllPrefixes( $local );
$data = array();
@@ -477,12 +498,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
public function appendLanguages( $property ) {
$params = $this->extractRequestParams();
$langCode = isset( $params['inlanguagecode'] ) ? $params['inlanguagecode'] : '';
-
- if( $langCode ) {
- $langNames = Language::getTranslatedLanguageNames( $langCode );
- } else {
- $langNames = Language::getLanguageNames();
- }
+ $langNames = Language::fetchLanguageNames( $langCode );
$data = array();
@@ -522,6 +538,12 @@ class ApiQuerySiteinfo extends ApiQueryBase {
return $this->getResult()->addValue( 'query', $property, $hooks );
}
+ public function appendVariables( $property ) {
+ $variables = MagicWord::getVariableIDs();
+ $this->getResult()->setIndexedTagName( $variables, 'v' );
+ return $this->getResult()->addValue( 'query', $property, $variables );
+ }
+
private function formatParserTags( $item ) {
return "<{$item}>";
}
@@ -573,6 +595,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
'extensiontags',
'functionhooks',
'showhooks',
+ 'variables',
)
),
'filteriw' => array(
@@ -608,7 +631,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
' 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)'
+ ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)',
+ ' variables - Returns a list of variable IDs',
),
'filteriw' => 'Return only local or only nonlocal entries of the interwiki map',
'showalldb' => 'List all database servers, not just the one lagging the most',
diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php
index 4501ec58..a310d109 100644
--- a/includes/api/ApiQueryStashImageInfo.php
+++ b/includes/api/ApiQueryStashImageInfo.php
@@ -113,7 +113,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
public function getParamDescription() {
$p = $this->getModulePrefix();
return array(
- 'prop' => self::getPropertyDescriptions( $this->propertyFilter ),
+ 'prop' => self::getPropertyDescriptions( $this->propertyFilter, $p ),
'filekey' => 'Key that identifies a previous upload that was stashed temporarily.',
'sessionkey' => 'Alias for filekey, for backward compatibility.',
'urlwidth' => "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.",
@@ -123,6 +123,10 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
);
}
+ public function getResultProperties() {
+ return ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter );
+ }
+
public function getDescription() {
return 'Returns image information for stashed images';
}
diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php
index 12cea1d7..f97c1b2a 100644
--- a/includes/api/ApiQueryTags.php
+++ b/includes/api/ApiQueryTags.php
@@ -59,7 +59,7 @@ class ApiQueryTags extends ApiQueryBase {
$this->addTables( 'change_tag' );
$this->addFields( 'ct_tag' );
- $this->addFieldsIf( 'count(*) AS hitcount', $this->fld_hitcount );
+ $this->addFieldsIf( array( 'hitcount' => 'COUNT(*)' ), $this->fld_hitcount );
$this->addOption( 'LIMIT', $this->limit + 1 );
$this->addOption( 'GROUP BY', 'ct_tag' );
@@ -73,7 +73,7 @@ class ApiQueryTags extends ApiQueryBase {
if ( !$ok ) {
break;
}
- $ok = $this->doTag( $row->ct_tag, $row->hitcount );
+ $ok = $this->doTag( $row->ct_tag, $this->fld_hitcount ? $row->hitcount : 0 );
}
// include tags with no hits yet
@@ -169,6 +169,23 @@ class ApiQueryTags extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'name' => 'string'
+ ),
+ 'displayname' => array(
+ 'displayname' => 'string'
+ ),
+ 'description' => array(
+ 'description' => 'string'
+ ),
+ 'hitcount' => array(
+ 'hitcount' => 'integer'
+ )
+ );
+ }
+
public function getDescription() {
return 'List change tags';
}
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index 8e2f20db..f30b1325 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -4,7 +4,7 @@
*
* Created on Oct 16, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -35,10 +35,10 @@ class ApiQueryContributions extends ApiQueryBase {
parent::__construct( $query, $moduleName, 'uc' );
}
- private $params, $prefixMode, $userprefix, $multiUserMode, $usernames;
+ private $params, $prefixMode, $userprefix, $multiUserMode, $usernames, $parentLens;
private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
$fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
- $fld_patrolled = false, $fld_tags = false, $fld_size = false;
+ $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
public function execute() {
// Parse some parameters
@@ -50,6 +50,7 @@ class ApiQueryContributions extends ApiQueryBase {
$this->fld_comment = isset( $prop['comment'] );
$this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
$this->fld_size = isset( $prop['size'] );
+ $this->fld_sizediff = isset( $prop['sizediff'] );
$this->fld_flags = isset( $prop['flags'] );
$this->fld_timestamp = isset( $prop['timestamp'] );
$this->fld_patrolled = isset( $prop['patrolled'] );
@@ -82,6 +83,17 @@ class ApiQueryContributions extends ApiQueryBase {
// Do the actual query.
$res = $this->select( __METHOD__ );
+ if( $this->fld_sizediff ) {
+ $revIds = array();
+ foreach ( $res as $row ) {
+ if( $row->rev_parent_id ) {
+ $revIds[] = $row->rev_parent_id;
+ }
+ }
+ $this->parentLens = Revision::getParentLengths( $this->getDB(), $revIds );
+ $res->rewind(); // reset
+ }
+
// Initialise some variables
$count = 0;
$limit = $this->params['limit'];
@@ -152,13 +164,14 @@ class ApiQueryContributions extends ApiQueryBase {
$this->dieUsage( 'Invalid continue param. You should pass the original ' .
'value returned by the previous query', '_badcontinue' );
}
- $encUser = $this->getDB()->strencode( $continue[0] );
- $encTS = wfTimestamp( TS_MW, $continue[1] );
+ $db = $this->getDB();
+ $encUser = $db->addQuotes( $continue[0] );
+ $encTS = $db->addQuotes( $db->timestamp( $continue[1] ) );
$op = ( $this->params['dir'] == 'older' ? '<' : '>' );
$this->addWhere(
- "rev_user_text $op '$encUser' OR " .
- "(rev_user_text = '$encUser' AND " .
- "rev_timestamp $op= '$encTS')"
+ "rev_user_text $op $encUser OR " .
+ "(rev_user_text = $encUser AND " .
+ "rev_timestamp $op= $encTS)"
);
}
@@ -185,7 +198,7 @@ class ApiQueryContributions extends ApiQueryBase {
if ( !is_null( $show ) ) {
$show = array_flip( $show );
if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
- || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) ) {
+ || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) ) {
$this->dieUsageMsg( 'show' );
}
@@ -243,8 +256,9 @@ class ApiQueryContributions extends ApiQueryBase {
$this->addFieldsIf( 'page_latest', $this->fld_flags );
// $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
$this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment );
- $this->addFieldsIf( 'rev_len', $this->fld_size );
- $this->addFieldsIf( array( 'rev_minor_edit', 'rev_parent_id' ), $this->fld_flags );
+ $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( 'rc_patrolled', $this->fld_patrolled );
if ( $this->fld_tags ) {
@@ -332,6 +346,11 @@ class ApiQueryContributions extends ApiQueryBase {
$vals['size'] = intval( $row->rev_len );
}
+ if ( $this->fld_sizediff && !is_null( $row->rev_len ) && !is_null( $row->rev_parent_id ) ) {
+ $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) ? $this->parentLens[$row->rev_parent_id] : 0;
+ $vals['sizediff'] = intval( $row->rev_len - $parentLen );
+ }
+
if ( $this->fld_tags ) {
if ( $row->ts_tags ) {
$tags = explode( ',', $row->ts_tags );
@@ -397,6 +416,7 @@ class ApiQueryContributions extends ApiQueryBase {
'comment',
'parsedcomment',
'size',
+ 'sizediff',
'flags',
'patrolled',
'tags'
@@ -435,7 +455,8 @@ class ApiQueryContributions extends ApiQueryBase {
' timestamp - Adds the timestamp of the edit',
' comment - Adds the comment of the edit',
' parsedcomment - Adds the parsed comment of the edit',
- ' size - Adds the size of the page',
+ ' size - Adds the new size of the edit',
+ ' sizediff - Adds the size delta of the edit against its parent',
' flags - Adds flags of the edit',
' patrolled - Tags patrolled edits',
' tags - Lists tags for the edit',
@@ -447,6 +468,61 @@ class ApiQueryContributions extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'userid' => 'integer',
+ 'user' => 'string',
+ 'userhidden' => 'boolean'
+ ),
+ 'ids' => array(
+ 'pageid' => 'integer',
+ 'revid' => 'integer'
+ ),
+ 'title' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'flags' => array(
+ 'new' => 'boolean',
+ 'minor' => 'boolean',
+ 'top' => 'boolean'
+ ),
+ 'comment' => array(
+ 'commenthidden' => 'boolean',
+ 'comment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'parsedcomment' => array(
+ 'commenthidden' => 'boolean',
+ 'parsedcomment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'patrolled' => array(
+ 'patrolled' => 'boolean'
+ ),
+ 'size' => array(
+ 'size' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'sizediff' => array(
+ 'sizediff' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'Get all edits by a user';
}
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index a0ee227f..66906659 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -4,7 +4,7 @@
*
* Created on July 30, 2007
*
- * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright © 2007 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
@@ -50,7 +50,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
protected function getCurrentUserInfo() {
- global $wgRequest, $wgHiddenPrefs;
+ global $wgHiddenPrefs;
$user = $this->getUser();
$result = $this->getResult();
$vals = array();
@@ -63,7 +63,10 @@ class ApiQueryUserInfo extends ApiQueryBase {
if ( isset( $this->prop['blockinfo'] ) ) {
if ( $user->isBlocked() ) {
- $vals['blockedby'] = User::whoIs( $user->blockedBy() );
+ $block = $user->getBlock();
+ $vals['blockid'] = $block->getId();
+ $vals['blockedby'] = $block->getByName();
+ $vals['blockedbyid'] = $block->getBy();
$vals['blockreason'] = $user->blockedFor();
}
}
@@ -73,14 +76,12 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
if ( isset( $this->prop['groups'] ) ) {
- $autolist = ApiQueryUsers::getAutoGroups( $user );
-
- $vals['groups'] = array_merge( $autolist, $user->getGroups() );
+ $vals['groups'] = $user->getEffectiveGroups();
$result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty
}
if ( isset( $this->prop['implicitgroups'] ) ) {
- $vals['implicitgroups'] = ApiQueryUsers::getAutoGroups( $user );
+ $vals['implicitgroups'] = $user->getAutomaticGroups();
$result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
}
@@ -136,7 +137,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
if ( isset( $this->prop['acceptlang'] ) ) {
- $langs = $wgRequest->getAcceptLang();
+ $langs = $this->getRequest()->getAcceptLang();
$acceptLang = array();
foreach ( $langs as $lang => $val ) {
$r = array( 'q' => $val );
@@ -231,6 +232,63 @@ class ApiQueryUserInfo extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ return array(
+ ApiBase::PROP_LIST => false,
+ '' => array(
+ 'id' => 'integer',
+ 'name' => 'string',
+ 'anon' => 'boolean'
+ ),
+ 'blockinfo' => array(
+ 'blockid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedby' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedbyid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedreason' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'hasmsg' => array(
+ 'messages' => 'boolean'
+ ),
+ 'preferencestoken' => array(
+ 'preferencestoken' => 'string'
+ ),
+ 'editcount' => array(
+ 'editcount' => 'integer'
+ ),
+ 'realname' => array(
+ 'realname' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'email' => array(
+ 'email' => 'string',
+ 'emailauthenticated' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'registrationdate' => array(
+ 'registrationdate' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return 'Get information about the current user';
}
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
index 31624bdf..bf438d1d 100644
--- a/includes/api/ApiQueryUsers.php
+++ b/includes/api/ApiQueryUsers.php
@@ -4,7 +4,7 @@
*
* Created on July 30, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -61,10 +61,10 @@ class ApiQueryUsers extends ApiQueryBase {
return $this->tokenFunctions;
}
- /**
- * @param $user User
- * @return String
- */
+ /**
+ * @param $user User
+ * @return String
+ */
public static function getUserrightsToken( $user ) {
global $wgUser;
// Since the permissions check for userrights is non-trivial,
@@ -107,7 +107,7 @@ class ApiQueryUsers extends ApiQueryBase {
if ( count( $goodNames ) ) {
$this->addTables( 'user' );
- $this->addFields( '*' );
+ $this->addFields( User::selectFields() );
$this->addWhereFld( 'user_name', $goodNames );
if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) {
@@ -138,7 +138,7 @@ class ApiQueryUsers extends ApiQueryBase {
if ( isset( $this->prop['groups'] ) ) {
if ( !isset( $data[$name]['groups'] ) ) {
- $data[$name]['groups'] = self::getAutoGroups( $user );
+ $data[$name]['groups'] = $user->getAutomaticGroups();
}
if ( !is_null( $row->ug_group ) ) {
@@ -148,7 +148,7 @@ class ApiQueryUsers extends ApiQueryBase {
}
if ( isset( $this->prop['implicitgroups'] ) && !isset( $data[$name]['implicitgroups'] ) ) {
- $data[$name]['implicitgroups'] = self::getAutoGroups( $user );
+ $data[$name]['implicitgroups'] = $user->getAutomaticGroups();
}
if ( isset( $this->prop['rights'] ) ) {
@@ -165,7 +165,9 @@ class ApiQueryUsers extends ApiQueryBase {
$data[$name]['hidden'] = '';
}
if ( isset( $this->prop['blockinfo'] ) && !is_null( $row->ipb_by_text ) ) {
+ $data[$name]['blockid'] = $row->ipb_id;
$data[$name]['blockedby'] = $row->ipb_by_text;
+ $data[$name]['blockedbyid'] = $row->ipb_by;
$data[$name]['blockreason'] = $row->ipb_reason;
$data[$name]['blockexpiry'] = $row->ipb_expiry;
}
@@ -247,18 +249,15 @@ class ApiQueryUsers extends ApiQueryBase {
/**
* 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 ) {
- $groups = array();
- $groups[] = '*';
+ wfDeprecated( __METHOD__, '1.20' );
- if ( !$user->isAnon() ) {
- $groups[] = 'user';
- }
-
- return array_merge( $groups, Autopromote::getAutopromoteGroups( $user ) );
+ return $user->getAutomaticGroups();
}
public function getCacheMode( $params ) {
@@ -313,6 +312,73 @@ class ApiQueryUsers extends ApiQueryBase {
);
}
+ public function getResultProperties() {
+ $props = array(
+ '' => array(
+ 'userid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'name' => 'string',
+ 'invalid' => 'boolean',
+ 'hidden' => 'boolean',
+ 'interwiki' => 'boolean',
+ 'missing' => 'boolean'
+ ),
+ 'editcount' => array(
+ 'editcount' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'registration' => array(
+ 'registration' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'blockinfo' => array(
+ 'blockid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedby' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedbyid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedreason' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'blockedexpiry' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'emailable' => array(
+ 'emailable' => 'boolean'
+ ),
+ 'gender' => array(
+ 'gender' => array(
+ ApiBase::PROP_TYPE => array(
+ 'male',
+ 'female',
+ 'unknown'
+ ),
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+
+ self::addTokenProperties( $props, $this->getTokenFunctions() );
+
+ return $props;
+ }
+
public function getDescription() {
return 'Get information about a list of users';
}
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index ea56fcd9..a1a33728 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -4,7 +4,7 @@
*
* Created on Sep 25, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -96,7 +96,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'rc_last_oldid',
) );
- $this->addFieldsIf( array( 'rc_new', 'rc_minor', 'rc_bot' ), $this->fld_flags );
+ $this->addFieldsIf( array( 'rc_type', 'rc_minor', 'rc_bot' ), $this->fld_flags );
$this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid );
$this->addFieldsIf( 'rc_user_text', $this->fld_user );
$this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment );
@@ -254,7 +254,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
if ( $this->fld_flags ) {
- if ( $row->rc_new ) {
+ if ( $row->rc_type == RC_NEW ) {
$vals['new'] = '';
}
if ( $row->rc_minor ) {
@@ -296,13 +296,14 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$vals['logid'] = intval( $row->rc_logid );
$vals['logtype'] = $row->rc_log_type;
$vals['logaction'] = $row->rc_log_action;
+ $logEntry = DatabaseLogEntry::newFromRow( (array)$row );
ApiQueryLogEvents::addLogParams(
$this->getResult(),
$vals,
- $row->rc_params,
- $row->rc_log_type,
- $row->rc_log_action,
- $row->rc_timestamp
+ $logEntry->getParameters(),
+ $logEntry->getType(),
+ $logEntry->getSubtype(),
+ $logEntry->getTimestamp()
);
}
@@ -417,6 +418,76 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
);
}
+ public function getResultProperties() {
+ global $wgLogTypes;
+ return array(
+ 'ids' => array(
+ 'pageid' => 'integer',
+ 'revid' => 'integer',
+ 'old_revid' => 'integer'
+ ),
+ 'title' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'user' => array(
+ 'user' => 'string',
+ 'anon' => 'boolean'
+ ),
+ 'userid' => array(
+ 'userid' => 'integer',
+ 'anon' => 'boolean'
+ ),
+ 'flags' => array(
+ 'new' => 'boolean',
+ 'minor' => 'boolean',
+ 'bot' => 'boolean'
+ ),
+ 'patrol' => array(
+ 'patrolled' => 'boolean'
+ ),
+ 'timestamp' => array(
+ 'timestamp' => 'timestamp'
+ ),
+ 'sizes' => array(
+ 'oldlen' => 'integer',
+ 'newlen' => 'integer'
+ ),
+ 'notificationtimestamp' => array(
+ 'notificationtimestamp' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'comment' => array(
+ 'comment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'parsedcomment' => array(
+ 'parsedcomment' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ 'loginfo' => array(
+ 'logid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'logtype' => array(
+ ApiBase::PROP_TYPE => $wgLogTypes,
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'logaction' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return "Get all recent changes to pages in the logged in user's watchlist";
}
diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php
index 506944f0..6b24aef3 100644
--- a/includes/api/ApiQueryWatchlistRaw.php
+++ b/includes/api/ApiQueryWatchlistRaw.php
@@ -4,7 +4,7 @@
*
* Created on Oct 4, 2008
*
- * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2008 Roan Kattouw "<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
@@ -76,19 +76,24 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
"original value returned by the previous query", "_badcontinue" );
}
$ns = intval( $cont[0] );
- $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $title = $this->getDB()->addQuotes( $cont[1] );
+ $op = $params['dir'] == 'ascending' ? '>' : '<';
$this->addWhere(
- "wl_namespace > '$ns' OR " .
- "(wl_namespace = '$ns' AND " .
- "wl_title >= '$title')"
+ "wl_namespace $op $ns OR " .
+ "(wl_namespace = $ns AND " .
+ "wl_title $op= $title)"
);
}
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
// Don't ORDER BY wl_namespace if it's constant in the WHERE clause
if ( count( $params['namespace'] ) == 1 ) {
- $this->addOption( 'ORDER BY', 'wl_title' );
+ $this->addOption( 'ORDER BY', 'wl_title' . $sort );
} else {
- $this->addOption( 'ORDER BY', 'wl_namespace, wl_title' );
+ $this->addOption( 'ORDER BY', array(
+ 'wl_namespace' . $sort,
+ 'wl_title' . $sort
+ ));
}
$this->addOption( 'LIMIT', $params['limit'] + 1 );
$res = $this->select( __METHOD__ );
@@ -98,8 +103,7 @@ class ApiQueryWatchlistRaw 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( 'continue', $row->wl_namespace . '|' .
- $this->keyToTitle( $row->wl_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title );
break;
}
$t = Title::makeTitle( $row->wl_namespace, $row->wl_title );
@@ -113,8 +117,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
}
$fit = $this->getResult()->addValue( $this->getModuleName(), null, $vals );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' .
- $this->keyToTitle( $row->wl_title ) );
+ $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title );
break;
}
} else {
@@ -160,7 +163,14 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
),
'token' => array(
ApiBase::PARAM_TYPE => 'string'
- )
+ ),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ ),
+ ),
);
}
@@ -176,6 +186,22 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
'show' => 'Only list items that meet these criteria',
'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',
+ 'dir' => 'Direction to sort the titles and namespaces in',
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'ns' => 'namespace',
+ 'title' => 'string'
+ ),
+ 'changed' => array(
+ 'changed' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
);
}
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index 798b2275..91e20812 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -4,7 +4,7 @@
*
* Created on Sep 4, 2006
*
- * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * 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
@@ -165,7 +165,7 @@ class ApiResult extends ApiBase {
* @param $value Mixed
* @param $subElemName string 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
+ * format "<elem>text</elem>" without attributes.
*/
public static function setContent( &$arr, $value, $subElemName = null ) {
if ( is_array( $value ) ) {
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
index 436c392b..677df16a 100644
--- a/includes/api/ApiRollback.php
+++ b/includes/api/ApiRollback.php
@@ -4,7 +4,7 @@
*
* Created on Jun 20, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -49,7 +49,7 @@ class ApiRollback extends ApiBase {
// User and title already validated in call to getTokenSalt from Main
$titleObj = $this->getRbTitle();
$pageObj = WikiPage::factory( $titleObj );
- $summary = ( isset( $params['summary'] ) ? $params['summary'] : '' );
+ $summary = $params['summary'];
$details = array();
$retval = $pageObj->doRollback( $this->getRbUser(), $summary, $params['token'], $params['markbot'], $details, $this->getUser() );
@@ -90,8 +90,11 @@ class ApiRollback extends ApiBase {
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
),
- 'token' => null,
- 'summary' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
+ 'summary' => '',
'markbot' => false,
'watchlist' => array(
ApiBase::PARAM_DFLT => 'preferences',
@@ -110,12 +113,25 @@ class ApiRollback extends ApiBase {
'title' => 'Title of the page you want to rollback.',
'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.',
'token' => "A rollback token previously retrieved through {$this->getModulePrefix()}prop=revisions",
- 'summary' => 'Custom edit summary. If not set, default summary will be used',
+ 'summary' => 'Custom edit summary. If empty, default summary will be used',
'markbot' => 'Mark the reverted edits and the revert as bot edits',
'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch',
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'title' => 'string',
+ 'pageid' => 'integer',
+ 'summary' => 'string',
+ 'revid' => 'integer',
+ 'old_revid' => 'integer',
+ 'last_revid' => 'integer'
+ )
+ );
+ }
+
public function getDescription() {
return array(
'Undo the last edit to the page. If the last user who edited the page made multiple edits in a row,',
diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php
new file mode 100644
index 00000000..098b1a66
--- /dev/null
+++ b/includes/api/ApiSetNotificationTimestamp.php
@@ -0,0 +1,285 @@
+<?php
+
+/**
+ * API for MediaWiki 1.14+
+ *
+ * Created on Jun 18, 2012
+ *
+ * Copyright © 2012 Brad Jorsch
+ *
+ * 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
+ */
+
+/**
+ * API interface for setting the wl_notificationtimestamp field
+ * @ingroup API
+ */
+class ApiSetNotificationTimestamp extends ApiBase {
+
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
+ }
+
+ public function execute() {
+ $user = $this->getUser();
+
+ if ( $user->isAnon() ) {
+ $this->dieUsage( 'Anonymous users cannot use watchlist change notifications', 'notloggedin' );
+ }
+
+ $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 );
+
+ $dbw = $this->getDB( DB_MASTER );
+
+ $timestamp = null;
+ if ( isset( $params['timestamp'] ) ) {
+ $timestamp = $dbw->timestamp( $params['timestamp'] );
+ }
+
+ if ( !$params['entirewatchlist'] ) {
+ $pageSet->execute();
+ }
+
+ if ( isset( $params['torevid'] ) ) {
+ if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) {
+ $this->dieUsage( 'torevid may only be used with a single page', 'multpages' );
+ }
+ $title = reset( $pageSet->getGoodTitles() );
+ $timestamp = Revision::getTimestampFromId( $title, $params['torevid'] );
+ if ( $timestamp ) {
+ $timestamp = $dbw->timestamp( $timestamp );
+ } else {
+ $timestamp = null;
+ }
+ } elseif ( isset( $params['newerthanrevid'] ) ) {
+ if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) {
+ $this->dieUsage( 'newerthanrevid may only be used with a single page', 'multpages' );
+ }
+ $title = reset( $pageSet->getGoodTitles() );
+ $revid = $title->getNextRevisionID( $params['newerthanrevid'] );
+ if ( $revid ) {
+ $timestamp = $dbw->timestamp( Revision::getTimestampFromId( $title, $revid ) );
+ } else {
+ $timestamp = null;
+ }
+ }
+
+ $apiResult = $this->getResult();
+ $result = array();
+ if ( $params['entirewatchlist'] ) {
+ // Entire watchlist mode: Just update the thing and return a success indicator
+ $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $timestamp ),
+ array( 'wl_user' => $user->getID() ),
+ __METHOD__
+ );
+
+ $result['notificationtimestamp'] = ( is_null( $timestamp ) ? '' : wfTimestamp( TS_ISO_8601, $timestamp ) );
+ } else {
+ // First, log the invalid titles
+ 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'] = '';
+ $page['notwatched'] = '';
+ $result[] = $page;
+ }
+ foreach( $pageSet->getMissingRevisionIDs() as $r ) {
+ $rev = array();
+ $rev['revid'] = $r;
+ $rev['missing'] = '';
+ $rev['notwatched'] = '';
+ $result[] = $rev;
+ }
+
+ // Now process the valid titles
+ $lb = new LinkBatch( $pageSet->getTitles() );
+ $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $timestamp ),
+ array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ),
+ __METHOD__
+ );
+
+ // Query the results of our update
+ $timestamps = array();
+ $res = $dbw->select( 'watchlist', array( 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ),
+ array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ),
+ __METHOD__
+ );
+ foreach ( $res as $row ) {
+ $timestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp;
+ }
+
+ // Now, put the valid titles into the result
+ foreach ( $pageSet->getTitles() as $title ) {
+ $ns = $title->getNamespace();
+ $dbkey = $title->getDBkey();
+ $r = array(
+ 'ns' => intval( $ns ),
+ 'title' => $title->getPrefixedText(),
+ );
+ if ( !$title->exists() ) {
+ $r['missing'] = '';
+ }
+ if ( isset( $timestamps[$ns] ) && array_key_exists( $dbkey, $timestamps[$ns] ) ) {
+ $r['notificationtimestamp'] = '';
+ if ( $timestamps[$ns][$dbkey] !== null ) {
+ $r['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $timestamps[$ns][$dbkey] );
+ }
+ } else {
+ $r['notwatched'] = '';
+ }
+ $result[] = $r;
+ }
+
+ $apiResult->setIndexedTagName( $result, 'page' );
+ }
+ $apiResult->addValue( null, $this->getModuleName(), $result );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function needsToken() {
+ return true;
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
+
+ public function getAllowedParams() {
+ $psModule = new ApiPageSet( $this );
+ return $psModule->getAllowedParams() + array(
+ 'entirewatchlist' => array(
+ ApiBase::PARAM_TYPE => 'boolean'
+ ),
+ 'token' => null,
+ 'timestamp' => array(
+ ApiBase::PARAM_TYPE => 'timestamp'
+ ),
+ 'torevid' => array(
+ ApiBase::PARAM_TYPE => 'integer'
+ ),
+ 'newerthanrevid' => array(
+ ApiBase::PARAM_TYPE => 'integer'
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ $psModule = new ApiPageSet( $this );
+ return $psModule->getParamDescription() + 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)',
+ 'newerthanrevid' => 'Revision to set the notification timestamp newer than (one page only)',
+ 'token' => 'A token previously acquired via prop=info',
+ );
+ }
+
+ public function getResultProperties() {
+ return array(
+ ApiBase::PROP_LIST => true,
+ ApiBase::PROP_ROOT => array(
+ 'notificationtimestamp' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ ),
+ '' => array(
+ 'ns' => array(
+ ApiBase::PROP_TYPE => 'namespace',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'title' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'pageid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'revid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'invalid' => 'boolean',
+ 'missing' => 'boolean',
+ 'notwatched' => 'boolean',
+ 'notificationtimestamp' => array(
+ ApiBase::PROP_TYPE => 'timestamp',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
+ 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',
+ 'changed" preference is enabled.'
+ );
+ }
+
+ public function getPossibleErrors() {
+ $psModule = new ApiPageSet( $this );
+ return array_merge(
+ parent::getPossibleErrors(),
+ $psModule->getPossibleErrors(),
+ $this->getRequireMaxOneParameterErrorMessages( array( 'timestamp', 'torevid', 'newerthanrevid' ) ),
+ $this->getRequireOnlyOneParameterErrorMessages( array_merge( array( 'entirewatchlist' ), array_keys( $psModule->getAllowedParams() ) ) ),
+ 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' ),
+ array( 'code' => 'multpages', 'info' => 'newerthanrevid may only be used with a single page' ),
+ )
+ );
+ }
+
+ 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',
+ );
+ }
+
+ 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
new file mode 100644
index 00000000..2c9b482c
--- /dev/null
+++ b/includes/api/ApiTokens.php
@@ -0,0 +1,158 @@
+<?php
+/**
+ *
+ *
+ * Created on Jul 29, 2011
+ *
+ * Copyright © 2011 John Du Hart john@johnduhart.me
+ *
+ * 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 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 ) {
+ $this->setWarning( "Action '$type' is not allowed for the current user" );
+ } else {
+ $res[$type . 'token'] = $val;
+ }
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
+ wfProfileOut( __METHOD__ );
+ }
+
+ private function getTokenTypes() {
+ static $types = null;
+ if ( $types ) {
+ return $types;
+ }
+ wfProfileIn( __METHOD__ );
+ $types = array( 'patrol' => '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';
+ }
+ wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) );
+ ksort( $types );
+ wfProfileOut( __METHOD__ );
+ return $types;
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'type' => array(
+ ApiBase::PARAM_DFLT => 'edit',
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_TYPE => array_keys( $this->getTokenTypes() ),
+ ),
+ );
+ }
+
+ 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
+ )
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'type' => 'Type of token(s) to request'
+ );
+ }
+
+ public function getDescription() {
+ return 'Gets tokens for data-modifying actions';
+ }
+
+ protected function getExamples() {
+ return array(
+ 'api.php?action=tokens' => 'Retrieve an edit token (the default)',
+ '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 db94fd5b..e34771fc 100644
--- a/includes/api/ApiUnblock.php
+++ b/includes/api/ApiUnblock.php
@@ -4,7 +4,7 @@
*
* Created on Sep 7, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -44,7 +44,7 @@ class ApiUnblock extends ApiBase {
$params = $this->extractRequestParams();
if ( $params['gettoken'] ) {
- $res['unblocktoken'] = $user->getEditToken( '', $this->getMain()->getRequest() );
+ $res['unblocktoken'] = $user->getEditToken();
$this->getResult()->addValue( null, $this->getModuleName(), $res );
return;
}
@@ -69,7 +69,7 @@ class ApiUnblock extends ApiBase {
$data = array(
'Target' => is_null( $params['id'] ) ? $params['user'] : "#{$params['id']}",
- 'Reason' => is_null( $params['reason'] ) ? '' : $params['reason']
+ 'Reason' => $params['reason']
);
$block = Block::newFromTarget( $data['Target'] );
$retval = SpecialUnblock::processUnblock( $data, $this->getContext() );
@@ -78,7 +78,9 @@ class ApiUnblock extends ApiBase {
}
$res['id'] = $block->getId();
- $res['user'] = $block->getType() == Block::TYPE_AUTO ? '' : $block->getTarget();
+ $target = $block->getType() == Block::TYPE_AUTO ? '' : $block->getTarget();
+ $res['user'] = $target;
+ $res['userid'] = $target instanceof User ? $target->getId() : 0;
$res['reason'] = $params['reason'];
$this->getResult()->addValue( null, $this->getModuleName(), $res );
}
@@ -98,8 +100,11 @@ class ApiUnblock extends ApiBase {
),
'user' => null,
'token' => null,
- 'gettoken' => false,
- 'reason' => null,
+ 'gettoken' => array(
+ ApiBase::PARAM_DFLT => false,
+ ApiBase::PARAM_DEPRECATED => true,
+ ),
+ 'reason' => '',
);
}
@@ -108,9 +113,36 @@ class ApiUnblock extends ApiBase {
return array(
'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 the gettoken parameter or {$p}prop=info",
+ '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 (optional)',
+ 'reason' => 'Reason for unblock',
+ );
+ }
+
+ 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
+ ),
+ 'user' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'userid' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'reason' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
);
}
diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php
index d3429972..c9962517 100644
--- a/includes/api/ApiUndelete.php
+++ b/includes/api/ApiUndelete.php
@@ -4,7 +4,7 @@
*
* Created on Jul 3, 2007
*
- * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2007 Roan Kattouw "<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
@@ -76,7 +76,7 @@ class ApiUndelete extends ApiBase {
$info['title'] = $titleObj->getPrefixedText();
$info['revisions'] = intval( $retval[0] );
$info['fileversions'] = intval( $retval[1] );
- $info['reason'] = intval( $retval[2] );
+ $info['reason'] = $retval[2];
$this->getResult()->addValue( null, $this->getModuleName(), $info );
}
@@ -94,7 +94,10 @@ class ApiUndelete extends ApiBase {
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
),
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'reason' => '',
'timestamps' => array(
ApiBase::PARAM_TYPE => 'timestamp',
@@ -116,12 +119,23 @@ class ApiUndelete extends ApiBase {
return array(
'title' => 'Title of the page you want to restore',
'token' => 'An undelete token previously retrieved through list=deletedrevs',
- 'reason' => 'Reason for restoring (optional)',
+ 'reason' => 'Reason for restoring',
'timestamps' => 'Timestamps of the revisions to restore. If not set, all revisions will be restored.',
'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch',
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'title' => 'string',
+ 'revisions' => 'integer',
+ 'filerevisions' => 'integer',
+ 'reason' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return array(
'Restore certain revisions of a deleted page. A list of deleted revisions (including timestamps) can be',
diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php
index fdc1eff0..3a9b5c56 100644
--- a/includes/api/ApiUpload.php
+++ b/includes/api/ApiUpload.php
@@ -113,27 +113,30 @@ class ApiUpload extends ApiBase {
}
/**
* Get an uplaod result based on upload context
+ * @return array
*/
private function getContextResult(){
$warnings = $this->getApiWarnings();
- if ( $warnings ) {
+ if ( $warnings && !$this->mParams['ignorewarnings'] ) {
// Get warnings formated in result array format
return $this->getWarningsResult( $warnings );
} elseif ( $this->mParams['chunk'] ) {
// Add chunk, and get result
- return $this->getChunkResult();
+ return $this->getChunkResult( $warnings );
} elseif ( $this->mParams['stash'] ) {
// Stash the file and get stash result
- return $this->getStashResult();
+ return $this->getStashResult( $warnings );
}
// This is the most common case -- a normal upload with no warnings
// performUpload will return a formatted properly for the API with status
- return $this->performUpload();
+ return $this->performUpload( $warnings );
}
/**
- * Get Stash Result, throws an expetion if the file could not be stashed.
+ * Get Stash Result, throws an expetion if the file could not be stashed.
+ * @param $warnings array Array of Api upload warnings
+ * @return array
*/
- private function getStashResult(){
+ 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
@@ -141,6 +144,9 @@ class ApiUpload extends ApiBase {
$result['result'] = 'Success';
$result['filekey'] = $this->performStash();
$result['sessionkey'] = $result['filekey']; // backwards compatibility
+ if ( $warnings && count( $warnings ) > 0 ) {
+ $result['warnings'] = $warnings;
+ }
} catch ( MWException $e ) {
$this->dieUsage( $e->getMessage(), 'stashfailed' );
}
@@ -148,7 +154,8 @@ class ApiUpload extends ApiBase {
}
/**
* Get Warnings Result
- * @param $warnings Array of Api upload warnings
+ * @param $warnings array Array of Api upload warnings
+ * @return array
*/
private function getWarningsResult( $warnings ){
$result = array();
@@ -165,12 +172,17 @@ class ApiUpload extends ApiBase {
return $result;
}
/**
- * Get the result of a chunk upload.
+ * Get the result of a chunk upload.
+ * @param $warnings array Array of Api upload warnings
+ * @return array
*/
- private function getChunkResult(){
+ private function getChunkResult( $warnings ){
$result = array();
-
+
$result['result'] = 'Continue';
+ if ( $warnings && count( $warnings ) > 0 ) {
+ $result['warnings'] = $warnings;
+ }
$request = $this->getMain()->getRequest();
$chunkPath = $request->getFileTempname( 'chunk' );
$chunkSize = $request->getUpload( 'chunk' )->getSize();
@@ -181,17 +193,30 @@ class ApiUpload extends ApiBase {
$this->mParams['offset']);
if ( !$status->isGood() ) {
$this->dieUsage( $status->getWikiText(), 'stashfailed' );
- return ;
+ return array();
}
- $result['filekey'] = $this->mParams['filekey'];
+
// Check we added the last chunk:
if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) {
$status = $this->mUpload->concatenateChunks();
+
if ( !$status->isGood() ) {
$this->dieUsage( $status->getWikiText(), 'stashfailed' );
- return ;
+ 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'] );
+
$result['result'] = 'Success';
+
+ } else {
+
+ // Continue passing through the filekey for adding further chunks.
+ $result['filekey'] = $this->mParams['filekey'];
}
}
$result['offset'] = $this->mParams['offset'] + $chunkSize;
@@ -318,6 +343,10 @@ class ApiUpload extends ApiBase {
$this->dieUsageMsg( 'copyuploaddisabled' );
}
+ if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
+ $this->dieUsageMsg( 'copyuploadbaddomain' );
+ }
+
$async = false;
if ( $this->mParams['asyncdownload'] ) {
$this->checkAsyncDownloadEnabled();
@@ -399,11 +428,21 @@ class ApiUpload extends ApiBase {
break;
case UploadBase::FILETYPE_BADTYPE:
- $this->dieUsage( 'This type of file is banned', 'filetype-banned',
- 0, array(
- 'filetype' => $verification['finalExt'],
- 'allowed' => $wgFileExtensions
- ) );
+ $extradata = array(
+ 'filetype' => $verification['finalExt'],
+ 'allowed' => $wgFileExtensions
+ );
+ $this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' );
+
+ $msg = "Filetype not permitted: ";
+ if ( isset( $verification['blacklistedExt'] ) ) {
+ $msg .= join( ', ', $verification['blacklistedExt'] );
+ $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
+ $this->getResult()->setIndexedTagName( $extradata['blacklisted'], 'ext' );
+ } else {
+ $msg .= $verification['finalExt'];
+ }
+ $this->dieUsage( $msg, 'filetype-banned', 0, $extradata );
break;
case UploadBase::VERIFICATION_ERROR:
$this->getResult()->setIndexedTagName( $verification['details'], 'detail' );
@@ -423,18 +462,15 @@ class ApiUpload extends ApiBase {
/**
- * Check warnings if ignorewarnings is not set.
+ * Check warnings.
* Returns a suitable array for inclusion into API results if there were warnings
* Returns the empty array if there were no warnings
*
* @return array
*/
protected function getApiWarnings() {
- $warnings = array();
+ $warnings = $this->mUpload->checkWarnings();
- if ( !$this->mParams['ignorewarnings'] ) {
- $warnings = $this->mUpload->checkWarnings();
- }
return $this->transformWarnings( $warnings );
}
@@ -467,9 +503,10 @@ class ApiUpload extends ApiBase {
* Perform the actual upload. Returns a suitable result array on success;
* dies on failure.
*
+ * @param $warnings array Array of Api upload warnings
* @return array
*/
- protected function performUpload() {
+ protected function performUpload( $warnings ) {
// Use comment as initial page text by default
if ( is_null( $this->mParams['text'] ) ) {
$this->mParams['text'] = $this->mParams['comment'];
@@ -508,6 +545,9 @@ class ApiUpload extends ApiBase {
$result['result'] = 'Success';
$result['filename'] = $file->getName();
+ if ( $warnings && count( $warnings ) > 0 ) {
+ $result['warnings'] = $warnings;
+ }
return $result;
}
@@ -539,7 +579,10 @@ class ApiUpload extends ApiBase {
ApiBase::PARAM_DFLT => ''
),
'text' => null,
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'watch' => array(
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_DEPRECATED => true,
@@ -602,6 +645,41 @@ class ApiUpload extends ApiBase {
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'result' => array(
+ ApiBase::PROP_TYPE => array(
+ 'Success',
+ 'Warning',
+ 'Continue',
+ 'Queued'
+ ),
+ ),
+ 'filekey' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'sessionkey' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'offset' => array(
+ ApiBase::PROP_TYPE => 'integer',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'statuskey' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ ),
+ 'filename' => array(
+ ApiBase::PROP_TYPE => 'string',
+ ApiBase::PROP_NULLABLE => true
+ )
+ )
+ );
+ }
+
public function getDescription() {
return array(
'Upload a file, or get the status of pending uploads. Several methods are available:',
@@ -631,6 +709,8 @@ class ApiUpload extends ApiBase {
array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ),
array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ),
array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ),
+ array( 'fileexists-forbidden' ),
+ array( 'fileexists-shared-forbidden' ),
)
);
}
diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php
index 191dd3ec..cbb66a41 100644
--- a/includes/api/ApiUserrights.php
+++ b/includes/api/ApiUserrights.php
@@ -5,7 +5,7 @@
*
* Created on Mar 24, 2009
*
- * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+ * Copyright © 2009 Roan Kattouw "<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
@@ -43,6 +43,7 @@ class ApiUserrights extends ApiBase {
$form = new UserrightsPage;
$r['user'] = $user->getName();
+ $r['userid'] = $user->getId();
list( $r['added'], $r['removed'] ) =
$form->doSaveUserGroups(
$user, (array)$params['add'],
@@ -99,7 +100,10 @@ class ApiUserrights extends ApiBase {
ApiBase::PARAM_TYPE => User::getAllGroups(),
ApiBase::PARAM_ISMULTI => true
),
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
'reason' => array(
ApiBase::PARAM_DFLT => ''
)
diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php
index fa382b3b..0509f1f8 100644
--- a/includes/api/ApiWatch.php
+++ b/includes/api/ApiWatch.php
@@ -4,7 +4,7 @@
*
* Created on Jan 4, 2008
*
- * Copyright © 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com,
+ * Copyright © 2008 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -88,7 +88,10 @@ class ApiWatch extends ApiBase {
ApiBase::PARAM_REQUIRED => true
),
'unwatch' => false,
- 'token' => null,
+ 'token' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ),
);
}
@@ -100,6 +103,17 @@ class ApiWatch extends ApiBase {
);
}
+ public function getResultProperties() {
+ return array(
+ '' => array(
+ 'title' => 'string',
+ 'unwatched' => 'boolean',
+ 'watched' => 'boolean',
+ 'message' => 'string'
+ )
+ );
+ }
+
public function getDescription() {
return 'Add or remove a page from/to the current user\'s watchlist';
}
diff --git a/includes/cache/CacheDependency.php b/includes/cache/CacheDependency.php
index 0df0cd89..a3c2b52a 100644
--- a/includes/cache/CacheDependency.php
+++ b/includes/cache/CacheDependency.php
@@ -1,11 +1,32 @@
<?php
/**
+ * Data caching with dependencies.
+ *
+ * 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
+ */
+
+/**
* This class stores an arbitrary value along with its dependencies.
* Users should typically only use DependencyWrapper::getValueFromCache(),
* rather than instantiating one of these objects directly.
* @ingroup Cache
*/
-
class DependencyWrapper {
var $value;
var $deps;
diff --git a/includes/cache/FileCacheBase.php b/includes/cache/FileCacheBase.php
index 37401655..c0c5609c 100644
--- a/includes/cache/FileCacheBase.php
+++ b/includes/cache/FileCacheBase.php
@@ -1,9 +1,31 @@
<?php
/**
- * Contain the FileCacheBase class
+ * Data storage in the file system.
+ *
+ * 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
*/
+
+/**
+ * Base class for data storage in the file system.
+ *
+ * @ingroup Cache
+ */
abstract class FileCacheBase {
protected $mKey;
protected $mType = 'object';
@@ -74,7 +96,7 @@ abstract class FileCacheBase {
/**
* Get the last-modified timestamp of the cache file
- * @return string|false TS_MW timestamp
+ * @return string|bool TS_MW timestamp
*/
public function cacheTimestamp() {
$timestamp = filemtime( $this->cachePath() );
@@ -116,9 +138,12 @@ abstract class FileCacheBase {
* @return string
*/
public function fetchText() {
- // gzopen can transparently read from gziped or plain text
- $fh = gzopen( $this->cachePath(), 'rb' );
- return stream_get_contents( $fh );
+ if( $this->useGzip() ) {
+ $fh = gzopen( $this->cachePath(), 'rb' );
+ return stream_get_contents( $fh );
+ } else {
+ return file_get_contents( $this->cachePath() );
+ }
}
/**
diff --git a/includes/cache/GenderCache.php b/includes/cache/GenderCache.php
index 342f8dba..2a169bb3 100644
--- a/includes/cache/GenderCache.php
+++ b/includes/cache/GenderCache.php
@@ -1,8 +1,30 @@
<?php
-
/**
* Caches user genders when needed to use correct namespace aliases.
+ *
+ * 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
+ * @ingroup Cache
+ */
+
+/**
+ * Caches user genders when needed to use correct namespace aliases.
+ *
* @since 1.18
*/
class GenderCache {
@@ -37,14 +59,18 @@ class GenderCache {
/**
* Returns the gender for given username.
- * @param $username String: username
+ * @param $username String or User: username
* @param $caller String: the calling method
* @return String
*/
public function getGenderOf( $username, $caller = '' ) {
global $wgUser;
- $username = strtr( $username, '_', ' ' );
+ if( $username instanceof User ) {
+ $username = $username->getName();
+ }
+
+ $username = self::normalizeUsername( $username );
if ( !isset( $this->cache[$username] ) ) {
if ( $this->misses >= $this->missLimit && $wgUser->getName() !== $username ) {
@@ -56,11 +82,7 @@ class GenderCache {
} else {
$this->misses++;
- if ( !User::isValidUserName( $username ) ) {
- $this->cache[$username] = $this->getDefault();
- } else {
- $this->doQuery( $username, $caller );
- }
+ $this->doQuery( $username, $caller );
}
}
@@ -82,7 +104,6 @@ class GenderCache {
foreach ( $data as $ns => $pagenames ) {
if ( !MWNamespace::hasGenderDistinction( $ns ) ) continue;
foreach ( array_keys( $pagenames ) as $username ) {
- if ( isset( $this->cache[$username] ) ) continue;
$users[$username] = true;
}
}
@@ -91,6 +112,29 @@ class GenderCache {
}
/**
+ * Wrapper for doQuery that processes a title or string array.
+ *
+ * @since 1.20
+ * @param $titles List: array of Title objects or strings
+ * @param $caller String: the calling method
+ */
+ public function doTitlesArray( $titles, $caller = '' ) {
+ $users = array();
+ foreach ( $titles as $title ) {
+ $titleObj = is_string( $title ) ? Title::newFromText( $title ) : $title;
+ if ( !$titleObj ) {
+ continue;
+ }
+ if ( !MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
+ continue;
+ }
+ $users[] = $titleObj->getText();
+ }
+
+ $this->doQuery( $users, $caller );
+ }
+
+ /**
* Preloads genders for given list of users.
* @param $users List|String: usernames
* @param $caller String: the calling method
@@ -98,26 +142,28 @@ class GenderCache {
public function doQuery( $users, $caller = '' ) {
$default = $this->getDefault();
- foreach ( (array) $users as $index => $value ) {
- $name = strtr( $value, '_', ' ' );
- if ( isset( $this->cache[$name] ) ) {
- // Skip users whose gender setting we already know
- unset( $users[$index] );
- } else {
- $users[$index] = $name;
+ $usersToCheck = array();
+ 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 ) ) {
+ $usersToCheck[] = $name;
+ }
}
}
- if ( count( $users ) === 0 ) {
+ if ( count( $usersToCheck ) === 0 ) {
return;
}
$dbr = wfGetDB( DB_SLAVE );
$table = array( 'user', 'user_properties' );
$fields = array( 'user_name', 'up_value' );
- $conds = array( 'user_name' => $users );
+ $conds = array( 'user_name' => $usersToCheck );
$joins = array( 'user_properties' =>
array( 'LEFT JOIN', array( 'user_id = up_user', 'up_property' => 'gender' ) ) );
@@ -125,11 +171,20 @@ class GenderCache {
if ( strval( $caller ) !== '' ) {
$comment .= "/$caller";
}
- $res = $dbr->select( $table, $fields, $conds, $comment, $joins, $joins );
+ $res = $dbr->select( $table, $fields, $conds, $comment, array(), $joins );
foreach ( $res as $row ) {
$this->cache[$row->user_name] = $row->up_value ? $row->up_value : $default;
}
}
+ private static function normalizeUsername( $username ) {
+ // Strip off subpages
+ $indexSlash = strpos( $username, '/' );
+ if ( $indexSlash !== false ) {
+ $username = substr( $username, 0, $indexSlash );
+ }
+ // normalize underscore/spaces
+ return strtr( $username, '_', ' ' );
+ }
}
diff --git a/includes/cache/HTMLCacheUpdate.php b/includes/cache/HTMLCacheUpdate.php
index 11e2ae74..0a3c0023 100644
--- a/includes/cache/HTMLCacheUpdate.php
+++ b/includes/cache/HTMLCacheUpdate.php
@@ -1,4 +1,25 @@
<?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
+ */
/**
* Class to invalidate the HTML cache of all the pages linking to a given title.
diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php
index 92130f69..6bfeed32 100644
--- a/includes/cache/HTMLFileCache.php
+++ b/includes/cache/HTMLFileCache.php
@@ -1,9 +1,33 @@
<?php
/**
- * Contain the HTMLFileCache class
+ * Page view caching in the file system.
+ *
+ * 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
*/
+
+/**
+ * Page view caching in the file system.
+ * The only cacheable actions are "view" and "history". Also special pages
+ * will not be cached.
+ *
+ * @ingroup Cache
+ */
class HTMLFileCache extends FileCacheBase {
/**
* Construct an ObjectFileCache from a Title and an action
@@ -105,19 +129,22 @@ class HTMLFileCache extends FileCacheBase {
wfDebug( __METHOD__ . "()\n");
$filename = $this->cachePath();
+
$context->getOutput()->sendCacheControl();
header( "Content-Type: $wgMimeType; charset=UTF-8" );
header( "Content-Language: $wgLanguageCode" );
if ( $this->useGzip() ) {
if ( wfClientAcceptsGzip() ) {
header( 'Content-Encoding: gzip' );
+ readfile( $filename );
} else {
/* Send uncompressed */
+ wfDebug( __METHOD__ . " uncompressing cache file and sending it\n" );
readgzfile( $filename );
- return;
}
+ } else {
+ readfile( $filename );
}
- readfile( $filename );
$context->getOutput()->disable(); // tell $wgOut that output is taken care of
}
diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php
index 17e8739b..372f983b 100644
--- a/includes/cache/LinkBatch.php
+++ b/includes/cache/LinkBatch.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Batch query to determine page existence.
+ *
+ * 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
+ */
/**
* Class representing a list of titles
@@ -45,6 +66,11 @@ class LinkBatch {
}
}
+ /**
+ * @param $ns int
+ * @param $dbkey string
+ * @return mixed
+ */
public function add( $ns, $dbkey ) {
if ( $ns < 0 ) {
return;
@@ -190,7 +216,7 @@ class LinkBatch {
}
$genderCache = GenderCache::singleton();
- $genderCache->dolinkBatch( $this->data, $this->caller );
+ $genderCache->doLinkBatch( $this->data, $this->caller );
return true;
}
diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php
index a73eaaa4..f759c020 100644
--- a/includes/cache/LinkCache.php
+++ b/includes/cache/LinkCache.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Page existence cache.
+ *
+ * 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
+ */
+
+/**
* Cache for article titles (prefixed DB keys) and ids linked from one source
*
* @ingroup Cache
diff --git a/includes/cache/MemcachedSessions.php b/includes/cache/MemcachedSessions.php
deleted file mode 100644
index 36733595..00000000
--- a/includes/cache/MemcachedSessions.php
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-/**
- * This file gets included if $wgSessionsInMemcache is set in the config.
- * It redirects session handling functions to store their data in memcached
- * instead of the local filesystem. Depending on circumstances, it may also
- * be necessary to change the cookie settings to work across hostnames.
- * See: http://www.php.net/manual/en/function.session-set-save-handler.php
- *
- * @file
- * @ingroup Cache
- */
-
-/**
- * Get a cache key for the given session id.
- *
- * @param $id String: session id
- * @return String: cache key
- */
-function memsess_key( $id ) {
- return wfMemcKey( 'session', $id );
-}
-
-/**
- * Callback when opening a session.
- * NOP: $wgMemc should be set up already.
- *
- * @param $save_path String: path used to store session files, unused
- * @param $session_name String: session name
- * @return Boolean: success
- */
-function memsess_open( $save_path, $session_name ) {
- return true;
-}
-
-/**
- * Callback when closing a session.
- * NOP.
- *
- * @return Boolean: success
- */
-function memsess_close() {
- return true;
-}
-
-/**
- * Callback when reading session data.
- *
- * @param $id String: session id
- * @return Mixed: session data
- */
-function memsess_read( $id ) {
- global $wgMemc;
- $data = $wgMemc->get( memsess_key( $id ) );
- if( ! $data ) return '';
- return $data;
-}
-
-/**
- * Callback when writing session data.
- *
- * @param $id String: session id
- * @param $data Mixed: session data
- * @return Boolean: success
- */
-function memsess_write( $id, $data ) {
- global $wgMemc;
- $wgMemc->set( memsess_key( $id ), $data, 3600 );
- return true;
-}
-
-/**
- * Callback to destroy a session when calling session_destroy().
- *
- * @param $id String: session id
- * @return Boolean: success
- */
-function memsess_destroy( $id ) {
- global $wgMemc;
-
- $wgMemc->delete( memsess_key( $id ) );
- return true;
-}
-
-/**
- * Callback to execute garbage collection.
- * NOP: Memcached performs garbage collection.
- *
- * @param $maxlifetime Integer: maximum session life time
- * @return Boolean: success
- */
-function memsess_gc( $maxlifetime ) {
- return true;
-}
-
-function memsess_write_close() {
- session_write_close();
-}
-
diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php
index 146edd28..b854a2ec 100644
--- a/includes/cache/MessageCache.php
+++ b/includes/cache/MessageCache.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Localisation messages cache.
+ *
+ * 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
*/
@@ -115,6 +132,7 @@ class MessageCache {
function getParserOptions() {
if ( !$this->mParserOptions ) {
$this->mParserOptions = new ParserOptions;
+ $this->mParserOptions->setEditSection( false );
}
return $this->mParserOptions;
}
@@ -126,7 +144,7 @@ class MessageCache {
*
* @param $hash String: the hash of contents, to check validity.
* @param $code Mixed: Optional language code, see documenation of load().
- * @return false on failure.
+ * @return bool on failure.
*/
function loadFromLocal( $hash, $code ) {
global $wgCacheDirectory, $wgLocalMessageCacheSerialized;
@@ -260,6 +278,7 @@ class MessageCache {
* is disabled.
*
* @param $code String: language to which load messages
+ * @return bool
*/
function load( $code = false ) {
global $wgUseLocalMessageCache;
@@ -496,7 +515,7 @@ class MessageCache {
if ( $code === 'en' ) {
// Delete all sidebars, like for example on action=purge on the
// sidebar messages
- $codes = array_keys( Language::getLanguageNames() );
+ $codes = array_keys( Language::fetchLanguageNames() );
}
global $wgMemc;
@@ -520,7 +539,7 @@ class MessageCache {
* @param $cache Array: cached messages with a version.
* @param $memc Bool: Wether to update or not memcache.
* @param $code String: Language code.
- * @return False on somekind of error.
+ * @return bool on somekind of error.
*/
protected function saveToCaches( $cache, $memc = true, $code = false ) {
wfProfileIn( __METHOD__ );
@@ -588,7 +607,7 @@ class MessageCache {
* @param $isFullKey Boolean: specifies whether $key is a two part key
* "msg/lang".
*
- * @return string|false
+ * @return string|bool
*/
function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
global $wgLanguageCode, $wgContLang;
@@ -696,7 +715,7 @@ class MessageCache {
* @param $title String: Message cache key with initial uppercase letter.
* @param $code String: code denoting the language to try.
*
- * @return string|false
+ * @return string|bool False on failure
*/
function getMsgFromNamespace( $title, $code ) {
global $wgAdaptiveMessageCache;
@@ -747,12 +766,14 @@ class MessageCache {
}
# Try loading it from the database
- $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
+ $revision = Revision::newFromTitle(
+ Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST
+ );
if ( $revision ) {
$message = $revision->getText();
if ($message === false) {
// A possibly temporary loading failure.
- wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$title->getDbKey()} ($code)" );
+ wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$title} ($code)" );
} else {
$this->mCache[$code][$title] = ' ' . $message;
$this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
@@ -868,7 +889,7 @@ class MessageCache {
* Clear all stored messages. Mainly used after a mass rebuild.
*/
function clear() {
- $langs = Language::getLanguageNames( false );
+ $langs = Language::fetchLanguageNames( null, 'mw' );
foreach ( array_keys($langs) as $code ) {
# Global cache
$this->mMemc->delete( wfMemcKey( 'messages', $code ) );
@@ -890,8 +911,7 @@ class MessageCache {
}
$lang = array_pop( $pieces );
- $validCodes = Language::getLanguageNames();
- if( !array_key_exists( $lang, $validCodes ) ) {
+ if( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
return array( $key, $wgLanguageCode );
}
diff --git a/includes/cache/ObjectFileCache.php b/includes/cache/ObjectFileCache.php
index 3356f1fc..ed1e49a6 100644
--- a/includes/cache/ObjectFileCache.php
+++ b/includes/cache/ObjectFileCache.php
@@ -1,9 +1,31 @@
<?php
/**
- * Contain the ObjectFileCache class
+ * Object cache in the file system.
+ *
+ * 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
*/
+
+/**
+ * Object cache in the file system.
+ *
+ * @ingroup Cache
+ */
class ObjectFileCache extends FileCacheBase {
/**
* Construct an ObjectFileCache from a key and a type
diff --git a/includes/cache/ProcessCacheLRU.php b/includes/cache/ProcessCacheLRU.php
new file mode 100644
index 00000000..f215ebd8
--- /dev/null
+++ b/includes/cache/ProcessCacheLRU.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Per-process memory cache for storing items.
+ *
+ * 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
+ */
+
+/**
+ * Handles per process caching of items
+ * @ingroup Cache
+ */
+class ProcessCacheLRU {
+ /** @var Array */
+ protected $cache = array(); // (key => prop => value)
+
+ protected $maxCacheKeys; // integer; max entries
+
+ /**
+ * @param $maxKeys integer Maximum number of entries allowed (min 1).
+ * @throws MWException When $maxCacheKeys is not an int or =< 0.
+ */
+ public function __construct( $maxKeys ) {
+ if ( !is_int( $maxKeys ) || $maxKeys < 1 ) {
+ throw new MWException( __METHOD__ . " must be given an integer and >= 1" );
+ }
+ $this->maxCacheKeys = $maxKeys;
+ }
+
+ /**
+ * Set a property field for a cache entry.
+ * This will prune the cache if it gets too large.
+ * If the item is already set, it will be pushed to the top of the cache.
+ *
+ * @param $key string
+ * @param $prop string
+ * @param $value mixed
+ * @return void
+ */
+ public function set( $key, $prop, $value ) {
+ if ( isset( $this->cache[$key] ) ) {
+ $this->ping( $key ); // push to top
+ } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
+ reset( $this->cache );
+ unset( $this->cache[key( $this->cache )] );
+ }
+ $this->cache[$key][$prop] = $value;
+ }
+
+ /**
+ * Check if a property field exists for a cache entry.
+ *
+ * @param $key string
+ * @param $prop string
+ * @return bool
+ */
+ public function has( $key, $prop ) {
+ return isset( $this->cache[$key][$prop] );
+ }
+
+ /**
+ * Get a property field for a cache entry.
+ * This returns null if the property is not set.
+ * If the item is already set, it will be pushed to the top of the cache.
+ *
+ * @param $key string
+ * @param $prop string
+ * @return mixed
+ */
+ public function get( $key, $prop ) {
+ if ( isset( $this->cache[$key][$prop] ) ) {
+ $this->ping( $key ); // push to top
+ return $this->cache[$key][$prop];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Clear one or several cache entries, or all cache entries
+ *
+ * @param $keys string|Array
+ * @return void
+ */
+ public function clear( $keys = null ) {
+ if ( $keys === null ) {
+ $this->cache = array();
+ } else {
+ foreach ( (array)$keys as $key ) {
+ unset( $this->cache[$key] );
+ }
+ }
+ }
+
+ /**
+ * Push an entry to the top of the cache
+ *
+ * @param $key string
+ */
+ protected function ping( $key ) {
+ $item = $this->cache[$key];
+ unset( $this->cache[$key] );
+ $this->cache[$key] = $item;
+ }
+}
diff --git a/includes/cache/ResourceFileCache.php b/includes/cache/ResourceFileCache.php
index e73fc2d7..61f1e8c3 100644
--- a/includes/cache/ResourceFileCache.php
+++ b/includes/cache/ResourceFileCache.php
@@ -1,9 +1,31 @@
<?php
/**
- * Contain the ResourceFileCache class
+ * Resource loader request result caching in the file system.
+ *
+ * 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
*/
+
+/**
+ * Resource loader request result caching in the file system.
+ *
+ * @ingroup Cache
+ */
class ResourceFileCache extends FileCacheBase {
protected $mCacheWorthy;
diff --git a/includes/cache/SquidUpdate.php b/includes/cache/SquidUpdate.php
index d47b5b5e..423e3884 100644
--- a/includes/cache/SquidUpdate.php
+++ b/includes/cache/SquidUpdate.php
@@ -1,6 +1,22 @@
<?php
/**
- * See deferred.txt
+ * Squid 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
+ * 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
*/
@@ -12,13 +28,18 @@
class SquidUpdate {
var $urlArr, $mMaxTitles;
- function __construct( $urlArr = Array(), $maxTitles = false ) {
+ /**
+ * @param $urlArr array
+ * @param $maxTitles bool|int
+ */
+ function __construct( $urlArr = array(), $maxTitles = false ) {
global $wgMaxSquidPurgeTitles;
if ( $maxTitles === false ) {
$this->mMaxTitles = $wgMaxSquidPurgeTitles;
} else {
$this->mMaxTitles = $maxTitles;
}
+ $urlArr = array_unique( $urlArr ); // Remove duplicates
if ( count( $urlArr ) > $this->mMaxTitles ) {
$urlArr = array_slice( $urlArr, 0, $this->mMaxTitles );
}
@@ -102,23 +123,19 @@ class SquidUpdate {
* @return void
*/
static function purge( $urlArr ) {
- global $wgSquidServers, $wgHTCPMulticastAddress, $wgHTCPPort;
-
- /*if ( (@$wgSquidServers[0]) == 'echo' ) {
- echo implode("<br />\n", $urlArr) . "<br />\n";
- return;
- }*/
+ global $wgSquidServers, $wgHTCPMulticastRouting;
if( !$urlArr ) {
return;
}
- if ( $wgHTCPMulticastAddress && $wgHTCPPort ) {
+ if ( $wgHTCPMulticastRouting ) {
SquidUpdate::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
$socketsPerSquid = ceil( count( $urlArr ) / $urlsPerSocket );
@@ -147,7 +164,7 @@ class SquidUpdate {
* @param $urlArr array
*/
static function HTCPPurge( $urlArr ) {
- global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort;
+ global $wgHTCPMulticastRouting, $wgHTCPMulticastTTL;
wfProfileIn( __METHOD__ );
$htcpOpCLR = 4; // HTCP CLR
@@ -168,11 +185,20 @@ class SquidUpdate {
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'] ) ) {
+ throw new MWException( "Invalid HTCP rule for URL $url\n" );
+ }
// Construct a minimal HTCP request diagram
// as per RFC 2756
@@ -196,7 +222,7 @@ class SquidUpdate {
// Send out
wfDebug( "Purging URL $url via HTCP\n" );
socket_sendto( $conn, $htcpPacket, $htcpLen, 0,
- $wgHTCPMulticastAddress, $wgHTCPPort );
+ $conf['host'], $conf['port'] );
}
} else {
$errstr = socket_strerror( socket_last_error() );
@@ -223,4 +249,20 @@ class SquidUpdate {
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
+ * @return mixed Element of $rules that matched, or false if nothing matched
+ */
+ static function getRuleForURL( $url, $rules ) {
+ foreach ( $rules as $regex => $routing ) {
+ if ( $regex === '' || preg_match( $regex, $url ) ) {
+ return $routing;
+ }
+ }
+ return false;
+ }
+
}
diff --git a/includes/cache/UserCache.php b/includes/cache/UserCache.php
new file mode 100644
index 00000000..6ec23669
--- /dev/null
+++ b/includes/cache/UserCache.php
@@ -0,0 +1,134 @@
+<?php
+/**
+ * Caches current user names and other info based on user IDs.
+ *
+ * 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
+ */
+
+/**
+ * @since 1.20
+ */
+class UserCache {
+ protected $cache = array(); // (uid => property => value)
+ protected $typesCached = array(); // (uid => cache type => 1)
+
+ /**
+ * @return UserCache
+ */
+ public static function singleton() {
+ static $instance = null;
+ if ( $instance === null ) {
+ $instance = new self();
+ }
+ return $instance;
+ }
+
+ protected function __construct() {}
+
+ /**
+ * Get a property of a user based on their user ID
+ *
+ * @param $userId integer User ID
+ * @param $prop string User property
+ * @return mixed The property or false if the user does not exist
+ */
+ public function getProp( $userId, $prop ) {
+ if ( !isset( $this->cache[$userId][$prop] ) ) {
+ wfDebug( __METHOD__ . ": querying DB for prop '$prop' for user ID '$userId'.\n" );
+ $this->doQuery( array( $userId ) ); // cache miss
+ }
+ return isset( $this->cache[$userId][$prop] )
+ ? $this->cache[$userId][$prop]
+ : false; // user does not exist?
+ }
+
+ /**
+ * 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
+ */
+ public function doQuery( array $userIds, $options = array(), $caller = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ $usersToCheck = array();
+ $usersToQuery = array();
+
+ foreach ( $userIds as $userId ) {
+ $userId = (int)$userId;
+ if ( $userId <= 0 ) {
+ continue; // skip anons
+ }
+ if ( isset( $this->cache[$userId]['name'] ) ) {
+ $usersToCheck[$userId] = $this->cache[$userId]['name']; // already have name
+ } else {
+ $usersToQuery[] = $userId; // we need to get the name
+ }
+ }
+
+ // Lookup basic info for users not yet loaded...
+ if ( count( $usersToQuery ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $table = array( 'user' );
+ $conds = array( 'user_id' => $usersToQuery );
+ $fields = array( 'user_name', 'user_real_name', 'user_registration', 'user_id' );
+
+ $comment = __METHOD__;
+ if ( strval( $caller ) !== '' ) {
+ $comment .= "/$caller";
+ }
+
+ $res = $dbr->select( $table, $fields, $conds, $comment );
+ foreach ( $res as $row ) { // load each user into cache
+ $userId = (int)$row->user_id;
+ $this->cache[$userId]['name'] = $row->user_name;
+ $this->cache[$userId]['real_name'] = $row->user_real_name;
+ $this->cache[$userId]['registration'] = $row->user_registration;
+ $usersToCheck[$userId] = $row->user_name;
+ }
+ }
+
+ $lb = new LinkBatch();
+ foreach ( $usersToCheck as $userId => $name ) {
+ if ( $this->queryNeeded( $userId, 'userpage', $options ) ) {
+ $lb->add( NS_USER, str_replace( ' ', '_', $row->user_name ) );
+ $this->typesCached[$userId]['userpage'] = 1;
+ }
+ if ( $this->queryNeeded( $userId, 'usertalk', $options ) ) {
+ $lb->add( NS_USER_TALK, str_replace( ' ', '_', $row->user_name ) );
+ $this->typesCached[$userId]['usertalk'] = 1;
+ }
+ }
+ $lb->execute();
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * 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
+ * @return bool
+ */
+ protected function queryNeeded( $uid, $type, array $options ) {
+ return ( in_array( $type, $options ) && !isset( $this->typesCached[$uid][$type] ) );
+ }
+}
diff --git a/includes/dao/IDBAccessObject.php b/includes/dao/IDBAccessObject.php
new file mode 100644
index 00000000..e30522a5
--- /dev/null
+++ b/includes/dao/IDBAccessObject.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * This file contains database access object related constants.
+ *
+ * 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
+ */
+
+/**
+ * Interface for database access objects.
+ *
+ * Classes using this support a set of constants in a bitfield argument to their data loading
+ * functions. In general, objects should assume READ_NORMAL if no flags are explicitly given,
+ * though certain objects may assume READ_LATEST for common use case or legacy reasons.
+ *
+ * There are three types of reads:
+ * - READ_NORMAL : Potentially cached read of data (e.g. from a slave or stale replica)
+ * - READ_LATEST : Up-to-date read as of transaction start (e.g. from master or a quorum read)
+ * - READ_LOCKING : Up-to-date read as of now, that locks the records for the transaction
+ *
+ * Callers should use READ_NORMAL (or pass in no flags) unless the read determines a write.
+ * In theory, such cases may require READ_LOCKING, though to avoid contention, READ_LATEST is
+ * often good enough. If UPDATE race condition checks are required on a row and expensive code
+ * must run after the row is fetched to determine the UPDATE, it may help to do something like:
+ * - a) Read the current row
+ * - 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
+ */
+interface IDBAccessObject {
+ // Constants for object loading bitfield flags (higher => higher QoS)
+ 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
+ const READ_NORMAL = 0; // read from the slave
+
+ // Convenience constant for tracking how data was loaded (higher => higher QoS)
+ const READ_NONE = -1; // not loaded yet (or the object was cleared)
+}
diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php
index bd0895cf..4e43642f 100644
--- a/includes/db/CloneDatabase.php
+++ b/includes/db/CloneDatabase.php
@@ -20,6 +20,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
+ * @file
* @ingroup Database
*/
diff --git a/includes/db/Database.php b/includes/db/Database.php
index f3e84675..5f10b97d 100644
--- a/includes/db/Database.php
+++ b/includes/db/Database.php
@@ -2,10 +2,26 @@
/**
* @defgroup Database Database
*
+ * This file deals with database interface functions
+ * and query specifics/optimisations.
+ *
+ * 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
- * This file deals with database interface functions
- * and query specifics/optimisations
*/
/** Number of times to re-try an operation in case of deadlock */
@@ -209,12 +225,15 @@ abstract class DatabaseBase implements DatabaseType {
protected $mServer, $mUser, $mPassword, $mDBname;
- /**
- * @var DatabaseBase
- */
protected $mConn = null;
protected $mOpened = false;
+ /**
+ * @since 1.20
+ * @var array of Closure
+ */
+ protected $mTrxIdleCallbacks = array();
+
protected $mTablePrefix;
protected $mFlags;
protected $mTrxLevel = 0;
@@ -253,9 +272,9 @@ abstract class DatabaseBase implements DatabaseType {
* - false to disable debugging
* - omitted or null to do nothing
*
- * @return The previous value of the flag
+ * @return bool|null previous value of the flag
*/
- function debug( $debug = null ) {
+ public function debug( $debug = null ) {
return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
}
@@ -279,9 +298,9 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $buffer null|bool
*
- * @return The previous value of the flag
+ * @return null|bool The previous value of the flag
*/
- function bufferResults( $buffer = null ) {
+ public function bufferResults( $buffer = null ) {
if ( is_null( $buffer ) ) {
return !(bool)( $this->mFlags & DBO_NOBUFFER );
} else {
@@ -300,7 +319,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool The previous value of the flag.
*/
- function ignoreErrors( $ignoreErrors = null ) {
+ public function ignoreErrors( $ignoreErrors = null ) {
return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
}
@@ -310,28 +329,28 @@ 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 An integer (0 or 1), or omitted to leave it unchanged.
- * @return The previous value
+ * @param $level int An integer (0 or 1), or omitted to leave it unchanged.
+ * @return int The previous value
*/
- function trxLevel( $level = null ) {
+ public function trxLevel( $level = null ) {
return wfSetVar( $this->mTrxLevel, $level );
}
/**
* Get/set the number of errors logged. Only useful when errors are ignored
- * @param $count The count to set, or omitted to leave it unchanged.
- * @return The error count
+ * @param $count int The count to set, or omitted to leave it unchanged.
+ * @return int The error count
*/
- function errorCount( $count = null ) {
+ public function errorCount( $count = null ) {
return wfSetVar( $this->mErrorCount, $count );
}
/**
* Get/set the table prefix.
- * @param $prefix The table prefix to set, or omitted to leave it unchanged.
- * @return The previous table prefix.
+ * @param $prefix string The table prefix to set, or omitted to leave it unchanged.
+ * @return string The previous table prefix.
*/
- function tablePrefix( $prefix = null ) {
+ public function tablePrefix( $prefix = null ) {
return wfSetVar( $this->mTablePrefix, $prefix );
}
@@ -344,7 +363,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return LoadBalancer|null
*/
- function getLBInfo( $name = null ) {
+ public function getLBInfo( $name = null ) {
if ( is_null( $name ) ) {
return $this->mLBInfo;
} else {
@@ -364,7 +383,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $name
* @param $value
*/
- function setLBInfo( $name, $value = null ) {
+ public function setLBInfo( $name, $value = null ) {
if ( is_null( $value ) ) {
$this->mLBInfo = $name;
} else {
@@ -377,7 +396,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $lag int
*/
- function setFakeSlaveLag( $lag ) {
+ public function setFakeSlaveLag( $lag ) {
$this->mFakeSlaveLag = $lag;
}
@@ -386,7 +405,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $enabled bool
*/
- function setFakeMaster( $enabled = true ) {
+ public function setFakeMaster( $enabled = true ) {
$this->mFakeMaster = $enabled;
}
@@ -395,7 +414,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function cascadingDeletes() {
+ public function cascadingDeletes() {
return false;
}
@@ -404,7 +423,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function cleanupTriggers() {
+ public function cleanupTriggers() {
return false;
}
@@ -414,7 +433,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function strictIPs() {
+ public function strictIPs() {
return false;
}
@@ -423,7 +442,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function realTimestamps() {
+ public function realTimestamps() {
return false;
}
@@ -432,7 +451,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function implicitGroupby() {
+ public function implicitGroupby() {
return true;
}
@@ -442,17 +461,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function implicitOrderby() {
- return true;
- }
-
- /**
- * Returns true if this database requires that SELECT DISTINCT queries require that all
- ORDER BY expressions occur in the SELECT list per the SQL92 standard
- *
- * @return bool
- */
- function standardSelectDistinct() {
+ public function implicitOrderby() {
return true;
}
@@ -462,7 +471,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function searchableIPs() {
+ public function searchableIPs() {
return false;
}
@@ -471,7 +480,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function functionalIndexes() {
+ public function functionalIndexes() {
return false;
}
@@ -479,7 +488,7 @@ abstract class DatabaseBase implements DatabaseType {
* Return the last query that went through DatabaseBase::query()
* @return String
*/
- function lastQuery() {
+ public function lastQuery() {
return $this->mLastQuery;
}
@@ -489,15 +498,25 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function doneWrites() {
+ public function doneWrites() {
return $this->mDoneWrites;
}
/**
+ * Returns true if there is a transaction open with possible write
+ * queries or transaction idle callbacks waiting on it to finish.
+ *
+ * @return bool
+ */
+ public function writesOrCallbacksPending() {
+ return $this->mTrxLevel && ( $this->mDoneWrites || $this->mTrxIdleCallbacks );
+ }
+
+ /**
* Is a connection to the database open?
* @return Boolean
*/
- function isOpen() {
+ public function isOpen() {
return $this->mOpened;
}
@@ -513,8 +532,12 @@ abstract class DatabaseBase implements DatabaseType {
* and removes it in command line mode
* - DBO_PERSISTENT: use persistant database connection
*/
- function setFlag( $flag ) {
+ public function setFlag( $flag ) {
+ global $wgDebugDBTransactions;
$this->mFlags |= $flag;
+ if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) {
+ wfDebug("Implicit transactions are now disabled.\n");
+ }
}
/**
@@ -522,8 +545,12 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $flag: same as setFlag()'s $flag param
*/
- function clearFlag( $flag ) {
+ public function clearFlag( $flag ) {
+ global $wgDebugDBTransactions;
$this->mFlags &= ~$flag;
+ if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) {
+ wfDebug("Implicit transactions are now disabled.\n");
+ }
}
/**
@@ -532,7 +559,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $flag: same as setFlag()'s $flag param
* @return Boolean
*/
- function getFlag( $flag ) {
+ public function getFlag( $flag ) {
return !!( $this->mFlags & $flag );
}
@@ -543,14 +570,14 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return string
*/
- function getProperty( $name ) {
+ public function getProperty( $name ) {
return $this->$name;
}
/**
* @return string
*/
- function getWikiID() {
+ public function getWikiID() {
if ( $this->mTablePrefix ) {
return "{$this->mDBname}-{$this->mTablePrefix}";
} else {
@@ -588,15 +615,21 @@ abstract class DatabaseBase implements DatabaseType {
function __construct( $server = false, $user = false, $password = false, $dbName = false,
$flags = 0, $tablePrefix = 'get from global'
) {
- global $wgDBprefix, $wgCommandLineMode;
+ global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions;
$this->mFlags = $flags;
if ( $this->mFlags & DBO_DEFAULT ) {
if ( $wgCommandLineMode ) {
$this->mFlags &= ~DBO_TRX;
+ if ( $wgDebugDBTransactions ) {
+ wfDebug("Implicit transaction open disabled.\n");
+ }
} else {
$this->mFlags |= DBO_TRX;
+ if ( $wgDebugDBTransactions ) {
+ wfDebug("Implicit transaction open enabled.\n");
+ }
}
}
@@ -622,35 +655,6 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Same as new DatabaseMysql( ... ), kept for backward compatibility
- * @deprecated since 1.17
- *
- * @param $server
- * @param $user
- * @param $password
- * @param $dbName
- * @param $flags int
- * @return DatabaseMysql
- */
- static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) {
- wfDeprecated( __METHOD__, '1.17' );
- return new DatabaseMysql( $server, $user, $password, $dbName, $flags );
- }
-
- /**
- * Same as new factory( ... ), kept for backward compatibility
- * @deprecated since 1.18
- * @see Database::factory()
- */
- public final static function newFromType( $dbType, $p = array() ) {
- wfDeprecated( __METHOD__, '1.18' );
- if ( isset( $p['tableprefix'] ) ) {
- $p['tablePrefix'] = $p['tableprefix'];
- }
- return self::factory( $dbType, $p );
- }
-
- /**
* 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 ) );
@@ -665,6 +669,8 @@ abstract class DatabaseBase implements DatabaseType {
* @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
@@ -707,7 +713,7 @@ abstract class DatabaseBase implements DatabaseType {
}
if ( $this->mPHPError ) {
$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
- $error = preg_replace( '!^.*?:(.*)$!', '$1', $error );
+ $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
return $error;
} else {
return false;
@@ -728,12 +734,31 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return Bool operation success. true if already closed.
*/
- function close() {
- # Stub, should probably be overridden
- return true;
+ public function close() {
+ if ( count( $this->mTrxIdleCallbacks ) ) { // sanity
+ throw new MWException( "Transaction idle callbacks still pending." );
+ }
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ if ( $this->trxLevel() ) {
+ $this->commit( __METHOD__ );
+ }
+ $ret = $this->closeConnection();
+ $this->mConn = false;
+ return $ret;
+ } else {
+ return true;
+ }
}
/**
+ * Closes underlying database connection
+ * @since 1.20
+ * @return bool: Whether connection was closed successfully
+ */
+ protected abstract function closeConnection();
+
+ /**
* @param $error String: fallback error message, used if none is given by DB
*/
function reportConnectionError( $error = 'Unknown error' ) {
@@ -762,8 +787,8 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function isWriteQuery( $sql ) {
- return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW|\(SELECT)\b/i', $sql );
+ public function isWriteQuery( $sql ) {
+ return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
}
/**
@@ -833,8 +858,13 @@ abstract class DatabaseBase implements DatabaseType {
# 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 )
+ if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) {
+ global $wgDebugDBTransactions;
+ if ( $wgDebugDBTransactions ) {
+ wfDebug("Implicit transaction start.\n");
+ }
$this->begin( __METHOD__ . " ($fname)" );
+ }
}
if ( $this->debug() ) {
@@ -863,6 +893,7 @@ abstract class DatabaseBase implements DatabaseType {
if ( false === $ret && $this->wasErrorReissuable() ) {
# Transaction is gone, like it or not
$this->mTrxLevel = 0;
+ $this->mTrxIdleCallbacks = array(); // cancel
wfDebug( "Connection lost, reconnecting...\n" );
if ( $this->ping() ) {
@@ -903,7 +934,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fname String
* @param $tempIgnore Boolean
*/
- function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
# Ignore errors during error handling to avoid infinite recursion
$ignore = $this->ignoreErrors( true );
++$this->mErrorCount;
@@ -928,16 +959,12 @@ abstract class DatabaseBase implements DatabaseType {
* & = filename; reads the file and inserts as a blob
* (we don't use this though...)
*
- * This function should not be used directly by new code outside of the
- * database classes. The query wrapper functions (select() etc.) should be
- * used instead.
- *
* @param $sql string
* @param $func string
*
* @return array
*/
- function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
+ protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
/* MySQL doesn't support prepared statements (yet), so just
pack up the query for reference. We'll manually replace
the bits later. */
@@ -948,7 +975,7 @@ abstract class DatabaseBase implements DatabaseType {
* Free a prepared query, generated by prepare().
* @param $prepared
*/
- function freePrepared( $prepared ) {
+ protected function freePrepared( $prepared ) {
/* No-op by default */
}
@@ -959,7 +986,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return ResultWrapper
*/
- function execute( $prepared, $args = null ) {
+ public function execute( $prepared, $args = null ) {
if ( !is_array( $args ) ) {
# Pull the var args
$args = func_get_args();
@@ -972,41 +999,13 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Prepare & execute an SQL statement, quoting and inserting arguments
- * in the appropriate places.
+ * For faking prepared SQL statements on DBs that don't support it directly.
*
- * This function should not be used directly by new code outside of the
- * database classes. The query wrapper functions (select() etc.) should be
- * used instead.
- *
- * @param $query String
- * @param $args ...
- *
- * @return ResultWrapper
- */
- function safeQuery( $query, $args = null ) {
- $prepared = $this->prepare( $query, 'DatabaseBase::safeQuery' );
-
- if ( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
- }
-
- $retval = $this->execute( $prepared, $args );
- $this->freePrepared( $prepared );
-
- return $retval;
- }
-
- /**
- * For faking prepared SQL statements on DBs that don't support
- * it directly.
* @param $preparedQuery String: a 'preparable' SQL statement
* @param $args Array of arguments to fill it with
* @return string executable SQL
*/
- function fillPrepared( $preparedQuery, $args ) {
+ public function fillPrepared( $preparedQuery, $args ) {
reset( $args );
$this->preparedArgs =& $args;
@@ -1022,7 +1021,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $matches Array
* @return String
*/
- function fillPreparedArg( $matches ) {
+ protected function fillPreparedArg( $matches ) {
switch( $matches[1] ) {
case '\\?': return '?';
case '\\!': return '!';
@@ -1049,32 +1048,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $res Mixed: A SQL result
*/
- function freeResult( $res ) {
- }
-
- /**
- * Simple UPDATE wrapper.
- * Usually throws a DBQueryError on failure.
- * If errors are explicitly ignored, returns success
- *
- * This function exists for historical reasons, DatabaseBase::update() has a more standard
- * calling convention and feature set
- *
- * @param $table string
- * @param $var
- * @param $value
- * @param $cond
- * @param $fname string
- *
- * @return bool
- */
- function set( $table, $var, $value, $cond, $fname = 'DatabaseBase::set' ) {
- $table = $this->tableName( $table );
- $sql = "UPDATE $table SET $var = '" .
- $this->strencode( $value ) . "' WHERE ($cond)";
-
- return (bool)$this->query( $sql, $fname );
- }
+ public function freeResult( $res ) {}
/**
* A SELECT wrapper which returns a single field from a single result row.
@@ -1091,9 +1065,9 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fname string The function name of the caller.
* @param $options string|array The query options. See DatabaseBase::select() for details.
*
- * @return false|mixed The value from the field, or false on failure.
+ * @return bool|mixed The value from the field, or false on failure.
*/
- function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField',
+ public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField',
$options = array() )
{
if ( !is_array( $options ) ) {
@@ -1126,7 +1100,7 @@ abstract class DatabaseBase implements DatabaseType {
* @return Array
* @see DatabaseBase::select()
*/
- function makeSelectOptions( $options ) {
+ public function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
$startOpts = '';
@@ -1146,7 +1120,10 @@ abstract class DatabaseBase implements DatabaseType {
}
if ( isset( $options['HAVING'] ) ) {
- $preLimitTail .= " HAVING {$options['HAVING']}";
+ $having = is_array( $options['HAVING'] )
+ ? $this->makeList( $options['HAVING'], LIST_AND )
+ : $options['HAVING'];
+ $preLimitTail .= " HAVING {$having}";
}
if ( isset( $options['ORDER BY'] ) ) {
@@ -1245,10 +1222,12 @@ abstract class DatabaseBase implements DatabaseType {
* @param $vars string|array
*
* May be either a field name or an array of field names. The field names
- * here are complete fragments of SQL, for direct inclusion into the SELECT
- * query. Expressions and aliases may be specified as in SQL, for example:
+ * can be complete fragments of SQL, for direct inclusion into the SELECT
+ * query. If an array is given, field aliases can be specified, for example:
+ *
+ * array( 'maxrev' => 'MAX(rev_id)' )
*
- * array( 'MAX(rev_id) AS maxrev' )
+ * This includes an expression with the alias "maxrev" in the query.
*
* If an expression is given, care must be taken to ensure that it is
* DBMS-independent.
@@ -1305,7 +1284,9 @@ abstract class DatabaseBase implements DatabaseType {
* - GROUP BY: May be either an SQL fragment string naming a field or
* expression to group by, or an array of such SQL fragments.
*
- * - HAVING: A string containing a HAVING clause.
+ * - HAVING: May be either an string containing a HAVING clause or an array of
+ * conditions building the HAVING clause. If an array is given, the conditions
+ * constructed from each element are combined with AND.
*
* - ORDER BY: May be either an SQL fragment giving a field name or
* expression to order by, or an array of such SQL fragments.
@@ -1351,7 +1332,7 @@ abstract class DatabaseBase implements DatabaseType {
* DBQueryError exception will be thrown, except if the "ignore errors"
* option was set, in which case false will be returned.
*/
- function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
$options = array(), $join_conds = array() ) {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
@@ -1360,7 +1341,9 @@ abstract class DatabaseBase implements DatabaseType {
/**
* The equivalent of DatabaseBase::select() except that the constructed SQL
- * is returned, instead of being immediately executed.
+ * is returned, instead of being immediately executed. This can be useful for
+ * 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
@@ -1369,12 +1352,14 @@ abstract class DatabaseBase implements DatabaseType {
* @param $options string|array Query options
* @param $join_conds string|array Join conditions
*
- * @return SQL query string.
+ * @return string SQL query string.
* @see DatabaseBase::select()
*/
- function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) {
+ public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ $options = array(), $join_conds = array() )
+ {
if ( is_array( $vars ) ) {
- $vars = implode( ',', $vars );
+ $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
}
$options = (array)$options;
@@ -1435,9 +1420,9 @@ abstract class DatabaseBase implements DatabaseType {
* @param $options string|array Query options
* @param $join_conds array|string Join conditions
*
- * @return ResultWrapper|bool
+ * @return object|bool
*/
- function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow',
+ public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow',
$options = array(), $join_conds = array() )
{
$options = (array)$options;
@@ -1481,7 +1466,7 @@ abstract class DatabaseBase implements DatabaseType {
$fname = 'DatabaseBase::estimateRowCount', $options = array() )
{
$rows = 0;
- $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options );
+ $res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options );
if ( $res ) {
$row = $this->fetchRow( $res );
@@ -1527,7 +1512,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fname String: calling function name (optional)
* @return Boolean: whether $table has filed $field
*/
- function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) {
+ public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) {
$info = $this->fieldInfo( $table, $field );
return (bool)$info;
@@ -1544,7 +1529,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool|null
*/
- function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
+ public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
$info = $this->indexInfo( $table, $index, $fname );
if ( is_null( $info ) ) {
return null;
@@ -1561,7 +1546,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function tableExists( $table, $fname = __METHOD__ ) {
+ public function tableExists( $table, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
$old = $this->ignoreErrors( true );
$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
@@ -1576,7 +1561,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $index
* @return string
*/
- function fieldType( $res, $index ) {
+ public function fieldType( $res, $index ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
@@ -1592,7 +1577,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function indexUnique( $table, $index ) {
+ public function indexUnique( $table, $index ) {
$indexInfo = $this->indexInfo( $table, $index );
if ( !$indexInfo ) {
@@ -1608,7 +1593,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $options array
* @return string
*/
- function makeInsertOptions( $options ) {
+ protected function makeInsertOptions( $options ) {
return implode( ' ', $options );
}
@@ -1645,7 +1630,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) {
+ public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) {
# No rows to insert, easy just return now
if ( !count( $a ) ) {
return true;
@@ -1693,7 +1678,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $options Array: The options passed to DatabaseBase::update
* @return string
*/
- function makeUpdateOptions( $options ) {
+ protected function makeUpdateOptions( $options ) {
if ( !is_array( $options ) ) {
$options = array( $options );
}
@@ -1759,7 +1744,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return string
*/
- function makeList( $a, $mode = LIST_COMMA ) {
+ public function makeList( $a, $mode = LIST_COMMA ) {
if ( !is_array( $a ) ) {
throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
}
@@ -1819,12 +1804,12 @@ abstract class DatabaseBase implements DatabaseType {
* The keys on each level may be either integers or strings.
*
* @param $data Array: organized as 2-d
- * array(baseKeyVal => array(subKeyVal => <ignored>, ...), ...)
+ * 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')
* @return Mixed: string SQL fragment, or false if no items in array.
*/
- function makeWhereFrom2d( $data, $baseKey, $subKey ) {
+ public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
$conds = array();
foreach ( $data as $base => $sub ) {
@@ -1844,14 +1829,22 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Bitwise operations
+ * Return aggregated value alias
+ *
+ * @param $valuedata
+ * @param $valuename string
+ *
+ * @return string
*/
+ public function aggregateValue( $valuedata, $valuename = 'value' ) {
+ return $valuename;
+ }
/**
* @param $field
* @return string
*/
- function bitNot( $field ) {
+ public function bitNot( $field ) {
return "(~$field)";
}
@@ -1860,7 +1853,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fieldRight
* @return string
*/
- function bitAnd( $fieldLeft, $fieldRight ) {
+ public function bitAnd( $fieldLeft, $fieldRight ) {
return "($fieldLeft & $fieldRight)";
}
@@ -1869,11 +1862,20 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fieldRight
* @return string
*/
- function bitOr( $fieldLeft, $fieldRight ) {
+ public function bitOr( $fieldLeft, $fieldRight ) {
return "($fieldLeft | $fieldRight)";
}
/**
+ * Build a concatenation list to feed into a SQL query
+ * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting
+ * @return String
+ */
+ public function buildConcat( $stringList ) {
+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ }
+
+ /**
* Change the current database
*
* @todo Explain what exactly will fail if this is not overridden.
@@ -1882,7 +1884,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool Success or failure
*/
- function selectDB( $db ) {
+ public function selectDB( $db ) {
# Stub. Shouldn't cause serious problems if it's not overridden, but
# if your database engine supports a concept similar to MySQL's
# databases you may as well.
@@ -1893,14 +1895,14 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Get the current DB name
*/
- function getDBname() {
+ public function getDBname() {
return $this->mDBname;
}
/**
* Get the server hostname or IP address
*/
- function getServer() {
+ public function getServer() {
return $this->mServer;
}
@@ -1921,7 +1923,7 @@ abstract class DatabaseBase implements DatabaseType {
* raw - Do not add identifier quotes to the table name
* @return String: full database name
*/
- function tableName( $name, $format = 'quoted' ) {
+ public function tableName( $name, $format = 'quoted' ) {
global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
# Skip the entire process when we have a string quoted on both ends.
# Note that we check the end so that we will still quote any use of
@@ -2067,6 +2069,39 @@ 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)
+ * @return string SQL name for aliased field. Will not alias a field to its own name
+ */
+ public function fieldNameWithAlias( $name, $alias = false ) {
+ if ( !$alias || (string)$alias === (string)$name ) {
+ return $name;
+ } else {
+ return $name . ' AS ' . $alias; //PostgreSQL needs AS
+ }
+ }
+
+ /**
+ * Gets an array of aliased field names
+ *
+ * @param $fields array( [alias] => field )
+ * @return array of strings, see fieldNameWithAlias()
+ */
+ public function fieldNamesWithAlias( $fields ) {
+ $retval = array();
+ foreach ( $fields as $alias => $field ) {
+ if ( is_numeric( $alias ) ) {
+ $alias = $field;
+ }
+ $retval[] = $this->fieldNameWithAlias( $field, $alias );
+ }
+ return $retval;
+ }
+
+ /**
* Get the aliased table name clause for a FROM clause
* which might have a JOIN and/or USE INDEX clause
*
@@ -2134,7 +2169,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return string
*/
- function indexName( $index ) {
+ protected function indexName( $index ) {
// Backwards-compatibility hack
$renamed = array(
'ar_usertext_timestamp' => 'usertext_timestamp',
@@ -2157,7 +2192,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return string
*/
- function addQuotes( $s ) {
+ public function addQuotes( $s ) {
if ( $s === null ) {
return 'NULL';
} else {
@@ -2196,36 +2231,6 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Backwards compatibility, identifier quoting originated in DatabasePostgres
- * which used quote_ident which does not follow our naming conventions
- * was renamed to addIdentifierQuotes.
- * @deprecated since 1.18 use addIdentifierQuotes
- *
- * @param $s string
- *
- * @return string
- */
- function quote_ident( $s ) {
- wfDeprecated( __METHOD__, '1.18' );
- return $this->addIdentifierQuotes( $s );
- }
-
- /**
- * Escape string for safe LIKE usage.
- * WARNING: you should almost never use this function directly,
- * instead use buildLike() that escapes everything automatically
- * @deprecated since 1.17, warnings in 1.17, removed in ???
- *
- * @param $s string
- *
- * @return string
- */
- public function escapeLike( $s ) {
- wfDeprecated( __METHOD__, '1.17' );
- return $this->escapeLikeInternal( $s );
- }
-
- /**
* @param $s string
* @return string
*/
@@ -2249,7 +2254,7 @@ abstract class DatabaseBase implements DatabaseType {
* @since 1.16
* @return String: fully built LIKE statement
*/
- function buildLike() {
+ public function buildLike() {
$params = func_get_args();
if ( count( $params ) > 0 && is_array( $params[0] ) ) {
@@ -2274,7 +2279,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return LikeMatch
*/
- function anyChar() {
+ public function anyChar() {
return new LikeMatch( '_' );
}
@@ -2283,7 +2288,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return LikeMatch
*/
- function anyString() {
+ public function anyString() {
return new LikeMatch( '%' );
}
@@ -2298,7 +2303,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $seqName string
* @return null
*/
- function nextSequenceValue( $seqName ) {
+ public function nextSequenceValue( $seqName ) {
return null;
}
@@ -2312,7 +2317,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $index
* @return string
*/
- function useIndexClause( $index ) {
+ public function useIndexClause( $index ) {
return '';
}
@@ -2338,7 +2343,7 @@ abstract class DatabaseBase implements DatabaseType {
* a field name or an array of field names
* @param $fname String: Calling function name (use __METHOD__) for logs/profiling
*/
- function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) {
+ public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) {
$quotedTable = $this->tableName( $table );
if ( count( $rows ) == 0 ) {
@@ -2439,7 +2444,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fname String: Calling function name (use __METHOD__) for
* logs/profiling
*/
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
+ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
$fname = 'DatabaseBase::deleteJoin' )
{
if ( !$conds ) {
@@ -2466,7 +2471,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return int
*/
- function textFieldSize( $table, $field ) {
+ public function textFieldSize( $table, $field ) {
$table = $this->tableName( $table );
$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
@@ -2491,7 +2496,7 @@ abstract class DatabaseBase implements DatabaseType {
* @return string Returns the text of the low priority option if it is
* supported, or a blank string otherwise
*/
- function lowPriorityOption() {
+ public function lowPriorityOption() {
return '';
}
@@ -2505,7 +2510,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) {
+ public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) {
if ( !$conds ) {
throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
}
@@ -2546,7 +2551,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return ResultWrapper
*/
- function insertSelect( $destTable, $srcTable, $varMap, $conds,
+ public function insertSelect( $destTable, $srcTable, $varMap, $conds,
$fname = 'DatabaseBase::insertSelect',
$insertOptions = array(), $selectOptions = array() )
{
@@ -2592,35 +2597,24 @@ abstract class DatabaseBase implements DatabaseType {
* If the result of the query is not ordered, then the rows to be returned
* are theoretically arbitrary.
*
- * $sql is expected to be a SELECT, if that makes a difference. For
- * UPDATE, limitResultForUpdate should be used.
+ * $sql is expected to be a SELECT, if that makes a difference.
*
* The version provided by default works in MySQL and SQLite. It will very
* likely need to be overridden for most other DBMSes.
*
* @param $sql String SQL query we will append the limit too
* @param $limit Integer the SQL limit
- * @param $offset Integer|false the SQL offset (default false)
+ * @param $offset Integer|bool the SQL offset (default false)
*
* @return string
*/
- function limitResult( $sql, $limit, $offset = false ) {
+ public function limitResult( $sql, $limit, $offset = false ) {
if ( !is_numeric( $limit ) ) {
throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
}
-
return "$sql LIMIT "
- . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
- . "{$limit} ";
- }
-
- /**
- * @param $sql
- * @param $num
- * @return string
- */
- function limitResultForUpdate( $sql, $num ) {
- return $this->limitResult( $sql, $num, 0 );
+ . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
+ . "{$limit} ";
}
/**
@@ -2628,7 +2622,7 @@ abstract class DatabaseBase implements DatabaseType {
* within the UNION construct.
* @return Boolean
*/
- function unionSupportsOrderAndLimit() {
+ public function unionSupportsOrderAndLimit() {
return true; // True for almost every DB supported
}
@@ -2640,7 +2634,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $all Boolean: use UNION ALL
* @return String: SQL fragment
*/
- function unionQueries( $sqls, $all ) {
+ public function unionQueries( $sqls, $all ) {
$glue = $all ? ') UNION ALL (' : ') UNION (';
return '(' . implode( $glue, $sqls ) . ')';
}
@@ -2649,12 +2643,15 @@ 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: SQL expression which will result in a boolean value
+ * @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
* @return String: SQL fragment
*/
- function conditional( $cond, $trueVal, $falseVal ) {
+ public function conditional( $cond, $trueVal, $falseVal ) {
+ if ( is_array( $cond ) ) {
+ $cond = $this->makeList( $cond, LIST_AND );
+ }
return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
}
@@ -2668,7 +2665,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return string
*/
- function strreplace( $orig, $old, $new ) {
+ public function strreplace( $orig, $old, $new ) {
return "REPLACE({$orig}, {$old}, {$new})";
}
@@ -2678,7 +2675,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return int
*/
- function getServerUptime() {
+ public function getServerUptime() {
return 0;
}
@@ -2688,7 +2685,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function wasDeadlock() {
+ public function wasDeadlock() {
return false;
}
@@ -2698,7 +2695,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function wasLockTimeout() {
+ public function wasLockTimeout() {
return false;
}
@@ -2709,7 +2706,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function wasErrorReissuable() {
+ public function wasErrorReissuable() {
return false;
}
@@ -2719,7 +2716,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function wasReadOnlyError() {
+ public function wasReadOnlyError() {
return false;
}
@@ -2741,8 +2738,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- function deadlockLoop() {
-
+ public function deadlockLoop() {
$this->begin( __METHOD__ );
$args = func_get_args();
$function = array_shift( $args );
@@ -2790,11 +2786,11 @@ abstract class DatabaseBase implements DatabaseType {
* @param $timeout Integer: the maximum number of seconds to wait for
* synchronisation
*
- * @return An integer: zero if the slave was past that position already,
+ * @return integer: zero if the slave was past that position already,
* greater than zero if we waited for some period of time, less than
* zero if we timed out.
*/
- function masterPosWait( DBMasterPos $pos, $timeout ) {
+ public function masterPosWait( DBMasterPos $pos, $timeout ) {
wfProfileIn( __METHOD__ );
if ( !is_null( $this->mFakeSlaveLag ) ) {
@@ -2827,7 +2823,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return DBMasterPos, or false if this is not a slave.
*/
- function getSlavePos() {
+ public function getSlavePos() {
if ( !is_null( $this->mFakeSlaveLag ) ) {
$pos = new MySQLMasterPos( 'fake', microtime( true ) - $this->mFakeSlaveLag );
wfDebug( __METHOD__ . ": fake slave pos = $pos\n" );
@@ -2843,7 +2839,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return DBMasterPos, or false if this is not a master
*/
- function getMasterPos() {
+ public function getMasterPos() {
if ( $this->mFakeMaster ) {
return new MySQLMasterPos( 'fake', microtime( true ) );
} else {
@@ -2852,11 +2848,65 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Begin a transaction, committing any previously open transaction
+ * 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.
+ * Callbacks must commit any transactions that they begin.
+ *
+ * This is useful for updates to different systems or separate transactions are needed.
+ *
+ * @param Closure $callback
+ * @return void
+ */
+ final public function onTransactionIdle( Closure $callback ) {
+ if ( $this->mTrxLevel ) {
+ $this->mTrxIdleCallbacks[] = $callback;
+ } else {
+ $callback();
+ }
+ }
+
+ /**
+ * Actually run the "on transaction idle" callbacks
+ */
+ protected function runOnTransactionIdleCallbacks() {
+ $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
+
+ $e = null; // last exception
+ do { // callbacks may add callbacks :)
+ $callbacks = $this->mTrxIdleCallbacks;
+ $this->mTrxIdleCallbacks = array(); // recursion guard
+ foreach ( $callbacks as $callback ) {
+ try {
+ $this->clearFlag( DBO_TRX ); // make each query its own transaction
+ $callback();
+ $this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin()
+ } catch ( Exception $e ) {}
+ }
+ } while ( count( $this->mTrxIdleCallbacks ) );
+
+ if ( $e instanceof Exception ) {
+ throw $e; // re-throw any last exception
+ }
+ }
+
+ /**
+ * Begin a transaction
*
* @param $fname string
*/
- function begin( $fname = 'DatabaseBase::begin' ) {
+ final public function begin( $fname = 'DatabaseBase::begin' ) {
+ if ( $this->mTrxLevel ) { // implicit commit
+ $this->doCommit( $fname );
+ $this->runOnTransactionIdleCallbacks();
+ }
+ $this->doBegin( $fname );
+ }
+
+ /**
+ * @see DatabaseBase::begin()
+ * @param type $fname
+ */
+ protected function doBegin( $fname ) {
$this->query( 'BEGIN', $fname );
$this->mTrxLevel = 1;
}
@@ -2866,7 +2916,16 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $fname string
*/
- function commit( $fname = 'DatabaseBase::commit' ) {
+ final public function commit( $fname = 'DatabaseBase::commit' ) {
+ $this->doCommit( $fname );
+ $this->runOnTransactionIdleCallbacks();
+ }
+
+ /**
+ * @see DatabaseBase::commit()
+ * @param type $fname
+ */
+ protected function doCommit( $fname ) {
if ( $this->mTrxLevel ) {
$this->query( 'COMMIT', $fname );
$this->mTrxLevel = 0;
@@ -2879,7 +2938,16 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $fname string
*/
- function rollback( $fname = 'DatabaseBase::rollback' ) {
+ final public function rollback( $fname = 'DatabaseBase::rollback' ) {
+ $this->doRollback( $fname );
+ $this->mTrxIdleCallbacks = array(); // cancel
+ }
+
+ /**
+ * @see DatabaseBase::rollback()
+ * @param type $fname
+ */
+ protected function doRollback( $fname ) {
if ( $this->mTrxLevel ) {
$this->query( 'ROLLBACK', $fname, true );
$this->mTrxLevel = 0;
@@ -2900,7 +2968,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $fname String: calling function name
* @return Boolean: true if operation was successful
*/
- function duplicateTableStructure( $oldName, $newName, $temporary = false,
+ public function duplicateTableStructure( $oldName, $newName, $temporary = false,
$fname = 'DatabaseBase::duplicateTableStructure' )
{
throw new MWException(
@@ -2910,7 +2978,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* List all tables on the database
*
- * @param $prefix Only show tables with this prefix, e.g. mw_
+ * @param $prefix string Only show tables with this prefix, e.g. mw_
* @param $fname String: calling function name
*/
function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) {
@@ -2928,7 +2996,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return string
*/
- function timestamp( $ts = 0 ) {
+ public function timestamp( $ts = 0 ) {
return wfTimestamp( TS_MW, $ts );
}
@@ -2945,7 +3013,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return string
*/
- function timestampOrNull( $ts = null ) {
+ public function timestampOrNull( $ts = null ) {
if ( is_null( $ts ) ) {
return null;
} else {
@@ -2968,7 +3036,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool|ResultWrapper
*/
- function resultObject( $result ) {
+ public function resultObject( $result ) {
if ( empty( $result ) ) {
return false;
} elseif ( $result instanceof ResultWrapper ) {
@@ -2982,23 +3050,11 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Return aggregated value alias
- *
- * @param $valuedata
- * @param $valuename string
- *
- * @return string
- */
- function aggregateValue ( $valuedata, $valuename = 'value' ) {
- return $valuename;
- }
-
- /**
* Ping the server and try to reconnect if it there is no connection
*
* @return bool Success or failure
*/
- function ping() {
+ public function ping() {
# Stub. Not essential to override.
return true;
}
@@ -3010,9 +3066,9 @@ abstract class DatabaseBase implements DatabaseType {
* installations. Most callers should use LoadBalancer::safeGetLag()
* instead.
*
- * @return Database replication lag in seconds
+ * @return int Database replication lag in seconds
*/
- function getLag() {
+ public function getLag() {
return intval( $this->mFakeSlaveLag );
}
@@ -3033,7 +3089,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $b string
* @return string
*/
- function encodeBlob( $b ) {
+ public function encodeBlob( $b ) {
return $b;
}
@@ -3044,23 +3100,11 @@ abstract class DatabaseBase implements DatabaseType {
* @param $b string
* @return string
*/
- function decodeBlob( $b ) {
+ public function decodeBlob( $b ) {
return $b;
}
/**
- * Override database's default connection timeout
- *
- * @param $timeout Integer in seconds
- * @return void
- * @deprecated since 1.19; use setSessionOptions()
- */
- public function setTimeout( $timeout ) {
- wfDeprecated( __METHOD__, '1.19' );
- $this->setSessionOptions( array( 'connTimeout' => $timeout ) );
- }
-
- /**
* Override database's default behavior. $options include:
* 'connTimeout' : Set the connection timeout value in seconds.
* May be useful for very long batch queries such as
@@ -3085,7 +3129,9 @@ abstract class DatabaseBase implements DatabaseType {
* generated dynamically using $filename
* @return bool|string
*/
- function sourceFile( $filename, $lineCallback = false, $resultCallback = false, $fname = false ) {
+ public function sourceFile(
+ $filename, $lineCallback = false, $resultCallback = false, $fname = false
+ ) {
wfSuppressWarnings();
$fp = fopen( $filename, 'r' );
wfRestoreWarnings();
@@ -3135,9 +3181,9 @@ 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 False, or array mapping variable name to value.
+ * @param $vars bool|array mapping variable name to value.
*/
- function setSchemaVars( $vars ) {
+ public function setSchemaVars( $vars ) {
$this->mSchemaVars = $vars;
}
@@ -3323,12 +3369,15 @@ 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
- * @return String
+ * 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
*/
- function buildConcat( $stringList ) {
- return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ public function lockIsFree( $lockName, $method ) {
+ return true;
}
/**
@@ -3352,7 +3401,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param $lockName String: Name of lock to release
* @param $method String: Name of method calling us
*
- * @return Returns 1 if the lock was released, 0 if the lock was not established
+ * @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
* lock did not exist
*/
@@ -3425,17 +3474,28 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * Encode an expiry time
+ * Encode an expiry time into the DBMS dependent format
*
* @param $expiry String: timestamp for expiry, or the 'infinity' string
* @return String
*/
public function encodeExpiry( $expiry ) {
- if ( $expiry == '' || $expiry == $this->getInfinity() ) {
- return $this->getInfinity();
- } else {
- return $this->timestamp( $expiry );
- }
+ return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
+ ? $this->getInfinity()
+ : $this->timestamp( $expiry );
+ }
+
+ /**
+ * Decode an expiry time into a DBMS independent format
+ *
+ * @param $expiry String: DB timestamp field value for expiry
+ * @param $format integer: TS_* constant, defaults to TS_MW
+ * @return String
+ */
+ public function decodeExpiry( $expiry, $format = TS_MW ) {
+ return ( $expiry == '' || $expiry == $this->getInfinity() )
+ ? 'infinity'
+ : wfTimestamp( $format, $expiry );
}
/**
@@ -3450,4 +3510,17 @@ abstract class DatabaseBase implements DatabaseType {
public function setBigSelects( $value = true ) {
// no-op
}
+
+ /**
+ * @since 1.19
+ */
+ public function __toString() {
+ return (string)$this->mConn;
+ }
+
+ public function __destruct() {
+ if ( count( $this->mTrxIdleCallbacks ) ) { // sanity
+ trigger_error( "Transaction idle callbacks still pending." );
+ }
+ }
}
diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php
index 836d7814..a53a6747 100644
--- a/includes/db/DatabaseError.php
+++ b/includes/db/DatabaseError.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * This file contains database error 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
+ * 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 error base class
@@ -189,7 +210,7 @@ class DBConnectionError extends DBError {
* @return string
*/
function searchForm() {
- global $wgSitename, $wgServer, $wgRequest;
+ global $wgSitename, $wgCanonicalServer, $wgRequest;
$usegoogle = htmlspecialchars( $this->msg( 'dberr-usegoogle', 'You can try searching via Google in the meantime.' ) );
$outofdate = htmlspecialchars( $this->msg( 'dberr-outofdate', 'Note that their indexes of our content may be out of date.' ) );
@@ -197,7 +218,7 @@ class DBConnectionError extends DBError {
$search = htmlspecialchars( $wgRequest->getVal( 'search' ) );
- $server = htmlspecialchars( $wgServer );
+ $server = htmlspecialchars( $wgCanonicalServer );
$sitename = htmlspecialchars( $wgSitename );
$trygoogle = <<<EOT
@@ -297,7 +318,7 @@ class DBQueryError extends DBError {
$fname = $this->fname;
$error = $this->error;
}
- return wfMsg( $msg, $sql, $fname, $this->errno, $error );
+ return wfMessage( $msg )->rawParams( $sql, $fname, $this->errno, $error )->text();
} else {
return parent::getContentMessage( $html );
}
diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php
index fed3b12e..f1f6dfca 100644
--- a/includes/db/DatabaseIbm_db2.php
+++ b/includes/db/DatabaseIbm_db2.php
@@ -2,7 +2,22 @@
/**
* This is the IBM DB2 database abstraction layer.
* See maintenance/ibm_db2/README for development notes
- * and other specific information
+ * 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
@@ -122,7 +137,7 @@ class IBM_DB2Result{
/**
* Construct and initialize a wrapper for DB2 query results
- * @param $db Database
+ * @param $db DatabaseBase
* @param $result Object
* @param $num_rows Integer
* @param $sql String
@@ -130,21 +145,21 @@ class IBM_DB2Result{
*/
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++ ) {
@@ -155,11 +170,11 @@ class IBM_DB2Result{
else {
return false;
}
-
+
$this->columns[$i] = strtolower( $name );
}
}
-
+
$this->sql = $sql;
}
@@ -187,14 +202,14 @@ class IBM_DB2Result{
* @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 )
+ 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;
@@ -210,9 +225,9 @@ class IBM_DB2Result{
* @throws DBUnexpectedError
*/
public function fetchRow(){
- if ( $this->result
- && $this->num_rows > 0
- && $this->current_pos >= 0
+ if ( $this->result
+ && $this->num_rows > 0
+ && $this->current_pos >= 0
&& $this->current_pos < $this->num_rows )
{
if ( $this->loadedLines <= $this->current_pos ) {
@@ -227,7 +242,7 @@ class IBM_DB2Result{
if ( $this->loadedLines > $this->current_pos ){
return $this->resultSet[$this->current_pos++];
}
-
+
}
return false;
}
@@ -313,6 +328,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Returns true if this database supports (and uses) cascading deletes
+ * @return bool
*/
function cascadingDeletes() {
return true;
@@ -321,6 +337,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Returns true if this database supports (and uses) triggers (e.g. on the
* page table)
+ * @return bool
*/
function cleanupTriggers() {
return true;
@@ -330,6 +347,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* 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;
@@ -337,13 +355,15 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* 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;
@@ -353,6 +373,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* 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;
@@ -361,6 +382,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* 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;
@@ -368,6 +390,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Returns true if this database can use functional indexes
+ * @return bool
*/
function functionalIndexes() {
return true;
@@ -375,6 +398,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Returns a unique string representing the wiki on the server
+ * @return string
*/
public function getWikiID() {
if( $this->mSchema ) {
@@ -392,7 +416,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
return 'ibm_db2';
}
- /**
+ /**
* Returns the database connection object
* @return Object
*/
@@ -472,7 +496,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* @param $user String
* @param $password String
* @param $dbName String: database name
- * @return a fresh connection
+ * @return DatabaseBase a fresh connection
*/
public function open( $server, $user, $password, $dbName ) {
wfProfileIn( __METHOD__ );
@@ -546,32 +570,26 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Closes a database connection, if it is open
* Returns success, true if already closed
+ * @return bool
*/
- public function close() {
- $this->mOpened = false;
- if ( $this->mConn ) {
- if ( $this->trxLevel() > 0 ) {
- $this->commit();
- }
- return db2_close( $this->mConn );
- } else {
- return true;
- }
+ 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();
+ //$this->rollback( __METHOD__ );
return $connerr;
}
$stmterr = db2_stmt_errormsg();
if ( $stmterr ) {
- //$this->rollback();
+ //$this->rollback( __METHOD__ );
return $stmterr;
}
@@ -667,7 +685,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* Fields can be retrieved with $row->fieldname, with fields acting like
* member variables.
*
- * @param $res SQL result object as returned from Database::query(), etc.
+ * @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
*/
@@ -689,8 +707,8 @@ class DatabaseIbm_db2 extends DatabaseBase {
* Fetch the next row from the given result object, in associative array
* form. Fields are retrieved with $row['fieldname'].
*
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return DB2 row object
+ * @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 ) {
@@ -715,7 +733,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* Doesn't escape numbers
*
* @param $s String: string to escape
- * @return escaped string
+ * @return string escaped string
*/
public function addQuotes( $s ) {
//$this->installPrint( "DB2::addQuotes( $s )\n" );
@@ -758,7 +776,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Alias for addQuotes()
* @param $s String: string to escape
- * @return escaped string
+ * @return string escaped string
*/
public function strencode( $s ) {
// Bloody useless function
@@ -780,16 +798,16 @@ class DatabaseIbm_db2 extends DatabaseBase {
protected function applySchema() {
if ( !( $this->mSchemaSet ) ) {
$this->mSchemaSet = true;
- $this->begin();
+ $this->begin( __METHOD__ );
$this->doQuery( "SET SCHEMA = $this->mSchema" );
- $this->commit();
+ $this->commit( __METHOD__ );
}
}
/**
* Start a transaction (mandatory)
*/
- public function begin( $fname = 'DatabaseIbm_db2::begin' ) {
+ protected function doBegin( $fname = 'DatabaseIbm_db2::begin' ) {
// BEGIN is implicit for DB2
// However, it requires that AutoCommit be off.
@@ -805,7 +823,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* End a transaction
* Must have a preceding begin()
*/
- public function commit( $fname = 'DatabaseIbm_db2::commit' ) {
+ protected function doCommit( $fname = 'DatabaseIbm_db2::commit' ) {
db2_commit( $this->mConn );
// Some MediaWiki code is still transaction-less (?).
@@ -819,7 +837,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Cancel a transaction
*/
- public function rollback( $fname = 'DatabaseIbm_db2::rollback' ) {
+ protected function doRollback( $fname = 'DatabaseIbm_db2::rollback' ) {
db2_rollback( $this->mConn );
// turn auto-commit back on
// not sure if this is appropriate
@@ -836,6 +854,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* 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 ) ) {
@@ -873,6 +892,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* @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 ) ) {
@@ -904,7 +924,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* Generates a timestamp in an insertable format
*
- * @param $ts timestamp
+ * @param $ts string timestamp
* @return String: timestamp value
*/
public function timestamp( $ts = 0 ) {
@@ -915,7 +935,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* 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 next value in that sequence
+ * @return int next value in that sequence
*/
public function nextSequenceValue( $seqName ) {
// Not using sequences in the primary schema to allow for easier migration
@@ -934,7 +954,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
/**
* This must be called after nextSequenceVal
- * @return Last sequence value used as a primary key
+ * @return int Last sequence value used as a primary key
*/
public function insertId() {
return $this->mInsertId;
@@ -1003,7 +1023,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
$res = true;
// If we are not in a transaction, we need to be for savepoint trickery
if ( !$this->mTrxLevel ) {
- $this->begin();
+ $this->begin( __METHOD__ );
}
$sql = "INSERT INTO $table ( " . implode( ',', $keys ) . ' ) VALUES ';
@@ -1018,7 +1038,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
$stmt = $this->prepare( $sql );
// start a transaction/enter transaction mode
- $this->begin();
+ $this->begin( __METHOD__ );
if ( !$ignore ) {
//$first = true;
@@ -1071,7 +1091,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
$this->mAffectedRows = $numrowsinserted;
}
// commit either way
- $this->commit();
+ $this->commit( __METHOD__ );
$this->freePrepared( $stmt );
return $res;
@@ -1121,11 +1141,11 @@ class DatabaseIbm_db2 extends DatabaseBase {
* UPDATE wrapper, takes a condition array and a SET array
*
* @param $table String: The table to UPDATE
- * @param $values An array of values to SET
- * @param $conds An array of conditions ( WHERE ). Use '*' to update all rows.
+ * @param $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 An array of UPDATE options, can be one or
+ * @param $options array An array of UPDATE options, can be one or
* more of IGNORE, LOW_PRIORITY
* @return Boolean
*/
@@ -1153,6 +1173,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* DELETE query wrapper
*
* Use $conds == "*" to delete all rows
+ * @return bool|\ResultWrapper
*/
public function delete( $table, $conds, $fname = 'DatabaseIbm_db2::delete' ) {
if ( !$conds ) {
@@ -1206,7 +1227,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
* Moves the row pointer of the result set
* @param $res Object: result set
* @param $row Integer: row number
- * @return success or failure
+ * @return bool success or failure
*/
public function dataSeek( $res, $row ) {
if ( $res instanceof ResultWrapper ) {
@@ -1279,11 +1300,11 @@ class DatabaseIbm_db2 extends DatabaseBase {
* @param $conds Array or string, condition(s) for WHERE
* @param $fname String: calling function name (use __METHOD__)
* for logs/profiling
- * @param $options Associative array of options
+ * @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 Associative array of table join conditions (optional)
+ * @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
@@ -1320,10 +1341,10 @@ class DatabaseIbm_db2 extends DatabaseBase {
$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 ) );
}
@@ -1333,7 +1354,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
*
* @private
*
- * @param $options Associative array of options to be turned into
+ * @param $options array Associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return Array
*/
@@ -1412,7 +1433,7 @@ class DatabaseIbm_db2 extends DatabaseBase {
// db2_ping() doesn't exist
// Emulate
$this->close();
- $this->mConn = $this->openUncataloged( $this->mDBName, $this->mUser,
+ $this->openUncataloged( $this->mDBName, $this->mUser,
$this->mPassword, $this->mServer, $this->mPort );
return false;
@@ -1420,14 +1441,6 @@ class DatabaseIbm_db2 extends DatabaseBase {
######################################
# Unimplemented and not applicable
######################################
- /**
- * Not implemented
- * @return string $sql
- */
- public function limitResultForUpdate( $sql, $num ) {
- $this->installPrint( 'Not implemented for DB2: limitResultForUpdate()' );
- return $sql;
- }
/**
* Only useful with fake prepare like in base Database class
@@ -1502,7 +1515,7 @@ SQL;
* Verifies that an index was created as unique
* @param $table String: table name
* @param $index String: index name
- * @param $fname function name for profiling
+ * @param $fname string function name for profiling
* @return Bool
*/
public function indexUnique ( $table, $index,
@@ -1636,25 +1649,6 @@ SQL;
}
/**
- * Prepare & execute an SQL statement, quoting and inserting arguments
- * in the appropriate places.
- * @param $query String
- * @param $args ...
- */
- public function safeQuery( $query, $args = null ) {
- // copied verbatim from Database.php
- $prepared = $this->prepare( $query, 'DB2::safeQuery' );
- if( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
- }
- $retval = $this->execute( $prepared, $args );
- $this->freePrepared( $prepared );
- return $retval;
- }
-
- /**
* For faking prepared SQL statements on DBs that don't support
* it directly.
* @param $preparedQuery String: a 'preparable' SQL statement
@@ -1674,6 +1668,7 @@ SQL;
/**
* Switches module between regular and install modes
+ * @return string
*/
public function setMode( $mode ) {
$old = $this->mMode;
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
index 38be4cbb..914ab408 100644
--- a/includes/db/DatabaseMssql.php
+++ b/includes/db/DatabaseMssql.php
@@ -2,6 +2,21 @@
/**
* This is the MS SQL Server Native 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
* @author Joel Penner <a-joelpe at microsoft dot com>
@@ -46,6 +61,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Usually aborts on failure
+ * @return bool|DatabaseBase|null
*/
function open( $server, $user, $password, $dbName ) {
# Test for driver support, to avoid suppressed fatal error
@@ -107,14 +123,10 @@ class DatabaseMssql extends DatabaseBase {
/**
* Closes a database connection, if it is open
* Returns success, true if already closed
+ * @return bool
*/
- function close() {
- $this->mOpened = false;
- if ( $this->mConn ) {
- return sqlsrv_close( $this->mConn );
- } else {
- return true;
- }
+ protected function closeConnection() {
+ return sqlsrv_close( $this->mConn );
}
protected function doQuery( $sql ) {
@@ -226,6 +238,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* This must be called after nextSequenceVal
+ * @return null
*/
function insertId() {
return $this->mInsertId;
@@ -310,6 +323,7 @@ class DatabaseMssql extends DatabaseBase {
* This is not necessarily an accurate estimate, so use sparingly
* Returns -1 if count cannot be found
* Takes same arguments as Database::select()
+ * @return int
*/
function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
$options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
@@ -326,6 +340,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Returns information about an index
* If errors are explicitly ignored, returns NULL on failure
+ * @return array|bool|null
*/
function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
# This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
@@ -365,6 +380,7 @@ class DatabaseMssql extends DatabaseBase {
*
* Usually aborts on failure
* If errors are explicitly ignored, returns success
+ * @return bool
*/
function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
# No rows to insert, easy just return now
@@ -494,6 +510,7 @@ 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
*/
function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
$insertOptions = array(), $selectOptions = array() ) {
@@ -511,6 +528,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Return the next in a sequence, save the value for retrieval via insertId()
+ * @return
*/
function nextSequenceValue( $seqName ) {
if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
@@ -527,6 +545,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
+ * @return
*/
function currentSequenceValue( $seqName ) {
$ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
@@ -559,6 +578,7 @@ class DatabaseMssql extends DatabaseBase {
* $sql string SQL query we will append the limit too
* $limit integer the SQL limit
* $offset integer the SQL offset (default false)
+ * @return mixed|string
*/
function limitResult( $sql, $limit, $offset = false ) {
if ( $offset === false || $offset == 0 ) {
@@ -600,14 +620,6 @@ class DatabaseMssql extends DatabaseBase {
return $sql;
}
- // MSSQL does support this, but documentation is too thin to make a generalized
- // function for this. Apparently UPDATE TOP (N) works, but the sort order
- // may not be what we're expecting so the top n results may be a random selection.
- // TODO: Implement properly.
- function limitResultForUpdate( $sql, $num ) {
- return $sql;
- }
-
function timestamp( $ts = 0 ) {
return wfTimestamp( TS_ISO_8601, $ts );
}
@@ -647,6 +659,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Query whether a given column exists in the mediawiki schema
+ * @return bool
*/
function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
$table = $this->tableName( $table );
@@ -681,7 +694,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Begin a transaction, committing any previously open transaction
*/
- function begin( $fname = 'DatabaseMssql::begin' ) {
+ protected function doBegin( $fname = 'DatabaseMssql::begin' ) {
sqlsrv_begin_transaction( $this->mConn );
$this->mTrxLevel = 1;
}
@@ -689,7 +702,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* End a transaction
*/
- function commit( $fname = 'DatabaseMssql::commit' ) {
+ protected function doCommit( $fname = 'DatabaseMssql::commit' ) {
sqlsrv_commit( $this->mConn );
$this->mTrxLevel = 0;
}
@@ -698,7 +711,7 @@ class DatabaseMssql extends DatabaseBase {
* Rollback a transaction.
* No-op on non-transactional databases.
*/
- function rollback( $fname = 'DatabaseMssql::rollback' ) {
+ protected function doRollback( $fname = 'DatabaseMssql::rollback' ) {
sqlsrv_rollback( $this->mConn );
$this->mTrxLevel = 0;
}
@@ -707,6 +720,7 @@ 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
+ * @return string
*/
private function escapeIdentifier( $identifier ) {
if ( strlen( $identifier ) == 0 ) {
@@ -795,6 +809,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* @private
+ * @return string
*/
function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
$ret = array();
@@ -893,6 +908,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Get the type of the DBMS, as it appears in $wgDBtype.
+ * @return string
*/
function getType(){
return 'mssql';
@@ -909,6 +925,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Since MSSQL doesn't recognize the infinity keyword, set date manually.
* @todo Remove magic date
+ * @return string
*/
public function getInfinity() {
return '3000-01-31 00:00:00.000';
diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php
index c179b724..7f389da9 100644
--- a/includes/db/DatabaseMysql.php
+++ b/includes/db/DatabaseMysql.php
@@ -2,6 +2,21 @@
/**
* 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
*/
@@ -44,7 +59,7 @@ class DatabaseMysql extends DatabaseBase {
* @throws DBConnectionError
*/
function open( $server, $user, $password, $dbName ) {
- global $wgAllDBsAreLocalhost;
+ global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode;
wfProfileIn( __METHOD__ );
# Load mysql.so if we don't have it
@@ -68,7 +83,15 @@ class DatabaseMysql extends DatabaseBase {
$this->mPassword = $password;
$this->mDBname = $dbName;
- wfProfileIn("dbconnect-$server");
+ $connFlags = 0;
+ if ( $this->mFlags & DBO_SSL ) {
+ $connFlags |= MYSQL_CLIENT_SSL;
+ }
+ if ( $this->mFlags & DBO_COMPRESS ) {
+ $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
@@ -85,85 +108,71 @@ class DatabaseMysql extends DatabaseBase {
usleep( 1000 );
}
if ( $this->mFlags & DBO_PERSISTENT ) {
- $this->mConn = mysql_pconnect( $realServer, $user, $password );
+ $this->mConn = mysql_pconnect( $realServer, $user, $password, $connFlags );
} else {
# Create a new connection...
- $this->mConn = mysql_connect( $realServer, $user, $password, true );
+ $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");
#}
}
- $phpError = $this->restoreErrorHandler();
+ $error = $this->restoreErrorHandler();
+
+ wfProfileOut( "dbconnect-$server" );
+
# Always log connection errors
if ( !$this->mConn ) {
- $error = $this->lastError();
if ( !$error ) {
- $error = $phpError;
+ $error = $this->lastError();
}
wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
- }
+ wfDebug( "DB connection error\n" .
+ "Server: $server, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
- wfProfileOut("dbconnect-$server");
+ wfProfileOut( __METHOD__ );
+ $this->reportConnectionError( $error );
+ }
- if ( $dbName != '' && $this->mConn !== false ) {
+ if ( $dbName != '' ) {
wfSuppressWarnings();
$success = mysql_select_db( $dbName, $this->mConn );
wfRestoreWarnings();
if ( !$success ) {
- $error = "Error selecting database $dbName on server {$this->mServer} " .
- "from client host " . wfHostname() . "\n";
- wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
- wfDebug( $error );
- }
- } else {
- # Delay USE query
- $success = (bool)$this->mConn;
- }
+ wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" );
+ wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host " . wfHostname() . "\n" );
- if ( $success ) {
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- global $wgDBmysql5;
- if( $wgDBmysql5 ) {
- $this->query( 'SET NAMES utf8', __METHOD__ );
- } else {
- $this->query( 'SET NAMES binary', __METHOD__ );
- }
- // Set SQL mode, default is turning them all off, can be overridden or skipped with null
- global $wgSQLMode;
- if ( is_string( $wgSQLMode ) ) {
- $mode = $this->addQuotes( $wgSQLMode );
- $this->query( "SET sql_mode = $mode", __METHOD__ );
+ wfProfileOut( __METHOD__ );
+ $this->reportConnectionError( "Error selecting database $dbName" );
}
+ }
- // Turn off strict mode if it is on
+ // 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->reportConnectionError( $phpError );
+ $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 = $success;
+ $this->mOpened = true;
wfProfileOut( __METHOD__ );
- return $success;
+ return true;
}
/**
* @return bool
*/
- function close() {
- $this->mOpened = false;
- if ( $this->mConn ) {
- if ( $this->trxLevel() ) {
- $this->commit();
- }
- return mysql_close( $this->mConn );
- } else {
- return true;
- }
+ protected function closeConnection() {
+ return mysql_close( $this->mConn );
}
/**
@@ -194,7 +203,13 @@ class DatabaseMysql extends DatabaseBase {
wfSuppressWarnings();
$row = mysql_fetch_object( $res );
wfRestoreWarnings();
- if( $this->lastErrno() ) {
+
+ $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;
@@ -212,7 +227,13 @@ class DatabaseMysql extends DatabaseBase {
wfSuppressWarnings();
$row = mysql_fetch_array( $res );
wfRestoreWarnings();
- if ( $this->lastErrno() ) {
+
+ $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;
@@ -385,7 +406,7 @@ class DatabaseMysql extends DatabaseBase {
* @param $table string
* @param $index string
* @param $fname string
- * @return false|array
+ * @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.
@@ -558,7 +579,7 @@ class DatabaseMysql extends DatabaseBase {
# Commit any open transactions
if ( $this->mTrxLevel ) {
- $this->commit();
+ $this->commit( __METHOD__ );
}
if ( !is_null( $this->mFakeSlaveLag ) ) {
@@ -585,7 +606,7 @@ class DatabaseMysql extends DatabaseBase {
/**
* Get the position of the master from SHOW SLAVE STATUS
*
- * @return MySQLMasterPos|false
+ * @return MySQLMasterPos|bool
*/
function getSlavePos() {
if ( !is_null( $this->mFakeSlaveLag ) ) {
@@ -606,7 +627,7 @@ class DatabaseMysql extends DatabaseBase {
/**
* Get the position of the master from SHOW MASTER STATUS
*
- * @return MySQLMasterPos|false
+ * @return MySQLMasterPos|bool
*/
function getMasterPos() {
if ( $this->mFakeMaster ) {
@@ -653,13 +674,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @return bool
- */
- function standardSelectDistinct() {
- return false;
- }
-
- /**
* @param $options array
*/
public function setSessionOptions( array $options ) {
@@ -680,6 +694,21 @@ class DatabaseMysql extends DatabaseBase {
}
/**
+ * 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
@@ -708,7 +737,7 @@ class DatabaseMysql extends DatabaseBase {
$lockName = $this->addQuotes( $lockName );
$result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
$row = $this->fetchObject( $result );
- return $row->lockstatus;
+ return ( $row->lockstatus == 1 );
}
/**
@@ -860,7 +889,7 @@ class DatabaseMysql extends DatabaseBase {
/**
* List all tables on the database
*
- * @param $prefix Only show tables with this prefix, e.g. mw_
+ * @param $prefix string Only show tables with this prefix, e.g. mw_
* @param $fname String: calling function name
* @return array
*/
diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php
index 855fc831..7d8884fb 100644
--- a/includes/db/DatabaseOracle.php
+++ b/includes/db/DatabaseOracle.php
@@ -2,6 +2,21 @@
/**
* This is the Oracle 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
*/
@@ -226,6 +241,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* Usually aborts on failure
+ * @return DatabaseBase|null
*/
function open( $server, $user, $password, $dbName ) {
if ( !function_exists( 'oci_connect' ) ) {
@@ -285,17 +301,10 @@ class DatabaseOracle extends DatabaseBase {
/**
* Closes a database connection, if it is open
* Returns success, true if already closed
+ * @return bool
*/
- function close() {
- $this->mOpened = false;
- if ( $this->mConn ) {
- if ( $this->mTrxLevel ) {
- $this->commit();
- }
- return oci_close( $this->mConn );
- } else {
- return true;
- }
+ protected function closeConnection() {
+ return oci_close( $this->mConn );
}
function execFlags() {
@@ -401,6 +410,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* This must be called after nextSequenceVal
+ * @return null
*/
function insertId() {
return $this->mInsertId;
@@ -439,6 +449,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* Returns information about an index
* If errors are explicitly ignored, returns NULL on failure
+ * @return bool
*/
function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
return false;
@@ -679,6 +690,7 @@ class DatabaseOracle extends DatabaseBase {
}
/**
* Return the next in a sequence, save the value for retrieval via insertId()
+ * @return null
*/
function nextSequenceValue( $seqName ) {
$res = $this->query( "SELECT $seqName.nextval FROM dual" );
@@ -689,6 +701,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* Return sequence_name if table has a sequence
+ * @return bool
*/
private function getSequenceData( $table ) {
if ( $this->sequenceData == null ) {
@@ -797,7 +810,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* Return aggregated value function call
*/
- function aggregateValue ( $valuedata, $valuename = 'value' ) {
+ public function aggregateValue( $valuedata, $valuename = 'value' ) {
return $valuedata;
}
@@ -836,6 +849,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* Query whether a given index exists
+ * @return bool
*/
function indexExists( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
$table = $this->tableName( $table );
@@ -855,6 +869,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
*/
function tableExists( $table, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
@@ -940,12 +955,12 @@ class DatabaseOracle extends DatabaseBase {
return $this->fieldInfoMulti ($table, $field);
}
- function begin( $fname = 'DatabaseOracle::begin' ) {
+ protected function doBegin( $fname = 'DatabaseOracle::begin' ) {
$this->mTrxLevel = 1;
$this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' );
}
- function commit( $fname = 'DatabaseOracle::commit' ) {
+ protected function doCommit( $fname = 'DatabaseOracle::commit' ) {
if ( $this->mTrxLevel ) {
$ret = oci_commit( $this->mConn );
if ( !$ret ) {
@@ -956,7 +971,7 @@ class DatabaseOracle extends DatabaseBase {
}
}
- function rollback( $fname = 'DatabaseOracle::rollback' ) {
+ protected function doRollback( $fname = 'DatabaseOracle::rollback' ) {
if ( $this->mTrxLevel ) {
oci_rollback( $this->mConn );
$this->mTrxLevel = 0;
@@ -964,11 +979,6 @@ class DatabaseOracle extends DatabaseBase {
}
}
- /* Not even sure why this is used in the main codebase... */
- function limitResultForUpdate( $sql, $num ) {
- return $sql;
- }
-
/* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */
function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
$fname = 'DatabaseOracle::sourceStream', $inputCallback = false ) {
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
index 98cf3c75..457bf384 100644
--- a/includes/db/DatabasePostgres.php
+++ b/includes/db/DatabasePostgres.php
@@ -2,12 +2,28 @@
/**
* This is the Postgres 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
*/
class PostgresField implements Field {
- private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname;
+ private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
+ $has_default, $default;
/**
* @param $db DatabaseBase
@@ -16,11 +32,11 @@ class PostgresField implements Field {
* @return null|PostgresField
*/
static function fromText( $db, $table, $field ) {
- global $wgDBmwschema;
-
$q = <<<SQL
SELECT
- attnotnull, attlen, COALESCE(conname, '') AS conname,
+ attnotnull, attlen, conname AS conname,
+ atthasdef,
+ adsrc,
COALESCE(condeferred, 'f') AS deferred,
COALESCE(condeferrable, 'f') AS deferrable,
CASE WHEN typname = 'int2' THEN 'smallint'
@@ -33,6 +49,7 @@ JOIN pg_namespace n ON (n.oid = c.relnamespace)
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
+LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
WHERE relkind = 'r'
AND nspname=%s
AND relname=%s
@@ -42,7 +59,7 @@ SQL;
$table = $db->tableName( $table, 'raw' );
$res = $db->query(
sprintf( $q,
- $db->addQuotes( $wgDBmwschema ),
+ $db->addQuotes( $db->getCoreSchema() ),
$db->addQuotes( $table ),
$db->addQuotes( $field )
)
@@ -60,6 +77,8 @@ SQL;
$n->deferrable = ( $row->deferrable == 't' );
$n->deferred = ( $row->deferred == 't' );
$n->conname = $row->conname;
+ $n->has_default = ( $row->atthasdef === 't' );
+ $n->default = $row->adsrc;
return $n;
}
@@ -94,7 +113,169 @@ SQL;
function conname() {
return $this->conname;
}
+ /**
+ * @since 1.19
+ */
+ function defaultValue() {
+ if( $this->has_default ) {
+ return $this->default;
+ } else {
+ return false;
+ }
+ }
+
+}
+
+/**
+ * Used to debug transaction processing
+ * Only used if $wgDebugDBTransactions is true
+ *
+ * @since 1.19
+ * @ingroup Database
+ */
+class PostgresTransactionState {
+
+ static $WATCHED = array(
+ array(
+ "desc" => "%s: Connection state changed from %s -> %s\n",
+ "states" => array(
+ 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_INTRANS => "TRANS",
+ PGSQL_TRANSACTION_INERROR => "ERROR",
+ PGSQL_TRANSACTION_UNKNOWN => "UNKNOWN"
+ )
+ )
+ );
+
+ public function __construct( $conn ) {
+ $this->mConn = $conn;
+ $this->update();
+ $this->mCurrentState = $this->mNewState;
+ }
+
+ public function update() {
+ $this->mNewState = array(
+ pg_connection_status( $this->mConn ),
+ pg_transaction_status( $this->mConn )
+ );
+ }
+
+ public function check() {
+ global $wgDebugDBTransactions;
+ $this->update();
+ if ( $wgDebugDBTransactions ) {
+ if ( $this->mCurrentState !== $this->mNewState ) {
+ $old = reset( $this->mCurrentState );
+ $new = reset( $this->mNewState );
+ foreach ( self::$WATCHED as $watched ) {
+ if ($old !== $new) {
+ $this->log_changed($old, $new, $watched);
+ }
+ $old = next( $this->mCurrentState );
+ $new = next( $this->mNewState );
+
+ }
+ }
+ }
+ $this->mCurrentState = $this->mNewState;
+ }
+
+ protected function describe_changed( $status, $desc_table ) {
+ if( isset( $desc_table[$status] ) ) {
+ return $desc_table[$status];
+ } else {
+ return "STATUS " . $status;
+ }
+ }
+ protected function log_changed( $old, $new, $watched ) {
+ wfDebug(sprintf($watched["desc"],
+ $this->mConn,
+ $this->describe_changed( $old, $watched["states"] ),
+ $this->describe_changed( $new, $watched["states"] ))
+ );
+ }
+}
+
+/**
+ * Manage savepoints within a transaction
+ * @ingroup Database
+ * @since 1.19
+ */
+class SavepointPostgres {
+ /**
+ * Establish a savepoint within a transaction
+ */
+ protected $dbw;
+ protected $id;
+ protected $didbegin;
+
+ public function __construct ($dbw, $id) {
+ $this->dbw = $dbw;
+ $this->id = $id;
+ $this->didbegin = false;
+ /* If we are not in a transaction, we need to be for savepoint trickery */
+ if ( !$dbw->trxLevel() ) {
+ $dbw->begin( "FOR SAVEPOINT" );
+ $this->didbegin = true;
+ }
+ }
+
+ public function __destruct() {
+ if ( $this->didbegin ) {
+ $this->dbw->rollback();
+ }
+ }
+
+ public function commit() {
+ if ( $this->didbegin ) {
+ $this->dbw->commit();
+ }
+ }
+
+ protected function query( $keyword, $msg_ok, $msg_failed ) {
+ global $wgDebugDBTransactions;
+ if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
+ if ( $wgDebugDBTransactions ) {
+ wfDebug( sprintf ($msg_ok, $this->id ) );
+ }
+ } else {
+ wfDebug( sprintf ($msg_failed, $this->id ) );
+ }
+ }
+
+ public function savepoint() {
+ $this->query("SAVEPOINT",
+ "Transaction state: savepoint \"%s\" established.\n",
+ "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
+ );
+ }
+
+ public function 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",
+ "Transaction state: savepoint \"%s\" rolled back.\n",
+ "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
+ );
+ }
+
+ public function __toString() {
+ return (string)$this->id;
+ }
}
/**
@@ -136,15 +317,15 @@ class DatabasePostgres extends DatabaseBase {
}
function hasConstraint( $name ) {
- global $wgDBmwschema;
$SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" .
- pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $wgDBmwschema ) ."'";
+ 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
+ * @return DatabaseBase|null
*/
function open( $server, $user, $password, $dbName ) {
# Test for Postgres support, to avoid suppressed fatal error
@@ -158,7 +339,6 @@ class DatabasePostgres extends DatabaseBase {
return;
}
- $this->close();
$this->mServer = $server;
$port = $wgDBport;
$this->mUser = $user;
@@ -176,10 +356,14 @@ class DatabasePostgres extends DatabaseBase {
if ( $port != false && $port != '' ) {
$connectVars['port'] = $port;
}
- $connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
+ if ( $this->mFlags & DBO_SSL ) {
+ $connectVars['sslmode'] = 1;
+ }
+ $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
+ $this->close();
$this->installErrorHandler();
- $this->mConn = pg_connect( $connectString );
+ $this->mConn = pg_connect( $this->connectString );
$phpError = $this->restoreErrorHandler();
if ( !$this->mConn ) {
@@ -190,6 +374,7 @@ class DatabasePostgres extends DatabaseBase {
}
$this->mOpened = true;
+ $this->mTransactionState = new PostgresTransactionState( $this->mConn );
global $wgCommandLineMode;
# If called from the command-line (e.g. importDump), only show errors
@@ -203,12 +388,7 @@ class DatabasePostgres extends DatabaseBase {
$this->query( "SET standard_conforming_strings = on", __METHOD__ );
global $wgDBmwschema;
- if ( $this->schemaExists( $wgDBmwschema ) ) {
- $safeschema = $this->addIdentifierQuotes( $wgDBmwschema );
- $this->doQuery( "SET search_path = $safeschema" );
- } else {
- $this->doQuery( "SET search_path = public" );
- }
+ $this->determineCoreSchema( $wgDBmwschema );
return $this->mConn;
}
@@ -237,25 +417,62 @@ class DatabasePostgres extends DatabaseBase {
/**
* Closes a database connection, if it is open
* Returns success, true if already closed
+ * @return bool
*/
- function close() {
- $this->mOpened = false;
- if ( $this->mConn ) {
- return pg_close( $this->mConn );
- } else {
- return true;
- }
+ protected function closeConnection() {
+ return pg_close( $this->mConn );
}
- protected function doQuery( $sql ) {
+ public function doQuery( $sql ) {
if ( function_exists( 'mb_convert_encoding' ) ) {
$sql = mb_convert_encoding( $sql, 'UTF-8' );
}
- $this->mLastResult = pg_query( $this->mConn, $sql );
- $this->mAffectedRows = null; // use pg_affected_rows(mLastResult)
+ $this->mTransactionState->check();
+ 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 );
+ $this->mTransactionState->check();
+ $this->mAffectedRows = null;
+ if ( pg_result_error( $this->mLastResult ) ) {
+ return false;
+ }
return $this->mLastResult;
}
+ protected function dumpError () {
+ $diags = array( PGSQL_DIAG_SEVERITY,
+ PGSQL_DIAG_SQLSTATE,
+ PGSQL_DIAG_MESSAGE_PRIMARY,
+ PGSQL_DIAG_MESSAGE_DETAIL,
+ PGSQL_DIAG_MESSAGE_HINT,
+ PGSQL_DIAG_STATEMENT_POSITION,
+ PGSQL_DIAG_INTERNAL_POSITION,
+ PGSQL_DIAG_INTERNAL_QUERY,
+ PGSQL_DIAG_CONTEXT,
+ PGSQL_DIAG_SOURCE_FILE,
+ 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 ) ) );
+ }
+ }
+
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ /* Transaction stays in the ERROR state until rolledback */
+ if ( $tempIgnore ) {
+ /* Check for constraint violation */
+ if ( $errno === '23505' ) {
+ parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
+ return;
+ }
+ }
+ /* Don't ignore serious errors */
+ $this->rollback( __METHOD__ );
+ parent::reportQueryError( $error, $errno, $sql, $fname, false );
+ }
+
+
function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) {
return $this->query( $sql, $fname, true );
}
@@ -331,6 +548,7 @@ class DatabasePostgres extends DatabaseBase {
/**
* This must be called after nextSequenceVal
+ * @return null
*/
function insertId() {
return $this->mInsertId;
@@ -345,13 +563,21 @@ class DatabasePostgres extends DatabaseBase {
function lastError() {
if ( $this->mConn ) {
- return pg_last_error();
+ if ( $this->mLastResult ) {
+ return pg_result_error( $this->mLastResult );
+ } else {
+ return pg_last_error();
+ }
} else {
return 'No database connection';
}
}
function lastErrno() {
- return pg_last_error() ? 1 : 0;
+ if ( $this->mLastResult ) {
+ return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
+ } else {
+ return false;
+ }
}
function affectedRows() {
@@ -371,6 +597,7 @@ class DatabasePostgres extends DatabaseBase {
* This is not necessarily an accurate estimate, so use sparingly
* Returns -1 if count cannot be found
* Takes same arguments as Database::select()
+ * @return int
*/
function estimateRowCount( $table, $vars = '*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) {
$options['EXPLAIN'] = true;
@@ -389,6 +616,7 @@ class DatabasePostgres extends DatabaseBase {
/**
* Returns information about an index
* If errors are explicitly ignored, returns NULL on failure
+ * @return bool|null
*/
function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) {
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
@@ -404,6 +632,68 @@ class DatabasePostgres extends DatabaseBase {
return false;
}
+ /**
+ * Returns is of attributes used in index
+ *
+ * @since 1.19
+ * @return Array
+ */
+ 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
+ */
+ $sql = <<<__INDEXATTR__
+
+ SELECT opcname,
+ attname,
+ i.indoption[s.g] as option,
+ pg_am.amname
+ FROM
+ (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
+ FROM
+ pg_index isub
+ JOIN pg_class cis
+ ON cis.oid=isub.indexrelid
+ JOIN pg_namespace ns
+ ON cis.relnamespace = ns.oid
+ WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
+ pg_attribute,
+ pg_opclass opcls,
+ pg_am,
+ pg_class ci
+ JOIN pg_index i
+ ON ci.oid=i.indexrelid
+ JOIN pg_class ct
+ ON ct.oid = i.indrelid
+ JOIN pg_namespace n
+ ON ci.relnamespace = n.oid
+ WHERE
+ ci.relname='$index' AND n.nspname='$schema'
+ AND attrelid = ct.oid
+ AND i.indkey[s.g] = attnum
+ AND i.indclass[s.g] = opcls.oid
+ AND pg_am.oid = opcls.opcmethod
+__INDEXATTR__;
+ $res = $this->query($sql, __METHOD__);
+ $a = array();
+ if ( $res ) {
+ foreach ( $res as $row ) {
+ $a[] = array(
+ $row->attname,
+ $row->opcname,
+ $row->amname,
+ $row->option);
+ }
+ } else {
+ return null;
+ }
+ return $a;
+ }
+
+
function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) {
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
" AND indexdef LIKE 'CREATE UNIQUE%(" .
@@ -455,15 +745,9 @@ class DatabasePostgres extends DatabaseBase {
}
// If IGNORE is set, we use savepoints to emulate mysql's behavior
- $ignore = in_array( 'IGNORE', $options ) ? 'mw' : '';
-
- // If we are not in a transaction, we need to be for savepoint trickery
- $didbegin = 0;
- if ( $ignore ) {
- if ( !$this->mTrxLevel ) {
- $this->begin();
- $didbegin = 1;
- }
+ $savepoint = null;
+ if ( in_array( 'IGNORE', $options ) ) {
+ $savepoint = new SavepointPostgres( $this, 'mw' );
$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
@@ -473,7 +757,7 @@ class DatabasePostgres extends DatabaseBase {
$sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
if ( $multi ) {
- if ( $this->numeric_version >= 8.2 && !$ignore ) {
+ if ( $this->numeric_version >= 8.2 && !$savepoint ) {
$first = true;
foreach ( $args as $row ) {
if ( $first ) {
@@ -483,7 +767,7 @@ class DatabasePostgres extends DatabaseBase {
}
$sql .= '(' . $this->makeList( $row ) . ')';
}
- $res = (bool)$this->query( $sql, $fname, $ignore );
+ $res = (bool)$this->query( $sql, $fname, $savepoint );
} else {
$res = true;
$origsql = $sql;
@@ -491,18 +775,18 @@ class DatabasePostgres extends DatabaseBase {
$tempsql = $origsql;
$tempsql .= '(' . $this->makeList( $row ) . ')';
- if ( $ignore ) {
- pg_query( $this->mConn, "SAVEPOINT $ignore" );
+ if ( $savepoint ) {
+ $savepoint->savepoint();
}
- $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
+ $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
- if ( $ignore ) {
+ if ( $savepoint ) {
$bar = pg_last_error();
if ( $bar != false ) {
- pg_query( $this->mConn, "ROLLBACK TO $ignore" );
+ $savepoint->rollback();
} else {
- pg_query( $this->mConn, "RELEASE $ignore" );
+ $savepoint->release();
$numrowsinserted++;
}
}
@@ -516,27 +800,25 @@ class DatabasePostgres extends DatabaseBase {
}
} else {
// Not multi, just a lone insert
- if ( $ignore ) {
- pg_query($this->mConn, "SAVEPOINT $ignore");
+ if ( $savepoint ) {
+ $savepoint->savepoint();
}
$sql .= '(' . $this->makeList( $args ) . ')';
- $res = (bool)$this->query( $sql, $fname, $ignore );
- if ( $ignore ) {
+ $res = (bool)$this->query( $sql, $fname, $savepoint );
+ if ( $savepoint ) {
$bar = pg_last_error();
if ( $bar != false ) {
- pg_query( $this->mConn, "ROLLBACK TO $ignore" );
+ $savepoint->rollback();
} else {
- pg_query( $this->mConn, "RELEASE $ignore" );
+ $savepoint->release();
$numrowsinserted++;
}
}
}
- if ( $ignore ) {
+ if ( $savepoint ) {
$olde = error_reporting( $olde );
- if ( $didbegin ) {
- $this->commit();
- }
+ $savepoint->commit();
// Set the affected row count for the whole operation
$this->mAffectedRows = $numrowsinserted;
@@ -555,18 +837,29 @@ class DatabasePostgres extends DatabaseBase {
* $conds may be "*" to copy the whole table
* srcTable may be an array of tables.
* @todo FIXME: Implement this a little better (seperate select/insert)?
+ * @return bool
*/
function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect',
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
- // If IGNORE is set, we use savepoints to emulate mysql's behavior
- $ignore = in_array( 'IGNORE', $insertOptions ) ? 'mw' : '';
+ if( !is_array( $insertOptions ) ) {
+ $insertOptions = array( $insertOptions );
+ }
- if( is_array( $insertOptions ) ) {
- $insertOptions = implode( ' ', $insertOptions ); // FIXME: This is unused
+ /*
+ * If IGNORE is set, we use savepoints to emulate mysql's behavior
+ * Ignore LOW PRIORITY option, since it is MySQL-specific
+ */
+ $savepoint = null;
+ if ( in_array( 'IGNORE', $insertOptions ) ) {
+ $savepoint = new SavepointPostgres( $this, 'mw' );
+ $olde = error_reporting( 0 );
+ $numrowsinserted = 0;
+ $savepoint->savepoint();
}
+
if( !is_array( $selectOptions ) ) {
$selectOptions = array( $selectOptions );
}
@@ -577,18 +870,6 @@ class DatabasePostgres extends DatabaseBase {
$srcTable = $this->tableName( $srcTable );
}
- // If we are not in a transaction, we need to be for savepoint trickery
- $didbegin = 0;
- if ( $ignore ) {
- if( !$this->mTrxLevel ) {
- $this->begin();
- $didbegin = 1;
- }
- $olde = error_reporting( 0 );
- $numrowsinserted = 0;
- pg_query( $this->mConn, "SAVEPOINT $ignore");
- }
-
$sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
" SELECT $startOpts " . implode( ',', $varMap ) .
" FROM $srcTable $useIndex";
@@ -599,19 +880,17 @@ class DatabasePostgres extends DatabaseBase {
$sql .= " $tailOpts";
- $res = (bool)$this->query( $sql, $fname, $ignore );
- if( $ignore ) {
+ $res = (bool)$this->query( $sql, $fname, $savepoint );
+ if( $savepoint ) {
$bar = pg_last_error();
if( $bar != false ) {
- pg_query( $this->mConn, "ROLLBACK TO $ignore" );
+ $savepoint->rollback();
} else {
- pg_query( $this->mConn, "RELEASE $ignore" );
+ $savepoint->release();
$numrowsinserted++;
}
$olde = error_reporting( $olde );
- if( $didbegin ) {
- $this->commit();
- }
+ $savepoint->commit();
// Set the affected row count for the whole operation
$this->mAffectedRows = $numrowsinserted;
@@ -642,6 +921,7 @@ class DatabasePostgres extends DatabaseBase {
/**
* Return the next in a sequence, save the value for retrieval via insertId()
+ * @return null
*/
function nextSequenceValue( $seqName ) {
$safeseq = str_replace( "'", "''", $seqName );
@@ -653,6 +933,7 @@ class DatabasePostgres extends DatabaseBase {
/**
* Return the current value of a sequence. Assumes it has been nextval'ed in this session.
+ * @return
*/
function currentSequenceValue( $seqName ) {
$safeseq = str_replace( "'", "''", $seqName );
@@ -694,10 +975,8 @@ class DatabasePostgres extends DatabaseBase {
}
function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) {
- global $wgDBmwschema;
- $eschema = $this->addQuotes( $wgDBmwschema );
+ $eschema = $this->addQuotes( $this->getCoreSchema() );
$result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
-
$endArray = array();
foreach( $result as $table ) {
@@ -715,10 +994,54 @@ class DatabasePostgres extends DatabaseBase {
return wfTimestamp( TS_POSTGRES, $ts );
}
+ /*
+ * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
+ * to http://www.php.net/manual/en/ref.pgsql.php
+ *
+ * Parsing a postgres array can be a tricky problem, he's my
+ * take on this, it handles multi-dimensional arrays plus
+ * escaping using a nasty regexp to determine the limits of each
+ * data-item.
+ *
+ * This should really be handled by PHP PostgreSQL module
+ *
+ * @since 1.19
+ * @param $text string: postgreql array returned in a text form like {a,b}
+ * @param $output string
+ * @param $limit int
+ * @param $offset int
+ * @return string
+ */
+ function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
+ if( false === $limit ) {
+ $limit = strlen( $text )-1;
+ $output = array();
+ }
+ if( '{}' == $text ) {
+ return $output;
+ }
+ do {
+ if ( '{' != $text{$offset} ) {
+ preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
+ $text, $match, 0, $offset );
+ $offset += strlen( $match[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 );
+ }
+ } while ( $limit > $offset );
+ return $output;
+ }
+
/**
* Return aggregated value function call
*/
- function aggregateValue( $valuedata, $valuename = 'value' ) {
+ public function aggregateValue( $valuedata, $valuename = 'value' ) {
return $valuedata;
}
@@ -729,6 +1052,115 @@ class DatabasePostgres extends DatabaseBase {
return '[http://www.postgresql.org/ PostgreSQL]';
}
+
+ /**
+ * Return current schema (executes SELECT current_schema())
+ * Needs transaction
+ *
+ * @since 1.19
+ * @return string return default schema for the current session
+ */
+ function getCurrentSchema() {
+ $res = $this->query( "SELECT current_schema()", __METHOD__);
+ $row = $this->fetchRow( $res );
+ return $row[0];
+ }
+
+ /**
+ * Return list of schemas which are accessible without schema name
+ * This is list does not contain magic keywords like "$user"
+ * Needs transaction
+ *
+ * @seealso getSearchPath()
+ * @seealso setSearchPath()
+ * @since 1.19
+ * @return array list of actual schemas for the current sesson
+ */
+ function getSchemas() {
+ $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 search patch for schemas
+ * This is different from getSchemas() since it contain magic keywords
+ * (like "$user").
+ * Needs transaction
+ *
+ * @since 1.19
+ * @return array how to search for table names schemas for the current user
+ */
+ function getSearchPath() {
+ $res = $this->query( "SHOW search_path", __METHOD__);
+ $row = $this->fetchRow( $res );
+ /* PostgreSQL returns SHOW values as strings */
+ return explode(",", $row[0]);
+ }
+
+ /**
+ * Update search_path, values should already be sanitized
+ * Values may contain magic keywords like "$user"
+ * @since 1.19
+ *
+ * @param $search_path array list of schemas to be searched by default
+ */
+ function setSearchPath( $search_path ) {
+ $this->query( "SET search_path = " . implode(", ", $search_path) );
+ }
+
+ /**
+ * Determine default schema for MediaWiki core
+ * Adjust this session schema search path if desired schema exists
+ * and is not alread there.
+ *
+ * We need to have name of the core schema stored to be able
+ * to query database metadata.
+ *
+ * This will be also called by the installer after the schema is created
+ *
+ * @since 1.19
+ * @param $desired_schema string
+ */
+ function determineCoreSchema( $desired_schema ) {
+ $this->begin( __METHOD__ );
+ 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");
+ } else {
+ /**
+ * Prepend our schema (e.g. 'mediawiki') in front
+ * of the search path
+ * Fixes bug 15816
+ */
+ $search_path = $this->getSearchPath();
+ array_unshift( $search_path,
+ $this->addIdentifierQuotes( $desired_schema ));
+ $this->setSearchPath( $search_path );
+ $this->mCoreSchema = $desired_schema;
+ 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");
+ }
+ /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
+ $this->commit( __METHOD__ );
+ }
+
+ /**
+ * Return schema name fore core MediaWiki tables
+ *
+ * @since 1.19
+ * @return string core schema name
+ */
+ function getCoreSchema() {
+ return $this->mCoreSchema;
+ }
+
/**
* @return string Version information from the database
*/
@@ -752,14 +1184,14 @@ class DatabasePostgres extends DatabaseBase {
/**
* Query whether a given relation exists (in the given schema, or the
* default mw one if not given)
+ * @return bool
*/
function relationExists( $table, $types, $schema = false ) {
- global $wgDBmwschema;
if ( !is_array( $types ) ) {
$types = array( $types );
}
if ( !$schema ) {
- $schema = $wgDBmwschema;
+ $schema = $this->getCoreSchema();
}
$table = $this->realTableName( $table, 'raw' );
$etable = $this->addQuotes( $table );
@@ -775,6 +1207,7 @@ class DatabasePostgres extends DatabaseBase {
/**
* For backward compatibility, this function checks both tables and
* views.
+ * @return bool
*/
function tableExists( $table, $fname = __METHOD__, $schema = false ) {
return $this->relationExists( $table, array( 'r', 'v' ), $schema );
@@ -785,8 +1218,6 @@ class DatabasePostgres extends DatabaseBase {
}
function triggerExists( $table, $trigger ) {
- global $wgDBmwschema;
-
$q = <<<SQL
SELECT 1 FROM pg_class, pg_namespace, pg_trigger
WHERE relnamespace=pg_namespace.oid AND relkind='r'
@@ -796,7 +1227,7 @@ SQL;
$res = $this->query(
sprintf(
$q,
- $this->addQuotes( $wgDBmwschema ),
+ $this->addQuotes( $this->getCoreSchema() ),
$this->addQuotes( $table ),
$this->addQuotes( $trigger )
)
@@ -809,22 +1240,20 @@ SQL;
}
function ruleExists( $table, $rule ) {
- global $wgDBmwschema;
$exists = $this->selectField( 'pg_rules', 'rulename',
array(
'rulename' => $rule,
'tablename' => $table,
- 'schemaname' => $wgDBmwschema
+ 'schemaname' => $this->getCoreSchema()
)
);
return $exists === $rule;
}
function constraintExists( $table, $constraint ) {
- global $wgDBmwschema;
$SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints ".
"WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
- $this->addQuotes( $wgDBmwschema ),
+ $this->addQuotes( $this->getCoreSchema() ),
$this->addQuotes( $table ),
$this->addQuotes( $constraint )
);
@@ -838,6 +1267,7 @@ SQL;
/**
* Query whether a given schema exists. Returns true if it does, false if it doesn't.
+ * @return bool
*/
function schemaExists( $schema ) {
$exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
@@ -847,6 +1277,7 @@ SQL;
/**
* Returns true if a given role (i.e. user) exists, false otherwise.
+ * @return bool
*/
function roleExists( $roleName ) {
$exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
@@ -860,6 +1291,7 @@ SQL;
/**
* pg_field_type() wrapper
+ * @return string
*/
function fieldType( $res, $index ) {
if ( $res instanceof ResultWrapper ) {
@@ -868,11 +1300,6 @@ SQL;
return pg_field_type( $res, $index );
}
- /* Not even sure why this is used in the main codebase... */
- function limitResultForUpdate( $sql, $num ) {
- return $sql;
- }
-
/**
* @param $b
* @return Blob
@@ -979,9 +1406,6 @@ SQL;
if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
$postLimitTail .= ' FOR UPDATE';
}
- if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
- $postLimitTail .= ' LOCK IN SHARE MODE';
- }
if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
$startOpts .= 'DISTINCT';
}
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
index b2eb1c6b..f1e553d7 100644
--- a/includes/db/DatabaseSqlite.php
+++ b/includes/db/DatabaseSqlite.php
@@ -3,6 +3,21 @@
* This is the SQLite database abstraction layer.
* See maintenance/sqlite/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
*/
@@ -88,7 +103,7 @@ class DatabaseSqlite extends DatabaseBase {
*
* @param $fileName string
*
- * @return PDO|false SQL connection or false if failed
+ * @return PDO|bool SQL connection or false if failed
*/
function openFile( $fileName ) {
$this->mDatabaseFile = $fileName;
@@ -115,16 +130,11 @@ class DatabaseSqlite extends DatabaseBase {
}
/**
- * Close an SQLite database
- *
+ * Does not actually close the connection, just destroys the reference for GC to do its work
* @return bool
*/
- function close() {
- $this->mOpened = false;
- if ( is_object( $this->mConn ) ) {
- if ( $this->trxLevel() ) $this->commit();
- $this->mConn = null;
- }
+ protected function closeConnection() {
+ $this->mConn = null;
return true;
}
@@ -140,7 +150,7 @@ class DatabaseSqlite extends DatabaseBase {
/**
* Check if the searchindext table is FTS enabled.
- * @return false if not enabled.
+ * @return bool False if not enabled.
*/
function checkForEnabledSearch() {
if ( self::$fulltextEnabled === null ) {
@@ -166,7 +176,7 @@ class DatabaseSqlite extends DatabaseBase {
}
$cachedResult = false;
$table = 'dummy_search_test';
-
+
$db = new DatabaseSqliteStandalone( ':memory:' );
if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
@@ -303,7 +313,7 @@ class DatabaseSqlite extends DatabaseBase {
/**
* @param $res ResultWrapper
- * @param $n
+ * @param $n
* @return bool
*/
function fieldName( $res, $n ) {
@@ -347,7 +357,8 @@ class DatabaseSqlite extends DatabaseBase {
* @return int
*/
function insertId() {
- return $this->mConn->lastInsertId();
+ // PDO::lastInsertId yields a string :(
+ return intval( $this->mConn->lastInsertId() );
}
/**
@@ -497,6 +508,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() ) {
if ( !count( $a ) ) {
@@ -610,14 +622,16 @@ class DatabaseSqlite extends DatabaseBase {
* @return string User-friendly database information
*/
public function getServerInfo() {
- return wfMsg( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() );
+ return wfMessage( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() )->text();
}
/**
* Get information about a given field
* Returns false if the field does not exist.
*
- * @return SQLiteField|false
+ * @param $table string
+ * @param $field string
+ * @return SQLiteField|bool False on failure
*/
function fieldInfo( $table, $field ) {
$tableName = $this->tableName( $table );
@@ -631,15 +645,15 @@ class DatabaseSqlite extends DatabaseBase {
return false;
}
- function begin( $fname = '' ) {
+ protected function doBegin( $fname = '' ) {
if ( $this->mTrxLevel == 1 ) {
- $this->commit();
+ $this->commit( __METHOD__ );
}
$this->mConn->beginTransaction();
$this->mTrxLevel = 1;
}
- function commit( $fname = '' ) {
+ protected function doCommit( $fname = '' ) {
if ( $this->mTrxLevel == 0 ) {
return;
}
@@ -647,7 +661,7 @@ class DatabaseSqlite extends DatabaseBase {
$this->mTrxLevel = 0;
}
- function rollback( $fname = '' ) {
+ protected function doRollback( $fname = '' ) {
if ( $this->mTrxLevel == 0 ) {
return;
}
@@ -656,15 +670,6 @@ class DatabaseSqlite extends DatabaseBase {
}
/**
- * @param $sql
- * @param $num
- * @return string
- */
- function limitResultForUpdate( $sql, $num ) {
- return $this->limitResult( $sql, $num );
- }
-
- /**
* @param $s string
* @return string
*/
@@ -723,6 +728,7 @@ class DatabaseSqlite extends DatabaseBase {
/**
* No-op version of deadlockLoop
+ * @return mixed
*/
public function deadlockLoop( /*...*/ ) {
$args = func_get_args();
@@ -812,12 +818,12 @@ class DatabaseSqlite extends DatabaseBase {
}
return $this->query( $sql, $fname );
}
-
-
+
+
/**
* List all tables on the database
*
- * @param $prefix Only show tables with this prefix, e.g. mw_
+ * @param $prefix string Only show tables with this prefix, e.g. mw_
* @param $fname String: calling function name
*
* @return array
@@ -828,21 +834,21 @@ class DatabaseSqlite extends DatabaseBase {
'name',
"type='table'"
);
-
+
$endArray = array();
-
- foreach( $result as $table ) {
+
+ foreach( $result as $table ) {
$vars = get_object_vars($table);
$table = array_pop( $vars );
-
+
if( !$prefix || strpos( $table, $prefix ) === 0 ) {
if ( strpos( $table, 'sqlite_' ) !== 0 ) {
$endArray[] = $table;
}
-
+
}
}
-
+
return $endArray;
}
diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php
index 0ea713c8..c846788d 100644
--- a/includes/db/DatabaseUtility.php
+++ b/includes/db/DatabaseUtility.php
@@ -1,5 +1,27 @@
<?php
/**
+ * This file contains database-related utiliy 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
+ * 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
+ */
+
+/**
* Utility class.
* @ingroup Database
*/
@@ -220,7 +242,11 @@ class FakeResultWrapper extends ResultWrapper {
$this->currentRow = false;
}
$this->pos++;
- return $this->currentRow;
+ if ( is_object( $this->currentRow ) ) {
+ return get_object_vars( $this->currentRow );
+ } else {
+ return $this->currentRow;
+ }
}
function seek( $row ) {
diff --git a/includes/db/IORMRow.php b/includes/db/IORMRow.php
new file mode 100644
index 00000000..e99ba6cc
--- /dev/null
+++ b/includes/db/IORMRow.php
@@ -0,0 +1,275 @@
+<?php
+/**
+ * Interface for representing objects that are stored in some DB table.
+ * This is basically an ORM-like wrapper around rows in database tables that
+ * aims to be both simple and very flexible. It is centered around an associative
+ * array of fields and various methods to do common interaction with the database.
+ *
+ * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable
+ *
+ * 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.20
+ *
+ * @file
+ * @ingroup ORM
+ *
+ * @licence 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
+ *
+ * @param array|null $fields
+ * @param boolean $override
+ * @param boolean $skipLoaded
+ *
+ * @return bool Success indicator
+ */
+ public function loadFields( $fields = null, $override = true, $skipLoaded = false );
+
+ /**
+ * Gets the value of a field.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ * @param mixed $default
+ *
+ * @throws MWException
+ * @return mixed
+ */
+ public function getField( $name, $default = null );
+
+ /**
+ * Gets the value of a field but first loads it if not done so already.
+ *
+ * @since 1.20
+ *
+ * @param string$name
+ *
+ * @return mixed
+ */
+ public function loadAndGetField( $name );
+
+ /**
+ * Remove a field.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ */
+ public function removeField( $name );
+
+ /**
+ * Returns the objects database id.
+ *
+ * @since 1.20
+ *
+ * @return integer|null
+ */
+ public function getId();
+
+ /**
+ * Sets the objects database id.
+ *
+ * @since 1.20
+ *
+ * @param integer|null $id
+ */
+ public function setId( $id );
+
+ /**
+ * Gets if a certain field is set.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ *
+ * @return boolean
+ */
+ public function hasField( $name );
+
+ /**
+ * Gets if the id field is set.
+ *
+ * @since 1.20
+ *
+ * @return boolean
+ */
+ public function hasIdField();
+
+ /**
+ * 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 );
+
+ /**
+ * Serializes the object to an associative array which
+ * can then easily be converted into JSON or similar.
+ *
+ * @since 1.20
+ *
+ * @param null|array $fields
+ * @param boolean $incNullId
+ *
+ * @return array
+ */
+ public function toArray( $fields = null, $incNullId = false );
+
+ /**
+ * Load the default values, via getDefaults.
+ *
+ * @since 1.20
+ *
+ * @param boolean $override
+ */
+ public function loadDefaults( $override = true );
+
+ /**
+ * Writes the answer to the database, either updating it
+ * when it already exists, or inserting it when it doesn't.
+ *
+ * @since 1.20
+ *
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function save( $functionName = null );
+
+ /**
+ * Removes the object from the database.
+ *
+ * @since 1.20
+ *
+ * @return boolean Success indicator
+ */
+ public function remove();
+
+ /**
+ * Return the names and values of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFields();
+
+ /**
+ * Return the names of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getSetFieldNames();
+
+ /**
+ * Sets the value of a field.
+ * Strings can be provided for other types,
+ * so this method can be called from unserialization handlers.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @throws MWException
+ */
+ public function setField( $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
+ *
+ * @param string $field
+ * @param integer $amount
+ *
+ * @return boolean Success indicator
+ */
+ public function addToField( $field, $amount );
+
+ /**
+ * Return the names of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFieldNames();
+
+ /**
+ * Computes and updates the values of the summary fields.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $summaryFields
+ */
+ public function loadSummaryFields( $summaryFields = null );
+
+ /**
+ * Sets the value for the @see $updateSummaries field.
+ *
+ * @since 1.20
+ *
+ * @param boolean $update
+ */
+ public function setUpdateSummaries( $update );
+
+ /**
+ * Sets the value for the @see $inSummaryMode field.
+ *
+ * @since 1.20
+ *
+ * @param boolean $summaryMode
+ */
+ public function setSummaryMode( $summaryMode );
+
+ /**
+ * Returns the table this IORMRow is a row in.
+ *
+ * @since 1.20
+ *
+ * @return IORMTable
+ */
+ public function getTable();
+
+} \ No newline at end of file
diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php
new file mode 100644
index 00000000..99413f99
--- /dev/null
+++ b/includes/db/IORMTable.php
@@ -0,0 +1,448 @@
+<?php
+/**
+ * Interface for objects representing a single database table.
+ * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable
+ *
+ * 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.20
+ *
+ * @file
+ * @ingroup ORM
+ *
+ * @licence GNU GPL v2 or later
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+
+interface IORMTable {
+
+ /**
+ * Returns the name of the database table objects of this type are stored in.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Returns the name of a IORMRow implementing class that
+ * represents single rows in this table.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public function getRowClass();
+
+ /**
+ * Returns an array with the fields and their types this object contains.
+ * This corresponds directly to the fields in the database, without prefix.
+ *
+ * field name => type
+ *
+ * Allowed types:
+ * * id
+ * * str
+ * * int
+ * * float
+ * * bool
+ * * array
+ * * blob
+ *
+ * TODO: get rid of the id field. Every row instance needs to have
+ * one so this is just causing hassle at various locations by requiring an extra check for field name.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFields();
+
+ /**
+ * Returns a list of default field values.
+ * field name => field value
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getDefaults();
+
+ /**
+ * Returns a list of the summary fields.
+ * These are fields that cache computed values, such as the amount of linked objects of $type.
+ * This is relevant as one might not want to do actions such as log changes when these get updated.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getSummaryFields();
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions and returns them as DBDataObject. Field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @return ORMResult
+ */
+ public function select( $fields = null, array $conditions = array(),
+ array $options = array(), $functionName = null );
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions and returns them as DBDataObject. Field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @return array of self
+ */
+ public function selectObjects( $fields = null, array $conditions = array(),
+ array $options = array(), $functionName = null );
+
+ /**
+ * Do the actual select.
+ *
+ * @since 1.20
+ *
+ * @param null|string|array $fields
+ * @param array $conditions
+ * @param array $options
+ * @param null|string $functionName
+ *
+ * @return ResultWrapper
+ */
+ public function rawSelect( $fields = null, array $conditions = array(),
+ array $options = array(), $functionName = null );
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions and returns them as associative arrays.
+ * Provided field names get prefixed.
+ * Returned field names will not have a prefix.
+ *
+ * When $collapse is true:
+ * If one field is selected, each item in the result array will be this field.
+ * If two fields are selected, each item in the result array will have as key
+ * the first field and as value the second field.
+ * If more then two fields are selected, each item will be an associative array.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param boolean $collapse Set to false to always return each result row as associative array.
+ * @param string|null $functionName
+ *
+ * @return array of array
+ */
+ public function selectFields( $fields = null, array $conditions = array(),
+ array $options = array(), $collapse = true, $functionName = null );
+
+ /**
+ * Selects the the specified fields of the first matching record.
+ * Field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @return IORMRow|bool False on failure
+ */
+ public function selectRow( $fields = null, array $conditions = array(),
+ array $options = array(), $functionName = null );
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions. Field names do NOT get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @return ResultWrapper
+ */
+ public function rawSelectRow( array $fields, array $conditions = array(),
+ array $options = array(), $functionName = null );
+
+ /**
+ * Selects the the specified fields of the first record matching the provided
+ * conditions and returns it as an associative array, or false when nothing matches.
+ * This method makes use of selectFields and expects the same parameters and
+ * returns the same results (if there are any, if there are none, this method returns false).
+ * @see IORMTable::selectFields
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param boolean $collapse Set to false to always return each result row as associative array.
+ * @param string|null $functionName
+ *
+ * @return mixed|array|bool False on failure
+ */
+ public function selectFieldsRow( $fields = null, array $conditions = array(),
+ array $options = array(), $collapse = true, $functionName = null );
+
+ /**
+ * Returns if there is at least one record matching the provided conditions.
+ * Condition field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array $conditions
+ *
+ * @return boolean
+ */
+ public function has( array $conditions = array() );
+
+ /**
+ * Returns the amount of matching records.
+ * Condition field names get prefixed.
+ *
+ * Note that this can be expensive on large tables.
+ * In such cases you might want to use DatabaseBase::estimateRowCount instead.
+ *
+ * @since 1.20
+ *
+ * @param array $conditions
+ * @param array $options
+ *
+ * @return integer
+ */
+ public function count( array $conditions = array(), array $options = array() );
+
+ /**
+ * Removes the object from the database.
+ *
+ * @since 1.20
+ *
+ * @param array $conditions
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function delete( array $conditions, $functionName = null );
+
+ /**
+ * Get API parameters for the fields supported by this object.
+ *
+ * @since 1.20
+ *
+ * @param boolean $requireParams
+ * @param boolean $setDefaults
+ *
+ * @return array
+ */
+ public function getAPIParams( $requireParams = false, $setDefaults = false );
+
+ /**
+ * Returns an array with the fields and their descriptions.
+ *
+ * field name => field description
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFieldDescriptions();
+
+ /**
+ * Get the database type used for read operations.
+ *
+ * @since 1.20
+ *
+ * @return integer DB_ enum
+ */
+ public function getReadDb();
+
+ /**
+ * Set the database type to use for read operations.
+ *
+ * @param integer $db
+ *
+ * @since 1.20
+ */
+ public function setReadDb( $db );
+
+ /**
+ * Update the records matching the provided conditions by
+ * setting the fields that are keys in the $values param to
+ * their corresponding values.
+ *
+ * @since 1.20
+ *
+ * @param array $values
+ * @param array $conditions
+ *
+ * @return boolean Success indicator
+ */
+ public function update( array $values, array $conditions = array() );
+
+ /**
+ * Computes the values of the summary fields of the objects matching the provided conditions.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $summaryFields
+ * @param array $conditions
+ */
+ public function updateSummaryFields( $summaryFields = null, array $conditions = array() );
+
+ /**
+ * Takes in an associative array with field names as keys and
+ * their values as value. The field names are prefixed with the
+ * db field prefix.
+ *
+ * @since 1.20
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ public function getPrefixedValues( array $values );
+
+ /**
+ * Takes in a field or array of fields and returns an
+ * array with their prefixed versions, ready for db usage.
+ *
+ * @since 1.20
+ *
+ * @param array|string $fields
+ *
+ * @return array
+ */
+ public function getPrefixedFields( array $fields );
+
+ /**
+ * Takes in a field and returns an it's prefixed version, ready for db usage.
+ *
+ * @since 1.20
+ *
+ * @param string|array $field
+ *
+ * @return string
+ */
+ public function getPrefixedField( $field );
+
+ /**
+ * Takes an array of field names with prefix and returns the unprefixed equivalent.
+ *
+ * @since 1.20
+ *
+ * @param array $fieldNames
+ *
+ * @return array
+ */
+ public function unprefixFieldNames( array $fieldNames );
+
+ /**
+ * Takes a field name with prefix and returns the unprefixed equivalent.
+ *
+ * @since 1.20
+ *
+ * @param string $fieldName
+ *
+ * @return string
+ */
+ 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.
+ *
+ * @since 1.20
+ *
+ * @param stdClass $result
+ *
+ * @return array
+ */
+ public function getFieldsFromDBResult( stdClass $result );
+
+ /**
+ * Get a new instance of the class from a database result.
+ *
+ * @since 1.20
+ *
+ * @param stdClass $result
+ *
+ * @return IORMRow
+ */
+ public function newRowFromDBResult( stdClass $result );
+
+ /**
+ * Get a new instance of the class from an array.
+ *
+ * @since 1.20
+ *
+ * @param array $data
+ * @param boolean $loadDefaults
+ *
+ * @return IORMRow
+ */
+ public function newRow( array $data, $loadDefaults = false );
+
+ /**
+ * Return the names of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFieldNames();
+
+ /**
+ * Gets if the object can take a certain field.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ *
+ * @return boolean
+ */
+ public function canHaveField( $name );
+
+}
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
index dec6ae16..e82c54ba 100644
--- a/includes/db/LBFactory.php
+++ b/includes/db/LBFactory.php
@@ -1,6 +1,21 @@
<?php
/**
- * Generator of database load balancing objects
+ * 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
@@ -176,6 +191,16 @@ class LBFactory_Simple extends LBFactory {
$servers = $wgDBservers;
} else {
global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
+ global $wgDBssl, $wgDBcompress;
+
+ $flags = ( $wgDebugDumpSql ? DBO_DEBUG : 0 ) | DBO_DEFAULT;
+ if ( $wgDBssl ) {
+ $flags |= DBO_SSL;
+ }
+ if ( $wgDBcompress ) {
+ $flags |= DBO_COMPRESS;
+ }
+
$servers = array(array(
'host' => $wgDBserver,
'user' => $wgDBuser,
@@ -183,7 +208,7 @@ class LBFactory_Simple extends LBFactory {
'dbname' => $wgDBname,
'type' => $wgDBtype,
'load' => 1,
- 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
+ 'flags' => $flags
));
}
diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php
index b7977a21..6008813b 100644
--- a/includes/db/LBFactory_Multi.php
+++ b/includes/db/LBFactory_Multi.php
@@ -1,6 +1,21 @@
<?php
/**
- * Advanced generator of database load balancing objects for wiki farms
+ * Advanced generator of database load balancing objects for wiki farms.
+ *
+ * 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
diff --git a/includes/db/LBFactory_Single.php b/includes/db/LBFactory_Single.php
index f80aa4bc..4b165b2a 100644
--- a/includes/db/LBFactory_Single.php
+++ b/includes/db/LBFactory_Single.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Simple generator of database connections that always returns the same 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
+ *
+ * @file
+ * @ingroup Database
+ */
/**
* An LBFactory class that always returns a single database object.
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
index e96c6720..0e455e0c 100644
--- a/includes/db/LoadBalancer.php
+++ b/includes/db/LoadBalancer.php
@@ -1,6 +1,21 @@
<?php
/**
- * Database load balancing
+ * Database load balancing.
+ *
+ * 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
@@ -91,7 +106,7 @@ class LoadBalancer {
/**
* Get or set arbitrary data used by the parent object, usually an LBFactory
* @param $x
- * @return \Mixed
+ * @return Mixed
*/
function parentInfo( $x = null ) {
return wfSetVar( $this->mParentInfo, $x );
@@ -385,7 +400,7 @@ class LoadBalancer {
* Returns false if there is no connection open
*
* @param $i int
- * @return DatabaseBase|false
+ * @return DatabaseBase|bool False on failure
*/
function getAnyOpenConnection( $i ) {
foreach ( $this->mConns as $conns ) {
@@ -894,7 +909,7 @@ class LoadBalancer {
foreach ( $this->mConns as $conns2 ) {
foreach ( $conns2 as $conns3 ) {
foreach ( $conns3 as $conn ) {
- $conn->commit();
+ $conn->commit( __METHOD__ );
}
}
}
@@ -911,7 +926,7 @@ class LoadBalancer {
continue;
}
foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->doneWrites() ) {
+ if ( $conn->writesOrCallbacksPending() ) {
$conn->commit( __METHOD__ );
}
}
diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php
index 16a0343f..146ac61e 100644
--- a/includes/db/LoadMonitor.php
+++ b/includes/db/LoadMonitor.php
@@ -1,6 +1,21 @@
<?php
/**
- * Database load monitoring
+ * Database load monitoring.
+ *
+ * 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
diff --git a/includes/db/ORMIterator.php b/includes/db/ORMIterator.php
new file mode 100644
index 00000000..090b8932
--- /dev/null
+++ b/includes/db/ORMIterator.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * Interface for Iterators containing IORMRows.
+ *
+ * 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.20
+ *
+ * @file
+ * @ingroup ORM
+ *
+ * @licence 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
new file mode 100644
index 00000000..2a5837a1
--- /dev/null
+++ b/includes/db/ORMResult.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * ORMIterator that takes a ResultWrapper object returned from
+ * a select operation returning IORMRow objects (ie IORMTable::select).
+ *
+ * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable
+ *
+ * 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.20
+ *
+ * @file ORMResult.php
+ * @ingroup ORM
+ *
+ * @licence GNU GPL v2 or later
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+
+class ORMResult implements ORMIterator {
+
+ /**
+ * @var ResultWrapper
+ */
+ protected $res;
+
+ /**
+ * @var integer
+ */
+ protected $key;
+
+ /**
+ * @var IORMRow
+ */
+ protected $current;
+
+ /**
+ * @var IORMTable
+ */
+ protected $table;
+
+ /**
+ * @param IORMTable $table
+ * @param ResultWrapper $res
+ */
+ public function __construct( IORMTable $table, ResultWrapper $res ) {
+ $this->table = $table;
+ $this->res = $res;
+ $this->key = 0;
+ $this->setCurrent( $this->res->current() );
+ }
+
+ /**
+ * @param $row
+ */
+ protected function setCurrent( $row ) {
+ if ( $row === false ) {
+ $this->current = false;
+ } else {
+ $this->current = $this->table->newRowFromDBResult( $row );
+ }
+ }
+
+ /**
+ * @return integer
+ */
+ public function count() {
+ return $this->res->numRows();
+ }
+
+ /**
+ * @return boolean
+ */
+ public function isEmpty() {
+ return $this->res->numRows() === 0;
+ }
+
+ /**
+ * @return IORMRow
+ */
+ public function current() {
+ return $this->current;
+ }
+
+ /**
+ * @return integer
+ */
+ public function key() {
+ return $this->key;
+ }
+
+ public function next() {
+ $row = $this->res->next();
+ $this->setCurrent( $row );
+ $this->key++;
+ }
+
+ public function rewind() {
+ $this->res->rewind();
+ $this->key = 0;
+ $this->setCurrent( $this->res->current() );
+ }
+
+ /**
+ * @return boolean
+ */
+ public function valid() {
+ return $this->current !== false;
+ }
+
+}
diff --git a/includes/db/ORMRow.php b/includes/db/ORMRow.php
new file mode 100644
index 00000000..303f3a20
--- /dev/null
+++ b/includes/db/ORMRow.php
@@ -0,0 +1,663 @@
+<?php
+/**
+ * Abstract base class for representing objects that are stored in some DB table.
+ * This is basically an ORM-like wrapper around rows in database tables that
+ * aims to be both simple and very flexible. It is centered around an associative
+ * array of fields and various methods to do common interaction with the database.
+ *
+ * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable
+ *
+ * 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.20
+ *
+ * @file ORMRow.php
+ * @ingroup ORM
+ *
+ * @licence GNU GPL v2 or later
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+
+abstract class ORMRow implements IORMRow {
+
+ /**
+ * The fields of the object.
+ * field name (w/o prefix) => value
+ *
+ * @since 1.20
+ * @var array
+ */
+ 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.
+ *
+ * @since 1.20
+ * @var bool
+ */
+ protected $updateSummaries = true;
+
+ /**
+ * Indicates if the object is in summary mode.
+ * This mode indicates that only summary fields got updated,
+ * which allows for optimizations.
+ *
+ * @since 1.20
+ * @var bool
+ */
+ protected $inSummaryMode = false;
+
+ /**
+ * Constructor.
+ *
+ * @since 1.20
+ *
+ * @param IORMTable $table
+ * @param array|null $fields
+ * @param boolean $loadDefaults
+ */
+ public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ) {
+ $this->table = $table;
+
+ if ( !is_array( $fields ) ) {
+ $fields = array();
+ }
+
+ if ( $loadDefaults ) {
+ $fields = array_merge( $this->table->getDefaults(), $fields );
+ }
+
+ $this->setFields( $fields );
+ }
+
+ /**
+ * Load the specified fields from the database.
+ *
+ * @since 1.20
+ *
+ * @param array|null $fields
+ * @param boolean $override
+ * @param boolean $skipLoaded
+ *
+ * @return bool Success indicator
+ */
+ public function loadFields( $fields = null, $override = true, $skipLoaded = false ) {
+ if ( is_null( $this->getId() ) ) {
+ return false;
+ }
+
+ if ( is_null( $fields ) ) {
+ $fields = array_keys( $this->table->getFields() );
+ }
+
+ if ( $skipLoaded ) {
+ $fields = array_diff( $fields, array_keys( $this->fields ) );
+ }
+
+ if ( !empty( $fields ) ) {
+ $result = $this->table->rawSelectRow(
+ $this->table->getPrefixedFields( $fields ),
+ array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
+ array( 'LIMIT' => 1 )
+ );
+
+ if ( $result !== false ) {
+ $this->setFields( $this->table->getFieldsFromDBResult( $result ), $override );
+ return true;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Gets the value of a field.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ * @param mixed $default
+ *
+ * @throws MWException
+ * @return mixed
+ */
+ public function getField( $name, $default = null ) {
+ if ( $this->hasField( $name ) ) {
+ return $this->fields[$name];
+ } elseif ( !is_null( $default ) ) {
+ return $default;
+ } else {
+ throw new MWException( 'Attempted to get not-set field ' . $name );
+ }
+ }
+
+ /**
+ * Gets the value of a field but first loads it if not done so already.
+ *
+ * @since 1.20
+ *
+ * @param string$name
+ *
+ * @return mixed
+ */
+ public function loadAndGetField( $name ) {
+ if ( !$this->hasField( $name ) ) {
+ $this->loadFields( array( $name ) );
+ }
+
+ return $this->getField( $name );
+ }
+
+ /**
+ * Remove a field.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ */
+ public function removeField( $name ) {
+ unset( $this->fields[$name] );
+ }
+
+ /**
+ * Returns the objects database id.
+ *
+ * @since 1.20
+ *
+ * @return integer|null
+ */
+ public function getId() {
+ return $this->getField( 'id' );
+ }
+
+ /**
+ * Sets the objects database id.
+ *
+ * @since 1.20
+ *
+ * @param integer|null $id
+ */
+ public function setId( $id ) {
+ $this->setField( 'id', $id );
+ }
+
+ /**
+ * Gets if a certain field is set.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ *
+ * @return boolean
+ */
+ public function hasField( $name ) {
+ return array_key_exists( $name, $this->fields );
+ }
+
+ /**
+ * Gets if the id field is set.
+ *
+ * @since 1.20
+ *
+ * @return boolean
+ */
+ public function hasIdField() {
+ return $this->hasField( 'id' )
+ && !is_null( $this->getField( 'id' ) );
+ }
+
+ /**
+ * 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
+ *
+ * @return array
+ */
+ protected function getWriteValues() {
+ $values = array();
+
+ foreach ( $this->table->getFields() as $name => $type ) {
+ if ( array_key_exists( $name, $this->fields ) ) {
+ $value = $this->fields[$name];
+
+ switch ( $type ) {
+ case 'array':
+ $value = (array)$value;
+ case 'blob':
+ $value = serialize( $value );
+ }
+
+ $values[$this->table->getPrefixedField( $name )] = $value;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Serializes the object to an associative array which
+ * can then easily be converted into JSON or similar.
+ *
+ * @since 1.20
+ *
+ * @param null|array $fields
+ * @param boolean $incNullId
+ *
+ * @return array
+ */
+ public function toArray( $fields = null, $incNullId = false ) {
+ $data = array();
+ $setFields = array();
+
+ if ( !is_array( $fields ) ) {
+ $setFields = $this->getSetFieldNames();
+ } else {
+ foreach ( $fields as $field ) {
+ if ( $this->hasField( $field ) ) {
+ $setFields[] = $field;
+ }
+ }
+ }
+
+ foreach ( $setFields as $field ) {
+ if ( $incNullId || $field != 'id' || $this->hasIdField() ) {
+ $data[$field] = $this->getField( $field );
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Load the default values, via getDefaults.
+ *
+ * @since 1.20
+ *
+ * @param boolean $override
+ */
+ public function loadDefaults( $override = true ) {
+ $this->setFields( $this->table->getDefaults(), $override );
+ }
+
+ /**
+ * Writes the answer to the database, either updating it
+ * when it already exists, or inserting it when it doesn't.
+ *
+ * @since 1.20
+ *
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function save( $functionName = null ) {
+ if ( $this->hasIdField() ) {
+ return $this->saveExisting( $functionName );
+ } else {
+ return $this->insert( $functionName );
+ }
+ }
+
+ /**
+ * Updates the object in the database.
+ *
+ * @since 1.20
+ *
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ protected function saveExisting( $functionName = null ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $success = $dbw->update(
+ $this->table->getName(),
+ $this->getWriteValues(),
+ $this->table->getPrefixedValues( $this->getUpdateConditions() ),
+ is_null( $functionName ) ? __METHOD__ : $functionName
+ );
+
+ // DatabaseBase::update does not always return true for success as documented...
+ return $success !== false;
+ }
+
+ /**
+ * Returns the WHERE considtions needed to identify this object so
+ * it can be updated.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ protected function getUpdateConditions() {
+ return array( 'id' => $this->getId() );
+ }
+
+ /**
+ * Inserts the object into the database.
+ *
+ * @since 1.20
+ *
+ * @param string|null $functionName
+ * @param array|null $options
+ *
+ * @return boolean Success indicator
+ */
+ protected function insert( $functionName = null, array $options = null ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $success = $dbw->insert(
+ $this->table->getName(),
+ $this->getWriteValues(),
+ is_null( $functionName ) ? __METHOD__ : $functionName,
+ is_null( $options ) ? array( 'IGNORE' ) : $options
+ );
+
+ // DatabaseBase::insert does not always return true for success as documented...
+ $success = $success !== false;
+
+ if ( $success ) {
+ $this->setField( 'id', $dbw->insertId() );
+ }
+
+ return $success;
+ }
+
+ /**
+ * Removes the object from the database.
+ *
+ * @since 1.20
+ *
+ * @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;
+
+ if ( $success ) {
+ $this->onRemoved();
+ }
+
+ return $success;
+ }
+
+ /**
+ * Gets called before an object is removed from the database.
+ *
+ * @since 1.20
+ */
+ protected function beforeRemove() {
+ $this->loadFields( $this->getBeforeRemoveFields(), false, true );
+ }
+
+ /**
+ * Before removal of an object happens, @see beforeRemove gets called.
+ * This method loads the fields of which the names have been returned by this one (or all fields if null is returned).
+ * This allows for loading info needed after removal to get rid of linked data and the like.
+ *
+ * @since 1.20
+ *
+ * @return array|null
+ */
+ protected function getBeforeRemoveFields() {
+ return array();
+ }
+
+ /**
+ * Gets called after successfull removal.
+ * Can be overriden to get rid of linked data.
+ *
+ * @since 1.20
+ */
+ protected function onRemoved() {
+ $this->setField( 'id', null );
+ }
+
+ /**
+ * Return the names and values of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFields() {
+ return $this->fields;
+ }
+
+ /**
+ * Return the names of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getSetFieldNames() {
+ return array_keys( $this->fields );
+ }
+
+ /**
+ * Sets the value of a field.
+ * Strings can be provided for other types,
+ * so this method can be called from unserialization handlers.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @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 );
+ }
+ }
+
+ /**
+ * 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
+ *
+ * @param string $field
+ * @param integer $amount
+ *
+ * @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 the names of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFieldNames() {
+ return array_keys( $this->table->getFields() );
+ }
+
+ /**
+ * Computes and updates the values of the summary fields.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $summaryFields
+ */
+ public function loadSummaryFields( $summaryFields = null ) {
+
+ }
+
+ /**
+ * Sets the value for the @see $updateSummaries field.
+ *
+ * @since 1.20
+ *
+ * @param boolean $update
+ */
+ public function setUpdateSummaries( $update ) {
+ $this->updateSummaries = $update;
+ }
+
+ /**
+ * Sets the value for the @see $inSummaryMode field.
+ *
+ * @since 1.20
+ *
+ * @param boolean $summaryMode
+ */
+ public function setSummaryMode( $summaryMode ) {
+ $this->inSummaryMode = $summaryMode;
+ }
+
+ /**
+ * 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
+ *
+ * @return IORMTable
+ */
+ public function getTable() {
+ return $this->table;
+ }
+
+}
diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php
new file mode 100644
index 00000000..a77074ff
--- /dev/null
+++ b/includes/db/ORMTable.php
@@ -0,0 +1,675 @@
+<?php
+/**
+ * Abstract base class for representing a single database table.
+ * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable
+ *
+ * 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.20
+ *
+ * @file ORMTable.php
+ * @ingroup ORM
+ *
+ * @licence GNU GPL v2 or later
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+
+abstract class ORMTable implements IORMTable {
+
+ /**
+ * Gets the db field prefix.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ protected abstract function getFieldPrefix();
+
+ /**
+ * Cache for instances, used by the singleton method.
+ *
+ * @since 1.20
+ * @var array of DBTable
+ */
+ protected static $instanceCache = array();
+
+ /**
+ * 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;
+
+ /**
+ * Returns a list of default field values.
+ * field name => field value
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getDefaults() {
+ return array();
+ }
+
+ /**
+ * Returns a list of the summary fields.
+ * These are fields that cache computed values, such as the amount of linked objects of $type.
+ * This is relevant as one might not want to do actions such as log changes when these get updated.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getSummaryFields() {
+ return array();
+ }
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions and returns them as DBDataObject. Field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @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 ) );
+ }
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions and returns them as DBDataObject. Field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @return array of self
+ */
+ public function selectObjects( $fields = null, array $conditions = array(),
+ array $options = array(), $functionName = null ) {
+ $result = $this->selectFields( $fields, $conditions, $options, false, $functionName );
+
+ $objects = array();
+
+ foreach ( $result as $record ) {
+ $objects[] = $this->newRow( $record );
+ }
+
+ return $objects;
+ }
+
+ /**
+ * Do the actual select.
+ *
+ * @since 1.20
+ *
+ * @param null|string|array $fields
+ * @param array $conditions
+ * @param array $options
+ * @param null|string $functionName
+ *
+ * @return ResultWrapper
+ */
+ public function rawSelect( $fields = null, array $conditions = array(),
+ array $options = array(), $functionName = null ) {
+ if ( is_null( $fields ) ) {
+ $fields = array_keys( $this->getFields() );
+ }
+ else {
+ $fields = (array)$fields;
+ }
+
+ return wfGetDB( $this->getReadDb() )->select(
+ $this->getName(),
+ $this->getPrefixedFields( $fields ),
+ $this->getPrefixedValues( $conditions ),
+ is_null( $functionName ) ? __METHOD__ : $functionName,
+ $options
+ );
+ }
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions and returns them as associative arrays.
+ * Provided field names get prefixed.
+ * Returned field names will not have a prefix.
+ *
+ * When $collapse is true:
+ * If one field is selected, each item in the result array will be this field.
+ * If two fields are selected, each item in the result array will have as key
+ * the first field and as value the second field.
+ * If more then two fields are selected, each item will be an associative array.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param boolean $collapse Set to false to always return each result row as associative array.
+ * @param string|null $functionName
+ *
+ * @return array of array
+ */
+ public function selectFields( $fields = null, array $conditions = array(),
+ array $options = array(), $collapse = true, $functionName = null ) {
+ $objects = array();
+
+ $result = $this->rawSelect( $fields, $conditions, $options, $functionName );
+
+ foreach ( $result as $record ) {
+ $objects[] = $this->getFieldsFromDBResult( $record );
+ }
+
+ if ( $collapse ) {
+ if ( count( $fields ) === 1 ) {
+ $objects = array_map( 'array_shift', $objects );
+ }
+ elseif ( count( $fields ) === 2 ) {
+ $o = array();
+
+ foreach ( $objects as $object ) {
+ $o[array_shift( $object )] = array_shift( $object );
+ }
+
+ $objects = $o;
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * Selects the the specified fields of the first matching record.
+ * Field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @return IORMRow|bool False on failure
+ */
+ public function selectRow( $fields = null, array $conditions = array(),
+ array $options = array(), $functionName = null ) {
+ $options['LIMIT'] = 1;
+
+ $objects = $this->select( $fields, $conditions, $options, $functionName );
+
+ return $objects->isEmpty() ? false : $objects->current();
+ }
+
+ /**
+ * Selects the the specified fields of the records matching the provided
+ * conditions. Field names do NOT get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array $fields
+ * @param array $conditions
+ * @param array $options
+ * @param string|null $functionName
+ *
+ * @return ResultWrapper
+ */
+ public function rawSelectRow( array $fields, array $conditions = array(),
+ array $options = array(), $functionName = null ) {
+ $dbr = wfGetDB( $this->getReadDb() );
+
+ return $dbr->selectRow(
+ $this->getName(),
+ $fields,
+ $conditions,
+ is_null( $functionName ) ? __METHOD__ : $functionName,
+ $options
+ );
+ }
+
+ /**
+ * Selects the the specified fields of the first record matching the provided
+ * conditions and returns it as an associative array, or false when nothing matches.
+ * This method makes use of selectFields and expects the same parameters and
+ * returns the same results (if there are any, if there are none, this method returns false).
+ * @see ORMTable::selectFields
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $fields
+ * @param array $conditions
+ * @param array $options
+ * @param boolean $collapse Set to false to always return each result row as associative array.
+ * @param string|null $functionName
+ *
+ * @return mixed|array|bool False on failure
+ */
+ public function selectFieldsRow( $fields = null, array $conditions = array(),
+ array $options = array(), $collapse = true, $functionName = null ) {
+ $options['LIMIT'] = 1;
+
+ $objects = $this->selectFields( $fields, $conditions, $options, $collapse, $functionName );
+
+ return empty( $objects ) ? false : $objects[0];
+ }
+
+ /**
+ * Returns if there is at least one record matching the provided conditions.
+ * Condition field names get prefixed.
+ *
+ * @since 1.20
+ *
+ * @param array $conditions
+ *
+ * @return boolean
+ */
+ public function has( array $conditions = array() ) {
+ return $this->selectRow( array( 'id' ), $conditions ) !== false;
+ }
+
+ /**
+ * Returns the amount of matching records.
+ * Condition field names get prefixed.
+ *
+ * Note that this can be expensive on large tables.
+ * In such cases you might want to use DatabaseBase::estimateRowCount instead.
+ *
+ * @since 1.20
+ *
+ * @param array $conditions
+ * @param array $options
+ *
+ * @return integer
+ */
+ public function count( array $conditions = array(), array $options = array() ) {
+ $res = $this->rawSelectRow(
+ array( 'rowcount' => 'COUNT(*)' ),
+ $this->getPrefixedValues( $conditions ),
+ $options
+ );
+
+ return $res->rowcount;
+ }
+
+ /**
+ * Removes the object from the database.
+ *
+ * @since 1.20
+ *
+ * @param array $conditions
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function delete( array $conditions, $functionName = null ) {
+ return wfGetDB( DB_MASTER )->delete(
+ $this->getName(),
+ $conditions === array() ? '*' : $this->getPrefixedValues( $conditions ),
+ $functionName
+ ) !== false; // DatabaseBase::delete does not always return true for success as documented...
+ }
+
+ /**
+ * Get API parameters for the fields supported by this object.
+ *
+ * @since 1.20
+ *
+ * @param boolean $requireParams
+ * @param boolean $setDefaults
+ *
+ * @return array
+ */
+ public function getAPIParams( $requireParams = false, $setDefaults = false ) {
+ $typeMap = array(
+ 'id' => 'integer',
+ 'int' => 'integer',
+ 'float' => 'NULL',
+ 'str' => 'string',
+ 'bool' => 'integer',
+ 'array' => 'string',
+ 'blob' => 'string',
+ );
+
+ $params = array();
+ $defaults = $this->getDefaults();
+
+ foreach ( $this->getFields() as $field => $type ) {
+ if ( $field == 'id' ) {
+ continue;
+ }
+
+ $hasDefault = array_key_exists( $field, $defaults );
+
+ $params[$field] = array(
+ ApiBase::PARAM_TYPE => $typeMap[$type],
+ ApiBase::PARAM_REQUIRED => $requireParams && !$hasDefault
+ );
+
+ if ( $type == 'array' ) {
+ $params[$field][ApiBase::PARAM_ISMULTI] = true;
+ }
+
+ if ( $setDefaults && $hasDefault ) {
+ $default = is_array( $defaults[$field] ) ? implode( '|', $defaults[$field] ) : $defaults[$field];
+ $params[$field][ApiBase::PARAM_DFLT] = $default;
+ }
+ }
+
+ return $params;
+ }
+
+ /**
+ * Returns an array with the fields and their descriptions.
+ *
+ * field name => field description
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFieldDescriptions() {
+ return array();
+ }
+
+ /**
+ * Get the database type used for read operations.
+ *
+ * @since 1.20
+ *
+ * @return integer DB_ enum
+ */
+ public function getReadDb() {
+ return $this->readDb;
+ }
+
+ /**
+ * Set the database type to use for read operations.
+ *
+ * @param integer $db
+ *
+ * @since 1.20
+ */
+ public function setReadDb( $db ) {
+ $this->readDb = $db;
+ }
+
+ /**
+ * Update the records matching the provided conditions by
+ * setting the fields that are keys in the $values param to
+ * their corresponding values.
+ *
+ * @since 1.20
+ *
+ * @param array $values
+ * @param array $conditions
+ *
+ * @return boolean Success indicator
+ */
+ public function update( array $values, array $conditions = array() ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ return $dbw->update(
+ $this->getName(),
+ $this->getPrefixedValues( $values ),
+ $this->getPrefixedValues( $conditions ),
+ __METHOD__
+ ) !== false; // DatabaseBase::update does not always return true for success as documented...
+ }
+
+ /**
+ * Computes the values of the summary fields of the objects matching the provided conditions.
+ *
+ * @since 1.20
+ *
+ * @param array|string|null $summaryFields
+ * @param array $conditions
+ */
+ public function updateSummaryFields( $summaryFields = null, array $conditions = array() ) {
+ $this->setReadDb( DB_MASTER );
+
+ /**
+ * @var IORMRow $item
+ */
+ foreach ( $this->select( null, $conditions ) as $item ) {
+ $item->loadSummaryFields( $summaryFields );
+ $item->setSummaryMode( true );
+ $item->save();
+ }
+
+ $this->setReadDb( DB_SLAVE );
+ }
+
+ /**
+ * Takes in an associative array with field names as keys and
+ * their values as value. The field names are prefixed with the
+ * db field prefix.
+ *
+ * @since 1.20
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ public function getPrefixedValues( array $values ) {
+ $prefixedValues = array();
+
+ foreach ( $values as $field => $value ) {
+ if ( is_integer( $field ) ) {
+ if ( is_array( $value ) ) {
+ $field = $value[0];
+ $value = $value[1];
+ }
+ else {
+ $value = explode( ' ', $value, 2 );
+ $value[0] = $this->getPrefixedField( $value[0] );
+ $prefixedValues[] = implode( ' ', $value );
+ continue;
+ }
+ }
+
+ $prefixedValues[$this->getPrefixedField( $field )] = $value;
+ }
+
+ return $prefixedValues;
+ }
+
+ /**
+ * Takes in a field or array of fields and returns an
+ * array with their prefixed versions, ready for db usage.
+ *
+ * @since 1.20
+ *
+ * @param array|string $fields
+ *
+ * @return array
+ */
+ public function getPrefixedFields( array $fields ) {
+ foreach ( $fields as &$field ) {
+ $field = $this->getPrefixedField( $field );
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Takes in a field and returns an it's prefixed version, ready for db usage.
+ *
+ * @since 1.20
+ *
+ * @param string|array $field
+ *
+ * @return string
+ */
+ public function getPrefixedField( $field ) {
+ return $this->getFieldPrefix() . $field;
+ }
+
+ /**
+ * Takes an array of field names with prefix and returns the unprefixed equivalent.
+ *
+ * @since 1.20
+ *
+ * @param array $fieldNames
+ *
+ * @return array
+ */
+ public function unprefixFieldNames( array $fieldNames ) {
+ return array_map( array( $this, 'unprefixFieldName' ), $fieldNames );
+ }
+
+ /**
+ * Takes a field name with prefix and returns the unprefixed equivalent.
+ *
+ * @since 1.20
+ *
+ * @param string $fieldName
+ *
+ * @return string
+ */
+ public function unprefixFieldName( $fieldName ) {
+ return substr( $fieldName, strlen( $this->getFieldPrefix() ) );
+ }
+
+ /**
+ * Get an instance of this class.
+ *
+ * @since 1.20
+ *
+ * @return IORMTable
+ */
+ public static function singleton() {
+ $class = get_called_class();
+
+ if ( !array_key_exists( $class, self::$instanceCache ) ) {
+ self::$instanceCache[$class] = new $class;
+ }
+
+ return self::$instanceCache[$class];
+ }
+
+ /**
+ * Get an array with fields from a database result,
+ * that can be fed directly to the constructor or
+ * to setFields.
+ *
+ * @since 1.20
+ *
+ * @param stdClass $result
+ *
+ * @return array
+ */
+ public function getFieldsFromDBResult( stdClass $result ) {
+ $result = (array)$result;
+ return array_combine(
+ $this->unprefixFieldNames( array_keys( $result ) ),
+ array_values( $result )
+ );
+ }
+
+ /**
+ * @see ORMTable::newRowFromFromDBResult
+ *
+ * @deprecated use newRowFromDBResult instead
+ * @since 1.20
+ *
+ * @param stdClass $result
+ *
+ * @return IORMRow
+ */
+ public function newFromDBResult( stdClass $result ) {
+ return self::newRowFromDBResult( $result );
+ }
+
+ /**
+ * Get a new instance of the class from a database result.
+ *
+ * @since 1.20
+ *
+ * @param stdClass $result
+ *
+ * @return IORMRow
+ */
+ public function newRowFromDBResult( stdClass $result ) {
+ return $this->newRow( $this->getFieldsFromDBResult( $result ) );
+ }
+
+ /**
+ * @see ORMTable::newRow
+ *
+ * @deprecated use newRow instead
+ * @since 1.20
+ *
+ * @param array $data
+ * @param boolean $loadDefaults
+ *
+ * @return IORMRow
+ */
+ public function newFromArray( array $data, $loadDefaults = false ) {
+ return static::newRow( $data, $loadDefaults );
+ }
+
+ /**
+ * Get a new instance of the class from an array.
+ *
+ * @since 1.20
+ *
+ * @param array $data
+ * @param boolean $loadDefaults
+ *
+ * @return IORMRow
+ */
+ public function newRow( array $data, $loadDefaults = false ) {
+ $class = $this->getRowClass();
+ return new $class( $this, $data, $loadDefaults );
+ }
+
+ /**
+ * Return the names of the fields.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFieldNames() {
+ return array_keys( $this->getFields() );
+ }
+
+ /**
+ * Gets if the object can take a certain field.
+ *
+ * @since 1.20
+ *
+ * @param string $name
+ *
+ * @return boolean
+ */
+ public function canHaveField( $name ) {
+ return array_key_exists( $name, $this->getFields() );
+ }
+
+}
diff --git a/includes/debug/Debug.php b/includes/debug/Debug.php
index de50ccac..d02bcf53 100644
--- a/includes/debug/Debug.php
+++ b/includes/debug/Debug.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * 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
+ * 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
+ */
/**
* New debugger system that outputs a toolbar on page view
@@ -7,6 +27,8 @@
* to explicitly call MWDebug::init() to enabled them.
*
* @todo Profiler support
+ *
+ * @since 1.19
*/
class MWDebug {
@@ -49,6 +71,8 @@ class MWDebug {
/**
* Enabled the debugger and load resource module.
* This is called by Setup.php when $wgDebugToolbar is true.
+ *
+ * @since 1.19
*/
public static function init() {
self::$enabled = true;
@@ -58,6 +82,7 @@ class MWDebug {
* Add ResourceLoader modules to the OutputPage object if debugging is
* enabled.
*
+ * @since 1.19
* @param $out OutputPage
*/
public static function addModules( OutputPage $out ) {
@@ -71,6 +96,7 @@ class MWDebug {
*
* @todo Add support for passing objects
*
+ * @since 1.19
* @param $str string
*/
public static function log( $str ) {
@@ -87,6 +113,8 @@ class MWDebug {
/**
* Returns internal log array
+ * @since 1.19
+ * @return array
*/
public static function getLog() {
return self::$log;
@@ -94,6 +122,7 @@ class MWDebug {
/**
* Clears internal log array and deprecation tracking
+ * @since 1.19
*/
public static function clearLog() {
self::$log = array();
@@ -103,87 +132,178 @@ class MWDebug {
/**
* Adds a warning entry to the log
*
- * @param $msg
- * @param int $callerOffset
+ * @since 1.19
+ * @param $msg string
+ * @param $callerOffset int
* @return mixed
*/
- public static function warning( $msg, $callerOffset = 1 ) {
- if ( !self::$enabled ) {
- return;
+ public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) {
+ $callerDescription = self::getCallerDescription( $callerOffset );
+
+ self::sendWarning( $msg, $callerDescription, $level );
+
+ if ( self::$enabled ) {
+ self::$log[] = array(
+ 'msg' => htmlspecialchars( $msg ),
+ 'type' => 'warn',
+ 'caller' => $callerDescription['func'],
+ );
}
+ }
+
+ /**
+ * Show a warning that $function is deprecated.
+ * This will send it to the following locations:
+ * - Debug toolbar, with one item per function and caller, if $wgDebugToolbar
+ * is set to true.
+ * - PHP's error log, with level E_USER_DEPRECATED, if $wgDevelopmentWarnings
+ * is set to true.
+ * - 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.
+ * 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
+ * MWDebug::deprecated() (Added in 1.20).
+ * @return mixed
+ */
+ public static function deprecated( $function, $version = false, $component = false, $callerOffset = 2 ) {
+ $callerDescription = self::getCallerDescription( $callerOffset );
+ $callerFunc = $callerDescription['func'];
+
+ $sendToLog = true;
- // Check to see if there was already a deprecation notice, so not to
- // get a duplicate warning
- $logCount = count( self::$log );
- if ( $logCount ) {
- $lastLog = self::$log[ $logCount - 1 ];
- if ( $lastLog['type'] == 'deprecated' && $lastLog['caller'] == wfGetCaller( $callerOffset + 1 ) ) {
+ // Check to see if there already was a warning about this function
+ if ( isset( self::$deprecationWarnings[$function][$callerFunc] ) ) {
+ return;
+ } elseif ( isset( self::$deprecationWarnings[$function] ) ) {
+ if ( self::$enabled ) {
+ $sendToLog = false;
+ } else {
return;
}
}
- self::$log[] = array(
- 'msg' => htmlspecialchars( $msg ),
- 'type' => 'warn',
- 'caller' => wfGetCaller( $callerOffset ),
- );
+ self::$deprecationWarnings[$function][$callerFunc] = true;
+
+ if ( $version ) {
+ global $wgDeprecationReleaseLimit;
+ if ( $wgDeprecationReleaseLimit && $component === false ) {
+ # Strip -* off the end of $version so that branches can use the
+ # format #.##-branchname to avoid issues if the branch is merged into
+ # a version of MediaWiki later than what it was branched from
+ $comparableVersion = preg_replace( '/-.*$/', '', $version );
+
+ # If the comparableVersion is larger than our release limit then
+ # skip the warning message for the deprecation
+ if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
+ $sendToLog = false;
+ }
+ }
+
+ $component = $component === false ? 'MediaWiki' : $component;
+ $msg = "Use of $function was deprecated in $component $version.";
+ } else {
+ $msg = "Use of $function is deprecated.";
+ }
+
+ if ( $sendToLog ) {
+ self::sendWarning( $msg, $callerDescription, E_USER_DEPRECATED );
+ }
+
+ if ( self::$enabled ) {
+ $logMsg = htmlspecialchars( $msg ) .
+ Html::rawElement( 'div', array( 'class' => 'mw-debug-backtrace' ),
+ Html::element( 'span', array(), 'Backtrace:' ) . wfBacktrace()
+ );
+
+ self::$log[] = array(
+ 'msg' => $logMsg,
+ 'type' => 'deprecated',
+ 'caller' => $callerFunc,
+ );
+ }
}
/**
- * Adds a depreciation entry to the log, along with a backtrace
+ * Get an array describing the calling function at a specified offset.
*
- * @param $function
- * @param $version
- * @param $component
- * @return mixed
+ * @param $callerOffset integer: How far up the callstack is the original
+ * caller. 0 = function that called getCallerDescription()
+ * @return array with two keys: 'file' and 'func'
*/
- public static function deprecated( $function, $version, $component ) {
- if ( !self::$enabled ) {
- return;
+ private static function getCallerDescription( $callerOffset ) {
+ $callers = wfDebugBacktrace();
+
+ if ( isset( $callers[$callerOffset] ) ) {
+ $callerfile = $callers[$callerOffset];
+ if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
+ $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
+ } else {
+ $file = '(internal function)';
+ }
+ } else {
+ $file = '(unknown location)';
}
- // Chain: This function -> wfDeprecated -> deprecatedFunction -> caller
- $caller = wfGetCaller( 4 );
-
- // Check to see if there already was a warning about this function
- $functionString = "$function-$caller";
- if ( in_array( $functionString, self::$deprecationWarnings ) ) {
- return;
+ if ( isset( $callers[$callerOffset + 1] ) ) {
+ $callerfunc = $callers[$callerOffset + 1];
+ $func = '';
+ if ( isset( $callerfunc['class'] ) ) {
+ $func .= $callerfunc['class'] . '::';
+ }
+ if ( isset( $callerfunc['function'] ) ) {
+ $func .= $callerfunc['function'];
+ }
+ } else {
+ $func = 'unknown';
}
- $version = $version === false ? '(unknown version)' : $version;
- $component = $component === false ? 'MediaWiki' : $component;
- $msg = htmlspecialchars( "Use of function $function was deprecated in $component $version" );
- $msg .= Html::rawElement( 'div', array( 'class' => 'mw-debug-backtrace' ),
- Html::element( 'span', array(), 'Backtrace:' )
- . wfBacktrace()
- );
+ return array( 'file' => $file, 'func' => $func );
+ }
- self::$deprecationWarnings[] = $functionString;
- self::$log[] = array(
- 'msg' => $msg,
- 'type' => 'deprecated',
- 'caller' => $caller,
- );
+ /**
+ * Send a warning either to the debug log or by triggering an user PHP
+ * error depending on $wgDevelopmentWarnings.
+ *
+ * @param $msg string Message to send
+ * @param $caller array caller description get from getCallerDescription()
+ * @param $level error level to use if $wgDevelopmentWarnings is true
+ */
+ private static function sendWarning( $msg, $caller, $level ) {
+ global $wgDevelopmentWarnings;
+
+ $msg .= ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
+
+ if ( $wgDevelopmentWarnings ) {
+ trigger_error( $msg, $level );
+ } else {
+ wfDebug( "$msg\n" );
+ }
}
/**
* This is a method to pass messages from wfDebug to the pretty debugger.
* Do NOT use this method, use MWDebug::log or wfDebug()
*
+ * @since 1.19
* @param $str string
*/
public static function debugMsg( $str ) {
- if ( !self::$enabled ) {
- return;
- }
+ global $wgDebugComments, $wgShowDebug;
- self::$debug[] = trim( $str );
+ if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
+ self::$debug[] = rtrim( $str );
+ }
}
/**
* Begins profiling on a database query
*
+ * @since 1.19
* @param $sql string
* @param $function string
* @param $isMaster bool
@@ -209,6 +329,7 @@ class MWDebug {
/**
* Calculates how long a query took.
*
+ * @since 1.19
* @param $id int
*/
public static function queryTime( $id ) {
@@ -243,26 +364,162 @@ class MWDebug {
/**
* Returns the HTML to add to the page for the toolbar
*
+ * @since 1.19
* @param $context IContextSource
* @return string
*/
public static function getDebugHTML( IContextSource $context ) {
- if ( !self::$enabled ) {
+ global $wgDebugComments;
+
+ $html = '';
+
+ if ( self::$enabled ) {
+ MWDebug::log( 'MWDebug output complete' );
+ $debugInfo = self::getDebugInfo( $context );
+
+ // Cannot use OutputPage::addJsConfigVars because those are already outputted
+ // by the time this method is called.
+ $html = Html::inlineScript(
+ ResourceLoader::makeLoaderConditionalScript(
+ ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) )
+ )
+ );
+ }
+
+ if ( $wgDebugComments ) {
+ $html .= "<!-- Debug output:\n" .
+ htmlspecialchars( implode( "\n", self::$debug ) ) .
+ "\n\n-->";
+ }
+
+ return $html;
+ }
+
+ /**
+ * Generate debug log in HTML for displaying at the bottom of the main
+ * content area.
+ * If $wgShowDebug is false, an empty string is always returned.
+ *
+ * @since 1.20
+ * @return string HTML fragment
+ */
+ public static function getHTMLDebugLog() {
+ global $wgDebugTimestamps, $wgShowDebug;
+
+ if ( !$wgShowDebug ) {
return '';
}
- global $wgVersion, $wgRequestTime;
+ $curIdent = 0;
+ $ret = "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">\n<li>";
+
+ foreach ( self::$debug as $line ) {
+ $pre = '';
+ if ( $wgDebugTimestamps ) {
+ $matches = array();
+ if ( preg_match( '/^(\d+\.\d+ {1,3}\d+.\dM\s{2})/', $line, $matches ) ) {
+ $pre = $matches[1];
+ $line = substr( $line, strlen( $pre ) );
+ }
+ }
+ $display = ltrim( $line );
+ $ident = strlen( $line ) - strlen( $display );
+ $diff = $ident - $curIdent;
+
+ $display = $pre . $display;
+ if ( $display == '' ) {
+ $display = "\xc2\xa0";
+ }
+
+ if ( !$ident && $diff < 0 && substr( $display, 0, 9 ) != 'Entering ' && substr( $display, 0, 8 ) != 'Exiting ' ) {
+ $ident = $curIdent;
+ $diff = 0;
+ $display = '<span style="background:yellow;">' . nl2br( htmlspecialchars( $display ) ) . '</span>';
+ } else {
+ $display = nl2br( htmlspecialchars( $display ) );
+ }
+
+ if ( $diff < 0 ) {
+ $ret .= str_repeat( "</li></ul>\n", -$diff ) . "</li><li>\n";
+ } elseif ( $diff == 0 ) {
+ $ret .= "</li><li>\n";
+ } else {
+ $ret .= str_repeat( "<ul><li>\n", $diff );
+ }
+ $ret .= "<tt>$display</tt>\n";
+
+ $curIdent = $ident;
+ }
+
+ $ret .= str_repeat( '</li></ul>', $curIdent ) . "</li>\n</ul>\n";
+
+ return $ret;
+ }
+
+ /**
+ * Append the debug info to given ApiResult
+ *
+ * @param $context IContextSource
+ * @param $result ApiResult
+ */
+ public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
+ if ( !self::$enabled ) {
+ return;
+ }
+
+ // 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 ) {
+ $obContentArray = explode( '<br />', $obContents );
+ foreach( $obContentArray as $obContent ) {
+ if( trim( $obContent ) ) {
+ self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
+ }
+ }
+ }
+
MWDebug::log( 'MWDebug output complete' );
+ $debugInfo = self::getDebugInfo( $context );
+
+ $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 );
+ }
+
+ /**
+ * Returns the HTML to add to the page for the toolbar
+ *
+ * @param $context IContextSource
+ * @return array
+ */
+ public static function getDebugInfo( IContextSource $context ) {
+ if ( !self::$enabled ) {
+ return array();
+ }
+
+ global $wgVersion, $wgRequestTime;
$request = $context->getRequest();
- $debugInfo = array(
+ return array(
'mwVersion' => $wgVersion,
'phpVersion' => PHP_VERSION,
+ 'gitRevision' => GitInfo::headSHA1(),
+ 'gitBranch' => GitInfo::currentBranch(),
+ 'gitViewUrl' => GitInfo::headViewUrl(),
'time' => microtime( true ) - $wgRequestTime,
'log' => self::$log,
'debugLog' => self::$debug,
'queries' => self::$query,
'request' => array(
- 'method' => $_SERVER['REQUEST_METHOD'],
+ 'method' => $request->getMethod(),
'url' => $request->getRequestURL(),
'headers' => $request->getAllHeaders(),
'params' => $request->getValues(),
@@ -271,15 +528,5 @@ class MWDebug {
'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage() ),
'includes' => self::getFilesIncluded( $context ),
);
-
- // Cannot use OutputPage::addJsConfigVars because those are already outputted
- // by the time this method is called.
- $html = Html::inlineScript(
- ResourceLoader::makeLoaderConditionalScript(
- ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) )
- )
- );
-
- return $html;
}
}
diff --git a/includes/diff/DairikiDiff.php b/includes/diff/DairikiDiff.php
index c935eee2..72eb5d3c 100644
--- a/includes/diff/DairikiDiff.php
+++ b/includes/diff/DairikiDiff.php
@@ -185,8 +185,8 @@ class _DiffEngine {
$edits = array();
$xi = $yi = 0;
while ( $xi < $n_from || $yi < $n_to ) {
- assert( $yi < $n_to || $this->xchanged[$xi] );
- assert( $xi < $n_from || $this->ychanged[$yi] );
+ assert( '$yi < $n_to || $this->xchanged[$xi]' );
+ assert( '$xi < $n_from || $this->ychanged[$yi]' );
// Skip matching "snake".
$copy = array();
@@ -374,14 +374,14 @@ class _DiffEngine {
while ( list( , $y ) = each( $matches ) ) {
if ( empty( $this->in_seq[$y] ) ) {
$k = $this->_lcs_pos( $y );
- assert( $k > 0 );
+ assert( '$k > 0' );
$ymids[$k] = $ymids[$k -1];
break;
}
}
while ( list ( , $y ) = each( $matches ) ) {
if ( $y > $this->seq[$k -1] ) {
- assert( $y < $this->seq[$k] );
+ assert( '$y < $this->seq[$k]' );
// Optimization: this is a common case:
// next match is just replacing previous match.
$this->in_seq[$this->seq[$k]] = false;
@@ -389,7 +389,7 @@ class _DiffEngine {
$this->in_seq[$y] = 1;
} elseif ( empty( $this->in_seq[$y] ) ) {
$k = $this->_lcs_pos( $y );
- assert( $k > 0 );
+ assert( '$k > 0' );
$ymids[$k] = $ymids[$k -1];
}
}
@@ -430,7 +430,7 @@ class _DiffEngine {
}
}
- assert( $ypos != $this->seq[$end] );
+ assert( '$ypos != $this->seq[$end]' );
$this->in_seq[$this->seq[$end]] = false;
$this->seq[$end] = $ypos;
@@ -661,7 +661,7 @@ class Diff {
*
* $diff = new Diff($lines1, $lines2);
* $rev = $diff->reverse();
- * @return object A Diff object representing the inverse of the
+ * @return Object A Diff object representing the inverse of the
* original diff.
*/
function reverse() {
@@ -814,8 +814,8 @@ 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( 'sizeof( $from_lines ) == sizeof( $mapped_from_lines )' );
+ assert( 'sizeof( $to_lines ) == sizeof( $mapped_to_lines )' );
parent::__construct( $mapped_from_lines, $mapped_to_lines );
@@ -1205,7 +1205,7 @@ class _HWLDF_WordAccumulator {
$this->_flushLine( $tag );
$word = substr( $word, 1 );
}
- assert( !strstr( $word, "\n" ) );
+ assert( '!strstr( $word, "\n" )' );
$this->_group .= $word;
}
}
diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php
index 439e3204..c7156fb2 100644
--- a/includes/diff/DifferenceEngine.php
+++ b/includes/diff/DifferenceEngine.php
@@ -1,6 +1,21 @@
<?php
/**
- * User interface for the difference engine
+ * User interface for the difference engine.
+ *
+ * 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
@@ -164,6 +179,22 @@ class DifferenceEngine extends ContextSource {
}
}
+ private function showMissingRevision() {
+ $out = $this->getOutput();
+
+ $missing = array();
+ if ( $this->mOldRev === null ) {
+ $missing[] = $this->deletedIdMarker( $this->mOldid );
+ }
+ if ( $this->mNewRev === null ) {
+ $missing[] = $this->deletedIdMarker( $this->mNewid );
+ }
+
+ $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
+ $out->addWikiMsg( 'difference-missing-revision',
+ $this->getLanguage()->listToText( $missing ), count( $missing ) );
+ }
+
function showDiffPage( $diffOnly = false ) {
wfProfileIn( __METHOD__ );
@@ -173,13 +204,7 @@ class DifferenceEngine extends ContextSource {
$out->setRobotPolicy( 'noindex,nofollow' );
if ( !$this->loadRevisionData() ) {
- // Sounds like a deleted revision... Let's see what we can do.
- $t = $this->getTitle()->getPrefixedText();
- $d = $this->msg( 'missingarticle-diff',
- $this->deletedIdMarker( $this->mOldid ),
- $this->deletedIdMarker( $this->mNewid ) )->escaped();
- $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
- $out->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", "<span class='plainlinks'>$d</span>" );
+ $this->showMissingRevision();
wfProfileOut( __METHOD__ );
return;
}
@@ -239,8 +264,7 @@ class DifferenceEngine extends ContextSource {
# a diff between a version V and its previous version V' AND the version V
# is the first version of that article. In that case, V' does not exist.
if ( $this->mOldRev === false ) {
- $out->setPageTitle( $this->mNewPage->getPrefixedText() );
- $out->addSubtitle( $this->msg( 'difference' ) );
+ $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
$samePage = true;
$oldHeader = '';
} else {
@@ -252,19 +276,19 @@ class DifferenceEngine extends ContextSource {
}
if ( $this->mNewPage->equals( $this->mOldPage ) ) {
- $out->setPageTitle( $this->mNewPage->getPrefixedText() );
- $out->addSubtitle( $this->msg( 'difference' ) );
+ $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
$samePage = true;
} else {
- $out->setPageTitle( $this->mOldPage->getPrefixedText() . ', ' . $this->mNewPage->getPrefixedText() );
+ $out->setPageTitle( $this->msg( 'difference-title-multipage', $this->mOldPage->getPrefixedText(),
+ $this->mNewPage->getPrefixedText() ) );
$out->addSubtitle( $this->msg( 'difference-multipage' ) );
$samePage = false;
}
- if ( $samePage && $this->mNewPage->userCan( 'edit', $user ) ) {
+ 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 );
+ $rollback = '&#160;&#160;&#160;' . Linker::generateRollback( $this->mNewRev, $this->getContext() );
}
if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
$undoLink = ' ' . $this->msg( 'parentheses' )->rawParams(
@@ -403,7 +427,7 @@ class DifferenceEngine extends ContextSource {
if ( $this->mMarkPatrolledLink === null ) {
// Prepare a change patrol link, if applicable
- if ( $wgUseRCPatrol && $this->mNewPage->userCan( 'patrol', $this->getUser() ) ) {
+ 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;
@@ -512,9 +536,7 @@ class DifferenceEngine extends ContextSource {
$wikiPage = WikiPage::factory( $this->mNewPage );
}
- $parserOptions = ParserOptions::newFromContext( $this->getContext() );
- $parserOptions->enableLimitReport();
- $parserOptions->setTidy( true );
+ $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
if ( !$this->mNewRev->isCurrent() ) {
$parserOptions->setEditSection( false );
@@ -543,7 +565,7 @@ class DifferenceEngine extends ContextSource {
function showDiff( $otitle, $ntitle, $notice = '' ) {
$diff = $this->getDiff( $otitle, $ntitle, $notice );
if ( $diff === false ) {
- $this->getOutput()->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
+ $this->showMissingRevision();
return false;
} else {
$this->showDiffStyle();
@@ -598,7 +620,7 @@ class DifferenceEngine extends ContextSource {
return false;
}
// Short-circuit
- // If mOldRev is false, it means that the
+ // If mOldRev is false, it means that the
if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
&& $this->mOldRev->getID() == $this->mNewRev->getID() ) )
{
@@ -672,6 +694,7 @@ class DifferenceEngine extends ContextSource {
*
* @param $otext String: old text, must be already segmented
* @param $ntext String: new text, must be already segmented
+ * @return bool|string
*/
function generateDiffBody( $otext, $ntext ) {
global $wgExternalDiffEngine, $wgContLang;
@@ -705,9 +728,9 @@ class DifferenceEngine extends ContextSource {
}
if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
# Diff via the shell
- global $wgTmpDirectory;
- $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
- $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
+ $tmpDir = wfTempDir();
+ $tempName1 = tempnam( $tmpDir, 'diff_' );
+ $tempName2 = tempnam( $tmpDir, 'diff_' );
$tempFile1 = fopen( $tempName1, "w" );
if ( !$tempFile1 ) {
@@ -747,6 +770,7 @@ class DifferenceEngine extends ContextSource {
/**
* Generate a debug comment indicating diff generating time,
* server node, and generator backend.
+ * @return string
*/
protected function debug( $generator = "internal" ) {
global $wgShowHostnames;
@@ -768,6 +792,7 @@ class DifferenceEngine extends ContextSource {
/**
* Replace line numbers with the text in the user's language
+ * @return mixed
*/
function localiseLineNumbers( $text ) {
return preg_replace_callback( '/<!--LINE (\d+)-->/',
@@ -864,8 +889,9 @@ class DifferenceEngine extends ContextSource {
$editQuery['oldid'] = $rev->getID();
}
- $msg = $this->msg( $title->userCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped();
- $header .= ' (' . Linker::linkKnown( $title, $msg, array(), $editQuery ) . ')';
+ $msg = $this->msg( $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped();
+ $header .= ' ' . $this->msg( 'parentheses' )->rawParams(
+ Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain();
if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header );
}
@@ -889,7 +915,7 @@ class DifferenceEngine extends ContextSource {
if ( !$diff && !$otitle ) {
$header .= "
- <tr valign='top'>
+ <tr style='vertical-align: top;'>
<td class='diff-ntitle'>{$ntitle}</td>
</tr>";
$multiColspan = 1;
@@ -907,17 +933,17 @@ class DifferenceEngine extends ContextSource {
$multiColspan = 2;
}
$header .= "
- <tr valign='top'>
+ <tr style='vertical-align: top;'>
<td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
<td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
</tr>";
}
if ( $multi != '' ) {
- $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>";
+ $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' class='diff-multi'>{$multi}</td></tr>";
}
if ( $notice != '' ) {
- $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>";
+ $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;'>{$notice}</td></tr>";
}
return $header . $diff . "</table>";
@@ -1002,7 +1028,7 @@ class DifferenceEngine extends ContextSource {
// Load the new revision object
$this->mNewRev = $this->mNewid
? Revision::newFromId( $this->mNewid )
- : Revision::newFromTitle( $this->getTitle() );
+ : Revision::newFromTitle( $this->getTitle(), false, Revision::READ_NORMAL );
if ( !$this->mNewRev instanceof Revision ) {
return false;
diff --git a/includes/filerepo/backend/FSFile.php b/includes/filebackend/FSFile.php
index 54dd1359..e07c99d4 100644
--- a/includes/filerepo/backend/FSFile.php
+++ b/includes/filebackend/FSFile.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Non-directory file on the file system.
+ *
+ * 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 FileBackend
*/
@@ -15,7 +32,8 @@ class FSFile {
/**
* Sets up the file object
*
- * @param String $path Path to temporary file on local disk
+ * @param $path string Path to temporary file on local disk
+ * @throws MWException
*/
public function __construct( $path ) {
if ( FileBackend::isStoragePath( $path ) ) {
@@ -45,7 +63,7 @@ class FSFile {
/**
* Get the file size in bytes
*
- * @return int|false
+ * @return int|bool
*/
public function getSize() {
return filesize( $this->path );
@@ -54,7 +72,7 @@ class FSFile {
/**
* Get the file's last-modified timestamp
*
- * @return string|false TS_MW timestamp or false on failure
+ * @return string|bool TS_MW timestamp or false on failure
*/
public function getTimestamp() {
wfSuppressWarnings();
@@ -152,6 +170,7 @@ class FSFile {
/**
* Exract image size information
*
+ * @param $gis array
* @return Array
*/
protected function extractImageSizeInfo( array $gis ) {
@@ -174,7 +193,7 @@ class FSFile {
* 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
* fairly neatly.
*
- * @return false|string False on failure
+ * @return bool|string False on failure
*/
public function getSha1Base36() {
wfProfileIn( __METHOD__ );
@@ -224,7 +243,7 @@ class FSFile {
*
* @param $path string
*
- * @return false|string False on failure
+ * @return bool|string False on failure
*/
public static function getSha1Base36FromPath( $path ) {
$fsFile = new self( $path );
diff --git a/includes/filebackend/FSFileBackend.php b/includes/filebackend/FSFileBackend.php
new file mode 100644
index 00000000..93495340
--- /dev/null
+++ b/includes/filebackend/FSFileBackend.php
@@ -0,0 +1,986 @@
+<?php
+/**
+ * File system based 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
+ * @ingroup FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for a file system (FS) based file backend.
+ *
+ * All "containers" each map to a directory under the backend's base directory.
+ * For backwards-compatibility, some container paths can be set to custom paths.
+ * The wiki ID will not be used in any custom paths, so this should be avoided.
+ *
+ * Having directories with thousands of files will diminish performance.
+ * Sharding can be accomplished by using FileRepo-style hash paths.
+ *
+ * Status messages should avoid mentioning the internal FS paths.
+ * PHP warnings are assumed to be logged rather than output.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+class FSFileBackend extends FileBackendStore {
+ protected $basePath; // string; directory holding the container directories
+ /** @var Array Map of container names to root paths */
+ protected $containerPaths = array(); // for custom container paths
+ protected $fileMode; // integer; file permission mode
+ protected $fileOwner; // string; required OS username to own files
+ protected $currentUser; // string; OS username running this script
+
+ protected $hadWarningErrors = array();
+
+ /**
+ * @see FileBackendStore::__construct()
+ * Additional $config params include:
+ * - basePath : File system directory that holds containers.
+ * - containerPaths : Map of container names to custom file system directories.
+ * This should only be used for backwards-compatibility.
+ * - fileMode : Octal UNIX file permissions to use on files stored.
+ */
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+
+ // Remove any possible trailing slash from directories
+ if ( isset( $config['basePath'] ) ) {
+ $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
+ } else {
+ $this->basePath = null; // none; containers must have explicit paths
+ }
+
+ if ( isset( $config['containerPaths'] ) ) {
+ $this->containerPaths = (array)$config['containerPaths'];
+ foreach ( $this->containerPaths as &$path ) {
+ $path = rtrim( $path, '/' ); // remove trailing slash
+ }
+ }
+
+ $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
+ if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
+ $this->fileOwner = $config['fileOwner'];
+ $info = posix_getpwuid( posix_getuid() );
+ $this->currentUser = $info['name']; // cache this, assuming it doesn't change
+ }
+ }
+
+ /**
+ * @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 ) ) {
+ // Check for sane relative paths (assume the base paths are OK)
+ if ( $this->isLegalRelPath( $relStoragePath ) ) {
+ return $relStoragePath;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sanity check a relative file system path for validity
+ *
+ * @param $path string Normalized relative path
+ * @return bool
+ */
+ protected function isLegalRelPath( $path ) {
+ // Check for file names longer than 255 chars
+ if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
+ return false;
+ }
+ if ( wfIsWindows() ) { // NTFS
+ return !preg_match( '![:*?"<>|]!', $path );
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * 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
+ * @return string|null
+ */
+ protected function containerFSRoot( $shortCont, $fullCont ) {
+ if ( isset( $this->containerPaths[$shortCont] ) ) {
+ return $this->containerPaths[$shortCont];
+ } elseif ( isset( $this->basePath ) ) {
+ return "{$this->basePath}/{$fullCont}";
+ }
+ return null; // no container base path defined
+ }
+
+ /**
+ * Get the absolute file system path for a storage path
+ *
+ * @param $storagePath string Storage path
+ * @return string|null
+ */
+ protected function resolveToFSPath( $storagePath ) {
+ list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
+ if ( $relPath === null ) {
+ return null; // invalid
+ }
+ list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath );
+ $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+ if ( $relPath != '' ) {
+ $fsPath .= "/{$relPath}";
+ }
+ return $fsPath;
+ }
+
+ /**
+ * @see FileBackendStore::isPathUsableInternal()
+ * @return bool
+ */
+ public function isPathUsableInternal( $storagePath ) {
+ $fsPath = $this->resolveToFSPath( $storagePath );
+ if ( $fsPath === null ) {
+ return false; // invalid
+ }
+ $parentDir = dirname( $fsPath );
+
+ if ( file_exists( $fsPath ) ) {
+ $ok = is_file( $fsPath ) && is_writable( $fsPath );
+ } else {
+ $ok = is_dir( $parentDir ) && is_writable( $parentDir );
+ }
+
+ if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
+ $ok = false;
+ trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
+ }
+
+ return $ok;
+ }
+
+ /**
+ * @see FileBackendStore::doStoreInternal()
+ * @return Status
+ */
+ 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 ( 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
+ $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
+ wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Store', $cmd, $dest );
+ } else { // immediate write
+ $ok = copy( $params['src'], $dest );
+ // In some cases (at least over NFS), copy() returns true when it fails
+ if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
+ if ( $ok ) { // PHP bug
+ unlink( $dest ); // remove broken file
+ trigger_error( __METHOD__ . ": copy() failed but returned true." );
+ }
+ $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+ return $status;
+ }
+ $this->chmod( $dest );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseStore( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doCopyInternal()
+ * @return Status
+ */
+ protected function doCopyInternal( array $params ) {
+ $status = Status::newGood();
+
+ $source = $this->resolveToFSPath( $params['src'] );
+ if ( $source === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ return $status;
+ }
+
+ $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
+ $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp',
+ wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd, $dest );
+ } else { // immediate write
+ $ok = copy( $source, $dest );
+ // In some cases (at least over NFS), copy() returns true when it fails
+ if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
+ if ( $ok ) { // PHP bug
+ unlink( $dest ); // remove broken file
+ trigger_error( __METHOD__ . ": copy() failed but returned true." );
+ }
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ return $status;
+ }
+ $this->chmod( $dest );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCopy( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doMoveInternal()
+ * @return Status
+ */
+ protected function doMoveInternal( array $params ) {
+ $status = Status::newGood();
+
+ $source = $this->resolveToFSPath( $params['src'] );
+ if ( $source === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ return $status;
+ }
+
+ $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'] ) ) {
+ // 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 ( !empty( $params['async'] ) ) { // deferred
+ $cmd = implode( ' ', array( wfIsWindows() ? 'MOVE' : 'mv',
+ wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
+ wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Move', $cmd );
+ } else { // immediate write
+ $ok = rename( $source, $dest );
+ clearstatcache(); // file no longer at source
+ if ( !$ok ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ return $status;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseMove( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doDeleteInternal()
+ * @return Status
+ */
+ protected function doDeleteInternal( array $params ) {
+ $status = Status::newGood();
+
+ $source = $this->resolveToFSPath( $params['src'] );
+ if ( $source === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ return $status;
+ }
+
+ if ( !is_file( $source ) ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ }
+ return $status; // do nothing; either OK or bad status
+ }
+
+ if ( !empty( $params['async'] ) ) { // deferred
+ $cmd = implode( ' ', array( wfIsWindows() ? 'DEL' : 'unlink',
+ wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
+ ) );
+ $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd );
+ } else { // immediate write
+ $ok = unlink( $source );
+ if ( !$ok ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ return $status;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FSFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseDelete( $errors, Status $status, array $params, $cmd ) {
+ if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+ }
+ }
+
+ /**
+ * @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'] );
+ $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
+ $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'] );
+ }
+ 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'] );
+ $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" ) ) {
+ $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
+ 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" ) ) {
+ $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
+ 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'] );
+ $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() );
+ if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
+ $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
+ return $status;
+ }
+ }
+ // 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() );
+ if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
+ $storeDir = "mwstore://{$this->name}/{$shortCont}";
+ $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
+ return $status;
+ }
+ }
+ 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'] );
+ $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+ $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+ wfSuppressWarnings();
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir ); // remove directory if empty
+ }
+ wfRestoreWarnings();
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doFileExists()
+ * @return array|bool|null
+ */
+ protected function doGetFileStat( array $params ) {
+ $source = $this->resolveToFSPath( $params['src'] );
+ if ( $source === null ) {
+ return false; // invalid storage path
+ }
+
+ $this->trapWarnings(); // don't trust 'false' if there were errors
+ $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
+ $hadError = $this->untrapWarnings();
+
+ if ( $stat ) {
+ return array(
+ 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
+ 'size' => $stat['size']
+ );
+ } elseif ( !$hadError ) {
+ return false; // file does not exist
+ } else {
+ return null; // failure
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doClearCache()
+ */
+ protected function doClearCache( array $paths = null ) {
+ 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'] );
+ $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+ $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+ $this->trapWarnings(); // don't trust 'false' if there were errors
+ $exists = is_dir( $dir );
+ $hadError = $this->untrapWarnings();
+
+ return $hadError ? null : $exists;
+ }
+
+ /**
+ * @see FileBackendStore::getDirectoryListInternal()
+ * @return Array|null
+ */
+ public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
+ list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+ $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+ $exists = is_dir( $dir );
+ if ( !$exists ) {
+ wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+ return array(); // nothing under this dir
+ } elseif ( !is_readable( $dir ) ) {
+ wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+ return null; // bad permissions?
+ }
+ return new FSFileBackendDirList( $dir, $params );
+ }
+
+ /**
+ * @see FileBackendStore::getFileListInternal()
+ * @return array|FSFileBackendFileList|null
+ */
+ public function getFileListInternal( $fullCont, $dirRel, array $params ) {
+ list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+ $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+ $exists = is_dir( $dir );
+ if ( !$exists ) {
+ wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+ return array(); // nothing under this dir
+ } elseif ( !is_readable( $dir ) ) {
+ wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+ return null; // bad permissions?
+ }
+ 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 );
+ }
+
+ /**
+ * @see FileBackendStore::getLocalCopy()
+ * @return null|TempFSFile
+ */
+ public function getLocalCopy( array $params ) {
+ $source = $this->resolveToFSPath( $params['src'] );
+ if ( $source === null ) {
+ return null;
+ }
+
+ // 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();
+
+ // Copy the source file over the temp file
+ $ok = copy( $source, $tmpPath );
+ if ( !$ok ) {
+ return null;
+ }
+
+ $this->chmod( $tmpPath );
+
+ return $tmpFile;
+ }
+
+ /**
+ * @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();
+
+ $pipes = array();
+ foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+ $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
+ }
+
+ $errs = array();
+ foreach ( $pipes as $index => $pipe ) {
+ // Result will be empty on success in *NIX. On Windows,
+ // it may be something like " 1 file(s) [copied|moved].".
+ $errs[$index] = stream_get_contents( $pipe );
+ fclose( $pipe );
+ }
+
+ foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+ $status = Status::newGood();
+ $function = '_getResponse' . $fileOpHandle->call;
+ $this->$function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
+ $statuses[$index] = $status;
+ if ( $status->isOK() && $fileOpHandle->chmodPath ) {
+ $this->chmod( $fileOpHandle->chmodPath );
+ }
+ }
+
+ clearstatcache(); // files changed
+ return $statuses;
+ }
+
+ /**
+ * Chmod a file, suppressing the warnings
+ *
+ * @param $path string Absolute file system path
+ * @return bool Success
+ */
+ protected function chmod( $path ) {
+ wfSuppressWarnings();
+ $ok = chmod( $path, $this->fileMode );
+ wfRestoreWarnings();
+
+ return $ok;
+ }
+
+ /**
+ * Return the text of an index.html file to hide directory listings
+ *
+ * @return string
+ */
+ protected function indexHtmlPrivate() {
+ return '';
+ }
+
+ /**
+ * Return the text of a .htaccess file to make a directory private
+ *
+ * @return string
+ */
+ protected function htaccessPrivate() {
+ return "Deny from all\n";
+ }
+
+ /**
+ * Clean up directory separators for the given OS
+ *
+ * @param $path string FS path
+ * @return string
+ */
+ protected function cleanPathSlashes( $path ) {
+ return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path;
+ }
+
+ /**
+ * Listen for E_WARNING errors and track whether any happen
+ *
+ * @return bool
+ */
+ protected function trapWarnings() {
+ $this->hadWarningErrors[] = false; // push to stack
+ set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
+ return false; // invoke normal PHP error handler
+ }
+
+ /**
+ * Stop listening for E_WARNING errors and return true if any happened
+ *
+ * @return bool
+ */
+ protected function untrapWarnings() {
+ restore_error_handler(); // restore previous handler
+ return array_pop( $this->hadWarningErrors ); // pop from stack
+ }
+
+ /**
+ * @return bool
+ */
+ private function handleWarning() {
+ $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
+ return true; // suppress from PHP handler
+ }
+}
+
+/**
+ * @see FileBackendStoreOpHandle
+ */
+class FSFileOpHandle extends FileBackendStoreOpHandle {
+ public $cmd; // string; shell command
+ public $chmodPath; // string; file to chmod
+
+ /**
+ * @param $backend
+ * @param $params array
+ * @param $call
+ * @param $cmd
+ * @param $chmodPath null
+ */
+ public function __construct( $backend, array $params, $call, $cmd, $chmodPath = null ) {
+ $this->backend = $backend;
+ $this->params = $params;
+ $this->call = $call;
+ $this->cmd = $cmd;
+ $this->chmodPath = $chmodPath;
+ }
+}
+
+/**
+ * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that
+ * catches exception or does any custom behavoir that we may want.
+ * Do not use this class from places outside FSFileBackend.
+ *
+ * @ingroup FileBackend
+ */
+abstract class FSFileBackendList implements Iterator {
+ /** @var Iterator */
+ protected $iter;
+ protected $suffixStart; // integer
+ protected $pos = 0; // integer
+ /** @var Array */
+ protected $params = array();
+
+ /**
+ * @param $dir string file system directory
+ * @param $params array
+ */
+ public function __construct( $dir, array $params ) {
+ $dir = realpath( $dir ); // normalize
+ $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
+ $this->params = $params;
+
+ try {
+ $this->iter = $this->initIterator( $dir );
+ } catch ( UnexpectedValueException $e ) {
+ $this->iter = null; // bad permissions? deleted?
+ }
+ }
+
+ /**
+ * Return an appropriate iterator object to wrap
+ *
+ * @param $dir string file system directory
+ * @return Iterator
+ */
+ protected function initIterator( $dir ) {
+ if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
+ # Get an iterator that will get direct sub-nodes
+ return new DirectoryIterator( $dir );
+ } else { // recursive
+ # Get an iterator that will return leaf nodes (non-directories)
+ # RecursiveDirectoryIterator extends FilesystemIterator.
+ # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
+ $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
+ return new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $dir, $flags ),
+ RecursiveIteratorIterator::CHILD_FIRST // include dirs
+ );
+ }
+ }
+
+ /**
+ * @see Iterator::key()
+ * @return integer
+ */
+ public function key() {
+ return $this->pos;
+ }
+
+ /**
+ * @see Iterator::current()
+ * @return string|bool String or false
+ */
+ public function current() {
+ return $this->getRelPath( $this->iter->current()->getPathname() );
+ }
+
+ /**
+ * @see Iterator::next()
+ * @return void
+ */
+ public function next() {
+ try {
+ $this->iter->next();
+ $this->filterViaNext();
+ } catch ( UnexpectedValueException $e ) {
+ $this->iter = null;
+ }
+ ++$this->pos;
+ }
+
+ /**
+ * @see Iterator::rewind()
+ * @return void
+ */
+ public function rewind() {
+ $this->pos = 0;
+ try {
+ $this->iter->rewind();
+ $this->filterViaNext();
+ } catch ( UnexpectedValueException $e ) {
+ $this->iter = null;
+ }
+ }
+
+ /**
+ * @see Iterator::valid()
+ * @return bool
+ */
+ public function valid() {
+ return $this->iter && $this->iter->valid();
+ }
+
+ /**
+ * Filter out items by advancing to the next ones
+ */
+ protected function filterViaNext() {}
+
+ /**
+ * 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
+ * @return string
+ */
+ protected function getRelPath( $path ) {
+ return strtr( substr( realpath( $path ), $this->suffixStart ), '\\', '/' );
+ }
+}
+
+class FSFileBackendDirList extends FSFileBackendList {
+ protected function filterViaNext() {
+ while ( $this->iter->valid() ) {
+ if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
+ $this->iter->next(); // skip non-directories and dot files
+ } else {
+ break;
+ }
+ }
+ }
+}
+
+class FSFileBackendFileList extends FSFileBackendList {
+ protected function filterViaNext() {
+ while ( $this->iter->valid() ) {
+ if ( !$this->iter->current()->isFile() ) {
+ $this->iter->next(); // skip non-files and dot files
+ } else {
+ break;
+ }
+ }
+ }
+}
diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php
new file mode 100644
index 00000000..76c761b0
--- /dev/null
+++ b/includes/filebackend/FileBackend.php
@@ -0,0 +1,1173 @@
+<?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.
+ */
+
+/**
+ * Base class for all file backends.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Base class for all file backend classes (including multi-write backends).
+ *
+ * This class defines the methods as abstract that subclasses must implement.
+ * 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.
+ *
+ * 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.
+ *
+ * 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
+ * store files in such a way that no files and directories are under the same path.
+ *
+ * Methods of subclasses should avoid throwing exceptions at all costs.
+ * As a corollary, external dependencies should be kept to a minimum.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+abstract class FileBackend {
+ protected $name; // string; unique backend name
+ protected $wikiId; // string; unique wiki name
+ protected $readOnly; // string; read-only explanation message
+ protected $parallelize; // string; when to do operations in parallel
+ protected $concurrency; // integer; how many operations can be done in parallel
+
+ /** @var LockManager */
+ protected $lockManager;
+ /** @var FileJournal */
+ protected $fileJournal;
+
+ /**
+ * Create a new backend instance from configuration.
+ * This should only be called from within FileBackendGroup.
+ *
+ * $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.
+ * It should only consist of alphanumberic, '-', and '_' characters.
+ * - 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.
+ * - readOnly : Write operations are disallowed if this is a non-empty string.
+ * It should be an explanation for the backend being read-only.
+ * - parallelize : When to do file operations in parallel (when possible).
+ * Allowed values are "implicit", "explicit" and "off".
+ * - concurrency : How many file operations can be done in parallel.
+ *
+ * @param $config Array
+ * @throws MWException
+ */
+ public function __construct( array $config ) {
+ $this->name = $config['name'];
+ if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
+ throw new MWException( "Backend name `{$this->name}` is invalid." );
+ }
+ $this->wikiId = isset( $config['wikiId'] )
+ ? $config['wikiId']
+ : wfWikiID(); // e.g. "my_wiki-en_"
+ $this->lockManager = ( $config['lockManager'] instanceof LockManager )
+ ? $config['lockManager']
+ : LockManagerGroup::singleton()->get( $config['lockManager'] );
+ $this->fileJournal = isset( $config['fileJournal'] )
+ ? ( ( $config['fileJournal'] instanceof FileJournal )
+ ? $config['fileJournal']
+ : FileJournal::factory( $config['fileJournal'], $this->name ) )
+ : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name );
+ $this->readOnly = isset( $config['readOnly'] )
+ ? (string)$config['readOnly']
+ : '';
+ $this->parallelize = isset( $config['parallelize'] )
+ ? (string)$config['parallelize']
+ : 'off';
+ $this->concurrency = isset( $config['concurrency'] )
+ ? (int)$config['concurrency']
+ : 50;
+ }
+
+ /**
+ * Get the unique backend name.
+ * We may have multiple different backends of the same type.
+ * For example, we can have two Swift backends using different proxies.
+ *
+ * @return string
+ */
+ final public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Get the wiki identifier used for this backend (possibly empty)
+ *
+ * @return string
+ * @since 1.20
+ */
+ final public function getWikiId() {
+ return $this->wikiId;
+ }
+
+ /**
+ * Check if this backend is read-only
+ *
+ * @return bool
+ */
+ final public function isReadOnly() {
+ return ( $this->readOnly != '' );
+ }
+
+ /**
+ * Get an explanatory message if this backend is read-only
+ *
+ * @return string|bool Returns false if the backend is not read-only
+ */
+ final public function getReadOnlyReason() {
+ return ( $this->readOnly != '' ) ? $this->readOnly : false;
+ }
+
+ /**
+ * This is the main entry point into the backend for write operations.
+ * Callers supply an ordered list of operations to perform as a transaction.
+ * Files will be locked, the stat cache cleared, and then the operations attempted.
+ * If any serious errors occur, all attempted operations will be rolled back.
+ *
+ * $ops is an array of arrays. The outer array holds a list of operations.
+ * Each inner array is a set of key value pairs that specify an operation.
+ *
+ * Supported operations and their parameters. The supported actions are:
+ * - create
+ * - store
+ * - copy
+ * - move
+ * - delete
+ * - null
+ *
+ * a) Create a new file in storage with the contents of a string
+ * @code
+ * array(
+ * 'op' => 'create',
+ * 'dst' => <storage path>,
+ * 'content' => <string of new file contents>,
+ * 'overwrite' => <boolean>,
+ * 'overwriteSame' => <boolean>,
+ * 'disposition' => <Content-Disposition header value>
+ * );
+ * @endcode
+ *
+ * b) Copy a file system file into storage
+ * @code
+ * array(
+ * 'op' => 'store',
+ * 'src' => <file system path>,
+ * 'dst' => <storage path>,
+ * 'overwrite' => <boolean>,
+ * 'overwriteSame' => <boolean>,
+ * 'disposition' => <Content-Disposition header value>
+ * )
+ * @endcode
+ *
+ * c) Copy a file within storage
+ * @code
+ * array(
+ * 'op' => 'copy',
+ * 'src' => <storage path>,
+ * 'dst' => <storage path>,
+ * 'overwrite' => <boolean>,
+ * 'overwriteSame' => <boolean>,
+ * 'disposition' => <Content-Disposition header value>
+ * )
+ * @endcode
+ *
+ * d) Move a file within storage
+ * @code
+ * array(
+ * 'op' => 'move',
+ * 'src' => <storage path>,
+ * 'dst' => <storage path>,
+ * 'overwrite' => <boolean>,
+ * 'overwriteSame' => <boolean>,
+ * 'disposition' => <Content-Disposition header value>
+ * )
+ * @endcode
+ *
+ * e) Delete a file within storage
+ * @code
+ * array(
+ * 'op' => 'delete',
+ * 'src' => <storage path>,
+ * 'ignoreMissingSource' => <boolean>
+ * )
+ * @endcode
+ *
+ * f) Do nothing (no-op)
+ * @code
+ * array(
+ * 'op' => 'null',
+ * )
+ * @endcode
+ *
+ * Boolean flags for operations (operation-specific):
+ * - 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).
+ *
+ * $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.
+ * - 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).
+ * - 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).
+ *
+ * @remarks Remarks on locking:
+ * File system paths given to operations should refer to files that are
+ * already locked or otherwise safe from modification from other processes.
+ * Normally these files will be new temp files, which should be adequate.
+ *
+ * @par Return value:
+ *
+ * This returns a Status, which contains all warnings and fatals that occurred
+ * during the operation. The 'failCount', 'successCount', and 'success' members
+ * will reflect each operation attempted.
+ *
+ * The status will be "OK" unless:
+ * - 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
+ * @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 ( 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;
+ }
+ }
+ return $this->doOperationsInternal( $ops, $opts );
+ }
+
+ /**
+ * @see FileBackend::doOperations()
+ */
+ abstract protected function doOperationsInternal( array $ops, array $opts );
+
+ /**
+ * Same as doOperations() except it takes a single operation.
+ * If you are doing a batch of operations that should either
+ * all succeed or all fail, then use that function instead.
+ *
+ * @see FileBackend::doOperations()
+ *
+ * @param $op Array Operation
+ * @param $opts Array Operation options
+ * @return Status
+ */
+ final public function doOperation( array $op, array $opts = array() ) {
+ return $this->doOperations( array( $op ), $opts );
+ }
+
+ /**
+ * Performs a single create operation.
+ * This sets $params['op'] to 'create' and passes it to doOperation().
+ *
+ * @see FileBackend::doOperation()
+ *
+ * @param $params Array Operation parameters
+ * @param $opts Array Operation options
+ * @return Status
+ */
+ final public function create( array $params, array $opts = array() ) {
+ return $this->doOperation( array( 'op' => 'create' ) + $params, $opts );
+ }
+
+ /**
+ * Performs a single store operation.
+ * This sets $params['op'] to 'store' and passes it to doOperation().
+ *
+ * @see FileBackend::doOperation()
+ *
+ * @param $params Array Operation parameters
+ * @param $opts Array Operation options
+ * @return Status
+ */
+ final public function store( array $params, array $opts = array() ) {
+ return $this->doOperation( array( 'op' => 'store' ) + $params, $opts );
+ }
+
+ /**
+ * Performs a single copy operation.
+ * This sets $params['op'] to 'copy' and passes it to doOperation().
+ *
+ * @see FileBackend::doOperation()
+ *
+ * @param $params Array Operation parameters
+ * @param $opts Array Operation options
+ * @return Status
+ */
+ final public function copy( array $params, array $opts = array() ) {
+ return $this->doOperation( array( 'op' => 'copy' ) + $params, $opts );
+ }
+
+ /**
+ * Performs a single move operation.
+ * This sets $params['op'] to 'move' and passes it to doOperation().
+ *
+ * @see FileBackend::doOperation()
+ *
+ * @param $params Array Operation parameters
+ * @param $opts Array Operation options
+ * @return Status
+ */
+ final public function move( array $params, array $opts = array() ) {
+ return $this->doOperation( array( 'op' => 'move' ) + $params, $opts );
+ }
+
+ /**
+ * Performs a single delete operation.
+ * This sets $params['op'] to 'delete' and passes it to doOperation().
+ *
+ * @see FileBackend::doOperation()
+ *
+ * @param $params Array Operation parameters
+ * @param $opts Array Operation options
+ * @return Status
+ */
+ final public function delete( array $params, array $opts = array() ) {
+ return $this->doOperation( array( 'op' => 'delete' ) + $params, $opts );
+ }
+
+ /**
+ * Perform a set of independent file operations on some files.
+ *
+ * This does no locking, nor journaling, and possibly no stat calls.
+ * Any destination files that already exist will be overwritten.
+ * This should *only* be used on non-original files, like cache files.
+ *
+ * Supported operations and their parameters:
+ * - create
+ * - store
+ * - copy
+ * - move
+ * - delete
+ * - null
+ *
+ * a) Create a new file in storage with the contents of a string
+ * @code
+ * array(
+ * 'op' => 'create',
+ * 'dst' => <storage path>,
+ * 'content' => <string of new file contents>,
+ * 'disposition' => <Content-Disposition header value>
+ * )
+ * @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>
+ * )
+ * @endcode
+ * c) Copy a file within storage
+ * @code
+ * array(
+ * 'op' => 'copy',
+ * 'src' => <storage path>,
+ * 'dst' => <storage path>,
+ * 'disposition' => <Content-Disposition header value>
+ * )
+ * @endcode
+ * d) Move a file within storage
+ * @code
+ * array(
+ * 'op' => 'move',
+ * 'src' => <storage path>,
+ * 'dst' => <storage path>,
+ * 'disposition' => <Content-Disposition header value>
+ * )
+ * @endcode
+ * e) Delete a file within storage
+ * @code
+ * array(
+ * 'op' => 'delete',
+ * 'src' => <storage path>,
+ * 'ignoreMissingSource' => <boolean>
+ * )
+ * @endcode
+ * f) Do nothing (no-op)
+ * @code
+ * array(
+ * 'op' => 'null',
+ * )
+ * @endcode
+ *
+ * @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).
+ *
+ * $opts is an associative of boolean flags, including:
+ * - bypassReadOnly : Allow writes in read-only mode (since 1.20)
+ *
+ * @par Return value:
+ * This returns a Status, which contains all warnings and fatals that occurred
+ * during the operation. The 'failCount', 'successCount', and 'success' members
+ * 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
+ * @return Status
+ * @since 1.20
+ */
+ final public function doQuickOperations( array $ops, array $opts = array() ) {
+ if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
+ return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
+ }
+ foreach ( $ops as &$op ) {
+ $op['overwrite'] = true; // avoids RTTs in key/value stores
+ }
+ return $this->doQuickOperationsInternal( $ops );
+ }
+
+ /**
+ * @see FileBackend::doQuickOperations()
+ * @since 1.20
+ */
+ abstract protected function doQuickOperationsInternal( array $ops );
+
+ /**
+ * Same as doQuickOperations() except it takes a single operation.
+ * If you are doing a batch of operations, then use that function instead.
+ *
+ * @see FileBackend::doQuickOperations()
+ *
+ * @param $op Array Operation
+ * @return Status
+ * @since 1.20
+ */
+ final public function doQuickOperation( array $op ) {
+ return $this->doQuickOperations( array( $op ) );
+ }
+
+ /**
+ * Performs a single quick create operation.
+ * This sets $params['op'] to 'create' and passes it to doQuickOperation().
+ *
+ * @see FileBackend::doQuickOperation()
+ *
+ * @param $params Array Operation parameters
+ * @return Status
+ * @since 1.20
+ */
+ final public function quickCreate( array $params ) {
+ return $this->doQuickOperation( array( 'op' => 'create' ) + $params );
+ }
+
+ /**
+ * Performs a single quick store operation.
+ * This sets $params['op'] to 'store' and passes it to doQuickOperation().
+ *
+ * @see FileBackend::doQuickOperation()
+ *
+ * @param $params Array Operation parameters
+ * @return Status
+ * @since 1.20
+ */
+ final public function quickStore( array $params ) {
+ return $this->doQuickOperation( array( 'op' => 'store' ) + $params );
+ }
+
+ /**
+ * Performs a single quick copy operation.
+ * This sets $params['op'] to 'copy' and passes it to doQuickOperation().
+ *
+ * @see FileBackend::doQuickOperation()
+ *
+ * @param $params Array Operation parameters
+ * @return Status
+ * @since 1.20
+ */
+ final public function quickCopy( array $params ) {
+ return $this->doQuickOperation( array( 'op' => 'copy' ) + $params );
+ }
+
+ /**
+ * Performs a single quick move operation.
+ * This sets $params['op'] to 'move' and passes it to doQuickOperation().
+ *
+ * @see FileBackend::doQuickOperation()
+ *
+ * @param $params Array Operation parameters
+ * @return Status
+ * @since 1.20
+ */
+ final public function quickMove( array $params ) {
+ return $this->doQuickOperation( array( 'op' => 'move' ) + $params );
+ }
+
+ /**
+ * Performs a single quick delete operation.
+ * This sets $params['op'] to 'delete' and passes it to doQuickOperation().
+ *
+ * @see FileBackend::doQuickOperation()
+ *
+ * @param $params Array Operation parameters
+ * @return Status
+ * @since 1.20
+ */
+ final public function quickDelete( array $params ) {
+ return $this->doQuickOperation( array( 'op' => 'delete' ) + $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
+ * $params include:
+ * - srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
+ * - dst : file system path to 0-byte temp file
+ * @return Status
+ */
+ abstract public function concatenate( array $params );
+
+ /**
+ * Prepare a storage directory for usage.
+ * This will create any required containers and parent directories.
+ * Backends using key/value stores only need to create the container.
+ *
+ * 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.
+ *
+ * @param $params Array
+ * $params include:
+ * - dir : storage directory
+ * - noAccess : try to deny file access (since 1.20)
+ * - noListing : try to deny file listing (since 1.20)
+ * - bypassReadOnly : allow writes in read-only mode (since 1.20)
+ * @return Status
+ */
+ final public function prepare( array $params ) {
+ if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+ return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
+ }
+ return $this->doPrepare( $params );
+ }
+
+ /**
+ * @see FileBackend::prepare()
+ */
+ abstract protected function doPrepare( array $params );
+
+ /**
+ * Take measures to block web access to a storage directory and
+ * 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
+ * $params include:
+ * - dir : storage directory
+ * - noAccess : try to deny file access
+ * - noListing : try to deny file listing
+ * - bypassReadOnly : allow writes in read-only mode (since 1.20)
+ * @return Status
+ */
+ final public function secure( array $params ) {
+ if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+ return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
+ }
+ return $this->doSecure( $params );
+ }
+
+ /**
+ * @see FileBackend::secure()
+ */
+ abstract protected function doSecure( array $params );
+
+ /**
+ * Remove measures to block web access to a storage directory and
+ * the container it belongs to. FS backends might remove .htaccess
+ * files whereas key/value store backends might grant container
+ * access to the storage user representing end-users in web requests.
+ * This essentially can undo the result of secure() calls.
+ *
+ * @param $params Array
+ * $params include:
+ * - dir : storage directory
+ * - access : try to allow file access
+ * - listing : try to allow file listing
+ * - bypassReadOnly : allow writes in read-only mode (since 1.20)
+ * @return Status
+ * @since 1.20
+ */
+ final public function publish( array $params ) {
+ if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+ return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
+ }
+ return $this->doPublish( $params );
+ }
+
+ /**
+ * @see FileBackend::publish()
+ */
+ abstract protected function doPublish( array $params );
+
+ /**
+ * Delete a storage directory if it is empty.
+ * 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
+ * $params include:
+ * - dir : storage directory
+ * - recursive : recursively delete empty subdirectories first (since 1.20)
+ * - bypassReadOnly : allow writes in read-only mode (since 1.20)
+ * @return Status
+ */
+ final public function clean( array $params ) {
+ if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+ return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
+ }
+ return $this->doClean( $params );
+ }
+
+ /**
+ * @see FileBackend::clean()
+ */
+ abstract protected function doClean( array $params );
+
+ /**
+ * 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
+ * $params include:
+ * - src : source storage path
+ * - latest : use the latest available data
+ * @return bool|null Returns null on failure
+ */
+ abstract public function fileExists( array $params );
+
+ /**
+ * Get the last-modified timestamp of the file at a storage path.
+ *
+ * @param $params Array
+ * $params include:
+ * - src : source storage path
+ * - latest : use the latest available data
+ * @return string|bool TS_MW timestamp or false on failure
+ */
+ abstract public function getFileTimestamp( array $params );
+
+ /**
+ * Get the contents of a file at a storage path in the backend.
+ * This should be avoided for potentially large files.
+ *
+ * @param $params Array
+ * $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 );
+
+ /**
+ * Get the size (bytes) of a file at a storage path in the backend.
+ *
+ * @param $params Array
+ * $params include:
+ * - src : source storage path
+ * - latest : use the latest available data
+ * @return integer|bool Returns false on failure
+ */
+ abstract public function getFileSize( array $params );
+
+ /**
+ * Get quick information about a file at a storage path in the backend.
+ * If the file does not exist, then this returns false.
+ * Otherwise, the result is an associative array that includes:
+ * - mtime : the last-modified timestamp (TS_MW)
+ * - size : the file size (bytes)
+ * Additional values may be included for internal use only.
+ *
+ * @param $params Array
+ * $params include:
+ * - src : source storage path
+ * - latest : use the latest available data
+ * @return Array|bool|null Returns null on failure
+ */
+ abstract public function getFileStat( array $params );
+
+ /**
+ * Get a SHA-1 hash of the file at a storage path in the backend.
+ *
+ * @param $params Array
+ * $params include:
+ * - src : source storage path
+ * - latest : use the latest available data
+ * @return string|bool Hash string or false on failure
+ */
+ abstract public function getFileSha1Base36( array $params );
+
+ /**
+ * Get the properties of the file at a storage path in the backend.
+ * Returns FSFile::placeholderProps() on failure.
+ *
+ * @param $params Array
+ * $params include:
+ * - src : source storage path
+ * - latest : use the latest available data
+ * @return Array
+ */
+ abstract public function getFileProps( array $params );
+
+ /**
+ * Stream the file at a storage path in the backend.
+ * If the file does not exists, an HTTP 404 error will be given.
+ * Appropriate HTTP headers (Status, Content-Type, Content-Length)
+ * will be sent if streaming began, while none will be sent otherwise.
+ * Implementations should flush the output buffer before sending data.
+ *
+ * @param $params Array
+ * $params include:
+ * - src : source storage path
+ * - headers : list of additional HTTP headers to send on success
+ * - latest : use the latest available data
+ * @return Status
+ */
+ abstract public function streamFile( array $params );
+
+ /**
+ * Returns a file system file, identical to the file at a storage path.
+ * The file returned is either:
+ * - a) A local copy of the file at a storage path in the backend.
+ * The temporary copy will have the same extension as the source.
+ * - b) An original of the file at a storage path in the backend.
+ * Temporary files may be purged when the file object falls out of scope.
+ *
+ * Write operations should *never* be done on this file as some backends
+ * may do internal tracking or may be instances of FileBackendMultiWrite.
+ * 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
+ * $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 );
+
+ /**
+ * 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
+ * $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 );
+
+ /**
+ * Check if a directory exists at a given storage path.
+ * Backends using key/value stores will check if the path is a
+ * virtual directory, meaning there are files under the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * @param $params array
+ * $params include:
+ * - dir : storage directory
+ * @return bool|null Returns null on failure
+ * @since 1.20
+ */
+ abstract public function directoryExists( array $params );
+
+ /**
+ * Get an iterator to list *all* directories under a storage directory.
+ * If the directory is of the form "mwstore://backend/container",
+ * then all directories in the container will be listed.
+ * If the directory is of form "mwstore://backend/container/dir",
+ * then all directories directly under that directory will be listed.
+ * Results will be storage directories relative to the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * @param $params array
+ * $params include:
+ * - dir : storage directory
+ * - topOnly : only return direct child dirs of the directory
+ * @return Traversable|Array|null Returns null on failure
+ * @since 1.20
+ */
+ abstract public function getDirectoryList( array $params );
+
+ /**
+ * Same as FileBackend::getDirectoryList() except only lists
+ * directories that are immediately under the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * @param $params array
+ * $params include:
+ * - dir : storage directory
+ * @return Traversable|Array|null Returns null on failure
+ * @since 1.20
+ */
+ final public function getTopDirectoryList( array $params ) {
+ return $this->getDirectoryList( array( 'topOnly' => true ) + $params );
+ }
+
+ /**
+ * Get an iterator to list *all* stored files under a storage directory.
+ * If the directory is of the form "mwstore://backend/container",
+ * then all files in the container will be listed.
+ * If the directory is of form "mwstore://backend/container/dir",
+ * then all files under that directory will be listed.
+ * Results will be storage paths relative to the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * @param $params array
+ * $params include:
+ * - dir : storage directory
+ * - topOnly : only return direct child files of the directory (since 1.20)
+ * @return Traversable|Array|null Returns null on failure
+ */
+ abstract public function getFileList( array $params );
+
+ /**
+ * Same as FileBackend::getFileList() except only lists
+ * files that are immediately under the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * @param $params array
+ * $params include:
+ * - dir : storage directory
+ * @return Traversable|Array|null Returns null on failure
+ * @since 1.20
+ */
+ final public function getTopFileList( array $params ) {
+ return $this->getFileList( array( 'topOnly' => true ) + $params );
+ }
+
+ /**
+ * 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
+ * @return void
+ */
+ public function preloadCache( array $paths ) {}
+
+ /**
+ * 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)
+ * @return void
+ */
+ public function clearCache( array $paths = null ) {}
+
+ /**
+ * Lock the files at the given storage paths in the backend.
+ * This will either lock all the files or none (on failure).
+ *
+ * Callers should consider using getScopedFileLocks() instead.
+ *
+ * @param $paths Array Storage paths
+ * @param $type integer LockManager::LOCK_* constant
+ * @return Status
+ */
+ final public function lockFiles( array $paths, $type ) {
+ 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
+ * @return Status
+ */
+ final public function unlockFiles( array $paths, $type ) {
+ return $this->lockManager->unlock( $paths, $type );
+ }
+
+ /**
+ * Lock the files at the given storage paths in the backend.
+ * This will either lock all the files or none (on failure).
+ * On failure, the status object will be updated with errors.
+ *
+ * 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
+ * @return ScopedLock|null Returns null on failure
+ */
+ final public function getScopedFileLocks( array $paths, $type, Status $status ) {
+ return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
+ }
+
+ /**
+ * Get an array of scoped locks needed for a batch of file operations.
+ *
+ * Normally, FileBackend::doOperations() handles locking, unless
+ * the 'nonLocking' param is passed in. This function is useful if you
+ * want the files to be locked for a broader scope than just when the
+ * files are changing. For example, if you need to update DB metadata,
+ * you may want to keep the files locked until finished.
+ *
+ * @see FileBackend::doOperations()
+ *
+ * @param $ops Array 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
+ */
+ abstract public function getScopedLocksForOps( array $ops, Status $status );
+
+ /**
+ * Get the root storage path of this backend.
+ * All container paths are "subdirectories" of this path.
+ *
+ * @return string Storage path
+ * @since 1.20
+ */
+ final public function getRootStoragePath() {
+ return "mwstore://{$this->name}";
+ }
+
+ /**
+ * Get the file journal object for this backend
+ *
+ * @return FileJournal
+ */
+ final public function getJournal() {
+ return $this->fileJournal;
+ }
+
+ /**
+ * Check if a given path is a "mwstore://" path.
+ * This does not do any further validation or any existence checks.
+ *
+ * @param $path string
+ * @return bool
+ */
+ final public static function isStoragePath( $path ) {
+ return ( strpos( $path, 'mwstore://' ) === 0 );
+ }
+
+ /**
+ * Split a storage path into a backend name, a container name,
+ * 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
+ * @return Array (backend, container, rel object) or (null, null, null)
+ */
+ final public static function splitStoragePath( $storagePath ) {
+ if ( self::isStoragePath( $storagePath ) ) {
+ // Remove the "mwstore://" prefix and split the path
+ $parts = explode( '/', substr( $storagePath, 10 ), 3 );
+ if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
+ if ( count( $parts ) == 3 ) {
+ return $parts; // e.g. "backend/container/path"
+ } else {
+ return array( $parts[0], $parts[1], '' ); // e.g. "backend/container"
+ }
+ }
+ }
+ return array( null, null, null );
+ }
+
+ /**
+ * 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
+ * @return string|null
+ */
+ final public static function normalizeStoragePath( $storagePath ) {
+ list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
+ if ( $relPath !== null ) { // must be for this backend
+ $relPath = self::normalizeContainerPath( $relPath );
+ if ( $relPath !== null ) {
+ return ( $relPath != '' )
+ ? "mwstore://{$backend}/{$container}/{$relPath}"
+ : "mwstore://{$backend}/{$container}";
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the parent storage directory of a storage path.
+ * This returns a path like "mwstore://backend/container",
+ * "mwstore://backend/container/...", or null if there is no parent.
+ *
+ * @param $storagePath string
+ * @return string|null
+ */
+ final public static function parentStoragePath( $storagePath ) {
+ $storagePath = dirname( $storagePath );
+ list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath );
+ return ( $rel === null ) ? null : $storagePath;
+ }
+
+ /**
+ * Get the final extension from a storage or FS path
+ *
+ * @param $path string
+ * @return string
+ */
+ final public static function extensionFromPath( $path ) {
+ $i = strrpos( $path, '.' );
+ return strtolower( $i ? substr( $path, $i + 1 ) : '' );
+ }
+
+ /**
+ * Check if a relative path has no directory traversals
+ *
+ * @param $path string
+ * @return bool
+ * @since 1.20
+ */
+ final public static function isPathTraversalFree( $path ) {
+ return ( self::normalizeContainerPath( $path ) !== null );
+ }
+
+ /**
+ * 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)
+ * @return string
+ * @since 1.20
+ */
+ final public static function makeContentDisposition( $type, $filename = '' ) {
+ $parts = array();
+
+ $type = strtolower( $type );
+ if ( !in_array( $type, array( 'inline', 'attachment' ) ) ) {
+ throw new MWException( "Invalid Content-Disposition type '$type'." );
+ }
+ $parts[] = $type;
+
+ if ( strlen( $filename ) ) {
+ $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
+ }
+
+ return implode( ';', $parts );
+ }
+
+ /**
+ * Validate and normalize a relative storage path.
+ * Null is returned if the path involves directory traversal.
+ * Traversal is insecure for FS backends and broken for others.
+ *
+ * This uses the same traversal protection as Title::secureAndSplit().
+ *
+ * @param $path string Storage path relative to a container
+ * @return string|null
+ */
+ final protected static function normalizeContainerPath( $path ) {
+ // Normalize directory separators
+ $path = strtr( $path, '\\', '/' );
+ // Collapse any consecutive directory separators
+ $path = preg_replace( '![/]{2,}!', '/', $path );
+ // Remove any leading directory separator
+ $path = ltrim( $path, '/' );
+ // Use the same traversal protection as Title::secureAndSplit()
+ if ( strpos( $path, '.' ) !== false ) {
+ if (
+ $path === '.' ||
+ $path === '..' ||
+ strpos( $path, './' ) === 0 ||
+ strpos( $path, '../' ) === 0 ||
+ strpos( $path, '/./' ) !== false ||
+ strpos( $path, '/../' ) !== false
+ ) {
+ return null;
+ }
+ }
+ return $path;
+ }
+}
diff --git a/includes/filerepo/backend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php
index 73815cfb..8bbc96d0 100644
--- a/includes/filerepo/backend/FileBackendGroup.php
+++ b/includes/filebackend/FileBackendGroup.php
@@ -1,5 +1,22 @@
<?php
/**
+ * File backend registration 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 FileBackend
* @author Aaron Schulz
@@ -21,7 +38,6 @@ class FileBackendGroup {
protected $backends = array();
protected function __construct() {}
- protected function __clone() {}
/**
* @return FileBackendGroup
@@ -36,7 +52,7 @@ class FileBackendGroup {
/**
* Destroy the singleton instance
- *
+ *
* @return void
*/
public static function destroySingleton() {
@@ -45,7 +61,7 @@ class FileBackendGroup {
/**
* Register file backends from the global variables
- *
+ *
* @return void
*/
protected function initFromGlobals() {
@@ -141,6 +157,21 @@ class FileBackendGroup {
}
/**
+ * Get the config array for a backend object with a given name
+ *
+ * @param $name string
+ * @return Array
+ * @throws MWException
+ */
+ public function config( $name ) {
+ if ( !isset( $this->backends[$name] ) ) {
+ throw new MWException( "No backend defined with the name `$name`." );
+ }
+ $class = $this->backends[$name]['class'];
+ return array( 'class' => $class ) + $this->backends[$name]['config'];
+ }
+
+ /**
* Get an appropriate backend object from a storage path
*
* @param $storagePath string
diff --git a/includes/filebackend/FileBackendMultiWrite.php b/includes/filebackend/FileBackendMultiWrite.php
new file mode 100644
index 00000000..4be03231
--- /dev/null
+++ b/includes/filebackend/FileBackendMultiWrite.php
@@ -0,0 +1,689 @@
+<?php
+/**
+ * Proxy backend that mirrors writes to several internal backends.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Proxy backend that mirrors writes to several internal backends.
+ *
+ * This class defines a multi-write backend. Multiple backends can be
+ * registered to this proxy backend and it will act as a single backend.
+ * Use this when all access to those backends is through this proxy backend.
+ * At least one of the backends must be declared the "master" backend.
+ *
+ * Only use this class when transitioning from one storage system to another.
+ *
+ * Read operations are only done on the 'master' backend for consistency.
+ * Write operations are performed on all backends, in the order defined.
+ * If an operation fails on one backend it will be rolled back from the others.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+class FileBackendMultiWrite extends FileBackend {
+ /** @var Array Prioritized list of FileBackendStore objects */
+ protected $backends = array(); // array of (backend index => backends)
+ protected $masterIndex = -1; // integer; index of master backend
+ protected $syncChecks = 0; // integer; bitfield
+ protected $autoResync = false; // boolean
+
+ /** @var Array */
+ protected $noPushDirConts = array();
+ protected $noPushQuickOps = false; // boolean
+
+ /* Possible internal backend consistency checks */
+ const CHECK_SIZE = 1;
+ const CHECK_TIME = 2;
+ const CHECK_SHA1 = 4;
+
+ /**
+ * Construct a proxy backend that consists of several internal backends.
+ * Locking, journaling, and read-only checks are handled by the proxy backend.
+ *
+ * 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:
+ * - class : The name of the backend class
+ * - isMultiMaster : This must be set for one backend.
+ * - template: : If given a backend name, this will use
+ * the config of that backend as a template.
+ * Values specified here take precedence.
+ * - syncChecks : Integer bitfield of internal backend sync checks to perform.
+ * Possible bits include the FileBackendMultiWrite::CHECK_* constants.
+ * There are constants for SIZE, TIME, and SHA1.
+ * The checks are done before allowing any file operations.
+ * - 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.
+ * - noPushQuickOps : (hack) Only apply doQuickOperations() to the master backend.
+ * - noPushDirConts : (hack) Only apply directory functions to the master backend.
+ *
+ * @param $config Array
+ * @throws MWException
+ */
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+ $this->syncChecks = isset( $config['syncChecks'] )
+ ? $config['syncChecks']
+ : self::CHECK_SIZE;
+ $this->autoResync = !empty( $config['autoResync'] );
+ $this->noPushQuickOps = isset( $config['noPushQuickOps'] )
+ ? $config['noPushQuickOps']
+ : false;
+ $this->noPushDirConts = isset( $config['noPushDirConts'] )
+ ? $config['noPushDirConts']
+ : array();
+ // Construct backends here rather than via registration
+ // to keep these backends hidden from outside the proxy.
+ $namesUsed = array();
+ foreach ( $config['backends'] as $index => $config ) {
+ if ( isset( $config['template'] ) ) {
+ // Config is just a modified version of a registered backend's.
+ // This should only be used when that config is used only by this backend.
+ $config = $config + FileBackendGroup::singleton()->config( $config['template'] );
+ }
+ $name = $config['name'];
+ if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
+ throw new MWException( "Two or more backends defined with the name $name." );
+ }
+ $namesUsed[$name] = 1;
+ // Alter certain sub-backend settings for sanity
+ unset( $config['readOnly'] ); // use proxy backend setting
+ unset( $config['fileJournal'] ); // use proxy backend journal
+ $config['wikiId'] = $this->wikiId; // use the proxy backend wiki ID
+ $config['lockManager'] = 'nullLockManager'; // lock under proxy backend
+ if ( !empty( $config['isMultiMaster'] ) ) {
+ if ( $this->masterIndex >= 0 ) {
+ throw new MWException( 'More than one master backend defined.' );
+ }
+ $this->masterIndex = $index; // this is the "master"
+ $config['fileJournal'] = $this->fileJournal; // log under proxy backend
+ }
+ // Create sub-backend object
+ if ( !isset( $config['class'] ) ) {
+ throw new MWException( 'No class given for a backend config.' );
+ }
+ $class = $config['class'];
+ $this->backends[$index] = new $class( $config );
+ }
+ if ( $this->masterIndex < 0 ) { // need backends and must have a master
+ throw new MWException( 'No master backend defined.' );
+ }
+ }
+
+ /**
+ * @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 );
+ if ( !$status->isOK() ) {
+ return $status; // abort
+ }
+ }
+ // Clear any cache entries (after locks acquired)
+ $this->clearCache();
+ $opts['preserveCache'] = true; // only locked files are cached
+ // Get the list of paths to read/write...
+ $relevantPaths = $this->fileStoragePathsForOps( $ops );
+ // Check if the paths are valid and accessible on all backends...
+ $status->merge( $this->accessibilityCheck( $relevantPaths ) );
+ if ( !$status->isOK() ) {
+ return $status; // abort
+ }
+ // Do a consistency check to see if the backends are consistent...
+ $syncStatus = $this->consistencyCheck( $relevantPaths );
+ if ( !$syncStatus->isOK() ) {
+ wfDebugLog( 'FileOperation', get_class( $this ) .
+ " failed sync check: " . FormatJson::encode( $relevantPaths ) );
+ // Try to resync the clone backends to the master on the spot...
+ if ( !$this->autoResync || !$this->resyncFiles( $relevantPaths )->isOK() ) {
+ $status->merge( $syncStatus );
+ return $status; // abort
+ }
+ }
+ // Actually attempt the operation batch on the master backend...
+ $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() ) ) {
+ foreach ( $this->backends as $index => $backend ) {
+ if ( $index !== $this->masterIndex ) { // not done already
+ $realOps = $this->substOpBatchPaths( $ops, $backend );
+ $status->merge( $backend->doOperations( $realOps, $opts ) );
+ }
+ }
+ }
+ // Make 'success', 'successCount', and 'failCount' fields reflect
+ // the overall operation, rather than all the batches for each backend.
+ // Do this by only using success values from the master backend's batch.
+ $status->success = $masterStatus->success;
+ $status->successCount = $masterStatus->successCount;
+ $status->failCount = $masterStatus->failCount;
+
+ return $status;
+ }
+
+ /**
+ * Check that a set of files are consistent across all internal backends
+ *
+ * @param $paths Array List of storage paths
+ * @return Status
+ */
+ public function consistencyCheck( array $paths ) {
+ $status = Status::newGood();
+ if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
+ return $status; // skip checks
+ }
+
+ $mBackend = $this->backends[$this->masterIndex];
+ foreach ( $paths as $path ) {
+ $params = array( 'src' => $path, 'latest' => true );
+ $mParams = $this->substOpPaths( $params, $mBackend );
+ // Stat the file on the 'master' backend
+ $mStat = $mBackend->getFileStat( $mParams );
+ if ( $this->syncChecks & self::CHECK_SHA1 ) {
+ $mSha1 = $mBackend->getFileSha1Base36( $mParams );
+ } else {
+ $mSha1 = false;
+ }
+ // Check if all clone backends agree with the master...
+ foreach ( $this->backends as $index => $cBackend ) {
+ if ( $index === $this->masterIndex ) {
+ continue; // master
+ }
+ $cParams = $this->substOpPaths( $params, $cBackend );
+ $cStat = $cBackend->getFileStat( $cParams );
+ if ( $mStat ) { // file is in master
+ if ( !$cStat ) { // file should exist
+ $status->fatal( 'backend-fail-synced', $path );
+ continue;
+ }
+ if ( $this->syncChecks & self::CHECK_SIZE ) {
+ if ( $cStat['size'] != $mStat['size'] ) { // wrong size
+ $status->fatal( 'backend-fail-synced', $path );
+ continue;
+ }
+ }
+ if ( $this->syncChecks & self::CHECK_TIME ) {
+ $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
+ $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
+ if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
+ $status->fatal( 'backend-fail-synced', $path );
+ continue;
+ }
+ }
+ if ( $this->syncChecks & self::CHECK_SHA1 ) {
+ if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
+ $status->fatal( 'backend-fail-synced', $path );
+ continue;
+ }
+ }
+ } else { // file is not in master
+ if ( $cStat ) { // file should not exist
+ $status->fatal( 'backend-fail-synced', $path );
+ }
+ }
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * Check that a set of file paths are usable across all internal backends
+ *
+ * @param $paths Array List of storage paths
+ * @return Status
+ */
+ public function accessibilityCheck( array $paths ) {
+ $status = Status::newGood();
+ if ( count( $this->backends ) <= 1 ) {
+ return $status; // skip checks
+ }
+
+ foreach ( $paths as $path ) {
+ foreach ( $this->backends as $backend ) {
+ $realPath = $this->substPaths( $path, $backend );
+ if ( !$backend->isPathUsableInternal( $realPath ) ) {
+ $status->fatal( 'backend-fail-usable', $path );
+ }
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * 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
+ * @return Status
+ */
+ public function resyncFiles( array $paths ) {
+ $status = Status::newGood();
+
+ $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 ) {
+ $status->fatal( 'backend-fail-internal', $this->name );
+ }
+ // Check of all clone backends agree with the master...
+ foreach ( $this->backends as $index => $cBackend ) {
+ if ( $index === $this->masterIndex ) {
+ continue; // master
+ }
+ $cPath = $this->substPaths( $path, $cBackend );
+ $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath ) );
+ if ( $mSha1 === $cSha1 ) {
+ // already synced; nothing to do
+ } elseif ( $mSha1 ) { // file is in master
+ $fsFile = $mBackend->getLocalReference( array( 'src' => $mPath ) );
+ $status->merge( $cBackend->quickStore(
+ array( 'src' => $fsFile->getPath(), 'dst' => $cPath )
+ ) );
+ } elseif ( $mExist === false ) { // file is not in master
+ $status->merge( $cBackend->quickDelete( array( 'src' => $cPath ) ) );
+ }
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * Get a list of file storage paths to read or write for a list of operations
+ *
+ * @param $ops Array 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'];
+ }
+ if ( isset( $op['srcs'] ) ) {
+ $paths = array_merge( $paths, $op['srcs'] );
+ }
+ if ( isset( $op['dst'] ) ) {
+ $paths[] = $op['dst'];
+ }
+ }
+ return 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
+ * @return Array
+ */
+ protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
+ $newOps = array(); // operations
+ foreach ( $ops as $op ) {
+ $newOp = $op; // operation
+ foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
+ if ( isset( $newOp[$par] ) ) { // string or array
+ $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
+ }
+ }
+ $newOps[] = $newOp;
+ }
+ return $newOps;
+ }
+
+ /**
+ * Same as substOpBatchPaths() but for a single operation
+ *
+ * @param $ops array File operation array
+ * @param $backend FileBackendStore
+ * @return Array
+ */
+ protected function substOpPaths( array $ops, FileBackendStore $backend ) {
+ $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
+ return $newOps[0];
+ }
+
+ /**
+ * 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
+ * @return Array|string
+ */
+ protected function substPaths( $paths, FileBackendStore $backend ) {
+ return preg_replace(
+ '!^mwstore://' . preg_quote( $this->name ) . '/!',
+ StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
+ $paths // string or array
+ );
+ }
+
+ /**
+ * Substitute the backend of internal storage paths with the proxy backend's name
+ *
+ * @param $paths Array|string List of paths or single string path
+ * @return Array|string
+ */
+ protected function unsubstPaths( $paths ) {
+ return preg_replace(
+ '!^mwstore://([^/]+)!',
+ StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
+ $paths // string or array
+ );
+ }
+
+ /**
+ * @see FileBackend::doQuickOperationsInternal()
+ * @return Status
+ */
+ protected function doQuickOperationsInternal( array $ops ) {
+ $status = Status::newGood();
+ // Do the operations on the master backend; setting Status fields...
+ $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
+ $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
+ $status->merge( $masterStatus );
+ // Propagate the operations to the clone backends...
+ if ( !$this->noPushQuickOps ) {
+ foreach ( $this->backends as $index => $backend ) {
+ if ( $index !== $this->masterIndex ) { // not done already
+ $realOps = $this->substOpBatchPaths( $ops, $backend );
+ $status->merge( $backend->doQuickOperations( $realOps ) );
+ }
+ }
+ }
+ // Make 'success', 'successCount', and 'failCount' fields reflect
+ // the overall operation, rather than all the batches for each backend.
+ // Do this by only using success values from the master backend's batch.
+ $status->success = $masterStatus->success;
+ $status->successCount = $masterStatus->successCount;
+ $status->failCount = $masterStatus->failCount;
+ return $status;
+ }
+
+ /**
+ * @param $path string 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 );
+ return !in_array( $shortCont, $this->noPushDirConts );
+ }
+
+ /**
+ * @see FileBackend::doPrepare()
+ * @return Status
+ */
+ protected function doPrepare( array $params ) {
+ $status = Status::newGood();
+ $replicate = $this->replicateContainerDirChanges( $params['dir'] );
+ foreach ( $this->backends as $index => $backend ) {
+ if ( $replicate || $index == $this->masterIndex ) {
+ $realParams = $this->substOpPaths( $params, $backend );
+ $status->merge( $backend->doPrepare( $realParams ) );
+ }
+ }
+ return $status;
+ }
+
+ /**
+ * @see FileBackend::doSecure()
+ * @param $params array
+ * @return Status
+ */
+ protected function doSecure( array $params ) {
+ $status = Status::newGood();
+ $replicate = $this->replicateContainerDirChanges( $params['dir'] );
+ foreach ( $this->backends as $index => $backend ) {
+ if ( $replicate || $index == $this->masterIndex ) {
+ $realParams = $this->substOpPaths( $params, $backend );
+ $status->merge( $backend->doSecure( $realParams ) );
+ }
+ }
+ return $status;
+ }
+
+ /**
+ * @see FileBackend::doPublish()
+ * @param $params array
+ * @return Status
+ */
+ protected function doPublish( array $params ) {
+ $status = Status::newGood();
+ $replicate = $this->replicateContainerDirChanges( $params['dir'] );
+ foreach ( $this->backends as $index => $backend ) {
+ if ( $replicate || $index == $this->masterIndex ) {
+ $realParams = $this->substOpPaths( $params, $backend );
+ $status->merge( $backend->doPublish( $realParams ) );
+ }
+ }
+ return $status;
+ }
+
+ /**
+ * @see FileBackend::doClean()
+ * @param $params array
+ * @return Status
+ */
+ protected function doClean( array $params ) {
+ $status = Status::newGood();
+ $replicate = $this->replicateContainerDirChanges( $params['dir'] );
+ foreach ( $this->backends as $index => $backend ) {
+ if ( $replicate || $index == $this->masterIndex ) {
+ $realParams = $this->substOpPaths( $params, $backend );
+ $status->merge( $backend->doClean( $realParams ) );
+ }
+ }
+ 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 ) {
+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+ return $this->backends[$this->masterIndex]->getFileContents( $realParams );
+ }
+
+ /**
+ * @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 ) {
+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+ return $this->backends[$this->masterIndex]->getLocalReference( $realParams );
+ }
+
+ /**
+ * @see FileBackend::getLocalCopy()
+ * @param $params array
+ * @return null|TempFSFile
+ */
+ public function getLocalCopy( array $params ) {
+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+ return $this->backends[$this->masterIndex]->getLocalCopy( $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;
+ $backend->clearCache( $realPaths );
+ }
+ }
+
+ /**
+ * @see FileBackend::getScopedLocksForOps()
+ */
+ public function getScopedLocksForOps( array $ops, Status $status ) {
+ $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $ops );
+ // 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 )
+ );
+ }
+}
diff --git a/includes/filebackend/FileBackendStore.php b/includes/filebackend/FileBackendStore.php
new file mode 100644
index 00000000..083dfea9
--- /dev/null
+++ b/includes/filebackend/FileBackendStore.php
@@ -0,0 +1,1766 @@
+<?php
+/**
+ * Base class for all backends using particular storage 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Base class for all backends using particular storage medium.
+ *
+ * This class defines the methods as abstract that subclasses must implement.
+ * Outside callers should *not* use functions with "Internal" in the name.
+ *
+ * The FileBackend operations are implemented using basic functions
+ * such as storeInternal(), copyInternal(), deleteInternal() and the like.
+ * This class is also responsible for path resolution and sanitization.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+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 Array Map of container names to sharding settings */
+ protected $shardViaHashLevels = array(); // (container name => config array)
+
+ protected $maxFileSize = 4294967296; // integer bytes (4GiB)
+
+ /**
+ * @see FileBackend::__construct()
+ *
+ * @param $config Array
+ */
+ 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 );
+ }
+
+ /**
+ * Get the maximum allowable file size given backend
+ * medium restrictions and basic performance constraints.
+ * Do not call this function from places outside FileBackend and FileOp.
+ *
+ * @return integer Bytes
+ */
+ final public function maxFileSizeInternal() {
+ return $this->maxFileSize;
+ }
+
+ /**
+ * 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.
+ * Backends using key/value stores should check if the container exists.
+ *
+ * @param $storagePath string
+ * @return bool
+ */
+ abstract public function isPathUsableInternal( $storagePath );
+
+ /**
+ * Create a file in the backend with the given contents.
+ * 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.
+ *
+ * @param $params Array
+ * @return Status
+ */
+ final public function createInternal( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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
+ $this->deleteFileCache( $params['dst'] ); // persistent cache
+ }
+ }
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::createInternal()
+ */
+ abstract protected function doCreateInternal( array $params );
+
+ /**
+ * Store a file into the backend from a file on disk.
+ * 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.
+ *
+ * @param $params Array
+ * @return Status
+ */
+ final public function storeInternal( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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
+ $this->deleteFileCache( $params['dst'] ); // persistent cache
+ }
+ }
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::storeInternal()
+ */
+ abstract protected function doStoreInternal( array $params );
+
+ /**
+ * Copy a file from one storage path to another in the backend.
+ * 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.
+ *
+ * @param $params Array
+ * @return Status
+ */
+ final public function copyInternal( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+ $status = $this->doCopyInternal( $params );
+ $this->clearCache( array( $params['dst'] ) );
+ if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
+ $this->deleteFileCache( $params['dst'] ); // persistent cache
+ }
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::copyInternal()
+ */
+ abstract protected function doCopyInternal( array $params );
+
+ /**
+ * Delete a file at the storage path.
+ * Do not call this function from places outside FileBackend and FileOp.
+ *
+ * $params include:
+ * - src : source storage path
+ * - ignoreMissingSource : do nothing if the source file does not exist
+ * - 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 $params Array
+ * @return Status
+ */
+ final public function deleteInternal( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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()
+ */
+ abstract protected function doDeleteInternal( array $params );
+
+ /**
+ * Move a file from one storage path to another in the backend.
+ * 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.
+ *
+ * @param $params Array
+ * @return Status
+ */
+ final public function moveInternal( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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
+ $this->deleteFileCache( $params['dst'] ); // persistent cache
+ }
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::moveInternal()
+ * @return Status
+ */
+ protected function doMoveInternal( array $params ) {
+ unset( $params['async'] ); // two steps, won't work here :)
+ // Copy source to dest
+ $status = $this->copyInternal( $params );
+ if ( $status->isOK() ) {
+ // Delete source (only fails due to races or medium going down)
+ $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
+ $status->setResult( true, $status->value ); // ignore delete() errors
+ }
+ return $status;
+ }
+
+ /**
+ * No-op file operation that does nothing.
+ * Do not call this function from places outside FileBackend and FileOp.
+ *
+ * @param $params Array
+ * @return Status
+ */
+ final public function nullInternal( array $params ) {
+ return Status::newGood();
+ }
+
+ /**
+ * @see FileBackend::concatenate()
+ * @return Status
+ */
+ final public function concatenate( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+ $status = Status::newGood();
+
+ // Try to lock the source files for the scope of this function
+ $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
+ if ( $status->isOK() ) {
+ // Actually do the file concatenation...
+ $start_time = microtime( true );
+ $status->merge( $this->doConcatenate( $params ) );
+ $sec = microtime( true ) - $start_time;
+ if ( !$status->isOK() ) {
+ wfDebugLog( 'FileOperation', get_class( $this ) . " failed to concatenate " .
+ count( $params['srcs'] ) . " file(s) [$sec sec]" );
+ }
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::concatenate()
+ * @return Status
+ */
+ protected function doConcatenate( array $params ) {
+ $status = Status::newGood();
+ $tmpPath = $params['dst']; // convenience
+
+ // Check that the specified temp file is valid...
+ wfSuppressWarnings();
+ $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) );
+ 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)...
+ $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;
+ }
+ // Get a handle to the local FS version
+ $sourceHandle = fopen( $tmpFile->getPath(), 'r' );
+ if ( $sourceHandle === false ) {
+ fclose( $tmpHandle );
+ $status->fatal( 'backend-fail-read', $virtualSource );
+ return $status;
+ }
+ // Append chunk to file (pass chunk size to avoid magic quotes)
+ if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
+ fclose( $sourceHandle );
+ fclose( $tmpHandle );
+ $status->fatal( 'backend-fail-writetemp', $tmpPath );
+ return $status;
+ }
+ fclose( $sourceHandle );
+ }
+ if ( !fclose( $tmpHandle ) ) {
+ $status->fatal( 'backend-fail-closetemp', $tmpPath );
+ return $status;
+ }
+
+ clearstatcache(); // temp file changed
+
+ return $status;
+ }
+
+ /**
+ * @see FileBackend::doPrepare()
+ * @return Status
+ */
+ final protected function doPrepare( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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
+ }
+
+ if ( $shard !== null ) { // confined to a single container/shard
+ $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'] );
+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+ $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+ }
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doPrepare()
+ * @return Status
+ */
+ protected function doPrepareInternal( $container, $dir, array $params ) {
+ return Status::newGood();
+ }
+
+ /**
+ * @see FileBackend::doSecure()
+ * @return Status
+ */
+ final protected function doSecure( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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
+ }
+
+ if ( $shard !== null ) { // confined to a single container/shard
+ $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'] );
+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+ $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+ }
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doSecure()
+ * @return Status
+ */
+ protected function doSecureInternal( $container, $dir, array $params ) {
+ return Status::newGood();
+ }
+
+ /**
+ * @see FileBackend::doPublish()
+ * @return Status
+ */
+ final protected function doPublish( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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
+ }
+
+ if ( $shard !== null ) { // confined to a single container/shard
+ $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'] );
+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+ $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+ }
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doPublish()
+ * @return Status
+ */
+ protected function doPublishInternal( $container, $dir, array $params ) {
+ return Status::newGood();
+ }
+
+ /**
+ * @see FileBackend::doClean()
+ * @return Status
+ */
+ final protected function doClean( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+ $status = Status::newGood();
+
+ // Recursive: first delete all empty subdirs recursively
+ if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
+ $subDirsRel = $this->getTopDirectoryList( array( 'dir' => $params['dir'] ) );
+ if ( $subDirsRel !== null ) { // no errors
+ foreach ( $subDirsRel as $subDirRel ) {
+ $subDir = $params['dir'] . "/{$subDirRel}"; // full path
+ $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
+ }
+ }
+ }
+
+ 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
+ }
+
+ // Attempt to lock this directory...
+ $filesLockEx = array( $params['dir'] );
+ $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+ if ( !$status->isOK() ) {
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status; // abort
+ }
+
+ if ( $shard !== null ) { // confined to a single container/shard
+ $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
+ $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'] );
+ 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;
+ }
+
+ /**
+ * @see FileBackendStore::doClean()
+ * @return Status
+ */
+ protected function doCleanInternal( $container, $dir, array $params ) {
+ return Status::newGood();
+ }
+
+ /**
+ * @see FileBackend::fileExists()
+ * @return bool|null
+ */
+ final public function fileExists( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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 );
+ $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 );
+ $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 );
+ $latest = !empty( $params['latest'] ); // use latest data?
+ if ( !$this->cheapCache->has( $path, 'stat' ) ) {
+ $this->primeFileCache( array( $path ) ); // check persistent cache
+ }
+ if ( $this->cheapCache->has( $path, 'stat' ) ) {
+ $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;
+ }
+ }
+ wfProfileIn( __METHOD__ . '-miss' );
+ wfProfileIn( __METHOD__ . '-miss-' . $this->name );
+ $stat = $this->doGetFileStat( $params );
+ wfProfileOut( __METHOD__ . '-miss-' . $this->name );
+ wfProfileOut( __METHOD__ . '-miss' );
+ if ( is_array( $stat ) ) { // don't cache negatives
+ $stat['latest'] = $latest;
+ $this->cheapCache->set( $path, 'stat', $stat );
+ $this->setFileCache( $path, $stat ); // update persistent cache
+ if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
+ $this->cheapCache->set( $path, 'sha1',
+ array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
+ }
+ } else {
+ wfDebug( __METHOD__ . ": File $path does not exist.\n" );
+ }
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $stat;
+ }
+
+ /**
+ * @see FileBackendStore::getFileStat()
+ */
+ abstract protected function doGetFileStat( array $params );
+
+ /**
+ * @see FileBackend::getFileContents()
+ * @return bool|string
+ */
+ 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;
+ }
+ wfSuppressWarnings();
+ $data = file_get_contents( $tmpFile->getPath() );
+ wfRestoreWarnings();
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $data;
+ }
+
+ /**
+ * @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 );
+ $latest = !empty( $params['latest'] ); // use latest data?
+ if ( $this->cheapCache->has( $path, 'sha1' ) ) {
+ $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'];
+ }
+ }
+ wfProfileIn( __METHOD__ . '-miss' );
+ wfProfileIn( __METHOD__ . '-miss-' . $this->name );
+ $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__ );
+ return $hash;
+ }
+
+ /**
+ * @see FileBackendStore::getFileSha1Base36()
+ * @return bool|string
+ */
+ protected function doGetFileSha1Base36( array $params ) {
+ $fsFile = $this->getLocalReference( $params );
+ if ( !$fsFile ) {
+ return false;
+ } else {
+ return $fsFile->getSha1Base36();
+ }
+ }
+
+ /**
+ * @see FileBackend::getFileProps()
+ * @return Array
+ */
+ final public function getFileProps( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __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 );
+ $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'];
+ }
+ }
+ $tmpFile = $this->getLocalCopy( $params );
+ if ( $tmpFile ) { // don't cache negatives
+ $this->expensiveCache->set( $path, 'localRef',
+ array( 'object' => $tmpFile, 'latest' => $latest ) );
+ }
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $tmpFile;
+ }
+
+ /**
+ * @see FileBackend::streamFile()
+ * @return Status
+ */
+ final public function streamFile( array $params ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+ $status = Status::newGood();
+
+ $info = $this->getFileStat( $params );
+ if ( !$info ) { // let StreamFile handle the 404
+ $status->fatal( 'backend-fail-notexists', $params['src'] );
+ }
+
+ // Set output buffer and HTTP headers for stream
+ $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array();
+ $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
+ if ( $res == StreamFile::NOT_MODIFIED ) {
+ // do nothing; client cache is up to date
+ } elseif ( $res == StreamFile::READY_STREAM ) {
+ wfProfileIn( __METHOD__ . '-send' );
+ wfProfileIn( __METHOD__ . '-send-' . $this->name );
+ $status = $this->doStreamFile( $params );
+ wfProfileOut( __METHOD__ . '-send-' . $this->name );
+ wfProfileOut( __METHOD__ . '-send' );
+ } else {
+ $status->fatal( 'backend-fail-stream', $params['src'] );
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::streamFile()
+ * @return Status
+ */
+ protected function doStreamFile( array $params ) {
+ $status = Status::newGood();
+
+ $fsFile = $this->getLocalReference( $params );
+ if ( !$fsFile ) {
+ $status->fatal( 'backend-fail-stream', $params['src'] );
+ } elseif ( !readfile( $fsFile->getPath() ) ) {
+ $status->fatal( 'backend-fail-stream', $params['src'] );
+ }
+
+ 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 ) {
+ return false; // invalid storage path
+ }
+ if ( $shard !== null ) { // confined to a single container/shard
+ 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'] );
+ $res = false; // response
+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+ $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
+ if ( $exists ) {
+ $res = true;
+ break; // found one!
+ } elseif ( $exists === null ) { // error?
+ $res = null; // if we don't find anything, it is indeterminate
+ }
+ }
+ return $res;
+ }
+ }
+
+ /**
+ * @see FileBackendStore::directoryExists()
+ *
+ * @param $container string Resolved container name
+ * @param $dir string Resolved path relative to container
+ * @param $params Array
+ * @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
+ return null;
+ }
+ if ( $shard !== null ) {
+ // File listing is confined to a single container/shard
+ return $this->getDirectoryListInternal( $fullCont, $dir, $params );
+ } else {
+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+ // File listing spans multiple containers/shards
+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ return new FileBackendStoreShardDirIterator( $this,
+ $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
+ }
+ }
+
+ /**
+ * Do not call this function from places outside FileBackend
+ *
+ * @see FileBackendStore::getDirectoryList()
+ *
+ * @param $container string Resolved container name
+ * @param $dir string Resolved path relative to container
+ * @param $params Array
+ * @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
+ return null;
+ }
+ if ( $shard !== null ) {
+ // File listing is confined to a single container/shard
+ return $this->getFileListInternal( $fullCont, $dir, $params );
+ } else {
+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+ // File listing spans multiple containers/shards
+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ return new FileBackendStoreShardFileIterator( $this,
+ $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
+ }
+ }
+
+ /**
+ * Do not call this function from places outside FileBackend
+ *
+ * @see FileBackendStore::getFileList()
+ *
+ * @param $container string Resolved container name
+ * @param $dir string Resolved path relative to container
+ * @param $params Array
+ * @return Traversable|Array|null Returns null on failure
+ */
+ abstract public function getFileListInternal( $container, $dir, array $params );
+
+ /**
+ * Return a list of FileOp objects from a list of operations.
+ * Do not call this function from places outside 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()
+ * @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'
+ );
+
+ $performOps = array(); // array of FileOp objects
+ // Build up ordered array of FileOps...
+ foreach ( $ops as $operation ) {
+ $opName = $operation['op'];
+ if ( isset( $supportedOps[$opName] ) ) {
+ $class = $supportedOps[$opName];
+ // Get params for this operation
+ $params = $operation;
+ // Append the FileOp class
+ $performOps[] = new $class( $this, $params );
+ } else {
+ throw new MWException( "Operation '$opName' is not supported." );
+ }
+ }
+
+ return $performOps;
+ }
+
+ /**
+ * 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.
+ *
+ * @param $performOps Array List of FileOp objects
+ * @return Array ('sh' => list of paths, 'ex' => list of paths)
+ */
+ final public function getPathsToLockForOpsInternal( array $performOps ) {
+ // Build up a list of files to lock...
+ $paths = array( 'sh' => array(), 'ex' => array() );
+ foreach ( $performOps as $fileOp ) {
+ $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
+ $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
+ }
+ // Optimization: if doing an EX lock anyway, don't also set an SH one
+ $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
+ // 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;
+ }
+
+ /**
+ * @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 )
+ );
+ }
+
+ /**
+ * @see FileBackend::doOperationsInternal()
+ * @return Status
+ */
+ final protected function doOperationsInternal( array $ops, array $opts ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+ $status = Status::newGood();
+
+ // Build up a list of FileOps...
+ $performOps = $this->getOperationsInternal( $ops );
+
+ // Acquire any locks as needed...
+ if ( empty( $opts['nonLocking'] ) ) {
+ // 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 );
+ if ( !$status->isOK() ) {
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status; // abort
+ }
+ }
+
+ // Clear any file cache entries (after locks acquired)
+ if ( empty( $opts['preserveCache'] ) ) {
+ $this->clearCache();
+ }
+
+ // Load from the persistent file and container caches
+ $this->primeFileCache( $performOps );
+ $this->primeContainerCache( $performOps );
+
+ // Actually attempt the operation batch...
+ $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 );
+ $status = Status::newGood();
+
+ $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' );
+ $async = ( $this->parallelize === 'implicit' );
+ $maxConcurrency = $this->concurrency; // throttle
+
+ $statuses = array(); // array of (index => Status)
+ $fileOpHandles = array(); // list of (index => handle) arrays
+ $curFileOpHandles = array(); // current handle batch
+ // 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"
+ $subStatus = $this->$method( array( 'async' => $async ) + $params );
+ if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
+ if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
+ $fileOpHandles[] = $curFileOpHandles; // push this batch
+ $curFileOpHandles = array();
+ }
+ $curFileOpHandles[$index] = $subStatus->value; // keep index
+ } else { // error or completed
+ $statuses[$index] = $subStatus; // keep index
+ }
+ }
+ if ( count( $curFileOpHandles ) ) {
+ $fileOpHandles[] = $curFileOpHandles; // last batch
+ }
+ // Do all the async ops that can be done concurrently...
+ foreach ( $fileOpHandles as $fileHandleBatch ) {
+ $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
+ }
+ // Marshall and merge all the responses...
+ foreach ( $statuses as $index => $subStatus ) {
+ $status->merge( $subStatus );
+ if ( $subStatus->isOK() ) {
+ $status->success[$index] = true;
+ ++$status->successCount;
+ } else {
+ $status->success[$index] = false;
+ ++$status->failCount;
+ }
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * Execute a list of FileBackendStoreOpHandle handles in parallel.
+ * The resulting Status object fields will correspond
+ * to the order in which the handles where given.
+ *
+ * @param $handles Array List of FileBackendStoreOpHandle objects
+ * @return Array Map of Status objects
+ * @throws MWException
+ */
+ final public function executeOpHandlesInternal( array $fileOpHandles ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+ foreach ( $fileOpHandles as $fileOpHandle ) {
+ if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
+ throw new MWException( "Given a non-FileBackendStoreOpHandle object." );
+ } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
+ throw new MWException( "Given a FileBackendStoreOpHandle for the wrong backend." );
+ }
+ }
+ $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
+ foreach ( $fileOpHandles as $fileOpHandle ) {
+ $fileOpHandle->closeResources();
+ }
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ return $res;
+ }
+
+ /**
+ * @see FileBackendStore::executeOpHandlesInternal()
+ * @return Array List of corresponding Status objects
+ */
+ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+ foreach ( $fileOpHandles as $fileOpHandle ) { // OK if empty
+ throw new MWException( "This backend supports no asynchronous operations." );
+ }
+ return array();
+ }
+
+ /**
+ * @see FileBackend::preloadCache()
+ */
+ final public function preloadCache( array $paths ) {
+ $fullConts = array(); // full container names
+ foreach ( $paths as $path ) {
+ list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+ $fullConts[] = $fullCont;
+ }
+ // Load from the persistent file and container caches
+ $this->primeContainerCache( $fullConts );
+ $this->primeFileCache( $paths );
+ }
+
+ /**
+ * @see FileBackend::clearCache()
+ */
+ final public function clearCache( array $paths = null ) {
+ if ( is_array( $paths ) ) {
+ $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+ $paths = array_filter( $paths, 'strlen' ); // remove nulls
+ }
+ if ( $paths === null ) {
+ $this->cheapCache->clear();
+ $this->expensiveCache->clear();
+ } else {
+ foreach ( $paths as $path ) {
+ $this->cheapCache->clear( $path );
+ $this->expensiveCache->clear( $path );
+ }
+ }
+ $this->doClearCache( $paths );
+ }
+
+ /**
+ * Clears any additional stat caches for storage paths
+ *
+ * @see FileBackend::clearCache()
+ *
+ * @param $paths Array Storage paths (optional)
+ * @return void
+ */
+ protected function doClearCache( array $paths = null ) {}
+
+ /**
+ * Is this a key/value store where directories are just virtual?
+ * Virtual directories exists in so much as files exists that are
+ * prefixed with the directory path followed by a forward slash.
+ *
+ * @return bool
+ */
+ abstract protected function directoriesAreVirtual();
+
+ /**
+ * Check if a container name is valid.
+ * This checks for for length and illegal characters.
+ *
+ * @param $container string
+ * @return bool
+ */
+ final protected static function isValidContainerName( $container ) {
+ // This accounts for Swift and S3 restrictions while leaving room
+ // for things like '.xxx' (hex shard chars) or '.seg' (segments).
+ // This disallows directory separators or traversal characters.
+ // Note that matching strings URL encode to the same string;
+ // in Swift, the length restriction is *after* URL encoding.
+ return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
+ }
+
+ /**
+ * Splits a storage path into an internal container name,
+ * an internal relative file name, and a container shard suffix.
+ * Any shard suffix is already appended to the internal container name.
+ * This also checks that the storage path is valid and within this backend.
+ *
+ * If the container is sharded but a suffix could not be determined,
+ * 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
+ * @return Array (container, path, container suffix) or (null, null, null) if invalid
+ */
+ final protected function resolveStoragePath( $storagePath ) {
+ list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
+ if ( $backend === $this->name ) { // must be for this backend
+ $relPath = self::normalizeContainerPath( $relPath );
+ if ( $relPath !== null ) {
+ // Get shard for the normalized path if this container is sharded
+ $cShard = $this->getContainerShard( $container, $relPath );
+ // Validate and sanitize the relative path (backend-specific)
+ $relPath = $this->resolveContainerPath( $container, $relPath );
+ if ( $relPath !== null ) {
+ // Prepend any wiki ID prefix to the container name
+ $container = $this->fullContainerName( $container );
+ if ( self::isValidContainerName( $container ) ) {
+ // Validate and sanitize the container name (backend-specific)
+ $container = $this->resolveContainerName( "{$container}{$cShard}" );
+ if ( $container !== null ) {
+ return array( $container, $relPath, $cShard );
+ }
+ }
+ }
+ }
+ }
+ return array( null, null, null );
+ }
+
+ /**
+ * Like resolveStoragePath() except null values are returned if
+ * the container is sharded and the shard could not be determined.
+ *
+ * @see FileBackendStore::resolveStoragePath()
+ *
+ * @param $storagePath string
+ * @return Array (container, path) or (null, null) if invalid
+ */
+ final protected function resolveStoragePathReal( $storagePath ) {
+ list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
+ if ( $cShard !== null ) {
+ return array( $container, $relPath );
+ }
+ return array( null, null );
+ }
+
+ /**
+ * 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
+ * @return string|null Returns null if shard could not be determined
+ */
+ final protected function getContainerShard( $container, $relPath ) {
+ list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
+ if ( $levels == 1 || $levels == 2 ) {
+ // Hash characters are either base 16 or 36
+ $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
+ // Get a regex that represents the shard portion of paths.
+ // The concatenation of the captures gives us the shard.
+ if ( $levels === 1 ) { // 16 or 36 shards per container
+ $hashDirRegex = '(' . $char . ')';
+ } else { // 256 or 1296 shards per container
+ if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
+ $hashDirRegex = $char . '/(' . $char . '{2})';
+ } else { // short hash dir format (e.g. "a/b/c")
+ $hashDirRegex = '(' . $char . ')/(' . $char . ')';
+ }
+ }
+ // Allow certain directories to be above the hash dirs so as
+ // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
+ // They must be 2+ chars to avoid any hash directory ambiguity.
+ $m = array();
+ if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
+ return '.' . implode( '', array_slice( $m, 1 ) );
+ }
+ return null; // failed to match
+ }
+ return ''; // no sharding
+ }
+
+ /**
+ * Check if a storage path maps to a single shard.
+ * 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
+ * @return bool
+ */
+ final public function isSingleShardPathInternal( $storagePath ) {
+ list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
+ return ( $shard !== null );
+ }
+
+ /**
+ * Get the sharding config for a container.
+ * If greater than 0, then all file storage paths within
+ * the container are required to be hashed accordingly.
+ *
+ * @param $container string
+ * @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
+ */
+ final protected function getContainerHashLevels( $container ) {
+ if ( isset( $this->shardViaHashLevels[$container] ) ) {
+ $config = $this->shardViaHashLevels[$container];
+ $hashLevels = (int)$config['levels'];
+ if ( $hashLevels == 1 || $hashLevels == 2 ) {
+ $hashBase = (int)$config['base'];
+ if ( $hashBase == 16 || $hashBase == 36 ) {
+ return array( $hashLevels, $hashBase, $config['repeat'] );
+ }
+ }
+ }
+ return array( 0, 0, false ); // no sharding
+ }
+
+ /**
+ * Get a list of full container shard suffixes for a container
+ *
+ * @param $container string
+ * @return Array
+ */
+ final protected function getContainerSuffixes( $container ) {
+ $shards = array();
+ list( $digits, $base ) = $this->getContainerHashLevels( $container );
+ if ( $digits > 0 ) {
+ $numShards = pow( $base, $digits );
+ for ( $index = 0; $index < $numShards; $index++ ) {
+ $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
+ }
+ }
+ return $shards;
+ }
+
+ /**
+ * Get the full container name, including the wiki ID prefix
+ *
+ * @param $container string
+ * @return string
+ */
+ final protected function fullContainerName( $container ) {
+ if ( $this->wikiId != '' ) {
+ return "{$this->wikiId}-$container";
+ } else {
+ return $container;
+ }
+ }
+
+ /**
+ * Resolve a container name, checking if it's allowed by the backend.
+ * This is intended for internal use, such as encoding illegal chars.
+ * Subclasses can override this to be more restrictive.
+ *
+ * @param $container string
+ * @return string|null
+ */
+ protected function resolveContainerName( $container ) {
+ return $container;
+ }
+
+ /**
+ * Resolve a relative storage path, checking if it's allowed by the backend.
+ * This is intended for internal use, such as encoding illegal chars or perhaps
+ * 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
+ * @return string|null Path or null if not valid
+ */
+ protected function resolveContainerPath( $container, $relStoragePath ) {
+ return $relStoragePath;
+ }
+
+ /**
+ * Get the cache key for a container
+ *
+ * @param $container string Resolved container name
+ * @return string
+ */
+ private function containerCacheKey( $container ) {
+ return wfMemcKey( 'backend', $this->getName(), 'container', $container );
+ }
+
+ /**
+ * Set the cached info for a container
+ *
+ * @param $container string Resolved container name
+ * @param $val mixed Information to cache
+ */
+ final protected function setContainerCache( $container, $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
+ */
+ final protected function deleteContainerCache( $container ) {
+ if ( !$this->memCache->set( $this->containerCacheKey( $container ), 'PURGED', 300 ) ) {
+ trigger_error( "Unable to delete stat cache for container $container." );
+ }
+ }
+
+ /**
+ * Do a batch lookup from cache for container stats for all containers
+ * used in a list of container names, storage paths, or FileOp objects.
+ *
+ * @param $items Array
+ * @return void
+ */
+ final protected function primeContainerCache( array $items ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+
+ $paths = array(); // list of storage paths
+ $contNames = array(); // (cache key => resolved container name)
+ // Get all the paths/containers from the items...
+ foreach ( $items as $item ) {
+ if ( $item instanceof FileOp ) {
+ $paths = array_merge( $paths, $item->storagePathsRead() );
+ $paths = array_merge( $paths, $item->storagePathsChanged() );
+ } elseif ( self::isStoragePath( $item ) ) {
+ $paths[] = $item;
+ } elseif ( is_string( $item ) ) { // full container name
+ $contNames[$this->containerCacheKey( $item )] = $item;
+ }
+ }
+ // Get all the corresponding cache keys for paths...
+ foreach ( $paths as $path ) {
+ list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+ if ( $fullCont !== null ) { // valid path for this backend
+ $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
+ }
+ }
+
+ $contInfo = array(); // (resolved container name => cache value)
+ // Get all cache entries for these container cache keys...
+ $values = $this->memCache->getMulti( array_keys( $contNames ) );
+ foreach ( $values as $cacheKey => $val ) {
+ $contInfo[$contNames[$cacheKey]] = $val;
+ }
+
+ // Populate the container process cache for the backend...
+ $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Fill the backend-specific process cache given an array of
+ * 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
+ * @return void
+ */
+ protected function doPrimeContainerCache( array $containerInfo ) {}
+
+ /**
+ * Get the cache key for a file path
+ *
+ * @param $path string Storage path
+ * @return string
+ */
+ private function fileCacheKey( $path ) {
+ return wfMemcKey( 'backend', $this->getName(), 'file', sha1( $path ) );
+ }
+
+ /**
+ * Set the cached stat info for a file path.
+ * 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
+ */
+ final protected function setFileCache( $path, $val ) {
+ $this->memCache->add( $this->fileCacheKey( $path ), $val, 7*86400 );
+ }
+
+ /**
+ * Delete the cached stat info for a file path.
+ * The cache key is salted for a while to prevent race conditions.
+ *
+ * @param $path string Storage path
+ */
+ final protected function deleteFileCache( $path ) {
+ if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) {
+ trigger_error( "Unable to delete stat cache for file $path." );
+ }
+ }
+
+ /**
+ * Do a batch lookup from cache for file stats for all paths
+ * used in a list of storage paths or FileOp objects.
+ *
+ * @param $items Array List of storage paths or FileOps
+ * @return void
+ */
+ final protected function primeFileCache( array $items ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $this->name );
+
+ $paths = array(); // list of storage paths
+ $pathNames = array(); // (cache key => storage path)
+ // Get all the paths/containers from the items...
+ foreach ( $items as $item ) {
+ if ( $item instanceof FileOp ) {
+ $paths = array_merge( $paths, $item->storagePathsRead() );
+ $paths = array_merge( $paths, $item->storagePathsChanged() );
+ } elseif ( self::isStoragePath( $item ) ) {
+ $paths[] = $item;
+ }
+ }
+ // Get all the corresponding cache keys for paths...
+ foreach ( $paths as $path ) {
+ list( $cont, $rel, $s ) = $this->resolveStoragePath( $path );
+ if ( $rel !== null ) { // valid path for this backend
+ $pathNames[$this->fileCacheKey( $path )] = $path;
+ }
+ }
+ // Get all cache entries for these container cache keys...
+ $values = $this->memCache->getMulti( array_keys( $pathNames ) );
+ foreach ( $values as $cacheKey => $val ) {
+ if ( is_array( $val ) ) {
+ $path = $pathNames[$cacheKey];
+ $this->cheapCache->set( $path, 'stat', $val );
+ if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
+ $this->cheapCache->set( $path, 'sha1',
+ array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) );
+ }
+ }
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ wfProfileOut( __METHOD__ );
+ }
+}
+
+/**
+ * FileBackendStore helper class for performing asynchronous file operations.
+ *
+ * For example, calling FileBackendStore::createInternal() with the "async"
+ * param flag may result in a Status that contains this object as a value.
+ * This class is largely backend-specific and is mostly just "magic" to be
+ * passed to FileBackendStore::executeOpHandlesInternal().
+ */
+abstract class FileBackendStoreOpHandle {
+ /** @var Array */
+ public $params = array(); // params to caller functions
+ /** @var FileBackendStore */
+ public $backend;
+ /** @var Array */
+ public $resourcesToClose = array();
+
+ public $call; // string; name that identifies the function called
+
+ /**
+ * Close all open file handles
+ *
+ * @return void
+ */
+ public function closeResources() {
+ array_map( 'fclose', $this->resourcesToClose );
+ }
+}
+
+/**
+ * FileBackendStore helper function to handle listings that span container shards.
+ * Do not use this class from places outside of FileBackendStore.
+ *
+ * @ingroup FileBackend
+ */
+abstract class FileBackendStoreShardListIterator implements Iterator {
+ /** @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
+ */
+ public function __construct(
+ FileBackendStore $backend, $container, $dir, array $suffixes, array $params
+ ) {
+ $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
+ }
+ 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 );
+ }
+
+ /**
+ * @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();
+ }
+ }
+
+ /**
+ * 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 );
+ }
+ }
+
+ /**
+ * 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
+ */
+ abstract protected function listFromShard( $container, $dir, array $params );
+}
+
+/**
+ * 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 );
+ }
+}
+
+/**
+ * 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 );
+ }
+}
diff --git a/includes/filerepo/backend/FileOp.php b/includes/filebackend/FileOp.php
index 5844c9f2..7c43c489 100644
--- a/includes/filerepo/backend/FileOp.php
+++ b/includes/filebackend/FileOp.php
@@ -1,17 +1,35 @@
<?php
/**
+ * Helper class for representing operations with transaction support.
+ *
+ * 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 FileBackend
* @author Aaron Schulz
*/
/**
- * Helper class for representing operations with transaction support.
+ * FileBackend helper class for representing operations.
* Do not use this class from places outside FileBackend.
*
- * Methods called from attemptBatch() should avoid throwing exceptions at all costs.
- * FileOp objects should be lightweight in order to support large arrays in memory.
- *
+ * Methods called from FileOpBatch::attempt() should avoid throwing
+ * exceptions at all costs. FileOp objects should be lightweight in order
+ * to support large arrays in memory and serialization.
+ *
* @ingroup FileBackend
* @since 1.19
*/
@@ -23,7 +41,9 @@ 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 $sourceSha1; // string
protected $destSameAsSource; // boolean
@@ -33,15 +53,11 @@ abstract class FileOp {
const STATE_CHECKED = 2;
const STATE_ATTEMPTED = 3;
- /* Timeout related parameters */
- const MAX_BATCH_SIZE = 1000;
- const TIME_LIMIT_SEC = 300; // 5 minutes
-
/**
* Build a new file operation transaction
*
- * @params $backend FileBackendStore
- * @params $params Array
+ * @param $backend FileBackendStore
+ * @param $params Array
* @throws MWException
*/
final public function __construct( FileBackendStore $backend, array $params ) {
@@ -63,101 +79,28 @@ abstract class FileOp {
}
/**
- * Allow stale data for file reads and existence checks
+ * Set the batch UUID this operation belongs to
*
+ * @param $batchId string
* @return void
*/
- final protected function allowStaleReads() {
- $this->useLatest = false;
+ final public function setBatchId( $batchId ) {
+ $this->batchId = $batchId;
}
/**
- * Attempt a series of file operations.
- * Callers are responsible for handling file locking.
- *
- * $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.
+ * Whether to allow stale data for file reads and stat checks
*
- * The resulting Status will be "OK" unless:
- * a) unexpected operation errors occurred (network partitions, disk full...)
- * b) significant operation errors occured and 'force' was not set
- *
- * @param $performOps Array List of FileOp operations
- * @param $opts Array Batch operation options
- * @return Status
- */
- final public static function attemptBatch( array $performOps, array $opts ) {
- $status = Status::newGood();
-
- $allowStale = !empty( $opts['allowStale'] );
- $ignoreErrors = !empty( $opts['force'] );
-
- $n = count( $performOps );
- if ( $n > self::MAX_BATCH_SIZE ) {
- $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
- return $status;
- }
-
- $predicates = FileOp::newPredicates(); // account for previous op in prechecks
- // Do pre-checks for each operation; abort on failure...
- foreach ( $performOps as $index => $fileOp ) {
- if ( $allowStale ) {
- $fileOp->allowStaleReads(); // allow potentially stale reads
- }
- $subStatus = $fileOp->precheck( $predicates );
- $status->merge( $subStatus );
- if ( !$subStatus->isOK() ) { // operation failed?
- $status->success[$index] = false;
- ++$status->failCount;
- if ( !$ignoreErrors ) {
- return $status; // abort
- }
- }
- }
-
- if ( $ignoreErrors ) {
- # Treat all precheck() fatals as merely warnings
- $status->setResult( true, $status->value );
- }
-
- // Restart PHP's execution timer and set the timeout to safe amount.
- // This handles cases where the operations take a long time or where we are
- // already running low on time left. The old timeout is restored afterwards.
- # @TODO: re-enable this for when the number of batches is high.
- #$scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC );
-
- // Attempt each operation...
- 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 $status; // bail out
- }
- }
-
- return $status;
+ * @param $allowStale bool
+ * @return void
+ */
+ final public function allowStaleReads( $allowStale ) {
+ $this->useLatest = !$allowStale;
}
/**
* Get the value of the parameter with the given name
- *
+ *
* @param $name string
* @return mixed Returns null if the parameter is not set
*/
@@ -167,8 +110,8 @@ abstract class FileOp {
/**
* Check if this operation failed precheck() or attempt()
- *
- * @return bool
+ *
+ * @return bool
*/
final public function failed() {
return $this->failed;
@@ -177,13 +120,91 @@ abstract class FileOp {
/**
* Get a new empty predicates array for precheck()
*
- * @return Array
+ * @return Array
*/
final public static function newPredicates() {
return array( 'exists' => array(), 'sha1' => array() );
}
/**
+ * Get a new empty dependency tracking array for paths read/written to
+ *
+ * @return Array
+ */
+ final public static function newDependencies() {
+ return array( 'read' => array(), 'write' => array() );
+ }
+
+ /**
+ * Update a dependency tracking array to account for this operation
+ *
+ * @param $deps Array 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['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
+ return $deps;
+ }
+
+ /**
+ * Check if this operation changes files listed in $paths
+ *
+ * @param $paths Array Prior path reads/writes; format of FileOp::newPredicates()
+ * @return boolean
+ */
+ final public function dependsOn( array $deps ) {
+ foreach ( $this->storagePathsChanged() as $path ) {
+ if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
+ return true; // "output" or "anti" dependency
+ }
+ }
+ foreach ( $this->storagePathsRead() as $path ) {
+ if ( isset( $deps['write'][$path] ) ) {
+ return true; // "flow" dependency
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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)
+ * @return Array
+ */
+ final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
+ $nullEntries = array();
+ $updateEntries = array();
+ $deleteEntries = array();
+ $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
+ foreach ( $pathsUsed as $path ) {
+ $nullEntries[] = array( // assertion for recovery
+ '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,
+ 'newSha1' => ''
+ );
+ } else { // created/updated
+ $updateEntries[] = array(
+ 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
+ 'path' => $path,
+ 'newSha1' => $nPredicates['sha1'][$path]
+ );
+ }
+ }
+ return array_merge( $nullEntries, $updateEntries, $deleteEntries );
+ }
+
+ /**
* Check preconditions of the operation without writing anything
*
* @param $predicates Array
@@ -202,7 +223,14 @@ abstract class FileOp {
}
/**
- * Attempt the operation, backing up files as needed; this must be reversible
+ * @return Status
+ */
+ protected function doPrecheck( array &$predicates ) {
+ return Status::newGood();
+ }
+
+ /**
+ * Attempt the operation
*
* @return Status
*/
@@ -222,8 +250,27 @@ abstract class FileOp {
}
/**
+ * @return Status
+ */
+ protected function doAttempt() {
+ return Status::newGood();
+ }
+
+ /**
+ * Attempt the operation in the background
+ *
+ * @return Status
+ */
+ final public function attemptAsync() {
+ $this->async = true;
+ $result = $this->attempt();
+ $this->async = false;
+ return $result;
+ }
+
+ /**
* Get the file operation parameters
- *
+ *
* @return Array (required params list, optional params list)
*/
protected function allowedParams() {
@@ -231,42 +278,54 @@ abstract class FileOp {
}
/**
+ * Adjust params to FileBackendStore internal file calls
+ *
+ * @param $params Array
+ * @return Array (required params list, optional params list)
+ */
+ protected function setFlags( array $params ) {
+ return array( 'async' => $this->async ) + $params;
+ }
+
+ /**
* Get a list of storage paths read from for this operation
*
* @return Array
*/
- public function storagePathsRead() {
- return array();
+ final public function storagePathsRead() {
+ return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() );
}
/**
- * Get a list of storage paths written to for this operation
- *
+ * @see FileOp::storagePathsRead()
* @return Array
*/
- public function storagePathsChanged() {
+ protected function doStoragePathsRead() {
return array();
}
/**
- * @return Status
+ * Get a list of storage paths written to for this operation
+ *
+ * @return Array
*/
- protected function doPrecheck( array &$predicates ) {
- return Status::newGood();
+ final public function storagePathsChanged() {
+ return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() );
}
/**
- * @return Status
+ * @see FileOp::storagePathsChanged()
+ * @return Array
*/
- protected function doAttempt() {
- return Status::newGood();
+ protected function doStoragePathsChanged() {
+ return array();
}
/**
* Check for errors with regards to the destination file already existing.
* This also updates the destSameAsSource and sourceSha1 member variables.
* A bad status will be returned if there is no chance it can be overwritten.
- *
+ *
* @param $predicates Array
* @return Status
*/
@@ -305,7 +364,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.
*
- * @return string|false Returns false on failure
+ * @return string|bool Returns false on failure
*/
protected function getSourceSha1Base36() {
return null; // N/A
@@ -313,10 +372,10 @@ abstract class FileOp {
/**
* Check if a file will exist in storage when this operation is attempted
- *
+ *
* @param $source string Storage path
* @param $predicates Array
- * @return bool
+ * @return bool
*/
final protected function fileExists( $source, array $predicates ) {
if ( isset( $predicates['exists'][$source] ) ) {
@@ -329,10 +388,10 @@ 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
- * @return string|false
+ * @return string|bool False on failure
*/
final protected function fileSha1( $source, array $predicates ) {
if ( isset( $predicates['sha1'][$source] ) ) {
@@ -344,17 +403,26 @@ abstract class FileOp {
}
/**
+ * Get the backend this operation is for
+ *
+ * @return FileBackendStore
+ */
+ public function getBackend() {
+ return $this->backend;
+ }
+
+ /**
* Log a file operation failure and preserve any temp files
- *
+ *
* @param $action string
* @return void
*/
- final protected function logFailure( $action ) {
+ final public function logFailure( $action ) {
$params = $this->params;
$params['failedAction'] = $action;
try {
- wfDebugLog( 'FileOperation',
- get_class( $this ) . ' failed:' . serialize( $params ) );
+ wfDebugLog( 'FileOperation', get_class( $this ) .
+ " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
} catch ( Exception $e ) {
// bad config? debug log error?
}
@@ -362,75 +430,22 @@ abstract class FileOp {
}
/**
- * FileOp helper class to expand PHP execution time for a function.
- * On construction, set_time_limit() is called and set to $seconds.
- * When the object goes out of scope, the timer is restarted, with
- * the original time limit minus the time the object existed.
+ * Store a file into the backend from a file on the file system.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
*/
-class FileOpScopedPHPTimeout {
- protected $startTime; // float; seconds
- protected $oldTimeout; // integer; seconds
-
- protected static $stackDepth = 0; // integer
- protected static $totalCalls = 0; // integer
- protected static $totalElapsed = 0; // float; seconds
-
- /* Prevent callers in infinite loops from running forever */
- const MAX_TOTAL_CALLS = 1000000;
- const MAX_TOTAL_TIME = 300; // seconds
-
+class StoreFileOp extends FileOp {
/**
- * @param $seconds integer
+ * @return array
*/
- public function __construct( $seconds ) {
- if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0
- if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) {
- trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." );
- } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) {
- trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." );
- } elseif ( self::$stackDepth > 0 ) { // recursion guard
- trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." );
- } else {
- $this->oldTimeout = ini_set( 'max_execution_time', $seconds );
- $this->startTime = microtime( true );
- ++self::$stackDepth;
- ++self::$totalCalls; // proof against < 1us scopes
- }
- }
+ protected function allowedParams() {
+ return array( array( 'src', 'dst' ),
+ array( 'overwrite', 'overwriteSame', 'disposition' ) );
}
/**
- * Restore the original timeout.
- * This does not account for the timer value on __construct().
+ * @param $predicates array
+ * @return Status
*/
- public function __destruct() {
- if ( $this->oldTimeout ) {
- $elapsed = microtime( true ) - $this->startTime;
- // Note: a limit of 0 is treated as "forever"
- set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) );
- // If each scoped timeout is for less than one second, we end up
- // restoring the original timeout without any decrease in value.
- // Thus web scripts in an infinite loop can run forever unless we
- // take some measures to prevent this. Track total time and calls.
- self::$totalElapsed += $elapsed;
- --self::$stackDepth;
- }
- }
-}
-
-/**
- * Store a file into the backend from a file on the file system.
- * Parameters similar to FileBackendStore::storeInternal(), which include:
- * src : source path on file system
- * dst : destination storage path
- * overwrite : do nothing and pass if an identical file exists at destination
- * overwriteSame : override any existing file at destination
- */
-class StoreFileOp extends FileOp {
- protected function allowedParams() {
- return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
- }
-
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists on the file system
@@ -439,10 +454,13 @@ class StoreFileOp extends FileOp {
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-store', $this->params['src'], $this->params['dst'] );
return $status;
// Check if a file can be placed 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'] );
return $status;
}
@@ -456,15 +474,20 @@ class StoreFileOp extends FileOp {
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
// Store the file at the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->storeInternal( $this->params ) );
+ return $this->backend->storeInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
+ /**
+ * @return bool|string
+ */
protected function getSourceSha1Base36() {
wfSuppressWarnings();
$hash = sha1_file( $this->params['src'] );
@@ -475,32 +498,32 @@ class StoreFileOp extends FileOp {
return $hash;
}
- public function storagePathsChanged() {
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
/**
* Create a file in the backend with the given content.
- * Parameters similar to FileBackendStore::createInternal(), which include:
- * content : the raw file contents
- * dst : destination storage path
- * overwrite : do nothing and pass if an identical file exists at destination
- * overwriteSame : override any existing file at destination
+ * Parameters for this operation are outlined in FileBackend::doOperations().
*/
class CreateFileOp extends FileOp {
protected function allowedParams() {
- return array( array( 'content', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
+ return array( array( 'content', 'dst' ),
+ array( 'overwrite', 'overwriteSame', 'disposition' ) );
}
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// 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-create', $this->params['dst'] );
return $status;
// Check if a file can be placed 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'] );
return $status;
}
@@ -514,37 +537,49 @@ class CreateFileOp extends FileOp {
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
- // Create the file at the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->createInternal( $this->params ) );
+ // Create the file at the destination
+ return $this->backend->createInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
+ /**
+ * @return bool|String
+ */
protected function getSourceSha1Base36() {
return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
}
- public function storagePathsChanged() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
/**
* Copy a file from one storage path to another in the backend.
- * Parameters similar to FileBackendStore::copyInternal(), which include:
- * src : source storage path
- * dst : destination storage path
- * overwrite : do nothing and pass if an identical file exists at destination
- * overwriteSame : override any existing file at destination
+ * 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' ) );
+ return array( array( 'src', 'dst' ),
+ array( 'overwrite', 'overwriteSame', 'disposition' ) );
}
+ /**
+ * @param $predicates array
+ * @return Status
+ */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
@@ -553,6 +588,7 @@ class CopyFileOp extends FileOp {
return $status;
// Check if a file can be placed 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'] );
return $status;
}
@@ -566,40 +602,52 @@ class CopyFileOp extends FileOp {
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
// 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 ) {
- $status->merge( $this->backend->copyInternal( $this->params ) );
+ return $this->backend->copyInternal( $this->setFlags( $this->params ) );
}
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsRead() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsRead() {
return array( $this->params['src'] );
}
- public function storagePathsChanged() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
/**
* Move a file from one storage path to another in the backend.
- * Parameters similar to FileBackendStore::moveInternal(), which include:
- * src : source storage path
- * dst : destination storage path
- * overwrite : do nothing and pass if an identical file exists at destination
- * overwriteSame : override any existing file at destination
+ * 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' ) );
+ return array( array( 'src', 'dst' ),
+ array( 'overwrite', 'overwriteSame', 'disposition' ) );
}
+ /**
+ * @param $predicates array
+ * @return Status
+ */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
@@ -608,6 +656,7 @@ class MoveFileOp extends FileOp {
return $status;
// Check if a file can be placed 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'] );
return $status;
}
@@ -623,44 +672,57 @@ class MoveFileOp extends FileOp {
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
// 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
- $status->merge( $this->backend->moveInternal( $this->params ) );
+ return $this->backend->moveInternal( $this->setFlags( $this->params ) );
} else {
// Just delete source as the destination needs no changes
$params = array( 'src' => $this->params['src'] );
- $status->merge( $this->backend->deleteInternal( $params ) );
+ return $this->backend->deleteInternal( $this->setFlags( $params ) );
}
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsRead() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsRead() {
return array( $this->params['src'] );
}
- public function storagePathsChanged() {
- return array( $this->params['dst'] );
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
+ return array( $this->params['src'], $this->params['dst'] );
}
}
/**
* Delete a file at the given storage path from the backend.
- * Parameters similar to FileBackendStore::deleteInternal(), which include:
- * src : source storage path
- * ignoreMissingSource : don't return an error if the file does not exist
+ * 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
@@ -677,16 +739,21 @@ class DeleteFileOp extends FileOp {
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
if ( $this->needsDelete ) {
// Delete the source file
- $status->merge( $this->backend->deleteInternal( $this->params ) );
+ return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsChanged() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
return array( $this->params['src'] );
}
}
diff --git a/includes/filebackend/FileOpBatch.php b/includes/filebackend/FileOpBatch.php
new file mode 100644
index 00000000..33558725
--- /dev/null
+++ b/includes/filebackend/FileOpBatch.php
@@ -0,0 +1,240 @@
+<?php
+/**
+ * Helper class for representing batch file operations.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Helper class for representing batch file operations.
+ * Do not use this class from places outside FileBackend.
+ *
+ * Methods should avoid throwing exceptions at all costs.
+ *
+ * @ingroup FileBackend
+ * @since 1.20
+ */
+class FileOpBatch {
+ /* Timeout related parameters */
+ const MAX_BATCH_SIZE = 1000; // integer
+
+ /**
+ * Attempt to perform a series of file operations.
+ * Callers are responsible for handling file locking.
+ *
+ * $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.
+ *
+ * The resulting Status will be "OK" unless:
+ * - 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
+ * @return Status
+ */
+ public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
+ wfProfileIn( __METHOD__ );
+ $status = Status::newGood();
+
+ $n = count( $performOps );
+ if ( $n > self::MAX_BATCH_SIZE ) {
+ $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ $batchId = $journal->getTimestampedUUID();
+ $allowStale = !empty( $opts['allowStale'] );
+ $ignoreErrors = !empty( $opts['force'] );
+ $journaled = empty( $opts['nonJournaled'] );
+ $maxConcurrency = isset( $opts['concurrency'] ) ? $opts['concurrency'] : 1;
+
+ $entries = array(); // file journal entry list
+ $predicates = FileOp::newPredicates(); // account for previous ops in prechecks
+ $curBatch = array(); // concurrent FileOp sub-batch accumulation
+ $curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch
+ $pPerformOps = array(); // ordered list of concurrent FileOp sub-batches
+ $lastBackend = null; // last op backend name
+ // Do pre-checks for each operation; abort on failure...
+ 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 )
+ || count( $curBatch ) >= $maxConcurrency
+ || ( $backendName !== $lastBackend && count( $curBatch ) )
+ ) {
+ $pPerformOps[] = $curBatch; // push this batch
+ $curBatch = array(); // start a new sub-batch
+ $curBatchDeps = FileOp::newDependencies();
+ }
+ $lastBackend = $backendName;
+ $curBatch[$index] = $fileOp; // keep index
+ // Update list of affected paths in this batch
+ $curBatchDeps = $fileOp->applyDependencies( $curBatchDeps );
+ // Simulate performing the operation...
+ $oldPredicates = $predicates;
+ $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
+ $status->merge( $subStatus );
+ if ( $subStatus->isOK() ) {
+ if ( $journaled ) { // journal log entries
+ $entries = array_merge( $entries,
+ $fileOp->getJournalEntries( $oldPredicates, $predicates ) );
+ }
+ } else { // operation failed?
+ $status->success[$index] = false;
+ ++$status->failCount;
+ if ( !$ignoreErrors ) {
+ wfProfileOut( __METHOD__ );
+ return $status; // abort
+ }
+ }
+ }
+ // Push the last sub-batch
+ if ( count( $curBatch ) ) {
+ $pPerformOps[] = $curBatch;
+ }
+
+ // Log the operations in the file journal...
+ if ( count( $entries ) ) {
+ $subStatus = $journal->logChangeBatch( $entries, $batchId );
+ if ( !$subStatus->isOK() ) {
+ wfProfileOut( __METHOD__ );
+ return $subStatus; // abort
+ }
+ }
+
+ if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
+ $status->setResult( true, $status->value );
+ }
+
+ // Attempt each operation (in parallel if allowed and possible)...
+ if ( count( $pPerformOps ) < count( $performOps ) ) {
+ self::runBatchParallel( $pPerformOps, $status );
+ } else {
+ self::runBatchSeries( $performOps, $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.
+ * The caller is responsible for making sure the operations
+ * within any given sub-batch do not depend on each other.
+ * This will abort remaining ops on failure.
+ *
+ * @param $pPerformOps Array
+ * @param $status Status
+ * @return bool Success
+ */
+ protected static function runBatchParallel( array $pPerformOps, Status $status ) {
+ $aborted = false;
+ foreach ( $pPerformOps as $performOpsBatch ) {
+ if ( $aborted ) { // check batch op abort flag...
+ // We can't continue (even with $ignoreErrors) as $predicates is wrong.
+ // Log the remaining ops as failed for recovery...
+ foreach ( $performOpsBatch as $i => $fileOp ) {
+ $performOpsBatch[$i]->logFailure( 'attempt_aborted' );
+ }
+ continue;
+ }
+ $statuses = array();
+ $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.
+ foreach ( $performOpsBatch as $i => $fileOp ) {
+ if ( !$fileOp->failed() ) { // failed => already has Status
+ $subStatus = $fileOp->attemptAsync();
+ if ( $subStatus->value instanceof FileBackendStoreOpHandle ) {
+ $opHandles[$i] = $subStatus->value; // deferred
+ } else {
+ $statuses[$i] = $subStatus; // done already
+ }
+ }
+ }
+ // Try to do all the operations concurrently...
+ $statuses = $statuses + $backend->executeOpHandlesInternal( $opHandles );
+ // Marshall and merge all the responses (blocking)...
+ foreach ( $performOpsBatch as $i => $fileOp ) {
+ if ( !$fileOp->failed() ) { // failed => already has Status
+ $subStatus = $statuses[$i];
+ $status->merge( $subStatus );
+ if ( $subStatus->isOK() ) {
+ $status->success[$i] = true;
+ ++$status->successCount;
+ } else {
+ $status->success[$i] = false;
+ ++$status->failCount;
+ $aborted = true; // set abort flag; we can't continue
+ }
+ }
+ }
+ }
+ return $status;
+ }
+}
diff --git a/includes/filebackend/SwiftFileBackend.php b/includes/filebackend/SwiftFileBackend.php
new file mode 100644
index 00000000..b6f0aa60
--- /dev/null
+++ b/includes/filebackend/SwiftFileBackend.php
@@ -0,0 +1,1544 @@
+<?php
+/**
+ * OpenStack Swift based 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
+ * @ingroup FileBackend
+ * @author Russ Nelson
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for an OpenStack Swift based file backend.
+ *
+ * This requires the SwiftCloudFiles MediaWiki extension, which includes
+ * the php-cloudfiles library (https://github.com/rackspace/php-cloudfiles).
+ * php-cloudfiles requires the curl, fileinfo, and mb_string PHP extensions.
+ *
+ * Status messages should avoid mentioning the Swift account name.
+ * Likewise, error suppression should be used to avoid path disclosure.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+class SwiftFileBackend extends FileBackendStore {
+ /** @var CF_Authentication */
+ protected $auth; // Swift authentication handler
+ protected $authTTL; // integer seconds
+ 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
+
+ /** @var CF_Connection */
+ protected $conn; // Swift connection handle
+ protected $sessionStarted = 0; // integer UNIX timestamp
+
+ /** @var CloudFilesException */
+ protected $connException;
+ protected $connErrorTime = 0; // UNIX timestamp
+
+ /** @var BagOStuff */
+ protected $srvCache;
+
+ /** @var ProcessCacheLRU */
+ protected $connContainerCache; // container object cache
+
+ /**
+ * @see FileBackendStore::__construct()
+ * Additional $config params include:
+ * - swiftAuthUrl : Swift authentication server URL
+ * - swiftUser : Swift user used by MediaWiki (account:username)
+ * - swiftKey : Swift authentication key for the above user
+ * - swiftAuthTTL : Swift authentication TTL (seconds)
+ * - 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
+ * accessible to unauthenticated requests via ".r:*" in the ACL.
+ * - swiftUseCDN : Whether a Cloud Files Content Delivery Network is set up
+ * - swiftCDNExpiry : How long (in seconds) to store content in the CDN.
+ * If files may likely change, this should probably not exceed
+ * a few days. For example, deletions may take this long to apply.
+ * If object purging is enabled, however, this is not an issue.
+ * - swiftCDNPurgable : Whether object purge requests are allowed by the CDN.
+ * - shardViaHashLevels : Map of container names to sharding config with:
+ * - base : base of hash characters, 16 or 36
+ * - levels : the number of hash levels (and digits)
+ * - repeat : hash subdirectories are prefixed with all the
+ * parent hash directory names (e.g. "a/ab/abc")
+ * - 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.
+ */
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+ if ( !MWInit::classExists( 'CF_Constants' ) ) {
+ throw new MWException( 'SwiftCloudFiles extension not installed.' );
+ }
+ // Required settings
+ $this->auth = new CF_Authentication(
+ $config['swiftUser'],
+ $config['swiftKey'],
+ null, // account; unused
+ $config['swiftAuthUrl']
+ );
+ // Optional settings
+ $this->authTTL = isset( $config['swiftAuthTTL'] )
+ ? $config['swiftAuthTTL']
+ : 5 * 60; // some sane number
+ $this->swiftAnonUser = isset( $config['swiftAnonUser'] )
+ ? $config['swiftAnonUser']
+ : '';
+ $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
+ ? $config['shardViaHashLevels']
+ : '';
+ $this->swiftUseCDN = isset( $config['swiftUseCDN'] )
+ ? $config['swiftUseCDN']
+ : false;
+ $this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] )
+ ? $config['swiftCDNExpiry']
+ : 12*3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org)
+ $this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] )
+ ? $config['swiftCDNPurgable']
+ : true;
+ // 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' ) {
+ $this->srvCache = wfGetMainCache(); // preferrably memcached
+ } else {
+ try { // look for APC, XCache, WinCache, ect...
+ $this->srvCache = ObjectCache::newAccelerator( array() );
+ } catch ( Exception $e ) {}
+ }
+ }
+ $this->srvCache = $this->srvCache ? $this->srvCache : new EmptyBagOStuff();
+ }
+
+ /**
+ * @see FileBackendStore::resolveContainerPath()
+ * @return null
+ */
+ protected function resolveContainerPath( $container, $relStoragePath ) {
+ if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) { // mb_string required by CF
+ return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
+ } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
+ return null; // too long for Swift
+ }
+ return $relStoragePath;
+ }
+
+ /**
+ * @see FileBackendStore::isPathUsableInternal()
+ * @return bool
+ */
+ public function isPathUsableInternal( $storagePath ) {
+ list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
+ if ( $rel === null ) {
+ return false; // invalid
+ }
+
+ try {
+ $this->getContainer( $container );
+ return true; // container exists
+ } catch ( NoSuchContainerException $e ) {
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__, array( 'path' => $storagePath ) );
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $disposition string 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}";
+ if ( strlen( $new ) <= 255 ) {
+ $res = $new;
+ } else {
+ break; // too long; sigh
+ }
+ }
+ return $res;
+ }
+
+ /**
+ * @see FileBackendStore::doCreateInternal()
+ * @return Status
+ */
+ protected function doCreateInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+ if ( $dstRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+ return $status;
+ }
+
+ // (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;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ // (b) Get a SHA-1 hash of the object
+ $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
+
+ // (c) Actually create the object
+ try {
+ // 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 );
+ // 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'] );
+ }
+ 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;
+ }
+ } else { // actually write the object in Swift
+ $obj->write( $params['content'] );
+ if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
+ $this->purgeCDNCache( array( $obj ) );
+ }
+ }
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( BadContentTypeException $e ) {
+ $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCreate( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( BadContentTypeException $e ) {
+ $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doStoreInternal()
+ * @return Status
+ */
+ protected function doStoreInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+ if ( $dstRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+ return $status;
+ }
+
+ // (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;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ // (b) Get a SHA-1 hash of the object
+ $sha1Hash = sha1_file( $params['src'] );
+ if ( $sha1Hash === false ) { // source doesn't exist?
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ return $status;
+ }
+ $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
+
+ // (c) Actually store the object
+ try {
+ // 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 );
+ // 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'] );
+ }
+ if ( !empty( $params['async'] ) ) { // deferred
+ wfSuppressWarnings();
+ $fp = fopen( $params['src'], 'rb' );
+ wfRestoreWarnings();
+ if ( !$fp ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ } else {
+ $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;
+ }
+ }
+ } 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 ) );
+ }
+ }
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( BadContentTypeException $e ) {
+ $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+ } catch ( IOException $e ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseStore( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( BadContentTypeException $e ) {
+ $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+ } catch ( IOException $e ) {
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doCopyInternal()
+ * @return Status
+ */
+ protected function doCopyInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+ if ( $srcRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ return $status;
+ }
+
+ list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+ if ( $dstRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+ return $status;
+ }
+
+ // (a) Check the source/destination containers and destination object
+ 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'] );
+ return $status;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ // (b) Actually copy the file to the destination
+ 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'] );
+ }
+ 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;
+ }
+ } 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 ) );
+ }
+ }
+ } 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'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseCopy( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( NoSuchObjectException $e ) { // source object does not exist
+ $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doMoveInternal()
+ * @return Status
+ */
+ protected function doMoveInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+ if ( $srcRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ return $status;
+ }
+
+ list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+ if ( $dstRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+ return $status;
+ }
+
+ // (a) Check the source/destination containers and destination object
+ 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'] );
+ return $status;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ // (b) Actually move the file to the destination
+ try {
+ $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'] );
+ }
+ 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;
+ }
+ } 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 ) );
+ }
+ }
+ } 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'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseMove( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( NoSuchObjectException $e ) { // source object does not exist
+ $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doDeleteInternal()
+ * @return Status
+ */
+ protected function doDeleteInternal( 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 );
+ $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
+ if ( !empty( $params['async'] ) ) { // deferred
+ $op = $sContObj->delete_object_async( $srcRel );
+ $status->value = new SwiftFileOpHandle( $this, $params, 'Delete', $op );
+ $status->value->affectedObjects[] = $srcObj;
+ } else { // actually write the object in Swift
+ $sContObj->delete_object( $srcRel );
+ $this->purgeCDNCache( array( $srcObj ) );
+ }
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( NoSuchContainerException $e ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ } catch ( NoSuchObjectException $e ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ }
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see SwiftFileBackend::doExecuteOpHandlesInternal()
+ */
+ protected function _getResponseDelete( CF_Async_Op $cfOp, Status $status, array $params ) {
+ try {
+ $cfOp->getLastResponse();
+ } catch ( NoSuchContainerException $e ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ } catch ( NoSuchObjectException $e ) {
+ if ( empty( $params['ignoreMissingSource'] ) ) {
+ $status->fatal( 'backend-fail-delete', $params['src'] );
+ }
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doPrepareInternal()
+ * @return Status
+ */
+ protected function doPrepareInternal( $fullCont, $dir, array $params ) {
+ $status = Status::newGood();
+
+ // (a) Check if container already exists
+ try {
+ $contObj = $this->getContainer( $fullCont );
+ // NoSuchContainerException not thrown: container must exist
+ return $status; // already exists
+ } catch ( NoSuchContainerException $e ) {
+ // NoSuchContainerException thrown: container does not exist
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ // (b) Create container as needed
+ try {
+ $contObj = $this->createContainer( $fullCont );
+ if ( !empty( $params['noAccess'] ) ) {
+ // Make container private to end-users...
+ $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
+ } else {
+ // Make container public to end-users...
+ $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
+ }
+ if ( $this->swiftUseCDN ) { // Rackspace style CDN
+ $contObj->make_public( $this->swiftCDNExpiry );
+ }
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doSecureInternal()
+ * @return Status
+ */
+ protected function doSecureInternal( $fullCont, $dir, array $params ) {
+ $status = Status::newGood();
+ if ( empty( $params['noAccess'] ) ) {
+ return $status; // nothing to do
+ }
+
+ // Restrict container from end-users...
+ try {
+ // doPrepareInternal() should have been called,
+ // so the Swift container should already exist...
+ $contObj = $this->getContainer( $fullCont ); // normally a cache hit
+ // NoSuchContainerException not thrown: container must exist
+
+ // Make container private to end-users...
+ $status->merge( $this->setContainerAccess(
+ $contObj,
+ array( $this->auth->username ), // read
+ array( $this->auth->username ) // write
+ ) );
+ if ( $this->swiftUseCDN && $contObj->is_public() ) { // Rackspace style CDN
+ $contObj->make_private();
+ }
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doPublishInternal()
+ * @return Status
+ */
+ protected function doPublishInternal( $fullCont, $dir, array $params ) {
+ $status = Status::newGood();
+
+ // Unrestrict container from end-users...
+ try {
+ // doPrepareInternal() should have been called,
+ // so the Swift container should already exist...
+ $contObj = $this->getContainer( $fullCont ); // normally a cache hit
+ // NoSuchContainerException not thrown: container must exist
+
+ // Make container public to end-users...
+ if ( $this->swiftAnonUser != '' ) {
+ $status->merge( $this->setContainerAccess(
+ $contObj,
+ array( $this->auth->username, $this->swiftAnonUser ), // read
+ array( $this->auth->username, $this->swiftAnonUser ) // write
+ ) );
+ } else {
+ $status->merge( $this->setContainerAccess(
+ $contObj,
+ array( $this->auth->username, '.r:*' ), // read
+ array( $this->auth->username ) // write
+ ) );
+ }
+ if ( $this->swiftUseCDN && !$contObj->is_public() ) { // Rackspace style CDN
+ $contObj->make_public();
+ }
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doCleanInternal()
+ * @return Status
+ */
+ protected function doCleanInternal( $fullCont, $dir, array $params ) {
+ $status = Status::newGood();
+
+ // Only containers themselves can be removed, all else is virtual
+ if ( $dir != '' ) {
+ return $status; // nothing to do
+ }
+
+ // (a) Check the container
+ try {
+ $contObj = $this->getContainer( $fullCont, true );
+ } catch ( NoSuchContainerException $e ) {
+ return $status; // ok, nothing to do
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ // (b) Delete the container if empty
+ if ( $contObj->object_count == 0 ) {
+ try {
+ $this->deleteContainer( $fullCont );
+ } catch ( NoSuchContainerException $e ) {
+ return $status; // race?
+ } catch ( NonEmptyContainerException $e ) {
+ return $status; // race? consistency delay?
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FileBackendStore::doFileExists()
+ * @return array|bool|null
+ */
+ protected function doGetFileStat( array $params ) {
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+ if ( $srcRel === null ) {
+ return false; // invalid storage path
+ }
+
+ $stat = false;
+ try {
+ $contObj = $this->getContainer( $srcCont );
+ $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) );
+ $this->addMissingMetadata( $srcObj, $params['src'] );
+ $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']
+ );
+ } catch ( NoSuchContainerException $e ) {
+ } catch ( NoSuchObjectException $e ) {
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $stat = null;
+ $this->handleException( $e, null, __METHOD__, $params );
+ }
+
+ return $stat;
+ }
+
+ /**
+ * Fill in any missing object metadata and save it to Swift
+ *
+ * @param $obj CF_Object
+ * @param $path string Storage path to object
+ * @return bool Success
+ * @throws Exception cloudfiles exceptions
+ */
+ protected function addMissingMetadata( CF_Object $obj, $path ) {
+ if ( isset( $obj->metadata['Sha1base36'] ) ) {
+ return true; // nothing to do
+ }
+ wfProfileIn( __METHOD__ );
+ $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 ) );
+ if ( $tmpFile ) {
+ $hash = $tmpFile->getSha1Base36();
+ if ( $hash !== false ) {
+ $obj->metadata['Sha1base36'] = $hash;
+ $obj->sync_metadata(); // save to Swift
+ wfProfileOut( __METHOD__ );
+ return true; // success
+ }
+ }
+ }
+ $obj->metadata['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
+ }
+
+ if ( !$this->fileExists( $params ) ) {
+ return null;
+ }
+
+ $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 );
+ }
+
+ return $data;
+ }
+
+ /**
+ * @see FileBackendStore::doDirectoryExists()
+ * @return bool|null
+ */
+ protected function doDirectoryExists( $fullCont, $dir, array $params ) {
+ try {
+ $container = $this->getContainer( $fullCont );
+ $prefix = ( $dir == '' ) ? null : "{$dir}/";
+ return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 );
+ } catch ( NoSuchContainerException $e ) {
+ return false;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__,
+ array( 'cont' => $fullCont, 'dir' => $dir ) );
+ }
+
+ return null; // error
+ }
+
+ /**
+ * @see FileBackendStore::getDirectoryListInternal()
+ * @return SwiftFileBackendDirList
+ */
+ public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
+ return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
+ }
+
+ /**
+ * @see FileBackendStore::getFileListInternal()
+ * @return SwiftFileBackendFileList
+ */
+ public function getFileListInternal( $fullCont, $dir, array $params ) {
+ return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
+ }
+
+ /**
+ * 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
+ */
+ public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
+ $dirs = array();
+ if ( $after === INF ) {
+ return $dirs; // nothing more
+ }
+ wfProfileIn( __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
+ if ( substr( $object, -1 ) === '/' ) {
+ $dirs[] = $object; // directories end in '/'
+ }
+ }
+ // Recursive: list all dirs under $dir and its subdirs
+ } else {
+ // Get directory from last item of prior page
+ $lastDir = $this->getParentDir( $after ); // must be first page
+ $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
+ // 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,
+ // then it was already listed by the calling iterator.
+ if ( strcmp( $objectDir, $lastDir ) > 0 ) {
+ $pDir = $objectDir;
+ do { // add dir and all its parent dirs
+ $dirs[] = "{$pDir}/";
+ $pDir = $this->getParentDir( $pDir );
+ } while ( $pDir !== false // sanity
+ && strcmp( $pDir, $lastDir ) > 0 // not done already
+ && strlen( $pDir ) > strlen( $dir ) // within $dir
+ );
+ }
+ $lastDir = $objectDir;
+ }
+ }
+ }
+ if ( count( $objects ) < $limit ) {
+ $after = INF; // avoid a second RTT
+ } else {
+ $after = end( $objects ); // update last item
+ }
+ } catch ( NoSuchContainerException $e ) {
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__,
+ array( 'cont' => $fullCont, 'dir' => $dir ) );
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ return $dirs;
+ }
+
+ protected function getParentDir( $path ) {
+ return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
+ }
+
+ /**
+ * 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
+ */
+ public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
+ $files = array();
+ if ( $after === INF ) {
+ return $files; // nothing more
+ }
+ wfProfileIn( __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 '/'
+ }
+ }
+ // Recursive: list all files under $dir and its subdirs
+ } else { // files
+ $objects = $container->list_objects( $limit, $after, $prefix );
+ $files = $objects;
+ }
+ if ( count( $objects ) < $limit ) {
+ $after = INF; // avoid a second RTT
+ } else {
+ $after = end( $objects ); // update last item
+ }
+ } catch ( NoSuchContainerException $e ) {
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__,
+ array( 'cont' => $fullCont, 'dir' => $dir ) );
+ }
+
+ wfProfileOut( __METHOD__ . '-' . $this->name );
+ return $files;
+ }
+
+ /**
+ * @see FileBackendStore::doGetFileSha1base36()
+ * @return bool
+ */
+ protected function doGetFileSha1base36( array $params ) {
+ $stat = $this->getFileStat( $params );
+ if ( $stat ) {
+ return $stat['sha1'];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @see FileBackendStore::doStreamFile()
+ * @return Status
+ */
+ protected function doStreamFile( array $params ) {
+ $status = Status::newGood();
+
+ list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+ if ( $srcRel === null ) {
+ $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+ }
+
+ try {
+ $cont = $this->getContainer( $srcCont );
+ } catch ( NoSuchContainerException $e ) {
+ $status->fatal( 'backend-fail-stream', $params['src'] );
+ return $status;
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ return $status;
+ }
+
+ try {
+ $output = fopen( 'php://output', 'wb' );
+ $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD
+ $obj->stream( $output, $this->headersFromParams( $params ) );
+ } catch ( NoSuchObjectException $e ) {
+ $status->fatal( 'backend-fail-stream', $params['src'] );
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, $status, __METHOD__, $params );
+ }
+
+ 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;
+ }
+
+ // 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
+ }
+ }
+ } 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;
+ }
+
+ /**
+ * @see FileBackendStore::directoriesAreVirtual()
+ * @return bool
+ */
+ protected function directoriesAreVirtual() {
+ return true;
+ }
+
+ /**
+ * Get headers to send to Swift when reading a file based
+ * on a FileBackend params array, e.g. that of getLocalCopy().
+ * $params is currently only checked for a 'latest' flag.
+ *
+ * @param $params Array
+ * @return Array
+ */
+ protected function headersFromParams( array $params ) {
+ $hdrs = array();
+ if ( !empty( $params['latest'] ) ) {
+ $hdrs[] = 'X-Newest: true';
+ }
+ return $hdrs;
+ }
+
+ /**
+ * @see FileBackendStore::doExecuteOpHandlesInternal()
+ * @return Array List of corresponding Status objects
+ */
+ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+ $statuses = array();
+
+ $cfOps = array(); // list of CF_Async_Op objects
+ foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+ $cfOps[$index] = $fileOpHandle->cfOp;
+ }
+ $batch = new CF_Async_Op_Batch( $cfOps );
+
+ $cfOps = $batch->execute();
+ foreach ( $cfOps as $index => $cfOp ) {
+ $status = Status::newGood();
+ 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?
+ $this->handleException( $e, $status,
+ __CLASS__ . ":$function", $fileOpHandles[$index]->params );
+ }
+ $statuses[$index] = $status;
+ }
+
+ return $statuses;
+ }
+
+ /**
+ * Set read/write permissions for a Swift container.
+ *
+ * $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.
+ *
+ * $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:
+ * - account:user : Grants access if the request is by the given user
+ *
+ * @see http://swift.openstack.org/misc.html#acls
+ *
+ * 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
+ * @return Status
+ */
+ protected function setContainerAccess(
+ CF_Container $contObj, array $readGrps, array $writeGrps
+ ) {
+ $creds = $contObj->cfs_auth->export_credentials();
+
+ $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name );
+
+ // Note: 10 second timeout consistent with php-cloudfiles
+ $req = MWHttpRequest::factory( $url, array( 'method' => 'POST', 'timeout' => 10 ) );
+ $req->setHeader( 'X-Auth-Token', $creds['auth_token'] );
+ $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) );
+ $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) );
+
+ return $req->execute(); // should return 204
+ }
+
+ /**
+ * 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
+ * @return void
+ */
+ public function purgeCDNCache( array $objects ) {
+ if ( $this->swiftUseCDN && $this->swiftCDNPurgable ) {
+ foreach ( $objects as $object ) {
+ try {
+ $object->purge_from_cdn();
+ } catch ( CDNNotEnabledException $e ) {
+ // CDN not enabled; nothing to see here
+ } catch ( CloudFilesException $e ) {
+ $this->handleException( $e, null, __METHOD__,
+ array( 'cont' => $object->container->name, 'obj' => $object->name ) );
+ }
+ }
+ }
+ }
+
+ /**
+ * Get an authenticated connection handle to the Swift proxy
+ *
+ * @return CF_Connection|bool False on failure
+ * @throws CloudFilesException
+ */
+ protected function getConnection() {
+ if ( $this->connException instanceof CloudFilesException ) {
+ if ( ( time() - $this->connErrorTime ) < 60 ) {
+ throw $this->connException; // failed last attempt; don't bother
+ } else { // actually retry this time
+ $this->connException = null;
+ $this->connErrorTime = 0;
+ }
+ }
+ // Session keys expire after a while, so we renew them periodically
+ $reAuth = ( ( time() - $this->sessionStarted ) > $this->authTTL );
+ // Authenticate with proxy and get a session key...
+ if ( !$this->conn || $reAuth ) {
+ $this->sessionStarted = 0;
+ $this->connContainerCache->clear();
+ $cacheKey = $this->getCredsCacheKey( $this->auth->username );
+ $creds = $this->srvCache->get( $cacheKey ); // credentials
+ 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
+ } else { // cache miss
+ try {
+ $this->auth->authenticate();
+ $creds = $this->auth->export_credentials();
+ $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL/2 ) ); // cache
+ $this->sessionStarted = time();
+ } catch ( CloudFilesException $e ) {
+ $this->connException = $e; // don't keep re-trying
+ $this->connErrorTime = time();
+ throw $e; // throw it back
+ }
+ }
+ if ( $this->conn ) { // re-authorizing?
+ $this->conn->close(); // close active cURL handles in CF_Http object
+ }
+ $this->conn = new CF_Connection( $this->auth );
+ }
+ return $this->conn;
+ }
+
+ /**
+ * Close the connection to the Swift proxy
+ *
+ * @return void
+ */
+ protected function closeConnection() {
+ if ( $this->conn ) {
+ $this->conn->close(); // close active cURL handles in CF_Http object
+ $this->sessionStarted = 0;
+ $this->connContainerCache->clear();
+ }
+ }
+
+ /**
+ * Get the cache key for a container
+ *
+ * @param $username string
+ * @return string
+ */
+ private function getCredsCacheKey( $username ) {
+ return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username );
+ }
+
+ /**
+ * @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
+ * @return CF_Container
+ * @throws CloudFilesException
+ */
+ protected function getContainer( $container, $bypassCache = false ) {
+ $conn = $this->getConnection(); // Swift proxy connection
+ if ( $bypassCache ) { // purge cache
+ $this->connContainerCache->clear( $container );
+ } elseif ( !$this->connContainerCache->has( $container, 'obj' ) ) {
+ $this->primeContainerCache( array( $container ) ); // check persistent cache
+ }
+ if ( !$this->connContainerCache->has( $container, 'obj' ) ) {
+ $contObj = $conn->get_container( $container );
+ // NoSuchContainerException not thrown: container must exist
+ $this->connContainerCache->set( $container, 'obj', $contObj ); // cache it
+ if ( !$bypassCache ) {
+ $this->setContainerCache( $container, // update persistent cache
+ array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count )
+ );
+ }
+ }
+ return $this->connContainerCache->get( $container, 'obj' );
+ }
+
+ /**
+ * Create a Swift container
+ *
+ * @param $container string Container name
+ * @return CF_Container
+ * @throws CloudFilesException
+ */
+ protected function createContainer( $container ) {
+ $conn = $this->getConnection(); // Swift proxy connection
+ $contObj = $conn->create_container( $container );
+ $this->connContainerCache->set( $container, 'obj', $contObj ); // cache
+ return $contObj;
+ }
+
+ /**
+ * Delete a Swift container
+ *
+ * @param $container string Container name
+ * @return void
+ * @throws CloudFilesException
+ */
+ protected function deleteContainer( $container ) {
+ $conn = $this->getConnection(); // Swift proxy connection
+ $this->connContainerCache->clear( $container ); // purge
+ $conn->delete_container( $container );
+ }
+
+ /**
+ * @see FileBackendStore::doPrimeContainerCache()
+ * @return void
+ */
+ protected function doPrimeContainerCache( array $containerInfo ) {
+ try {
+ $conn = $this->getConnection(); // Swift proxy connection
+ foreach ( $containerInfo as $container => $info ) {
+ $contObj = new CF_Container( $conn->cfs_auth, $conn->cfs_http,
+ $container, $info['count'], $info['bytes'] );
+ $this->connContainerCache->set( $container, 'obj', $contObj );
+ }
+ } catch ( CloudFilesException $e ) { // some other exception?
+ $this->handleException( $e, null, __METHOD__, array() );
+ }
+ }
+
+ /**
+ * 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
+ * @return void
+ */
+ protected function handleException( Exception $e, $status, $func, array $params ) {
+ if ( $status instanceof Status ) {
+ if ( $e instanceof AuthenticationException ) {
+ $status->fatal( 'backend-fail-connect', $this->name );
+ } else {
+ $status->fatal( 'backend-fail-internal', $this->name );
+ }
+ }
+ if ( $e->getMessage() ) {
+ trigger_error( "$func: " . $e->getMessage(), E_USER_WARNING );
+ }
+ if ( $e instanceof InvalidResponseException ) { // possibly a stale token
+ $this->srvCache->delete( $this->getCredsCacheKey( $this->auth->username ) );
+ $this->closeConnection(); // force a re-connect and re-auth next time
+ }
+ wfDebugLog( 'SwiftBackend',
+ get_class( $e ) . " in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
+ ( $e->getMessage() ? ": {$e->getMessage()}" : "" )
+ );
+ }
+}
+
+/**
+ * @see FileBackendStoreOpHandle
+ */
+class SwiftFileOpHandle extends FileBackendStoreOpHandle {
+ /** @var CF_Async_Op */
+ public $cfOp;
+ /** @var Array */
+ public $affectedObjects = array();
+
+ public function __construct( $backend, array $params, $call, CF_Async_Op $cfOp ) {
+ $this->backend = $backend;
+ $this->params = $params;
+ $this->call = $call;
+ $this->cfOp = $cfOp;
+ }
+}
+
+/**
+ * SwiftFileBackend helper class to page through listings.
+ * Swift also has a listing limit of 10,000 objects for sanity.
+ * Do not use this class from places outside SwiftFileBackend.
+ *
+ * @ingroup FileBackend
+ */
+abstract class SwiftFileBackendList implements Iterator {
+ /** @var Array */
+ protected $bufferIter = array();
+ protected $bufferAfter = null; // string; list items *after* this path
+ protected $pos = 0; // integer
+ /** @var Array */
+ protected $params = array();
+
+ /** @var SwiftFileBackend */
+ protected $backend;
+ protected $container; // string; container name
+ protected $dir; // string; storage directory
+ protected $suffixStart; // integer
+
+ 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
+ */
+ public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
+ $this->backend = $backend;
+ $this->container = $fullCont;
+ $this->dir = $dir;
+ if ( substr( $this->dir, -1 ) === '/' ) {
+ $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
+ }
+ if ( $this->dir == '' ) { // whole container
+ $this->suffixStart = 0;
+ } else { // dir within container
+ $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
+ }
+ $this->params = $params;
+ }
+
+ /**
+ * @see Iterator::key()
+ * @return integer
+ */
+ public function key() {
+ return $this->pos;
+ }
+
+ /**
+ * @see Iterator::next()
+ * @return void
+ */
+ public function next() {
+ // Advance to the next file in the page
+ next( $this->bufferIter );
+ ++$this->pos;
+ // Check if there are no files left in this page and
+ // advance to the next page if this page was not empty.
+ if ( !$this->valid() && count( $this->bufferIter ) ) {
+ $this->bufferIter = $this->pageFromList(
+ $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+ ); // updates $this->bufferAfter
+ }
+ }
+
+ /**
+ * @see Iterator::rewind()
+ * @return void
+ */
+ public function rewind() {
+ $this->pos = 0;
+ $this->bufferAfter = null;
+ $this->bufferIter = $this->pageFromList(
+ $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+ ); // updates $this->bufferAfter
+ }
+
+ /**
+ * @see Iterator::valid()
+ * @return bool
+ */
+ public function valid() {
+ if ( $this->bufferIter === null ) {
+ return false; // some failure?
+ } else {
+ return ( current( $this->bufferIter ) !== false ); // no paths can have this value
+ }
+ }
+
+ /**
+ * 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
+ */
+ abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class SwiftFileBackendDirList extends SwiftFileBackendList {
+ /**
+ * @see Iterator::current()
+ * @return string|bool String (relative path) or false
+ */
+ public function current() {
+ return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
+ }
+
+ /**
+ * @see SwiftFileBackendList::pageFromList()
+ * @return Array|null
+ */
+ protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+ return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
+ }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class SwiftFileBackendFileList extends SwiftFileBackendList {
+ /**
+ * @see Iterator::current()
+ * @return string|bool String (relative path) or false
+ */
+ public function current() {
+ return substr( current( $this->bufferIter ), $this->suffixStart );
+ }
+
+ /**
+ * @see SwiftFileBackendList::pageFromList()
+ * @return Array|null
+ */
+ protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+ return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
+ }
+}
diff --git a/includes/filerepo/backend/TempFSFile.php b/includes/filebackend/TempFSFile.php
index 7843d6cd..5032bf68 100644
--- a/includes/filerepo/backend/TempFSFile.php
+++ b/includes/filebackend/TempFSFile.php
@@ -1,12 +1,29 @@
<?php
/**
+ * Location holder of files stored temporarily
+ *
+ * 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 FileBackend
*/
/**
* This class is used to hold the location and do limited manipulation
- * of files stored temporarily (usually this will be $wgTmpDirectory)
+ * of files stored temporarily (this will be whatever wfTempDir() returns)
*
* @ingroup FileBackend
*/
@@ -19,13 +36,14 @@ 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
- * @return TempFSFile|null
+ * @return TempFSFile|null
*/
public static function factory( $prefix, $extension = '' ) {
- $base = wfTempDir() . '/' . $prefix . dechex( mt_rand( 0, 99999999 ) );
+ wfProfileIn( __METHOD__ );
+ $base = wfTempDir() . '/' . $prefix . wfRandomString( 12 );
$ext = ( $extension != '' ) ? ".{$extension}" : "";
for ( $attempt = 1; true; $attempt++ ) {
$path = "{$base}-{$attempt}{$ext}";
@@ -36,18 +54,20 @@ class TempFSFile extends FSFile {
fclose( $newFileHandle );
break; // got it
}
- if ( $attempt >= 15 ) {
+ if ( $attempt >= 5 ) {
+ wfProfileOut( __METHOD__ );
return null; // give up
}
}
$tmpFile = new self( $path );
$tmpFile->canDelete = true; // safely instantiated
+ wfProfileOut( __METHOD__ );
return $tmpFile;
}
/**
* Purge this file off the file system
- *
+ *
* @return bool Success
*/
public function purge() {
@@ -80,6 +100,15 @@ class TempFSFile extends FSFile {
}
/**
+ * Set flag clean up after the temporary file
+ *
+ * @return void
+ */
+ public function autocollect() {
+ $this->canDelete = true;
+ }
+
+ /**
* Cleans up after the temporary file by deleting it
*/
function __destruct() {
diff --git a/includes/filebackend/filejournal/DBFileJournal.php b/includes/filebackend/filejournal/DBFileJournal.php
new file mode 100644
index 00000000..f6268c25
--- /dev/null
+++ b/includes/filebackend/filejournal/DBFileJournal.php
@@ -0,0 +1,152 @@
+<?php
+/**
+ * Version of FileJournal that logs to a DB table.
+ *
+ * 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 FileJournal
+ * @author Aaron Schulz
+ */
+
+/**
+ * Version of FileJournal that logs to a DB table
+ * @since 1.20
+ */
+class DBFileJournal extends FileJournal {
+ /** @var DatabaseBase */
+ protected $dbw;
+
+ protected $wiki = false; // string; wiki DB name
+
+ /**
+ * Construct a new instance from configuration.
+ * $config includes:
+ * 'wiki' : wiki name to use for LoadBalancer
+ *
+ * @param $config Array
+ */
+ protected function __construct( array $config ) {
+ parent::__construct( $config );
+
+ $this->wiki = $config['wiki'];
+ }
+
+ /**
+ * @see FileJournal::logChangeBatch()
+ * @return Status
+ */
+ protected function doLogChangeBatch( array $entries, $batchId ) {
+ $status = Status::newGood();
+
+ try {
+ $dbw = $this->getMasterDB();
+ } catch ( DBError $e ) {
+ $status->fatal( 'filejournal-fail-dbconnect', $this->backend );
+ return $status;
+ }
+
+ $now = wfTimestamp( TS_UNIX );
+
+ $data = array();
+ 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 )
+ );
+ }
+
+ try {
+ $dbw->insert( 'filejournal', $data, __METHOD__ );
+ } catch ( DBError $e ) {
+ $status->fatal( 'filejournal-fail-dbquery', $this->backend );
+ return $status;
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see FileJournal::doGetChangeEntries()
+ * @return Array
+ * @throws DBError
+ */
+ protected function doGetChangeEntries( $start, $limit ) {
+ $dbw = $this->getMasterDB();
+
+ $res = $dbw->select( 'filejournal', '*',
+ array(
+ 'fj_backend' => $this->backend,
+ 'fj_id >= ' . $dbw->addQuotes( (int)$start ) ), // $start may be 0
+ __METHOD__,
+ array_merge( array( 'ORDER BY' => 'fj_id ASC' ),
+ $limit ? array( 'LIMIT' => $limit ) : array() )
+ );
+
+ $entries = array();
+ foreach ( $res as $row ) {
+ $item = array();
+ foreach ( (array)$row as $key => $value ) {
+ $item[substr( $key, 3 )] = $value; // "fj_op" => "op"
+ }
+ $entries[] = $item;
+ }
+
+ return $entries;
+ }
+
+ /**
+ * @see FileJournal::purgeOldLogs()
+ * @return Status
+ * @throws DBError
+ */
+ protected function doPurgeOldLogs() {
+ $status = Status::newGood();
+ if ( $this->ttlDays <= 0 ) {
+ return $status; // nothing to do
+ }
+
+ $dbw = $this->getMasterDB();
+ $dbCutoff = $dbw->timestamp( time() - 86400 * $this->ttlDays );
+
+ $dbw->delete( 'filejournal',
+ array( 'fj_timestamp < ' . $dbw->addQuotes( $dbCutoff ) ),
+ __METHOD__
+ );
+
+ return $status;
+ }
+
+ /**
+ * Get a master connection to the logging DB
+ *
+ * @return DatabaseBase
+ * @throws DBError
+ */
+ protected function getMasterDB() {
+ if ( !$this->dbw ) {
+ // Get a separate connection in autocommit mode
+ $lb = wfGetLBFactory()->newMainLB();
+ $this->dbw = $lb->getConnection( DB_MASTER, array(), $this->wiki );
+ $this->dbw->clearFlag( DBO_TRX );
+ }
+ return $this->dbw;
+ }
+}
diff --git a/includes/filebackend/filejournal/FileJournal.php b/includes/filebackend/filejournal/FileJournal.php
new file mode 100644
index 00000000..ce029bbe
--- /dev/null
+++ b/includes/filebackend/filejournal/FileJournal.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * @defgroup FileJournal File journal
+ * @ingroup FileBackend
+ */
+
+/**
+ * File operation journaling.
+ *
+ * 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 FileJournal
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for handling file operation journaling.
+ *
+ * Subclasses should avoid throwing exceptions at all costs.
+ *
+ * @ingroup FileJournal
+ * @since 1.20
+ */
+abstract class FileJournal {
+ protected $backend; // string
+ protected $ttlDays; // integer
+
+ /**
+ * Construct a new instance from configuration.
+ * $config includes:
+ * 'ttlDays' : days to keep log entries around (false means "forever")
+ *
+ * @param $config Array
+ */
+ protected function __construct( array $config ) {
+ $this->ttlDays = isset( $config['ttlDays'] ) ? $config['ttlDays'] : false;
+ }
+
+ /**
+ * Create an appropriate FileJournal object from config
+ *
+ * @param $config Array
+ * @param $backend string A registered file backend name
+ * @throws MWException
+ * @return FileJournal
+ */
+ final public static function factory( array $config, $backend ) {
+ $class = $config['class'];
+ $jrn = new $class( $config );
+ if ( !$jrn instanceof self ) {
+ throw new MWException( "Class given is not an instance of FileJournal." );
+ }
+ $jrn->backend = $backend;
+ return $jrn;
+ }
+
+ /**
+ * Get a statistically unique ID string
+ *
+ * @return string <9 char TS_MW timestamp in base 36><22 random base 36 chars>
+ */
+ final public function getTimestampedUUID() {
+ $s = '';
+ for ( $i = 0; $i < 5; $i++ ) {
+ $s .= mt_rand( 0, 2147483647 );
+ }
+ $s = wfBaseConvert( sha1( $s ), 16, 36, 31 );
+ return substr( wfBaseConvert( wfTimestamp( TS_MW ), 10, 36, 9 ) . $s, 0, 31 );
+ }
+
+ /**
+ * 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)
+ * 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
+ * @return Status
+ */
+ final public function logChangeBatch( array $entries, $batchId ) {
+ if ( !count( $entries ) ) {
+ return Status::newGood();
+ }
+ return $this->doLogChangeBatch( $entries, $batchId );
+ }
+
+ /**
+ * @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
+ * @return Status
+ */
+ abstract protected function doLogChangeBatch( array $entries, $batchId );
+
+ /**
+ * Get an array of file change log entries.
+ * A starting change ID and/or limit can be specified.
+ *
+ * The result as a list of associative arrays, each having:
+ * id : unique, monotonic, ID for this change
+ * batch_uuid : UUID for an operation batch
+ * backend : the backend name
+ * op : primitive operation (create,update,delete,null)
+ * path : affected storage path
+ * new_sha1 : base 36 sha1 of the new file had the operation succeeded
+ * timestamp : TS_MW timestamp of the batch change
+
+ * Also, $next is updated to the ID of the next entry.
+ *
+ * @param $start integer Starting change ID or null
+ * @param $limit integer Maximum number of items to return
+ * @param &$next string
+ * @return Array
+ */
+ final public function getChangeEntries( $start = null, $limit = 0, &$next = null ) {
+ $entries = $this->doGetChangeEntries( $start, $limit ? $limit + 1 : 0 );
+ if ( $limit && count( $entries ) > $limit ) {
+ $last = array_pop( $entries ); // remove the extra entry
+ $next = $last['id']; // update for next call
+ } else {
+ $next = null; // end of list
+ }
+ return $entries;
+ }
+
+ /**
+ * @see FileJournal::getChangeEntries()
+ * @return Array
+ */
+ abstract protected function doGetChangeEntries( $start, $limit );
+
+ /**
+ * Purge any old log entries
+ *
+ * @return Status
+ */
+ final public function purgeOldLogs() {
+ return $this->doPurgeOldLogs();
+ }
+
+ /**
+ * @see FileJournal::purgeOldLogs()
+ * @return Status
+ */
+ abstract protected function doPurgeOldLogs();
+}
+
+/**
+ * Simple version of FileJournal that does nothing
+ * @since 1.20
+ */
+class NullFileJournal extends FileJournal {
+ /**
+ * @see FileJournal::logChangeBatch()
+ * @param $entries array
+ * @param $batchId string
+ * @return Status
+ */
+ protected function doLogChangeBatch( array $entries, $batchId ) {
+ return Status::newGood();
+ }
+
+ /**
+ * @see FileJournal::doGetChangeEntries()
+ * @return Array
+ */
+ protected function doGetChangeEntries( $start, $limit ) {
+ return array();
+ }
+
+ /**
+ * @see FileJournal::purgeOldLogs()
+ * @return Status
+ */
+ protected function doPurgeOldLogs() {
+ return Status::newGood();
+ }
+}
diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php
new file mode 100644
index 00000000..a8fe258b
--- /dev/null
+++ b/includes/filebackend/lockmanager/DBLockManager.php
@@ -0,0 +1,374 @@
+<?php
+/**
+ * Version of LockManager based on using DB table 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 based on using DB table 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
+ * own server, all having the filelocks.sql tables (with row-level locking).
+ * A majority of peer DBs must agree for a lock to be acquired.
+ *
+ * Caching is used to avoid hitting servers that are down.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+class DBLockManager extends QuorumLockManager {
+ /** @var Array Map of DB names to server config */
+ protected $dbServers; // (DB name => server config array)
+ /** @var BagOStuff */
+ protected $statusCache;
+
+ protected $lockExpiry; // integer number of seconds
+ protected $safeDelay; // integer number of seconds
+
+ protected $session = 0; // random integer
+ /** @var Array Map Database connections (DB name => Database) */
+ protected $conns = array();
+
+ /**
+ * Construct a new instance from configuration.
+ *
+ * $config paramaters include:
+ * - dbServers : Associative array of DB names to server configuration.
+ * Configuration is an associative array that includes:
+ * - host : DB server name
+ * - dbname : DB name
+ * - type : DB type (mysql,postgres,...)
+ * - user : DB user
+ * - password : DB user password
+ * - tablePrefix : DB table prefix
+ * - flags : DB flags (see DatabaseBase)
+ * - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
+ * 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).
+ * - 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
+ */
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+
+ $this->dbServers = isset( $config['dbServers'] )
+ ? $config['dbServers']
+ : array(); // likely just using 'localDBMaster'
+ // Sanitize srvsByBucket config to prevent PHP errors
+ $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
+ $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
+
+ if ( isset( $config['lockExpiry'] ) ) {
+ $this->lockExpiry = $config['lockExpiry'];
+ } else {
+ $met = ini_get( 'max_execution_time' );
+ $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
+ }
+ $this->safeDelay = ( $this->lockExpiry <= 0 )
+ ? 60 // pick a safe-ish number to match DB timeout default
+ : $this->lockExpiry; // cover worst case
+
+ foreach ( $this->srvsByBucket as $bucket ) {
+ if ( count( $bucket ) > 1 ) { // multiple peers
+ // Tracks peers that couldn't be queried recently to avoid lengthy
+ // connection timeouts. This is useless if each bucket has one peer.
+ try {
+ $this->statusCache = ObjectCache::newAccelerator( array() );
+ } catch ( MWException $e ) {
+ trigger_error( __CLASS__ .
+ " using multiple DB peers without apc, xcache, or wincache." );
+ }
+ break;
+ }
+ }
+
+ $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 ) {
+ $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 );
+ }
+ }
+ }
+
+ 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;
+ }
+
+ /**
+ * @see QuorumLockManager::isServerUp()
+ * @return bool
+ */
+ protected function isServerUp( $lockSrv ) {
+ if ( !$this->cacheCheckFailures( $lockSrv ) ) {
+ return false; // recent failure to connect
+ }
+ try {
+ $this->getConnection( $lockSrv );
+ } catch ( DBError $e ) {
+ $this->cacheRecordFailure( $lockSrv );
+ return false; // failed to connect
+ }
+ return true;
+ }
+
+ /**
+ * Get (or reuse) a connection to a lock DB
+ *
+ * @param $lockDb string
+ * @return DatabaseBase
+ * @throws DBError
+ */
+ protected function getConnection( $lockDb ) {
+ if ( !isset( $this->conns[$lockDb] ) ) {
+ $db = null;
+ if ( $lockDb === 'localDBMaster' ) {
+ $lb = wfGetLBFactory()->newMainLB();
+ $db = $lb->getConnection( DB_MASTER );
+ } elseif ( isset( $this->dbServers[$lockDb] ) ) {
+ $config = $this->dbServers[$lockDb];
+ $db = DatabaseBase::factory( $config['type'], $config );
+ }
+ if ( !$db ) {
+ return null; // config error?
+ }
+ $this->conns[$lockDb] = $db;
+ $this->conns[$lockDb]->clearFlag( DBO_TRX );
+ # If the connection drops, try to avoid letting the DB rollback
+ # and release the locks before the file operations are finished.
+ # This won't handle the case of DB server restarts however.
+ $options = array();
+ if ( $this->lockExpiry > 0 ) {
+ $options['connTimeout'] = $this->lockExpiry;
+ }
+ $this->conns[$lockDb]->setSessionOptions( $options );
+ $this->initConnection( $lockDb, $this->conns[$lockDb] );
+ }
+ if ( !$this->conns[$lockDb]->trxLevel() ) {
+ $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
+ }
+ return $this->conns[$lockDb];
+ }
+
+ /**
+ * Do additional initialization for new lock DB connection
+ *
+ * @param $lockDb string
+ * @param $db DatabaseBase
+ * @return void
+ * @throws DBError
+ */
+ protected function initConnection( $lockDb, DatabaseBase $db ) {}
+
+ /**
+ * Checks if the DB has not recently had connection/query errors.
+ * This just avoids wasting time on doomed connection attempts.
+ *
+ * @param $lockDb string
+ * @return bool
+ */
+ protected function cacheCheckFailures( $lockDb ) {
+ return ( $this->statusCache && $this->safeDelay > 0 )
+ ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
+ : true;
+ }
+
+ /**
+ * Log a lock request failure to the cache
+ *
+ * @param $lockDb string
+ * @return bool Success
+ */
+ protected function cacheRecordFailure( $lockDb ) {
+ return ( $this->statusCache && $this->safeDelay > 0 )
+ ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
+ : true;
+ }
+
+ /**
+ * Get a cache key for recent query misses for a DB
+ *
+ * @param $lockDb string
+ * @return string
+ */
+ protected function getMissKey( $lockDb ) {
+ $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative
+ return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
+ }
+
+ /**
+ * Make sure remaining locks get cleared for sanity
+ */
+ function __destruct() {
+ 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();
+ }
+ }
+}
+
+/**
+ * MySQL version of DBLockManager that supports shared locks.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class MySqlLockManager 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
+ );
+
+ /**
+ * @param $lockDb string
+ * @param $db DatabaseBase
+ */
+ protected function initConnection( $lockDb, DatabaseBase $db ) {
+ # Let this transaction see lock rows from other transactions
+ $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
+ }
+
+ /**
+ * 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 DBLockManager::getLocksOnServer()
+ * @return Status
+ */
+ protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+ $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ # Build up values for INSERT clause
+ $data = array();
+ foreach ( $keys as $key ) {
+ $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session );
+ }
+ # Block new writers...
+ $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) );
+ # Actually do the locking queries...
+ if ( $type == self::LOCK_SH ) { // reader locks
+ # 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.
+ } 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.
+ # Note: if two writers come at the same time, both bail :)
+ $blocked = $db->selectField( 'filelocks_shared', '1',
+ array( 'fls_key' => $keys, "fls_session != $encSession" ),
+ __METHOD__
+ );
+ if ( !$blocked ) {
+ # Build up values for INSERT clause
+ $data = array();
+ foreach ( $keys as $key ) {
+ $data[] = array( 'fle_key' => $key );
+ }
+ # Block new readers/writers...
+ $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
+ # Bail if there are any existing readers...
+ $blocked = $db->selectField( 'filelocks_shared', '1',
+ array( 'fls_key' => $keys, "fls_session != $encSession" ),
+ __METHOD__
+ );
+ }
+ }
+
+ if ( $blocked ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+
+ return $status;
+ }
+}
diff --git a/includes/filerepo/backend/lockmanager/FSLockManager.php b/includes/filebackend/lockmanager/FSLockManager.php
index 42074fd3..9a6206fd 100644
--- a/includes/filerepo/backend/lockmanager/FSLockManager.php
+++ b/includes/filebackend/lockmanager/FSLockManager.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Simple version of LockManager based on using FS lock files.
+ *
+ * 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
+ */
/**
* Simple version of LockManager based on using FS lock files.
@@ -27,17 +48,24 @@ class FSLockManager extends LockManager {
/**
* Construct a new instance from configuration.
- *
+ *
* $config includes:
- * 'lockDirectory' : Directory containing the lock files
+ * - lockDirectory : Directory containing the lock files
*
* @param array $config
*/
function __construct( array $config ) {
parent::__construct( $config );
+
$this->lockDir = $config['lockDirectory'];
}
+ /**
+ * @see LockManager::doLock()
+ * @param $paths array
+ * @param $type int
+ * @return Status
+ */
protected function doLock( array $paths, $type ) {
$status = Status::newGood();
@@ -56,6 +84,12 @@ class FSLockManager extends LockManager {
return $status;
}
+ /**
+ * @see LockManager::doUnlock()
+ * @param $paths array
+ * @param $type int
+ * @return Status
+ */
protected function doUnlock( array $paths, $type ) {
$status = Status::newGood();
@@ -71,7 +105,7 @@ class FSLockManager extends LockManager {
*
* @param $path string
* @param $type integer
- * @return Status
+ * @return Status
*/
protected function doSingleLock( $path, $type ) {
$status = Status::newGood();
@@ -109,10 +143,10 @@ class FSLockManager extends LockManager {
/**
* Unlock a single resource key
- *
+ *
* @param $path string
* @param $type integer
- * @return Status
+ * @return Status
*/
protected function doSingleUnlock( $path, $type ) {
$status = Status::newGood();
@@ -129,11 +163,22 @@ class FSLockManager extends LockManager {
// 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] ) ) {
- // Mark this handle to be unlocked and closed
- $handlesToClose[] = $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
+ }
// Unlock handles to release locks and delete
// any lock files that end up with no locks on them...
if ( wfIsWindows() ) {
@@ -142,7 +187,7 @@ class FSLockManager extends LockManager {
$status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
$status->merge( $this->pruneKeyLockFiles( $path ) );
} else {
- // Unix: unlink() can be used on files currently open by this
+ // Unix: unlink() can be used on files currently open by this
// process and we must do so in order to avoid race conditions
$status->merge( $this->pruneKeyLockFiles( $path ) );
$status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
@@ -152,31 +197,35 @@ class FSLockManager extends LockManager {
return $status;
}
+ /**
+ * @param $path string
+ * @param $handlesToClose array
+ * @return Status
+ */
private function closeLockHandles( $path, array $handlesToClose ) {
$status = Status::newGood();
foreach ( $handlesToClose as $handle ) {
- wfSuppressWarnings();
if ( !flock( $handle, LOCK_UN ) ) {
$status->fatal( 'lockmanager-fail-releaselock', $path );
}
if ( !fclose( $handle ) ) {
$status->warning( 'lockmanager-fail-closelock', $path );
}
- wfRestoreWarnings();
}
return $status;
}
+ /**
+ * @param $path string
+ * @return Status
+ */
private function pruneKeyLockFiles( $path ) {
$status = Status::newGood();
- if ( !count( $this->locksHeld[$path] ) ) {
- wfSuppressWarnings();
+ if ( !isset( $this->locksHeld[$path] ) ) {
# No locks are held for the lock file anymore
if ( !unlink( $this->getLockPath( $path ) ) ) {
$status->warning( 'lockmanager-fail-deletelock', $path );
}
- wfRestoreWarnings();
- unset( $this->locksHeld[$path] );
unset( $this->handles[$path] );
}
return $status;
@@ -192,11 +241,15 @@ class FSLockManager extends LockManager {
return "{$this->lockDir}/{$hash}.lock";
}
+ /**
+ * Make sure remaining locks get cleared for sanity
+ */
function __destruct() {
- // Make sure remaining locks get cleared for sanity
- foreach ( $this->locksHeld as $path => $locks ) {
- $this->doSingleUnlock( $path, self::LOCK_EX );
- $this->doSingleUnlock( $path, self::LOCK_SH );
+ while ( count( $this->locksHeld ) ) {
+ foreach ( $this->locksHeld as $path => $locks ) {
+ $this->doSingleUnlock( $path, self::LOCK_EX );
+ $this->doSingleUnlock( $path, self::LOCK_SH );
+ }
}
}
}
diff --git a/includes/filebackend/lockmanager/LSLockManager.php b/includes/filebackend/lockmanager/LSLockManager.php
new file mode 100644
index 00000000..89428182
--- /dev/null
+++ b/includes/filebackend/lockmanager/LSLockManager.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * Version of LockManager based on using lock daemon 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 a lock daemon server.
+ *
+ * Version of LockManager based on using lock daemon 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 LockServerDaemon.php, listening on a designated TCP port.
+ * A majority of peers must agree for a lock to be acquired.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+class LSLockManager 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 Array Map of server names to server config */
+ protected $lockServers; // (server name => server config array)
+
+ /** @var Array Map Server connections (server name => resource) */
+ protected $conns = array();
+
+ protected $connTimeout; // float number of seconds
+ protected $session = ''; // random SHA-1 string
+
+ /**
+ * Construct a new instance from configuration.
+ *
+ * $config paramaters include:
+ * - lockServers : Associative array of server names to configuration.
+ * Configuration is an associative array that includes:
+ * - host : IP address/hostname
+ * - port : TCP port
+ * - authKey : Secret string the lock server uses
+ * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
+ * each having an odd-numbered list of server names (peers) as values.
+ * - connTimeout : Lock server connection attempt timeout. [optional]
+ *
+ * @param Array $config
+ */
+ 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
+
+ if ( isset( $config['connTimeout'] ) ) {
+ $this->connTimeout = $config['connTimeout'];
+ } else {
+ $this->connTimeout = 3; // use some sane amount
+ }
+
+ $this->session = wfRandomString( 32 ); // 128 bits
+ }
+
+ /**
+ * @see QuorumLockManager::getLocksOnServer()
+ * @return Status
+ */
+ protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ // Send out the command and get the response...
+ $type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX';
+ $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys );
+
+ if ( $response !== 'ACQUIRED' ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::freeLocksOnServer()
+ * @return Status
+ */
+ protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ // Send out the command and get the response...
+ $type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX';
+ $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ $response = $this->sendCommand( $lockSrv, 'RELEASE', $type, $keys );
+
+ if ( $response !== 'RELEASED' ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $path );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ $status = Status::newGood();
+
+ foreach ( $this->conns as $lockSrv => $conn ) {
+ $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() );
+ if ( $response !== 'RELEASED_ALL' ) {
+ $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::isServerUp()
+ * @return bool
+ */
+ protected function isServerUp( $lockSrv ) {
+ return (bool)$this->getConnection( $lockSrv );
+ }
+
+ /**
+ * Send a command and get back the response
+ *
+ * @param $lockSrv string
+ * @param $action string
+ * @param $type string
+ * @param $values Array
+ * @return string|bool
+ */
+ protected function sendCommand( $lockSrv, $action, $type, $values ) {
+ $conn = $this->getConnection( $lockSrv );
+ if ( !$conn ) {
+ return false; // no connection
+ }
+ $authKey = $this->lockServers[$lockSrv]['authKey'];
+ // Build of the command as a flat string...
+ $values = implode( '|', $values );
+ $key = sha1( $this->session . $action . $type . $values . $authKey );
+ // Send out the command...
+ if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) {
+ return false;
+ }
+ // Get the response...
+ $response = fgets( $conn );
+ if ( $response === false ) {
+ return false;
+ }
+ return trim( $response );
+ }
+
+ /**
+ * Get (or reuse) a connection to a lock server
+ *
+ * @param $lockSrv string
+ * @return resource
+ */
+ protected function getConnection( $lockSrv ) {
+ if ( !isset( $this->conns[$lockSrv] ) ) {
+ $cfg = $this->lockServers[$lockSrv];
+ wfSuppressWarnings();
+ $errno = $errstr = '';
+ $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout );
+ wfRestoreWarnings();
+ if ( $conn === false ) {
+ return null;
+ }
+ $sec = floor( $this->connTimeout );
+ $usec = floor( ( $this->connTimeout - floor( $this->connTimeout ) ) * 1e6 );
+ stream_set_timeout( $conn, $sec, $usec );
+ $this->conns[$lockSrv] = $conn;
+ }
+ return $this->conns[$lockSrv];
+ }
+
+ /**
+ * Make sure remaining locks get cleared for sanity
+ */
+ function __destruct() {
+ $this->releaseAllLocks();
+ foreach ( $this->conns as $conn ) {
+ fclose( $conn );
+ }
+ }
+}
diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php
new file mode 100644
index 00000000..07853f87
--- /dev/null
+++ b/includes/filebackend/lockmanager/LockManager.php
@@ -0,0 +1,425 @@
+<?php
+/**
+ * @defgroup LockManager Lock management
+ * @ingroup FileBackend
+ */
+
+/**
+ * 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
+ */
+
+/**
+ * @brief Class for handling resource locking.
+ *
+ * Locks on resource keys can either be shared or exclusive.
+ *
+ * Implementations must keep track of what is locked by this proccess
+ * in-memory and support nested locking calls (using reference counting).
+ * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
+ * Locks should either be non-blocking or have low wait timeouts.
+ *
+ * Subclasses should avoid throwing exceptions at all costs.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+abstract class LockManager {
+ /** @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_EX, // subclasses may use self::LOCK_SH
+ self::LOCK_EX => self::LOCK_EX
+ );
+
+ /** @var Array Map of (resource path => lock type => count) */
+ protected $locksHeld = array();
+
+ /* 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)
+
+ /**
+ * Construct a new instance from configuration
+ *
+ * @param $config Array
+ */
+ public function __construct( array $config ) {}
+
+ /**
+ * Lock the resources at the given abstract paths
+ *
+ * @param $paths Array List of resource names
+ * @param $type integer LockManager::LOCK_* constant
+ * @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;
+ }
+
+ /**
+ * Unlock the resources at the given abstract paths
+ *
+ * @param $paths Array List of storage paths
+ * @param $type integer LockManager::LOCK_* constant
+ * @return Status
+ */
+ final public function unlock( array $paths, $type = self::LOCK_EX ) {
+ wfProfileIn( __METHOD__ );
+ $status = $this->doUnlock( array_unique( $paths ), $this->lockTypeMap[$type] );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * Get the base 36 SHA-1 of a string, padded to 31 digits
+ *
+ * @param $path string
+ * @return string
+ */
+ final protected static function sha1Base36( $path ) {
+ return wfBaseConvert( sha1( $path ), 16, 36, 31 );
+ }
+
+ /**
+ * Lock resources with the given keys and lock type
+ *
+ * @param $paths Array List of storage paths
+ * @param $type integer LockManager::LOCK_* constant
+ * @return string
+ */
+ abstract protected function doLock( array $paths, $type );
+
+ /**
+ * Unlock resources with the given keys and lock type
+ *
+ * @param $paths Array List of storage paths
+ * @param $type integer LockManager::LOCK_* constant
+ * @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;
+ }
+
+ /**
+ * 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.
+ *
+ * @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
+ */
+ 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 );
+ }
+ }
+}
+
+/**
+ * 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
+ */
+ 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 );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see LockManager::doUnlock()
+ * @param $paths array
+ * @param $type int
+ * @return Status
+ */
+ final protected function doUnlock( array $paths, $type ) {
+ $status = Status::newGood();
+
+ $pathsToUnlock = array();
+ 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->getBucketFromKey( $path );
+ $pathsToUnlock[$bucket][] = $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 => $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.
+ *
+ * @param $bucket integer
+ * @param $paths Array List of resource keys to lock
+ * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
+ * @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;
+ }
+
+ /**
+ * 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
+ * @return Status
+ */
+ final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) {
+ $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 ) );
+ }
+ }
+
+ 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().
+ *
+ * @return Status
+ */
+ abstract protected function releaseAllLocks();
+}
+
+/**
+ * Simple version of LockManager that does nothing
+ * @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/filerepo/backend/lockmanager/LockManagerGroup.php b/includes/filebackend/lockmanager/LockManagerGroup.php
index 11e77972..8c8c940a 100644
--- a/includes/filerepo/backend/lockmanager/LockManagerGroup.php
+++ b/includes/filebackend/lockmanager/LockManagerGroup.php
@@ -1,13 +1,34 @@
<?php
/**
+ * Lock manager registration 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
+ */
+
+/**
* Class to handle file lock manager registration
- *
+ *
* @ingroup LockManager
* @author Aaron Schulz
* @since 1.19
*/
class LockManagerGroup {
-
/**
* @var LockManagerGroup
*/
@@ -17,7 +38,6 @@ class LockManagerGroup {
protected $managers = array();
protected function __construct() {}
- protected function __clone() {}
/**
* @return LockManagerGroup
@@ -31,8 +51,16 @@ class LockManagerGroup {
}
/**
+ * Destroy the singleton instance, so that a new one will be created next
+ * time singleton() is called.
+ */
+ public static function destroySingleton() {
+ self::$instance = null;
+ }
+
+ /**
* Register lock managers from the global variables
- *
+ *
* @return void
*/
protected function initFromGlobals() {
@@ -86,4 +114,30 @@ class LockManagerGroup {
}
return $this->managers[$name]['instance'];
}
+
+ /**
+ * Get the default lock manager configured for the site.
+ * Returns NullLockManager if no lock manager could be found.
+ *
+ * @return LockManager
+ */
+ public function getDefault() {
+ return isset( $this->managers['default'] )
+ ? $this->get( 'default' )
+ : new NullLockManager( array() );
+ }
+
+ /**
+ * Get the default lock manager configured for the site
+ * or at least some other effective configured lock manager.
+ * Throws an exception if no lock manager could be found.
+ *
+ * @return LockManager
+ * @throws MWException
+ */
+ public function getAny() {
+ return isset( $this->managers['default'] )
+ ? $this->get( 'default' )
+ : $this->get( 'fsLockManager' );
+ }
}
diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php
new file mode 100644
index 00000000..57c0463d
--- /dev/null
+++ b/includes/filebackend/lockmanager/MemcLockManager.php
@@ -0,0 +1,319 @@
+<?php
+/**
+ * Version of LockManager based on using memcached 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 memcached servers.
+ *
+ * Version of LockManager based on using memcached 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 memcached.
+ * A majority of peers must agree for a lock to be acquired.
+ *
+ * @ingroup LockManager
+ * @since 1.20
+ */
+class MemcLockManager 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 Array Map server names to MemcachedBagOStuff objects */
+ protected $bagOStuffs = array();
+ /** @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
+
+ /**
+ * 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.
+ * - 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
+ */
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+
+ // Sanitize srvsByBucket config to prevent PHP errors
+ $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
+ $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
+
+ $memcConfig = isset( $config['memcConfig'] )
+ ? $config['memcConfig']
+ : array( 'class' => 'MemcachedPhpBagOStuff' );
+
+ foreach ( $config['lockServers'] as $name => $address ) {
+ $params = array( 'servers' => array( $address ) ) + $memcConfig;
+ $cache = ObjectCache::newFromParams( $params );
+ if ( $cache instanceof MemcachedBagOStuff ) {
+ $this->bagOStuffs[$name] = $cache;
+ } else {
+ throw new MWException(
+ 'Only MemcachedBagOStuff classes are supported by MemcLockManager.' );
+ }
+ }
+
+ $this->wikiId = isset( $config['wikiId'] ) ? $config['wikiId'] : wfWikiID();
+
+ $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
+ $this->lockExpiry = $met ? 2*(int)$met : 2*3600;
+
+ $this->session = wfRandomString( 32 );
+ }
+
+ /**
+ * @see QuorumLockManager::getLocksOnServer()
+ * @return Status
+ */
+ protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $memc = $this->getCache( $lockSrv );
+ $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records
+
+ // Lock all of the active lock record keys...
+ if ( !$this->acquireMutexes( $memc, $keys ) ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ return;
+ }
+
+ // Fetch all the existing lock records...
+ $lockRecords = $memc->getMulti( $keys );
+
+ $now = time();
+ // Check if the requested locks conflict with existing ones...
+ foreach ( $paths as $path ) {
+ $locksKey = $this->recordKeyForPath( $path );
+ $locksHeld = isset( $lockRecords[$locksKey] )
+ ? $lockRecords[$locksKey]
+ : array( self::LOCK_SH => array(), self::LOCK_EX => array() ); // init
+ foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
+ if ( $expiry < $now ) { // stale?
+ unset( $locksHeld[self::LOCK_EX][$session] );
+ } elseif ( $session !== $this->session ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+ if ( $type === self::LOCK_EX ) {
+ foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
+ if ( $expiry < $now ) { // stale?
+ unset( $locksHeld[self::LOCK_SH][$session] );
+ } elseif ( $session !== $this->session ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+ }
+ if ( $status->isOK() ) {
+ // Register the session in the lock record array
+ $locksHeld[$type][$this->session] = $now + $this->lockExpiry;
+ // We will update this record if none of the other locks conflict
+ $lockRecords[$locksKey] = $locksHeld;
+ }
+ }
+
+ // 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" );
+ }
+ }
+
+ // Unlock all of the active lock record keys...
+ $this->releaseMutexes( $memc, $keys );
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::freeLocksOnServer()
+ * @return Status
+ */
+ protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $memc = $this->getCache( $lockSrv );
+ $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records
+
+ // Lock all of the active lock record keys...
+ if ( !$this->acquireMutexes( $memc, $keys ) ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $path );
+ }
+ return;
+ }
+
+ // Fetch all the existing lock records...
+ $lockRecords = $memc->getMulti( $keys );
+
+ // Remove the requested locks from all records...
+ foreach ( $paths as $path ) {
+ $locksKey = $this->recordKeyForPath( $path ); // lock record
+ if ( !isset( $lockRecords[$locksKey] ) ) {
+ continue; // nothing to do
+ }
+ $locksHeld = $lockRecords[$locksKey];
+ if ( is_array( $locksHeld ) && isset( $locksHeld[$type] ) ) {
+ unset( $locksHeld[$type][$this->session] );
+ $ok = $memc->set( $locksKey, $locksHeld );
+ } else {
+ $ok = true;
+ }
+ if ( !$ok ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $path );
+ }
+ wfDebug( __METHOD__ . ": released lock on key $locksKey.\n" );
+ }
+
+ // Unlock all of the active lock record keys...
+ $this->releaseMutexes( $memc, $keys );
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ return Status::newGood(); // not supported
+ }
+
+ /**
+ * @see QuorumLockManager::isServerUp()
+ * @return bool
+ */
+ protected function isServerUp( $lockSrv ) {
+ return (bool)$this->getCache( $lockSrv );
+ }
+
+ /**
+ * Get the MemcachedBagOStuff object for a $lockSrv
+ *
+ * @param $lockSrv string Server name
+ * @return MemcachedBagOStuff|null
+ */
+ protected function getCache( $lockSrv ) {
+ $memc = null;
+ if ( isset( $this->bagOStuffs[$lockSrv] ) ) {
+ $memc = $this->bagOStuffs[$lockSrv];
+ if ( !isset( $this->serversUp[$lockSrv] ) ) {
+ $this->serversUp[$lockSrv] = $memc->set( 'MemcLockManager:ping', 1, 1 );
+ if ( !$this->serversUp[$lockSrv] ) {
+ trigger_error( __METHOD__ . ": Could not contact $lockSrv.", E_USER_WARNING );
+ }
+ }
+ if ( !$this->serversUp[$lockSrv] ) {
+ return null; // server appears to be down
+ }
+ }
+ return $memc;
+ }
+
+ /**
+ * @param $path string
+ * @return string
+ */
+ protected function recordKeyForPath( $path ) {
+ $hash = LockManager::sha1Base36( $path );
+ list( $db, $prefix ) = wfSplitWikiID( $this->wikiId );
+ return wfForeignMemcKey( $db, $prefix, __CLASS__, 'locks', $hash );
+ }
+
+ /**
+ * @param $memc MemcachedBagOStuff
+ * @param $keys Array List of keys to acquire
+ * @return bool
+ */
+ protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
+ $lockedKeys = array();
+
+ // Acquire the keys in lexicographical order, to avoid deadlock problems.
+ // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
+ sort( $keys );
+
+ // Try to quickly loop to acquire the keys, but back off after a few rounds.
+ // This reduces memcached spam, especially in the rare case where a server acquires
+ // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
+ $rounds = 0;
+ $start = microtime( true );
+ do {
+ if ( ( ++$rounds % 4 ) == 0 ) {
+ usleep( 1000*50 ); // 50 ms
+ }
+ foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
+ if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
+ $lockedKeys[] = $key;
+ } else {
+ continue; // acquire in order
+ }
+ }
+ } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 6 );
+
+ if ( count( $lockedKeys ) != count( $keys ) ) {
+ $this->releaseMutexes( $lockedKeys ); // failed; release what was locked
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param $memc MemcachedBagOStuff
+ * @param $keys Array List of acquired keys
+ * @return void
+ */
+ protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
+ foreach ( $keys as $key ) {
+ $memc->delete( "$key:mutex" );
+ }
+ }
+
+ /**
+ * 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/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php
index 22dbdefc..9c8d85dc 100644
--- a/includes/filerepo/FSRepo.php
+++ b/includes/filerepo/FSRepo.php
@@ -2,6 +2,21 @@
/**
* A repository for files accessible via the local filesystem.
*
+ * 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 FileRepo
*/
@@ -16,6 +31,11 @@
* @deprecated since 1.19
*/
class FSRepo extends FileRepo {
+
+ /**
+ * @param $info array
+ * @throws MWException
+ */
function __construct( array $info ) {
if ( !isset( $info['backend'] ) ) {
// B/C settings...
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
index 8d4f2bd9..a31b148a 100644
--- a/includes/filerepo/FileRepo.php
+++ b/includes/filerepo/FileRepo.php
@@ -10,6 +10,21 @@
/**
* Base code for file 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
* @ingroup FileRepo
*/
@@ -20,8 +35,6 @@
* @ingroup FileRepo
*/
class FileRepo {
- const FILES_ONLY = 1;
-
const DELETE_SOURCE = 1;
const OVERWRITE = 2;
const OVERWRITE_SAME = 4;
@@ -38,6 +51,7 @@ class FileRepo {
var $pathDisclosureProtection = 'simple'; // 'paranoid'
var $descriptionCacheExpiry, $url, $thumbUrl;
var $hashLevels, $deletedHashLevels;
+ protected $abbrvThreshold;
/**
* Factory functions for creating new files
@@ -47,7 +61,11 @@ class FileRepo {
var $oldFileFactory = false;
var $fileFactoryKey = false, $oldFileFactoryKey = false;
- function __construct( Array $info = null ) {
+ /**
+ * @param $info array|null
+ * @throws MWException
+ */
+ public function __construct( array $info = null ) {
// Verify required settings presence
if(
$info === null
@@ -96,22 +114,24 @@ class FileRepo {
? $info['deletedHashLevels']
: $this->hashLevels;
$this->transformVia404 = !empty( $info['transformVia404'] );
- $this->zones = isset( $info['zones'] )
- ? $info['zones']
- : array();
+ $this->abbrvThreshold = isset( $info['abbrvThreshold'] )
+ ? $info['abbrvThreshold']
+ : 255;
+ $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 ) {
- if ( !isset( $this->zones[$zone] ) ) {
- $this->zones[$zone] = array(
- 'container' => "{$this->name}-{$zone}",
- 'directory' => '' // container root
- );
+ if ( !isset( $this->zones[$zone]['container'] ) ) {
+ $this->zones[$zone]['container'] = "{$this->name}-{$zone}";
+ }
+ if ( !isset( $this->zones[$zone]['directory'] ) ) {
+ $this->zones[$zone]['directory'] = '';
}
}
}
/**
- * Get the file backend instance
+ * Get the file backend instance. Use this function wisely.
*
* @return FileBackend
*/
@@ -120,10 +140,20 @@ class FileRepo {
}
/**
- * Prepare a single zone or list of zones for usage.
- * See initDeletedDir() for additional setup needed for the 'deleted' zone.
- *
+ * Get an explanatory message if this repo is read-only.
+ * This checks if an administrator disabled writes to the backend.
+ *
+ * @return string|bool Returns false if the repo is not read-only
+ */
+ public function getReadOnlyReason() {
+ return $this->backend->getReadOnlyReason();
+ }
+
+ /**
+ * Check if a single zone or list of zones is defined for usage
+ *
* @param $doZones Array Only do a particular zones
+ * @throws MWException
* @return Status
*/
protected function initZones( $doZones = array() ) {
@@ -138,18 +168,6 @@ class FileRepo {
}
/**
- * Take all available measures to prevent web accessibility of new deleted
- * directories, in case the user has not configured offline storage
- *
- * @param $dir string
- * @return void
- */
- protected function initDeletedDir( $dir ) {
- $this->backend->secure( // prevent web access & dir listings
- array( 'dir' => $dir, 'noAccess' => true, 'noListing' => true ) );
- }
-
- /**
* Determine if a string is an mwrepo:// URL
*
* @param $url string
@@ -164,7 +182,7 @@ class FileRepo {
* The suffix, if supplied, is considered to be unencoded, and will be
* URL-encoded before being returned.
*
- * @param $suffix string
+ * @param $suffix string|bool
* @return string
*/
public function getVirtualUrl( $suffix = false ) {
@@ -182,6 +200,11 @@ class FileRepo {
* @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
+ }
switch ( $zone ) {
case 'public':
return $this->url;
@@ -197,12 +220,36 @@ class FileRepo {
}
/**
- * Get the backend storage path corresponding to a virtual URL
+ * Get the thumb zone URL configured to be handled by scripts like thumb_handler.php.
+ * This is probably only useful for internal requests, such as from a fast frontend server
+ * to a slower backend server.
+ *
+ * Large sites may use a different host name for uploads than for wikis. In any case, the
+ * wiki configuration is needed in order to use thumb.php. To avoid extracting the wiki ID
+ * 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
+ * @return String or false
+ */
+ public function getZoneHandlerUrl( $zone ) {
+ if ( isset( $this->zones[$zone]['handlerUrl'] )
+ && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) )
+ {
+ return $this->zones[$zone]['handlerUrl'];
+ }
+ return false;
+ }
+
+ /**
+ * Get the backend storage path corresponding to a virtual URL.
+ * Use this function wisely.
*
* @param $url string
+ * @throws MWException
* @return string
*/
- function resolveVirtualUrl( $url ) {
+ public function resolveVirtualUrl( $url ) {
if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
throw new MWException( __METHOD__.': unknown protocol' );
}
@@ -223,7 +270,7 @@ class FileRepo {
/**
* The the storage container and base path of a zone
- *
+ *
* @param $zone string
* @return Array (container, base path) or (null, null)
*/
@@ -238,7 +285,7 @@ class FileRepo {
* Get the storage path corresponding to one of the zones
*
* @param $zone string
- * @return string|null
+ * @return string|null Returns null if the zone is not defined
*/
public function getZonePath( $zone ) {
list( $container, $base ) = $this->getZoneLocation( $zone );
@@ -286,16 +333,16 @@ class FileRepo {
*
* @param $title Mixed: Title object or string
* @param $options array Associative array of options:
- * time: requested time for an archived image, or false for the
+ * 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.
+ * created at the specified time (which may be archived or current).
*
* ignoreRedirect: If true, do not follow file redirects
*
* private: If true, return restricted (deleted) files if the current
* user is allowed to view them. Otherwise, such files will not
* be found.
- * @return File|false
+ * @return File|bool False on failure
*/
public function findFile( $title, $options = array() ) {
$title = File::normalizeTitle( $title );
@@ -344,7 +391,7 @@ class FileRepo {
/**
* Find many files at once.
*
- * @param $items An array of titles, or an array of findFile() options with
+ * @param $items array An array of titles, or an array of findFile() options with
* the "title" option giving the title. Example:
*
* $findItem = array( 'title' => $title, 'private' => true );
@@ -352,7 +399,7 @@ class FileRepo {
* $repo->findFiles( $findBatch );
* @return array
*/
- public function findFiles( $items ) {
+ public function findFiles( array $items ) {
$result = array();
foreach ( $items as $item ) {
if ( is_array( $item ) ) {
@@ -377,12 +424,11 @@ class FileRepo {
* version control should return false if the time is specified.
*
* @param $sha1 String base 36 SHA-1 hash
- * @param $options Option array, same as findFile().
- * @return File|false
+ * @param $options array Option array, same as findFile().
+ * @return File|bool False on failure
*/
public function findFileFromKey( $sha1, $options = array() ) {
$time = isset( $options['time'] ) ? $options['time'] : false;
-
# First try to find a matching current version of a file...
if ( $this->fileFactoryKey ) {
$img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
@@ -411,27 +457,39 @@ class FileRepo {
* SHA-1 content hash.
*
* STUB
+ * @param $hash
+ * @return array
*/
public function findBySha1( $hash ) {
return array();
}
/**
- * Get the public root URL of the repository
+ * Get an array of arrays or iterators of file objects for files that
+ * have the given SHA-1 content hashes.
*
- * @return string|false
+ * @param $hashes array An array of hashes
+ * @return array An Array of arrays or iterators of file objects and the hash as key
*/
- public function getRootUrl() {
- return $this->url;
+ public function findBySha1s( array $hashes ) {
+ $result = array();
+ foreach ( $hashes as $hash ) {
+ $files = $this->findBySha1( $hash );
+ if ( count( $files ) ) {
+ $result[$hash] = $files;
+ }
+ }
+ return $result;
}
/**
- * Returns true if the repository uses a multi-level directory structure
+ * Get the public root URL of the repository
*
+ * @deprecated since 1.20
* @return string
*/
- public function isHashed() {
- return (bool)$this->hashLevels;
+ public function getRootUrl() {
+ return $this->getZoneUrl( 'public' );
}
/**
@@ -456,6 +514,7 @@ class FileRepo {
* Get the name of an image from its title object
*
* @param $title Title
+ * @return String
*/
public function getNameFromTitle( Title $title ) {
global $wgContLang;
@@ -483,7 +542,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
+ * @param $name string Name of file
* @return string
*/
public function getHashPath( $name ) {
@@ -491,11 +550,24 @@ 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()
+ * @return string
+ */
+ public function getTempHashPath( $suffix ) {
+ $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name>
+ $name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp
+ return self::getHashPathForLevel( $name, $this->hashLevels );
+ }
+
+ /**
* @param $name
* @param $levels
* @return string
*/
- static function getHashPathForLevel( $name, $levels ) {
+ protected static function getHashPathForLevel( $name, $levels ) {
if ( $levels == 0 ) {
return '';
} else {
@@ -531,7 +603,7 @@ class FileRepo {
*
* @param $query mixed Query string to append
* @param $entry string Entry point; defaults to index
- * @return string|false
+ * @return string|bool False on failure
*/
public function makeUrl( $query = '', $entry = 'index' ) {
if ( isset( $this->scriptDirUrl ) ) {
@@ -611,7 +683,7 @@ class FileRepo {
/**
* Get the URL of the stylesheet to apply to description pages
*
- * @return string|false
+ * @return string|bool False on failure
*/
public function getDescriptionStylesheetUrl() {
if ( isset( $this->scriptDirUrl ) ) {
@@ -624,7 +696,7 @@ class FileRepo {
/**
* Store a file to a given destination.
*
- * @param $srcPath String: source FS path, storage path, or virtual URL
+ * @param $srcPath String: source file system path, storage path, or virtual URL
* @param $dstZone String: destination zone
* @param $dstRel String: destination relative path
* @param $flags Integer: bitwise combination of the following flags:
@@ -636,10 +708,13 @@ class FileRepo {
* @return FileRepoStatus
*/
public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
+ $this->assertWritableRepo(); // fail out if read-only
+
$status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
if ( $status->successCount == 0 ) {
$status->ok = false;
}
+
return $status;
}
@@ -653,12 +728,14 @@ class FileRepo {
* self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
* same contents as the source
* self::SKIP_LOCKING Skip any file locking when doing the store
+ * @throws MWException
* @return FileRepoStatus
*/
- public function storeBatch( $triplets, $flags = 0 ) {
- $backend = $this->backend; // convenience
+ public function storeBatch( array $triplets, $flags = 0 ) {
+ $this->assertWritableRepo(); // fail out if read-only
$status = $this->newGood();
+ $backend = $this->backend; // convenience
$operations = array();
$sourceFSFilesToDelete = array(); // cleanup for disk source files
@@ -680,18 +757,12 @@ class FileRepo {
$dstPath = "$root/$dstRel";
$dstDir = dirname( $dstPath );
// Create destination directories for this triplet
- if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) {
+ if ( !$this->initDirectory( $dstDir )->isOK() ) {
return $this->newFatal( 'directorycreateerror', $dstDir );
}
- if ( $dstZone == 'deleted' ) {
- $this->initDeletedDir( $dstDir );
- }
-
// Resolve source to a storage path if virtual
- if ( self::isVirtualUrl( $srcPath ) ) {
- $srcPath = $this->resolveVirtualUrl( $srcPath );
- }
+ $srcPath = $this->resolveToStoragePath( $srcPath );
// Get the appropriate file operation
if ( FileBackend::isStoragePath( $srcPath ) ) {
@@ -729,54 +800,136 @@ class FileRepo {
/**
* Deletes a batch of files.
- * Each file can be a (zone, rel) pair, virtual url, storage path, or FS path.
+ * 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 $pairs array List of files to delete
+ * @param $files array 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 void
+ * @return FileRepoStatus
*/
- public function cleanupBatch( $files, $flags = 0 ) {
+ public function cleanupBatch( array $files, $flags = 0 ) {
+ $this->assertWritableRepo(); // fail out if read-only
+
+ $status = $this->newGood();
+
$operations = array();
- $sourceFSFilesToDelete = array(); // cleanup for disk source files
- foreach ( $files as $file ) {
- if ( is_array( $file ) ) {
+ foreach ( $files as $path ) {
+ if ( is_array( $path ) ) {
// This is a pair, extract it
- list( $zone, $rel ) = $file;
- $root = $this->getZonePath( $zone );
- $path = "$root/$rel";
+ list( $zone, $rel ) = $path;
+ $path = $this->getZonePath( $zone ) . "/$rel";
} else {
- if ( self::isVirtualUrl( $file ) ) {
- // This is a virtual url, resolve it
- $path = $this->resolveVirtualUrl( $file );
- } else {
- // This is a full file name
- $path = $file;
- }
- }
- // Get a file operation if needed
- if ( FileBackend::isStoragePath( $path ) ) {
- $operations[] = array(
- 'op' => 'delete',
- 'src' => $path,
- );
- } else {
- $sourceFSFilesToDelete[] = $path;
+ // Resolve source to a storage path if virtual
+ $path = $this->resolveToStoragePath( $path );
}
+ $operations[] = array( 'op' => 'delete', 'src' => $path );
}
// Actually delete files from storage...
$opts = array( 'force' => true );
if ( $flags & self::SKIP_LOCKING ) {
$opts['nonLocking'] = true;
}
- $this->backend->doOperations( $operations, $opts );
- // Cleanup for disk source files...
- foreach ( $sourceFSFilesToDelete as $file ) {
- wfSuppressWarnings();
- unlink( $file ); // FS cleanup
- wfRestoreWarnings();
+ $status->merge( $this->backend->doOperations( $operations, $opts ) );
+
+ return $status;
+ }
+
+ /**
+ * Import a file from the local file system into the repo.
+ * This does no locking nor journaling and overrides existing files.
+ * 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
+ * @return FileRepoStatus
+ */
+ final public function quickImport( $src, $dst, $disposition = null ) {
+ return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) );
+ }
+
+ /**
+ * Purge a file from the repo. This does no locking nor journaling.
+ * 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
+ * @return FileRepoStatus
+ */
+ final public function quickPurge( $path ) {
+ return $this->quickPurgeBatch( array( $path ) );
+ }
+
+ /**
+ * 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
+ * @return Status
+ */
+ public function quickCleanDir( $dir ) {
+ $status = $this->newGood();
+ $status->merge( $this->backend->clean(
+ array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
+
+ return $status;
+ }
+
+ /**
+ * Import a batch of files from the local file system into the repo.
+ * This does no locking nor journaling and overrides existing files.
+ * This function can be used to write to otherwise read-only foreign repos.
+ * This is intended for copying generated thumbnails into the repo.
+ *
+ * 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)
+ * @return FileRepoStatus
+ */
+ public function quickImportBatch( array $triples ) {
+ $status = $this->newGood();
+ $operations = array();
+ foreach ( $triples as $triple ) {
+ list( $src, $dst ) = $triple;
+ $src = $this->resolveToStoragePath( $src );
+ $dst = $this->resolveToStoragePath( $dst );
+ $operations[] = array(
+ 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store',
+ 'src' => $src,
+ 'dst' => $dst,
+ 'disposition' => isset( $triple[2] ) ? $triple[2] : null
+ );
+ $status->merge( $this->initDirectory( dirname( $dst ) ) );
}
+ $status->merge( $this->backend->doQuickOperations( $operations ) );
+
+ return $status;
+ }
+
+ /**
+ * Purge a batch of files from the repo.
+ * 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
+ * @return FileRepoStatus
+ */
+ public function quickPurgeBatch( array $paths ) {
+ $status = $this->newGood();
+ $operations = array();
+ foreach ( $paths as $path ) {
+ $operations[] = array(
+ 'op' => 'delete',
+ 'src' => $this->resolveToStoragePath( $path ),
+ 'ignoreMissingSource' => true
+ );
+ }
+ $status->merge( $this->backend->doQuickOperations( $operations ) );
+
+ return $status;
}
/**
@@ -784,44 +937,63 @@ 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
* by the user. The file extension will be maintained.
* @param $srcPath String: the current location of the file.
* @return FileRepoStatus object with the URL in the value.
*/
public function storeTemp( $originalName, $srcPath ) {
- $date = gmdate( "YmdHis" );
- $hashPath = $this->getHashPath( $originalName );
- $dstRel = "{$hashPath}{$date}!{$originalName}";
- $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
+ $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;
+
+ $result = $this->quickImport( $srcPath, $virtualUrl );
+ $result->value = $virtualUrl;
- $result = $this->store( $srcPath, 'temp', $dstRel, self::SKIP_LOCKING );
- $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
return $result;
}
/**
- * Concatenate a list of files into a target file location.
- *
+ * Remove a temporary file or mark it for garbage collection
+ *
+ * @param $virtualUrl String: the virtual URL returned by FileRepo::storeTemp()
+ * @return Boolean: true on success, false on failure
+ */
+ public function freeTemp( $virtualUrl ) {
+ $this->assertWritableRepo(); // fail out if read-only
+
+ $temp = $this->getVirtualUrl( 'temp' );
+ if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
+ wfDebug( __METHOD__.": Invalid temp virtual URL\n" );
+ return false;
+ }
+
+ return $this->quickPurge( $virtualUrl )->isOK();
+ }
+
+ /**
+ * 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 $flags Integer: bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the source files
* @return FileRepoStatus
*/
- function concatenate( $srcPaths, $dstPath, $flags = 0 ) {
+ public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
+ $this->assertWritableRepo(); // fail out if read-only
+
$status = $this->newGood();
$sources = array();
- $deleteOperations = array(); // post-concatenate ops
foreach ( $srcPaths as $srcPath ) {
// Resolve source to a storage path if virtual
$source = $this->resolveToStoragePath( $srcPath );
$sources[] = $source; // chunk to merge
- if ( $flags & self::DELETE_SOURCE ) {
- $deleteOperations[] = array( 'op' => 'delete', 'src' => $source );
- }
}
// Concatenate the chunks into one FS file
@@ -832,50 +1004,34 @@ class FileRepo {
}
// Delete the sources if required
- if ( $deleteOperations ) {
- $opts = array( 'force' => true );
- $status->merge( $this->backend->doOperations( $deleteOperations, $opts ) );
+ if ( $flags & self::DELETE_SOURCE ) {
+ $status->merge( $this->quickPurgeBatch( $srcPaths ) );
}
- // Make sure status is OK, despite any $deleteOperations fatals
+ // Make sure status is OK, despite any quickPurgeBatch() fatals
$status->setResult( true );
return $status;
}
/**
- * Remove a temporary file or mark it for garbage collection
- *
- * @param $virtualUrl String: the virtual URL returned by FileRepo::storeTemp()
- * @return Boolean: true on success, false on failure
- */
- public function freeTemp( $virtualUrl ) {
- $temp = "mwrepo://{$this->name}/temp";
- if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
- wfDebug( __METHOD__.": Invalid temp virtual URL\n" );
- return false;
- }
- $path = $this->resolveVirtualUrl( $virtualUrl );
- $op = array( 'op' => 'delete', 'src' => $path );
- $status = $this->backend->doOperation( $op );
- return $status->isOK();
- }
-
- /**
* Copy or move a file either from a storage path, virtual URL,
- * or FS path, into this repository at the specified destination location.
+ * or file system path, into this repository at the specified destination location.
*
* Returns a FileRepoStatus object. On success, the value contains "new" or
* "archived", to indicate whether the file was new with that name.
*
- * @param $srcPath String: the source FS path, storage path, or URL
+ * @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
* 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
+ * @return FileRepoStatus
*/
public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
+ $this->assertWritableRepo(); // fail out if read-only
+
$status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
if ( $status->successCount == 0 ) {
$status->ok = false;
@@ -885,6 +1041,7 @@ class FileRepo {
} else {
$status->value = false;
}
+
return $status;
}
@@ -894,11 +1051,13 @@ class FileRepo {
* @param $triplets Array: (source, dest, archive) triplets 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( $triplets, $flags = 0 ) {
- $backend = $this->backend; // convenience
+ public function publishBatch( array $triplets, $flags = 0 ) {
+ $this->assertWritableRepo(); // fail out if read-only
+ $backend = $this->backend; // convenience
// Try creating directories
$status = $this->initZones( 'public' );
if ( !$status->isOK() ) {
@@ -913,9 +1072,7 @@ class FileRepo {
foreach ( $triplets as $i => $triplet ) {
list( $srcPath, $dstRel, $archiveRel ) = $triplet;
// Resolve source to a storage path if virtual
- if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
- $srcPath = $this->resolveVirtualUrl( $srcPath );
- }
+ $srcPath = $this->resolveToStoragePath( $srcPath );
if ( !$this->validateFilename( $dstRel ) ) {
throw new MWException( 'Validation error in $dstRel' );
}
@@ -930,10 +1087,10 @@ class FileRepo {
$dstDir = dirname( $dstPath );
$archiveDir = dirname( $archivePath );
// Abort immediately on directory creation errors since they're likely to be repetitive
- if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) {
+ if ( !$this->initDirectory( $dstDir )->isOK() ) {
return $this->newFatal( 'directorycreateerror', $dstDir );
}
- if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) {
+ if ( !$this->initDirectory($archiveDir )->isOK() ) {
return $this->newFatal( 'directorycreateerror', $archiveDir );
}
@@ -999,15 +1156,50 @@ 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
+ * @return Status
+ */
+ protected function initDirectory( $dir ) {
+ $path = $this->resolveToStoragePath( $dir );
+ list( $b, $container, $r ) = FileBackend::splitStoragePath( $path );
+
+ $params = array( 'dir' => $path );
+ if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) {
+ # Take all available measures to prevent web accessibility of new deleted
+ # directories, in case the user has not configured offline storage
+ $params = array( 'noAccess' => true, 'noListing' => true ) + $params;
+ }
+
+ return $this->backend->prepare( $params );
+ }
+
+ /**
+ * Deletes a directory if empty.
+ *
+ * @param $dir string Virtual URL (or storage path) of directory to clean
+ * @return Status
+ */
+ public function cleanDir( $dir ) {
+ $this->assertWritableRepo(); // fail out if read-only
+
+ $status = $this->newGood();
+ $status->merge( $this->backend->clean(
+ array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
+
+ return $status;
+ }
+
+ /**
* Checks existence of a a file
*
- * @param $file Virtual URL (or storage path) of file to check
- * @param $flags Integer: bitwise combination of the following flags:
- * self::FILES_ONLY Mark file as existing only if it is a file (not directory)
+ * @param $file string Virtual URL (or storage path) of file to check
* @return bool
*/
- public function fileExists( $file, $flags = 0 ) {
- $result = $this->fileExistsBatch( array( $file ), $flags );
+ public function fileExists( $file ) {
+ $result = $this->fileExistsBatch( array( $file ) );
return $result[0];
}
@@ -1015,27 +1207,14 @@ class FileRepo {
* Checks existence of an array of files.
*
* @param $files Array: Virtual URLs (or storage paths) of files to check
- * @param $flags Integer: bitwise combination of the following flags:
- * self::FILES_ONLY Mark file as existing only if it is a file (not directory)
- * @return Either array of files and existence flags, or false
+ * @return array|bool Either array of files and existence flags, or false
*/
- public function fileExistsBatch( $files, $flags = 0 ) {
+ public function fileExistsBatch( array $files ) {
$result = array();
foreach ( $files as $key => $file ) {
- if ( self::isVirtualUrl( $file ) ) {
- $file = $this->resolveVirtualUrl( $file );
- }
- if ( FileBackend::isStoragePath( $file ) ) {
- $result[$key] = $this->backend->fileExists( array( 'src' => $file ) );
- } else {
- if ( $flags & self::FILES_ONLY ) {
- $result[$key] = is_file( $file ); // FS only
- } else {
- $result[$key] = file_exists( $file ); // FS only
- }
- }
+ $file = $this->resolveToStoragePath( $file );
+ $result[$key] = $this->backend->fileExists( array( 'src' => $file ) );
}
-
return $result;
}
@@ -1050,6 +1229,8 @@ class FileRepo {
* @return FileRepoStatus object
*/
public function delete( $srcRel, $archiveRel ) {
+ $this->assertWritableRepo(); // fail out if read-only
+
return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
}
@@ -1067,10 +1248,11 @@ class FileRepo {
* 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.
+ * @throws MWException
* @return FileRepoStatus
*/
- public function deleteBatch( $sourceDestPairs ) {
- $backend = $this->backend; // convenience
+ public function deleteBatch( array $sourceDestPairs ) {
+ $this->assertWritableRepo(); // fail out if read-only
// Try creating directories
$status = $this->initZones( array( 'public', 'deleted' ) );
@@ -1080,14 +1262,14 @@ class FileRepo {
$status = $this->newGood();
+ $backend = $this->backend; // convenience
$operations = array();
// Validate filenames and create archive directories
foreach ( $sourceDestPairs as $pair ) {
list( $srcRel, $archiveRel ) = $pair;
if ( !$this->validateFilename( $srcRel ) ) {
throw new MWException( __METHOD__.':Validation error in $srcRel' );
- }
- if ( !$this->validateFilename( $archiveRel ) ) {
+ } elseif ( !$this->validateFilename( $archiveRel ) ) {
throw new MWException( __METHOD__.':Validation error in $archiveRel' );
}
@@ -1099,10 +1281,9 @@ class FileRepo {
$archiveDir = dirname( $archivePath ); // does not touch FS
// Create destination directories
- if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) {
+ if ( !$this->initDirectory( $archiveDir )->isOK() ) {
return $this->newFatal( 'directorycreateerror', $archiveDir );
}
- $this->initDeletedDir( $archiveDir );
$operations[] = array(
'op' => 'move',
@@ -1124,9 +1305,19 @@ class FileRepo {
}
/**
+ * Delete files in the deleted directory if they are not referenced in the filearchive table
+ *
+ * STUB
+ */
+ public function cleanupDeletedBatch( array $storageKeys ) {
+ $this->assertWritableRepo();
+ }
+
+ /**
* Get a relative path for a deletion archive key,
* e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
*
+ * @param $key string
* @return string
*/
public function getDeletedHashPath( $key ) {
@@ -1155,7 +1346,7 @@ class FileRepo {
/**
* Get a local FS copy of a file with a given virtual URL/storage path.
* Temporary files may be purged when the file object falls out of scope.
- *
+ *
* @param $virtualUrl string
* @return TempFSFile|null Returns null on failure
*/
@@ -1168,7 +1359,7 @@ class FileRepo {
* Get a local FS file with a given virtual URL/storage path.
* The file is either an original or a copy. It should not be changed.
* Temporary files may be purged when the file object falls out of scope.
- *
+ *
* @param $virtualUrl string
* @return FSFile|null Returns null on failure.
*/
@@ -1193,7 +1384,7 @@ class FileRepo {
* Get the timestamp of a file with a given virtual URL/storage path
*
* @param $virtualUrl string
- * @return string|false
+ * @return string|bool False on failure
*/
public function getFileTimestamp( $virtualUrl ) {
$path = $this->resolveToStoragePath( $virtualUrl );
@@ -1201,18 +1392,14 @@ class FileRepo {
}
/**
- * Get the sha1 of a file with a given virtual URL/storage path
+ * Get the sha1 (base 36) of a file with a given virtual URL/storage path
*
* @param $virtualUrl string
- * @return string|false
+ * @return string|bool
*/
public function getFileSha1( $virtualUrl ) {
$path = $this->resolveToStoragePath( $virtualUrl );
- $tmpFile = $this->backend->getLocalReference( array( 'src' => $path ) );
- if ( !$tmpFile ) {
- return false;
- }
- return $tmpFile->getSha1Base36();
+ return $this->backend->getFileSha1Base36( array( 'src' => $path ) );
}
/**
@@ -1276,23 +1463,7 @@ class FileRepo {
if ( strval( $filename ) == '' ) {
return false;
}
- if ( wfIsWindows() ) {
- $filename = strtr( $filename, '\\', '/' );
- }
- /**
- * Use the same traversal protection as Title::secureAndSplit()
- */
- if ( strpos( $filename, '.' ) !== false &&
- ( $filename === '.' || $filename === '..' ||
- strpos( $filename, './' ) === 0 ||
- strpos( $filename, '../' ) === 0 ||
- strpos( $filename, '/./' ) !== false ||
- strpos( $filename, '/../' ) !== false ) )
- {
- return false;
- } else {
- return true;
- }
+ return FileBackend::isPathTraversalFree( $filename );
}
/**
@@ -1303,11 +1474,9 @@ class FileRepo {
function getErrorCleanupFunction() {
switch ( $this->pathDisclosureProtection ) {
case 'none':
+ case 'simple': // b/c
$callback = array( $this, 'passThrough' );
break;
- case 'simple':
- $callback = array( $this, 'simpleClean' );
- break;
default: // 'paranoid'
$callback = array( $this, 'paranoidClean' );
}
@@ -1330,22 +1499,6 @@ class FileRepo {
* @param $param string
* @return string
*/
- function simpleClean( $param ) {
- global $IP;
- if ( !isset( $this->simpleCleanPairs ) ) {
- $this->simpleCleanPairs = array(
- $IP => '$IP', // sanity
- );
- }
- return strtr( $param, $this->simpleCleanPairs );
- }
-
- /**
- * Path disclosure protection function
- *
- * @param $param string
- * @return string
- */
function passThrough( $param ) {
return $param;
}
@@ -1355,7 +1508,7 @@ class FileRepo {
*
* @return FileRepoStatus
*/
- function newFatal( $message /*, parameters...*/ ) {
+ public function newFatal( $message /*, parameters...*/ ) {
$params = func_get_args();
array_unshift( $params, $this );
return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params );
@@ -1364,20 +1517,14 @@ class FileRepo {
/**
* Create a new good result
*
+ * @param $value null|string
* @return FileRepoStatus
*/
- function newGood( $value = null ) {
+ public function newGood( $value = null ) {
return FileRepoStatus::newGood( $this, $value );
}
/**
- * Delete files in the deleted directory if they are not referenced in the filearchive table
- *
- * STUB
- */
- public function cleanupDeletedBatch( $storageKeys ) {}
-
- /**
* Checks if there is a redirect named as $title. If there is, return the
* title object. If not, return false.
* STUB
@@ -1413,6 +1560,21 @@ class FileRepo {
}
/**
+ * Get the portion of the file that contains the origin file name.
+ * If that name is too long, then the name "thumbnail.<ext>" will be given.
+ *
+ * @param $name string
+ * @return string
+ */
+ public function nameForThumb( $name ) {
+ if ( strlen( $name ) > $this->abbrvThreshold ) {
+ $ext = FileBackend::extensionFromPath( $name );
+ $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
+ }
+ return $name;
+ }
+
+ /**
* Returns true if this the local file repository.
*
* @return bool
@@ -1427,8 +1589,9 @@ class FileRepo {
* The parameters are the parts of the key, as for wfMemcKey().
*
* STUB
+ * @return bool
*/
- function getSharedCacheKey( /*...*/ ) {
+ public function getSharedCacheKey( /*...*/ ) {
return false;
}
@@ -1439,13 +1602,43 @@ class FileRepo {
*
* @return string
*/
- function getLocalCacheKey( /*...*/ ) {
+ public function getLocalCacheKey( /*...*/ ) {
$args = func_get_args();
array_unshift( $args, 'filerepo', $this->getName() );
return call_user_func_array( 'wfMemcKey', $args );
}
/**
+ * Get an temporary FileRepo associated with this repo.
+ * Files will be created in the temp zone of this repo and
+ * thumbnails in a /temp subdirectory in thumb zone of this repo.
+ * It will have the same backend as this repo.
+ *
+ * @return TempFileRepo
+ */
+ public function getTempRepo() {
+ return new TempFileRepo( array(
+ 'name' => "{$this->name}-temp",
+ 'backend' => $this->backend,
+ 'zones' => array(
+ 'public' => array(
+ 'container' => $this->zones['temp']['container'],
+ 'directory' => $this->zones['temp']['directory']
+ ),
+ 'thumb' => array(
+ 'container' => $this->zones['thumb']['container'],
+ 'directory' => ( $this->zones['thumb']['directory'] == '' )
+ ? 'temp'
+ : $this->zones['thumb']['directory'] . '/temp'
+ )
+ ),
+ 'url' => $this->getZoneUrl( 'temp' ),
+ 'thumbUrl' => $this->getZoneUrl( 'thumb' ) . '/temp',
+ 'hashLevels' => $this->hashLevels // performance
+ ) );
+ }
+
+ /**
* Get an UploadStash associated with this repo.
*
* @return UploadStash
@@ -1453,4 +1646,22 @@ class FileRepo {
public function getUploadStash() {
return new UploadStash( $this );
}
+
+ /**
+ * Throw an exception if this repo is read-only by design.
+ * This does not and should not check getReadOnlyReason().
+ *
+ * @return void
+ * @throws MWException
+ */
+ protected function assertWritableRepo() {}
+}
+
+/**
+ * FileRepo for temporary files created via FileRepo::getTempRepo()
+ */
+class TempFileRepo extends FileRepo {
+ public function getTempRepo() {
+ throw new MWException( "Cannot get a temp repo from a temp repo." );
+ }
}
diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php
index 4eea9030..6f28b104 100644
--- a/includes/filerepo/FileRepoStatus.php
+++ b/includes/filerepo/FileRepoStatus.php
@@ -1,6 +1,21 @@
<?php
/**
- * Generic operation result for FileRepo-related operations
+ * Generic operation result for FileRepo-related operations.
+ *
+ * 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 FileRepo
diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php
index e544defb..13de9e6b 100644
--- a/includes/filerepo/ForeignAPIRepo.php
+++ b/includes/filerepo/ForeignAPIRepo.php
@@ -2,6 +2,21 @@
/**
* Foreign repository accessible through api.php requests.
*
+ * 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 FileRepo
*/
@@ -36,6 +51,9 @@ class ForeignAPIRepo extends FileRepo {
protected $mQueryCache = array();
protected $mFileExists = array();
+ /**
+ * @param $info array|null
+ */
function __construct( $info ) {
global $wgLocalFileRepo;
parent::__construct( $info );
@@ -66,6 +84,8 @@ class ForeignAPIRepo extends FileRepo {
* Per docs in FileRepo, this needs to return false if we don't support versioned
* files. Well, we don't.
*
+ * @param $title Title
+ * @param $time string|bool
* @return File
*/
function newFile( $title, $time = false ) {
@@ -76,38 +96,10 @@ class ForeignAPIRepo extends FileRepo {
}
/**
- * No-ops
+ * @param $files array
+ * @return array
*/
-
- function storeBatch( $triplets, $flags = 0 ) {
- return false;
- }
-
- function storeTemp( $originalName, $srcPath ) {
- return false;
- }
-
- function concatenate( $fileList, $targetPath, $flags = 0 ){
- return false;
- }
-
- function append( $srcPath, $toAppendPath, $flags = 0 ){
- return false;
- }
-
- function appendFinish( $toAppendPath ){
- return false;
- }
-
- function publishBatch( $triplets, $flags = 0 ) {
- return false;
- }
-
- function deleteBatch( $sourceDestPairs ) {
- return false;
- }
-
- function fileExistsBatch( $files, $flags = 0 ) {
+ function fileExistsBatch( array $files ) {
$results = array();
foreach ( $files as $k => $f ) {
if ( isset( $this->mFileExists[$k] ) ) {
@@ -119,6 +111,10 @@ class ForeignAPIRepo extends FileRepo {
# same repo.
$results[$k] = false;
unset( $files[$k] );
+ } elseif ( FileBackend::isStoragePath( $f ) ) {
+ $results[$k] = false;
+ unset( $files[$k] );
+ wfWarn( "Got mwstore:// path '$f'." );
}
}
@@ -134,17 +130,25 @@ class ForeignAPIRepo extends FileRepo {
return $results;
}
+ /**
+ * @param $virtualUrl string
+ * @return bool
+ */
function getFileProps( $virtualUrl ) {
return false;
}
+ /**
+ * @param $query array
+ * @return string
+ */
function fetchImageQuery( $query ) {
global $wgMemc;
$query = array_merge( $query,
array(
- 'format' => 'json',
- 'action' => 'query',
+ 'format' => 'json',
+ 'action' => 'query',
'redirects' => 'true'
) );
if ( $this->mApiBase ) {
@@ -173,6 +177,10 @@ class ForeignAPIRepo extends FileRepo {
return FormatJson::decode( $this->mQueryCache[$url], true );
}
+ /**
+ * @param $data array
+ * @return bool|array
+ */
function getImageInfo( $data ) {
if( $data && isset( $data['query']['pages'] ) ) {
foreach( $data['query']['pages'] as $info ) {
@@ -184,6 +192,10 @@ class ForeignAPIRepo extends FileRepo {
return false;
}
+ /**
+ * @param $hash string
+ * @return array
+ */
function findBySha1( $hash ) {
$results = $this->fetchImageQuery( array(
'aisha1base36' => $hash,
@@ -202,6 +214,14 @@ class ForeignAPIRepo extends FileRepo {
return $ret;
}
+ /**
+ * @param $name string
+ * @param $width int
+ * @param $height int
+ * @param $result null
+ * @param $otherParams string
+ * @return bool
+ */
function getThumbUrl( $name, $width = -1, $height = -1, &$result = null, $otherParams = '' ) {
$data = $this->fetchImageQuery( array(
'titles' => 'File:' . $name,
@@ -230,10 +250,14 @@ class ForeignAPIRepo extends FileRepo {
* @param $name String is a dbkey form of a title
* @param $width
* @param $height
- * @param String $param 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="" ) {
+ function getThumbUrlFromCache( $name, $width, $height, $params = "" ) {
global $wgMemc;
+ // We can't check the local cache using FileRepo functions because
+ // we override fileExistsBatch(). We have to use the FileBackend directly.
+ $backend = $this->getBackend(); // convenience
if ( !$this->canCacheThumbs() ) {
$result = null; // can't pass "null" by reference, but it's ok as default value
@@ -274,9 +298,11 @@ class ForeignAPIRepo extends FileRepo {
$localFilename = $localPath . "/" . $fileName;
$localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . rawurlencode( $name ) . "/" . rawurlencode( $fileName );
- if( $this->fileExists( $localFilename ) && isset( $metadata['timestamp'] ) ) {
+ if( $backend->fileExists( array( 'src' => $localFilename ) )
+ && isset( $metadata['timestamp'] ) )
+ {
wfDebug( __METHOD__ . " Thumbnail was already downloaded before\n" );
- $modified = $this->getFileTimestamp( $localFilename );
+ $modified = $backend->getFileTimestamp( array( 'src' => $localFilename ) );
$remoteModified = strtotime( $metadata['timestamp'] );
$current = time();
$diff = abs( $modified - $current );
@@ -294,16 +320,14 @@ class ForeignAPIRepo extends FileRepo {
return false;
}
+
# @todo FIXME: Delete old thumbs that aren't being used. Maintenance script?
- wfSuppressWarnings();
- $backend = $this->getBackend();
- $op = array( 'op' => 'create', 'dst' => $localFilename, 'content' => $thumb );
- if( !$backend->doOperation( $op )->isOK() ) {
- wfRestoreWarnings();
- wfDebug( __METHOD__ . " could not write to thumb path\n" );
+ $backend->prepare( array( 'dir' => dirname( $localFilename ) ) );
+ $params = array( 'dst' => $localFilename, 'content' => $thumb );
+ if( !$backend->quickCreate( $params )->isOK() ) {
+ wfDebug( __METHOD__ . " could not write to thumb path '$localFilename'\n" );
return $foreignUrl;
}
- wfRestoreWarnings();
$knownThumbUrls[$sizekey] = $localUrl;
$wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry );
wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" );
@@ -312,6 +336,8 @@ class ForeignAPIRepo extends FileRepo {
/**
* @see FileRepo::getZoneUrl()
+ * @param $zone String
+ * @return String
*/
function getZoneUrl( $zone ) {
switch ( $zone ) {
@@ -326,6 +352,8 @@ class ForeignAPIRepo extends FileRepo {
/**
* Get the local directory corresponding to one of the basic zones
+ * @param $zone string
+ * @return bool|null|string
*/
function getZonePath( $zone ) {
$supported = array( 'public', 'thumb' );
@@ -345,6 +373,7 @@ class ForeignAPIRepo extends FileRepo {
/**
* The user agent the ForeignAPIRepo will use.
+ * @return string
*/
public static function getUserAgent() {
return Http::userAgent() . " ForeignAPIRepo/" . self::VERSION;
@@ -353,6 +382,10 @@ class ForeignAPIRepo extends FileRepo {
/**
* Like a Http:get request, but with custom User-Agent.
* @see Http:get
+ * @param $url string
+ * @param $timeout string
+ * @param $options array
+ * @return bool|String
*/
public static function httpGet( $url, $timeout = 'default', $options = array() ) {
$options['timeout'] = $timeout;
@@ -362,7 +395,7 @@ class ForeignAPIRepo extends FileRepo {
$options['method'] = "GET";
if ( !isset( $options['timeout'] ) ) {
- $options['timeout'] = 'default';
+ $options['timeout'] = 'default';
}
$req = MWHttpRequest::factory( $url, $options );
@@ -370,13 +403,24 @@ class ForeignAPIRepo extends FileRepo {
$status = $req->execute();
if ( $status->isOK() ) {
- return $req->getContent();
+ return $req->getContent();
} else {
- return false;
+ return false;
}
}
+ /**
+ * @param $callback Array|string
+ * @throws MWException
+ */
function enumFiles( $callback ) {
throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) );
}
+
+ /**
+ * @throws MWException
+ */
+ protected function assertWritableRepo() {
+ throw new MWException( get_class( $this ) . ': write operations are not supported.' );
+ }
}
diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php
index 0311ebcd..4b206c3d 100644
--- a/includes/filerepo/ForeignDBRepo.php
+++ b/includes/filerepo/ForeignDBRepo.php
@@ -1,6 +1,21 @@
<?php
/**
- * A foreign repository with an accessible MediaWiki database
+ * A foreign repository with an accessible MediaWiki 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
* @ingroup FileRepo
@@ -21,6 +36,9 @@ class ForeignDBRepo extends LocalRepo {
var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' );
var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' );
+ /**
+ * @param $info array|null
+ */
function __construct( $info ) {
parent::__construct( $info );
$this->dbType = $info['dbType'];
@@ -33,6 +51,9 @@ class ForeignDBRepo extends LocalRepo {
$this->hasSharedCache = $info['hasSharedCache'];
}
+ /**
+ * @return DatabaseBase
+ */
function getMasterDB() {
if ( !isset( $this->dbConn ) ) {
$this->dbConn = DatabaseBase::factory( $this->dbType,
@@ -49,10 +70,16 @@ class ForeignDBRepo extends LocalRepo {
return $this->dbConn;
}
+ /**
+ * @return DatabaseBase
+ */
function getSlaveDB() {
return $this->getMasterDB();
}
+ /**
+ * @return bool
+ */
function hasSharedCache() {
return $this->hasSharedCache;
}
@@ -61,6 +88,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.
* The parameters are the parts of the key, as for wfMemcKey().
+ * @return bool|mixed
*/
function getSharedCacheKey( /*...*/ ) {
if ( $this->hasSharedCache() ) {
@@ -72,13 +100,7 @@ class ForeignDBRepo extends LocalRepo {
}
}
- function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
- throw new MWException( get_class($this) . ': write operations are not supported' );
- }
- function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
- throw new MWException( get_class($this) . ': write operations are not supported' );
- }
- function deleteBatch( $sourceDestPairs ) {
- throw new MWException( get_class($this) . ': write operations are not supported' );
+ protected function assertWritableRepo() {
+ throw new MWException( get_class( $this ) . ': write operations are not supported.' );
}
}
diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php
index 28b48b5e..bd76fce7 100644
--- a/includes/filerepo/ForeignDBViaLBRepo.php
+++ b/includes/filerepo/ForeignDBViaLBRepo.php
@@ -1,6 +1,21 @@
<?php
/**
- * A foreign repository with a MediaWiki database accessible via the configured LBFactory
+ * A foreign repository with a MediaWiki database accessible via the configured LBFactory.
+ *
+ * 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 FileRepo
@@ -16,6 +31,9 @@ class ForeignDBViaLBRepo extends LocalRepo {
var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' );
var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' );
+ /**
+ * @param $info array|null
+ */
function __construct( $info ) {
parent::__construct( $info );
$this->wiki = $info['wiki'];
@@ -23,10 +41,16 @@ class ForeignDBViaLBRepo extends LocalRepo {
$this->hasSharedCache = $info['hasSharedCache'];
}
+ /**
+ * @return DatabaseBase
+ */
function getMasterDB() {
return wfGetDB( DB_MASTER, array(), $this->wiki );
}
+ /**
+ * @return DatabaseBase
+ */
function getSlaveDB() {
return wfGetDB( DB_SLAVE, array(), $this->wiki );
}
@@ -39,6 +63,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.
* The parameters are the parts of the key, as for wfMemcKey().
+ * @return bool|string
*/
function getSharedCacheKey( /*...*/ ) {
if ( $this->hasSharedCache() ) {
@@ -50,13 +75,7 @@ class ForeignDBViaLBRepo extends LocalRepo {
}
}
- function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
- throw new MWException( get_class($this) . ': write operations are not supported' );
- }
- function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
- throw new MWException( get_class($this) . ': write operations are not supported' );
- }
- function deleteBatch( $fileMap ) {
- throw new MWException( get_class($this) . ': write operations are not supported' );
+ protected function assertWritableRepo() {
+ throw new MWException( get_class( $this ) . ': write operations are not supported.' );
}
}
diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php
index cc23fa31..0954422d 100644
--- a/includes/filerepo/LocalRepo.php
+++ b/includes/filerepo/LocalRepo.php
@@ -3,6 +3,21 @@
* Local repository that stores files in the local filesystem and registers them
* in the wiki's own 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
* @ingroup FileRepo
*/
@@ -24,7 +39,7 @@ class LocalRepo extends FileRepo {
/**
* @throws MWException
* @param $row
- * @return File
+ * @return LocalFile
*/
function newFileFromRow( $row ) {
if ( isset( $row->img_name ) ) {
@@ -55,7 +70,7 @@ class LocalRepo extends FileRepo {
*
* @return FileRepoStatus
*/
- function cleanupDeletedBatch( $storageKeys ) {
+ function cleanupDeletedBatch( array $storageKeys ) {
$backend = $this->backend; // convenience
$root = $this->getZonePath( 'deleted' );
$dbw = $this->getMasterDB();
@@ -64,7 +79,7 @@ class LocalRepo extends FileRepo {
foreach ( $storageKeys as $key ) {
$hashPath = $this->getDeletedHashPath( $key );
$path = "$root/$hashPath$key";
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
// Check for usage in deleted/hidden files and pre-emptively
// lock the key to avoid any future use until we are finished.
$deleted = $this->deletedFileHasKey( $key, 'lock' );
@@ -80,7 +95,7 @@ class LocalRepo extends FileRepo {
wfDebug( __METHOD__ . ": $key still in use\n" );
$status->successCount++;
}
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
}
return $status;
}
@@ -133,7 +148,7 @@ class LocalRepo extends FileRepo {
public static function getHashFromKey( $key ) {
return strtok( $key, '.' );
}
-
+
/**
* Checks if there is a redirect named as $title
*
@@ -183,12 +198,12 @@ class LocalRepo extends FileRepo {
}
}
-
/**
* Function link Title::getArticleID().
* We can't say Title object, what database it should use, so we duplicate that function here.
*
* @param $title Title
+ * @return bool|int|mixed
*/
protected function getArticleID( $title ) {
if( !$title instanceof Title ) {
@@ -219,7 +234,9 @@ class LocalRepo extends FileRepo {
$res = $dbr->select(
'image',
LocalFile::selectFields(),
- array( 'img_sha1' => $hash )
+ array( 'img_sha1' => $hash ),
+ __METHOD__,
+ array( 'ORDER BY' => 'img_name' )
);
$result = array();
@@ -232,7 +249,41 @@ class LocalRepo extends FileRepo {
}
/**
+ * Get an array of arrays or iterators of file objects for files that
+ * have the given SHA-1 content hashes.
+ *
+ * Overrides generic implementation in FileRepo for performance reason
+ *
+ * @param $hashes array 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 ) ) {
+ return array(); //empty parameter
+ }
+
+ $dbr = $this->getSlaveDB();
+ $res = $dbr->select(
+ 'image',
+ LocalFile::selectFields(),
+ array( 'img_sha1' => $hashes ),
+ __METHOD__,
+ array( 'ORDER BY' => 'img_name' )
+ );
+
+ $result = array();
+ foreach ( $res as $row ) {
+ $file = $this->newFileFromRow( $row );
+ $result[$file->getSha1()][] = $file;
+ }
+ $res->free();
+
+ return $result;
+ }
+
+ /**
* Get a connection to the slave DB
+ * @return DatabaseBase
*/
function getSlaveDB() {
return wfGetDB( DB_SLAVE );
@@ -240,6 +291,7 @@ class LocalRepo extends FileRepo {
/**
* Get a connection to the master DB
+ * @return DatabaseBase
*/
function getMasterDB() {
return wfGetDB( DB_MASTER );
diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php
index 65318f40..dda51cea 100644
--- a/includes/filerepo/NullRepo.php
+++ b/includes/filerepo/NullRepo.php
@@ -2,6 +2,21 @@
/**
* File repository with no files.
*
+ * 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 FileRepo
*/
@@ -11,40 +26,13 @@
* @ingroup FileRepo
*/
class NullRepo extends FileRepo {
- function __construct( $info ) {}
- function storeBatch( $triplets, $flags = 0 ) {
- return false;
- }
+ /**
+ * @param $info array|null
+ */
+ function __construct( $info ) {}
- function storeTemp( $originalName, $srcPath ) {
- return false;
- }
- function append( $srcPath, $toAppendPath, $flags = 0 ){
- return false;
- }
- function appendFinish( $toAppendPath ){
- return false;
- }
- function publishBatch( $triplets, $flags = 0 ) {
- return false;
- }
- function deleteBatch( $sourceDestPairs ) {
- return false;
- }
- function fileExistsBatch( $files, $flags = 0 ) {
- return false;
- }
- function getFileProps( $virtualUrl ) {
- return false;
- }
- function newFile( $title, $time = false ) {
- return false;
- }
- function findFile( $title, $options = array() ) {
- return false;
- }
- function concatenate( $fileList, $targetPath, $flags = 0 ) {
- return false;
+ protected function assertWritableRepo() {
+ throw new MWException( get_class( $this ) . ': write operations are not supported.' );
}
}
diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php
index 334ef2b8..f9e57599 100644
--- a/includes/filerepo/RepoGroup.php
+++ b/includes/filerepo/RepoGroup.php
@@ -1,6 +1,21 @@
<?php
/**
- * Prioritized list of file repositories
+ * Prioritized list of file 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
* @ingroup FileRepo
@@ -12,7 +27,6 @@
* @ingroup FileRepo
*/
class RepoGroup {
-
/**
* @var LocalRepo
*/
@@ -26,7 +40,7 @@ class RepoGroup {
* @var RepoGroup
*/
protected static $instance;
- const MAX_CACHE_SIZE = 1000;
+ const MAX_CACHE_SIZE = 500;
/**
* Get a RepoGroup instance. At present only one instance of RepoGroup is
@@ -65,7 +79,7 @@ class RepoGroup {
/**
* Construct a group of file repositories.
*
- * @param $localInfo Associative array for local repo's info
+ * @param $localInfo array Associative array for local repo's info
* @param $foreignInfo Array of repository info arrays.
* Each info array is an associative array with the 'class' member
* giving the class name. The entire array is passed to the repository
@@ -114,50 +128,47 @@ class RepoGroup {
&& empty( $options['private'] )
&& empty( $options['bypassCache'] ) )
{
- $useCache = true;
$time = isset( $options['time'] ) ? $options['time'] : '';
$dbkey = $title->getDBkey();
if ( isset( $this->cache[$dbkey][$time] ) ) {
wfDebug( __METHOD__.": got File:$dbkey from process cache\n" );
# Move it to the end of the list so that we can delete the LRU entry later
- $tmp = $this->cache[$dbkey];
- unset( $this->cache[$dbkey] );
- $this->cache[$dbkey] = $tmp;
+ $this->pingCache( $dbkey );
# Return the entry
return $this->cache[$dbkey][$time];
- } else {
- # Add a negative cache entry, may be overridden
- $this->trimCache();
- $this->cache[$dbkey][$time] = false;
- $cacheEntry =& $this->cache[$dbkey][$time];
}
+ $useCache = true;
} else {
$useCache = false;
}
# Check the local repo
$image = $this->localRepo->findFile( $title, $options );
- if ( $image ) {
- if ( $useCache ) {
- $cacheEntry = $image;
- }
- return $image;
- }
# Check the foreign repos
- foreach ( $this->foreignRepos as $repo ) {
- $image = $repo->findFile( $title, $options );
- if ( $image ) {
- if ( $useCache ) {
- $cacheEntry = $image;
+ if ( !$image ) {
+ foreach ( $this->foreignRepos as $repo ) {
+ $image = $repo->findFile( $title, $options );
+ if ( $image ) {
+ break;
}
- return $image;
}
}
- # Not found, do not override negative cache
- return false;
+
+ $image = $image ? $image : false; // type sanity
+ # Cache file existence or non-existence
+ if ( $useCache && ( !$image || $image->isCacheable() ) ) {
+ $this->trimCache();
+ $this->cache[$dbkey][$time] = $image;
+ }
+
+ return $image;
}
+ /**
+ * @param $inputItems array
+ * @return array
+ */
function findFiles( $inputItems ) {
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
@@ -189,6 +200,8 @@ class RepoGroup {
/**
* Interface for FileRepo::checkRedirect()
+ * @param $title Title
+ * @return bool
*/
function checkRedirect( Title $title ) {
if ( !$this->reposInitialised ) {
@@ -213,7 +226,7 @@ class RepoGroup {
* Returns false if the file does not exist.
*
* @param $hash String base 36 SHA-1 hash
- * @param $options Option array, same as findFile()
+ * @param $options array Option array, same as findFile()
* @return File object or false if it is not found
*/
function findFileFromKey( $hash, $options = array() ) {
@@ -246,11 +259,36 @@ class RepoGroup {
foreach ( $this->foreignRepos as $repo ) {
$result = array_merge( $result, $repo->findBySha1( $hash ) );
}
+ usort( $result, 'File::compare' );
+ return $result;
+ }
+
+ /**
+ * Find all instances of files with this keys
+ *
+ * @param $hashes Array base 36 SHA-1 hashes
+ * @return Array of array of File objects
+ */
+ function findBySha1s( array $hashes ) {
+ if ( !$this->reposInitialised ) {
+ $this->initialiseRepos();
+ }
+
+ $result = $this->localRepo->findBySha1s( $hashes );
+ foreach ( $this->foreignRepos as $repo ) {
+ $result = array_merge_recursive( $result, $repo->findBySha1s( $hashes ) );
+ }
+ //sort the merged (and presorted) sublist of each hash
+ foreach( $result as $hash => $files ) {
+ usort( $result[$hash], 'File::compare' );
+ }
return $result;
}
/**
* Get the repo instance with a given key.
+ * @param $index string|int
+ * @return bool|LocalRepo
*/
function getRepo( $index ) {
if ( !$this->reposInitialised ) {
@@ -264,16 +302,20 @@ class RepoGroup {
return false;
}
}
+
/**
* Get the repo instance by its name
+ * @param $name string
+ * @return bool
*/
function getRepoByName( $name ) {
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
}
foreach ( $this->foreignRepos as $repo ) {
- if ( $repo->name == $name)
+ if ( $repo->name == $name ) {
return $repo;
+ }
}
return false;
}
@@ -294,6 +336,7 @@ class RepoGroup {
*
* @param $callback Callback: the function to call
* @param $params Array: optional additional parameters to pass to the function
+ * @return bool
*/
function forEachForeignRepo( $callback, $params = array() ) {
foreach( $this->foreignRepos as $repo ) {
@@ -339,7 +382,9 @@ class RepoGroup {
/**
* Split a virtual URL into repo, zone and rel parts
- * @return an array containing repo, zone and rel
+ * @param $url string
+ * @throws MWException
+ * @return array containing repo, zone and rel
*/
function splitVirtualUrl( $url ) {
if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
@@ -353,6 +398,10 @@ class RepoGroup {
return $bits;
}
+ /**
+ * @param $fileName string
+ * @return array
+ */
function getFileProps( $fileName ) {
if ( FileRepo::isVirtualUrl( $fileName ) ) {
list( $repoName, /* $zone */, /* $rel */ ) = $this->splitVirtualUrl( $fileName );
@@ -367,6 +416,17 @@ class RepoGroup {
}
/**
+ * Move a cache entry to the top (such as when accessed)
+ */
+ protected function pingCache( $key ) {
+ if ( isset( $this->cache[$key] ) ) {
+ $tmp = $this->cache[$key];
+ unset( $this->cache[$key] );
+ $this->cache[$key] = $tmp;
+ }
+ }
+
+ /**
* Limit cache memory
*/
protected function trimCache() {
diff --git a/includes/filerepo/backend/FSFileBackend.php b/includes/filerepo/backend/FSFileBackend.php
deleted file mode 100644
index 1a4c44ad..00000000
--- a/includes/filerepo/backend/FSFileBackend.php
+++ /dev/null
@@ -1,600 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * Class for a file system (FS) based file backend.
- *
- * All "containers" each map to a directory under the backend's base directory.
- * For backwards-compatibility, some container paths can be set to custom paths.
- * The wiki ID will not be used in any custom paths, so this should be avoided.
- *
- * Having directories with thousands of files will diminish performance.
- * Sharding can be accomplished by using FileRepo-style hash paths.
- *
- * Status messages should avoid mentioning the internal FS paths.
- * PHP warnings are assumed to be logged rather than output.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class FSFileBackend extends FileBackendStore {
- protected $basePath; // string; directory holding the container directories
- /** @var Array Map of container names to root paths */
- protected $containerPaths = array(); // for custom container paths
- protected $fileMode; // integer; file permission mode
-
- protected $hadWarningErrors = array();
-
- /**
- * @see FileBackendStore::__construct()
- * Additional $config params include:
- * basePath : File system directory that holds containers.
- * containerPaths : Map of container names to custom file system directories.
- * This should only be used for backwards-compatibility.
- * fileMode : Octal UNIX file permissions to use on files stored.
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
-
- // Remove any possible trailing slash from directories
- if ( isset( $config['basePath'] ) ) {
- $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
- } else {
- $this->basePath = null; // none; containers must have explicit paths
- }
-
- if ( isset( $config['containerPaths'] ) ) {
- $this->containerPaths = (array)$config['containerPaths'];
- foreach ( $this->containerPaths as &$path ) {
- $path = rtrim( $path, '/' ); // remove trailing slash
- }
- }
-
- $this->fileMode = isset( $config['fileMode'] )
- ? $config['fileMode']
- : 0644;
- }
-
- /**
- * @see FileBackendStore::resolveContainerPath()
- */
- protected function resolveContainerPath( $container, $relStoragePath ) {
- // Check that container has a root directory
- if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
- // Check for sane relative paths (assume the base paths are OK)
- if ( $this->isLegalRelPath( $relStoragePath ) ) {
- return $relStoragePath;
- }
- }
- return null;
- }
-
- /**
- * Sanity check a relative file system path for validity
- *
- * @param $path string Normalized relative path
- * @return bool
- */
- protected function isLegalRelPath( $path ) {
- // Check for file names longer than 255 chars
- if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
- return false;
- }
- if ( wfIsWindows() ) { // NTFS
- return !preg_match( '![:*?"<>|]!', $path );
- } else {
- return true;
- }
- }
-
- /**
- * 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
- * @return string|null
- */
- protected function containerFSRoot( $shortCont, $fullCont ) {
- if ( isset( $this->containerPaths[$shortCont] ) ) {
- return $this->containerPaths[$shortCont];
- } elseif ( isset( $this->basePath ) ) {
- return "{$this->basePath}/{$fullCont}";
- }
- return null; // no container base path defined
- }
-
- /**
- * Get the absolute file system path for a storage path
- *
- * @param $storagePath string Storage path
- * @return string|null
- */
- protected function resolveToFSPath( $storagePath ) {
- list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
- if ( $relPath === null ) {
- return null; // invalid
- }
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath );
- $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- if ( $relPath != '' ) {
- $fsPath .= "/{$relPath}";
- }
- return $fsPath;
- }
-
- /**
- * @see FileBackendStore::isPathUsableInternal()
- */
- public function isPathUsableInternal( $storagePath ) {
- $fsPath = $this->resolveToFSPath( $storagePath );
- if ( $fsPath === null ) {
- return false; // invalid
- }
- $parentDir = dirname( $fsPath );
-
- if ( file_exists( $fsPath ) ) {
- $ok = is_file( $fsPath ) && is_writable( $fsPath );
- } else {
- $ok = is_dir( $parentDir ) && is_writable( $parentDir );
- }
-
- return $ok;
- }
-
- /**
- * @see FileBackendStore::doStoreInternal()
- */
- 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 ( 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;
- }
- }
-
- $ok = copy( $params['src'], $dest );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
- return $status;
- }
-
- $this->chmod( $dest );
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doCopyInternal()
- */
- protected function doCopyInternal( array $params ) {
- $status = Status::newGood();
-
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
- return $status;
- }
-
- $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;
- }
- }
-
- $ok = copy( $source, $dest );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- return $status;
- }
-
- $this->chmod( $dest );
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doMoveInternal()
- */
- protected function doMoveInternal( array $params ) {
- $status = Status::newGood();
-
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
- return $status;
- }
-
- $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'] ) ) {
- // 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;
- }
- }
-
- $ok = rename( $source, $dest );
- clearstatcache(); // file no longer at source
- if ( !$ok ) {
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
- return $status;
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doDeleteInternal()
- */
- protected function doDeleteInternal( array $params ) {
- $status = Status::newGood();
-
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
- return $status;
- }
-
- if ( !is_file( $source ) ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- }
- return $status; // do nothing; either OK or bad status
- }
-
- $ok = unlink( $source );
- if ( !$ok ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- return $status;
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doCreateInternal()
- */
- 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;
- }
- }
-
- $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 FileBackendStore::doPrepareInternal()
- */
- protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
- $status = Status::newGood();
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- if ( !wfMkdirParents( $dir ) ) { // make directory and its parents
- $status->fatal( 'directorycreateerror', $params['dir'] );
- } elseif ( !is_writable( $dir ) ) {
- $status->fatal( 'directoryreadonlyerror', $params['dir'] );
- } elseif ( !is_readable( $dir ) ) {
- $status->fatal( 'directorynotreadableerror', $params['dir'] );
- }
- return $status;
- }
-
- /**
- * @see FileBackendStore::doSecureInternal()
- */
- protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
- $status = Status::newGood();
- list( $b, $shortCont, $r ) = 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" ) ) {
- $bytes = file_put_contents( "{$dir}/index.html", '' );
- if ( !$bytes ) {
- $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'] ) ) {
- if ( !file_exists( "{$contRoot}/.htaccess" ) ) {
- $bytes = file_put_contents( "{$contRoot}/.htaccess", "Deny from all\n" );
- if ( !$bytes ) {
- $storeDir = "mwstore://{$this->name}/{$shortCont}";
- $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
- return $status;
- }
- }
- }
- return $status;
- }
-
- /**
- * @see FileBackendStore::doCleanInternal()
- */
- protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
- $status = Status::newGood();
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- wfSuppressWarnings();
- if ( is_dir( $dir ) ) {
- rmdir( $dir ); // remove directory if empty
- }
- wfRestoreWarnings();
- return $status;
- }
-
- /**
- * @see FileBackendStore::doFileExists()
- */
- protected function doGetFileStat( array $params ) {
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- return false; // invalid storage path
- }
-
- $this->trapWarnings(); // don't trust 'false' if there were errors
- $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
- $hadError = $this->untrapWarnings();
-
- if ( $stat ) {
- return array(
- 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
- 'size' => $stat['size']
- );
- } elseif ( !$hadError ) {
- return false; // file does not exist
- } else {
- return null; // failure
- }
- }
-
- /**
- * @see FileBackendStore::doClearCache()
- */
- protected function doClearCache( array $paths = null ) {
- clearstatcache(); // clear the PHP file stat cache
- }
-
- /**
- * @see FileBackendStore::getFileListInternal()
- */
- public function getFileListInternal( $fullCont, $dirRel, array $params ) {
- list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- $exists = is_dir( $dir );
- if ( !$exists ) {
- wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
- return array(); // nothing under this dir
- }
- $readable = is_readable( $dir );
- if ( !$readable ) {
- wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
- return null; // bad permissions?
- }
- return new FSFileBackendFileList( $dir );
- }
-
- /**
- * @see FileBackendStore::getLocalReference()
- */
- public function getLocalReference( array $params ) {
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- return null;
- }
- return new FSFile( $source );
- }
-
- /**
- * @see FileBackendStore::getLocalCopy()
- */
- public function getLocalCopy( array $params ) {
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- return null;
- }
-
- // Create a new temporary file with the same extension...
- $ext = FileBackend::extensionFromPath( $params['src'] );
- $tmpFile = TempFSFile::factory( wfBaseName( $source ) . '_', $ext );
- if ( !$tmpFile ) {
- return null;
- }
- $tmpPath = $tmpFile->getPath();
-
- // Copy the source file over the temp file
- $ok = copy( $source, $tmpPath );
- if ( !$ok ) {
- return null;
- }
-
- $this->chmod( $tmpPath );
-
- return $tmpFile;
- }
-
- /**
- * Chmod a file, suppressing the warnings
- *
- * @param $path string Absolute file system path
- * @return bool Success
- */
- protected function chmod( $path ) {
- wfSuppressWarnings();
- $ok = chmod( $path, $this->fileMode );
- wfRestoreWarnings();
-
- return $ok;
- }
-
- /**
- * Listen for E_WARNING errors and track whether any happen
- *
- * @return bool
- */
- protected function trapWarnings() {
- $this->hadWarningErrors[] = false; // push to stack
- set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
- return false; // invoke normal PHP error handler
- }
-
- /**
- * Stop listening for E_WARNING errors and return true if any happened
- *
- * @return bool
- */
- protected function untrapWarnings() {
- restore_error_handler(); // restore previous handler
- return array_pop( $this->hadWarningErrors ); // pop from stack
- }
-
- private function handleWarning() {
- $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
- return true; // suppress from PHP handler
- }
-}
-
-/**
- * Wrapper around RecursiveDirectoryIterator that catches
- * exception or does any custom behavoir that we may want.
- * Do not use this class from places outside FSFileBackend.
- *
- * @ingroup FileBackend
- */
-class FSFileBackendFileList implements Iterator {
- /** @var RecursiveIteratorIterator */
- protected $iter;
- protected $suffixStart; // integer
- protected $pos = 0; // integer
-
- /**
- * @param $dir string file system directory
- */
- public function __construct( $dir ) {
- $dir = realpath( $dir ); // normalize
- $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
- try {
- # Get an iterator that will return leaf nodes (non-directories)
- if ( MWInit::classExists( 'FilesystemIterator' ) ) { // PHP >= 5.3
- # RecursiveDirectoryIterator extends FilesystemIterator.
- # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
- $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
- $this->iter = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator( $dir, $flags ) );
- } else { // PHP < 5.3
- # RecursiveDirectoryIterator extends DirectoryIterator
- $this->iter = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator( $dir ) );
- }
- } catch ( UnexpectedValueException $e ) {
- $this->iter = null; // bad permissions? deleted?
- }
- }
-
- public function current() {
- // Return only the relative path and normalize slashes to FileBackend-style
- // Make sure to use the realpath since the suffix is based upon that
- return str_replace( '\\', '/',
- substr( realpath( $this->iter->current() ), $this->suffixStart ) );
- }
-
- public function key() {
- return $this->pos;
- }
-
- public function next() {
- try {
- $this->iter->next();
- } catch ( UnexpectedValueException $e ) {
- $this->iter = null;
- }
- ++$this->pos;
- }
-
- public function rewind() {
- $this->pos = 0;
- try {
- $this->iter->rewind();
- } catch ( UnexpectedValueException $e ) {
- $this->iter = null;
- }
- }
-
- public function valid() {
- return $this->iter && $this->iter->valid();
- }
-}
diff --git a/includes/filerepo/backend/FileBackend.php b/includes/filerepo/backend/FileBackend.php
deleted file mode 100644
index 9433bcb4..00000000
--- a/includes/filerepo/backend/FileBackend.php
+++ /dev/null
@@ -1,1739 +0,0 @@
-<?php
-/**
- * @defgroup FileBackend File backend
- * @ingroup FileRepo
- *
- * This module regroup classes meant for MediaWiki to interacts with
- */
-
-/**
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * Base class for all file backend classes (including multi-write backends).
- *
- * This class defines the methods as abstract that subclasses must implement.
- * Outside callers can assume that all backends will have these functions.
- *
- * All "storage paths" are of the format "mwstore://backend/container/path".
- * The paths use UNIX file system (FS) notation, though any particular backend may
- * not actually be using a local filesystem. Therefore, the 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.
- *
- * 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
- * store files in such a way that no files and directories are under the same path.
- *
- * Methods should avoid throwing exceptions at all costs.
- * As a corollary, external dependencies should be kept to a minimum.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileBackend {
- protected $name; // string; unique backend name
- protected $wikiId; // string; unique wiki name
- protected $readOnly; // string; read-only explanation message
- /** @var LockManager */
- protected $lockManager;
-
- /**
- * Create a new backend instance from configuration.
- * This should only be called from within FileBackendGroup.
- *
- * $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 should consist of alphanumberic, '-', and '_' characters.
- * 'lockManager' : Registered name of a file lock manager to use.
- * 'readOnly' : Write operations are disallowed if this is a non-empty string.
- * It should be an explanation for the backend being read-only.
- *
- * @param $config Array
- */
- public function __construct( array $config ) {
- $this->name = $config['name'];
- if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
- throw new MWException( "Backend name `{$this->name}` is invalid." );
- }
- $this->wikiId = isset( $config['wikiId'] )
- ? $config['wikiId']
- : wfWikiID(); // e.g. "my_wiki-en_"
- $this->lockManager = ( $config['lockManager'] instanceof LockManager )
- ? $config['lockManager']
- : LockManagerGroup::singleton()->get( $config['lockManager'] );
- $this->readOnly = isset( $config['readOnly'] )
- ? (string)$config['readOnly']
- : '';
- }
-
- /**
- * Get the unique backend name.
- * We may have multiple different backends of the same type.
- * For example, we can have two Swift backends using different proxies.
- *
- * @return string
- */
- final public function getName() {
- return $this->name;
- }
-
- /**
- * Check if this backend is read-only
- *
- * @return bool
- */
- final public function isReadOnly() {
- return ( $this->readOnly != '' );
- }
-
- /**
- * Get an explanatory message if this backend is read-only
- *
- * @return string|false Returns falls if the backend is not read-only
- */
- final public function getReadOnlyReason() {
- return ( $this->readOnly != '' ) ? $this->readOnly : false;
- }
-
- /**
- * This is the main entry point into the backend for write operations.
- * Callers supply an ordered list of operations to perform as a transaction.
- * Files will be locked, the stat cache cleared, and then the operations attempted.
- * If any serious errors occur, all attempted operations will be rolled back.
- *
- * $ops is an array of arrays. The outer array holds a list of operations.
- * Each inner array is a set of key value pairs that specify an operation.
- *
- * Supported operations and their parameters:
- * a) Create a new file in storage with the contents of a string
- * array(
- * 'op' => 'create',
- * 'dst' => <storage path>,
- * 'content' => <string of new file contents>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>
- * )
- * b) Copy a file system file into storage
- * array(
- * 'op' => 'store',
- * 'src' => <file system path>,
- * 'dst' => <storage path>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>
- * )
- * c) Copy a file within storage
- * array(
- * 'op' => 'copy',
- * 'src' => <storage path>,
- * 'dst' => <storage path>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>
- * )
- * d) Move a file within storage
- * array(
- * 'op' => 'move',
- * 'src' => <storage path>,
- * 'dst' => <storage path>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>
- * )
- * e) Delete a file within storage
- * array(
- * 'op' => 'delete',
- * 'src' => <storage path>,
- * 'ignoreMissingSource' => <boolean>
- * )
- * f) Do nothing (no-op)
- * array(
- * 'op' => 'null',
- * )
- *
- * Boolean flags for operations (operation-specific):
- * '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.
- *
- * $opts is an associative of boolean flags, including:
- * 'force' : Errors that would normally cause a rollback do not.
- * The remaining operations are still attempted if any fail.
- * '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.
- *
- * Remarks on locking:
- * File system paths given to operations should refer to files that are
- * already locked or otherwise safe from modification from other processes.
- * Normally these files will be new temp files, which should be adequate.
- *
- * Return value:
- * This returns a Status, which contains all warnings and fatals that occured
- * during the operation. The 'failCount', 'successCount', and 'success' members
- * will reflect each operation attempted. The status will be "OK" unless:
- * a) unexpected operation errors occurred (network partitions, disk full...)
- * b) significant operation errors occured and 'force' was not set
- *
- * @param $ops Array List of operations to execute in order
- * @param $opts Array Batch operation options
- * @return Status
- */
- final public function doOperations( array $ops, array $opts = array() ) {
- if ( $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- if ( empty( $opts['force'] ) ) { // sanity
- unset( $opts['nonLocking'] );
- unset( $opts['allowStale'] );
- }
- return $this->doOperationsInternal( $ops, $opts );
- }
-
- /**
- * @see FileBackend::doOperations()
- */
- abstract protected function doOperationsInternal( array $ops, array $opts );
-
- /**
- * Same as doOperations() except it takes a single operation.
- * If you are doing a batch of operations that should either
- * all succeed or all fail, then use that function instead.
- *
- * @see FileBackend::doOperations()
- *
- * @param $op Array Operation
- * @param $opts Array Operation options
- * @return Status
- */
- final public function doOperation( array $op, array $opts = array() ) {
- return $this->doOperations( array( $op ), $opts );
- }
-
- /**
- * Performs a single create operation.
- * This sets $params['op'] to 'create' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
- * @return Status
- */
- final public function create( array $params, array $opts = array() ) {
- $params['op'] = 'create';
- return $this->doOperation( $params, $opts );
- }
-
- /**
- * Performs a single store operation.
- * This sets $params['op'] to 'store' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
- * @return Status
- */
- final public function store( array $params, array $opts = array() ) {
- $params['op'] = 'store';
- return $this->doOperation( $params, $opts );
- }
-
- /**
- * Performs a single copy operation.
- * This sets $params['op'] to 'copy' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
- * @return Status
- */
- final public function copy( array $params, array $opts = array() ) {
- $params['op'] = 'copy';
- return $this->doOperation( $params, $opts );
- }
-
- /**
- * Performs a single move operation.
- * This sets $params['op'] to 'move' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
- * @return Status
- */
- final public function move( array $params, array $opts = array() ) {
- $params['op'] = 'move';
- return $this->doOperation( $params, $opts );
- }
-
- /**
- * Performs a single delete operation.
- * This sets $params['op'] to 'delete' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param $params Array Operation parameters
- * @param $opts Array Operation options
- * @return Status
- */
- final public function delete( array $params, array $opts = array() ) {
- $params['op'] = 'delete';
- return $this->doOperation( $params, $opts );
- }
-
- /**
- * 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.
- * $params include:
- * srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
- * dst : file system path to 0-byte temp file
- *
- * @param $params Array Operation parameters
- * @return Status
- */
- abstract public function concatenate( array $params );
-
- /**
- * Prepare a storage directory for usage.
- * This will create any required containers and parent directories.
- * Backends using key/value stores only need to create the container.
- *
- * $params include:
- * dir : storage directory
- *
- * @param $params Array
- * @return Status
- */
- final public function prepare( array $params ) {
- if ( $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- return $this->doPrepare( $params );
- }
-
- /**
- * @see FileBackend::prepare()
- */
- abstract protected function doPrepare( array $params );
-
- /**
- * Take measures to block web access to a storage directory and
- * the container it belongs to. FS backends might add .htaccess
- * files whereas key/value store backends might restrict container
- * access to the auth user that represents end-users in web request.
- * This is not guaranteed to actually do anything.
- *
- * $params include:
- * dir : storage directory
- * noAccess : try to deny file access
- * noListing : try to deny file listing
- *
- * @param $params Array
- * @return Status
- */
- final public function secure( array $params ) {
- if ( $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- $status = $this->doPrepare( $params ); // dir must exist to restrict it
- if ( $status->isOK() ) {
- $status->merge( $this->doSecure( $params ) );
- }
- return $status;
- }
-
- /**
- * @see FileBackend::secure()
- */
- abstract protected function doSecure( array $params );
-
- /**
- * Delete a storage directory if it is empty.
- * Backends using key/value stores may do nothing unless the directory
- * is that of an empty container, in which case it should be deleted.
- *
- * $params include:
- * dir : storage directory
- *
- * @param $params Array
- * @return Status
- */
- final public function clean( array $params ) {
- if ( $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- return $this->doClean( $params );
- }
-
- /**
- * @see FileBackend::clean()
- */
- abstract protected function doClean( array $params );
-
- /**
- * Check if a file exists at a storage path in the backend.
- * This returns false if only a directory exists at the path.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return bool|null Returns null on failure
- */
- abstract public function fileExists( array $params );
-
- /**
- * Get the last-modified timestamp of the file at a storage path.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return string|false TS_MW timestamp or false on failure
- */
- abstract public function getFileTimestamp( array $params );
-
- /**
- * Get the contents of a file at a storage path in the backend.
- * This should be avoided for potentially large files.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return string|false Returns false on failure
- */
- abstract public function getFileContents( array $params );
-
- /**
- * Get the size (bytes) of a file at a storage path in the backend.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return integer|false Returns false on failure
- */
- abstract public function getFileSize( array $params );
-
- /**
- * Get quick information about a file at a storage path in the backend.
- * If the file does not exist, then this returns false.
- * Otherwise, the result is an associative array that includes:
- * mtime : the last-modified timestamp (TS_MW)
- * size : the file size (bytes)
- * Additional values may be included for internal use only.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return Array|false|null Returns null on failure
- */
- abstract public function getFileStat( array $params );
-
- /**
- * Get a SHA-1 hash of the file at a storage path in the backend.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return string|false Hash string or false on failure
- */
- abstract public function getFileSha1Base36( array $params );
-
- /**
- * Get the properties of the file at a storage path in the backend.
- * Returns FSFile::placeholderProps() on failure.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return Array
- */
- abstract public function getFileProps( array $params );
-
- /**
- * Stream the file at a storage path in the backend.
- * If the file does not exists, a 404 error will be given.
- * Appropriate HTTP headers (Status, Content-Type, Content-Length)
- * must be sent if streaming began, while none should be sent otherwise.
- * Implementations should flush the output buffer before sending data.
- *
- * $params include:
- * src : source storage path
- * headers : additional HTTP headers to send on success
- * latest : use the latest available data
- *
- * @param $params Array
- * @return Status
- */
- abstract public function streamFile( array $params );
-
- /**
- * Returns a file system file, identical to the file at a storage path.
- * The file returned is either:
- * a) A local copy of the file at a storage path in the backend.
- * The temporary copy will have the same extension as the source.
- * b) An original of the file at a storage path in the backend.
- * Temporary files may be purged when the file object falls out of scope.
- *
- * Write operations should *never* be done on this file as some backends
- * may do internal tracking or may be instances of FileBackendMultiWrite.
- * 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.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return FSFile|null Returns null on failure
- */
- abstract public function getLocalReference( 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.
- *
- * $params include:
- * src : source storage path
- * latest : use the latest available data
- *
- * @param $params Array
- * @return TempFSFile|null Returns null on failure
- */
- abstract public function getLocalCopy( array $params );
-
- /**
- * Get an iterator to list out all stored files under a storage directory.
- * If the directory is of the form "mwstore://backend/container",
- * then all files in the container should be listed.
- * If the directory is of form "mwstore://backend/container/dir",
- * then all files under that container directory should be listed.
- * Results should be storage paths relative to the given directory.
- *
- * Storage backends with eventual consistency might return stale data.
- *
- * $params include:
- * dir : storage path directory
- *
- * @return Traversable|Array|null Returns null on failure
- */
- abstract public function getFileList( array $params );
-
- /**
- * Invalidate any in-process file existence and property cache.
- * If $paths is given, then only the cache for those files will be cleared.
- *
- * @param $paths Array Storage paths (optional)
- * @return void
- */
- public function clearCache( array $paths = null ) {}
-
- /**
- * Lock the files at the given storage paths in the backend.
- * This will either lock all the files or none (on failure).
- *
- * Callers should consider using getScopedFileLocks() instead.
- *
- * @param $paths Array Storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @return Status
- */
- final public function lockFiles( array $paths, $type ) {
- 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
- * @return Status
- */
- final public function unlockFiles( array $paths, $type ) {
- return $this->lockManager->unlock( $paths, $type );
- }
-
- /**
- * Lock the files at the given storage paths in the backend.
- * This will either lock all the files or none (on failure).
- * On failure, the status object will be updated with errors.
- *
- * 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
- * @return ScopedLock|null Returns null on failure
- */
- final public function getScopedFileLocks( array $paths, $type, Status $status ) {
- return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
- }
-
- /**
- * Check if a given path is a "mwstore://" path.
- * This does not do any further validation or any existence checks.
- *
- * @param $path string
- * @return bool
- */
- final public static function isStoragePath( $path ) {
- return ( strpos( $path, 'mwstore://' ) === 0 );
- }
-
- /**
- * Split a storage path into a backend name, a container name,
- * 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
- * @return Array (backend, container, rel object) or (null, null, null)
- */
- final public static function splitStoragePath( $storagePath ) {
- if ( self::isStoragePath( $storagePath ) ) {
- // Remove the "mwstore://" prefix and split the path
- $parts = explode( '/', substr( $storagePath, 10 ), 3 );
- if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
- if ( count( $parts ) == 3 ) {
- return $parts; // e.g. "backend/container/path"
- } else {
- return array( $parts[0], $parts[1], '' ); // e.g. "backend/container"
- }
- }
- }
- return array( null, null, null );
- }
-
- /**
- * 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
- * @return string|null
- */
- final public static function normalizeStoragePath( $storagePath ) {
- list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
- if ( $relPath !== null ) { // must be for this backend
- $relPath = self::normalizeContainerPath( $relPath );
- if ( $relPath !== null ) {
- return ( $relPath != '' )
- ? "mwstore://{$backend}/{$container}/{$relPath}"
- : "mwstore://{$backend}/{$container}";
- }
- }
- return null;
- }
-
- /**
- * Validate and normalize a relative storage path.
- * Null is returned if the path involves directory traversal.
- * Traversal is insecure for FS backends and broken for others.
- *
- * @param $path string Storage path relative to a container
- * @return string|null
- */
- final protected static function normalizeContainerPath( $path ) {
- // Normalize directory separators
- $path = strtr( $path, '\\', '/' );
- // Collapse any consecutive directory separators
- $path = preg_replace( '![/]{2,}!', '/', $path );
- // Remove any leading directory separator
- $path = ltrim( $path, '/' );
- // Use the same traversal protection as Title::secureAndSplit()
- if ( strpos( $path, '.' ) !== false ) {
- if (
- $path === '.' ||
- $path === '..' ||
- strpos( $path, './' ) === 0 ||
- strpos( $path, '../' ) === 0 ||
- strpos( $path, '/./' ) !== false ||
- strpos( $path, '/../' ) !== false
- ) {
- return null;
- }
- }
- return $path;
- }
-
- /**
- * Get the parent storage directory of a storage path.
- * This returns a path like "mwstore://backend/container",
- * "mwstore://backend/container/...", or null if there is no parent.
- *
- * @param $storagePath string
- * @return string|null
- */
- final public static function parentStoragePath( $storagePath ) {
- $storagePath = dirname( $storagePath );
- list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath );
- return ( $rel === null ) ? null : $storagePath;
- }
-
- /**
- * Get the final extension from a storage or FS path
- *
- * @param $path string
- * @return string
- */
- final public static function extensionFromPath( $path ) {
- $i = strrpos( $path, '.' );
- return strtolower( $i ? substr( $path, $i + 1 ) : '' );
- }
-}
-
-/**
- * @brief Base class for all backends associated with a particular storage medium.
- *
- * This class defines the methods as abstract that subclasses must implement.
- * Outside callers should *not* use functions with "Internal" in the name.
- *
- * The FileBackend operations are implemented using basic functions
- * such as storeInternal(), copyInternal(), deleteInternal() and the like.
- * This class is also responsible for path resolution and sanitization.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileBackendStore extends FileBackend {
- /** @var Array Map of paths to small (RAM/disk) cache items */
- protected $cache = array(); // (storage path => key => value)
- protected $maxCacheSize = 100; // integer; max paths with entries
- /** @var Array Map of paths to large (RAM/disk) cache items */
- protected $expensiveCache = array(); // (storage path => key => value)
- protected $maxExpensiveCacheSize = 10; // integer; max paths with entries
-
- /** @var Array Map of container names to sharding settings */
- protected $shardViaHashLevels = array(); // (container name => config array)
-
- protected $maxFileSize = 1000000000; // integer bytes (1GB)
-
- /**
- * Get the maximum allowable file size given backend
- * medium restrictions and basic performance constraints.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * @return integer Bytes
- */
- final public function maxFileSizeInternal() {
- return $this->maxFileSize;
- }
-
- /**
- * 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.
- * Backends using key/value stores should check if the container exists.
- *
- * @param $storagePath string
- * @return bool
- */
- abstract public function isPathUsableInternal( $storagePath );
-
- /**
- * Create a file in the backend with the given contents.
- * 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
- *
- * @param $params Array
- * @return Status
- */
- final public function createInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
- $status = Status::newFatal( 'backend-fail-create', $params['dst'] );
- } else {
- $status = $this->doCreateInternal( $params );
- $this->clearCache( array( $params['dst'] ) );
- }
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::createInternal()
- */
- abstract protected function doCreateInternal( array $params );
-
- /**
- * Store a file into the backend from a file on disk.
- * 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
- *
- * @param $params Array
- * @return Status
- */
- final public function storeInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
- $status = Status::newFatal( 'backend-fail-store', $params['dst'] );
- } else {
- $status = $this->doStoreInternal( $params );
- $this->clearCache( array( $params['dst'] ) );
- }
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::storeInternal()
- */
- abstract protected function doStoreInternal( array $params );
-
- /**
- * Copy a file from one storage path to another in the backend.
- * 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
- *
- * @param $params Array
- * @return Status
- */
- final public function copyInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- $status = $this->doCopyInternal( $params );
- $this->clearCache( array( $params['dst'] ) );
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::copyInternal()
- */
- abstract protected function doCopyInternal( array $params );
-
- /**
- * Delete a file at the storage path.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * $params include:
- * src : source storage path
- * ignoreMissingSource : do nothing if the source file does not exist
- *
- * @param $params Array
- * @return Status
- */
- final public function deleteInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- $status = $this->doDeleteInternal( $params );
- $this->clearCache( array( $params['src'] ) );
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::deleteInternal()
- */
- abstract protected function doDeleteInternal( array $params );
-
- /**
- * Move a file from one storage path to another in the backend.
- * 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
- *
- * @param $params Array
- * @return Status
- */
- final public function moveInternal( array $params ) {
- wfProfileIn( __METHOD__ );
- $status = $this->doMoveInternal( $params );
- $this->clearCache( array( $params['src'], $params['dst'] ) );
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::moveInternal()
- */
- protected function doMoveInternal( array $params ) {
- // Copy source to dest
- $status = $this->copyInternal( $params );
- if ( $status->isOK() ) {
- // Delete source (only fails due to races or medium going down)
- $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
- $status->setResult( true, $status->value ); // ignore delete() errors
- }
- return $status;
- }
-
- /**
- * @see FileBackend::concatenate()
- */
- final public function concatenate( array $params ) {
- wfProfileIn( __METHOD__ );
- $status = Status::newGood();
-
- // Try to lock the source files for the scope of this function
- $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
- if ( $status->isOK() ) {
- // Actually do the concatenation
- $status->merge( $this->doConcatenate( $params ) );
- }
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::concatenate()
- */
- protected function doConcatenate( array $params ) {
- $status = Status::newGood();
- $tmpPath = $params['dst']; // convenience
-
- // Check that the specified temp file is valid...
- wfSuppressWarnings();
- $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) );
- 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)...
- $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;
- }
- // Get a handle to the local FS version
- $sourceHandle = fopen( $tmpFile->getPath(), 'r' );
- if ( $sourceHandle === false ) {
- fclose( $tmpHandle );
- $status->fatal( 'backend-fail-read', $virtualSource );
- return $status;
- }
- // Append chunk to file (pass chunk size to avoid magic quotes)
- if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
- fclose( $sourceHandle );
- fclose( $tmpHandle );
- $status->fatal( 'backend-fail-writetemp', $tmpPath );
- return $status;
- }
- fclose( $sourceHandle );
- }
- if ( !fclose( $tmpHandle ) ) {
- $status->fatal( 'backend-fail-closetemp', $tmpPath );
- return $status;
- }
-
- clearstatcache(); // temp file changed
-
- return $status;
- }
-
- /**
- * @see FileBackend::doPrepare()
- */
- final protected function doPrepare( array $params ) {
- wfProfileIn( __METHOD__ );
-
- $status = Status::newGood();
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- wfProfileOut( __METHOD__ );
- return $status; // invalid storage path
- }
-
- if ( $shard !== null ) { // confined to a single container/shard
- $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'] );
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
- }
- }
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::doPrepare()
- */
- protected function doPrepareInternal( $container, $dir, array $params ) {
- return Status::newGood();
- }
-
- /**
- * @see FileBackend::doSecure()
- */
- final protected function doSecure( array $params ) {
- wfProfileIn( __METHOD__ );
- $status = Status::newGood();
-
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- wfProfileOut( __METHOD__ );
- return $status; // invalid storage path
- }
-
- if ( $shard !== null ) { // confined to a single container/shard
- $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'] );
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
- }
- }
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::doSecure()
- */
- protected function doSecureInternal( $container, $dir, array $params ) {
- return Status::newGood();
- }
-
- /**
- * @see FileBackend::doClean()
- */
- final protected function doClean( array $params ) {
- wfProfileIn( __METHOD__ );
- $status = Status::newGood();
-
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- wfProfileOut( __METHOD__ );
- return $status; // invalid storage path
- }
-
- // Attempt to lock this directory...
- $filesLockEx = array( $params['dir'] );
- $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
- if ( !$status->isOK() ) {
- wfProfileOut( __METHOD__ );
- return $status; // abort
- }
-
- if ( $shard !== null ) { // confined to a single container/shard
- $status->merge( $this->doCleanInternal( $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'] );
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
- }
- }
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::doClean()
- */
- protected function doCleanInternal( $container, $dir, array $params ) {
- return Status::newGood();
- }
-
- /**
- * @see FileBackend::fileExists()
- */
- final public function fileExists( array $params ) {
- wfProfileIn( __METHOD__ );
- $stat = $this->getFileStat( $params );
- wfProfileOut( __METHOD__ );
- return ( $stat === null ) ? null : (bool)$stat; // null => failure
- }
-
- /**
- * @see FileBackend::getFileTimestamp()
- */
- final public function getFileTimestamp( array $params ) {
- wfProfileIn( __METHOD__ );
- $stat = $this->getFileStat( $params );
- wfProfileOut( __METHOD__ );
- return $stat ? $stat['mtime'] : false;
- }
-
- /**
- * @see FileBackend::getFileSize()
- */
- final public function getFileSize( array $params ) {
- wfProfileIn( __METHOD__ );
- $stat = $this->getFileStat( $params );
- wfProfileOut( __METHOD__ );
- return $stat ? $stat['size'] : false;
- }
-
- /**
- * @see FileBackend::getFileStat()
- */
- final public function getFileStat( array $params ) {
- wfProfileIn( __METHOD__ );
- $path = self::normalizeStoragePath( $params['src'] );
- if ( $path === null ) {
- return false; // invalid storage path
- }
- $latest = !empty( $params['latest'] );
- if ( isset( $this->cache[$path]['stat'] ) ) {
- // If we want the latest data, check that this cached
- // value was in fact fetched with the latest available data.
- if ( !$latest || $this->cache[$path]['stat']['latest'] ) {
- wfProfileOut( __METHOD__ );
- return $this->cache[$path]['stat'];
- }
- }
- $stat = $this->doGetFileStat( $params );
- if ( is_array( $stat ) ) { // don't cache negatives
- $this->trimCache(); // limit memory
- $this->cache[$path]['stat'] = $stat;
- $this->cache[$path]['stat']['latest'] = $latest;
- }
- wfProfileOut( __METHOD__ );
- return $stat;
- }
-
- /**
- * @see FileBackendStore::getFileStat()
- */
- abstract protected function doGetFileStat( array $params );
-
- /**
- * @see FileBackend::getFileContents()
- */
- public function getFileContents( array $params ) {
- wfProfileIn( __METHOD__ );
- $tmpFile = $this->getLocalReference( $params );
- if ( !$tmpFile ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
- wfSuppressWarnings();
- $data = file_get_contents( $tmpFile->getPath() );
- wfRestoreWarnings();
- wfProfileOut( __METHOD__ );
- return $data;
- }
-
- /**
- * @see FileBackend::getFileSha1Base36()
- */
- final public function getFileSha1Base36( array $params ) {
- wfProfileIn( __METHOD__ );
- $path = $params['src'];
- if ( isset( $this->cache[$path]['sha1'] ) ) {
- wfProfileOut( __METHOD__ );
- return $this->cache[$path]['sha1'];
- }
- $hash = $this->doGetFileSha1Base36( $params );
- if ( $hash ) { // don't cache negatives
- $this->trimCache(); // limit memory
- $this->cache[$path]['sha1'] = $hash;
- }
- wfProfileOut( __METHOD__ );
- return $hash;
- }
-
- /**
- * @see FileBackendStore::getFileSha1Base36()
- */
- protected function doGetFileSha1Base36( array $params ) {
- $fsFile = $this->getLocalReference( $params );
- if ( !$fsFile ) {
- return false;
- } else {
- return $fsFile->getSha1Base36();
- }
- }
-
- /**
- * @see FileBackend::getFileProps()
- */
- final public function getFileProps( array $params ) {
- wfProfileIn( __METHOD__ );
- $fsFile = $this->getLocalReference( $params );
- $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
- wfProfileOut( __METHOD__ );
- return $props;
- }
-
- /**
- * @see FileBackend::getLocalReference()
- */
- public function getLocalReference( array $params ) {
- wfProfileIn( __METHOD__ );
- $path = $params['src'];
- if ( isset( $this->expensiveCache[$path]['localRef'] ) ) {
- wfProfileOut( __METHOD__ );
- return $this->expensiveCache[$path]['localRef'];
- }
- $tmpFile = $this->getLocalCopy( $params );
- if ( $tmpFile ) { // don't cache negatives
- $this->trimExpensiveCache(); // limit memory
- $this->expensiveCache[$path]['localRef'] = $tmpFile;
- }
- wfProfileOut( __METHOD__ );
- return $tmpFile;
- }
-
- /**
- * @see FileBackend::streamFile()
- */
- final public function streamFile( array $params ) {
- wfProfileIn( __METHOD__ );
- $status = Status::newGood();
-
- $info = $this->getFileStat( $params );
- if ( !$info ) { // let StreamFile handle the 404
- $status->fatal( 'backend-fail-notexists', $params['src'] );
- }
-
- // Set output buffer and HTTP headers for stream
- $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array();
- $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
- if ( $res == StreamFile::NOT_MODIFIED ) {
- // do nothing; client cache is up to date
- } elseif ( $res == StreamFile::READY_STREAM ) {
- $status = $this->doStreamFile( $params );
- } else {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- }
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackendStore::streamFile()
- */
- protected function doStreamFile( array $params ) {
- $status = Status::newGood();
-
- $fsFile = $this->getLocalReference( $params );
- if ( !$fsFile ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- } elseif ( !readfile( $fsFile->getPath() ) ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- }
-
- return $status;
- }
-
- /**
- * @copydoc FileBackend::getFileList()
- */
- final public function getFileList( array $params ) {
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) { // invalid storage path
- return null;
- }
- if ( $shard !== null ) {
- // File listing is confined to a single container/shard
- return $this->getFileListInternal( $fullCont, $dir, $params );
- } else {
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- // File listing spans multiple containers/shards
- list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
- return new FileBackendStoreShardListIterator( $this,
- $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
- }
- }
-
- /**
- * Do not call this function from places outside FileBackend
- *
- * @see FileBackendStore::getFileList()
- *
- * @param $container string Resolved container name
- * @param $dir string Resolved path relative to container
- * @param $params Array
- * @return Traversable|Array|null
- */
- abstract public function getFileListInternal( $container, $dir, array $params );
-
- /**
- * Get the list of supported operations and their corresponding FileOp classes.
- *
- * @return Array
- */
- protected function supportedOperations() {
- return array(
- 'store' => 'StoreFileOp',
- 'copy' => 'CopyFileOp',
- 'move' => 'MoveFileOp',
- 'delete' => 'DeleteFileOp',
- 'create' => 'CreateFileOp',
- 'null' => 'NullFileOp'
- );
- }
-
- /**
- * Return a list of FileOp objects from a list of operations.
- * Do not call this function from places outside 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()
- * @return Array List of FileOp objects
- * @throws MWException
- */
- final public function getOperations( array $ops ) {
- $supportedOps = $this->supportedOperations();
-
- $performOps = array(); // array of FileOp objects
- // Build up ordered array of FileOps...
- foreach ( $ops as $operation ) {
- $opName = $operation['op'];
- if ( isset( $supportedOps[$opName] ) ) {
- $class = $supportedOps[$opName];
- // Get params for this operation
- $params = $operation;
- // Append the FileOp class
- $performOps[] = new $class( $this, $params );
- } else {
- throw new MWException( "Operation `$opName` is not supported." );
- }
- }
-
- return $performOps;
- }
-
- /**
- * @see FileBackend::doOperationsInternal()
- */
- protected function doOperationsInternal( array $ops, array $opts ) {
- wfProfileIn( __METHOD__ );
- $status = Status::newGood();
-
- // Build up a list of FileOps...
- $performOps = $this->getOperations( $ops );
-
- // Acquire any locks as needed...
- if ( empty( $opts['nonLocking'] ) ) {
- // Build up a list of files to lock...
- $filesLockEx = $filesLockSh = array();
- foreach ( $performOps as $fileOp ) {
- $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
- $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
- }
- // Optimization: if doing an EX lock anyway, don't also set an SH one
- $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
- // Get a shared lock on the parent directory of each path changed
- $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
- // Try to lock those files for the scope of this function...
- $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
- $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
- if ( !$status->isOK() ) {
- wfProfileOut( __METHOD__ );
- return $status; // abort
- }
- }
-
- // Clear any cache entries (after locks acquired)
- $this->clearCache();
-
- // Actually attempt the operation batch...
- $subStatus = FileOp::attemptBatch( $performOps, $opts );
-
- // Merge errors into status fields
- $status->merge( $subStatus );
- $status->success = $subStatus->success; // not done in merge()
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * @see FileBackend::clearCache()
- */
- final public function clearCache( array $paths = null ) {
- if ( is_array( $paths ) ) {
- $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
- $paths = array_filter( $paths, 'strlen' ); // remove nulls
- }
- if ( $paths === null ) {
- $this->cache = array();
- $this->expensiveCache = array();
- } else {
- foreach ( $paths as $path ) {
- unset( $this->cache[$path] );
- unset( $this->expensiveCache[$path] );
- }
- }
- $this->doClearCache( $paths );
- }
-
- /**
- * Clears any additional stat caches for storage paths
- *
- * @see FileBackend::clearCache()
- *
- * @param $paths Array Storage paths (optional)
- * @return void
- */
- protected function doClearCache( array $paths = null ) {}
-
- /**
- * Prune the inexpensive cache if it is too big to add an item
- *
- * @return void
- */
- protected function trimCache() {
- if ( count( $this->cache ) >= $this->maxCacheSize ) {
- reset( $this->cache );
- unset( $this->cache[key( $this->cache )] );
- }
- }
-
- /**
- * Prune the expensive cache if it is too big to add an item
- *
- * @return void
- */
- protected function trimExpensiveCache() {
- if ( count( $this->expensiveCache ) >= $this->maxExpensiveCacheSize ) {
- reset( $this->expensiveCache );
- unset( $this->expensiveCache[key( $this->expensiveCache )] );
- }
- }
-
- /**
- * Check if a container name is valid.
- * This checks for for length and illegal characters.
- *
- * @param $container string
- * @return bool
- */
- final protected static function isValidContainerName( $container ) {
- // This accounts for Swift and S3 restrictions while leaving room
- // for things like '.xxx' (hex shard chars) or '.seg' (segments).
- // This disallows directory separators or traversal characters.
- // Note that matching strings URL encode to the same string;
- // in Swift, the length restriction is *after* URL encoding.
- return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
- }
-
- /**
- * Splits a storage path into an internal container name,
- * an internal relative file name, and a container shard suffix.
- * Any shard suffix is already appended to the internal container name.
- * This also checks that the storage path is valid and within this backend.
- *
- * If the container is sharded but a suffix could not be determined,
- * 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
- * @return Array (container, path, container suffix) or (null, null, null) if invalid
- */
- final protected function resolveStoragePath( $storagePath ) {
- list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
- if ( $backend === $this->name ) { // must be for this backend
- $relPath = self::normalizeContainerPath( $relPath );
- if ( $relPath !== null ) {
- // Get shard for the normalized path if this container is sharded
- $cShard = $this->getContainerShard( $container, $relPath );
- // Validate and sanitize the relative path (backend-specific)
- $relPath = $this->resolveContainerPath( $container, $relPath );
- if ( $relPath !== null ) {
- // Prepend any wiki ID prefix to the container name
- $container = $this->fullContainerName( $container );
- if ( self::isValidContainerName( $container ) ) {
- // Validate and sanitize the container name (backend-specific)
- $container = $this->resolveContainerName( "{$container}{$cShard}" );
- if ( $container !== null ) {
- return array( $container, $relPath, $cShard );
- }
- }
- }
- }
- }
- return array( null, null, null );
- }
-
- /**
- * Like resolveStoragePath() except null values are returned if
- * the container is sharded and the shard could not be determined.
- *
- * @see FileBackendStore::resolveStoragePath()
- *
- * @param $storagePath string
- * @return Array (container, path) or (null, null) if invalid
- */
- final protected function resolveStoragePathReal( $storagePath ) {
- list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
- if ( $cShard !== null ) {
- return array( $container, $relPath );
- }
- return array( null, null );
- }
-
- /**
- * 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 $relStoragePath string Storage path relative to the container
- * @return string|null Returns null if shard could not be determined
- */
- final protected function getContainerShard( $container, $relPath ) {
- list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
- if ( $levels == 1 || $levels == 2 ) {
- // Hash characters are either base 16 or 36
- $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
- // Get a regex that represents the shard portion of paths.
- // The concatenation of the captures gives us the shard.
- if ( $levels === 1 ) { // 16 or 36 shards per container
- $hashDirRegex = '(' . $char . ')';
- } else { // 256 or 1296 shards per container
- if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
- $hashDirRegex = $char . '/(' . $char . '{2})';
- } else { // short hash dir format (e.g. "a/b/c")
- $hashDirRegex = '(' . $char . ')/(' . $char . ')';
- }
- }
- // Allow certain directories to be above the hash dirs so as
- // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
- // They must be 2+ chars to avoid any hash directory ambiguity.
- $m = array();
- if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
- return '.' . implode( '', array_slice( $m, 1 ) );
- }
- return null; // failed to match
- }
- return ''; // no sharding
- }
-
- /**
- * Get the sharding config for a container.
- * If greater than 0, then all file storage paths within
- * the container are required to be hashed accordingly.
- *
- * @param $container string
- * @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
- */
- final protected function getContainerHashLevels( $container ) {
- if ( isset( $this->shardViaHashLevels[$container] ) ) {
- $config = $this->shardViaHashLevels[$container];
- $hashLevels = (int)$config['levels'];
- if ( $hashLevels == 1 || $hashLevels == 2 ) {
- $hashBase = (int)$config['base'];
- if ( $hashBase == 16 || $hashBase == 36 ) {
- return array( $hashLevels, $hashBase, $config['repeat'] );
- }
- }
- }
- return array( 0, 0, false ); // no sharding
- }
-
- /**
- * Get a list of full container shard suffixes for a container
- *
- * @param $container string
- * @return Array
- */
- final protected function getContainerSuffixes( $container ) {
- $shards = array();
- list( $digits, $base ) = $this->getContainerHashLevels( $container );
- if ( $digits > 0 ) {
- $numShards = pow( $base, $digits );
- for ( $index = 0; $index < $numShards; $index++ ) {
- $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
- }
- }
- return $shards;
- }
-
- /**
- * Get the full container name, including the wiki ID prefix
- *
- * @param $container string
- * @return string
- */
- final protected function fullContainerName( $container ) {
- if ( $this->wikiId != '' ) {
- return "{$this->wikiId}-$container";
- } else {
- return $container;
- }
- }
-
- /**
- * Resolve a container name, checking if it's allowed by the backend.
- * This is intended for internal use, such as encoding illegal chars.
- * Subclasses can override this to be more restrictive.
- *
- * @param $container string
- * @return string|null
- */
- protected function resolveContainerName( $container ) {
- return $container;
- }
-
- /**
- * Resolve a relative storage path, checking if it's allowed by the backend.
- * This is intended for internal use, such as encoding illegal chars or perhaps
- * 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
- * @return string|null Path or null if not valid
- */
- protected function resolveContainerPath( $container, $relStoragePath ) {
- return $relStoragePath;
- }
-}
-
-/**
- * FileBackendStore helper function to handle file listings that span container shards.
- * Do not use this class from places outside of FileBackendStore.
- *
- * @ingroup FileBackend
- */
-class FileBackendStoreShardListIterator implements Iterator {
- /* @var FileBackendStore */
- protected $backend;
- /* @var Array */
- protected $params;
- /* @var Array */
- protected $shardSuffixes;
- protected $container; // string
- protected $directory; // string
-
- /* @var Traversable */
- protected $iter;
- protected $curShard = 0; // integer
- protected $pos = 0; // integer
-
- /**
- * @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
- */
- public function __construct(
- FileBackendStore $backend, $container, $dir, array $suffixes, array $params
- ) {
- $this->backend = $backend;
- $this->container = $container;
- $this->directory = $dir;
- $this->shardSuffixes = $suffixes;
- $this->params = $params;
- }
-
- public function current() {
- if ( is_array( $this->iter ) ) {
- return current( $this->iter );
- } else {
- return $this->iter->current();
- }
- }
-
- public function key() {
- return $this->pos;
- }
-
- public function next() {
- ++$this->pos;
- if ( is_array( $this->iter ) ) {
- next( $this->iter );
- } else {
- $this->iter->next();
- }
- // Find the next non-empty shard if no elements are left
- $this->nextShardIteratorIfNotValid();
- }
-
- /**
- * If the 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() ) {
- if ( ++$this->curShard >= count( $this->shardSuffixes ) ) {
- break; // no more container shards
- }
- $this->setIteratorFromCurrentShard();
- }
- }
-
- protected function setIteratorFromCurrentShard() {
- $suffix = $this->shardSuffixes[$this->curShard];
- $this->iter = $this->backend->getFileListInternal(
- "{$this->container}{$suffix}", $this->directory, $this->params );
- }
-
- public function rewind() {
- $this->pos = 0;
- $this->curShard = 0;
- $this->setIteratorFromCurrentShard();
- // Find the next non-empty shard if this one has no elements
- $this->nextShardIteratorIfNotValid();
- }
-
- public function valid() {
- if ( $this->iter == null ) {
- return false; // some failure?
- } elseif ( is_array( $this->iter ) ) {
- return ( current( $this->iter ) !== false ); // no paths can have this value
- } else {
- return $this->iter->valid();
- }
- }
-}
diff --git a/includes/filerepo/backend/FileBackendMultiWrite.php b/includes/filerepo/backend/FileBackendMultiWrite.php
deleted file mode 100644
index c0f1ac57..00000000
--- a/includes/filerepo/backend/FileBackendMultiWrite.php
+++ /dev/null
@@ -1,420 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * This class defines a multi-write backend. Multiple backends can be
- * registered to this proxy backend and it will act as a single backend.
- * Use this when all access to those backends is through this proxy backend.
- * At least one of the backends must be declared the "master" backend.
- *
- * Only use this class when transitioning from one storage system to another.
- *
- * Read operations are only done on the 'master' backend for consistency.
- * Write operations are performed on all backends, in the order defined.
- * If an operation fails on one backend it will be rolled back from the others.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class FileBackendMultiWrite extends FileBackend {
- /** @var Array Prioritized list of FileBackendStore objects */
- protected $backends = array(); // array of (backend index => backends)
- protected $masterIndex = -1; // integer; index of master backend
- protected $syncChecks = 0; // integer bitfield
-
- /* Possible internal backend consistency checks */
- const CHECK_SIZE = 1;
- const CHECK_TIME = 2;
-
- /**
- * Construct a proxy backend that consists of several internal backends.
- * 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:
- * 'class' : The name of the backend class
- * 'isMultiMaster' : This must be set for one backend.
- * 'syncChecks' : Integer bitfield of internal backend sync checks to perform.
- * Possible bits include self::CHECK_SIZE and self::CHECK_TIME.
- * The checks are done before allowing any file operations.
- * @param $config Array
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
- $namesUsed = array();
- // Construct backends here rather than via registration
- // to keep these backends hidden from outside the proxy.
- foreach ( $config['backends'] as $index => $config ) {
- $name = $config['name'];
- if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
- throw new MWException( "Two or more backends defined with the name $name." );
- }
- $namesUsed[$name] = 1;
- if ( !isset( $config['class'] ) ) {
- throw new MWException( 'No class given for a backend config.' );
- }
- $class = $config['class'];
- $this->backends[$index] = new $class( $config );
- if ( !empty( $config['isMultiMaster'] ) ) {
- if ( $this->masterIndex >= 0 ) {
- throw new MWException( 'More than one master backend defined.' );
- }
- $this->masterIndex = $index;
- }
- }
- if ( $this->masterIndex < 0 ) { // need backends and must have a master
- throw new MWException( 'No master backend defined.' );
- }
- $this->syncChecks = isset( $config['syncChecks'] )
- ? $config['syncChecks']
- : self::CHECK_SIZE;
- }
-
- /**
- * @see FileBackend::doOperationsInternal()
- */
- final protected function doOperationsInternal( array $ops, array $opts ) {
- $status = Status::newGood();
-
- $performOps = array(); // list of FileOp objects
- $filesRead = $filesChanged = array(); // storage paths used
- // Build up a list of FileOps. The list will have all the ops
- // for one backend, then all the ops for the next, and so on.
- // These batches of ops are all part of a continuous array.
- // Also build up a list of files read/changed...
- foreach ( $this->backends as $index => $backend ) {
- $backendOps = $this->substOpBatchPaths( $ops, $backend );
- // Add on the operation batch for this backend
- $performOps = array_merge( $performOps, $backend->getOperations( $backendOps ) );
- if ( $index == 0 ) { // first batch
- // Get the files used for these operations. Each backend has a batch of
- // the same operations, so we only need to get them from the first batch.
- foreach ( $performOps as $fileOp ) {
- $filesRead = array_merge( $filesRead, $fileOp->storagePathsRead() );
- $filesChanged = array_merge( $filesChanged, $fileOp->storagePathsChanged() );
- }
- // Get the paths under the proxy backend's name
- $filesRead = $this->unsubstPaths( $filesRead );
- $filesChanged = $this->unsubstPaths( $filesChanged );
- }
- }
-
- // Try to lock those files for the scope of this function...
- if ( empty( $opts['nonLocking'] ) ) {
- $filesLockSh = array_diff( $filesRead, $filesChanged ); // optimization
- $filesLockEx = $filesChanged;
- // Get a shared lock on the parent directory of each path changed
- $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
- // Try to lock those files for the scope of this function...
- $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
- $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
- if ( !$status->isOK() ) {
- return $status; // abort
- }
- }
-
- // Clear any cache entries (after locks acquired)
- $this->clearCache();
-
- // Do a consistency check to see if the backends agree
- if ( count( $this->backends ) > 1 ) {
- $status->merge( $this->consistencyCheck( array_merge( $filesRead, $filesChanged ) ) );
- if ( !$status->isOK() ) {
- return $status; // abort
- }
- }
-
- // Actually attempt the operation batch...
- $subStatus = FileOp::attemptBatch( $performOps, $opts );
-
- $success = array();
- $failCount = $successCount = 0;
- // Make 'success', 'successCount', and 'failCount' fields reflect
- // the overall operation, rather than all the batches for each backend.
- // Do this by only using success values from the master backend's batch.
- $batchStart = $this->masterIndex * count( $ops );
- $batchEnd = $batchStart + count( $ops ) - 1;
- for ( $i = $batchStart; $i <= $batchEnd; $i++ ) {
- if ( !isset( $subStatus->success[$i] ) ) {
- break; // failed out before trying this op
- } elseif ( $subStatus->success[$i] ) {
- ++$successCount;
- } else {
- ++$failCount;
- }
- $success[] = $subStatus->success[$i];
- }
- $subStatus->success = $success;
- $subStatus->successCount = $successCount;
- $subStatus->failCount = $failCount;
-
- // Merge errors into status fields
- $status->merge( $subStatus );
- $status->success = $subStatus->success; // not done in merge()
-
- return $status;
- }
-
- /**
- * Check that a set of files are consistent across all internal backends
- *
- * @param $paths Array
- * @return Status
- */
- public function consistencyCheck( array $paths ) {
- $status = Status::newGood();
- if ( $this->syncChecks == 0 ) {
- return $status; // skip checks
- }
-
- $mBackend = $this->backends[$this->masterIndex];
- foreach ( array_unique( $paths ) as $path ) {
- $params = array( 'src' => $path, 'latest' => true );
- // Stat the file on the 'master' backend
- $mStat = $mBackend->getFileStat( $this->substOpPaths( $params, $mBackend ) );
- // Check of all clone backends agree with the master...
- foreach ( $this->backends as $index => $cBackend ) {
- if ( $index === $this->masterIndex ) {
- continue; // master
- }
- $cStat = $cBackend->getFileStat( $this->substOpPaths( $params, $cBackend ) );
- if ( $mStat ) { // file is in master
- if ( !$cStat ) { // file should exist
- $status->fatal( 'backend-fail-synced', $path );
- continue;
- }
- if ( $this->syncChecks & self::CHECK_SIZE ) {
- if ( $cStat['size'] != $mStat['size'] ) { // wrong size
- $status->fatal( 'backend-fail-synced', $path );
- continue;
- }
- }
- if ( $this->syncChecks & self::CHECK_TIME ) {
- $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
- $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
- if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
- $status->fatal( 'backend-fail-synced', $path );
- continue;
- }
- }
- } else { // file is not in master
- if ( $cStat ) { // file should not exist
- $status->fatal( 'backend-fail-synced', $path );
- }
- }
- }
- }
-
- return $status;
- }
-
- /**
- * 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
- * @return Array
- */
- protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
- $newOps = array(); // operations
- foreach ( $ops as $op ) {
- $newOp = $op; // operation
- foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
- if ( isset( $newOp[$par] ) ) { // string or array
- $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
- }
- }
- $newOps[] = $newOp;
- }
- return $newOps;
- }
-
- /**
- * Same as substOpBatchPaths() but for a single operation
- *
- * @param $op File operation array
- * @param $backend FileBackendStore
- * @return Array
- */
- protected function substOpPaths( array $ops, FileBackendStore $backend ) {
- $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
- return $newOps[0];
- }
-
- /**
- * 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
- * @return Array|string
- */
- protected function substPaths( $paths, FileBackendStore $backend ) {
- return preg_replace(
- '!^mwstore://' . preg_quote( $this->name ) . '/!',
- StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
- $paths // string or array
- );
- }
-
- /**
- * Substitute the backend of internal storage paths with the proxy backend's name
- *
- * @param $paths Array|string List of paths or single string path
- * @return Array|string
- */
- protected function unsubstPaths( $paths ) {
- return preg_replace(
- '!^mwstore://([^/]+)!',
- StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
- $paths // string or array
- );
- }
-
- /**
- * @see FileBackend::doPrepare()
- */
- public function doPrepare( array $params ) {
- $status = Status::newGood();
- foreach ( $this->backends as $backend ) {
- $realParams = $this->substOpPaths( $params, $backend );
- $status->merge( $backend->doPrepare( $realParams ) );
- }
- return $status;
- }
-
- /**
- * @see FileBackend::doSecure()
- */
- public function doSecure( array $params ) {
- $status = Status::newGood();
- foreach ( $this->backends as $backend ) {
- $realParams = $this->substOpPaths( $params, $backend );
- $status->merge( $backend->doSecure( $realParams ) );
- }
- return $status;
- }
-
- /**
- * @see FileBackend::doClean()
- */
- public function doClean( array $params ) {
- $status = Status::newGood();
- foreach ( $this->backends as $backend ) {
- $realParams = $this->substOpPaths( $params, $backend );
- $status->merge( $backend->doClean( $realParams ) );
- }
- return $status;
- }
-
- /**
- * @see FileBackend::getFileList()
- */
- 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()
- */
- public function fileExists( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->fileExists( $realParams );
- }
-
- /**
- * @see FileBackend::getFileTimestamp()
- */
- public function getFileTimestamp( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
- }
-
- /**
- * @see FileBackend::getFileSize()
- */
- public function getFileSize( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getFileSize( $realParams );
- }
-
- /**
- * @see FileBackend::getFileStat()
- */
- public function getFileStat( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getFileStat( $realParams );
- }
-
- /**
- * @see FileBackend::getFileContents()
- */
- public function getFileContents( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getFileContents( $realParams );
- }
-
- /**
- * @see FileBackend::getFileSha1Base36()
- */
- public function getFileSha1Base36( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
- }
-
- /**
- * @see FileBackend::getFileProps()
- */
- public function getFileProps( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getFileProps( $realParams );
- }
-
- /**
- * @see FileBackend::streamFile()
- */
- public function streamFile( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->streamFile( $realParams );
- }
-
- /**
- * @see FileBackend::getLocalReference()
- */
- public function getLocalReference( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getLocalReference( $realParams );
- }
-
- /**
- * @see FileBackend::getLocalCopy()
- */
- public function getLocalCopy( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
- }
-
- /**
- * @see FileBackend::getFileList()
- */
- 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;
- $backend->clearCache( $realPaths );
- }
- }
-}
diff --git a/includes/filerepo/backend/SwiftFileBackend.php b/includes/filerepo/backend/SwiftFileBackend.php
deleted file mode 100644
index a287f488..00000000
--- a/includes/filerepo/backend/SwiftFileBackend.php
+++ /dev/null
@@ -1,877 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup FileBackend
- * @author Russ Nelson
- * @author Aaron Schulz
- */
-
-/**
- * Class for an OpenStack Swift based file backend.
- *
- * This requires the SwiftCloudFiles MediaWiki extension, which includes
- * the php-cloudfiles library (https://github.com/rackspace/php-cloudfiles).
- * php-cloudfiles requires the curl, fileinfo, and mb_string PHP extensions.
- *
- * Status messages should avoid mentioning the Swift account name.
- * Likewise, error suppression should be used to avoid path disclosure.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class SwiftFileBackend extends FileBackendStore {
- /** @var CF_Authentication */
- protected $auth; // Swift authentication handler
- protected $authTTL; // integer seconds
- protected $swiftAnonUser; // string; username to handle unauthenticated requests
- protected $maxContCacheSize = 100; // integer; max containers with entries
-
- /** @var CF_Connection */
- protected $conn; // Swift connection handle
- protected $connStarted = 0; // integer UNIX timestamp
- protected $connContainers = array(); // container object cache
-
- /**
- * @see FileBackendStore::__construct()
- * Additional $config params include:
- * swiftAuthUrl : Swift authentication server URL
- * swiftUser : Swift user used by MediaWiki (account:username)
- * swiftKey : Swift authentication key for the above user
- * swiftAuthTTL : Swift authentication TTL (seconds)
- * swiftAnonUser : Swift user used for end-user requests (account:username)
- * shardViaHashLevels : Map of container names to sharding config with:
- * 'base' : base of hash characters, 16 or 36
- * 'levels' : the number of hash levels (and digits)
- * 'repeat' : hash subdirectories are prefixed with all the
- * parent hash directory names (e.g. "a/ab/abc")
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
- // Required settings
- $this->auth = new CF_Authentication(
- $config['swiftUser'],
- $config['swiftKey'],
- null, // account; unused
- $config['swiftAuthUrl']
- );
- // Optional settings
- $this->authTTL = isset( $config['swiftAuthTTL'] )
- ? $config['swiftAuthTTL']
- : 120; // some sane number
- $this->swiftAnonUser = isset( $config['swiftAnonUser'] )
- ? $config['swiftAnonUser']
- : '';
- $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
- ? $config['shardViaHashLevels']
- : '';
- }
-
- /**
- * @see FileBackendStore::resolveContainerPath()
- */
- protected function resolveContainerPath( $container, $relStoragePath ) {
- if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
- return null; // too long for Swift
- }
- return $relStoragePath;
- }
-
- /**
- * @see FileBackendStore::isPathUsableInternal()
- */
- public function isPathUsableInternal( $storagePath ) {
- list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
- if ( $rel === null ) {
- return false; // invalid
- }
-
- try {
- $this->getContainer( $container );
- return true; // container exists
- } catch ( NoSuchContainerException $e ) {
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, array( 'path' => $storagePath ) );
- }
-
- return false;
- }
-
- /**
- * @see FileBackendStore::doCreateInternal()
- */
- protected function doCreateInternal( array $params ) {
- $status = Status::newGood();
-
- list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
- if ( $dstRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
- return $status;
- }
-
- // (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;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
-
- // (b) Get a SHA-1 hash of the object
- $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
-
- // (c) Actually create the object
- try {
- // 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 );
- // 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'] );
- // Actually write the object in Swift
- $obj->write( $params['content'] );
- } catch ( BadContentTypeException $e ) {
- $status->fatal( 'backend-fail-contenttype', $params['dst'] );
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doStoreInternal()
- */
- protected function doStoreInternal( array $params ) {
- $status = Status::newGood();
-
- list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
- if ( $dstRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
- return $status;
- }
-
- // (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;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
-
- // (b) Get a SHA-1 hash of the object
- $sha1Hash = sha1_file( $params['src'] );
- if ( $sha1Hash === false ) { // source doesn't exist?
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- return $status;
- }
- $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
-
- // (c) Actually store the object
- try {
- // 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 );
- // 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'] );
- // Actually write the object in Swift
- $obj->load_from_filename( $params['src'], True ); // calls $obj->write()
- } catch ( BadContentTypeException $e ) {
- $status->fatal( 'backend-fail-contenttype', $params['dst'] );
- } catch ( IOException $e ) {
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doCopyInternal()
- */
- protected function doCopyInternal( array $params ) {
- $status = Status::newGood();
-
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
- return $status;
- }
-
- list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
- if ( $dstRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
- return $status;
- }
-
- // (a) Check the source/destination containers and destination object
- 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'] );
- return $status;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
-
- // (b) Actually copy the file to the destination
- try {
- $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel );
- } catch ( NoSuchObjectException $e ) { // source object does not exist
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doDeleteInternal()
- */
- protected function doDeleteInternal( 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 );
- $sContObj->delete_object( $srcRel );
- } catch ( NoSuchContainerException $e ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- } catch ( NoSuchObjectException $e ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- }
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doPrepareInternal()
- */
- protected function doPrepareInternal( $fullCont, $dir, array $params ) {
- $status = Status::newGood();
-
- // (a) Check if container already exists
- try {
- $contObj = $this->getContainer( $fullCont );
- // NoSuchContainerException not thrown: container must exist
- return $status; // already exists
- } catch ( NoSuchContainerException $e ) {
- // NoSuchContainerException thrown: container does not exist
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
-
- // (b) Create container as needed
- try {
- $contObj = $this->createContainer( $fullCont );
- if ( $this->swiftAnonUser != '' ) {
- // Make container public to end-users...
- $status->merge( $this->setContainerAccess(
- $contObj,
- array( $this->auth->username, $this->swiftAnonUser ), // read
- array( $this->auth->username ) // write
- ) );
- }
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doSecureInternal()
- */
- protected function doSecureInternal( $fullCont, $dir, array $params ) {
- $status = Status::newGood();
-
- if ( $this->swiftAnonUser != '' ) {
- // Restrict container from end-users...
- try {
- // doPrepareInternal() should have been called,
- // so the Swift container should already exist...
- $contObj = $this->getContainer( $fullCont ); // normally a cache hit
- // NoSuchContainerException not thrown: container must exist
- if ( !isset( $contObj->mw_wasSecured ) ) {
- $status->merge( $this->setContainerAccess(
- $contObj,
- array( $this->auth->username ), // read
- array( $this->auth->username ) // write
- ) );
- // @TODO: when php-cloudfiles supports container
- // metadata, we can make use of that to avoid RTTs
- $contObj->mw_wasSecured = true; // avoid useless RTTs
- }
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doCleanInternal()
- */
- protected function doCleanInternal( $fullCont, $dir, array $params ) {
- $status = Status::newGood();
-
- // Only containers themselves can be removed, all else is virtual
- if ( $dir != '' ) {
- return $status; // nothing to do
- }
-
- // (a) Check the container
- try {
- $contObj = $this->getContainer( $fullCont, true );
- } catch ( NoSuchContainerException $e ) {
- return $status; // ok, nothing to do
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
-
- // (b) Delete the container if empty
- if ( $contObj->object_count == 0 ) {
- try {
- $this->deleteContainer( $fullCont );
- } catch ( NoSuchContainerException $e ) {
- return $status; // race?
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-internal', $this->name );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doFileExists()
- */
- protected function doGetFileStat( array $params ) {
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- return false; // invalid storage path
- }
-
- $stat = false;
- try {
- $contObj = $this->getContainer( $srcCont );
- $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) );
- $this->addMissingMetadata( $srcObj, $params['src'] );
- $stat = array(
- // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
- 'mtime' => wfTimestamp( TS_MW, $srcObj->last_modified ),
- 'size' => $srcObj->content_length,
- 'sha1' => $srcObj->metadata['Sha1base36']
- );
- } catch ( NoSuchContainerException $e ) {
- } catch ( NoSuchObjectException $e ) {
- } catch ( InvalidResponseException $e ) {
- $stat = null;
- } catch ( Exception $e ) { // some other exception?
- $stat = null;
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $stat;
- }
-
- /**
- * Fill in any missing object metadata and save it to Swift
- *
- * @param $obj CF_Object
- * @param $path string Storage path to object
- * @return bool Success
- * @throws Exception cloudfiles exceptions
- */
- protected function addMissingMetadata( CF_Object $obj, $path ) {
- if ( isset( $obj->metadata['Sha1base36'] ) ) {
- return true; // nothing to do
- }
- $status = Status::newGood();
- $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status );
- if ( $status->isOK() ) {
- $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) );
- if ( $tmpFile ) {
- $hash = $tmpFile->getSha1Base36();
- if ( $hash !== false ) {
- $obj->metadata['Sha1base36'] = $hash;
- $obj->sync_metadata(); // save to Swift
- return true; // success
- }
- }
- }
- $obj->metadata['Sha1base36'] = false;
- return false; // failed
- }
-
- /**
- * @see FileBackend::getFileContents()
- */
- public function getFileContents( array $params ) {
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- return false; // invalid storage path
- }
-
- if ( !$this->fileExists( $params ) ) {
- return null;
- }
-
- $data = false;
- try {
- $sContObj = $this->getContainer( $srcCont );
- $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD request
- $data = $obj->read( $this->headersFromParams( $params ) );
- } catch ( NoSuchContainerException $e ) {
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $data;
- }
-
- /**
- * @see FileBackendStore::getFileListInternal()
- */
- public function getFileListInternal( $fullCont, $dir, array $params ) {
- return new SwiftFileBackendFileList( $this, $fullCont, $dir );
- }
-
- /**
- * 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 Storage path of file to list items after
- * @param $limit integer Max number of items to list
- * @return Array
- */
- public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
- $files = array();
-
- try {
- $container = $this->getContainer( $fullCont );
- $prefix = ( $dir == '' ) ? null : "{$dir}/";
- $files = $container->list_objects( $limit, $after, $prefix );
- } catch ( NoSuchContainerException $e ) {
- } catch ( NoSuchObjectException $e ) {
- } catch ( InvalidResponseException $e ) {
- } catch ( Exception $e ) { // some other exception?
- $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
- }
-
- return $files;
- }
-
- /**
- * @see FileBackendStore::doGetFileSha1base36()
- */
- public function doGetFileSha1base36( array $params ) {
- $stat = $this->getFileStat( $params );
- if ( $stat ) {
- return $stat['sha1'];
- } else {
- return false;
- }
- }
-
- /**
- * @see FileBackendStore::doStreamFile()
- */
- protected function doStreamFile( array $params ) {
- $status = Status::newGood();
-
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
- }
-
- try {
- $cont = $this->getContainer( $srcCont );
- } catch ( NoSuchContainerException $e ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- return $status;
- } catch ( InvalidResponseException $e ) {
- $status->fatal( 'backend-fail-connect', $this->name );
- return $status;
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-stream', $params['src'] );
- $this->logException( $e, __METHOD__, $params );
- return $status;
- }
-
- try {
- $output = fopen( 'php://output', 'wb' );
- $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD request
- $obj->stream( $output, $this->headersFromParams( $params ) );
- } catch ( InvalidResponseException $e ) { // 404? connection problem?
- $status->fatal( 'backend-fail-stream', $params['src'] );
- } catch ( Exception $e ) { // some other exception?
- $status->fatal( 'backend-fail-stream', $params['src'] );
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::getLocalCopy()
- */
- public function getLocalCopy( array $params ) {
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- return null;
- }
-
- if ( !$this->fileExists( $params ) ) {
- return null;
- }
-
- $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( wfBaseName( $srcRel ) . '_', $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
- }
- }
- } catch ( NoSuchContainerException $e ) {
- $tmpFile = null;
- } catch ( InvalidResponseException $e ) {
- $tmpFile = null;
- } catch ( Exception $e ) { // some other exception?
- $tmpFile = null;
- $this->logException( $e, __METHOD__, $params );
- }
-
- return $tmpFile;
- }
-
- /**
- * Get headers to send to Swift when reading a file based
- * on a FileBackend params array, e.g. that of getLocalCopy().
- * $params is currently only checked for a 'latest' flag.
- *
- * @param $params Array
- * @return Array
- */
- protected function headersFromParams( array $params ) {
- $hdrs = array();
- if ( !empty( $params['latest'] ) ) {
- $hdrs[] = 'X-Newest: true';
- }
- return $hdrs;
- }
-
- /**
- * Set read/write permissions for a Swift container
- *
- * @param $contObj CF_Container Swift container
- * @param $readGrps Array Swift users who can read (account:user)
- * @param $writeGrps Array Swift users who can write (account:user)
- * @return Status
- */
- protected function setContainerAccess(
- CF_Container $contObj, array $readGrps, array $writeGrps
- ) {
- $creds = $contObj->cfs_auth->export_credentials();
-
- $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name );
-
- // Note: 10 second timeout consistent with php-cloudfiles
- $req = new CurlHttpRequest( $url, array( 'method' => 'POST', 'timeout' => 10 ) );
- $req->setHeader( 'X-Auth-Token', $creds['auth_token'] );
- $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) );
- $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) );
-
- return $req->execute(); // should return 204
- }
-
- /**
- * Get a connection to the Swift proxy
- *
- * @return CF_Connection|false
- * @throws InvalidResponseException
- */
- protected function getConnection() {
- if ( $this->conn === false ) {
- throw new InvalidResponseException; // failed last attempt
- }
- // Session keys expire after a while, so we renew them periodically
- if ( $this->conn && ( time() - $this->connStarted ) > $this->authTTL ) {
- $this->conn->close(); // close active cURL connections
- $this->conn = null;
- }
- // Authenticate with proxy and get a session key...
- if ( $this->conn === null ) {
- $this->connContainers = array();
- try {
- $this->auth->authenticate();
- $this->conn = new CF_Connection( $this->auth );
- $this->connStarted = time();
- } catch ( AuthenticationException $e ) {
- $this->conn = false; // don't keep re-trying
- } catch ( InvalidResponseException $e ) {
- $this->conn = false; // don't keep re-trying
- }
- }
- if ( !$this->conn ) {
- throw new InvalidResponseException; // auth/connection problem
- }
- return $this->conn;
- }
-
- /**
- * @see FileBackendStore::doClearCache()
- */
- protected function doClearCache( array $paths = null ) {
- $this->connContainers = array(); // 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 $reCache bool Refresh the process cache
- * @return CF_Container
- */
- protected function getContainer( $container, $reCache = false ) {
- $conn = $this->getConnection(); // Swift proxy connection
- if ( $reCache ) {
- unset( $this->connContainers[$container] ); // purge cache
- }
- if ( !isset( $this->connContainers[$container] ) ) {
- $contObj = $conn->get_container( $container );
- // NoSuchContainerException not thrown: container must exist
- if ( count( $this->connContainers ) >= $this->maxContCacheSize ) { // trim cache?
- reset( $this->connContainers );
- $key = key( $this->connContainers );
- unset( $this->connContainers[$key] );
- }
- $this->connContainers[$container] = $contObj; // cache it
- }
- return $this->connContainers[$container];
- }
-
- /**
- * Create a Swift container
- *
- * @param $container string Container name
- * @return CF_Container
- */
- protected function createContainer( $container ) {
- $conn = $this->getConnection(); // Swift proxy connection
- $contObj = $conn->create_container( $container );
- $this->connContainers[$container] = $contObj; // cache it
- return $contObj;
- }
-
- /**
- * Delete a Swift container
- *
- * @param $container string Container name
- * @return void
- */
- protected function deleteContainer( $container ) {
- $conn = $this->getConnection(); // Swift proxy connection
- $conn->delete_container( $container );
- unset( $this->connContainers[$container] ); // purge cache
- }
-
- /**
- * Log an unexpected exception for this backend
- *
- * @param $e Exception
- * @param $func string
- * @param $params Array
- * @return void
- */
- protected function logException( Exception $e, $func, array $params ) {
- wfDebugLog( 'SwiftBackend',
- get_class( $e ) . " in '{$func}' (given '" . serialize( $params ) . "')" .
- ( $e instanceof InvalidResponseException
- ? ": {$e->getMessage()}"
- : ""
- )
- );
- }
-}
-
-/**
- * SwiftFileBackend helper class to page through object listings.
- * Swift also has a listing limit of 10,000 objects for sanity.
- * Do not use this class from places outside SwiftFileBackend.
- *
- * @ingroup FileBackend
- */
-class SwiftFileBackendFileList implements Iterator {
- /** @var Array */
- protected $bufferIter = array();
- protected $bufferAfter = null; // string; list items *after* this path
- protected $pos = 0; // integer
-
- /** @var SwiftFileBackend */
- protected $backend;
- protected $container; //
- protected $dir; // string storage directory
- protected $suffixStart; // integer
-
- const PAGE_SIZE = 5000; // file listing buffer size
-
- /**
- * @param $backend SwiftFileBackend
- * @param $fullCont string Resolved container name
- * @param $dir string Resolved directory relative to container
- */
- public function __construct( SwiftFileBackend $backend, $fullCont, $dir ) {
- $this->backend = $backend;
- $this->container = $fullCont;
- $this->dir = $dir;
- if ( substr( $this->dir, -1 ) === '/' ) {
- $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
- }
- if ( $this->dir == '' ) { // whole container
- $this->suffixStart = 0;
- } else { // dir within container
- $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
- }
- }
-
- public function current() {
- return substr( current( $this->bufferIter ), $this->suffixStart );
- }
-
- public function key() {
- return $this->pos;
- }
-
- public function next() {
- // Advance to the next file in the page
- next( $this->bufferIter );
- ++$this->pos;
- // Check if there are no files left in this page and
- // advance to the next page if this page was not empty.
- if ( !$this->valid() && count( $this->bufferIter ) ) {
- $this->bufferAfter = end( $this->bufferIter );
- $this->bufferIter = $this->backend->getFileListPageInternal(
- $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
- );
- }
- }
-
- public function rewind() {
- $this->pos = 0;
- $this->bufferAfter = null;
- $this->bufferIter = $this->backend->getFileListPageInternal(
- $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
- );
- }
-
- public function valid() {
- return ( current( $this->bufferIter ) !== false ); // no paths can have this value
- }
-}
diff --git a/includes/filerepo/backend/lockmanager/DBLockManager.php b/includes/filerepo/backend/lockmanager/DBLockManager.php
deleted file mode 100644
index 045056ea..00000000
--- a/includes/filerepo/backend/lockmanager/DBLockManager.php
+++ /dev/null
@@ -1,469 +0,0 @@
-<?php
-
-/**
- * Version of LockManager based on using DB table 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
- * own server, all having the filelocks.sql tables (with row-level locking).
- * A majority of peer DBs must agree for a lock to be acquired.
- *
- * Caching is used to avoid hitting servers that are down.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class DBLockManager extends LockManager {
- /** @var Array Map of DB names to server config */
- protected $dbServers; // (DB name => server config array)
- /** @var Array Map of bucket indexes to peer DB lists */
- protected $dbsByBucket; // (bucket index => (ldb1, ldb2, ...))
- /** @var BagOStuff */
- protected $statusCache;
-
- protected $lockExpiry; // integer number of seconds
- protected $safeDelay; // integer number of seconds
-
- protected $session = 0; // random integer
- /** @var Array Map Database connections (DB name => Database) */
- protected $conns = array();
-
- /**
- * Construct a new instance from configuration.
- *
- * $config paramaters include:
- * 'dbServers' : Associative array of DB names to server configuration.
- * Configuration is an associative array that includes:
- * 'host' - DB server name
- * 'dbname' - DB name
- * 'type' - DB type (mysql,postgres,...)
- * 'user' - DB user
- * 'password' - DB user password
- * 'tablePrefix' - DB table prefix
- * 'flags' - DB flags (see DatabaseBase)
- * 'dbsByBucket' : Array of 1-16 consecutive integer keys, starting from 0,
- * 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).
- * '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
- */
- public function __construct( array $config ) {
- $this->dbServers = isset( $config['dbServers'] )
- ? $config['dbServers']
- : array(); // likely just using 'localDBMaster'
- // Sanitize dbsByBucket config to prevent PHP errors
- $this->dbsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
- $this->dbsByBucket = array_values( $this->dbsByBucket ); // consecutive
-
- if ( isset( $config['lockExpiry'] ) ) {
- $this->lockExpiry = $config['lockExpiry'];
- } else {
- $met = ini_get( 'max_execution_time' );
- $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
- }
- $this->safeDelay = ( $this->lockExpiry <= 0 )
- ? 60 // pick a safe-ish number to match DB timeout default
- : $this->lockExpiry; // cover worst case
-
- foreach ( $this->dbsByBucket as $bucket ) {
- if ( count( $bucket ) > 1 ) {
- // Tracks peers that couldn't be queried recently to avoid lengthy
- // connection timeouts. This is useless if each bucket has one peer.
- $this->statusCache = wfGetMainCache();
- break;
- }
- }
-
- $this->session = '';
- for ( $i = 0; $i < 5; $i++ ) {
- $this->session .= mt_rand( 0, 2147483647 );
- }
- $this->session = wfBaseConvert( sha1( $this->session ), 16, 36, 31 );
- }
-
- /**
- * @see LockManager::doLock()
- */
- protected function doLock( array $paths, $type ) {
- $status = Status::newGood();
-
- $pathsToLock = array();
- // 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
- $res = $this->doLockingQueryAll( $bucket, $paths, $type );
- if ( $res === 'cantacquire' ) {
- // Resources already locked by another process.
- // Abort and unlock everything we just locked.
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- $status->merge( $this->doUnlock( $lockedPaths, $type ) );
- return $status;
- } elseif ( $res !== true ) {
- // Couldn't contact any DBs for this bucket.
- // Abort and unlock everything we just locked.
- $status->fatal( 'lockmanager-fail-db-bucket', $bucket );
- $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 );
- }
-
- return $status;
- }
-
- /**
- * @see LockManager::doUnlock()
- */
- protected function doUnlock( array $paths, $type ) {
- $status = Status::newGood();
-
- foreach ( $paths as $path ) {
- if ( !isset( $this->locksHeld[$path] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } else {
- --$this->locksHeld[$path][$type];
- if ( $this->locksHeld[$path][$type] <= 0 ) {
- unset( $this->locksHeld[$path][$type] );
- }
- if ( !count( $this->locksHeld[$path] ) ) {
- unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
- }
- }
- }
-
- // Reference count the locks held and COMMIT when zero
- if ( !count( $this->locksHeld ) ) {
- $status->merge( $this->finishLockTransactions() );
- }
-
- return $status;
- }
-
- /**
- * 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.
- *
- * @param $lockDb string
- * @param $paths Array
- * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
- * @return bool Resources able to be locked
- * @throws DBError
- */
- protected function doLockingQuery( $lockDb, array $paths, $type ) {
- if ( $type == self::LOCK_EX ) { // writer locks
- $db = $this->getConnection( $lockDb );
- if ( !$db ) {
- return false; // bad config
- }
- $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->insert( 'filelocks_exclusive', $data, __METHOD__ );
- }
- return true;
- }
-
- /**
- * Attempt to acquire locks with the peers for a bucket.
- * This should avoid throwing any exceptions.
- *
- * @param $bucket integer
- * @param $paths Array List of resource keys to lock
- * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
- * @return bool|string One of (true, 'cantacquire', 'dberrors')
- */
- protected function doLockingQueryAll( $bucket, array $paths, $type ) {
- $yesVotes = 0; // locks made on trustable DBs
- $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining DBs
- $quorum = floor( $votesLeft/2 + 1 ); // simple majority
- // Get votes for each DB, in order, until we have enough...
- foreach ( $this->dbsByBucket[$bucket] as $lockDb ) {
- // Check that DB is not *known* to be down
- if ( $this->cacheCheckFailures( $lockDb ) ) {
- try {
- // Attempt to acquire the lock on this DB
- if ( !$this->doLockingQuery( $lockDb, $paths, $type ) ) {
- return 'cantacquire'; // vetoed; resource locked
- }
- ++$yesVotes; // success for this peer
- if ( $yesVotes >= $quorum ) {
- return true; // lock obtained
- }
- } catch ( DBConnectionError $e ) {
- $this->cacheRecordFailure( $lockDb );
- } catch ( DBError $e ) {
- if ( $this->lastErrorIndicatesLocked( $lockDb ) ) {
- return 'cantacquire'; // vetoed; resource locked
- }
- }
- }
- --$votesLeft;
- $votesNeeded = $quorum - $yesVotes;
- if ( $votesNeeded > $votesLeft ) {
- // In "trust cache" mode we don't have to meet the quorum
- break; // short-circuit
- }
- }
- // At this point, we must not have meet the quorum
- return 'dberrors'; // not enough votes to ensure correctness
- }
-
- /**
- * Get (or reuse) a connection to a lock DB
- *
- * @param $lockDb string
- * @return Database
- * @throws DBError
- */
- protected function getConnection( $lockDb ) {
- if ( !isset( $this->conns[$lockDb] ) ) {
- $db = null;
- if ( $lockDb === 'localDBMaster' ) {
- $lb = wfGetLBFactory()->newMainLB();
- $db = $lb->getConnection( DB_MASTER );
- } elseif ( isset( $this->dbServers[$lockDb] ) ) {
- $config = $this->dbServers[$lockDb];
- $db = DatabaseBase::factory( $config['type'], $config );
- }
- if ( !$db ) {
- return null; // config error?
- }
- $this->conns[$lockDb] = $db;
- $this->conns[$lockDb]->clearFlag( DBO_TRX );
- # If the connection drops, try to avoid letting the DB rollback
- # and release the locks before the file operations are finished.
- # This won't handle the case of DB server restarts however.
- $options = array();
- if ( $this->lockExpiry > 0 ) {
- $options['connTimeout'] = $this->lockExpiry;
- }
- $this->conns[$lockDb]->setSessionOptions( $options );
- $this->initConnection( $lockDb, $this->conns[$lockDb] );
- }
- if ( !$this->conns[$lockDb]->trxLevel() ) {
- $this->conns[$lockDb]->begin(); // start transaction
- }
- return $this->conns[$lockDb];
- }
-
- /**
- * Do additional initialization for new lock DB connection
- *
- * @param $lockDb string
- * @param $db DatabaseBase
- * @return void
- * @throws DBError
- */
- protected function initConnection( $lockDb, DatabaseBase $db ) {}
-
- /**
- * Commit all changes to lock-active databases.
- * This should avoid throwing any exceptions.
- *
- * @return Status
- */
- protected function finishLockTransactions() {
- $status = Status::newGood();
- foreach ( $this->conns as $lockDb => $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback(); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- $status->fatal( 'lockmanager-fail-db-release', $lockDb );
- }
- }
- }
- return $status;
- }
-
- /**
- * Check if the last DB error for $lockDb indicates
- * that a requested resource was locked by another process.
- * This should avoid throwing any exceptions.
- *
- * @param $lockDb string
- * @return bool
- */
- protected function lastErrorIndicatesLocked( $lockDb ) {
- if ( isset( $this->conns[$lockDb] ) ) { // sanity
- $db = $this->conns[$lockDb];
- return ( $db->wasDeadlock() || $db->wasLockTimeout() );
- }
- return false;
- }
-
- /**
- * Checks if the DB has not recently had connection/query errors.
- * This just avoids wasting time on doomed connection attempts.
- *
- * @param $lockDb string
- * @return bool
- */
- protected function cacheCheckFailures( $lockDb ) {
- if ( $this->statusCache && $this->safeDelay > 0 ) {
- $path = $this->getMissKey( $lockDb );
- $misses = $this->statusCache->get( $path );
- return !$misses;
- }
- return true;
- }
-
- /**
- * Log a lock request failure to the cache
- *
- * @param $lockDb string
- * @return bool Success
- */
- protected function cacheRecordFailure( $lockDb ) {
- if ( $this->statusCache && $this->safeDelay > 0 ) {
- $path = $this->getMissKey( $lockDb );
- $misses = $this->statusCache->get( $path );
- if ( $misses ) {
- return $this->statusCache->incr( $path );
- } else {
- return $this->statusCache->add( $path, 1, $this->safeDelay );
- }
- }
- return true;
- }
-
- /**
- * Get a cache key for recent query misses for a DB
- *
- * @param $lockDb string
- * @return string
- */
- protected function getMissKey( $lockDb ) {
- return 'lockmanager:querymisses:' . str_replace( ' ', '_', $lockDb );
- }
-
- /**
- * 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 intval( base_convert( $prefix, 16, 10 ) ) % count( $this->dbsByBucket );
- }
-
- /**
- * Make sure remaining locks get cleared for sanity
- */
- function __destruct() {
- foreach ( $this->conns as $lockDb => $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback(); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- // oh well
- }
- }
- $db->close();
- }
- }
-}
-
-/**
- * MySQL version of DBLockManager that supports shared locks.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class MySqlLockManager 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 initConnection( $lockDb, DatabaseBase $db ) {
- # Let this transaction see lock rows from other transactions
- $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
- }
-
- protected function doLockingQuery( $lockDb, array $paths, $type ) {
- $db = $this->getConnection( $lockDb );
- if ( !$db ) {
- return false;
- }
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
- # Build up values for INSERT clause
- $data = array();
- foreach ( $keys as $key ) {
- $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session );
- }
- # Block new writers...
- $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) );
- # Actually do the locking queries...
- if ( $type == self::LOCK_SH ) { // reader locks
- # 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.
- } 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.
- # Note: if two writers come at the same time, both bail :)
- $blocked = $db->selectField( 'filelocks_shared', '1',
- array( 'fls_key' => $keys, "fls_session != $encSession" ),
- __METHOD__
- );
- if ( !$blocked ) {
- # Build up values for INSERT clause
- $data = array();
- foreach ( $keys as $key ) {
- $data[] = array( 'fle_key' => $key );
- }
- # Block new readers/writers...
- $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
- # Bail if there are any existing readers...
- $blocked = $db->selectField( 'filelocks_shared', '1',
- array( 'fls_key' => $keys, "fls_session != $encSession" ),
- __METHOD__
- );
- }
- }
- return !$blocked;
- }
-}
diff --git a/includes/filerepo/backend/lockmanager/LSLockManager.php b/includes/filerepo/backend/lockmanager/LSLockManager.php
deleted file mode 100644
index b7ac743c..00000000
--- a/includes/filerepo/backend/lockmanager/LSLockManager.php
+++ /dev/null
@@ -1,295 +0,0 @@
-<?php
-
-/**
- * Manage locks using a lock daemon server.
- *
- * Version of LockManager based on using lock daemon 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 LockServerDaemon.php, listening on a designated TCP port.
- * A majority of peers must agree for a lock to be acquired.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class LSLockManager extends LockManager {
- /** @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 Array Map of server names to server config */
- protected $lockServers; // (server name => server config array)
- /** @var Array Map of bucket indexes to peer server lists */
- protected $srvsByBucket; // (bucket index => (lsrv1, lsrv2, ...))
-
- /** @var Array Map Server connections (server name => resource) */
- protected $conns = array();
-
- protected $connTimeout; // float number of seconds
- protected $session = ''; // random SHA-1 string
-
- /**
- * Construct a new instance from configuration.
- *
- * $config paramaters include:
- * 'lockServers' : Associative array of server names to configuration.
- * Configuration is an associative array that includes:
- * 'host' - IP address/hostname
- * 'port' - TCP port
- * 'authKey' - Secret string the lock server uses
- * 'srvsByBucket' : Array of 1-16 consecutive integer keys, starting from 0,
- * each having an odd-numbered list of server names (peers) as values.
- * 'connTimeout' : Lock server connection attempt timeout. [optional]
- *
- * @param Array $config
- */
- public function __construct( array $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
-
- if ( isset( $config['connTimeout'] ) ) {
- $this->connTimeout = $config['connTimeout'];
- } else {
- $this->connTimeout = 3; // use some sane amount
- }
-
- $this->session = '';
- for ( $i = 0; $i < 5; $i++ ) {
- $this->session .= mt_rand( 0, 2147483647 );
- }
- $this->session = wfBaseConvert( sha1( $this->session ), 16, 36, 31 );
- }
-
- protected function doLock( array $paths, $type ) {
- $status = Status::newGood();
-
- $pathsToLock = array();
- // 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
- $res = $this->doLockingRequestAll( $bucket, $paths, $type );
- if ( $res === 'cantacquire' ) {
- // Resources already locked by another process.
- // Abort and unlock everything we just locked.
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- $status->merge( $this->doUnlock( $lockedPaths, $type ) );
- return $status;
- } elseif ( $res !== true ) {
- // Couldn't contact any servers for this bucket.
- // Abort and unlock everything we just locked.
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- $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 );
- }
-
- return $status;
- }
-
- protected function doUnlock( array $paths, $type ) {
- $status = Status::newGood();
-
- foreach ( $paths as $path ) {
- if ( !isset( $this->locksHeld[$path] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } else {
- --$this->locksHeld[$path][$type];
- if ( $this->locksHeld[$path][$type] <= 0 ) {
- unset( $this->locksHeld[$path][$type] );
- }
- if ( !count( $this->locksHeld[$path] ) ) {
- unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
- }
- }
- }
-
- // Reference count the locks held and release locks when zero
- if ( !count( $this->locksHeld ) ) {
- $status->merge( $this->releaseLocks() );
- }
-
- return $status;
- }
-
- /**
- * Get a connection to a lock server and acquire locks on $paths
- *
- * @param $lockSrv string
- * @param $paths Array
- * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
- * @return bool Resources able to be locked
- */
- protected function doLockingRequest( $lockSrv, array $paths, $type ) {
- if ( $type == self::LOCK_SH ) { // reader locks
- $type = 'SH';
- } elseif ( $type == self::LOCK_EX ) { // writer locks
- $type = 'EX';
- } else {
- return true; // ok...
- }
-
- // Send out the command and get the response...
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
- $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys );
-
- return ( $response === 'ACQUIRED' );
- }
-
- /**
- * Send a command and get back the response
- *
- * @param $lockSrv string
- * @param $action string
- * @param $type string
- * @param $values Array
- * @return string|false
- */
- protected function sendCommand( $lockSrv, $action, $type, $values ) {
- $conn = $this->getConnection( $lockSrv );
- if ( !$conn ) {
- return false; // no connection
- }
- $authKey = $this->lockServers[$lockSrv]['authKey'];
- // Build of the command as a flat string...
- $values = implode( '|', $values );
- $key = sha1( $this->session . $action . $type . $values . $authKey );
- // Send out the command...
- if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) {
- return false;
- }
- // Get the response...
- $response = fgets( $conn );
- if ( $response === false ) {
- return false;
- }
- return trim( $response );
- }
-
- /**
- * Attempt to acquire 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
- * @return bool|string One of (true, 'cantacquire', 'srverrors')
- */
- protected function doLockingRequestAll( $bucket, array $paths, $type ) {
- $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 ) {
- // Attempt to acquire the lock on this peer
- if ( !$this->doLockingRequest( $lockSrv, $paths, $type ) ) {
- return 'cantacquire'; // vetoed; resource locked
- }
- ++$yesVotes; // success for this peer
- if ( $yesVotes >= $quorum ) {
- return true; // lock obtained
- }
- --$votesLeft;
- $votesNeeded = $quorum - $yesVotes;
- if ( $votesNeeded > $votesLeft ) {
- // In "trust cache" mode we don't have to meet the quorum
- break; // short-circuit
- }
- }
- // At this point, we must not have meet the quorum
- return 'srverrors'; // not enough votes to ensure correctness
- }
-
- /**
- * Get (or reuse) a connection to a lock server
- *
- * @param $lockSrv string
- * @return resource
- */
- protected function getConnection( $lockSrv ) {
- if ( !isset( $this->conns[$lockSrv] ) ) {
- $cfg = $this->lockServers[$lockSrv];
- wfSuppressWarnings();
- $errno = $errstr = '';
- $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout );
- wfRestoreWarnings();
- if ( $conn === false ) {
- return null;
- }
- $sec = floor( $this->connTimeout );
- $usec = floor( ( $this->connTimeout - floor( $this->connTimeout ) ) * 1e6 );
- stream_set_timeout( $conn, $sec, $usec );
- $this->conns[$lockSrv] = $conn;
- }
- return $this->conns[$lockSrv];
- }
-
- /**
- * Release all locks that this session is holding
- *
- * @return Status
- */
- protected function releaseLocks() {
- $status = Status::newGood();
- foreach ( $this->conns as $lockSrv => $conn ) {
- $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() );
- if ( $response !== 'RELEASED_ALL' ) {
- $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
- }
- }
- 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 intval( base_convert( $prefix, 16, 10 ) ) % count( $this->srvsByBucket );
- }
-
- /**
- * Make sure remaining locks get cleared for sanity
- */
- function __destruct() {
- $this->releaseLocks();
- foreach ( $this->conns as $conn ) {
- fclose( $conn );
- }
- }
-}
diff --git a/includes/filerepo/backend/lockmanager/LockManager.php b/includes/filerepo/backend/lockmanager/LockManager.php
deleted file mode 100644
index 23603a4f..00000000
--- a/includes/filerepo/backend/lockmanager/LockManager.php
+++ /dev/null
@@ -1,182 +0,0 @@
-<?php
-/**
- * @defgroup LockManager Lock management
- * @ingroup FileBackend
- */
-
-/**
- * @file
- * @ingroup LockManager
- * @author Aaron Schulz
- */
-
-/**
- * Class for handling resource locking.
- *
- * Locks on resource keys can either be shared or exclusive.
- *
- * Implementations must keep track of what is locked by this proccess
- * in-memory and support nested locking calls (using reference counting).
- * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
- * Locks should either be non-blocking or have low wait timeouts.
- *
- * Subclasses should avoid throwing exceptions at all costs.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-abstract class LockManager {
- /** @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_EX, // subclasses may use self::LOCK_SH
- self::LOCK_EX => self::LOCK_EX
- );
-
- /** @var Array Map of (resource path => lock type => count) */
- protected $locksHeld = array();
-
- /* 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)
-
- /**
- * Construct a new instance from configuration
- *
- * @param $config Array
- */
- public function __construct( array $config ) {}
-
- /**
- * Lock the resources at the given abstract paths
- *
- * @param $paths Array List of resource names
- * @param $type integer LockManager::LOCK_* constant
- * @return Status
- */
- final public function lock( array $paths, $type = self::LOCK_EX ) {
- return $this->doLock( array_unique( $paths ), $this->lockTypeMap[$type] );
- }
-
- /**
- * Unlock the resources at the given abstract paths
- *
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @return Status
- */
- final public function unlock( array $paths, $type = self::LOCK_EX ) {
- return $this->doUnlock( array_unique( $paths ), $this->lockTypeMap[$type] );
- }
-
- /**
- * Get the base 36 SHA-1 of a string, padded to 31 digits
- *
- * @param $path string
- * @return string
- */
- final protected static function sha1Base36( $path ) {
- return wfBaseConvert( sha1( $path ), 16, 36, 31 );
- }
-
- /**
- * Lock resources with the given keys and lock type
- *
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @return string
- */
- abstract protected function doLock( array $paths, $type );
-
- /**
- * Unlock resources with the given keys and lock type
- *
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @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;
- }
-
- protected function __clone() {}
-
- /**
- * 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.
- *
- * @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
- */
- 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 );
- }
- }
-}
-
-/**
- * Simple version of LockManager that does nothing
- * @since 1.19
- */
-class NullLockManager extends LockManager {
- protected function doLock( array $paths, $type ) {
- return Status::newGood();
- }
-
- protected function doUnlock( array $paths, $type ) {
- return Status::newGood();
- }
-}
diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php
index 3b9bd7f0..c5a0bd1b 100644
--- a/includes/filerepo/file/ArchivedFile.php
+++ b/includes/filerepo/file/ArchivedFile.php
@@ -1,6 +1,21 @@
<?php
/**
- * Deleted file in the 'filearchive' table
+ * Deleted file in the 'filearchive' table.
+ *
+ * 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 FileAbstraction
@@ -93,7 +108,7 @@ class ArchivedFile {
/**
* Loads a file object from the filearchive table
- * @return true on success or null
+ * @return bool|null True on success or null
*/
public function load() {
if ( $this->dataLoaded ) {
@@ -143,7 +158,7 @@ class ArchivedFile {
array( 'ORDER BY' => 'fa_timestamp DESC' ) );
if ( $res == false || $dbr->numRows( $res ) == 0 ) {
// this revision does not exist?
- return;
+ return null;
}
$ret = $dbr->resultObject( $res );
$row = $ret->fetchObject();
diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php
index f74fb678..557609d4 100644
--- a/includes/filerepo/file/File.php
+++ b/includes/filerepo/file/File.php
@@ -9,6 +9,21 @@
/**
* Base code for files.
*
+ * 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 FileAbstraction
*/
@@ -48,6 +63,14 @@ abstract class File {
const DELETE_SOURCE = 1;
+ // Audience options for File::getDescription()
+ const FOR_PUBLIC = 1;
+ const FOR_THIS_USER = 2;
+ const RAW = 3;
+
+ // Options for File::thumbName()
+ const THUMB_FULL_NAME = 1;
+
/**
* Some member variables can be lazy-initialised using __get(). The
* initialisation function for these variables is always a function named
@@ -68,19 +91,19 @@ abstract class File {
*/
/**
- * @var FileRepo|false
+ * @var FileRepo|bool
*/
var $repo;
/**
- * @var Title|false
+ * @var Title
*/
var $title;
var $lastError, $redirected, $redirectedTitle;
/**
- * @var FSFile|false
+ * @var FSFile|bool False if undefined
*/
protected $fsFile;
@@ -94,6 +117,8 @@ abstract class File {
*/
protected $url, $extension, $name, $path, $hashPath, $pageCount, $transformScript;
+ protected $redirectTitle;
+
/**
* @var bool
*/
@@ -111,8 +136,8 @@ abstract class File {
* may return false or throw exceptions if they are not set.
* Most subclasses will want to call assertRepoDefined() here.
*
- * @param $title Title|string|false
- * @param $repo FileRepo|false
+ * @param $title Title|string|bool
+ * @param $repo FileRepo|bool
*/
function __construct( $title, $repo ) {
if ( $title !== false ) { // subclasses may not use MW titles
@@ -127,7 +152,8 @@ abstract class File {
* valid Title object with namespace NS_FILE or null
*
* @param $title Title|string
- * @param $exception string|false Use 'exception' to throw an error on bad titles
+ * @param $exception string|bool Use 'exception' to throw an error on bad titles
+ * @throws MWException
* @return Title|null
*/
static function normalizeTitle( $title, $exception = false ) {
@@ -223,6 +249,18 @@ abstract class File {
}
/**
+ * Callback for usort() to do file sorts by name
+ *
+ * @param $a File
+ * @param $b File
+ *
+ * @return Integer: result of name comparison
+ */
+ public static function compare( File $a, File $b ) {
+ return strcmp( $a->getName(), $b->getName() );
+ }
+
+ /**
* Return the name of this file
*
* @return string
@@ -252,7 +290,7 @@ abstract class File {
/**
* Return the associated title object
*
- * @return Title|false
+ * @return Title
*/
public function getTitle() {
return $this->title;
@@ -319,17 +357,17 @@ abstract class File {
}
/**
- * Return the storage path to the file. Note that this does
- * not mean that a file actually exists under that location.
- *
- * This path depends on whether directory hashing is active or not,
- * i.e. whether the files are all found in the same directory,
- * or in hashed paths like /images/3/3c.
- *
- * Most callers don't check the return value, but ForeignAPIFile::getPath
- * returns false.
+ * Return the storage path to the file. Note that this does
+ * not mean that a file actually exists under that location.
+ *
+ * This path depends on whether directory hashing is active or not,
+ * i.e. whether the files are all found in the same directory,
+ * or in hashed paths like /images/3/3c.
+ *
+ * Most callers don't check the return value, but ForeignAPIFile::getPath
+ * returns false.
*
- * @return string|false
+ * @return string|bool ForeignAPIFile::getPath can return false
*/
public function getPath() {
if ( !isset( $this->path ) ) {
@@ -344,7 +382,7 @@ abstract class File {
* Returns false on failure. Callers must not alter the file.
* Temporary files are cleared automatically.
*
- * @return string|false
+ * @return string|bool False on failure
*/
public function getLocalRefPath() {
$this->assertRepoDefined();
@@ -383,7 +421,7 @@ abstract class File {
*
* @param $page int
*
- * @return false|number
+ * @return bool|number False on failure
*/
public function getHeight( $page = 1 ) {
return false;
@@ -430,9 +468,43 @@ abstract class File {
}
/**
+ * Will the thumbnail be animated if one would expect it to be.
+ *
+ * Currently used to add a warning to the image description page
+ *
+ * @return bool false if the main image is both animated
+ * and the thumbnail is not. In all other cases must return
+ * true. If image is not renderable whatsoever, should
+ * return true.
+ */
+ public function canAnimateThumbIfAppropriate() {
+ $handler = $this->getHandler();
+ if ( !$handler ) {
+ // We cannot handle image whatsoever, thus
+ // one would not expect it to be animated
+ // so true.
+ return true;
+ } else {
+ if ( $this->allowInlineDisplay()
+ && $handler->isAnimatedImage( $this )
+ && !$handler->canAnimateThumbnail( $this )
+ ) {
+ // Image is animated, but thumbnail isn't.
+ // This is unexpected to the user.
+ return false;
+ } else {
+ // Image is not animated, so one would
+ // not expect thumb to be
+ return true;
+ }
+ }
+ }
+
+ /**
* Get handler-specific metadata
* Overridden by LocalFile, UnregisteredLocalFile
* STUB
+ * @return bool
*/
public function getMetadata() {
return false;
@@ -462,6 +534,7 @@ abstract class File {
* Return the bit depth of the file
* Overridden by LocalFile
* STUB
+ * @return int
*/
public function getBitDepth() {
return 0;
@@ -471,6 +544,7 @@ abstract class File {
* Return the size of the image file, in bytes
* Overridden by LocalFile, UnregisteredLocalFile
* STUB
+ * @return bool
*/
public function getSize() {
return false;
@@ -492,6 +566,7 @@ abstract class File {
* Use the value returned by this function with the MEDIATYPE_xxx constants.
* Overridden by LocalFile,
* STUB
+ * @return string
*/
function getMediaType() {
return MEDIATYPE_UNKNOWN;
@@ -518,6 +593,7 @@ abstract class File {
/**
* Accessor for __get()
+ * @return bool
*/
protected function getCanRender() {
return $this->canRender();
@@ -686,15 +762,19 @@ abstract class File {
}
/**
- * Return the file name of a thumbnail with the specified parameters
+ * Return the file name of a thumbnail with the specified parameters.
+ * 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
- * @private -ish
- *
+ * @param $flags integer Bitfield that supports THUMB_* constants
* @return string
*/
- function thumbName( $params ) {
- return $this->generateThumbName( $this->getName(), $params );
+ public function thumbName( $params, $flags = 0 ) {
+ $name = ( $this->repo && !( $flags & self::THUMB_FULL_NAME ) )
+ ? $this->repo->nameForThumb( $this->getName() )
+ : $this->getName();
+ return $this->generateThumbName( $name, $params );
}
/**
@@ -705,7 +785,7 @@ abstract class File {
*
* @return string
*/
- function generateThumbName( $name, $params ) {
+ public function generateThumbName( $name, $params ) {
if ( !$this->getHandler() ) {
return null;
}
@@ -750,7 +830,7 @@ abstract class File {
/**
* Return either a MediaTransformError or placeholder thumbnail (if $wgIgnoreImageErrors)
- *
+ *
* @param $thumbPath string Thumbnail storage path
* @param $thumbUrl string Thumbnail URL
* @param $params Array
@@ -764,7 +844,7 @@ abstract class File {
return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
} else {
return new MediaTransformError( 'thumbnail_error',
- $params['width'], 0, wfMsg( 'thumbnail-dest-create' ) );
+ $params['width'], 0, wfMessage( 'thumbnail-dest-create' )->text() );
}
}
@@ -774,7 +854,7 @@ abstract class File {
* @param $params Array: 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|false
+ * @return MediaTransformOutput|bool False on failure
*/
function transform( $params, $flags = 0 ) {
global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch;
@@ -837,6 +917,13 @@ abstract class File {
}
}
+ // If the backend is ready-only, don't keep generating thumbnails
+ // only to return transformation errors, just return the error now.
+ if ( $this->repo->getReadOnlyReason() !== false ) {
+ $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
+ break;
+ }
+
// Create a temp FS file with the same extension and the thumbnail
$thumbExt = FileBackend::extensionFromPath( $thumbPath );
$tmpFile = TempFSFile::factory( 'transform_', $thumbExt );
@@ -847,7 +934,9 @@ abstract class File {
$tmpThumbPath = $tmpFile->getPath(); // path of 0-byte temp file
// Actually render the thumbnail...
+ wfProfileIn( __METHOD__ . '-doTransform' );
$thumb = $this->handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $params );
+ wfProfileOut( __METHOD__ . '-doTransform' );
$tmpFile->bind( $thumb ); // keep alive with $thumb
if ( !$thumb ) { // bad params?
@@ -859,19 +948,16 @@ abstract class File {
$thumb = $this->handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $params );
}
} elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
- $backend = $this->repo->getBackend();
- // Copy the thumbnail from the file system into storage. This avoids using
- // FileRepo::store(); getThumbPath() uses a different zone in some subclasses.
- $backend->prepare( array( 'dir' => dirname( $thumbPath ) ) );
- $status = $backend->store(
- array( 'src' => $tmpThumbPath, 'dst' => $thumbPath, 'overwrite' => 1 ),
- array( 'force' => 1, 'nonLocking' => 1, 'allowStale' => 1 )
- );
+ // Copy the thumbnail from the file system into storage...
+ $disposition = $this->getThumbDisposition( $thumbName );
+ $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition );
if ( $status->isOK() ) {
$thumb->setStoragePath( $thumbPath );
} else {
$thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
}
+ // Give extensions a chance to do something with this thumbnail...
+ wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) );
}
// Purge. Useful in the event of Core -> Squid connection failure or squid
@@ -889,6 +975,19 @@ abstract class File {
}
/**
+ * @param $thumbName string Thumbnail name
+ * @return string Content-Disposition header value
+ */
+ function getThumbDisposition( $thumbName ) {
+ $fileName = $this->name; // file name to suggest
+ $thumbExt = FileBackend::extensionFromPath( $thumbName );
+ if ( $thumbExt != '' && $thumbExt !== $this->getExtension() ) {
+ $fileName .= ".$thumbExt";
+ }
+ return FileBackend::makeContentDisposition( 'inline', $fileName );
+ }
+
+ /**
* Hook into transform() to allow migration of thumbnail files
* STUB
* Overridden by LocalFile
@@ -920,7 +1019,8 @@ abstract class File {
$path = '/common/images/icons/' . $icon;
$filepath = $wgStyleDirectory . $path;
if ( file_exists( $filepath ) ) { // always FS
- return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 );
+ $params = array( 'width' => 120, 'height' => 120 );
+ return new ThumbnailImage( $this, $wgStylePath . $path, false, $params );
}
}
return null;
@@ -938,6 +1038,7 @@ abstract class File {
* Get all thumbnail names previously generated for this file
* STUB
* Overridden by LocalFile
+ * @return array
*/
function getThumbnails() {
return array();
@@ -987,13 +1088,13 @@ abstract class File {
*
* STUB
* @param $limit integer Limit of rows to return
- * @param $start timestamp Only revisions older than $start will be returned
- * @param $end timestamp Only revisions newer than $end will be returned
+ * @param $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
*
* @return array
*/
- function getHistory($limit = null, $start = null, $end = null, $inc=true) {
+ function getHistory( $limit = null, $start = null, $end = null, $inc=true ) {
return array();
}
@@ -1004,6 +1105,7 @@ abstract class File {
*
* STUB
* Overridden in LocalFile
+ * @return bool
*/
public function nextHistoryLine() {
return false;
@@ -1185,7 +1287,7 @@ abstract class File {
*
* @param $suffix bool|string if not false, the name of a thumbnail file
*
- * @return path
+ * @return string path
*/
function getThumbUrl( $suffix = false ) {
$this->assertRepoDefined();
@@ -1251,7 +1353,7 @@ abstract class File {
*/
function isHashed() {
$this->assertRepoDefined();
- return $this->repo->isHashed();
+ return (bool)$this->repo->getHashLevels();
}
/**
@@ -1329,7 +1431,7 @@ abstract class File {
/**
* Returns the repository
*
- * @return FileRepo|false
+ * @return FileRepo|bool
*/
function getRepo() {
return $this->repo;
@@ -1360,6 +1462,7 @@ abstract class File {
/**
* Return the deletion bitfield
* STUB
+ * @return int
*/
function getVisibility() {
return 0;
@@ -1401,7 +1504,7 @@ abstract class File {
*
* @param $reason String
* @param $suppress Boolean: hide content from sysops?
- * @return true on success, false on some kind of failure
+ * @return bool on success, false on some kind of failure
* STUB
* Overridden by LocalFile
*/
@@ -1418,7 +1521,7 @@ abstract class File {
* @param $versions array set of record ids of deleted items to restore,
* or empty to restore all revisions.
* @param $unsuppress bool remove restrictions on content upon restoration?
- * @return int|false the number of file revisions restored if successful,
+ * @return int|bool the number of file revisions restored if successful,
* or false on failure
* STUB
* Overridden by LocalFile
@@ -1442,7 +1545,7 @@ abstract class File {
* Returns the number of pages of a multipage document, or false for
* documents which aren't multipage documents
*
- * @return false|int
+ * @return bool|int
*/
function pageCount() {
if ( !isset( $this->pageCount ) ) {
@@ -1536,19 +1639,25 @@ abstract class File {
}
/**
- * Get discription of file revision
+ * Get description of file revision
* STUB
*
+ * @param $audience Integer: one of:
+ * File::FOR_PUBLIC to be displayed to all users
+ * File::FOR_THIS_USER to be displayed to the given user
+ * File::RAW get the description regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
* @return string
*/
- function getDescription() {
+ function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
return null;
}
/**
* Get the 14-character timestamp of the file upload
*
- * @return string|false TS_MW timestamp or false on failure
+ * @return string|bool TS_MW timestamp or false on failure
*/
function getTimestamp() {
$this->assertRepoDefined();
@@ -1566,7 +1675,7 @@ abstract class File {
}
/**
- * Get the deletion archive key, <sha1>.<ext>
+ * Get the deletion archive key, "<sha1>.<ext>"
*
* @return string
*/
@@ -1618,7 +1727,7 @@ abstract class File {
*
* @param $path string
*
- * @return false|string False on failure
+ * @return bool|string False on failure
*/
static function sha1Base36( $path ) {
wfDeprecated( __METHOD__, '1.19' );
@@ -1698,6 +1807,14 @@ abstract class File {
}
/**
+ * Check if this file object is small and can be cached
+ * @return boolean
+ */
+ public function isCacheable() {
+ return true;
+ }
+
+ /**
* Assert that $this->repo is set to a valid FileRepo instance
* @throws MWException
*/
diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php
index 681544fd..56482611 100644
--- a/includes/filerepo/file/ForeignAPIFile.php
+++ b/includes/filerepo/file/ForeignAPIFile.php
@@ -2,6 +2,21 @@
/**
* Foreign file accessible through api.php requests.
*
+ * 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 FileAbstraction
*/
@@ -39,9 +54,9 @@ 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()
) );
@@ -68,20 +83,33 @@ class ForeignAPIFile extends File {
/**
* Get the property string for iiprop and aiprop
+ * @return string
*/
static function getProps() {
return 'timestamp|user|comment|url|size|sha1|metadata|mime';
}
// Dummy functions...
+
+ /**
+ * @return bool
+ */
public function exists() {
return $this->mExists;
}
+ /**
+ * @return bool
+ */
public function getPath() {
return false;
}
+ /**
+ * @param Array $params
+ * @param int $flags
+ * @return bool|MediaTransformOutput
+ */
function transform( $params, $flags = 0 ) {
if( !$this->canRender() ) {
// show icon
@@ -101,6 +129,11 @@ class ForeignAPIFile extends File {
}
// Info we can get from API...
+
+ /**
+ * @param $page int
+ * @return int|number
+ */
public function getWidth( $page = 1 ) {
return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0;
}
@@ -113,6 +146,9 @@ class ForeignAPIFile extends File {
return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0;
}
+ /**
+ * @return bool|null|string
+ */
public function getMetadata() {
if ( isset( $this->mInfo['metadata'] ) ) {
return serialize( self::parseMetadata( $this->mInfo['metadata'] ) );
@@ -120,6 +156,10 @@ class ForeignAPIFile extends File {
return null;
}
+ /**
+ * @param $metadata array
+ * @return array
+ */
public static function parseMetadata( $metadata ) {
if( !is_array( $metadata ) ) {
return $metadata;
@@ -131,28 +171,47 @@ class ForeignAPIFile extends File {
return $ret;
}
+ /**
+ * @return bool|int|null
+ */
public function getSize() {
return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null;
}
+ /**
+ * @return null|string
+ */
public function getUrl() {
return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
}
+ /**
+ * @param string $method
+ * @return int|null|string
+ */
public function getUser( $method='text' ) {
return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null;
}
- public function getDescription() {
+ /**
+ * @return null|string
+ */
+ public function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
}
+ /**
+ * @return null|String
+ */
function getSha1() {
return isset( $this->mInfo['sha1'] )
? wfBaseConvert( strval( $this->mInfo['sha1'] ), 16, 36, 31 )
: null;
}
+ /**
+ * @return bool|Mixed|string
+ */
function getTimestamp() {
return wfTimestamp( TS_MW,
isset( $this->mInfo['timestamp'] )
@@ -161,6 +220,9 @@ class ForeignAPIFile extends File {
);
}
+ /**
+ * @return string
+ */
function getMimeType() {
if( !isset( $this->mInfo['mime'] ) ) {
$magic = MimeMagic::singleton();
@@ -169,12 +231,18 @@ class ForeignAPIFile extends File {
return $this->mInfo['mime'];
}
- /// @todo FIXME: May guess wrong on file types that can be eg audio or video
+ /**
+ * @todo FIXME: May guess wrong on file types that can be eg audio or video
+ * @return int|string
+ */
function getMediaType() {
$magic = MimeMagic::singleton();
return $magic->getMediaType( null, $this->getMimeType() );
}
+ /**
+ * @return bool|string
+ */
function getDescriptionUrl() {
return isset( $this->mInfo['descriptionurl'] )
? $this->mInfo['descriptionurl']
@@ -183,6 +251,8 @@ class ForeignAPIFile extends File {
/**
* Only useful if we're locally caching thumbs anyway...
+ * @param $suffix string
+ * @return null|string
*/
function getThumbPath( $suffix = '' ) {
if ( $this->repo->canCacheThumbs() ) {
@@ -196,6 +266,9 @@ class ForeignAPIFile extends File {
}
}
+ /**
+ * @return array
+ */
function getThumbnails() {
$dir = $this->getThumbPath( $this->getName() );
$iter = $this->repo->getBackend()->getFileList( array( 'dir' => $dir ) );
@@ -225,6 +298,9 @@ class ForeignAPIFile extends File {
$wgMemc->delete( $key );
}
+ /**
+ * @param $options array
+ */
function purgeThumbnails( $options = array() ) {
global $wgMemc;
@@ -245,8 +321,8 @@ class ForeignAPIFile extends File {
}
# Delete the thumbnails
- $this->repo->cleanupBatch( $purgeList, FileRepo::SKIP_LOCKING );
+ $this->repo->quickPurgeBatch( $purgeList );
# Clear out the thumbnail directory if empty
- $this->repo->getBackend()->clean( array( 'dir' => $dir ) );
+ $this->repo->quickCleanDir( $dir );
}
}
diff --git a/includes/filerepo/file/ForeignDBFile.php b/includes/filerepo/file/ForeignDBFile.php
index 191a712d..91f6cb62 100644
--- a/includes/filerepo/file/ForeignDBFile.php
+++ b/includes/filerepo/file/ForeignDBFile.php
@@ -1,6 +1,21 @@
<?php
/**
- * Foreign file with an accessible MediaWiki database
+ * Foreign file with an accessible MediaWiki 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
* @ingroup FileAbstraction
@@ -39,23 +54,52 @@ class ForeignDBFile extends LocalFile {
return $file;
}
+ /**
+ * @param $srcPath String
+ * @param $flags int
+ * @throws MWException
+ */
function publish( $srcPath, $flags = 0 ) {
$this->readOnlyError();
}
+ /**
+ * @param $oldver
+ * @param $desc string
+ * @param $license string
+ * @param $copyStatus string
+ * @param $source string
+ * @param $watch bool
+ * @param $timestamp bool|string
+ * @throws MWException
+ */
function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
$watch = false, $timestamp = false ) {
$this->readOnlyError();
}
+ /**
+ * @param $versions array
+ * @param $unsuppress bool
+ * @throws MWException
+ */
function restore( $versions = array(), $unsuppress = false ) {
$this->readOnlyError();
}
+ /**
+ * @param $reason string
+ * @param $suppress bool
+ * @throws MWException
+ */
function delete( $reason, $suppress = false ) {
$this->readOnlyError();
}
+ /**
+ * @param $target Title
+ * @throws MWException
+ */
function move( $target ) {
$this->readOnlyError();
}
diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php
index 0f8b4754..695c4e9e 100644
--- a/includes/filerepo/file/LocalFile.php
+++ b/includes/filerepo/file/LocalFile.php
@@ -1,6 +1,21 @@
<?php
/**
- * Local file in the wiki's own database
+ * Local file in the wiki's own 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
* @ingroup FileAbstraction
@@ -29,6 +44,8 @@ define( 'MW_FILE_VERSION', 8 );
* @ingroup FileAbstraction
*/
class LocalFile extends File {
+ const CACHE_FIELD_MAX_LEN = 1000;
+
/**#@+
* @private
*/
@@ -58,6 +75,11 @@ class LocalFile extends File {
/**#@-*/
+ /**
+ * @var LocalRepo
+ */
+ var $repo;
+
protected $repoClass = 'LocalRepo';
/**
@@ -121,6 +143,7 @@ class LocalFile extends File {
/**
* Fields in the image table
+ * @return array
*/
static function selectFields() {
return array(
@@ -160,6 +183,7 @@ class LocalFile extends File {
/**
* Get the memcached key for the main data for this file, or false if
* there is no access to the shared cache.
+ * @return bool
*/
function getCacheKey() {
$hashedName = md5( $this->getName() );
@@ -169,6 +193,7 @@ class LocalFile extends File {
/**
* Try to load file metadata from memcached. Returns true on success.
+ * @return bool
*/
function loadFromCache() {
global $wgMemc;
@@ -238,6 +263,10 @@ class LocalFile extends File {
$this->setProps( $props );
}
+ /**
+ * @param $prefix string
+ * @return array
+ */
function getCacheFields( $prefix = 'img_' ) {
static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
@@ -286,6 +315,10 @@ 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
+ * @param $prefix string
+ * @throws MWException
+ * @return array
*/
function decodeRow( $row, $prefix = 'img_' ) {
$array = (array)$row;
@@ -407,6 +440,7 @@ 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,
@@ -462,9 +496,12 @@ class LocalFile extends File {
/** getPath inherited */
/** isVisible inhereted */
+ /**
+ * @return bool
+ */
function isMissing() {
if ( $this->missing === null ) {
- list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl(), FileRepo::FILES_ONLY );
+ list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
$this->missing = !$fileExists;
}
return $this->missing;
@@ -473,7 +510,8 @@ class LocalFile extends File {
/**
* Return the width of the image
*
- * Returns false on error
+ * @param $page int
+ * @return bool|int Returns false on error
*/
public function getWidth( $page = 1 ) {
$this->load();
@@ -493,7 +531,8 @@ class LocalFile extends File {
/**
* Return the height of the image
*
- * Returns false on error
+ * @param $page int
+ * @return bool|int Returns false on error
*/
public function getHeight( $page = 1 ) {
$this->load();
@@ -514,6 +553,7 @@ class LocalFile extends File {
* Returns ID or name of user who uploaded the file
*
* @param $type string 'text' or 'id'
+ * @return int|string
*/
function getUser( $type = 'text' ) {
$this->load();
@@ -527,12 +567,16 @@ class LocalFile extends File {
/**
* Get handler-specific metadata
+ * @return string
*/
function getMetadata() {
$this->load();
return $this->metadata;
}
+ /**
+ * @return int
+ */
function getBitDepth() {
$this->load();
return $this->bits;
@@ -540,6 +584,7 @@ class LocalFile extends File {
/**
* Return the size of the image file, in bytes
+ * @return int
*/
public function getSize() {
$this->load();
@@ -548,6 +593,7 @@ class LocalFile extends File {
/**
* Returns the mime type of the file.
+ * @return string
*/
function getMimeType() {
$this->load();
@@ -557,6 +603,7 @@ class LocalFile extends File {
/**
* Return the type of the media in the file.
* Use the value returned by this function with the MEDIATYPE_xxx constants.
+ * @return string
*/
function getMediaType() {
$this->load();
@@ -586,6 +633,9 @@ class LocalFile extends File {
/**
* Fix thumbnail files from 1.4 or before, with extreme prejudice
+ * @todo : do we still care about this? Perhaps a maintenance script
+ * can be made instead. Enabling this code results in a serious
+ * RTT regression for wikis without 404 handling.
*/
function migrateThumbFile( $thumbName ) {
$thumbDir = $this->getThumbPath();
@@ -608,10 +658,12 @@ class LocalFile extends File {
}
*/
- if ( $this->repo->fileExists( $thumbDir, FileRepo::FILES_ONLY ) ) {
+ /*
+ if ( $this->repo->fileExists( $thumbDir ) ) {
// Delete file where directory should be
$this->repo->cleanupBatch( array( $thumbDir ) );
}
+ */
}
/** getHandler inherited */
@@ -620,12 +672,10 @@ class LocalFile extends File {
/**
* Get all thumbnail names previously generated for this file
- * @param $archiveName string|false Name of an archive file
+ * @param $archiveName string|bool 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 ) {
- $this->load();
-
if ( $archiveName ) {
$dir = $this->getArchiveThumbPath( $archiveName );
} else {
@@ -690,6 +740,8 @@ class LocalFile extends File {
*/
function purgeOldThumbnails( $archiveName ) {
global $wgUseSquid;
+ wfProfileIn( __METHOD__ );
+
// Get a list of old thumbnails and URLs
$files = $this->getThumbnails( $archiveName );
$dir = array_shift( $files );
@@ -706,6 +758,8 @@ class LocalFile extends File {
}
SquidUpdate::purge( $urls );
}
+
+ wfProfileOut( __METHOD__ );
}
/**
@@ -713,6 +767,7 @@ class LocalFile extends File {
*/
function purgeThumbnails( $options = array() ) {
global $wgUseSquid;
+ wfProfileIn( __METHOD__ );
// Delete thumbnails
$files = $this->getThumbnails();
@@ -739,6 +794,8 @@ class LocalFile extends File {
}
SquidUpdate::purge( $urls );
}
+
+ wfProfileOut( __METHOD__ );
}
/**
@@ -763,14 +820,21 @@ class LocalFile extends File {
}
# Delete the thumbnails
- $this->repo->cleanupBatch( $purgeList, FileRepo::SKIP_LOCKING );
+ $this->repo->quickPurgeBatch( $purgeList );
# Clear out the thumbnail directory if empty
- $this->repo->getBackend()->clean( array( 'dir' => $dir ) );
+ $this->repo->quickCleanDir( $dir );
}
/** purgeDescription inherited */
/** purgeEverything inherited */
+ /**
+ * @param $limit null
+ * @param $start null
+ * @param $end null
+ * @param $inc bool
+ * @return array
+ */
function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
$dbr = $this->repo->getSlaveDB();
$tables = array( 'oldimage' );
@@ -824,6 +888,7 @@ class LocalFile extends File {
* 0 return line for current version
* 1 query for old versions, return first one
* 2, ... return next old version from above query
+ * @return bool
*/
public function nextHistoryLine() {
# Polymorphic function name to distinguish foreign and local fetches
@@ -888,21 +953,25 @@ class LocalFile extends File {
* @param $comment String: upload description
* @param $pageText String: text to use for the new description page,
* if a new description page is created
- * @param $flags Integer: flags for publish()
- * @param $props Array: File properties, if known. This can be used to reduce the
+ * @param $flags Integer|bool: flags for publish()
+ * @param $props Array|bool: 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: timestamp for img_timestamp, or false to use the current time
- * @param $user Mixed: User object or null to use $wgUser
+ * @param $timestamp String|bool: 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
* archive name, or an empty string if it was a new file.
*/
function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
global $wgContLang;
+
+ if ( $this->getRepo()->getReadOnlyReason() !== false ) {
+ return $this->readOnlyFatalStatus();
+ }
+
// truncate nicely or the DB will do it for us
- // non-nicely (dangling multi-byte chars, non-truncated
- // version in cache).
+ // non-nicely (dangling multi-byte chars, non-truncated version in cache).
$comment = $wgContLang->truncate( $comment, 255 );
$this->lock(); // begin
$status = $this->publish( $srcPath, $flags );
@@ -923,6 +992,14 @@ class LocalFile extends File {
/**
* Record a file upload in the upload log and the image table
+ * @param $oldver
+ * @param $desc string
+ * @param $license string
+ * @param $copyStatus string
+ * @param $source string
+ * @param $watch bool
+ * @param $timestamp string|bool
+ * @return bool
*/
function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
$watch = false, $timestamp = false )
@@ -942,20 +1019,31 @@ class LocalFile extends File {
/**
* Record a file upload in the upload log and the image table
+ * @param $oldver
+ * @param $comment string
+ * @param $pageText string
+ * @param $props bool|array
+ * @param $timestamp bool|string
+ * @param $user null|User
+ * @return bool
*/
function recordUpload2(
$oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
) {
+ wfProfileIn( __METHOD__ );
+
if ( is_null( $user ) ) {
global $wgUser;
$user = $wgUser;
}
$dbw = $this->repo->getMasterDB();
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
if ( !$props ) {
+ wfProfileIn( __METHOD__ . '-getProps' );
$props = $this->repo->getFileProps( $this->getVirtualUrl() );
+ wfProfileOut( __METHOD__ . '-getProps' );
}
if ( $timestamp === false ) {
@@ -968,15 +1056,10 @@ class LocalFile extends File {
$props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
$this->setProps( $props );
- # Delete thumbnails
- $this->purgeThumbnails();
-
- # The file is already on its final location, remove it from the squid cache
- SquidUpdate::purge( array( $this->getURL() ) );
-
# Fail now if the file isn't there
if ( !$this->fileExists ) {
wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
+ wfProfileOut( __METHOD__ );
return false;
}
@@ -1005,15 +1088,12 @@ class LocalFile extends File {
__METHOD__,
'IGNORE'
);
-
if ( $dbw->affectedRows() == 0 ) {
- if ( $oldver == '' ) { // XXX
- # (bug 34993) publish() can displace the current file and yet fail to save
- # a new one. The next publish attempt will treat the file as a brand new file
- # and pass an empty $oldver. Allow this bogus value so we can displace the
- # `image` row to `oldimage`, leaving room for the new current file `image` row.
- #throw new MWException( "Empty oi_archive_name. Database and storage out of sync?" );
- }
+ # (bug 34993) Note: $oldver can be empty here, if the previous
+ # version of the file was broken. Allow registration of the new
+ # version to continue anyway, because that's better than having
+ # an image that's not fixable by user operations.
+
$reupload = true;
# Collision, this is an update of a file
# Insert previous contents into oldimage
@@ -1060,16 +1140,8 @@ class LocalFile extends File {
__METHOD__
);
} else {
- # This is a new file
- # Update the image count
- $dbw->begin( __METHOD__ );
- $dbw->update(
- 'site_stats',
- array( 'ss_images = ss_images+1' ),
- '*',
- __METHOD__
- );
- $dbw->commit( __METHOD__ );
+ # This is a new file, so update the image count
+ DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
}
$descTitle = $this->getTitle();
@@ -1079,14 +1151,17 @@ class LocalFile extends File {
# Add the log entry
$log = new LogPage( 'upload' );
$action = $reupload ? 'overwrite' : 'upload';
- $log->addEntry( $action, $descTitle, $comment, array(), $user );
+ $logId = $log->addEntry( $action, $descTitle, $comment, array(), $user );
+
+ wfProfileIn( __METHOD__ . '-edit' );
+ $exists = $descTitle->exists();
- if ( $descTitle->exists() ) {
+ if ( $exists ) {
# Create a null revision
$latest = $descTitle->getLatestRevID();
$nullRevision = Revision::newNullRevision(
$dbw,
- $descTitle->getArticleId(),
+ $descTitle->getArticleID(),
$log->getRcComment(),
false
);
@@ -1096,6 +1171,15 @@ class LocalFile extends File {
wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
$wikiPage->updateRevisionOn( $dbw, $nullRevision );
}
+ }
+
+ # Commit the transaction now, in case something goes wrong later
+ # The most important thing is that files don't get lost, especially archives
+ # NOTE: once we have support for nested transactions, the commit may be moved
+ # to after $wikiPage->doEdit has been called.
+ $dbw->commit( __METHOD__ );
+
+ if ( $exists ) {
# Invalidate the cache for the description page
$descTitle->invalidateCache();
$descTitle->purgeSquid();
@@ -1103,12 +1187,19 @@ class LocalFile extends File {
# 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.
- $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
+ $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
+
+ if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
+ $dbw->begin();
+ $dbw->update( 'logging',
+ array( 'log_page' => $status->value['revision']->getPage() ),
+ array( 'log_id' => $logId ),
+ __METHOD__
+ );
+ $dbw->commit(); // commit before anything bad can happen
+ }
}
-
- # Commit the transaction now, in case something goes wrong later
- # The most important thing is that files don't get lost, especially archives
- $dbw->commit();
+ wfProfileOut( __METHOD__ . '-edit' );
# Save to cache and purge the squid
# We shall not saveToCache before the commit since otherwise
@@ -1116,8 +1207,20 @@ class LocalFile extends File {
# which in fact doesn't really exist (bug 24978)
$this->saveToCache();
+ if ( $reupload ) {
+ # Delete old thumbnails
+ wfProfileIn( __METHOD__ . '-purge' );
+ $this->purgeThumbnails();
+ wfProfileOut( __METHOD__ . '-purge' );
+
+ # Remove the old file from the squid cache
+ SquidUpdate::purge( array( $this->getURL() ) );
+ }
+
# Hooks, hooks, the magic of hooks...
+ wfProfileIn( __METHOD__ . '-hooks' );
wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
+ wfProfileOut( __METHOD__ . '-hooks' );
# Invalidate cache for all pages using this file
$update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
@@ -1131,6 +1234,7 @@ class LocalFile extends File {
$update->doUpdate();
}
+ wfProfileOut( __METHOD__ );
return true;
}
@@ -1167,6 +1271,10 @@ class LocalFile extends File {
* archive name, or an empty string if it was a new file.
*/
function publishTo( $srcPath, $dstRel, $flags = 0 ) {
+ if ( $this->getRepo()->getReadOnlyReason() !== false ) {
+ return $this->readOnlyFatalStatus();
+ }
+
$this->lock(); // begin
$archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
@@ -1203,20 +1311,26 @@ class LocalFile extends File {
* @return FileRepoStatus object.
*/
function move( $target ) {
- wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
- $this->lock(); // begin
+ if ( $this->getRepo()->getReadOnlyReason() !== false ) {
+ return $this->readOnlyFatalStatus();
+ }
+ wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
$batch = new LocalFileMoveBatch( $this, $target );
- $batch->addCurrent();
- $batch->addOlds();
+ $this->lock(); // begin
+ $batch->addCurrent();
+ $archiveNames = $batch->addOlds();
$status = $batch->execute();
+ $this->unlock(); // done
+
wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
$this->purgeEverything();
- $this->unlock(); // done
-
- if ( $status->isOk() ) {
+ foreach ( $archiveNames as $archiveName ) {
+ $this->purgeOldThumbnails( $archiveName );
+ }
+ if ( $status->isOK() ) {
// Now switch the object
$this->title = $target;
// Force regeneration of the name and hashpath
@@ -1242,30 +1356,27 @@ class LocalFile extends File {
* @return FileRepoStatus object.
*/
function delete( $reason, $suppress = false ) {
- $this->lock(); // begin
+ if ( $this->getRepo()->getReadOnlyReason() !== false ) {
+ return $this->readOnlyFatalStatus();
+ }
$batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
- $batch->addCurrent();
+ $this->lock(); // begin
+ $batch->addCurrent();
# Get old version relative paths
- $dbw = $this->repo->getMasterDB();
- $result = $dbw->select( 'oldimage',
- array( 'oi_archive_name' ),
- array( 'oi_name' => $this->getName() ) );
- foreach ( $result as $row ) {
- $batch->addOld( $row->oi_archive_name );
- $this->purgeOldThumbnails( $row->oi_archive_name );
- }
+ $archiveNames = $batch->addOlds();
$status = $batch->execute();
+ $this->unlock(); // done
- if ( $status->ok ) {
- // Update site_stats
- $site_stats = $dbw->tableName( 'site_stats' );
- $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
- $this->purgeEverything();
+ if ( $status->isOK() ) {
+ DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
}
- $this->unlock(); // done
+ $this->purgeEverything();
+ foreach ( $archiveNames as $archiveName ) {
+ $this->purgeOldThumbnails( $archiveName );
+ }
return $status;
}
@@ -1285,16 +1396,19 @@ class LocalFile extends File {
* @return FileRepoStatus object.
*/
function deleteOld( $archiveName, $reason, $suppress = false ) {
- $this->lock(); // begin
+ if ( $this->getRepo()->getReadOnlyReason() !== false ) {
+ return $this->readOnlyFatalStatus();
+ }
$batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
+
+ $this->lock(); // begin
$batch->addOld( $archiveName );
- $this->purgeOldThumbnails( $archiveName );
$status = $batch->execute();
-
$this->unlock(); // done
- if ( $status->ok ) {
+ $this->purgeOldThumbnails( $archiveName );
+ if ( $status->isOK() ) {
$this->purgeDescription();
$this->purgeHistory();
}
@@ -1308,30 +1422,32 @@ class LocalFile extends File {
*
* May throw database exceptions on error.
*
- * @param $versions set of record ids of deleted items to restore,
+ * @param $versions array set of record ids of deleted items to restore,
* or empty to restore all revisions.
* @param $unsuppress Boolean
* @return FileRepoStatus
*/
function restore( $versions = array(), $unsuppress = false ) {
+ if ( $this->getRepo()->getReadOnlyReason() !== false ) {
+ return $this->readOnlyFatalStatus();
+ }
+
$batch = new LocalFileRestoreBatch( $this, $unsuppress );
+ $this->lock(); // begin
if ( !$versions ) {
$batch->addAll();
} else {
$batch->addIds( $versions );
}
-
$status = $batch->execute();
-
- if ( !$status->isGood() ) {
- return $status;
+ if ( $status->isGood() ) {
+ $cleanupStatus = $batch->cleanup();
+ $cleanupStatus->successCount = 0;
+ $cleanupStatus->failCount = 0;
+ $status->merge( $cleanupStatus );
}
-
- $cleanupStatus = $batch->cleanup();
- $cleanupStatus->successCount = 0;
- $cleanupStatus->failCount = 0;
- $status->merge( $cleanupStatus );
+ $this->unlock(); // done
return $status;
}
@@ -1343,6 +1459,7 @@ class LocalFile extends File {
/**
* Get the URL of the file description page.
+ * @return String
*/
function getDescriptionUrl() {
return $this->title->getLocalUrl();
@@ -1352,10 +1469,11 @@ class LocalFile extends File {
* 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.
+ * @return bool|mixed
*/
function getDescriptionText() {
global $wgParser;
- $revision = Revision::newFromTitle( $this->title );
+ $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
if ( !$revision ) return false;
$text = $revision->getText();
if ( !$text ) return false;
@@ -1363,16 +1481,33 @@ class LocalFile extends File {
return $pout->getText();
}
- function getDescription() {
+ /**
+ * @return string
+ */
+ function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
$this->load();
- return $this->description;
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
+ return '';
+ } elseif ( $audience == self::FOR_THIS_USER
+ && !$this->userCan( self::DELETED_COMMENT, $user ) )
+ {
+ return '';
+ } else {
+ return $this->description;
+ }
}
+ /**
+ * @return bool|string
+ */
function getTimestamp() {
$this->load();
return $this->timestamp;
}
+ /**
+ * @return string
+ */
function getSha1() {
$this->load();
// Initialise now if necessary
@@ -1396,6 +1531,14 @@ class LocalFile extends File {
}
/**
+ * @return bool
+ */
+ function isCacheable() {
+ $this->load();
+ return strlen( $this->metadata ) <= self::CACHE_FIELD_MAX_LEN; // avoid OOMs
+ }
+
+ /**
* Start a transaction and lock the image for update
* Increments a reference counter if the lock is already held
* @return boolean True if the image exists, false otherwise
@@ -1404,11 +1547,12 @@ class LocalFile extends File {
$dbw = $this->repo->getMasterDB();
if ( !$this->locked ) {
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$this->locked++;
}
- return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
+ return $dbw->selectField( 'image', '1',
+ array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
}
/**
@@ -1420,7 +1564,7 @@ class LocalFile extends File {
--$this->locked;
if ( !$this->locked ) {
$dbw = $this->repo->getMasterDB();
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
}
}
}
@@ -1431,7 +1575,15 @@ class LocalFile extends File {
function unlockAndRollback() {
$this->locked = false;
$dbw = $this->repo->getMasterDB();
- $dbw->rollback();
+ $dbw->rollback( __METHOD__ );
+ }
+
+ /**
+ * @return Status
+ */
+ protected function readOnlyFatalStatus() {
+ return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
+ $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
}
} // LocalFile class
@@ -1451,6 +1603,11 @@ class LocalFileDeleteBatch {
var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
var $status;
+ /**
+ * @param $file File
+ * @param $reason string
+ * @param $suppress bool
+ */
function __construct( File $file, $reason = '', $suppress = false ) {
$this->file = $file;
$this->reason = $reason;
@@ -1462,11 +1619,39 @@ class LocalFileDeleteBatch {
$this->srcRels['.'] = $this->file->getRel();
}
+ /**
+ * @param $oldName string
+ */
function addOld( $oldName ) {
$this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
$this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
}
+ /**
+ * Add the old versions of the image to the batch
+ * @return Array List of archive names from old versions
+ */
+ function addOlds() {
+ $archiveNames = array();
+
+ $dbw = $this->file->repo->getMasterDB();
+ $result = $dbw->select( 'oldimage',
+ array( 'oi_archive_name' ),
+ array( 'oi_name' => $this->file->getName() ),
+ __METHOD__
+ );
+
+ foreach ( $result as $row ) {
+ $this->addOld( $row->oi_archive_name );
+ $archiveNames[] = $row->oi_archive_name;
+ }
+
+ return $archiveNames;
+ }
+
+ /**
+ * @return array
+ */
function getOldRels() {
if ( !isset( $this->srcRels['.'] ) ) {
$oldRels =& $this->srcRels;
@@ -1480,6 +1665,9 @@ class LocalFileDeleteBatch {
return array( $oldRels, $deleteCurrent );
}
+ /**
+ * @return array
+ */
protected function getHashes() {
$hashes = array();
list( $oldRels, $deleteCurrent ) = $this->getOldRels();
@@ -1601,7 +1789,7 @@ class LocalFileDeleteBatch {
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_deleted_reason' => $encReason,
- 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
+ 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
'fa_name' => 'oi_name',
'fa_archive_name' => 'oi_archive_name',
@@ -1617,7 +1805,6 @@ class LocalFileDeleteBatch {
'fa_user' => 'oi_user',
'fa_user_text' => 'oi_user_text',
'fa_timestamp' => 'oi_timestamp',
- 'fa_deleted' => $bitfield
), $where, __METHOD__ );
}
}
@@ -1641,9 +1828,9 @@ class LocalFileDeleteBatch {
/**
* Run the transaction
+ * @return FileRepoStatus
*/
function execute() {
- global $wgUseSquid;
wfProfileIn( __METHOD__ );
$this->file->lock();
@@ -1699,7 +1886,7 @@ class LocalFileDeleteBatch {
$this->status->merge( $status );
}
- if ( !$this->status->ok ) {
+ if ( !$this->status->isOK() ) {
// Critical file deletion error
// Roll back inserts, release lock and abort
// TODO: delete the defunct filearchive rows if we are using a non-transactional DB
@@ -1708,17 +1895,6 @@ class LocalFileDeleteBatch {
return $this->status;
}
- // Purge squid
- if ( $wgUseSquid ) {
- $urls = array();
-
- foreach ( $this->srcRels as $srcRel ) {
- $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
- $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
- }
- SquidUpdate::purge( $urls );
- }
-
// Delete image/oldimage rows
$this->doDBDeletes();
@@ -1731,6 +1907,8 @@ class LocalFileDeleteBatch {
/**
* Removes non-existent files from a deletion batch.
+ * @param $batch array
+ * @return array
*/
function removeNonexistentFiles( $batch ) {
$files = $newBatch = array();
@@ -1740,7 +1918,7 @@ class LocalFileDeleteBatch {
$files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
}
- $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
+ $result = $this->file->repo->fileExistsBatch( $files );
foreach ( $batch as $batchItem ) {
if ( $result[$batchItem[0]] ) {
@@ -1766,6 +1944,10 @@ class LocalFileRestoreBatch {
var $cleanupBatch, $ids, $all, $unsuppress = false;
+ /**
+ * @param $file File
+ * @param $unsuppress bool
+ */
function __construct( File $file, $unsuppress = false ) {
$this->file = $file;
$this->cleanupBatch = $this->ids = array();
@@ -1800,6 +1982,7 @@ class LocalFileRestoreBatch {
* rows and there's no need to keep the image row locked while it's acquiring those locks
* The caller may have its own transaction open.
* So we save the batch and let the caller call cleanup()
+ * @return FileRepoStatus
*/
function execute() {
global $wgLang;
@@ -2003,9 +2186,7 @@ class LocalFileRestoreBatch {
if ( !$exists ) {
wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
- // Update site_stats
- $site_stats = $dbw->tableName( 'site_stats' );
- $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+ DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
$this->file->purgeEverything();
} else {
@@ -2022,13 +2203,16 @@ class LocalFileRestoreBatch {
/**
* Removes non-existent files from a store batch.
+ * @param $triplets array
+ * @return array
*/
function removeNonexistentFiles( $triplets ) {
$files = $filteredTriplets = array();
- foreach ( $triplets as $file )
+ foreach ( $triplets as $file ) {
$files[$file[0]] = $file[0];
+ }
- $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
+ $result = $this->file->repo->fileExistsBatch( $files );
foreach ( $triplets as $file ) {
if ( $result[$file[0]] ) {
@@ -2041,6 +2225,8 @@ class LocalFileRestoreBatch {
/**
* Removes non-existent files from a cleanup batch.
+ * @param $batch array
+ * @return array
*/
function removeNonexistentFromCleanup( $batch ) {
$files = $newBatch = array();
@@ -2051,7 +2237,7 @@ class LocalFileRestoreBatch {
rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
}
- $result = $repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
+ $result = $repo->fileExistsBatch( $files );
foreach ( $batch as $file ) {
if ( $result[$file] ) {
@@ -2065,6 +2251,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
*/
function cleanup() {
if ( !$this->cleanupBatch ) {
@@ -2109,7 +2296,7 @@ class LocalFileRestoreBatch {
class LocalFileMoveBatch {
/**
- * @var File
+ * @var LocalFile
*/
var $file;
@@ -2118,8 +2305,17 @@ class LocalFileMoveBatch {
*/
var $target;
- var $cur, $olds, $oldCount, $archive, $db;
+ var $cur, $olds, $oldCount, $archive;
+ /**
+ * @var DatabaseBase
+ */
+ var $db;
+
+ /**
+ * @param File $file
+ * @param Title $target
+ */
function __construct( File $file, Title $target ) {
$this->file = $file;
$this->target = $target;
@@ -2129,7 +2325,7 @@ class LocalFileMoveBatch {
$this->newName = $this->file->repo->getNameFromTitle( $this->target );
$this->oldRel = $this->oldHash . $this->oldName;
$this->newRel = $this->newHash . $this->newName;
- $this->db = $file->repo->getMasterDb();
+ $this->db = $file->getRepo()->getMasterDb();
}
/**
@@ -2141,11 +2337,13 @@ class LocalFileMoveBatch {
/**
* Add the old versions of the image to the batch
+ * @return Array List of archive names from old versions
*/
function addOlds() {
$archiveBase = 'archive';
$this->olds = array();
$this->oldCount = 0;
+ $archiveNames = array();
$result = $this->db->select( 'oldimage',
array( 'oi_archive_name', 'oi_deleted' ),
@@ -2154,6 +2352,7 @@ class LocalFileMoveBatch {
);
foreach ( $result as $row ) {
+ $archiveNames[] = $row->oi_archive_name;
$oldName = $row->oi_archive_name;
$bits = explode( '!', $oldName, 2 );
@@ -2181,39 +2380,49 @@ class LocalFileMoveBatch {
"{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
);
}
+
+ return $archiveNames;
}
/**
* Perform the move.
+ * @return FileRepoStatus
*/
function execute() {
$repo = $this->file->repo;
$status = $repo->newGood();
- $triplets = $this->getMoveTriplets();
+ $triplets = $this->getMoveTriplets();
$triplets = $this->removeNonexistentFiles( $triplets );
- // Copy the files into their new location
- $statusMove = $repo->storeBatch( $triplets );
+ $this->file->lock(); // begin
+ // Rename the file versions metadata in the DB.
+ // This implicitly locks the destination file, which avoids race conditions.
+ // If we moved the files from A -> C before DB updates, another process could
+ // move files from B -> C at this point, causing storeBatch() to fail and thus
+ // cleanupTarget() to trigger. It would delete the C files and cause data loss.
+ $statusDb = $this->doDBUpdates();
+ if ( !$statusDb->isGood() ) {
+ $this->file->unlockAndRollback();
+ $statusDb->ok = false;
+ return $statusDb;
+ }
+ wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
+
+ // Copy the files into their new location.
+ // If a prior process fataled copying or cleaning up files we tolerate any
+ // of the existing files if they are identical to the ones being stored.
+ $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
if ( !$statusMove->isGood() ) {
- wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
+ // Delete any files copied over (while the destination is still locked)
$this->cleanupTarget( $triplets );
+ $this->file->unlockAndRollback(); // unlocks the destination
+ wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
$statusMove->ok = false;
return $statusMove;
}
-
- $this->db->begin();
- $statusDb = $this->doDBUpdates();
- wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
- if ( !$statusDb->isGood() ) {
- $this->db->rollback();
- // Something went wrong with the DB updates, so remove the target files
- $this->cleanupTarget( $triplets );
- $statusDb->ok = false;
- return $statusDb;
- }
- $this->db->commit();
+ $this->file->unlock(); // done
// Everything went ok, remove the source files
$this->cleanupSource( $triplets );
@@ -2256,7 +2465,8 @@ class LocalFileMoveBatch {
'oldimage',
array(
'oi_name' => $this->newName,
- 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
+ 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
+ $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
),
array( 'oi_name' => $this->oldName ),
__METHOD__
@@ -2265,7 +2475,10 @@ class LocalFileMoveBatch {
$affected = $dbw->affectedRows();
$total = $this->oldCount;
$status->successCount += $affected;
- $status->failCount += $total - $affected;
+ // Bug 34934: $total is based on files that actually exist.
+ // There may be more DB rows than such files, in which case $affected
+ // can be greater than $total. We use max() to avoid negatives here.
+ $status->failCount += max( 0, $total - $affected );
if ( $status->failCount ) {
$status->error( 'imageinvalidfilename' );
}
@@ -2275,6 +2488,7 @@ class LocalFileMoveBatch {
/**
* Generate triplets for FileRepo::storeBatch().
+ * @return array
*/
function getMoveTriplets() {
$moves = array_merge( array( $this->cur ), $this->olds );
@@ -2292,6 +2506,8 @@ class LocalFileMoveBatch {
/**
* Removes non-existent files from move batch.
+ * @param $triplets array
+ * @return array
*/
function removeNonexistentFiles( $triplets ) {
$files = array();
@@ -2300,7 +2516,7 @@ class LocalFileMoveBatch {
$files[$file[0]] = $file[0];
}
- $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
+ $result = $this->file->repo->fileExistsBatch( $files );
$filteredTriplets = array();
foreach ( $triplets as $file ) {
@@ -2322,6 +2538,7 @@ class LocalFileMoveBatch {
// Create dest pairs from the triplets
$pairs = array();
foreach ( $triplets as $triplet ) {
+ // $triplet: (old source virtual URL, dst zone, dest rel)
$pairs[] = array( $triplet[1], $triplet[2] );
}
diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php
index ebd83c4d..40d7dca7 100644
--- a/includes/filerepo/file/OldLocalFile.php
+++ b/includes/filerepo/file/OldLocalFile.php
@@ -1,6 +1,21 @@
<?php
/**
- * Old file in the oldimage table
+ * Old file in the oldimage table.
+ *
+ * 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 FileAbstraction
@@ -17,6 +32,13 @@ class OldLocalFile extends LocalFile {
const CACHE_VERSION = 1;
const MAX_CACHE_ROWS = 20;
+ /**
+ * @param $title Title
+ * @param $repo FileRepo
+ * @param $time null
+ * @return OldLocalFile
+ * @throws MWException
+ */
static function newFromTitle( $title, $repo, $time = null ) {
# The null default value is only here to avoid an E_STRICT
if ( $time === null ) {
@@ -25,10 +47,21 @@ class OldLocalFile extends LocalFile {
return new self( $title, $repo, $time, null );
}
+ /**
+ * @param $title Title
+ * @param $repo FileRepo
+ * @param $archiveName
+ * @return OldLocalFile
+ */
static function newFromArchiveName( $title, $repo, $archiveName ) {
return new self( $title, $repo, null, $archiveName );
}
+ /**
+ * @param $row
+ * @param $repo FileRepo
+ * @return OldLocalFile
+ */
static function newFromRow( $row, $repo ) {
$title = Title::makeTitle( NS_FILE, $row->oi_name );
$file = new self( $title, $repo, null, $row->oi_archive_name );
@@ -61,9 +94,10 @@ class OldLocalFile extends LocalFile {
return false;
}
}
-
+
/**
* Fields in the oldimage table
+ * @return array
*/
static function selectFields() {
return array(
@@ -91,6 +125,7 @@ class OldLocalFile extends LocalFile {
* @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
+ * @throws MWException
*/
function __construct( $title, $repo, $time, $archiveName ) {
parent::__construct( $title, $repo );
@@ -101,10 +136,16 @@ class OldLocalFile extends LocalFile {
}
}
+ /**
+ * @return bool
+ */
function getCacheKey() {
return false;
}
+ /**
+ * @return String
+ */
function getArchiveName() {
if ( !isset( $this->archive_name ) ) {
$this->load();
@@ -112,10 +153,16 @@ class OldLocalFile extends LocalFile {
return $this->archive_name;
}
+ /**
+ * @return bool
+ */
function isOld() {
return true;
}
+ /**
+ * @return bool
+ */
function isVisible() {
return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
}
@@ -140,6 +187,10 @@ class OldLocalFile extends LocalFile {
wfProfileOut( __METHOD__ );
}
+ /**
+ * @param $prefix string
+ * @return array
+ */
function getCacheFields( $prefix = 'img_' ) {
$fields = parent::getCacheFields( $prefix );
$fields[] = $prefix . 'archive_name';
@@ -147,10 +198,16 @@ class OldLocalFile extends LocalFile {
return $fields;
}
+ /**
+ * @return string
+ */
function getRel() {
return 'archive/' . $this->getHashPath() . $this->getArchiveName();
}
+ /**
+ * @return string
+ */
function getUrlRel() {
return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() );
}
@@ -172,14 +229,15 @@ class OldLocalFile extends LocalFile {
wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n");
$dbw->update( 'oldimage',
array(
- 'oi_width' => $this->width,
- 'oi_height' => $this->height,
- 'oi_bits' => $this->bits,
+ 'oi_size' => $this->size, // sanity
+ 'oi_width' => $this->width,
+ 'oi_height' => $this->height,
+ 'oi_bits' => $this->bits,
'oi_media_type' => $this->media_type,
'oi_major_mime' => $major,
'oi_minor_mime' => $minor,
- 'oi_metadata' => $this->metadata,
- 'oi_sha1' => $this->sha1,
+ 'oi_metadata' => $this->metadata,
+ 'oi_sha1' => $this->sha1,
), array(
'oi_name' => $this->getName(),
'oi_archive_name' => $this->archive_name ),
@@ -219,47 +277,52 @@ class OldLocalFile extends LocalFile {
$this->load();
return Revision::userCanBitfield( $this->deleted, $field, $user );
}
-
+
/**
* 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
- * $timestamp!$filename, where $filename must match $this->getName()
+ * @param $archiveName string Full archive name of the file, in the form
+ * $timestamp!$filename, where $filename must match $this->getName()
*
+ * @param $timestamp string
+ * @param $comment string
+ * @param $user
+ * @param $flags int
* @return FileRepoStatus
*/
function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) {
$this->lock();
-
+
$dstRel = 'archive/' . $this->getHashPath() . $archiveName;
$status = $this->publishTo( $srcPath, $dstRel,
$flags & File::DELETE_SOURCE ? FileRepo::DELETE_SOURCE : 0
);
-
+
if ( $status->isGood() ) {
if ( !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) ) {
$status->fatal( 'filenotfound', $srcPath );
}
}
-
+
$this->unlock();
-
+
return $status;
}
-
+
/**
* 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 $timestamp string
* @param $comment string Upload comment
* @param $user User User who did this upload
* @return bool
*/
function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) {
$dbw = $this->repo->getMasterDB();
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
$props = $this->repo->getFileProps( $dstPath );
@@ -287,9 +350,9 @@ class OldLocalFile extends LocalFile {
), __METHOD__
);
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
return true;
}
-
+
}
diff --git a/includes/filerepo/file/UnregisteredLocalFile.php b/includes/filerepo/file/UnregisteredLocalFile.php
index cd9d3d02..8d4a3f88 100644
--- a/includes/filerepo/file/UnregisteredLocalFile.php
+++ b/includes/filerepo/file/UnregisteredLocalFile.php
@@ -1,6 +1,21 @@
<?php
/**
- * File without associated database record
+ * File without associated database record.
+ *
+ * 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 FileAbstraction
@@ -19,7 +34,7 @@
* @ingroup FileAbstraction
*/
class UnregisteredLocalFile extends File {
- var $title, $path, $mime, $dims;
+ var $title, $path, $mime, $dims, $metadata;
/**
* @var MediaHandler
@@ -47,12 +62,12 @@ class UnregisteredLocalFile extends File {
/**
* Create an UnregisteredLocalFile based on a path or a (title,repo) pair.
* A FileRepo object is not required here, unlike most other File classes.
- *
+ *
* @throws MWException
- * @param $title Title|false
- * @param $repo FileRepo
- * @param $path string
- * @param $mime string
+ * @param $title Title|bool
+ * @param $repo FileRepo|bool
+ * @param $path string|bool
+ * @param $mime string|bool
*/
function __construct( $title = false, $repo = false, $path = false, $mime = false ) {
if ( !( $title && $repo ) && !$path ) {
@@ -79,6 +94,10 @@ class UnregisteredLocalFile extends File {
$this->dims = array();
}
+ /**
+ * @param $page int
+ * @return bool
+ */
private function cachePageDimensions( $page = 1 ) {
if ( !isset( $this->dims[$page] ) ) {
if ( !$this->getHandler() ) {
@@ -89,16 +108,27 @@ class UnregisteredLocalFile extends File {
return $this->dims[$page];
}
+ /**
+ * @param $page int
+ * @return number
+ */
function getWidth( $page = 1 ) {
$dim = $this->cachePageDimensions( $page );
return $dim['width'];
}
+ /**
+ * @param $page int
+ * @return number
+ */
function getHeight( $page = 1 ) {
$dim = $this->cachePageDimensions( $page );
return $dim['height'];
}
+ /**
+ * @return bool|string
+ */
function getMimeType() {
if ( !isset( $this->mime ) ) {
$magic = MimeMagic::singleton();
@@ -107,6 +137,10 @@ class UnregisteredLocalFile extends File {
return $this->mime;
}
+ /**
+ * @param $filename String
+ * @return Array|bool
+ */
function getImageSize( $filename ) {
if ( !$this->getHandler() ) {
return false;
@@ -114,6 +148,9 @@ class UnregisteredLocalFile extends File {
return $this->handler->getImageSize( $this, $this->getLocalRefPath() );
}
+ /**
+ * @return bool
+ */
function getMetadata() {
if ( !isset( $this->metadata ) ) {
if ( !$this->getHandler() ) {
@@ -125,6 +162,9 @@ class UnregisteredLocalFile extends File {
return $this->metadata;
}
+ /**
+ * @return bool|string
+ */
function getURL() {
if ( $this->repo ) {
return $this->repo->getZoneUrl( 'public' ) . '/' .
@@ -134,6 +174,9 @@ class UnregisteredLocalFile extends File {
}
}
+ /**
+ * @return bool|int
+ */
function getSize() {
$this->assertRepoDefined();
$props = $this->repo->getFileProps( $this->path );
diff --git a/includes/installer/CliInstaller.php b/includes/installer/CliInstaller.php
index f9afbb20..38b4a824 100644
--- a/includes/installer/CliInstaller.php
+++ b/includes/installer/CliInstaller.php
@@ -2,6 +2,21 @@
/**
* Core installer command line interface.
*
+ * 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
*/
@@ -117,7 +132,7 @@ class CliInstaller extends Installer {
* @param $path String Full path to write LocalSettings.php to
*/
public function writeConfigurationFile( $path ) {
- $ls = new LocalSettingsGenerator( $this );
+ $ls = InstallerOverrides::getLocalSettingsGenerator( $this );
$ls->writeFile( "$path/LocalSettings.php" );
}
@@ -148,7 +163,7 @@ class CliInstaller extends Installer {
protected function getMessageText( $params ) {
$msg = array_shift( $params );
- $text = wfMsgExt( $msg, array( 'parseinline' ), $params );
+ $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 );
@@ -172,7 +187,7 @@ class CliInstaller extends Installer {
if ( !$status->isOk() ) {
echo "\n";
- exit;
+ exit( 1 );
}
}
diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php
index ab77e2d3..de59b2d6 100644
--- a/includes/installer/DatabaseInstaller.php
+++ b/includes/installer/DatabaseInstaller.php
@@ -2,6 +2,21 @@
/**
* DBMS-specific installation helper.
*
+ * 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
*/
@@ -50,7 +65,7 @@ abstract class DatabaseInstaller {
public abstract function getName();
/**
- * @return true if the client library is compiled in.
+ * @return bool Returns true if the client library is compiled in.
*/
public abstract function isCompiled();
@@ -88,6 +103,7 @@ abstract class DatabaseInstaller {
* $this->parent can be assumed to be a WebInstaller.
* If the DB type has no settings beyond those already configured with
* getConnectForm(), this should return false.
+ * @return bool
*/
public function getSettingsForm() {
return false;
@@ -140,7 +156,7 @@ abstract class DatabaseInstaller {
$this->db = $status->value;
// Enable autocommit
$this->db->clearFlag( DBO_TRX );
- $this->db->commit();
+ $this->db->commit( __METHOD__ );
}
return $status;
}
@@ -207,6 +223,7 @@ abstract class DatabaseInstaller {
/**
* Override this to provide DBMS-specific schema variables, to be
* substituted into tables.sql and other schema files.
+ * @return array
*/
public function getSchemaVars() {
return array();
@@ -256,7 +273,7 @@ abstract class DatabaseInstaller {
$up = DatabaseUpdater::newForDB( $this->db );
$up->doUpdates();
} catch ( MWException $e ) {
- echo "\nAn error occured:\n";
+ echo "\nAn error occurred:\n";
echo $e->getText();
$ret = false;
}
@@ -282,6 +299,7 @@ abstract class DatabaseInstaller {
/**
* Get an array of MW configuration globals that will be configured by this class.
+ * @return array
*/
public function getGlobalNames() {
return $this->globalNames;
@@ -313,14 +331,16 @@ abstract class DatabaseInstaller {
/**
* Get the internationalised name for this DBMS.
+ * @return String
*/
public function getReadableName() {
- return wfMsg( 'config-type-' . $this->getName() );
+ return wfMessage( 'config-type-' . $this->getName() )->text();
}
/**
* Get a name=>value map of MW configuration globals that overrides.
* DefaultSettings.php
+ * @return array
*/
public function getGlobalDefaults() {
return array();
@@ -328,6 +348,7 @@ abstract class DatabaseInstaller {
/**
* Get a name=>value map of internal variables used during installation.
+ * @return array
*/
public function getInternalDefaults() {
return $this->internalDefaults;
@@ -439,6 +460,7 @@ abstract class DatabaseInstaller {
* values: List of allowed values (required)
* itemAttribs Array of attribute arrays, outer key is the value name (optional)
*
+ * @return string
*/
public function getRadioSet( $params ) {
$params['controlName'] = $this->getName() . '_' . $params['var'];
@@ -451,6 +473,7 @@ abstract class DatabaseInstaller {
* Assumes that variables containing "password" in the name are (potentially
* fake) passwords.
* @param $varNames Array
+ * @return array
*/
public function setVarsFromRequest( $varNames ) {
return $this->parent->setVarsFromRequest( $varNames, $this->getName() . '_' );
@@ -486,7 +509,7 @@ abstract class DatabaseInstaller {
public function getInstallUserBox() {
return
Html::openElement( 'fieldset' ) .
- Html::element( 'legend', array(), wfMsg( 'config-db-install-account' ) ) .
+ 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' ) ) .
Html::closeElement( 'fieldset' );
@@ -494,6 +517,7 @@ abstract class DatabaseInstaller {
/**
* Submit a standard install user fieldset.
+ * @return Status
*/
public function submitInstallUserBox() {
$this->setVarsFromRequest( array( '_InstallUser', '_InstallPassword' ) );
@@ -510,7 +534,7 @@ abstract class DatabaseInstaller {
public function getWebUserBox( $noCreateMsg = false ) {
$wrapperStyle = $this->getVar( '_SameAccount' ) ? 'display: none' : '';
$s = Html::openElement( 'fieldset' ) .
- Html::element( 'legend', array(), wfMsg( 'config-db-web-account' ) ) .
+ Html::element( 'legend', array(), wfMessage( 'config-db-web-account' )->text() ) .
$this->getCheckBox(
'_SameAccount', 'config-db-web-account-same',
array( 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' )
@@ -520,7 +544,7 @@ abstract class DatabaseInstaller {
$this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
$this->parent->getHelpBox( 'config-db-web-help' );
if ( $noCreateMsg ) {
- $s .= $this->parent->getWarningBox( wfMsgNoTrans( $noCreateMsg ) );
+ $s .= $this->parent->getWarningBox( wfMessage( $noCreateMsg )->plain() );
} else {
$s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
}
diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php
index f2e36aec..ff0a99e9 100644
--- a/includes/installer/DatabaseUpdater.php
+++ b/includes/installer/DatabaseUpdater.php
@@ -2,11 +2,26 @@
/**
* DBMS-specific updater helper.
*
+ * 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
*/
-require_once( dirname(__FILE__) . '/../../maintenance/Maintenance.php' );
+require_once( __DIR__ . '/../../maintenance/Maintenance.php' );
/**
* Class for handling database updates. Roughly based off of updaters.inc, with
@@ -203,10 +218,44 @@ abstract class DatabaseUpdater {
}
/**
+ *
+ * @since 1.20
+ *
+ * @param $tableName string
+ * @param $columnName string
+ * @param $sqlPath string
+ */
+ public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
+ $this->extensionUpdates[] = array( 'dropField', $tableName, $columnName, $sqlPath, true );
+ }
+
+ /**
+ *
+ * @since 1.20
+ *
+ * @param $tableName string
+ * @param $sqlPath string
+ */
+ public function dropExtensionTable( $tableName, $sqlPath ) {
+ $this->extensionUpdates[] = array( 'dropTable', $tableName, $sqlPath, true );
+ }
+
+ /**
+ *
+ * @since 1.20
+ *
+ * @param $tableName string
+ * @return bool
+ */
+ public function tableExists( $tableName ) {
+ return ( $this->db->tableExists( $tableName, __METHOD__ ) );
+ }
+
+ /**
* Add a maintenance script to be run after the database updates are complete.
- *
+ *
* @since 1.19
- *
+ *
* @param $class string Name of a Maintenance subclass
*/
public function addPostDatabaseUpdateMaintenance( $class ) {
@@ -224,7 +273,7 @@ abstract class DatabaseUpdater {
/**
* @since 1.17
- *
+ *
* @return array
*/
public function getPostDatabaseUpdateMaintenance() {
@@ -239,6 +288,7 @@ abstract class DatabaseUpdater {
public function doUpdates( $what = array( 'core', 'extensions', 'purge', 'stats' ) ) {
global $wgLocalisationCacheConf, $wgVersion;
+ $this->db->begin( __METHOD__ );
$what = array_flip( $what );
if ( isset( $what['core'] ) ) {
$this->runUpdates( $this->getCoreUpdateList(), false );
@@ -261,6 +311,7 @@ abstract class DatabaseUpdater {
$this->rebuildLocalisationCache();
}
}
+ $this->db->commit( __METHOD__ );
}
/**
@@ -412,13 +463,20 @@ abstract class DatabaseUpdater {
* Applies a SQL patch
* @param $path String Path to the patch file
* @param $isFullPath Boolean Whether to treat $path as a relative or not
+ * @param $msg String Description of the patch
*/
- protected function applyPatch( $path, $isFullPath = false ) {
- if ( $isFullPath ) {
- $this->db->sourceFile( $path );
- } else {
- $this->db->sourceFile( $this->db->patchPath( $path ) );
+ protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
+ if ( $msg === null ) {
+ $msg = "Applying $path patch";
}
+
+ if ( !$isFullPath ) {
+ $path = $this->db->patchPath( $path );
+ }
+
+ $this->output( "$msg ..." );
+ $this->db->sourceFile( $path );
+ $this->output( "done.\n" );
}
/**
@@ -431,9 +489,7 @@ abstract class DatabaseUpdater {
if ( $this->db->tableExists( $name, __METHOD__ ) ) {
$this->output( "...$name table already exists.\n" );
} else {
- $this->output( "Creating $name table..." );
- $this->applyPatch( $patch, $fullpath );
- $this->output( "done.\n" );
+ $this->applyPatch( $patch, $fullpath, "Creating $name table" );
}
}
@@ -450,9 +506,7 @@ abstract class DatabaseUpdater {
} elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
$this->output( "...have $field field in $table table.\n" );
} else {
- $this->output( "Adding $field field to table $table..." );
- $this->applyPatch( $patch, $fullpath );
- $this->output( "done.\n" );
+ $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
}
}
@@ -467,9 +521,7 @@ abstract class DatabaseUpdater {
if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
$this->output( "...index $index already set on $table table.\n" );
} else {
- $this->output( "Adding index $index to table $table... " );
- $this->applyPatch( $patch, $fullpath );
- $this->output( "done.\n" );
+ $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
}
}
@@ -483,9 +535,7 @@ abstract class DatabaseUpdater {
*/
protected function dropField( $table, $field, $patch, $fullpath = false ) {
if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
- $this->output( "Table $table contains $field field. Dropping... " );
- $this->applyPatch( $patch, $fullpath );
- $this->output( "done.\n" );
+ $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
} else {
$this->output( "...$table table does not contain $field field.\n" );
}
@@ -501,24 +551,35 @@ abstract class DatabaseUpdater {
*/
protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
- $this->output( "Dropping $index index from table $table... " );
- $this->applyPatch( $patch, $fullpath );
- $this->output( "done.\n" );
+ $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
} else {
$this->output( "...$index key doesn't exist.\n" );
}
}
/**
+ * If the specified table exists, drop it, or execute the
+ * patch if one is provided.
+ *
+ * Public @since 1.20
+ *
* @param $table string
- * @param $patch string
+ * @param $patch string|false
* @param $fullpath bool
*/
- protected function dropTable( $table, $patch, $fullpath = false ) {
+ public function dropTable( $table, $patch = false, $fullpath = false ) {
if ( $this->db->tableExists( $table, __METHOD__ ) ) {
- $this->output( "Dropping table $table... " );
- $this->applyPatch( $patch, $fullpath );
- $this->output( "done.\n" );
+ $msg = "Dropping table $table";
+
+ if ( $patch === false ) {
+ $this->output( "$msg ..." );
+ $this->db->dropTable( $table, __METHOD__ );
+ $this->output( "done.\n" );
+ }
+ else {
+ $this->applyPatch( $patch, $fullpath, $msg );
+ }
+
} else {
$this->output( "...$table doesn't exist.\n" );
}
@@ -541,10 +602,8 @@ abstract class DatabaseUpdater {
} elseif( $this->updateRowExists( $updateKey ) ) {
$this->output( "...$field in table $table already modified by patch $patch.\n" );
} else {
- $this->output( "Modifying $field field of table $table..." );
- $this->applyPatch( $patch, $fullpath );
+ $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
$this->insertUpdateRow( $updateKey );
- $this->output( "done.\n" );
}
}
@@ -637,9 +696,7 @@ abstract class DatabaseUpdater {
return;
}
- $this->output( "Converting tc_time from UNIX epoch to MediaWiki timestamp... " );
- $this->applyPatch( 'patch-tc-timestamp.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-tc-timestamp.sql', false, "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
}
/**
diff --git a/includes/installer/Ibm_db2Installer.php b/includes/installer/Ibm_db2Installer.php
index a6c4fd65..ca9bdf4b 100644
--- a/includes/installer/Ibm_db2Installer.php
+++ b/includes/installer/Ibm_db2Installer.php
@@ -2,6 +2,21 @@
/**
* 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
*/
@@ -53,7 +68,7 @@ class Ibm_db2Installer extends DatabaseInstaller {
$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(), wfMsg( 'config-db-wiki-settings' ) ) .
+ 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' ) .
diff --git a/includes/installer/Ibm_db2Updater.php b/includes/installer/Ibm_db2Updater.php
index 03540bb0..9daba9c2 100644
--- a/includes/installer/Ibm_db2Updater.php
+++ b/includes/installer/Ibm_db2Updater.php
@@ -2,6 +2,21 @@
/**
* 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
*/
@@ -55,20 +70,22 @@ class Ibm_db2Updater extends DatabaseUpdater {
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' ),
-
+ 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' )
+ array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ),
+
+ // 1.20
);
}
}
diff --git a/includes/installer/InstallDocFormatter.php b/includes/installer/InstallDocFormatter.php
index 5801f26d..9a389dd8 100644
--- a/includes/installer/InstallDocFormatter.php
+++ b/includes/installer/InstallDocFormatter.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Installer-specific wikitext formating.
+ *
+ * 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 InstallDocFormatter {
static function format( $text ) {
diff --git a/includes/installer/Installer.i18n.php b/includes/installer/Installer.i18n.php
index fb68a2b5..4f1c4d0c 100644
--- a/includes/installer/Installer.i18n.php
+++ b/includes/installer/Installer.i18n.php
@@ -550,6 +550,7 @@ $3
When that has been done, you can '''[$2 enter your wiki]'''.",
'config-download-localsettings' => 'Download LocalSettings.php',
'config-help' => 'help',
+ 'config-nofile' => 'File "$1" could not be found. Has it been deleted?',
'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.
@@ -4484,7 +4485,7 @@ Esta no es la contraseña para la cuenta de MediaWiki; esta es la contraseña pa
'config-db-wiki-help' => 'Introduce el nombre de usuario y la contraseña que serán usados para acceder a la base de datos durante la operación normal del wiki.
Si esta cuenta no existe y la cuenta de instalación tiene suficientes privilegios, se creará esta cuenta de usuario con los privilegios mínimos necesarios para la operación normal del wiki.',
'config-db-prefix' => 'Prefijo para las tablas de la base de datos:',
- 'config-db-prefix-help' => 'Si necesita compartir una base de datos entre múltiples wikis, o entre MediaWiki y otra aplicación web, puede optar por agregar un prefijo a todos los nombres de tabla para evitar conflictos.
+ 'config-db-prefix-help' => 'Si necesita compartir una base de datos entre múltiples wikis, o entre MediaWiki y otra aplicación web, puede optar por agregar un prefijo a todos los nombres de tabla para evitar conflictos.
No utilice espacios.
Normalmente se deja este campo vacío.',
@@ -14228,7 +14229,7 @@ Jeśli korzystasz ze współdzielonego hostingu, dostawca usługi hostingowej mo
Możesz utworzyć konto użytkownika bazy danych podczas instalacji MediaWiki. Wówczas należy podać nazwę i hasło użytkownika z rolą SYSDBA w celu użycia go przez instalator do utworzenia nowe konta użytkownika, z którego korzystać będzie MediaWiki.
-Możesz również skorzystać z konta użytkownika bazy danych utworzonego bezpośrednio w Oracle i wówczas wystarczy podać tylko nazwę i hasło tego użytkownika. Konto z rolą SYSDBA nie będzie potrzebne, jednak konto użytkownika powinno mieć uprawnienia do utworzenia obiektów w schemacie bazy danych. Możesz też podać dwa konta - konto dla instalatora, z pomocą którego zostaną obiekty w schemacie bazy danych i drugie konto, z którego będzie MediaWiki korzystać będzie do pracy.
+Możesz również skorzystać z konta użytkownika bazy danych utworzonego bezpośrednio w Oracle i wówczas wystarczy podać tylko nazwę i hasło tego użytkownika. Konto z rolą SYSDBA nie będzie potrzebne, jednak konto użytkownika powinno mieć uprawnienia do utworzenia obiektów w schemacie bazy danych. Możesz też podać dwa konta - konto dla instalatora, z pomocą którego zostaną obiekty w schemacie bazy danych i drugie konto, z którego będzie MediaWiki korzystać będzie do pracy.
W podkatalogu "maintenance/oracle" znajduje się skrypt do tworzenia konta użytkownika. Korzystanie z konta użytkownika z ograniczonymi uprawnieniami spowoduje wyłączenie funkcji związanych z aktualizacją oprogramowania MediaWiki.',
'config-db-install-account' => 'Konto użytkownika dla instalatora',
@@ -17765,7 +17766,7 @@ Ang mas masasalimuot na mga kaayusan ng mga karapatan ng tagagamit ay makukuha p
'config-license-gfdl' => 'Lisensiyang 1.3 ng Malayang Dokumentasyon ng GNU o mas lalong huli',
'config-license-pd' => 'Nasasakupan ng Madla',
'config-license-cc-choose' => 'Pumili ng isang pasadyang Lisensiya ng Malikhaing mga Pangkaraniwan',
- 'config-license-help' => "Maraming mga pangmadlang wiki ang naglalagay ng lahat ng mga ambag sa ilalim ng [http://freedomdefined.org/Definition lisensiyang malaya].
+ 'config-license-help' => "Maraming mga pangmadlang wiki ang naglalagay ng lahat ng mga ambag sa ilalim ng [http://freedomdefined.org/Definition lisensiyang malaya].
Nakakatulong ito sa paglikha ng isang diwa ng pagmamay-ari ng pamayanan at nakapanghihikayat ng ambag na pangmahabang panahon.
Sa pangkalahatan, hindi kailangan ang isang wiking pribado o pangsamahan.
diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php
index 990d4449..ac5dbd74 100644
--- a/includes/installer/Installer.php
+++ b/includes/installer/Installer.php
@@ -2,6 +2,21 @@
/**
* Base code for MediaWiki 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
*/
@@ -24,7 +39,7 @@
abstract class Installer {
// This is the absolute minimum PHP version we can support
- const MINIMUM_PHP_VERSION = '5.2.3';
+ const MINIMUM_PHP_VERSION = '5.3.2';
/**
* @var array
@@ -293,7 +308,7 @@ abstract class Installer {
/**
* UI interface for displaying a short message
- * The parameters are like parameters to wfMsg().
+ * The parameters are like parameters to wfMessage().
* The messages will be in wikitext format, which will be converted to an
* output format such as HTML or text before being sent to the user.
* @param $msg
@@ -324,7 +339,7 @@ abstract class Installer {
// Load the installer's i18n file.
$wgExtensionMessagesFiles['MediawikiInstaller'] =
- dirname( __FILE__ ) . '/Installer.i18n.php';
+ __DIR__ . '/Installer.i18n.php';
// Having a user with id = 0 safeguards us from DB access via User::loadOptions().
$wgUser = User::newFromId( 0 );
@@ -543,7 +558,7 @@ abstract class Installer {
* write your messages. This appears to work well enough. Basic formatting and
* external links work just fine.
*
- * But in case a translator decides to throw in a #ifexist or internal link or
+ * But in case a translator decides to throw in a "#ifexist" or internal link or
* whatever, this function is guarded to catch the attempted DB access and to present
* some fallback text.
*
@@ -630,7 +645,7 @@ abstract class Installer {
$allNames = array();
foreach ( self::getDBTypes() as $name ) {
- $allNames[] = wfMsg( "config-type-$name" );
+ $allNames[] = wfMessage( "config-type-$name" )->text();
}
// cache initially available databases to make sure that everything will be displayed correctly
@@ -659,6 +674,7 @@ abstract class Installer {
return false;
}
$this->setVar( '_CompiledDBs', $databases );
+ return true;
}
/**
@@ -672,6 +688,7 @@ abstract class Installer {
/**
* Some versions of libxml+PHP break < and > encoding horribly
+ * @return bool
*/
protected function envCheckBrokenXML() {
$test = new PhpXmlBugTester();
@@ -679,11 +696,13 @@ abstract class Installer {
$this->showError( 'config-brokenlibxml' );
return false;
}
+ return true;
}
/**
* Test PHP (probably 5.3.1, but it could regress again) to make sure that
* reference parameters to __call() are not converted to null
+ * @return bool
*/
protected function envCheckPHP531() {
$test = new PhpRefCallBugTester;
@@ -692,66 +711,79 @@ abstract class Installer {
$this->showError( 'config-using531', phpversion() );
return false;
}
+ return true;
}
/**
* Environment check for magic_quotes_runtime.
+ * @return bool
*/
protected function envCheckMagicQuotes() {
if( wfIniGetBool( "magic_quotes_runtime" ) ) {
$this->showError( 'config-magic-quotes-runtime' );
return false;
}
+ return true;
}
/**
* Environment check for magic_quotes_sybase.
+ * @return bool
*/
protected function envCheckMagicSybase() {
if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
$this->showError( 'config-magic-quotes-sybase' );
return false;
}
+ return true;
}
/**
* Environment check for mbstring.func_overload.
+ * @return bool
*/
protected function envCheckMbstring() {
if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
$this->showError( 'config-mbstring' );
return false;
}
+ return true;
}
/**
* Environment check for zend.ze1_compatibility_mode.
+ * @return bool
*/
protected function envCheckZE1() {
if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
$this->showError( 'config-ze1' );
return false;
}
+ return true;
}
/**
* Environment check for safe_mode.
+ * @return bool
*/
protected function envCheckSafeMode() {
if ( wfIniGetBool( 'safe_mode' ) ) {
$this->setVar( '_SafeMode', true );
$this->showMessage( 'config-safe-mode' );
}
+ return true;
}
/**
* Environment check for the XML module.
+ * @return bool
*/
protected function envCheckXML() {
if ( !function_exists( "utf8_encode" ) ) {
$this->showError( 'config-xml-bad' );
return false;
}
+ return true;
}
/**
@@ -779,10 +811,12 @@ abstract class Installer {
$this->showError( 'config-pcre-no-utf8' );
return false;
}
+ return true;
}
/**
* Environment check for available memory.
+ * @return bool
*/
protected function envCheckMemory() {
$limit = ini_get( 'memory_limit' );
@@ -802,9 +836,8 @@ abstract class Installer {
$this->showMessage( 'config-memory-raised', $limit, $newLimit );
$this->setVar( '_RaiseMemory', true );
}
- } else {
- return true;
}
+ return true;
}
/**
@@ -830,15 +863,18 @@ abstract class Installer {
/**
* Scare user to death if they have mod_security
+ * @return bool
*/
protected function envCheckModSecurity() {
if ( self::apacheModulePresent( 'mod_security' ) ) {
$this->showMessage( 'config-mod-security' );
}
+ return true;
}
/**
* Search for GNU diff3.
+ * @return bool
*/
protected function envCheckDiff3() {
$names = array( "gdiff3", "diff3", "diff3.exe" );
@@ -852,10 +888,12 @@ abstract class Installer {
$this->setVar( 'wgDiff3', false );
$this->showMessage( 'config-diff3-bad' );
}
+ return true;
}
/**
* Environment check for ImageMagick and GD.
+ * @return bool
*/
protected function envCheckGraphics() {
$names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
@@ -868,10 +906,11 @@ abstract class Installer {
return true;
} elseif ( function_exists( 'imagejpeg' ) ) {
$this->showMessage( 'config-gd' );
- return true;
+
} else {
$this->showMessage( 'config-no-scaling' );
}
+ return true;
}
/**
@@ -881,6 +920,7 @@ abstract class Installer {
$server = $this->envGetDefaultServer();
$this->showMessage( 'config-using-server', $server );
$this->setVar( 'wgServer', $server );
+ return true;
}
/**
@@ -895,7 +935,7 @@ abstract class Installer {
*/
protected function envCheckPath() {
global $IP;
- $IP = dirname( dirname( dirname( __FILE__ ) ) );
+ $IP = dirname( dirname( __DIR__ ) );
$this->setVar( 'IP', $IP );
$this->showMessage( 'config-using-uri', $this->getVar( 'wgServer' ), $this->getVar( 'wgScriptPath' ) );
@@ -913,6 +953,7 @@ abstract class Installer {
$ext = 'php';
}
$this->setVar( 'wgScriptExtension', ".$ext" );
+ return true;
}
/**
@@ -991,6 +1032,7 @@ abstract class Installer {
/**
* TODO: document
+ * @return bool
*/
protected function envCheckUploadsDirectory() {
global $IP;
@@ -999,17 +1041,17 @@ abstract class Installer {
$url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
$safe = !$this->dirIsExecutable( $dir, $url );
- if ( $safe ) {
- return true;
- } else {
+ 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
+ * @return bool
*/
protected function envCheckSuhosinMaxValueLength() {
$maxValueLength = ini_get( 'suhosin.get.max_value_length' );
@@ -1022,6 +1064,7 @@ abstract class Installer {
$maxValueLength = -1;
}
$this->setVar( 'wgResourceLoaderMaxQueryLength', $maxValueLength );
+ return true;
}
/**
@@ -1075,12 +1118,16 @@ abstract class Installer {
if( $utf8 ) {
$useNormalizer = 'utf8';
$utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC );
- if ( $utf8 !== $normal_c ) $needsUpdate = true;
+ if ( $utf8 !== $normal_c ) {
+ $needsUpdate = true;
+ }
}
if( $intl ) {
$useNormalizer = 'intl';
$intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
- if ( $intl !== $normal_c ) $needsUpdate = true;
+ if ( $intl !== $normal_c ) {
+ $needsUpdate = true;
+ }
}
// Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
@@ -1094,11 +1141,15 @@ abstract class Installer {
}
}
+ /**
+ * @return bool
+ */
protected function envCheckCtype() {
if ( !function_exists( 'ctype_digit' ) ) {
$this->showError( 'config-ctype' );
return false;
}
+ return true;
}
/**
@@ -1131,6 +1182,7 @@ abstract class Installer {
*
* If $versionInfo is not false, only executables with a version
* matching $versionInfo[1] will be returned.
+ * @return bool|string
*/
public static function locateExecutable( $path, $names, $versionInfo = false ) {
if ( !is_array( $names ) ) {
@@ -1179,6 +1231,9 @@ abstract class Installer {
* Checks if scripts located in the given directory can be executed via the given URL.
*
* Used only by environment checks.
+ * @param $dir string
+ * @param $url string
+ * @return bool|int|string
*/
public function dirIsExecutable( $dir, $url ) {
$scriptTypes = array(
@@ -1539,12 +1594,13 @@ abstract class Installer {
$status = Status::newGood();
try {
$page = WikiPage::factory( Title::newMainPage() );
- $page->doEdit( wfMsgForContent( 'mainpagetext' ) . "\n\n" .
- wfMsgForContent( 'mainpagedocfooter' ),
- '',
- EDIT_NEW,
- false,
- User::newFromName( 'MediaWiki default' ) );
+ $page->doEdit( wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
+ wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text(),
+ '',
+ 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() );
@@ -1561,6 +1617,8 @@ abstract class Installer {
// Don't access the database
$GLOBALS['wgUseDatabaseMessages'] = false;
+ // Don't cache langconv tables
+ $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
// Debug-friendly
$GLOBALS['wgShowExceptionDetails'] = true;
// Don't break forms
diff --git a/includes/installer/LocalSettingsGenerator.php b/includes/installer/LocalSettingsGenerator.php
index 89154e58..bbc6b64e 100644
--- a/includes/installer/LocalSettingsGenerator.php
+++ b/includes/installer/LocalSettingsGenerator.php
@@ -2,6 +2,21 @@
/**
* Generator for LocalSettings.php file.
*
+ * 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
*/
@@ -14,16 +29,16 @@
*/
class LocalSettingsGenerator {
- private $extensions = array();
- private $values = array();
- private $groupPermissions = array();
- private $dbSettings = '';
- private $safeMode = false;
+ protected $extensions = array();
+ protected $values = array();
+ protected $groupPermissions = array();
+ protected $dbSettings = '';
+ protected $safeMode = false;
/**
* @var Installer
*/
- private $installer;
+ protected $installer;
/**
* Constructor.
@@ -151,7 +166,7 @@ class LocalSettingsGenerator {
/**
* @return String
*/
- private function buildMemcachedServerList() {
+ protected function buildMemcachedServerList() {
$servers = $this->values['_MemCachedServers'];
if( !$servers ) {
@@ -172,7 +187,7 @@ class LocalSettingsGenerator {
/**
* @return String
*/
- private function getDefaultText() {
+ protected function getDefaultText() {
if( !$this->values['wgImageMagickConvertCommand'] ) {
$this->values['wgImageMagickConvertCommand'] = '/usr/bin/convert';
$magic = '#';
@@ -244,7 +259,8 @@ if ( !defined( 'MEDIAWIKI' ) ) {
{$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 please see:
+## 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']}\";
diff --git a/includes/installer/MysqlInstaller.php b/includes/installer/MysqlInstaller.php
index 7585fe7a..f66f15f2 100644
--- a/includes/installer/MysqlInstaller.php
+++ b/includes/installer/MysqlInstaller.php
@@ -2,6 +2,21 @@
/**
* MySQL-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
*/
@@ -74,7 +89,7 @@ class MysqlInstaller extends DatabaseInstaller {
return
$this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
Html::openElement( 'fieldset' ) .
- Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) .
+ 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' ) ) .
Html::closeElement( 'fieldset' ) .
@@ -336,7 +351,7 @@ class MysqlInstaller extends DatabaseInstaller {
$s .= Xml::openElement( 'div', array(
'id' => 'dbMyisamWarning'
));
- $s .= $this->parent->getWarningBox( wfMsg( 'config-mysql-myisam-dep' ) );
+ $s .= $this->parent->getWarningBox( wfMessage( 'config-mysql-myisam-dep' )->text() );
$s .= Xml::closeElement( 'div' );
if( $this->getVar( '_MysqlEngine' ) != 'MyISAM' ) {
@@ -514,21 +529,21 @@ class MysqlInstaller extends DatabaseInstaller {
$fullName = $this->buildFullUserName( $dbUser, $host );
if( !$this->userDefinitelyExists( $dbUser, $host ) ) {
try{
- $this->db->begin();
+ $this->db->begin( __METHOD__ );
$this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
- $this->db->commit();
+ $this->db->commit( __METHOD__ );
$grantableNames[] = $fullName;
} catch( DBQueryError $dqe ) {
if( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
// User (probably) already exists
- $this->db->rollback();
+ $this->db->rollback( __METHOD__ );
$status->warning( 'config-install-user-alreadyexists', $dbUser );
$grantableNames[] = $fullName;
break;
} else {
// If we couldn't create for some bizzare reason and the
// user probably doesn't exist, skip the grant
- $this->db->rollback();
+ $this->db->rollback( __METHOD__ );
$status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
}
}
@@ -544,11 +559,11 @@ class MysqlInstaller extends DatabaseInstaller {
$dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
foreach( $grantableNames as $name ) {
try {
- $this->db->begin();
+ $this->db->begin( __METHOD__ );
$this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
- $this->db->commit();
+ $this->db->commit( __METHOD__ );
} catch( DBQueryError $dqe ) {
- $this->db->rollback();
+ $this->db->rollback( __METHOD__ );
$status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() );
}
}
diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php
index 9e7869ec..49dff805 100644
--- a/includes/installer/MysqlUpdater.php
+++ b/includes/installer/MysqlUpdater.php
@@ -2,6 +2,21 @@
/**
* MySQL-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
*/
@@ -192,6 +207,12 @@ class MysqlUpdater extends DatabaseUpdater {
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' ),
+
+ // 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' ),
);
}
@@ -211,9 +232,7 @@ class MysqlUpdater extends DatabaseUpdater {
if ( in_array( 'binary', $flags ) ) {
$this->output( "...$table table has correct $field encoding.\n" );
} else {
- $this->output( "Fixing $field encoding on $table table... " );
- $this->applyPatch( $patchFile );
- $this->output( "done.\n" );
+ $this->applyPatch( $patchFile, false, "Fixing $field encoding on $table table" );
}
}
@@ -250,12 +269,8 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( 'Creating interwiki table...' );
- $this->applyPatch( 'patch-interwiki.sql' );
- $this->output( "done.\n" );
- $this->output( 'Adding default interwiki definitions...' );
- $this->applyPatch( "$IP/maintenance/interwiki.sql", true );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-interwiki.sql', false, 'Creating interwiki table' );
+ $this->applyPatch( "$IP/maintenance/interwiki.sql", true, 'Adding default interwiki definitions' );
}
/**
@@ -271,9 +286,7 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Updating indexes to 20031107..." );
- $this->applyPatch( 'patch-indexes.sql', true );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-indexes.sql', true, "Updating indexes to 20031107" );
}
protected function doOldLinksUpdate() {
@@ -288,10 +301,9 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Fixing ancient broken imagelinks table.\n" );
- $this->output( "NOTE: you will have to run maintenance/refreshLinks.php after this.\n" );
- $this->applyPatch( 'patch-fix-il_from.sql' );
- $this->output( "done.\n" );
+ 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." );
+ }
}
/**
@@ -513,9 +525,7 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Converting links and brokenlinks tables to pagelinks... " );
- $this->applyPatch( 'patch-pagelinks.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-pagelinks.sql', false, "Converting links and brokenlinks tables to pagelinks" );
global $wgContLang;
foreach ( MWNamespace::getCanonicalNamespaces() as $ns => $name ) {
@@ -551,9 +561,7 @@ class MysqlUpdater extends DatabaseUpdater {
if ( !$duper->clearDupes() ) {
$this->output( "WARNING: This next step will probably fail due to unfixed duplicates...\n" );
}
- $this->output( "Adding unique index on user_name... " );
- $this->applyPatch( 'patch-user_nameindex.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-user_nameindex.sql', false, "Adding unique index on user_name" );
}
protected function doUserGroupsUpdate() {
@@ -566,9 +574,7 @@ class MysqlUpdater extends DatabaseUpdater {
$this->db->query( "ALTER TABLE $oldug RENAME TO $newug", __METHOD__ );
$this->output( "done.\n" );
- $this->output( "Re-adding fresh user_groups table... " );
- $this->applyPatch( 'patch-user_groups.sql' );
- $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" );
@@ -580,15 +586,11 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Adding user_groups table... " );
- $this->applyPatch( 'patch-user_groups.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-user_groups.sql', false, "Adding user_groups table" );
if ( !$this->db->tableExists( 'user_rights', __METHOD__ ) ) {
if ( $this->db->fieldExists( 'user', 'user_rights', __METHOD__ ) ) {
- $this->output( "Upgrading from a 1.3 or older database? Breaking out user_rights for conversion..." );
- $this->db->applyPatch( 'patch-user_rights.sql' );
- $this->output( "done.\n" );
+ $this->db->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" );
@@ -630,9 +632,7 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Making wl_notificationtimestamp nullable... " );
- $this->applyPatch( 'patch-watchlist-null.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-watchlist-null.sql', false, "Making wl_notificationtimestamp nullable" );
}
/**
@@ -658,8 +658,8 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Creating templatelinks table...\n" );
- $this->applyPatch( 'patch-templatelinks.sql' );
+ $this->applyPatch( 'patch-templatelinks.sql', false, "Creating templatelinks table" );
+
$this->output( "Populating...\n" );
if ( wfGetLB()->getServerCount() > 1 ) {
// Slow, replication-friendly update
@@ -700,8 +700,7 @@ class MysqlUpdater extends DatabaseUpdater {
!$this->indexHasField( 'templatelinks', 'tl_namespace', 'tl_from' ) ||
!$this->indexHasField( 'imagelinks', 'il_to', 'il_from' ) )
{
- $this->applyPatch( 'patch-backlinkindexes.sql' );
- $this->output( "...backlinking indices updated\n" );
+ $this->applyPatch( 'patch-backlinkindexes.sql', false, "Updating backlinking indices" );
}
}
@@ -716,9 +715,8 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Creating page_restrictions table..." );
- $this->applyPatch( 'patch-page_restrictions.sql' );
- $this->applyPatch( 'patch-page_restrictions_sortkey.sql' );
+ $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" );
@@ -728,8 +726,7 @@ class MysqlUpdater extends DatabaseUpdater {
protected function doCategorylinksIndicesUpdate() {
if ( !$this->indexHasField( 'categorylinks', 'cl_sortkey', 'cl_from' ) ) {
- $this->applyPatch( 'patch-categorylinksindex.sql' );
- $this->output( "...categorylinks indices updated\n" );
+ $this->applyPatch( 'patch-categorylinksindex.sql', false, "Updating categorylinks Indices" );
}
}
@@ -768,18 +765,14 @@ class MysqlUpdater extends DatabaseUpdater {
} elseif ( $this->db->fieldExists( 'profiling', 'pf_memory', __METHOD__ ) ) {
$this->output( "...profiling table has pf_memory field.\n" );
} else {
- $this->output( "Adding pf_memory field to table profiling..." );
- $this->applyPatch( 'patch-profiling-memory.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-profiling-memory.sql', false, "Adding pf_memory field to table profiling" );
}
}
protected function doFilearchiveIndicesUpdate() {
$info = $this->db->indexInfo( 'filearchive', 'fa_user_timestamp', __METHOD__ );
if ( !$info ) {
- $this->output( "Updating filearchive indices..." );
- $this->applyPatch( 'patch-filearchive-user-index.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-filearchive-user-index.sql', false, "Updating filearchive indices" );
}
}
@@ -790,9 +783,7 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Making pl_namespace, tl_namespace and il_to indices UNIQUE... " );
- $this->applyPatch( 'patch-pl-tl-il-unique.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-pl-tl-il-unique.sql', false, "Making pl_namespace, tl_namespace and il_to indices UNIQUE" );
}
protected function renameEuWikiId() {
@@ -801,9 +792,7 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Renaming eu_wiki_id -> eu_local_id... " );
- $this->applyPatch( 'patch-eu_local_id.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-eu_local_id.sql', false, "Renaming eu_wiki_id -> eu_local_id" );
}
protected function doUpdateMimeMinorField() {
@@ -812,9 +801,7 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Altering all *_mime_minor fields to 100 bytes in size ... " );
- $this->applyPatch( 'patch-mime_minor_length.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-mime_minor_length.sql', false, "Altering all *_mime_minor fields to 100 bytes in size" );
}
protected function doClFieldsUpdate() {
@@ -823,9 +810,7 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( 'Updating categorylinks (again)...' );
- $this->applyPatch( 'patch-categorylinks-better-collation2.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-categorylinks-better-collation2.sql', false, 'Updating categorylinks (again)' );
}
protected function doLangLinksLengthUpdate() {
@@ -834,9 +819,7 @@ class MysqlUpdater extends DatabaseUpdater {
$row = $this->db->fetchObject( $res );
if ( $row && $row->Type == "varbinary(10)" ) {
- $this->output( 'Updating length of ll_lang in langlinks...' );
- $this->applyPatch( 'patch-langlinks-ll_lang-20.sql' );
- $this->output( "done.\n" );
+ $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" );
}
@@ -849,8 +832,6 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
- $this->output( "Making user_last_timestamp nullable... " );
- $this->applyPatch( 'patch-user-newtalk-timestamp-null.sql' );
- $this->output( "done.\n" );
+ $this->applyPatch( 'patch-user-newtalk-timestamp-null.sql', false, "Making user_last_timestamp nullable" );
}
}
diff --git a/includes/installer/OracleInstaller.php b/includes/installer/OracleInstaller.php
index 51e6d4a2..72ec800d 100644
--- a/includes/installer/OracleInstaller.php
+++ b/includes/installer/OracleInstaller.php
@@ -2,6 +2,21 @@
/**
* Oracle-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
*/
@@ -47,12 +62,12 @@ class OracleInstaller extends DatabaseInstaller {
return
$this->getTextBox( 'wgDBserver', 'config-db-host-oracle', array(), $this->parent->getHelpBox( 'config-db-host-oracle-help' ) ) .
Html::openElement( 'fieldset' ) .
- Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) .
+ 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' ) ) .
Html::closeElement( 'fieldset' ) .
- $this->parent->getWarningBox( wfMsg( 'config-db-account-oracle-warn' ) ).
+ $this->parent->getWarningBox( wfMessage( 'config-db-account-oracle-warn' )->text() ).
$this->getInstallUserBox().
$this->getWebUserBox();
}
@@ -243,6 +258,7 @@ class OracleInstaller extends DatabaseInstaller {
/**
* Overload: after this action field info table has to be rebuilt
+ * @return Status
*/
public function createTables() {
$this->setupSchemaVars();
diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php
index 93c2726b..e71c26fe 100644
--- a/includes/installer/OracleUpdater.php
+++ b/includes/installer/OracleUpdater.php
@@ -2,6 +2,21 @@
/**
* Oracle-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
*/
@@ -51,6 +66,10 @@ class OracleUpdater extends DatabaseUpdater {
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' ),
+
// KEEP THIS AT THE BOTTOM!!
array( 'doRebuildDuplicateFunction' ),
@@ -63,40 +82,32 @@ class OracleUpdater extends DatabaseUpdater {
* Oracle inserts NULL, so namespace fields should have a default value
*/
protected function doNamespaceDefaults() {
- $this->output( "Altering namespace fields with default value ... " );
$meta = $this->db->fieldInfo( 'page', 'page_namespace' );
if ( $meta->defaultValue() != null ) {
- $this->output( "defaults seem to present on namespace fields\n" );
return;
}
- $this->applyPatch( 'patch_namespace_defaults.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_namespace_defaults.sql', false, "Altering namespace fields with default value" );
}
/**
* Uniform FK names + deferrable state
*/
protected function doFKRenameDeferr() {
- $this->output( "Altering foreign keys ... " );
$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 ) {
- $this->output( "at least one FK is deferrable, considering up to date\n" );
return;
}
- $this->applyPatch( 'patch_fk_rename_deferred.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_fk_rename_deferred.sql', false, "Altering foreign keys ... " );
}
/**
* Recreate functions to 17 schema layout
*/
protected function doFunctions17() {
- $this->output( "Recreating functions ... " );
- $this->applyPatch( 'patch_create_17_functions.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_create_17_functions.sql', false, "Recreating functions" );
}
/**
@@ -104,14 +115,11 @@ class OracleUpdater extends DatabaseUpdater {
* there are no incremental patches prior to this
*/
protected function doSchemaUpgrade17() {
- $this->output( "Updating schema to 17 ... " );
// check if iwlinks table exists which was added in 1.17
if ( $this->db->tableExists( 'iwlinks' ) ) {
- $this->output( "schema seem to be up to date.\n" );
return;
}
- $this->applyPatch( 'patch_16_17_schema_changes.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_16_17_schema_changes.sql', false, "Updating schema to 17" );
}
/**
@@ -140,24 +148,19 @@ class OracleUpdater extends DatabaseUpdater {
* converted to NULL in Oracle
*/
protected function doRemoveNotNullEmptyDefaults() {
- $this->output( "Removing not null empty constraints ... " );
$meta = $this->db->fieldInfo( 'categorylinks' , 'cl_sortkey_prefix' );
if ( $meta->isNullable() ) {
- $this->output( "constraints seem to be removed\n" );
return;
}
- $this->applyPatch( 'patch_remove_not_null_empty_defs.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_remove_not_null_empty_defs.sql', false, "Removing not null empty constraints" );
}
+
protected function doRemoveNotNullEmptyDefaults2() {
- $this->output( "Removing not null empty constraints ... " );
$meta = $this->db->fieldInfo( 'ipblocks' , 'ipb_by_text' );
if ( $meta->isNullable() ) {
- $this->output( "constraints seem to be removed\n" );
return;
}
- $this->applyPatch( 'patch_remove_not_null_empty_defs2.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_remove_not_null_empty_defs2.sql', false, "Removing not null empty constraints" );
}
/**
@@ -165,17 +168,13 @@ class OracleUpdater extends DatabaseUpdater {
* cascading taken in account in the deleting function
*/
protected function doRecentchangesFK2Cascade() {
- $this->output( "Altering RECENTCHANGES_FK2 ... " );
-
$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 ) {
- $this->output( "FK up to date\n" );
return;
}
- $this->applyPatch( 'patch_recentchanges_fk2_cascade.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_recentchanges_fk2_cascade.sql', false, "Altering RECENTCHANGES_FK2" );
}
/**
@@ -199,9 +198,7 @@ class OracleUpdater extends DatabaseUpdater {
* rebuilding of the function that duplicates tables for tests
*/
protected function doRebuildDuplicateFunction() {
- $this->output( "Rebuilding duplicate function ... " );
- $this->applyPatch( 'patch_rebuild_dupfunc.sql', false );
- $this->output( "ok\n" );
+ $this->applyPatch( 'patch_rebuild_dupfunc.sql', false, "Rebuilding duplicate function" );
}
/**
diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php
index fea012e2..3ac2b3a8 100644
--- a/includes/installer/PostgresInstaller.php
+++ b/includes/installer/PostgresInstaller.php
@@ -2,6 +2,21 @@
/**
* PostgreSQL-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
*/
@@ -45,7 +60,7 @@ class PostgresInstaller extends DatabaseInstaller {
$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(), wfMsg( 'config-db-wiki-settings' ) ) .
+ 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' ) .
@@ -110,9 +125,9 @@ class PostgresInstaller extends DatabaseInstaller {
/**
* Open a PG connection with given parameters
- * @param $user User name
- * @param $password Password
- * @param $dbName Database name
+ * @param $user string User name
+ * @param $password string Password
+ * @param $dbName string Database name
* @return Status
*/
protected function openConnectionWithParams( $user, $password, $dbName ) {
@@ -147,7 +162,7 @@ class PostgresInstaller extends DatabaseInstaller {
*/
$conn = $status->value;
$conn->clearFlag( DBO_TRX );
- $conn->commit();
+ $conn->commit( __METHOD__ );
$this->pgConns[$type] = $conn;
}
return $status;
@@ -168,14 +183,14 @@ class PostgresInstaller extends DatabaseInstaller {
* separate connection for this allows us to avoid accidental cross-module
* dependencies.
*
- * @param $type The type of connection to get:
+ * @param $type string 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.
*
- * @return A Status object. On success, a connection object will be in the
+ * @return Status object. On success, a connection object will be in the
* value member.
*/
protected function openPgConnection( $type ) {
@@ -344,6 +359,7 @@ class PostgresInstaller extends DatabaseInstaller {
/**
* Returns true if the install user is able to create objects owned
* by the web user, false otherwise.
+ * @return bool
*/
protected function canCreateObjectsForWebUser() {
if ( $this->isSuperUser() ) {
@@ -365,10 +381,11 @@ class PostgresInstaller extends DatabaseInstaller {
/**
* Recursive helper for canCreateObjectsForWebUser().
- * @param $conn Database object
- * @param $targetMember Role ID of the member to look for
- * @param $group Role ID of the group to look for
- * @param $maxDepth Maximum recursive search depth
+ * @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
+ * @return bool
*/
protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
if ( $targetMember === $group ) {
@@ -429,10 +446,6 @@ class PostgresInstaller extends DatabaseInstaller {
$conn = $status->value;
$dbName = $this->getVar( 'wgDBname' );
- //$schema = $this->getVar( 'wgDBmwschema' );
- //$user = $this->getVar( 'wgDBuser' );
- //$safeschema = $conn->addIdentifierQuotes( $schema );
- //$safeuser = $conn->addIdentifierQuotes( $user );
$exists = $conn->selectField( '"pg_catalog"."pg_database"', '1',
array( 'datname' => $dbName ), __METHOD__ );
@@ -464,19 +477,13 @@ class PostgresInstaller extends DatabaseInstaller {
}
}
- // If we created a user, alter it now to search the new schema by default
- if ( $this->getVar( '_CreateDBAccount' ) ) {
- $conn->query( "ALTER ROLE $safeuser SET search_path = $safeschema, public",
- __METHOD__ );
- }
-
// Select the new schema in the current connection
- $conn->query( "SET search_path = $safeschema" );
+ $conn->determineCoreSchema( $schema );
return Status::newGood();
}
function commitChanges() {
- $this->db->commit();
+ $this->db->commit( __METHOD__ );
return Status::newGood();
}
@@ -491,10 +498,8 @@ class PostgresInstaller extends DatabaseInstaller {
}
$conn = $status->value;
- //$schema = $this->getVar( 'wgDBmwschema' );
$safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
$safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
- //$safeschema = $conn->addIdentifierQuotes( $schema );
// Check if the user already exists
$userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) );
@@ -551,7 +556,7 @@ class PostgresInstaller extends DatabaseInstaller {
*/
$conn = $status->value;
- if( $conn->tableExists( 'user' ) ) {
+ if( $conn->tableExists( 'archive' ) ) {
$status->warning( 'config-install-tables-exist' );
$this->enableLB();
return $status;
diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php
index 023cb300..6cffe84a 100644
--- a/includes/installer/PostgresUpdater.php
+++ b/includes/installer/PostgresUpdater.php
@@ -2,6 +2,21 @@
/**
* PostgreSQL-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
*/
@@ -23,28 +38,35 @@ class PostgresUpdater extends DatabaseUpdater {
/**
* @todo FIXME: Postgres should use sequential updates like Mysql, Sqlite
* and everybody else. It never got refactored like it should've.
+ * @return array
*/
protected function getCoreUpdateList() {
return array(
# rename tables 1.7.3
# r15791 Change reserved word table names "user" and "text"
- array( 'renameTable', 'user', 'mwuser'),
- array( 'renameTable', 'text', 'pagecontent'),
-
- # new sequences
- array( 'addSequence', 'logging_log_id_seq' ),
- array( 'addSequence', 'page_restrictions_pr_id_seq' ),
+ array( 'renameTable', 'user', 'mwuser' ),
+ array( 'renameTable', 'text', 'pagecontent' ),
+ 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' ),
# 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', 'category_id_seq', 'category_cat_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'),
+
+ # 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' ),
+
# new tables
array( 'addTable', 'category', 'patch-category.sql' ),
array( 'addTable', 'page', 'patch-page.sql' ),
@@ -67,11 +89,13 @@ class PostgresUpdater extends DatabaseUpdater {
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' ),
# 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' ),
@@ -87,6 +111,7 @@ class PostgresUpdater extends DatabaseUpdater {
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')" ),
@@ -138,7 +163,7 @@ class PostgresUpdater extends DatabaseUpdater {
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 DEFAULT 0' ),
+ 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" ),
@@ -168,18 +193,23 @@ class PostgresUpdater extends DatabaseUpdater {
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', '' ),
# null changes
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', '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)' ),
@@ -192,12 +222,78 @@ class PostgresUpdater extends DatabaseUpdater {
array( 'addPgIndex', 'iwlinks', 'iwl_prefix_title_from', '(iwl_prefix, iwl_title, iwl_from)' ),
array( 'addPgIndex', 'job', 'job_timestamp_idx', '(job_timestamp)' ),
+ 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),
+ ),
+ '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),
+ ),
+ 'CREATE INDEX cl_sortkey ON "categorylinks" USING "btree" ("cl_to", "cl_sortkey", "cl_from")' ),
+ array( 'checkIndex', 'logging_times', array(
+ 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),
+ ),
+ '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),
+ ),
+ '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),
+ ),
+ '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),
+ ),
+ '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),
+ ),
+ '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),
+ ),
+ '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),
+ ),
+ '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),
+ ),
+ '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),
+ ),
+ 'CREATE INDEX "ts2_page_text" ON "pagecontent" USING "gist" ("textvector")' ),
+ array( 'checkIndex', 'ts2_page_title', array(
+ array('titlevector', 'tsvector_ops', 'gist', 0),
+ ),
+ 'CREATE INDEX "ts2_page_title" ON "page" USING "gist" ("titlevector")' ),
+
array( 'checkOiNameConstraint' ),
array( 'checkPageDeletedTrigger' ),
- array( 'checkRcCurIdNullable' ),
- array( 'checkPagelinkUniqueIndex' ),
array( 'checkRevUserFkey' ),
- array( 'checkIpbAdress' ),
+ 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),
+ ),
+ '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
@@ -210,6 +306,7 @@ class PostgresUpdater extends DatabaseUpdater {
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' ),
@@ -229,6 +326,8 @@ class PostgresUpdater extends DatabaseUpdater {
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' ),
# end
array( 'tsearchFixes' ),
);
@@ -274,7 +373,6 @@ class PostgresUpdater extends DatabaseUpdater {
}
protected function describeTable( $table ) {
- global $wgDBmwschema;
$q = <<<END
SELECT attname, attnum FROM pg_namespace, pg_class, pg_attribute
WHERE pg_class.relnamespace = pg_namespace.oid
@@ -283,7 +381,7 @@ SELECT attname, attnum FROM pg_namespace, pg_class, pg_attribute
END;
$res = $this->db->query( sprintf( $q,
$this->db->addQuotes( $table ),
- $this->db->addQuotes( $wgDBmwschema ) ) );
+ $this->db->addQuotes( $this->db->getCoreSchema() ) ) );
if ( !$res ) {
return null;
}
@@ -299,8 +397,6 @@ END;
}
function describeIndex( $idx ) {
- global $wgDBmwschema;
-
// first fetch the key (which is a list of columns ords) and
// the table the index applies to (an oid)
$q = <<<END
@@ -313,7 +409,7 @@ END;
$res = $this->db->query(
sprintf(
$q,
- $this->db->addQuotes( $wgDBmwschema ),
+ $this->db->addQuotes( $this->db->getCoreSchema() ),
$this->db->addQuotes( $idx )
)
);
@@ -350,7 +446,6 @@ END;
}
function fkeyDeltype( $fkey ) {
- global $wgDBmwschema;
$q = <<<END
SELECT confdeltype FROM pg_constraint, pg_namespace
WHERE connamespace=pg_namespace.oid
@@ -360,7 +455,7 @@ END;
$r = $this->db->query(
sprintf(
$q,
- $this->db->addQuotes( $wgDBmwschema ),
+ $this->db->addQuotes( $this->db->getCoreSchema() ),
$this->db->addQuotes( $fkey )
)
);
@@ -371,7 +466,6 @@ END;
}
function ruleDef( $table, $rule ) {
- global $wgDBmwschema;
$q = <<<END
SELECT definition FROM pg_rules
WHERE schemaname = %s
@@ -381,7 +475,7 @@ END;
$r = $this->db->query(
sprintf(
$q,
- $this->db->addQuotes( $wgDBmwschema ),
+ $this->db->addQuotes( $this->db->getCoreSchema() ),
$this->db->addQuotes( $table ),
$this->db->addQuotes( $rule )
)
@@ -394,10 +488,13 @@ END;
return $d;
}
- protected function addSequence( $ns ) {
+ protected function addSequence( $table, $pkey, $ns ) {
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")' );
+ }
}
}
@@ -412,12 +509,22 @@ END;
}
}
- protected function renameTable( $old, $new ) {
+ protected function renameTable( $old, $new, $patch = false ) {
if ( $this->db->tableExists( $old ) ) {
$this->output( "Renaming table $old to $new\n" );
$old = $this->db->realTableName( $old, "quoted" );
$new = $this->db->realTableName( $new, "quoted" );
$this->db->query( "ALTER TABLE $old RENAME TO $new" );
+ 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" );
}
}
@@ -453,13 +560,20 @@ END;
}
$sql .= " USING $default";
}
- $this->db->begin( __METHOD__ );
$this->db->query( $sql );
- $this->db->commit( __METHOD__ );
}
}
- protected function changeNullableField( $table, $field, $null ) {
+ protected function setDefault( $table, $field, $default ) {
+
+ $info = $this->db->fieldInfo( $table, $field );
+ if ( $info->defaultValue() !== $default ) {
+ $this->output( "Changing '$table.$field' default value\n" );
+ $this->db->query( "ALTER TABLE $table ALTER $field SET DEFAULT " . $default );
+ }
+ }
+
+ 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" );
@@ -498,11 +612,11 @@ END;
if ( $this->db->indexExists( $table, $index ) ) {
$this->output( "...index '$index' on table '$table' already exists\n" );
} else {
- $this->output( "Creating index '$index' on table '$table'\n" );
if ( preg_match( '/^\(/', $type ) ) {
+ $this->output( "Creating index '$index' on table '$table'\n" );
$this->db->query( "CREATE INDEX $index ON $table $type" );
} else {
- $this->applyPatch( $type, true );
+ $this->applyPatch( $type, true, "Creating index '$index' on table '$table'" );
}
}
}
@@ -518,15 +632,20 @@ END;
}
$this->output( "Altering column '$table.$field' to be DEFERRABLE INITIALLY DEFERRED\n" );
$conname = $fi->conname();
- $command = "ALTER TABLE $table DROP CONSTRAINT $conname";
- $this->db->query( $command );
- $command = "ALTER TABLE $table ADD CONSTRAINT $conname FOREIGN KEY ($field) REFERENCES $clause DEFERRABLE INITIALLY DEFERRED";
+ if ( $fi->conname() ) {
+ $conclause = "CONSTRAINT \"$conname\"";
+ $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" );
+ $conclause = "";
+ }
+ $command = "ALTER TABLE $table ADD $conclause FOREIGN KEY ($field) REFERENCES $clause DEFERRABLE INITIALLY DEFERRED";
$this->db->query( $command );
}
protected function convertArchive2() {
if ( $this->db->tableExists( "archive2" ) ) {
- $this->output( "Converting 'archive2' back to normal archive table\n" );
if ( $this->db->ruleExists( 'archive', 'archive_insert' ) ) {
$this->output( "Dropping rule 'archive_insert'\n" );
$this->db->query( 'DROP RULE archive_insert ON archive' );
@@ -535,7 +654,7 @@ END;
$this->output( "Dropping rule 'archive_delete'\n" );
$this->db->query( 'DROP RULE archive_delete ON archive' );
}
- $this->applyPatch( 'patch-remove-archive2.sql' );
+ $this->applyPatch( 'patch-remove-archive2.sql', false, "Converting 'archive2' back to normal archive table" );
} else {
$this->output( "...obsolete table 'archive2' does not exist\n" );
}
@@ -570,38 +689,34 @@ END;
protected function checkPageDeletedTrigger() {
if ( !$this->db->triggerExists( 'page', 'page_deleted' ) ) {
- $this->output( "Adding function and trigger 'page_deleted' to table 'page'\n" );
- $this->applyPatch( 'patch-page_deleted.sql' );
+ $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" );
}
}
- protected function checkRcCurIdNullable(){
- $fi = $this->db->fieldInfo( 'recentchanges', 'rc_cur_id' );
- if ( !$fi->isNullable() ) {
- $this->output( "Removing NOT NULL constraint from 'recentchanges.rc_cur_id'\n" );
- $this->applyPatch( 'patch-rc_cur_id-not-null.sql' );
- } else {
- $this->output( "...column 'recentchanges.rc_cur_id' has a NOT NULL constraint\n" );
+ 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 ."\"" );
}
}
- protected function checkPagelinkUniqueIndex() {
- $pu = $this->describeIndex( 'pagelink_unique' );
- if ( !is_null( $pu ) && ( $pu[0] != 'pl_from' || $pu[1] != 'pl_namespace' || $pu[2] != 'pl_title' ) ) {
- $this->output( "Dropping obsolete version of index 'pagelink_unique index'\n" );
- $this->db->query( 'DROP INDEX pagelink_unique' );
- $pu = null;
+ protected function checkIndex( $index, $should_be, $good_def ) {
+ $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 ."\"" );
+ $pu = array();
} else {
- $this->output( "...obsolete version of index 'pagelink_unique index' does not exist\n" );
+ $this->output( "...no need to drop index '$index'\n" );
}
- if ( is_null( $pu ) ) {
- $this->output( "Creating index 'pagelink_unique index'\n" );
- $this->db->query( 'CREATE UNIQUE INDEX pagelink_unique ON pagelinks (pl_from,pl_namespace,pl_title)' );
+ if ( empty( $pu ) ) {
+ $this->output( "Creating index '$index'\n" );
+ $this->db->query( $good_def );
} else {
- $this->output( "...index 'pagelink_unique_index' already exists\n" );
+ $this->output( "...index '$index' exists\n" );
}
}
@@ -609,43 +724,30 @@ END;
if ( $this->fkeyDeltype( 'revision_rev_user_fkey' ) == 'r' ) {
$this->output( "...constraint 'revision_rev_user_fkey' is ON DELETE RESTRICT\n" );
} else {
- $this->output( "Changing constraint 'revision_rev_user_fkey' to ON DELETE RESTRICT\n" );
- $this->applyPatch( 'patch-revision_rev_user_fkey.sql' );
- }
- }
-
- protected function checkIpbAdress() {
- if ( $this->db->indexExists( 'ipblocks', 'ipb_address' ) ) {
- $this->output( "Removing deprecated index 'ipb_address'...\n" );
- $this->db->query( 'DROP INDEX ipb_address' );
- }
- if ( $this->db->indexExists( 'ipblocks', 'ipb_address_unique' ) ) {
- $this->output( "...have ipb_address_unique\n" );
- } else {
- $this->output( "Adding ipb_address_unique index\n" );
- $this->applyPatch( 'patch-ipb_address_unique.sql' );
+ $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->output( "Replacing index 'iwl_prefix' with 'iwl_prefix_from_title'...\n" );
- $this->applyPatch( 'patch-rename-iwl_prefix.sql' );
+ $this->applyPatch( 'patch-rename-iwl_prefix.sql', false, "Replacing index 'iwl_prefix' with 'iwl_prefix_from_title'" );
}
}
+ protected function addInterwikiType() {
+ $this->applyPatch( 'patch-add_interwiki.sql', false, "Refreshing add_interwiki()" );
+ }
+
protected function tsearchFixes() {
# Tweak the page_title tsearch2 trigger to filter out slashes
# This is create or replace, so harmless to call if not needed
- $this->output( "Refreshing ts2_page_title()...\n" );
- $this->applyPatch( 'patch-ts2pagetitle.sql' );
+ $this->applyPatch( 'patch-ts2pagetitle.sql', false, "Refreshing ts2_page_title()" );
# If the server is 8.3 or higher, rewrite the tsearch2 triggers
# in case they have the old 'default' versions
# Gather version numbers in case we need them
if ( $this->db->getServerVersion() >= 8.3 ) {
- $this->output( "Rewriting tsearch2 triggers...\n" );
- $this->applyPatch( 'patch-tsearch2funcs.sql' );
+ $this->applyPatch( 'patch-tsearch2funcs.sql', false, "Rewriting tsearch2 triggers" );
}
}
}
diff --git a/includes/installer/SqliteInstaller.php b/includes/installer/SqliteInstaller.php
index 658a3b16..6e1a74f6 100644
--- a/includes/installer/SqliteInstaller.php
+++ b/includes/installer/SqliteInstaller.php
@@ -2,6 +2,21 @@
/**
* Sqlite-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
*/
@@ -207,7 +222,7 @@ class SqliteInstaller extends DatabaseInstaller {
}
/**
- * @return Staus
+ * @return Status
*/
public function createTables() {
$status = parent::createTables();
diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php
index e1bc2926..12a310af 100644
--- a/includes/installer/SqliteUpdater.php
+++ b/includes/installer/SqliteUpdater.php
@@ -2,6 +2,21 @@
/**
* Sqlite-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
*/
@@ -71,6 +86,12 @@ class SqliteUpdater extends DatabaseUpdater {
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' ),
+
+ // 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' ),
);
}
@@ -80,22 +101,16 @@ class SqliteUpdater extends DatabaseUpdater {
$this->output( "...have initial indexes\n" );
return;
}
- $this->output( "Adding initial indexes..." );
- $this->applyPatch( 'initial-indexes.sql' );
- $this->output( "done\n" );
+ $this->applyPatch( 'initial-indexes.sql', false, "Adding initial indexes" );
}
protected function sqliteSetupSearchindex() {
$module = DatabaseSqlite::getFulltextSearchModule();
$fts3tTable = $this->updateRowExists( 'fts3' );
if ( $fts3tTable && !$module ) {
- $this->output( '...PHP is missing FTS3 support, downgrading tables...' );
- $this->applyPatch( 'searchindex-no-fts.sql' );
- $this->output( "done\n" );
+ $this->applyPatch( 'searchindex-no-fts.sql', false, 'PHP is missing FTS3 support, downgrading tables' );
} elseif ( !$fts3tTable && $module == 'FTS3' ) {
- $this->output( '...adding FTS3 search capabilities...' );
- $this->applyPatch( 'searchindex-fts3.sql' );
- $this->output( "done\n" );
+ $this->applyPatch( 'searchindex-fts3.sql', false, "Adding FTS3 search capabilities" );
} else {
$this->output( "...fulltext search table appears to be in order.\n" );
}
diff --git a/includes/installer/WebInstaller.php b/includes/installer/WebInstaller.php
index 1ff77db7..2f46ff0b 100644
--- a/includes/installer/WebInstaller.php
+++ b/includes/installer/WebInstaller.php
@@ -2,6 +2,21 @@
/**
* Core installer web interface.
*
+ * 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
*/
@@ -146,7 +161,7 @@ class WebInstaller extends Installer {
'Content-Disposition: attachment; filename="LocalSettings.php"'
);
- $ls = new LocalSettingsGenerator( $this );
+ $ls = InstallerOverrides::getLocalSettingsGenerator( $this );
$rightsProfile = $this->rightsProfiles[$this->getVar( '_RightsProfile' )];
foreach( $rightsProfile as $group => $rightsArr ) {
$ls->setGroupRights( $group, $rightsArr );
@@ -347,21 +362,21 @@ class WebInstaller extends Installer {
$url = $m[1];
}
return md5( serialize( array(
- 'local path' => dirname( dirname( __FILE__ ) ),
+ 'local path' => dirname( __DIR__ ),
'url' => $url,
'version' => $GLOBALS['wgVersion']
) ) );
}
/**
- * Show an error message in a box. Parameters are like wfMsg().
+ * Show an error message in a box. Parameters are like wfMessage().
* @param $msg
*/
public function showError( $msg /*...*/ ) {
$args = func_get_args();
array_shift( $args );
$args = array_map( 'htmlspecialchars', $args );
- $msg = wfMsgReal( $msg, $args, false, false, false );
+ $msg = wfMessage( $msg, $args )->useDatabase( false )->plain();
$this->output->addHTML( $this->getErrorBox( $msg ) );
}
@@ -433,6 +448,7 @@ class WebInstaller extends Installer {
*
* @param $name String
* @param $default
+ * @return null
*/
public function getSession( $name, $default = null ) {
if ( !isset( $this->session[$name] ) ) {
@@ -484,7 +500,7 @@ class WebInstaller extends Installer {
public function getAcceptLanguage() {
global $wgLanguageCode, $wgRequest;
- $mwLanguages = Language::getLanguageNames();
+ $mwLanguages = Language::fetchLanguageNames();
$headerLanguages = array_keys( $wgRequest->getAcceptLang() );
foreach ( $headerLanguages as $lang ) {
@@ -524,7 +540,7 @@ class WebInstaller extends Installer {
$s .= $this->getPageListItem( 'Restart', true, $currentPageName );
$s .= "</ul></div>\n"; // end list pane
$s .= Html::element( 'h2', array(),
- wfMsg( 'config-page-' . strtolower( $currentPageName ) ) );
+ wfMessage( 'config-page-' . strtolower( $currentPageName ) )->text() );
$this->output->addHTMLNoFlush( $s );
}
@@ -540,7 +556,7 @@ class WebInstaller extends Installer {
*/
private function getPageListItem( $pageName, $enabled, $currentPageName ) {
$s = "<li class=\"config-page-list-item\">";
- $name = wfMsg( 'config-page-' . strtolower( $pageName ) );
+ $name = wfMessage( 'config-page-' . strtolower( $pageName ) )->text();
if ( $enabled ) {
$query = array( 'page' => $pageName );
@@ -593,7 +609,7 @@ class WebInstaller extends Installer {
/**
* Get HTML for an error box with an icon.
*
- * @param $text String: wikitext, get this with wfMsgNoTrans()
+ * @param $text String: wikitext, get this with wfMessage()->plain()
*
* @return string
*/
@@ -604,7 +620,7 @@ class WebInstaller extends Installer {
/**
* Get HTML for a warning box with an icon.
*
- * @param $text String: wikitext, get this with wfMsgNoTrans()
+ * @param $text String: wikitext, get this with wfMessage()->plain()
*
* @return string
*/
@@ -615,7 +631,7 @@ class WebInstaller extends Installer {
/**
* Get HTML for an info box with an icon.
*
- * @param $text String: wikitext, get this with wfMsgNoTrans()
+ * @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
*
@@ -624,13 +640,13 @@ class WebInstaller extends Installer {
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;
- $alt = wfMsg( 'config-information' );
+ $alt = wfMessage( 'config-information' )->text();
return Html::infoBox( $text, $icon, $alt, $class, false );
}
/**
* Get small text indented help for a preceding form field.
- * Parameters like wfMsg().
+ * Parameters like wfMessage().
*
* @param $msg
* @return string
@@ -639,18 +655,19 @@ class WebInstaller extends Installer {
$args = func_get_args();
array_shift( $args );
$args = array_map( 'htmlspecialchars', $args );
- $text = wfMsgReal( $msg, $args, false, false, false );
+ $text = wfMessage( $msg, $args )->useDatabase( false )->plain();
$html = $this->parse( $text, true );
return "<div class=\"mw-help-field-container\">\n" .
- "<span class=\"mw-help-field-hint\">" . wfMsgHtml( 'config-help' ) . "</span>\n" .
+ "<span class=\"mw-help-field-hint\">" . wfMessage( 'config-help' )->escaped() .
+ "</span>\n" .
"<span class=\"mw-help-field-data\">" . $html . "</span>\n" .
"</div>\n";
}
/**
* Output a help box.
- * @param $msg String key for wfMsg()
+ * @param $msg String key for wfMessage()
*/
public function showHelpBox( $msg /*, ... */ ) {
$args = func_get_args();
@@ -668,7 +685,7 @@ class WebInstaller extends Installer {
$args = func_get_args();
array_shift( $args );
$html = '<div class="config-message">' .
- $this->parse( wfMsgReal( $msg, $args, false, false, false ) ) .
+ $this->parse( wfMessage( $msg, $args )->useDatabase( false )->plain() ) .
"</div>\n";
$this->output->addHTML( $html );
}
@@ -697,7 +714,7 @@ class WebInstaller extends Installer {
if ( strval( $msg ) == '' ) {
$labelText = '&#160;';
} else {
- $labelText = wfMsgHtml( $msg );
+ $labelText = wfMessage( $msg )->escaped();
}
$attributes = array( 'class' => 'config-label' );
@@ -877,7 +894,7 @@ class WebInstaller extends Installer {
if( isset( $params['rawtext'] ) ) {
$labelText = $params['rawtext'];
} else {
- $labelText = $this->parse( wfMsg( $params['label'] ) );
+ $labelText = $this->parse( wfMessage( $params['label'] )->text() );
}
return
@@ -953,7 +970,7 @@ class WebInstaller extends Installer {
Xml::radio( $params['controlName'], $value, $checked, $itemAttribs ) .
'&#160;' .
Xml::tags( 'label', array( 'for' => $id ), $this->parse(
- wfMsgNoTrans( $params['itemLabelPrefix'] . strtolower( $value ) )
+ wfMessage( $params['itemLabelPrefix'] . strtolower( $value ) )->plain()
) ) .
"</li>\n";
}
@@ -1061,7 +1078,7 @@ class WebInstaller extends Installer {
) );
$anchor = Html::rawElement( 'a',
array( 'href' => $this->getURL( array( 'localsettings' => 1 ) ) ),
- $img . ' ' . wfMsgHtml( 'config-download-localsettings' ) );
+ $img . ' ' . wfMessage( 'config-download-localsettings' )->escaped() );
return Html::rawElement( 'div', array( 'class' => 'config-download-link' ), $anchor );
}
diff --git a/includes/installer/WebInstallerOutput.php b/includes/installer/WebInstallerOutput.php
index c6c8a4c2..f3166c25 100644
--- a/includes/installer/WebInstallerOutput.php
+++ b/includes/installer/WebInstallerOutput.php
@@ -2,6 +2,21 @@
/**
* Output handler for the web 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
*/
@@ -93,7 +108,7 @@ class WebInstallerOutput {
* @return String
*/
public function getCSS( $dir ) {
- $skinDir = dirname( dirname( dirname( __FILE__ ) ) ) . '/skins';
+ $skinDir = dirname( dirname( __DIR__ ) ) . '/skins';
// All these files will be concatenated in sequence and loaded
// as one file.
@@ -103,6 +118,7 @@ class WebInstallerOutput {
$cssFileNames = array(
// Basically the "skins.vector" ResourceLoader module styles
+ 'common/shared.css',
'common/commonElements.css',
'common/commonContent.css',
'common/commonInterface.css',
@@ -133,11 +149,12 @@ class WebInstallerOutput {
if( $dir == 'rtl' ) {
$css = CSSJanus::transform( $css, true );
}
+
return $css;
}
/**
- * <link> to index.php?css=foobar for the <head>
+ * "<link>" to index.php?css=foobar for the "<head>"
* @return String
*/
private function getCssUrl( ) {
@@ -221,7 +238,6 @@ class WebInstallerOutput {
<meta name="robots" content="noindex, nofollow" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title><?php $this->outputTitle(); ?></title>
- <?php echo Html::linkedStyle( '../skins/common/shared.css' ) . "\n"; ?>
<?php echo $this->getCssUrl() . "\n"; ?>
<?php echo Html::inlineScript( "var dbTypes = " . Xml::encodeJsVar( $dbTypes ) ) . "\n"; ?>
<?php echo $this->getJQuery() . "\n"; ?>
@@ -258,7 +274,7 @@ class WebInstallerOutput {
</div>
<div class="portal"><div class="body">
<?php
- echo $this->parent->parse( wfMsgNoTrans( 'config-sidebar' ), true );
+ echo $this->parent->parse( wfMessage( 'config-sidebar' )->plain(), true );
?>
</div></div>
</div>
@@ -286,7 +302,7 @@ class WebInstallerOutput {
public function outputTitle() {
global $wgVersion;
- echo htmlspecialchars( wfMsg( 'config-title', $wgVersion ) );
+ echo wfMessage( 'config-title', $wgVersion )->escaped();
}
public function getJQuery() {
diff --git a/includes/installer/WebInstallerPage.php b/includes/installer/WebInstallerPage.php
index ff8185a1..a193afb7 100644
--- a/includes/installer/WebInstallerPage.php
+++ b/includes/installer/WebInstallerPage.php
@@ -2,6 +2,21 @@
/**
* Base code for web installer 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 Deployment
*/
@@ -36,6 +51,7 @@ abstract class WebInstallerPage {
* Is this a slow-running page in the installer? If so, WebInstaller will
* set_time_limit(0) before calling execute(). Right now this only applies
* to Install and Upgrade pages
+ * @return bool
*/
public function isSlow() {
return false;
@@ -68,13 +84,13 @@ abstract class WebInstallerPage {
if ( $continue ) {
// Fake submit button for enter keypress (bug 26267)
- $s .= Xml::submitButton( wfMsg( "config-$continue" ),
+ $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( wfMsg( "config-$back" ),
+ $s .= Xml::submitButton( wfMessage( "config-$back" )->text(),
array(
'name' => "submit-$back",
'tabindex' => $this->parent->nextTabIndex()
@@ -82,7 +98,7 @@ abstract class WebInstallerPage {
}
if ( $continue ) {
- $s .= Xml::submitButton( wfMsg( "config-$continue" ),
+ $s .= Xml::submitButton( wfMessage( "config-$continue" )->text(),
array(
'name' => "submit-$continue",
'tabindex' => $this->parent->nextTabIndex(),
@@ -117,7 +133,7 @@ abstract class WebInstallerPage {
* @return string
*/
protected function getFieldsetStart( $legend ) {
- return "\n<fieldset><legend>" . wfMsgHtml( $legend ) . "</legend>\n";
+ return "\n<fieldset><legend>" . wfMessage( $legend )->escaped() . "</legend>\n";
}
/**
@@ -161,7 +177,7 @@ class WebInstaller_Language extends WebInstallerPage {
$userLang = $r->getVal( 'uselang' );
$contLang = $r->getVal( 'ContLang' );
- $languages = Language::getLanguageNames();
+ $languages = Language::fetchLanguageNames();
$lifetime = intval( ini_get( 'session.gc_maxlifetime' ) );
if ( !$lifetime ) {
$lifetime = 1440; // PHP default
@@ -216,7 +232,7 @@ class WebInstaller_Language extends WebInstallerPage {
}
/**
- * Get a <select> for selecting languages.
+ * Get a "<select>" for selecting languages.
*
* @param $name
* @param $label
@@ -232,7 +248,7 @@ class WebInstaller_Language extends WebInstallerPage {
$s .= Html::openElement( 'select', array( 'id' => $name, 'name' => $name,
'tabindex' => $this->parent->nextTabIndex() ) ) . "\n";
- $languages = Language::getLanguageNames();
+ $languages = Language::fetchLanguageNames();
ksort( $languages );
foreach ( $languages as $code => $lang ) {
if ( isset( $wgDummyLanguageCodes[$code] ) ) continue;
@@ -279,8 +295,8 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
}
$this->startForm();
$this->addHTML( $this->parent->getInfoBox(
- wfMsgNoTrans( 'config-upgrade-key-missing',
- "<pre dir=\"ltr\">\$wgUpgradeKey = '" . $this->getVar( 'wgUpgradeKey' ) . "';</pre>" )
+ wfMessage( 'config-upgrade-key-missing', "<pre dir=\"ltr\">\$wgUpgradeKey = '" .
+ $this->getVar( 'wgUpgradeKey' ) . "';</pre>" )->plain()
) );
$this->endForm( 'continue' );
return 'output';
@@ -317,7 +333,7 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
protected function showKeyForm() {
$this->startForm();
$this->addHTML(
- $this->parent->getInfoBox( wfMsgNoTrans( 'config-localsettings-upgrade' ) ).
+ $this->parent->getInfoBox( wfMessage( 'config-localsettings-upgrade' )->plain() ).
'<br />' .
$this->parent->getTextBox( array(
'var' => 'wgUpgradeKey',
@@ -341,7 +357,7 @@ class WebInstaller_ExistingWiki extends WebInstallerPage {
/**
* Initiate an upgrade of the existing database
- * @param $vars Variables from LocalSettings.php and AdminSettings.php
+ * @param $vars array Variables from LocalSettings.php and AdminSettings.php
* @return Status
*/
protected function handleExistingUpgrade( $vars ) {
@@ -394,13 +410,13 @@ class WebInstaller_Welcome extends WebInstallerPage {
return 'continue';
}
}
- $this->parent->output->addWikiText( wfMsgNoTrans( 'config-welcome' ) );
+ $this->parent->output->addWikiText( wfMessage( 'config-welcome' )->plain() );
$status = $this->parent->doEnvironmentChecks();
if ( $status->isGood() ) {
$this->parent->output->addHTML( '<span class="success-message">' .
- wfMsgHtml( 'config-env-good' ) . '</span>' );
- $this->parent->output->addWikiText( wfMsgNoTrans( 'config-copyright',
- SpecialVersion::getCopyrightAndAuthorList() ) );
+ wfMessage( 'config-env-good' )->escaped() . '</span>' );
+ $this->parent->output->addWikiText( wfMessage( 'config-copyright',
+ SpecialVersion::getCopyrightAndAuthorList() )->plain() );
$this->startForm();
$this->endForm();
} else {
@@ -438,10 +454,10 @@ class WebInstaller_DBConnect extends WebInstallerPage {
$dbSupport = '';
foreach( $this->parent->getDBTypes() as $type ) {
$link = DatabaseBase::factory( $type )->getSoftwareLink();
- $dbSupport .= wfMsgNoTrans( "config-support-$type", $link ) . "\n";
+ $dbSupport .= wfMessage( "config-support-$type", $link )->plain() . "\n";
}
$this->addHTML( $this->parent->getInfoBox(
- wfMsg( 'config-support-info', $dbSupport ) ) );
+ wfMessage( 'config-support-info', trim( $dbSupport ) )->text() ) );
foreach ( $this->parent->getVar( '_CompiledDBs' ) as $type ) {
$installer = $this->parent->getDBInstaller( $type );
@@ -460,7 +476,7 @@ class WebInstaller_DBConnect extends WebInstallerPage {
$settings .=
Html::openElement( 'div', array( 'id' => 'DB_wrapper_' . $type,
'class' => 'dbWrapper' ) ) .
- Html::element( 'h3', array(), wfMsg( 'config-header-' . $type ) ) .
+ Html::element( 'h3', array(), wfMessage( 'config-header-' . $type )->text() ) .
$installer->getConnectForm() .
"</div>\n";
}
@@ -539,7 +555,7 @@ class WebInstaller_Upgrade extends WebInstallerPage {
$this->startForm();
$this->addHTML( $this->parent->getInfoBox(
- wfMsgNoTrans( 'config-can-upgrade', $GLOBALS['wgVersion'] ) ) );
+ wfMessage( 'config-can-upgrade', $GLOBALS['wgVersion'] )->plain() ) );
$this->endForm();
}
@@ -554,11 +570,11 @@ class WebInstaller_Upgrade extends WebInstallerPage {
$this->parent->disableLinkPopups();
$this->addHTML(
$this->parent->getInfoBox(
- wfMsgNoTrans( $msg,
+ wfMessage( $msg,
$this->getVar( 'wgServer' ) .
$this->getVar( 'wgScriptPath' ) . '/index' .
$this->getVar( 'wgScriptExtension' )
- ), 'tick-32.png'
+ )->plain(), 'tick-32.png'
)
);
$this->parent->restoreLinkPopups();
@@ -619,7 +635,10 @@ class WebInstaller_Name extends WebInstallerPage {
// Set wgMetaNamespace to something valid before we show the form.
// $wgMetaNamespace defaults to $wgSiteName which is 'MediaWiki'
$metaNS = $this->getVar( 'wgMetaNamespace' );
- $this->setVar( 'wgMetaNamespace', wfMsgForContent( 'config-ns-other-default' ) );
+ $this->setVar(
+ 'wgMetaNamespace',
+ wfMessage( 'config-ns-other-default' )->inContentLanguage()->text()
+ );
$this->addHTML(
$this->parent->getTextBox( array(
@@ -667,7 +686,7 @@ class WebInstaller_Name extends WebInstallerPage {
'help' => $this->parent->getHelpBox( 'config-subscribe-help' )
) ) .
$this->getFieldSetEnd() .
- $this->parent->getInfoBox( wfMsg( 'config-almost-done' ) ) .
+ $this->parent->getInfoBox( wfMessage( 'config-almost-done' )->text() ) .
$this->parent->getRadioSet( array(
'var' => '_SkipOptional',
'itemLabelPrefix' => 'config-optional-',
@@ -705,7 +724,7 @@ class WebInstaller_Name extends WebInstallerPage {
$name = preg_replace( '/__+/', '_', $name );
$name = ucfirst( trim( $name, '_' ) );
} elseif ( $nsType == 'generic' ) {
- $name = wfMsg( 'config-ns-generic' );
+ $name = wfMessage( 'config-ns-generic' )->text();
} else { // other
$name = $this->getVar( 'wgMetaNamespace' );
}
@@ -817,7 +836,7 @@ class WebInstaller_Options extends WebInstallerPage {
'itemLabelPrefix' => 'config-profile-',
'values' => array_keys( $this->parent->rightsProfiles ),
) ) .
- $this->parent->getInfoBox( wfMsgNoTrans( 'config-profile-help' ) ) .
+ $this->parent->getInfoBox( wfMessage( 'config-profile-help' )->plain() ) .
# Licensing
$this->parent->getRadioSet( array(
@@ -1030,7 +1049,7 @@ class WebInstaller_Options extends WebInstallerPage {
'href' => $this->getCCPartnerUrl(),
'onclick' => $expandJs,
),
- wfMsg( 'config-cc-again' )
+ wfMessage( 'config-cc-again' )->text()
) .
"</p>\n" .
"<script type=\"text/javascript\">\n" .
@@ -1076,7 +1095,7 @@ class WebInstaller_Options extends WebInstallerPage {
if ( isset( $entry['text'] ) ) {
$this->setVar( 'wgRightsText', $entry['text'] );
} else {
- $this->setVar( 'wgRightsText', wfMsg( 'config-license-' . $code ) );
+ $this->setVar( 'wgRightsText', wfMessage( 'config-license-' . $code )->text() );
}
$this->setVar( 'wgRightsUrl', $entry['url'] );
$this->setVar( 'wgRightsIcon', $entry['icon'] );
@@ -1149,14 +1168,14 @@ class WebInstaller_Install extends WebInstallerPage {
$this->endForm( $continue, $back );
} else {
$this->startForm();
- $this->addHTML( $this->parent->getInfoBox( wfMsgNoTrans( 'config-install-begin' ) ) );
+ $this->addHTML( $this->parent->getInfoBox( wfMessage( 'config-install-begin' )->plain() ) );
$this->endForm();
}
return true;
}
public function startStage( $step ) {
- $this->addHTML( "<li>" . wfMsgHtml( "config-install-$step" ) . wfMsg( 'ellipsis') );
+ $this->addHTML( "<li>" . wfMessage( "config-install-$step" )->escaped() . wfMessage( 'ellipsis')->escaped() );
if ( $step == 'extension-tables' ) {
$this->startLiveBox();
}
@@ -1171,7 +1190,7 @@ class WebInstaller_Install extends WebInstallerPage {
$this->endLiveBox();
}
$msg = $status->isOk() ? 'config-install-step-done' : 'config-install-step-failed';
- $html = wfMsgHtml( 'word-separator' ) . wfMsgHtml( $msg );
+ $html = wfMessage( 'word-separator' )->escaped() . wfMessage( $msg )->escaped();
if ( !$status->isOk() ) {
$html = "<span class=\"error\">$html</span>";
}
@@ -1203,13 +1222,13 @@ class WebInstaller_Complete extends WebInstallerPage {
$this->parent->disableLinkPopups();
$this->addHTML(
$this->parent->getInfoBox(
- wfMsgNoTrans( 'config-install-done',
+ wfMessage( 'config-install-done',
$lsUrl,
$this->getVar( 'wgServer' ) .
$this->getVar( 'wgScriptPath' ) . '/index' .
$this->getVar( 'wgScriptExtension' ),
'<downloadlink/>'
- ), 'tick-32.png'
+ )->plain(), 'tick-32.png'
)
);
$this->parent->restoreLinkPopups();
@@ -1230,7 +1249,7 @@ class WebInstaller_Restart extends WebInstallerPage {
}
$this->startForm();
- $s = $this->parent->getWarningBox( wfMsgNoTrans( 'config-help-restart' ) );
+ $s = $this->parent->getWarningBox( wfMessage( 'config-help-restart' )->plain() );
$this->addHTML( $s );
$this->endForm( 'restart' );
}
@@ -1250,7 +1269,11 @@ abstract class WebInstaller_Document extends WebInstallerPage {
}
public function getFileContents() {
- return file_get_contents( dirname( __FILE__ ) . '/../../' . $this->getFileName() );
+ $file = __DIR__ . '/../../' . $this->getFileName();
+ if( ! file_exists( $file ) ) {
+ return wfMessage( 'config-nofile', $file )->plain();
+ }
+ return file_get_contents( $file );
}
}
@@ -1260,7 +1283,15 @@ class WebInstaller_Readme extends WebInstaller_Document {
}
class WebInstaller_ReleaseNotes extends WebInstaller_Document {
- protected function getFileName() { return 'RELEASE-NOTES'; }
+ protected function getFileName() {
+ global $wgVersion;
+
+ if(! preg_match( '/^(\d+)\.(\d+).*/i', $wgVersion, $result ) ) {
+ throw new MWException('Variable $wgVersion has an invalid value.');
+ }
+
+ return 'RELEASE-NOTES-' . $result[1] . '.' . $result[2];
+ }
}
class WebInstaller_UpgradeDoc extends WebInstaller_Document {
diff --git a/includes/interwiki/Interwiki.php b/includes/interwiki/Interwiki.php
index 3aaa1c52..eacf9a87 100644
--- a/includes/interwiki/Interwiki.php
+++ b/includes/interwiki/Interwiki.php
@@ -1,7 +1,23 @@
<?php
/**
+ * Interwiki table entry.
+ *
+ * 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
- * Interwiki table entry
*/
/**
@@ -42,7 +58,7 @@ class Interwiki {
* Fetch an Interwiki object
*
* @param $prefix String: interwiki prefix to use
- * @return Interwiki Object, or null if not valid
+ * @return Interwiki|null|bool
*/
static public function fetch( $prefix ) {
global $wgContLang;
@@ -130,6 +146,7 @@ class Interwiki {
$value = '';
}
+
return $value;
}
@@ -164,7 +181,7 @@ class Interwiki {
$db = wfGetDB( DB_SLAVE );
- $row = $db->fetchRow( $db->select( 'interwiki', '*', array( 'iw_prefix' => $prefix ),
+ $row = $db->fetchRow( $db->select( 'interwiki', self::selectFields(), array( 'iw_prefix' => $prefix ),
__METHOD__ ) );
$iw = Interwiki::loadFromArray( $row );
if ( $iw ) {
@@ -288,7 +305,7 @@ class Interwiki {
}
$res = $db->select( 'interwiki',
- array( 'iw_prefix', 'iw_url', 'iw_api', 'iw_wikiid', 'iw_local', 'iw_trans' ),
+ self::selectFields(),
$where, __METHOD__, array( 'ORDER BY' => 'iw_prefix' )
);
$retval = array();
@@ -389,4 +406,20 @@ class Interwiki {
$msg = wfMessage( 'interwiki-desc-' . $this->mPrefix )->inContentLanguage();
return !$msg->exists() ? '' : $msg;
}
+
+ /**
+ * Return the list of interwiki fields that should be selected to create
+ * a new interwiki object.
+ * @return array
+ */
+ public static function selectFields() {
+ return array(
+ 'iw_prefix',
+ 'iw_url',
+ 'iw_api',
+ 'iw_wikiid',
+ 'iw_local',
+ 'iw_trans'
+ );
+ }
}
diff --git a/includes/job/DoubleRedirectJob.php b/includes/job/DoubleRedirectJob.php
index 2b7cd7c8..08af9975 100644
--- a/includes/job/DoubleRedirectJob.php
+++ b/includes/job/DoubleRedirectJob.php
@@ -1,6 +1,21 @@
<?php
/**
- * Job to fix double redirects after moving a page
+ * Job to fix double redirects after moving a page.
+ *
+ * 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
@@ -21,7 +36,7 @@ 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 $reason String: the reason for the fix, see message "double-redirect-fixed-<reason>"
* @param $redirTitle Title: the title which has changed, redirects pointing to this title are fixed
* @param $destTitle bool Not used
*/
@@ -74,7 +89,7 @@ class DoubleRedirectJob extends Job {
return false;
}
- $targetRev = Revision::newFromTitle( $this->title );
+ $targetRev = Revision::newFromTitle( $this->title, false, Revision::READ_LATEST );
if ( !$targetRev ) {
wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" );
return true;
@@ -126,8 +141,9 @@ class DoubleRedirectJob extends Job {
$oldUser = $wgUser;
$wgUser = $this->getUser();
$article = WikiPage::factory( $this->title );
- $reason = wfMsgForContent( 'double-redirect-fixed-' . $this->reason,
- $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() );
+ $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() );
$wgUser = $oldUser;
@@ -139,7 +155,7 @@ class DoubleRedirectJob extends Job {
*
* @param $title Title
*
- * @return false if the specified title is not a redirect, or if it is a circular redirect
+ * @return bool if the specified title is not a redirect, or if it is a circular redirect
*/
public static function getFinalDestination( $title ) {
$dbw = wfGetDB( DB_MASTER );
@@ -179,7 +195,7 @@ class DoubleRedirectJob extends Job {
*/
function getUser() {
if ( !self::$user ) {
- self::$user = User::newFromName( wfMsgForContent( 'double-redirect-fixer' ), false );
+ 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->addToDatabase();
diff --git a/includes/job/EmaillingJob.php b/includes/job/EmaillingJob.php
index 89b74a41..d3599882 100644
--- a/includes/job/EmaillingJob.php
+++ b/includes/job/EmaillingJob.php
@@ -2,6 +2,21 @@
/**
* Old job for notification emails.
*
+ * 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
*/
diff --git a/includes/job/EnotifNotifyJob.php b/includes/job/EnotifNotifyJob.php
index eb154ece..b4c925e9 100644
--- a/includes/job/EnotifNotifyJob.php
+++ b/includes/job/EnotifNotifyJob.php
@@ -2,6 +2,21 @@
/**
* Job for notification emails.
*
+ * 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
*/
diff --git a/includes/job/JobQueue.php b/includes/job/Job.php
index e7c66719..d777a5d4 100644
--- a/includes/job/JobQueue.php
+++ b/includes/job/Job.php
@@ -1,15 +1,26 @@
<?php
/**
- * Job queue base code
+ * 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
*/
-if ( !defined( 'MEDIAWIKI' ) ) {
- die( "This file is part of MediaWiki, it is not a valid entry point\n" );
-}
-
/**
* Class to both describe a background job and handle jobs.
*
@@ -56,7 +67,7 @@ abstract class Job {
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$row = $dbw->selectRow(
'job',
@@ -67,7 +78,7 @@ abstract class Job {
);
if ( $row === false ) {
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
wfProfileOut( __METHOD__ );
return false;
}
@@ -75,7 +86,7 @@ abstract class Job {
/* Ensure we "own" this row */
$dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
$affected = $dbw->affectedRows();
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
if ( $affected == 0 ) {
wfProfileOut( __METHOD__ );
@@ -102,7 +113,6 @@ abstract class Job {
* @return Job or false if there's no jobs
*/
static function pop( $offset = 0 ) {
- global $wgJobTypesExcludedFromDefaultQueue;
wfProfileIn( __METHOD__ );
$dbr = wfGetDB( DB_SLAVE );
@@ -113,12 +123,9 @@ abstract class Job {
NB: If random fetch previously was used, offset
will always be ahead of few entries
*/
- $conditions = array();
- if ( count( $wgJobTypesExcludedFromDefaultQueue ) != 0 ) {
- foreach ( $wgJobTypesExcludedFromDefaultQueue as $cmdType ) {
- $conditions[] = "job_cmd != " . $dbr->addQuotes( $cmdType );
- }
- }
+
+ $conditions = self::defaultQueueConditions();
+
$offset = intval( $offset );
$options = array( 'ORDER BY' => 'job_id', 'USE INDEX' => 'PRIMARY' );
@@ -146,13 +153,12 @@ abstract class Job {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
$affected = $dbw->affectedRows();
- $dbw->commit();
if ( !$affected ) {
// Failed, someone else beat us to it
// Try getting a random row
- $row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob',
- 'MAX(job_id) as maxjob' ), '1=1', __METHOD__ );
+ $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__ );
@@ -170,7 +176,6 @@ abstract class Job {
// Delete the random row
$dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
$affected = $dbw->affectedRows();
- $dbw->commit();
if ( !$affected ) {
// Random job gone before we exclusively deleted it
@@ -186,6 +191,12 @@ abstract class Job {
$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
@@ -200,11 +211,12 @@ abstract class Job {
*
* @param $command String: Job command
* @param $title Title: Associated title
- * @param $params Array: Job parameters
+ * @param $params Array|bool: Job parameters
* @param $id Int: Job identifier
+ * @throws MWException
* @return Job
*/
- static function factory( $command, $title, $params = false, $id = 0 ) {
+ static function factory( $command, Title $title, $params = false, $id = 0 ) {
global $wgJobClasses;
if( isset( $wgJobClasses[$command] ) ) {
$class = $wgJobClasses[$command];
@@ -260,16 +272,16 @@ abstract class Job {
$rows[] = $job->insertFields();
if ( count( $rows ) >= 50 ) {
# Do a small transaction to avoid slave lag
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
$rows = array();
}
}
if ( $rows ) { // last chunk
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
}
wfIncrStats( 'job-insert', count( $jobs ) );
}
@@ -302,6 +314,27 @@ abstract class Job {
wfIncrStats( 'job-insert', count( $jobs ) );
}
+
+ /**
+ * SQL conditions to apply on most JobQueue queries
+ *
+ * Whenever we exclude jobs types from the default queue, we want to make
+ * sure that queries to the job queue actually ignore them.
+ *
+ * @return array SQL conditions suitable for Database:: methods
+ */
+ 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;
+ }
+
/*-------------------------------------------------------------------------
* Non-static functions
*------------------------------------------------------------------------*/
@@ -309,8 +342,8 @@ abstract class Job {
/**
* @param $command
* @param $title
- * @param $params array
- * @param int $id
+ * @param $params array|bool
+ * @param $id int
*/
function __construct( $command, $title, $params = false, $id = 0 ) {
$this->command = $command;
@@ -368,11 +401,12 @@ abstract class Job {
$fields = $this->insertFields();
unset( $fields['job_id'] );
+ unset( $fields['job_timestamp'] );
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$dbw->delete( 'job', $fields, __METHOD__ );
$affected = $dbw->affectedRows();
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
if ( $affected ) {
wfIncrStats( 'job-dup-delete', $affected );
}
diff --git a/includes/job/RefreshLinksJob.php b/includes/job/RefreshLinksJob.php
index 1aa206f0..b23951c6 100644
--- a/includes/job/RefreshLinksJob.php
+++ b/includes/job/RefreshLinksJob.php
@@ -2,6 +2,21 @@
/**
* 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
*/
@@ -22,7 +37,6 @@ class RefreshLinksJob extends Job {
* @return boolean success
*/
function run() {
- global $wgParser, $wgContLang;
wfProfileIn( __METHOD__ );
$linkCache = LinkCache::singleton();
@@ -34,24 +48,41 @@ class RefreshLinksJob extends Job {
return false;
}
- $revision = Revision::newFromTitle( $this->title );
+ # 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() . '"';
+ $this->error = 'refreshLinks: Article not found "' .
+ $this->title->getPrefixedDBkey() . '"';
wfProfileOut( __METHOD__ );
- return false;
+ return false; // XXX: what if it was just deleted?
}
- wfProfileIn( __METHOD__.'-parse' );
- $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
- $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
- wfProfileOut( __METHOD__.'-parse' );
- wfProfileIn( __METHOD__.'-update' );
- $update = new LinksUpdate( $this->title, $parserOutput, false );
- $update->doUpdate();
- wfProfileOut( __METHOD__.'-update' );
+ 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' );
+ }
}
/**
@@ -61,6 +92,7 @@ class RefreshLinksJob extends Job {
* @ingroup JobQueue
*/
class RefreshLinksJob2 extends Job {
+ const MAX_TITLES_RUN = 10;
function __construct( $title, $params, $id = 0 ) {
parent::__construct( 'refreshLinks2', $title, $params, $id );
@@ -71,60 +103,100 @@ class RefreshLinksJob2 extends Job {
* @return boolean success
*/
function run() {
- global $wgParser, $wgContLang;
-
wfProfileIn( __METHOD__ );
$linkCache = LinkCache::singleton();
$linkCache->clear();
- if( is_null( $this->title ) ) {
+ if ( is_null( $this->title ) ) {
$this->error = "refreshLinks2: Invalid title";
wfProfileOut( __METHOD__ );
return false;
- }
- if( !isset($this->params['start']) || !isset($this->params['end']) ) {
+ } 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';
- $titles = $this->title->getBacklinkCache()->getLinks(
- $table, $this->params['start'], $this->params['end']);
-
- # Not suitable for page load triggered job running!
- # Gracefully switch to refreshLinks jobs if this happens.
- if( php_sapi_name() != 'cli' ) {
+
+ // 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, '' );
+ $jobs[] = new RefreshLinksJob( $title, array( 'masterPos' => $masterPos ) );
}
Job::batchInsert( $jobs );
-
- wfProfileOut( __METHOD__ );
- return true;
- }
- $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
- # Re-parse each page that transcludes this page and update their tracking links...
- foreach ( $titles as $title ) {
- $revision = Revision::newFromTitle( $title );
- if ( !$revision ) {
- $this->error = 'refreshLinks: Article not found "' . $title->getPrefixedDBkey() . '"';
- wfProfileOut( __METHOD__ );
- return false;
+ } 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();
}
- wfProfileIn( __METHOD__.'-parse' );
- $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
- wfProfileOut( __METHOD__.'-parse' );
- wfProfileIn( __METHOD__.'-update' );
- $update = new LinksUpdate( $title, $parserOutput, false );
- $update->doUpdate();
- wfProfileOut( __METHOD__.'-update' );
- wfWaitForSlaves();
}
- wfProfileOut( __METHOD__ );
+ wfProfileOut( __METHOD__ );
return true;
}
}
diff --git a/includes/job/UploadFromUrlJob.php b/includes/job/UploadFromUrlJob.php
index 26f6e4ba..e06f68e4 100644
--- a/includes/job/UploadFromUrlJob.php
+++ b/includes/job/UploadFromUrlJob.php
@@ -2,6 +2,21 @@
/**
* Job for asynchronous upload-by-url.
*
+ * 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
*/
@@ -67,10 +82,10 @@ class UploadFromUrlJob extends Job {
if ( $this->params['leaveMessage'] ) {
$this->user->leaveUserMessage(
- wfMsg( 'upload-warning-subj' ),
- wfMsg( 'upload-warning-msg',
+ wfMessage( 'upload-warning-subj' )->text(),
+ wfMessage( 'upload-warning-msg',
$key,
- $this->params['url'] )
+ $this->params['url'] )->text()
);
} else {
wfSetupSession( $this->params['sessionId'] );
@@ -104,17 +119,17 @@ class UploadFromUrlJob extends Job {
protected function leaveMessage( $status ) {
if ( $this->params['leaveMessage'] ) {
if ( $status->isGood() ) {
- $this->user->leaveUserMessage( wfMsg( 'upload-success-subj' ),
- wfMsg( 'upload-success-msg',
+ $this->user->leaveUserMessage( wfMessage( 'upload-success-subj' )->text(),
+ wfMessage( 'upload-success-msg',
$this->upload->getTitle()->getText(),
$this->params['url']
- ) );
+ )->text() );
} else {
- $this->user->leaveUserMessage( wfMsg( 'upload-failure-subj' ),
- wfMsg( 'upload-failure-msg',
+ $this->user->leaveUserMessage( wfMessage( 'upload-failure-subj' )->text(),
+ wfMessage( 'upload-failure-msg',
$status->getWikiText(),
$this->params['url']
- ) );
+ )->text() );
}
} else {
wfSetupSession( $this->params['sessionId'] );
diff --git a/includes/json/FormatJson.php b/includes/json/FormatJson.php
index f7373e45..f67700c9 100644
--- a/includes/json/FormatJson.php
+++ b/includes/json/FormatJson.php
@@ -1,15 +1,26 @@
<?php
/**
- * Simple wrapper for json_econde and json_decode that falls back on Services_JSON class
+ * Simple wrapper for json_econde and json_decode that falls back on Services_JSON class.
+ *
+ * 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 dirname( __FILE__ ) . '/Services_JSON.php';
+require_once __DIR__ . '/Services_JSON.php';
/**
* JSON formatter wrapper class
@@ -30,14 +41,11 @@ class FormatJson {
* @return string
*/
public static function encode( $value, $isHtml = false ) {
- // Some versions of PHP have a broken json_encode, see PHP bug
- // 46944. Test encoding an affected character (U+20000) to
- // avoid this.
- if ( !function_exists( 'json_encode' ) || $isHtml || strtolower( json_encode( "\xf0\xa0\x80\x80" ) ) != '"\ud840\udc00"' ) {
+ 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 );
+ return json_encode( $value, $isHtml ? JSON_PRETTY_PRINT : 0 );
}
}
@@ -49,7 +57,7 @@ class FormatJson {
*
* @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
+ * and "&null;" respectively. "&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 ) {
diff --git a/includes/json/Services_JSON.php b/includes/json/Services_JSON.php
index b2090dce..398ed6a2 100644
--- a/includes/json/Services_JSON.php
+++ b/includes/json/Services_JSON.php
@@ -826,6 +826,7 @@ class Services_JSON
/**
* @todo Ultimately, this should just call PEAR::isError()
+ * @return bool
*/
function isError($data, $code = null)
{
diff --git a/includes/libs/CSSJanus.php b/includes/libs/CSSJanus.php
index c8fc296b..4ebbc497 100644
--- a/includes/libs/CSSJanus.php
+++ b/includes/libs/CSSJanus.php
@@ -1,5 +1,7 @@
<?php
/**
+ * PHP port of CSSJanus.
+ *
* 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
@@ -15,6 +17,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
+ * @file
*/
/**
@@ -122,7 +125,7 @@ class CSSJanus {
* @param $css String: 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 Transformed stylesheet
+ * @return string Transformed stylesheet
*/
public static function transform( $css, $swapLtrRtlInURL = false, $swapLeftRightInURL = false ) {
// We wrap tokens in ` , not ~ like the original implementation does.
@@ -265,10 +268,17 @@ class CSSJanus {
* @return string
*/
private static function fixBackgroundPosition( $css ) {
- $css = preg_replace_callback( self::$patterns['bg_horizontal_percentage'],
+ $replaced = preg_replace_callback( self::$patterns['bg_horizontal_percentage'],
array( 'self', 'calculateNewBackgroundPosition' ), $css );
- $css = preg_replace_callback( self::$patterns['bg_horizontal_percentage_x'],
+ if ( $replaced !== null ) {
+ // Check for null; sometimes preg_replace_callback() returns null here for some weird reason
+ $css = $replaced;
+ }
+ $replaced = preg_replace_callback( self::$patterns['bg_horizontal_percentage_x'],
array( 'self', 'calculateNewBackgroundPosition' ), $css );
+ if ( $replaced !== null ) {
+ $css = $replaced;
+ }
return $css;
}
diff --git a/includes/libs/CSSMin.php b/includes/libs/CSSMin.php
index 4f4b28bb..fc75cdcc 100644
--- a/includes/libs/CSSMin.php
+++ b/includes/libs/CSSMin.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Minification of CSS stylesheets.
+ *
* Copyright 2010 Wikimedia Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -12,12 +14,6 @@
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
* OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
- */
-
-/**
- * Transforms CSS data
- *
- * This class provides minification, URL remapping, URL extracting, and data-URL embedding.
*
* @file
* @version 0.1.1 -- 2010-09-11
@@ -25,6 +21,12 @@
* @copyright Copyright 2010 Wikimedia Foundation
* @license http://www.apache.org/licenses/LICENSE-2.0
*/
+
+/**
+ * Transforms CSS data
+ *
+ * This class provides minification, URL remapping, URL extracting, and data-URL embedding.
+ */
class CSSMin {
/* Constants */
@@ -150,6 +152,13 @@ class CSSMin {
$offset = $match[0][1] + strlen( $match[0][0] ) + $lengthIncrease;
continue;
}
+
+ // Guard against double slashes, because "some/remote/../foo.png"
+ // resolves to "some/remote/foo.png" on (some?) clients (bug 27052).
+ if ( substr( $remote, -1 ) == '/' ) {
+ $remote = substr( $remote, 0, -1 );
+ }
+
// Shortcuts
$embed = $match['embed'][0];
$pre = $match['pre'][0];
@@ -157,10 +166,9 @@ class CSSMin {
$query = $match['query'][0];
$url = "{$remote}/{$match['file'][0]}";
$file = "{$local}/{$match['file'][0]}";
- // bug 27052 - Guard against double slashes, because foo//../bar
- // apparently resolves to foo/bar on (some?) clients
- $url = preg_replace( '#([^:])//+#', '\1/', $url );
+
$replacement = false;
+
if ( $local !== false && file_exists( $file ) ) {
// Add version parameter as a time-stamp in ISO 8601 format,
// using Z for the timezone, meaning GMT
diff --git a/includes/libs/GenericArrayObject.php b/includes/libs/GenericArrayObject.php
new file mode 100644
index 00000000..b4b9d610
--- /dev/null
+++ b/includes/libs/GenericArrayObject.php
@@ -0,0 +1,244 @@
+<?php
+
+/**
+ * Extends ArrayObject and does two things:
+ *
+ * Allows for deriving classes to easily intercept additions
+ * and deletions for purposes such as additional indexing.
+ *
+ * Enforces the objects to be of a certain type, so this
+ * can be replied upon, much like if this had true support
+ * for generics, which sadly enough is not possible in 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
+ *
+ * @since 1.20
+ *
+ * @file
+ * @ingroup Diff
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class GenericArrayObject extends ArrayObject {
+
+ /**
+ * Returns the name of an interface/class that the element should implement/extend.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public abstract function getObjectType();
+
+ /**
+ * @see SiteList::getNewOffset()
+ * @since 1.20
+ * @var integer
+ */
+ protected $indexOffset = 0;
+
+ /**
+ * Finds a new offset for when appending an element.
+ * The base class does this, so it would be better to integrate,
+ * but there does not appear to be any way to do this...
+ *
+ * @since 1.20
+ *
+ * @return integer
+ */
+ protected function getNewOffset() {
+ while ( true ) {
+ if ( !$this->offsetExists( $this->indexOffset ) ) {
+ return $this->indexOffset;
+ }
+
+ $this->indexOffset++;
+ }
+ }
+
+ /**
+ * Constructor.
+ * @see ArrayObject::__construct
+ *
+ * @since 1.20
+ *
+ * @param null|array $input
+ * @param int $flags
+ * @param string $iterator_class
+ */
+ public function __construct( $input = null, $flags = 0, $iterator_class = 'ArrayIterator' ) {
+ parent::__construct( array(), $flags, $iterator_class );
+
+ if ( !is_null( $input ) ) {
+ foreach ( $input as $offset => $value ) {
+ $this->offsetSet( $offset, $value );
+ }
+ }
+ }
+
+ /**
+ * @see ArrayObject::append
+ *
+ * @since 1.20
+ *
+ * @param mixed $value
+ */
+ public function append( $value ) {
+ $this->setElement( null, $value );
+ }
+
+ /**
+ * @see ArrayObject::offsetSet()
+ *
+ * @since 1.20
+ *
+ * @param mixed $index
+ * @param mixed $value
+ */
+ public function offsetSet( $index, $value ) {
+ $this->setElement( $index, $value );
+ }
+
+ /**
+ * Returns if the provided value has the same type as the elements
+ * that can be added to this ArrayObject.
+ *
+ * @since 1.20
+ *
+ * @param mixed $value
+ *
+ * @return boolean
+ */
+ protected function hasValidType( $value ) {
+ $class = $this->getObjectType();
+ return $value instanceof $class;
+ }
+
+ /**
+ * Method that actually sets the element and holds
+ * all common code needed for set operations, including
+ * type checking and offset resolving.
+ *
+ * If you want to do additional indexing or have code that
+ * otherwise needs to be executed whenever an element is added,
+ * you can overload @see preSetElement.
+ *
+ * @since 1.20
+ *
+ * @param mixed $index
+ * @param mixed $value
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function setElement( $index, $value ) {
+ if ( !$this->hasValidType( $value ) ) {
+ throw new InvalidArgumentException(
+ 'Can only add ' . $this->getObjectType() . ' implementing objects to ' . get_called_class() . '.'
+ );
+ }
+
+ if ( is_null( $index ) ) {
+ $index = $this->getNewOffset();
+ }
+
+ if ( $this->preSetElement( $index, $value ) ) {
+ parent::offsetSet( $index, $value );
+ }
+ }
+
+ /**
+ * Gets called before a new element is added to the ArrayObject.
+ *
+ * At this point the index is always set (ie not null) and the
+ * value is always of the type returned by @see getObjectType.
+ *
+ * Should return a boolean. When false is returned the element
+ * does not get added to the ArrayObject.
+ *
+ * @since 1.20
+ *
+ * @param integer|string $index
+ * @param mixed $value
+ *
+ * @return boolean
+ */
+ protected function preSetElement( $index, $value ) {
+ return true;
+ }
+
+ /**
+ * @see Serializable::serialize
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public function serialize() {
+ return serialize( $this->getSerializationData() );
+ }
+
+ /**
+ * 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.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ protected function getSerializationData() {
+ return array(
+ 'data' => $this->getArrayCopy(),
+ 'index' => $this->indexOffset,
+ );
+ }
+
+ /**
+ * @see Serializable::unserialize
+ *
+ * @since 1.20
+ *
+ * @param string $serialization
+ *
+ * @return array
+ */
+ public function unserialize( $serialization ) {
+ $serializationData = unserialize( $serialization );
+
+ foreach ( $serializationData['data'] as $offset => $value ) {
+ // Just set the element, bypassing checks and offset resolving,
+ // as these elements have already gone through this.
+ parent::offsetSet( $offset, $value );
+ }
+
+ $this->indexOffset = $serializationData['index'];
+
+ return $serializationData;
+ }
+
+ /**
+ * Returns if the ArrayObject has no elements.
+ *
+ * @since 1.20
+ *
+ * @return boolean
+ */
+ public function isEmpty() {
+ return $this->count() === 0;
+ }
+
+}
diff --git a/includes/libs/HttpStatus.php b/includes/libs/HttpStatus.php
index 2985c652..78d81803 100644
--- a/includes/libs/HttpStatus.php
+++ b/includes/libs/HttpStatus.php
@@ -1,5 +1,26 @@
<?php
/**
+ * List of HTTP status codes.
+ *
+ * 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 HttpStatus {
diff --git a/includes/libs/IEContentAnalyzer.php b/includes/libs/IEContentAnalyzer.php
index 01e72e68..cfc7f536 100644
--- a/includes/libs/IEContentAnalyzer.php
+++ b/includes/libs/IEContentAnalyzer.php
@@ -1,4 +1,10 @@
<?php
+/**
+ * Simulation of Microsoft Internet Explorer's MIME type detection algorithm.
+ *
+ * @file
+ * @todo Define the exact license of this file.
+ */
/**
* This class simulates Microsoft Internet Explorer's terribly broken and
diff --git a/includes/libs/IEUrlExtension.php b/includes/libs/IEUrlExtension.php
index e00e6663..e9cfa997 100644
--- a/includes/libs/IEUrlExtension.php
+++ b/includes/libs/IEUrlExtension.php
@@ -1,4 +1,24 @@
<?php
+/**
+ * Checks for validity of requested URL's extension.
+ *
+ * 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
+ */
/**
* Internet Explorer derives a cache filename from a URL, and then in certain
@@ -35,8 +55,8 @@ class IEUrlExtension {
*
* If the a variable is unset in $_SERVER, it should be unset in $vars.
*
- * @param $vars A subset of $_SERVER.
- * @param $extWhitelist Extensions which are allowed, assumed harmless.
+ * @param $vars array A subset of $_SERVER.
+ * @param $extWhitelist array Extensions which are allowed, assumed harmless.
* @return bool
*/
public static function areServerVarsBad( $vars, $extWhitelist = array() ) {
@@ -73,7 +93,7 @@ class IEUrlExtension {
* a potentially harmful file extension.
*
* @param $urlPart string The right-hand portion of a URL
- * @param $extWhitelist An array of file extensions which may occur in this
+ * @param $extWhitelist array An array of file extensions which may occur in this
* URL, and which should be allowed.
* @return bool
*/
diff --git a/includes/libs/JavaScriptMinifier.php b/includes/libs/JavaScriptMinifier.php
index baf93385..0b4be9ae 100644
--- a/includes/libs/JavaScriptMinifier.php
+++ b/includes/libs/JavaScriptMinifier.php
@@ -2,17 +2,19 @@
/**
* JavaScript Minifier
*
+ * @file
+ * @author Paul Copperman <paul.copperman@gmail.com>
+ * @license Choose any of Apache, MIT, GPL, LGPL
+ */
+
+/**
* This class is meant to safely minify javascript code, while leaving syntactically correct
* programs intact. Other libraries, such as JSMin require a certain coding style to work
* correctly. OTOH, libraries like jsminplus, that do parse the code correctly are rather
* slow, because they construct a complete parse tree before outputting the code minified.
* So this class is meant to allow arbitrary (but syntactically correct) input, while being
* fast enough to be used for on-the-fly minifying.
- *
- * Author: Paul Copperman <paul.copperman@gmail.com>
- * License: choose any of Apache, MIT, GPL, LGPL
*/
-
class JavaScriptMinifier {
/* Class constants */
diff --git a/includes/libs/jsminplus.php b/includes/libs/jsminplus.php
index 8ed08d74..7c4e32bd 100644
--- a/includes/libs/jsminplus.php
+++ b/includes/libs/jsminplus.php
@@ -1,5 +1,4 @@
<?php
-
/**
* JSMinPlus version 1.4
*
@@ -25,6 +24,7 @@
*
* Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
*
+ * @file
*/
/* ***** BEGIN LICENSE BLOCK *****
diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php
index 4aa6a826..37560d80 100644
--- a/includes/logging/LogEntry.php
+++ b/includes/logging/LogEntry.php
@@ -7,6 +7,21 @@
* - formatting log entries based on database fields
* - user is now part of the action message
*
+ * 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
@@ -97,6 +112,7 @@ abstract class LogEntryBase implements LogEntry {
/**
* Whether the parameters for this log are stored in new or
* old format.
+ * @return bool
*/
public function isLegacy() {
return false;
@@ -344,7 +360,7 @@ class ManualLogEntry extends LogEntryBase {
*
* @since 1.19
*
- * @param $parameters Associative array
+ * @param $parameters array Associative array
*/
public function setParameters( $parameters ) {
$this->parameters = $parameters;
@@ -431,7 +447,7 @@ class ManualLogEntry extends LogEntryBase {
'log_user_text' => $this->getPerformer()->getName(),
'log_namespace' => $this->getTarget()->getNamespace(),
'log_title' => $this->getTarget()->getDBkey(),
- 'log_page' => $this->getTarget()->getArticleId(),
+ 'log_page' => $this->getTarget()->getArticleID(),
'log_comment' => $comment,
'log_params' => serialize( (array) $this->getParameters() ),
);
@@ -457,18 +473,29 @@ class ManualLogEntry extends LogEntryBase {
$logpage = SpecialPage::getTitleFor( 'Log', $this->getType() );
$user = $this->getPerformer();
+ $ip = "";
+ if ( $user->isAnon() ) {
+ /*
+ * "MediaWiki default" and friends may have
+ * no IP address in their name
+ */
+ if ( IP::isIPAddress( $user->getName() ) ) {
+ $ip = $user->getName();
+ }
+ }
$rc = RecentChange::newLogEntry(
$this->getTimestamp(),
$logpage,
$user,
- $formatter->getIRCActionText(), // Used for IRC feeds
- $user->isAnon() ? $user->getName() : '',
+ $formatter->getPlainActionText(),
+ $ip,
$this->getType(),
$this->getSubtype(),
$this->getTarget(),
$this->getComment(),
serialize( (array) $this->getParameters() ),
- $newId
+ $newId,
+ $formatter->getIRCActionComment() // Used for IRC feeds
);
if ( $to === 'rc' || $to === 'rcandudp' ) {
@@ -494,10 +521,16 @@ class ManualLogEntry extends LogEntryBase {
return $this->parameters;
}
+ /**
+ * @return User
+ */
public function getPerformer() {
return $this->performer;
}
+ /**
+ * @return Title
+ */
public function getTarget() {
return $this->target;
}
diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php
index 437670d0..4de1a974 100644
--- a/includes/logging/LogEventsList.php
+++ b/includes/logging/LogEventsList.php
@@ -23,52 +23,47 @@
* @file
*/
-class LogEventsList {
+class LogEventsList extends ContextSource {
const NO_ACTION_LINK = 1;
const NO_EXTRA_USER_LINKS = 2;
+ const USE_REVDEL_CHECKBOXES = 4;
- /**
- * @var Skin
- */
- private $skin;
-
- /**
- * @var OutputPage
- */
- private $out;
public $flags;
/**
* @var Array
*/
- protected $message;
+ protected $mDefaultQuery;
/**
- * @var Array
+ * Constructor.
+ * The first two parameters used to be $skin and $out, but now only a context
+ * is needed, that's why there's a second unused parameter.
+ *
+ * @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,
+ * self::NO_EXTRA_USER_LINKS or self::USE_REVDEL_CHECKBOXES.
*/
- protected $mDefaultQuery;
+ public function __construct( $context, $unused = null, $flags = 0 ) {
+ if ( $context instanceof IContextSource ) {
+ $this->setContext( $context );
+ } else {
+ // Old parameters, $context should be a Skin object
+ $this->setContext( $context->getContext() );
+ }
- public function __construct( $skin, $out, $flags = 0 ) {
- $this->skin = $skin;
- $this->out = $out;
$this->flags = $flags;
- $this->preCacheMessages();
}
/**
- * As we use the same small set of messages in various methods and that
- * they are called often, we call them once and save them in $this->message
+ * Deprecated alias for getTitle(); do not use.
+ *
+ * @deprecated in 1.20; use getTitle() instead.
+ * @return Title object
*/
- private function preCacheMessages() {
- // Precache various messages
- if( !isset( $this->message ) ) {
- $messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink',
- 'revertmove', 'undeletelink', 'undeleteviewlink', 'revdel-restore', 'hist', 'diff',
- 'pipe-separator', 'revdel-restore-deleted', 'revdel-restore-visible' );
- foreach( $messages as $msg ) {
- $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
- }
- }
+ public function getDisplayTitle() {
+ return $this->getTitle();
}
/**
@@ -80,12 +75,13 @@ class LogEventsList {
wfDeprecated( __METHOD__, '1.19' );
// If only one log type is used, then show a special message...
$headerType = (count($type) == 1) ? $type[0] : '';
+ $out = $this->getOutput();
if( LogPage::isLogType( $headerType ) ) {
$page = new LogPage( $headerType );
- $this->out->setPageTitle( $page->getName()->text() );
- $this->out->addHTML( $page->getDescription()->parseAsBlock() );
+ $out->setPageTitle( $page->getName()->text() );
+ $out->addHTML( $page->getDescription()->parseAsBlock() );
} else {
- $this->out->addHTML( wfMsgExt('alllogstext',array('parseinline')) );
+ $out->addHTML( $this->msg( 'alllogstext' )->parse() );
}
}
@@ -105,16 +101,14 @@ class LogEventsList {
$month = '', $filter = null, $tagFilter='' ) {
global $wgScript, $wgMiserMode;
- $action = $wgScript;
$title = SpecialPage::getTitleFor( 'Log' );
- $special = $title->getPrefixedDBkey();
// For B/C, we take strings, but make sure they are converted...
$types = ($types === '') ? array() : (array)$types;
$tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
- $html = Html::hidden( 'title', $special );
+ $html = Html::hidden( 'title', $title->getPrefixedDBkey() );
// Basic selectors
$html .= $this->getTypeMenu( $types ) . "\n";
@@ -141,15 +135,15 @@ class LogEventsList {
}
// Submit button
- $html .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+ $html .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() );
// Fieldset
- $html = Xml::fieldset( wfMsg( 'log' ), $html );
+ $html = Xml::fieldset( $this->msg( 'log' )->text(), $html );
// Form wrapping
- $html = Xml::tags( 'form', array( 'action' => $action, 'method' => 'get' ), $html );
+ $html = Xml::tags( 'form', array( 'action' => $wgScript, 'method' => 'get' ), $html );
- $this->out->addHTML( $html );
+ $this->getOutput()->addHTML( $html );
}
/**
@@ -157,9 +151,8 @@ class LogEventsList {
* @return String: Formatted HTML
*/
private function getFilterLinks( $filter ) {
- global $wgLang;
// show/hide links
- $messages = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
+ $messages = array( $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() );
// Option value -> message mapping
$links = array();
$hiddens = ''; // keep track for "go" button
@@ -172,26 +165,23 @@ class LogEventsList {
$hideVal = 1 - intval($val);
$query[$queryKey] = $hideVal;
- $link = Linker::link(
- $this->getDisplayTitle(),
+ $link = Linker::linkKnown(
+ $this->getTitle(),
$messages[$hideVal],
array(),
- $query,
- array( 'known', 'noclasses' )
+ $query
);
- $links[$type] = wfMsgHtml( "log-show-hide-{$type}", $link );
+ $links[$type] = $this->msg( "log-show-hide-{$type}" )->rawParams( $link )->escaped();
$hiddens .= Html::hidden( "hide_{$type}_log", $val ) . "\n";
}
// Build links
- return '<small>'.$wgLang->pipeList( $links ) . '</small>' . $hiddens;
+ return '<small>'.$this->getLanguage()->pipeList( $links ) . '</small>' . $hiddens;
}
private function getDefaultQuery() {
- global $wgRequest;
-
if ( !isset( $this->mDefaultQuery ) ) {
- $this->mDefaultQuery = $wgRequest->getQueryValues();
+ $this->mDefaultQuery = $this->getRequest()->getQueryValues();
unset( $this->mDefaultQuery['title'] );
unset( $this->mDefaultQuery['dir'] );
unset( $this->mDefaultQuery['offset'] );
@@ -204,20 +194,6 @@ class LogEventsList {
}
/**
- * Get the Title object of the page the links should point to.
- * This is NOT the Title of the page the entries should be restricted to.
- *
- * @return Title object
- */
- public function getDisplayTitle() {
- return $this->out->getTitle();
- }
-
- public function getContext() {
- return $this->out->getContext();
- }
-
- /**
* @param $queryTypes Array
* @return String: Formatted HTML
*/
@@ -234,14 +210,12 @@ class LogEventsList {
* @since 1.19
*/
public function getTypeSelector() {
- global $wgUser;
-
$typesByName = array(); // Temporary array
// First pass to load the log names
foreach( LogPage::validTypes() as $type ) {
$page = new LogPage( $type );
$restriction = $page->getRestriction();
- if ( $wgUser->isAllowed( $restriction ) ) {
+ if ( $this->getUser()->isAllowed( $restriction ) ) {
$typesByName[$type] = $page->getName()->text();
}
}
@@ -268,7 +242,7 @@ class LogEventsList {
*/
private function getUserInput( $user ) {
return '<span style="white-space: nowrap">' .
- Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user ) .
+ Xml::inputLabel( $this->msg( 'specialloguserlabel' )->text(), 'user', 'mw-log-user', 15, $user ) .
'</span>';
}
@@ -278,7 +252,7 @@ class LogEventsList {
*/
private function getTitleInput( $title ) {
return '<span style="white-space: nowrap">' .
- Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title ) .
+ Xml::inputLabel( $this->msg( 'speciallogtitlelabel' )->text(), 'page', 'mw-log-page', 20, $title ) .
'</span>';
}
@@ -288,7 +262,7 @@ class LogEventsList {
*/
private function getTitlePattern( $pattern ) {
return '<span style="white-space: nowrap">' .
- Xml::checkLabel( wfMsg( 'log-title-wildcard' ), 'pattern', 'pattern', $pattern ) .
+ Xml::checkLabel( $this->msg( 'log-title-wildcard' )->text(), 'pattern', 'pattern', $pattern ) .
'</span>';
}
@@ -297,14 +271,13 @@ class LogEventsList {
* @return string
*/
private function getExtraInputs( $types ) {
- global $wgRequest;
- $offender = $wgRequest->getVal('offender');
+ $offender = $this->getRequest()->getVal( 'offender' );
$user = User::newFromName( $offender, false );
if( !$user || ($user->getId() == 0 && !IP::isIPAddress($offender) ) ) {
$offender = ''; // Blank field if invalid
}
if( count($types) == 1 && $types[0] == 'suppress' ) {
- return Xml::inputLabel( wfMsg('revdelete-offender'), 'offender',
+ return Xml::inputLabel( $this->msg( 'revdelete-offender' )->text(), 'offender',
'mw-log-offender', 20, $offender );
}
return '';
@@ -331,169 +304,38 @@ class LogEventsList {
public function logLine( $row ) {
$entry = DatabaseLogEntry::newFromRow( $row );
$formatter = LogFormatter::newFromEntry( $entry );
+ $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() ) );
+
$action = $formatter->getActionText();
- $comment = $formatter->getComment();
- $classes = array( 'mw-logline-' . $entry->getType() );
- $title = $entry->getTarget();
- $time = $this->logTimestamp( $entry );
+ if ( $this->flags & self::NO_ACTION_LINK ) {
+ $revert = '';
+ } else {
+ $revert = $formatter->getActionLinks();
+ if ( $revert != '' ) {
+ $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
+ }
+ }
- // Extract extra parameters
- $paramArray = LogPage::extractParams( $row->log_params );
- // Add review/revert links and such...
- $revert = $this->logActionLinks( $row, $title, $paramArray, $comment );
+ $comment = $formatter->getComment();
// Some user can hide log items and have review links
$del = $this->getShowHideLinks( $row );
- if( $del != '' ) $del .= ' ';
// Any tags...
list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' );
- $classes = array_merge( $classes, $newClasses );
-
- return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ),
- $del . "$time $action $comment $revert $tagDisplay" ) . "\n";
- }
-
- private function logTimestamp( LogEntry $entry ) {
- global $wgLang;
- $time = $wgLang->timeanddate( wfTimestamp( TS_MW, $entry->getTimestamp() ), true );
- return htmlspecialchars( $time );
- }
+ $classes = array_merge(
+ array( 'mw-logline-' . $entry->getType() ),
+ $newClasses
+ );
- /**
- * @TODO: split up!
- *
- * @param $row
- * @param Title $title
- * @param Array $paramArray
- * @param $comment
- * @return String
- */
- private function logActionLinks( $row, $title, $paramArray, &$comment ) {
- global $wgUser;
- if( ( $this->flags & self::NO_ACTION_LINK ) // we don't want to see the action
- || self::isDeleted( $row, LogPage::DELETED_ACTION ) ) // action is hidden
- {
- return '';
- }
- $revert = '';
- if( self::typeAction( $row, 'move', 'move', 'move' ) && !empty( $paramArray[0] ) ) {
- $destTitle = Title::newFromText( $paramArray[0] );
- if( $destTitle ) {
- $revert = '(' . Linker::link(
- SpecialPage::getTitleFor( 'Movepage' ),
- $this->message['revertmove'],
- array(),
- array(
- 'wpOldTitle' => $destTitle->getPrefixedDBkey(),
- 'wpNewTitle' => $title->getPrefixedDBkey(),
- 'wpReason' => wfMsgForContent( 'revertmove' ),
- 'wpMovetalk' => 0
- ),
- array( 'known', 'noclasses' )
- ) . ')';
- }
- // Show undelete link
- } elseif( self::typeAction( $row, array( 'delete', 'suppress' ), 'delete', 'deletedhistory' ) ) {
- if( !$wgUser->isAllowed( 'undelete' ) ) {
- $viewdeleted = $this->message['undeleteviewlink'];
- } else {
- $viewdeleted = $this->message['undeletelink'];
- }
- $revert = '(' . Linker::link(
- SpecialPage::getTitleFor( 'Undelete' ),
- $viewdeleted,
- array(),
- array( 'target' => $title->getPrefixedDBkey() ),
- array( 'known', 'noclasses' )
- ) . ')';
- // Show unblock/change block link
- } elseif( self::typeAction( $row, array( 'block', 'suppress' ), array( 'block', 'reblock' ), 'block' ) ) {
- $revert = '(' .
- Linker::link(
- SpecialPage::getTitleFor( 'Unblock', $row->log_title ),
- $this->message['unblocklink'],
- array(),
- array(),
- 'known'
- ) .
- $this->message['pipe-separator'] .
- Linker::link(
- SpecialPage::getTitleFor( 'Block', $row->log_title ),
- $this->message['change-blocklink'],
- array(),
- array(),
- 'known'
- ) .
- ')';
- // Show change protection link
- } elseif( self::typeAction( $row, 'protect', array( 'modify', 'protect', 'unprotect' ) ) ) {
- $revert .= ' (' .
- Linker::link( $title,
- $this->message['hist'],
- array(),
- array(
- 'action' => 'history',
- 'offset' => $row->log_timestamp
- )
- );
- if( $wgUser->isAllowed( 'protect' ) ) {
- $revert .= $this->message['pipe-separator'] .
- Linker::link( $title,
- $this->message['protect_change'],
- array(),
- array( 'action' => 'protect' ),
- 'known' );
- }
- $revert .= ')';
- // Show unmerge link
- } elseif( self::typeAction( $row, 'merge', 'merge', 'mergehistory' ) ) {
- $revert = '(' . Linker::link(
- SpecialPage::getTitleFor( 'MergeHistory' ),
- $this->message['revertmerge'],
- array(),
- array(
- 'target' => $paramArray[0],
- 'dest' => $title->getPrefixedDBkey(),
- 'mergepoint' => $paramArray[1]
- ),
- array( 'known', 'noclasses' )
- ) . ')';
- // If an edit was hidden from a page give a review link to the history
- } elseif( self::typeAction( $row, array( 'delete', 'suppress' ), 'revision', 'deletedhistory' ) ) {
- $revert = RevisionDeleter::getLogLinks( $title, $paramArray,
- $this->message );
- // Hidden log items, give review link
- } elseif( self::typeAction( $row, array( 'delete', 'suppress' ), 'event', 'deletedhistory' ) ) {
- if( count($paramArray) >= 1 ) {
- $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
- // $paramArray[1] is a CSV of the IDs
- $query = $paramArray[0];
- // Link to each hidden object ID, $paramArray[1] is the url param
- $revert = '(' . Linker::link(
- $revdel,
- $this->message['revdel-restore'],
- array(),
- array(
- 'target' => $title->getPrefixedText(),
- 'type' => 'logging',
- 'ids' => $query
- ),
- array( 'known', 'noclasses' )
- ) . ')';
- }
- // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters.
- } else {
- wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray,
- &$comment, &$revert, $row->log_timestamp ) );
- }
- if( $revert != '' ) {
- $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
- }
- return $revert;
+ return Html::rawElement( 'li', array( 'class' => $classes ),
+ "$del $time $action $comment $revert $tagDisplay" ) . "\n";
}
/**
@@ -501,28 +343,33 @@ class LogEventsList {
* @return string
*/
private function getShowHideLinks( $row ) {
- global $wgUser;
- 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 = '';
- // Don't show useless link to people who cannot hide revisions
- if( $wgUser->isAllowed( 'deletedhistory' ) ) {
- if( $row->log_deleted || $wgUser->isAllowed( 'deleterevision' ) ) {
- $canHide = $wgUser->isAllowed( 'deleterevision' );
- // If event was hidden from sysops
- if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) {
- $del = Linker::revDeleteLinkDisabled( $canHide );
+ $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 ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) { // If event was hidden from sysops
+ $del = Xml::check( 'deleterevisions', false, array( 'disabled' => 'disabled' ) );
+ } else {
+ $del = Xml::check( 'showhiderevisions', false, array( 'name' => 'ids[' . $row->log_id . ']' ) );
+ }
} else {
- $target = SpecialPage::getTitleFor( 'Log', $row->log_type );
- $query = array(
- 'target' => $target->getPrefixedDBkey(),
- 'type' => 'logging',
- 'ids' => $row->log_id,
- );
- $del = Linker::revDeleteLink( $query,
- self::isDeleted( $row, LogPage::DELETED_RESTRICTED ), $canHide );
+ if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) { // If event was hidden from sysops
+ $del = Linker::revDeleteLinkDisabled( $canHide );
+ } else {
+ $query = array(
+ 'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
+ 'type' => 'logging',
+ 'ids' => $row->log_id,
+ );
+ $del = Linker::revDeleteLink( $query, self::isDeleted( $row, LogPage::DELETED_RESTRICTED ), $canHide );
+ }
}
}
}
@@ -606,15 +453,15 @@ class LogEventsList {
* @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 Associative Array with the following additional options:
+ * @param $param array Associative Array with the following additional options:
* - lim Integer Limit of items to show, default is 50
* - conds Array Extra conditions for the query (e.g. "log_action != 'revision'")
* - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty
* if set to true (default), "No matching items in log" is displayed if loglist is empty
* - msgKey Array If you want a nice box with a message, set this to the key of the message.
* First element is the message key, additional optional elements are parameters for the key
- * that are processed with wfMsgExt and option 'parse'
- * - offset Set to overwrite offset parameter in $wgRequest
+ * that are processed with wfMessage
+ * - offset Set to overwrite offset parameter in WebRequest
* 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)
@@ -652,9 +499,9 @@ class LogEventsList {
}
# Insert list of top 50 (or top $lim) items
- $loglist = new LogEventsList( $context->getSkin(), $context->getOutput(), $flags );
+ $loglist = new LogEventsList( $context, null, $flags );
$pager = new LogPager( $loglist, $types, $user, $page, '', $conds );
- if ( isset( $param['offset'] ) ) { # Tell pager to ignore $wgRequest offset
+ if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
$pager->setOffset( $param['offset'] );
}
if( $lim > 0 ) $pager->mLimit = $lim;
@@ -665,11 +512,11 @@ class LogEventsList {
$s = '<div class="mw-warning-with-logexcerpt">';
if ( count( $msgKey ) == 1 ) {
- $s .= wfMsgExt( $msgKey[0], array( 'parse' ) );
+ $s .= $context->msg( $msgKey[0] )->parseAsBlock();
} else { // Process additional arguments
$args = $msgKey;
array_shift( $args );
- $s .= wfMsgExt( $msgKey[0], array( 'parse' ), $args );
+ $s .= $context->msg( $msgKey[0], $args )->parseAsBlock();
}
}
$s .= $loglist->beginLogEventsList() .
@@ -678,7 +525,7 @@ class LogEventsList {
} else {
if ( $showIfEmpty ) {
$s = Html::rawElement( 'div', array( 'class' => 'mw-warning-logempty' ),
- wfMsgExt( 'logempty', array( 'parseinline' ) ) );
+ $context->msg( 'logempty' )->parse() );
}
}
if( $pager->getNumRows() > $pager->mLimit ) { # Show "Full log" link
@@ -697,7 +544,7 @@ class LogEventsList {
$urlParam['type'] = $types[0];
$s .= Linker::link(
SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'log-fulllog' ),
+ $context->msg( 'log-fulllog' )->escaped(),
array(),
$urlParam
);
diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php
index 24490eed..7586bb65 100644
--- a/includes/logging/LogFormatter.php
+++ b/includes/logging/LogFormatter.php
@@ -2,6 +2,21 @@
/**
* Contains classes for formatting 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
@@ -94,24 +109,24 @@ class LogFormatter {
/**
* Set the visibility restrictions for displaying content.
- * If set to public, and an item is deleted, then it will be replaced
+ * If set to public, and an item is deleted, then it will be replaced
* with a placeholder even if the context user is allowed to view it.
* @param $audience integer self::FOR_THIS_USER or self::FOR_PUBLIC
*/
public function setAudience( $audience ) {
$this->audience = ( $audience == self::FOR_THIS_USER )
- ? self::FOR_THIS_USER
+ ? self::FOR_THIS_USER
: self::FOR_PUBLIC;
}
/**
* Check if a log item can be displayed
* @param $field integer LogPage::DELETED_* constant
- * @return bool
+ * @return bool
*/
protected function canView( $field ) {
if ( $this->audience == self::FOR_THIS_USER ) {
- return LogEventsList::userCanBitfield(
+ return LogEventsList::userCanBitfield(
$this->entry->getDeleted(), $field, $this->context->getUser() );
} else {
return !$this->entry->isDeleted( $field );
@@ -148,14 +163,34 @@ class LogFormatter {
* @see getActionText()
* @return string text
*/
+ public function getIRCActionComment() {
+ $actionComment = $this->getIRCActionText();
+ $comment = $this->entry->getComment();
+
+ if ( $comment != '' ) {
+ if ( $actionComment == '' ) {
+ $actionComment = $comment;
+ } else {
+ $actionComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
+ }
+ }
+
+ return $actionComment;
+ }
+
+ /**
+ * Even uglier hack to maintain backwards compatibilty with IRC bots
+ * (bug 34508).
+ * @see getActionText()
+ * @return string text
+ */
public function getIRCActionText() {
$this->plaintext = true;
- $text = $this->getActionText();
+ $this->irctext = true;
$entry = $this->entry;
$parameters = $entry->getParameters();
// @see LogPage::actionText()
- $msgOpts = array( 'parsemag', 'escape', 'replaceafter', 'content' );
// Text of title the action is aimed at.
$target = $entry->getTarget()->getPrefixedText() ;
$text = null;
@@ -164,11 +199,13 @@ class LogFormatter {
switch( $entry->getSubtype() ) {
case 'move':
$movesource = $parameters['4::target'];
- $text = wfMsgExt( '1movedto2', $msgOpts, $target, $movesource );
+ $text = wfMessage( '1movedto2' )
+ ->rawParams( $target, $movesource )->inContentLanguage()->escaped();
break;
case 'move_redir':
$movesource = $parameters['4::target'];
- $text = wfMsgExt( '1movedto2_redir', $msgOpts, $target, $movesource );
+ $text = wfMessage( '1movedto2_redir' )
+ ->rawParams( $target, $movesource )->inContentLanguage()->escaped();
break;
case 'move-noredirect':
break;
@@ -180,10 +217,12 @@ class LogFormatter {
case 'delete':
switch( $entry->getSubtype() ) {
case 'delete':
- $text = wfMsgExt( 'deletedarticle', $msgOpts, $target );
+ $text = wfMessage( 'deletedarticle' )
+ ->rawParams( $target )->inContentLanguage()->escaped();
break;
case 'restore':
- $text = wfMsgExt( 'undeletedarticle', $msgOpts, $target );
+ $text = wfMessage( 'undeletedarticle' )
+ ->rawParams( $target )->inContentLanguage()->escaped();
break;
//case 'revision': // Revision deletion
//case 'event': // Log deletion
@@ -197,24 +236,46 @@ class LogFormatter {
// Create a diff link to the patrolled revision
if ( $entry->getSubtype() === 'patrol' ) {
$diffLink = htmlspecialchars(
- wfMsgForContent( 'patrol-log-diff', $parameters['4::curid'] ) );
- $text = wfMsgForContent( 'patrol-log-line', $diffLink, "[[$target]]", "" );
+ wfMessage( 'patrol-log-diff', $parameters['4::curid'] )
+ ->inContentLanguage()->text() );
+ $text = wfMessage( 'patrol-log-line', $diffLink, "[[$target]]", "" )
+ ->inContentLanguage()->text();
} else {
// broken??
}
break;
+ case 'protect':
+ switch( $entry->getSubtype() ) {
+ case 'protect':
+ $text = wfMessage( 'protectedarticle' )
+ ->rawParams( $target . ' ' . $parameters[0] )->inContentLanguage()->escaped();
+ break;
+ case 'unprotect':
+ $text = wfMessage( 'unprotectedarticle' )
+ ->rawParams( $target )->inContentLanguage()->escaped();
+ break;
+ case 'modify':
+ $text = wfMessage( 'modifiedarticleprotection' )
+ ->rawParams( $target . ' ' . $parameters[0] )->inContentLanguage()->escaped();
+ break;
+ }
+ break;
+
case 'newusers':
switch( $entry->getSubtype() ) {
case 'newusers':
case 'create':
- $text = wfMsgExt( 'newuserlog-create-entry', $msgOpts /* no params */ );
+ $text = wfMessage( 'newuserlog-create-entry' )
+ ->inContentLanguage()->escaped();
break;
case 'create2':
- $text = wfMsgExt( 'newuserlog-create2-entry', $msgOpts, $target );
+ $text = wfMessage( 'newuserlog-create2-entry' )
+ ->rawParams( $target )->inContentLanguage()->escaped();
break;
case 'autocreate':
- $text = wfMsgExt( 'newuserlog-autocreate-entry', $msgOpts /* no params */ );
+ $text = wfMessage( 'newuserlog-autocreate-entry' )
+ ->inContentLanguage()->escaped();
break;
}
break;
@@ -222,14 +283,17 @@ class LogFormatter {
case 'upload':
switch( $entry->getSubtype() ) {
case 'upload':
- $text = wfMsgExt( 'uploadedimage', $msgOpts, $target );
+ $text = wfMessage( 'uploadedimage' )
+ ->rawParams( $target )->inContentLanguage()->escaped();
break;
case 'overwrite':
- $text = wfMsgExt( 'overwroteimage', $msgOpts, $target );
+ $text = wfMessage( 'overwroteimage' )
+ ->rawParams( $target )->inContentLanguage()->escaped();
break;
}
break;
+
// case 'suppress' --private log -- aaron (sign your messages so we know who to blame in a few years :-D)
// default:
}
@@ -238,6 +302,7 @@ class LogFormatter {
}
$this->plaintext = false;
+ $this->irctext = false;
return $text;
}
@@ -266,7 +331,7 @@ class LogFormatter {
* Returns a sentence describing the log action. Usually
* a Message object is returned, but old style log types
* and entries might return pre-escaped html string.
- * @return Message|pre-escaped html
+ * @return Message|string pre-escaped html
*/
protected function getActionMessage() {
$message = $this->msg( $this->getMessageKey() );
@@ -289,6 +354,15 @@ class LogFormatter {
}
/**
+ * Returns extra links that comes after the action text, like "revert", etc.
+ *
+ * @return string
+ */
+ public function getActionLinks() {
+ return '';
+ }
+
+ /**
* Extracts the optional extra parameters for use in action messages.
* The array indexes start from number 3.
* @return array
@@ -373,6 +447,7 @@ class LogFormatter {
* Provides the name of the user who performed the log action.
* Used as part of log action message or standalone, depending
* which parts of the log entry has been hidden.
+ * @return String
*/
public function getPerformerElement() {
if ( $this->canView( LogPage::DELETED_USER ) ) {
@@ -442,9 +517,7 @@ class LogFormatter {
* @return Message
*/
protected function msg( $key ) {
- return wfMessage( $key )
- ->inLanguage( $this->context->getLanguage() )
- ->title( $this->context->getTitle() );
+ return $this->context->msg( $key );
}
protected function makeUserLink( User $user ) {
@@ -457,11 +530,9 @@ class LogFormatter {
);
if ( $this->linkFlood ) {
- $element .= Linker::userToolLinks(
+ $element .= Linker::userToolLinksRedContribs(
$user->getId(),
$user->getName(),
- true, // Red if no edits
- 0, // Flags
$user->getEditCount()
);
}
@@ -488,6 +559,41 @@ class LogFormatter {
* @since 1.19
*/
class LegacyLogFormatter extends LogFormatter {
+
+ /**
+ * Backward compatibility for extension changing the comment from
+ * the LogLine hook. This will be set by the first call on getComment(),
+ * then it might be modified by the hook when calling getActionLinks(),
+ * so that the modified value will be returned when calling getComment()
+ * a second time.
+ *
+ * @var string|null
+ */
+ private $comment = null;
+
+ /**
+ * Cache for the result of getActionLinks() so that it does not need to
+ * run multiple times depending on the order that getComment() and
+ * getActionLinks() are called.
+ *
+ * @var string|null
+ */
+ private $revert = null;
+
+ public function getComment() {
+ if ( $this->comment === null ) {
+ $this->comment = parent::getComment();
+ }
+
+ // Make sure we execute the LogLine hook so that we immediately return
+ // the correct value.
+ if ( $this->revert === null ) {
+ $this->getActionLinks();
+ }
+
+ return $this->comment;
+ }
+
protected function getActionMessage() {
$entry = $this->entry;
$action = LogPage::actionText(
@@ -500,9 +606,104 @@ class LegacyLogFormatter extends LogFormatter {
);
$performer = $this->getPerformerElement();
- return $performer . $this->msg( 'word-separator' )->text() . $action;
+ if ( !$this->irctext ) {
+ $action = $performer . $this->msg( 'word-separator' )->text() . $action;
+ }
+
+ return $action;
}
+ public function getActionLinks() {
+ if ( $this->revert !== null ) {
+ return $this->revert;
+ }
+
+ if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
+ return $this->revert = '';
+ }
+
+ $title = $this->entry->getTarget();
+ $type = $this->entry->getType();
+ $subtype = $this->entry->getSubtype();
+
+ // Show unblock/change block link
+ if ( ( $type == 'block' || $type == 'suppress' ) && ( $subtype == 'block' || $subtype == 'reblock' ) ) {
+ if ( !$this->context->getUser()->isAllowed( 'block' ) ) {
+ return '';
+ }
+
+ $links = array(
+ Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Unblock', $title->getDBkey() ),
+ $this->msg( 'unblocklink' )->escaped()
+ ),
+ Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Block', $title->getDBkey() ),
+ $this->msg( 'change-blocklink' )->escaped()
+ )
+ );
+ return $this->msg( 'parentheses' )->rawParams(
+ $this->context->getLanguage()->pipeList( $links ) )->escaped();
+ // Show change protection link
+ } elseif ( $type == 'protect' && ( $subtype == 'protect' || $subtype == 'modify' || $subtype == 'unprotect' ) ) {
+ $links = array(
+ Linker::link( $title,
+ $this->msg( 'hist' )->escaped(),
+ array(),
+ array(
+ 'action' => 'history',
+ 'offset' => $this->entry->getTimestamp()
+ )
+ )
+ );
+ if ( $this->context->getUser()->isAllowed( 'protect' ) ) {
+ $links[] = Linker::linkKnown(
+ $title,
+ $this->msg( 'protect_change' )->escaped(),
+ array(),
+ array( 'action' => 'protect' )
+ );
+ }
+ return $this->msg( 'parentheses' )->rawParams(
+ $this->context->getLanguage()->pipeList( $links ) )->escaped();
+ // Show unmerge link
+ } elseif( $type == 'merge' && $subtype == 'merge' ) {
+ if ( !$this->context->getUser()->isAllowed( 'mergehistory' ) ) {
+ return '';
+ }
+
+ $params = $this->extractParameters();
+ $revert = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'MergeHistory' ),
+ $this->msg( 'revertmerge' )->escaped(),
+ array(),
+ array(
+ 'target' => $params[3],
+ 'dest' => $title->getPrefixedDBkey(),
+ 'mergepoint' => $params[4]
+ )
+ );
+ return $this->msg( 'parentheses' )->rawParams( $revert )->escaped();
+ }
+
+ // Do nothing. The implementation is handled by the hook modifiying the
+ // passed-by-ref parameters. This also changes the default value so that
+ // getComment() and getActionLinks() do not call them indefinitely.
+ $this->revert = '';
+
+ // This is to populate the $comment member of this instance so that it
+ // can be modified when calling the hook just below.
+ if ( $this->comment === null ) {
+ $this->getComment();
+ }
+
+ $params = $this->entry->getParameters();
+
+ wfRunHooks( 'LogLine', array( $type, $subtype, $title, $params,
+ &$this->comment, &$this->revert, $this->entry->getTimestamp() ) );
+
+ return $this->revert;
+ }
}
/**
@@ -532,6 +733,34 @@ class MoveLogFormatter extends LogFormatter {
$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();
+ }
}
/**
@@ -601,6 +830,107 @@ class DeleteLogFormatter extends LogFormatter {
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 '';
+ }
+ }
}
/**
@@ -619,7 +949,6 @@ class PatrolLogFormatter extends LogFormatter {
protected function getMessageParameters() {
$params = parent::getMessageParameters();
- $newParams = array_slice( $params, 0, 3 );
$target = $this->entry->getTarget();
$oldid = $params[3];
@@ -637,8 +966,8 @@ class PatrolLogFormatter extends LogFormatter {
$revlink = htmlspecialchars( $revision );
}
- $newParams[3] = Message::rawParam( $revlink );
- return $newParams;
+ $params[3] = Message::rawParam( $revlink );
+ return $params;
}
}
@@ -670,4 +999,12 @@ class NewUsersLogFormatter extends LogFormatter {
}
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 bbb4de8f..d96a5ea5 100644
--- a/includes/logging/LogPage.php
+++ b/includes/logging/LogPage.php
@@ -68,7 +68,7 @@ class LogPage {
}
/**
- * @return bool|int|null
+ * @return int log_id of the inserted log entry
*/
protected function saveContent() {
global $wgLogRestrictions;
@@ -86,7 +86,7 @@ class LogPage {
'log_user_text' => $this->doer->getName(),
'log_namespace' => $this->target->getNamespace(),
'log_title' => $this->target->getDBkey(),
- 'log_page' => $this->target->getArticleId(),
+ 'log_page' => $this->target->getArticleID(),
'log_comment' => $this->comment,
'log_params' => $this->params
);
@@ -100,12 +100,12 @@ class LogPage {
RecentChange::notifyLog(
$now, $titleObj, $this->doer, $this->getRcComment(), '',
$this->type, $this->action, $this->target, $this->comment,
- $this->params, $newId
+ $this->params, $newId, $this->getRcCommentIRC()
);
} elseif( $this->sendToUDP ) {
# Don't send private logs to UDP
if( isset( $wgLogRestrictions[$this->type] ) && $wgLogRestrictions[$this->type] != '*' ) {
- return true;
+ return $newId;
}
# Notify external application via UDP.
@@ -114,7 +114,7 @@ class LogPage {
$rc = RecentChange::newLogEntry(
$now, $titleObj, $this->doer, $this->getRcComment(), '',
$this->type, $this->action, $this->target, $this->comment,
- $this->params, $newId
+ $this->params, $newId, $this->getRcCommentIRC()
);
$rc->notifyRC2UDP();
}
@@ -133,7 +133,28 @@ class LogPage {
if ( $rcComment == '' ) {
$rcComment = $this->comment;
} else {
- $rcComment .= wfMsgForContent( 'colon-separator' ) . $this->comment;
+ $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() .
+ $this->comment;
+ }
+ }
+
+ return $rcComment;
+ }
+
+ /**
+ * Get the RC comment from the last addEntry() call for IRC
+ *
+ * @return string
+ */
+ public function getRcCommentIRC() {
+ $rcComment = $this->ircActionText;
+
+ if( $this->comment != '' ) {
+ if ( $rcComment == '' ) {
+ $rcComment = $this->comment;
+ } else {
+ $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() .
+ $this->comment;
}
}
@@ -175,11 +196,10 @@ class LogPage {
* @deprecated in 1.19, warnings in 1.21. Use getName()
*/
public static function logName( $type ) {
- wfDeprecated( __METHOD__, '1.19' );
global $wgLogNames;
if( isset( $wgLogNames[$type] ) ) {
- return str_replace( '_', ' ', wfMsg( $wgLogNames[$type] ) );
+ return str_replace( '_', ' ', wfMessage( $wgLogNames[$type] )->text() );
} else {
// Bogus log types? Perhaps an extension was removed.
return $type;
@@ -195,9 +215,8 @@ class LogPage {
* @deprecated in 1.19, warnings in 1.21. Use getDescription()
*/
public static function logHeader( $type ) {
- wfDeprecated( __METHOD__, '1.19' );
global $wgLogHeaders;
- return wfMsgExt( $wgLogHeaders[$type], array( 'parseinline' ) );
+ return wfMessage( $wgLogHeaders[$type] )->parse();
}
/**
@@ -230,17 +249,20 @@ class LogPage {
if( isset( $wgLogActions[$key] ) ) {
if( is_null( $title ) ) {
- $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'language' => $langObj ) );
+ $rv = wfMessage( $wgLogActions[$key] )->inLanguage( $langObj )->escaped();
} else {
$titleLink = self::getTitleLink( $type, $langObjOrNull, $title, $params );
if( preg_match( '/^rights\/(rights|autopromote)/', $key ) ) {
- $rightsnone = wfMsgExt( 'rightsnone', array( 'parsemag', 'language' => $langObj ) );
+ $rightsnone = wfMessage( 'rightsnone' )->inLanguage( $langObj )->text();
if( $skin ) {
+ $username = $title->getText();
foreach ( $params as &$param ) {
$groupArray = array_map( 'trim', explode( ',', $param ) );
- $groupArray = array_map( array( 'User', 'getGroupMember' ), $groupArray );
+ foreach( $groupArray as &$group ) {
+ $group = User::getGroupMember( $group, $username );
+ }
$param = $wgLang->listToText( $groupArray );
}
}
@@ -255,7 +277,7 @@ class LogPage {
}
if( count( $params ) == 0 ) {
- $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'replaceafter', 'language' => $langObj ), $titleLink );
+ $rv = wfMessage( $wgLogActions[$key] )->rawParams( $titleLink )->inLanguage( $langObj )->escaped();
} else {
$details = '';
array_unshift( $params, $titleLink );
@@ -282,11 +304,11 @@ class LogPage {
// Cascading flag...
if( $params[2] ) {
- $details .= ' [' . wfMsgExt( 'protect-summary-cascade', array( 'parsemag', 'language' => $langObj ) ) . ']';
+ $details .= ' [' . wfMessage( 'protect-summary-cascade' )->inLanguage( $langObj )->text() . ']';
}
}
- $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'replaceafter', 'language' => $langObj ), $params ) . $details;
+ $rv = wfMessage( $wgLogActions[$key] )->rawParams( $params )->inLanguage( $langObj )->escaped() . $details;
}
}
} else {
@@ -399,7 +421,12 @@ class LogPage {
# Use the language name for log titles, rather than Log/X
if( $name == 'Log' ) {
- $titleLink = '(' . Linker::link( $title, LogPage::logName( $par ) ) . ')';
+ $logPage = new LogPage( $par );
+ $titleLink = Linker::link( $title, $logPage->getName()->escaped() );
+ $titleLink = wfMessage( 'parentheses' )
+ ->inLanguage( $lang )
+ ->rawParams( $titleLink )
+ ->escaped();
} else {
$titleLink = Linker::link( $title );
}
@@ -417,11 +444,10 @@ class LogPage {
* @param $action String: 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 wfMsg.* functions
+ * @param $params Array: parameters passed later to wfMessage function
* @param $doer User object: the user doing the action
*
- * @return bool|int|null
- * @TODO: make this use LogEntry::saveContent()
+ * @return int log_id of the inserted log entry
*/
public function addEntry( $action, $target, $comment, $params = array(), $doer = null ) {
global $wgContLang;
@@ -461,6 +487,7 @@ class LogPage {
$formatter->setContext( $context );
$this->actionText = $formatter->getPlainActionText();
+ $this->ircActionText = $formatter->getIRCActionText();
return $this->saveContent();
}
@@ -522,7 +549,7 @@ class LogPage {
* Convert a comma-delimited list of block log flags
* into a more readable (and translated) form
*
- * @param $flags Flags to format
+ * @param $flags string Flags to format
* @param $lang Language object to use
* @return String
*/
@@ -533,7 +560,8 @@ class LogPage {
for( $i = 0; $i < count( $flags ); $i++ ) {
$flags[$i] = self::formatBlockFlag( $flags[$i], $lang );
}
- return '(' . $lang->commaList( $flags ) . ')';
+ return wfMessage( 'parentheses' )->inLanguage( $lang )
+ ->rawParams( $lang->commaList( $flags ) )->escaped();
} else {
return '';
}
diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php
index 16781a6e..ea1be8e0 100644
--- a/includes/logging/LogPager.php
+++ b/includes/logging/LogPager.php
@@ -131,6 +131,7 @@ class LogPager extends ReverseChronologicalPager {
* Set the log reader to return only entries by the given user.
*
* @param $name String: (In)valid user name
+ * @return bool
*/
private function limitPerformer( $name ) {
if( $name == '' ) {
@@ -166,6 +167,7 @@ class LogPager extends ReverseChronologicalPager {
*
* @param $page String or Title object: Title name
* @param $pattern String
+ * @return bool
*/
private function limitTitle( $page, $pattern ) {
global $wgMiserMode;
diff --git a/includes/logging/PatrolLog.php b/includes/logging/PatrolLog.php
index 04fdc4f2..911fffc0 100644
--- a/includes/logging/PatrolLog.php
+++ b/includes/logging/PatrolLog.php
@@ -1,12 +1,31 @@
<?php
-
/**
- * Class containing static functions for working with
- * logs of patrol events
+ * Specific methods for the patrol log.
+ *
+ * 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 Rob Church <robchur@gmail.com>
* @author Niklas Laxström
*/
+
+/**
+ * Class containing static functions for working with
+ * logs of patrol events
+ */
class PatrolLog {
/**
@@ -14,10 +33,11 @@ class PatrolLog {
*
* @param $rc Mixed: change identifier or RecentChange object
* @param $auto Boolean: was this patrol event automatic?
+ * @param $user User: user performing the action or null to use $wgUser
*
* @return bool
*/
- public static function record( $rc, $auto = false ) {
+ public static function record( $rc, $auto = false, User $user = null ) {
if ( !$rc instanceof RecentChange ) {
$rc = RecentChange::newFromId( $rc );
if ( !is_object( $rc ) ) {
@@ -25,19 +45,20 @@ class PatrolLog {
}
}
- $title = Title::makeTitleSafe( $rc->getAttribute( 'rc_namespace' ), $rc->getAttribute( 'rc_title' ) );
- if( $title ) {
- $entry = new ManualLogEntry( 'patrol', 'patrol' );
- $entry->setTarget( $title );
- $entry->setParameters( self::buildParams( $rc, $auto ) );
- $entry->setPerformer( User::newFromName( $rc->getAttribute( 'rc_user_text' ), false ) );
- $logid = $entry->insert();
- if ( !$auto ) {
- $entry->publish( $logid, 'udp' );
- }
- return true;
+ if ( !$user ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+
+ $entry = new ManualLogEntry( 'patrol', 'patrol' );
+ $entry->setTarget( $rc->getTitle() );
+ $entry->setParameters( self::buildParams( $rc, $auto ) );
+ $entry->setPerformer( $user );
+ $logid = $entry->insert();
+ if ( !$auto ) {
+ $entry->publish( $logid, 'udp' );
}
- return false;
+ return true;
}
/**
diff --git a/includes/media/BMP.php b/includes/media/BMP.php
index 6886e950..a515c635 100644
--- a/includes/media/BMP.php
+++ b/includes/media/BMP.php
@@ -1,6 +1,21 @@
<?php
/**
- * Handler for Microsoft's bitmap format
+ * Handler for Microsoft's bitmap format.
+ *
+ * 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 Media
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index 619485cc..99ac854b 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -1,6 +1,21 @@
<?php
/**
- * Generic handler for bitmap images
+ * Generic handler for bitmap images.
+ *
+ * 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 Media
@@ -152,8 +167,11 @@ class BitmapHandler extends ImageHandler {
if ( $flags & self::TRANSFORM_LATER ) {
wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
- return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
- $scalerParams['clientHeight'], false );
+ $params = array(
+ 'width' => $scalerParams['clientWidth'],
+ 'height' => $scalerParams['clientHeight']
+ );
+ return new ThumbnailImage( $image, $dstUrl, false, $params );
}
# Try to make a target path for the thumbnail
@@ -205,8 +223,11 @@ class BitmapHandler extends ImageHandler {
} elseif ( $mto ) {
return $mto;
} else {
- return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
- $scalerParams['clientHeight'], $dstPath );
+ $params = array(
+ 'width' => $scalerParams['clientWidth'],
+ 'height' => $scalerParams['clientHeight']
+ );
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
}
}
@@ -243,14 +264,17 @@ class BitmapHandler extends ImageHandler {
* client side
*
* @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param $scalerParams array Array with scaler params
* @return ThumbnailImage
*
- * @fixme no rotation support
+ * @todo fixme: no rotation support
*/
- protected function getClientScalingThumbnailImage( $image, $params ) {
- return new ThumbnailImage( $image, $image->getURL(),
- $params['clientWidth'], $params['clientHeight'], null );
+ protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
+ $params = array(
+ 'width' => $scalerParams['clientWidth'],
+ 'height' => $scalerParams['clientHeight']
+ );
+ return new ThumbnailImage( $image, $image->getURL(), null, $params );
}
/**
@@ -259,7 +283,7 @@ class BitmapHandler extends ImageHandler {
* @param $image File File associated with this thumbnail
* @param $params array Array with scaler params
*
- * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
protected function transformImageMagick( $image, $params ) {
# use ImageMagick
@@ -358,7 +382,7 @@ class BitmapHandler extends ImageHandler {
* @param $image File File associated with this thumbnail
* @param $params array Array with scaler params
*
- * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
protected function transformImageMagickExt( $image, $params ) {
global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
@@ -435,7 +459,7 @@ class BitmapHandler extends ImageHandler {
* @param $image File File associated with this thumbnail
* @param $params array Array with scaler params
*
- * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
protected function transformCustom( $image, $params ) {
# Use a custom convert command
@@ -462,7 +486,7 @@ class BitmapHandler extends ImageHandler {
}
/**
- * Log an error that occured in an external process
+ * Log an error that occurred in an external process
*
* @param $retval int
* @param $err int
@@ -491,7 +515,7 @@ class BitmapHandler extends ImageHandler {
* @param $image File File associated with this thumbnail
* @param $params array Array with scaler params
*
- * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
*/
protected function transformGd( $image, $params ) {
# Use PHP's builtin GD library functions.
@@ -509,7 +533,7 @@ class BitmapHandler extends ImageHandler {
if ( !isset( $typemap[$params['mimeType']] ) ) {
$err = 'Image type not supported';
wfDebug( "$err\n" );
- $errMsg = wfMsg( 'thumbnail_image-type' );
+ $errMsg = wfMessage( 'thumbnail_image-type' )->text();
return $this->getMediaTransformError( $params, $errMsg );
}
list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
@@ -517,14 +541,14 @@ class BitmapHandler extends ImageHandler {
if ( !function_exists( $loader ) ) {
$err = "Incomplete GD library configuration: missing function $loader";
wfDebug( "$err\n" );
- $errMsg = wfMsg( 'thumbnail_gd-library', $loader );
+ $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
return $this->getMediaTransformError( $params, $errMsg );
}
if ( !file_exists( $params['srcPath'] ) ) {
$err = "File seems to be missing: {$params['srcPath']}";
wfDebug( "$err\n" );
- $errMsg = wfMsg( 'thumbnail_image-missing', $params['srcPath'] );
+ $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
return $this->getMediaTransformError( $params, $errMsg );
}
@@ -572,6 +596,7 @@ class BitmapHandler extends ImageHandler {
/**
* Escape a string for ImageMagick's property input (e.g. -set -comment)
* See InterpretImageProperties() in magick/property.c
+ * @return mixed|string
*/
function escapeMagickProperty( $s ) {
// Double the backslashes
@@ -599,6 +624,7 @@ class BitmapHandler extends ImageHandler {
*
* @param $path string The file path
* @param $scene string The scene specification, or false if there is none
+ * @return string
*/
function escapeMagickInput( $path, $scene = false ) {
# Die on initial metacharacters (caller should prepend path)
@@ -616,6 +642,7 @@ class BitmapHandler extends ImageHandler {
/**
* Escape a string for ImageMagick's output filename. See
* InterpretImageFilename() in magick/image.c.
+ * @return string
*/
function escapeMagickOutput( $path, $scene = false ) {
$path = str_replace( '%', '%%', $path );
@@ -628,6 +655,7 @@ class BitmapHandler extends ImageHandler {
*
* @param $path string The file path
* @param $scene string The scene specification, or false if there is none
+ * @return string
*/
protected function escapeMagickPath( $path, $scene = false ) {
# Die on format specifiers (other than drive letters). The regex is
diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php
index 746dddda..0a195547 100644
--- a/includes/media/BitmapMetadataHandler.php
+++ b/includes/media/BitmapMetadataHandler.php
@@ -1,13 +1,36 @@
<?php
/**
-Class to deal with reconciling and extracting metadata from bitmap images.
-This is meant to comply with http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf
+ * Extraction of metadata from different bitmap image types.
+ *
+ * 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 Media
+ */
-This sort of acts as an intermediary between MediaHandler::getMetadata
-and the various metadata extractors.
-
-@todo other image formats.
-*/
+/**
+ * Class to deal with reconciling and extracting metadata from bitmap images.
+ * This is meant to comply with http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf
+ *
+ * This sort of acts as an intermediary between MediaHandler::getMetadata
+ * and the various metadata extractors.
+ *
+ * @todo other image formats.
+ * @ingroup Media
+ */
class BitmapMetadataHandler {
private $metadata = array();
@@ -122,7 +145,7 @@ class BitmapMetadataHandler {
/** Main entry point for jpeg's.
*
* @param $filename string filename (with full path)
- * @return metadata result array.
+ * @return array metadata result array.
* @throws MWException on invalid file.
*/
static function Jpeg ( $filename ) {
@@ -193,7 +216,7 @@ class BitmapMetadataHandler {
* They don't really have native metadata, so just merges together
* XMP and image comment.
*
- * @param $filename full path to file
+ * @param $filename string full path to file
* @return Array metadata array
*/
static public function GIF ( $filename ) {
diff --git a/includes/media/Bitmap_ClientOnly.php b/includes/media/Bitmap_ClientOnly.php
index 3c5d9738..63af2552 100644
--- a/includes/media/Bitmap_ClientOnly.php
+++ b/includes/media/Bitmap_ClientOnly.php
@@ -1,6 +1,21 @@
<?php
/**
- * Handler for bitmap images that will be resized by clients
+ * Handler for bitmap images that will be resized by clients.
+ *
+ * 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 Media
@@ -37,7 +52,6 @@ class BitmapHandler_ClientOnly extends BitmapHandler {
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
}
- return new ThumbnailImage( $image, $image->getURL(), $params['width'],
- $params['height'], $image->getLocalRefPath() );
+ return new ThumbnailImage( $image, $image->getURL(), $image->getLocalRefPath(), $params );
}
}
diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php
index dedbee0d..84672e05 100644
--- a/includes/media/DjVu.php
+++ b/includes/media/DjVu.php
@@ -1,6 +1,21 @@
<?php
/**
- * Handler for DjVu images
+ * Handler for DjVu images.
+ *
+ * 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 Media
@@ -123,7 +138,7 @@ class DjVuHandler extends ImageHandler {
$width = isset( $params['width'] ) ? $params['width'] : 0;
$height = isset( $params['height'] ) ? $params['height'] : 0;
return new MediaTransformError( 'thumbnail_error', $width, $height,
- wfMsg( 'djvu_no_xml' ) );
+ wfMessage( 'djvu_no_xml' )->text() );
}
if ( !$this->normaliseParams( $image, $params ) ) {
@@ -131,20 +146,35 @@ class DjVuHandler extends ImageHandler {
}
$width = $params['width'];
$height = $params['height'];
- $srcPath = $image->getLocalRefPath();
$page = $params['page'];
if ( $page > $this->pageCount( $image ) ) {
- return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) );
+ return new MediaTransformError(
+ 'thumbnail_error',
+ $width,
+ $height,
+ wfMessage( 'djvu_page_error' )->text()
+ );
}
if ( $flags & self::TRANSFORM_LATER ) {
- return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page );
+ $params = array(
+ 'width' => $width,
+ 'height' => $height,
+ 'page' => $page
+ );
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
}
if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
- return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'thumbnail_dest_directory' ) );
+ return new MediaTransformError(
+ 'thumbnail_error',
+ $width,
+ $height,
+ wfMessage( 'thumbnail_dest_directory' )->text()
+ );
}
+ $srcPath = $image->getLocalRefPath();
# Use a subshell (brackets) to aggregate stderr from both pipeline commands
# before redirecting it to the overall stdout. This works in both Linux and Windows XP.
$cmd = '(' . wfEscapeShellArg( $wgDjvuRenderer ) . " -format=ppm -page={$page}" .
@@ -167,7 +197,12 @@ class DjVuHandler extends ImageHandler {
wfHostname(), $retval, trim($err), $cmd ) );
return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
} else {
- return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page );
+ $params = array(
+ 'width' => $width,
+ 'height' => $height,
+ 'page' => $page
+ );
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
}
}
@@ -191,6 +226,7 @@ class DjVuHandler extends ImageHandler {
* Cache a document tree for the DjVu XML metadata
* @param $image File
* @param $gettext Boolean: DOCUMENT (Default: false)
+ * @return bool
*/
function getMetaTree( $image , $gettext = false ) {
if ( isset( $image->dejaMetaTree ) ) {
diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php
index 80b7408c..6aef562b 100644
--- a/includes/media/DjVuImage.php
+++ b/includes/media/DjVuImage.php
@@ -1,6 +1,6 @@
<?php
/**
- * DjVu image handler
+ * DjVu image handler.
*
* Copyright © 2006 Brion Vibber <brion@pobox.com>
* http://www.mediawiki.org/
@@ -21,6 +21,7 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
+ * @ingroup Media
*/
/**
@@ -284,6 +285,7 @@ EOR;
/**
* Hack to temporarily work around djvutoxml bug
+ * @return bool|string
*/
function convertDumpToXML( $dump ) {
if ( strval( $dump ) == '' ) {
diff --git a/includes/media/Exif.php b/includes/media/Exif.php
index a4acdfe0..784a6018 100644
--- a/includes/media/Exif.php
+++ b/includes/media/Exif.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Extraction and validation of image metadata.
+ *
* 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
@@ -368,6 +370,12 @@ class Exif {
$this->exifGPStoNumber( 'GPSDestLongitude' );
if ( isset( $this->mFilteredExifData['GPSAltitude'] ) && isset( $this->mFilteredExifData['GPSAltitudeRef'] ) ) {
+
+ // We know altitude data is a <num>/<denom> from the validation functions ran earlier.
+ // But multiplying such a string by -1 doesn't work well, so convert.
+ list( $num, $denom ) = explode( '/', $this->mFilteredExifData['GPSAltitude'] );
+ $this->mFilteredExifData['GPSAltitude'] = $num / $denom;
+
if ( $this->mFilteredExifData['GPSAltitudeRef'] === "\1" ) {
$this->mFilteredExifData['GPSAltitude'] *= - 1;
}
@@ -549,6 +557,7 @@ class Exif {
*/
/**
* Get $this->mRawExifData
+ * @return array
*/
function getData() {
return $this->mRawExifData;
diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php
index 7b9867f7..34a1f511 100644
--- a/includes/media/ExifBitmap.php
+++ b/includes/media/ExifBitmap.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Handler for bitmap images with exif metadata.
+ *
+ * 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 Media
*/
@@ -182,7 +199,8 @@ class ExifBitmapHandler extends BitmapHandler {
*
* @param string $data
* @return int 0, 90, 180 or 270
- * @fixme orientation can include flipping as well; see if this is an issue!
+ * @todo FIXME orientation can include flipping as well; see if this is an
+ * issue!
*/
protected function getRotationForExif( $data ) {
if ( !$data ) {
diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php
index 91cb6914..843c1fa2 100644
--- a/includes/media/FormatMetadata.php
+++ b/includes/media/FormatMetadata.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Formating 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
* the Free Software Foundation; either version 2 of the License, or
@@ -98,14 +100,20 @@ class FormatMetadata {
) {
continue;
}
- $tags[$tag] = intval( $h[0] / $h[1] )
+ $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2, '0', STR_PAD_LEFT )
. ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT )
. ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT );
- $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] );
- // the 1971:01:01 is just a placeholder, and not shown to user.
- if ( $time && intval( $time ) > 0 ) {
- $tags[$tag] = $wgLang->time( $time );
+ try {
+ $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] );
+ // the 1971:01:01 is just a placeholder, and not shown to user.
+ if ( $time && intval( $time ) > 0 ) {
+ $tags[$tag] = $wgLang->time( $time );
+ }
+ } catch ( TimestampException $e ) {
+ // This shouldn't happen, but we've seen bad formats
+ // such as 4-digit seconds in the wild.
+ // leave $tags[$tag] as-is
}
continue;
}
@@ -231,7 +239,7 @@ class FormatMetadata {
case 'dc-date':
case 'DateTimeMetadata':
if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) {
- $val = wfMsg( 'exif-unknowndate' );
+ $val = wfMessage( 'exif-unknowndate' )->text();
} elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', $val ) ) {
// Full date.
$time = wfTimestamp( TS_MW, $val );
@@ -307,7 +315,7 @@ class FormatMetadata {
'redeye' => ( $val & bindec( '01000000' ) ) >> 6,
// 'reserved' => ($val & bindec( '10000000' )) >> 7,
);
-
+ $flashMsgs = array();
# We do not need to handle unknown values since all are used.
foreach ( $flashDecode as $subTag => $subValue ) {
# We do not need any message for zeroed values.
@@ -589,7 +597,7 @@ class FormatMetadata {
case 'Software':
if ( is_array( $val ) ) {
//if its a software, version array.
- $val = wfMsg( 'exif-software-version-value', $val[0], $val[1] );
+ $val = wfMessage( 'exif-software-version-value', $val[0], $val[1] )->text();
} else {
$val = self::msg( $tag, '', $val );
}
@@ -597,8 +605,8 @@ class FormatMetadata {
case 'ExposureTime':
// Show the pretty fraction as well as decimal version
- $val = wfMsg( 'exif-exposuretime-format',
- self::formatFraction( $val ), self::formatNum( $val ) );
+ $val = wfMessage( 'exif-exposuretime-format',
+ self::formatFraction( $val ), self::formatNum( $val ) )->text();
break;
case 'ISOSpeedRatings':
// If its = 65535 that means its at the
@@ -611,13 +619,13 @@ class FormatMetadata {
}
break;
case 'FNumber':
- $val = wfMsg( 'exif-fnumber-format',
- self::formatNum( $val ) );
+ $val = wfMessage( 'exif-fnumber-format',
+ self::formatNum( $val ) )->text();
break;
case 'FocalLength': case 'FocalLengthIn35mmFilm':
- $val = wfMsg( 'exif-focallength-format',
- self::formatNum( $val ) );
+ $val = wfMessage( 'exif-focallength-format',
+ self::formatNum( $val ) )->text();
break;
case 'MaxApertureValue':
@@ -631,14 +639,14 @@ class FormatMetadata {
if ( is_numeric( $val ) ) {
$fNumber = pow( 2, $val / 2 );
if ( $fNumber !== false ) {
- $val = wfMsg( 'exif-maxaperturevalue-value',
+ $val = wfMessage( 'exif-maxaperturevalue-value',
self::formatNum( $val ),
self::formatNum( $fNumber, 2 )
- );
+ )->text();
}
}
break;
-
+
case 'iimCategory':
switch( strtolower( $val ) ) {
// See pg 29 of IPTC photo
@@ -694,7 +702,7 @@ class FormatMetadata {
case 'PixelYDimension':
case 'ImageWidth':
case 'ImageLength':
- $val = self::formatNum( $val ) . ' ' . wfMsg( 'unit-pixel' );
+ $val = self::formatNum( $val ) . ' ' . wfMessage( 'unit-pixel' )->text();
break;
// Do not transform fields with pure text.
@@ -800,7 +808,7 @@ class FormatMetadata {
break;
case 'LanguageCode':
- $lang = $wgLang->getLanguageName( strtolower( $val ) );
+ $lang = Language::fetchLanguageName( strtolower( $val ), $wgLang->getCode() );
if ($lang) {
$val = htmlspecialchars( $lang );
} else {
@@ -825,14 +833,14 @@ class FormatMetadata {
* 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).
+ * 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 ) {
@@ -874,7 +882,7 @@ class FormatMetadata {
// If default is set, save it for later,
// as we don't know if it's equal to
// one of the lang codes. (In xmp
- // you specify the language for a
+ // you specify the language for a
// default property by having both
// a default prop, and one in the language
// that are identical)
@@ -937,11 +945,11 @@ class FormatMetadata {
* @param $lang String lang code of item or false
* @param $default Boolean if it is default value.
* @param $noHtml Boolean If to avoid html (for back-compat)
- * @return language item (Note: despite how this looks,
- * this is treated as wikitext not html).
+ * @throws MWException
+ * @return string language item (Note: despite how this looks,
+ * this is treated as wikitext not html).
*/
private static function langItem( $value, $lang, $default = false, $noHtml = false ) {
- global $wgContLang;
if ( $lang === false && $default === false) {
throw new MWException('$lang and $default cannot both '
. 'be false.');
@@ -956,21 +964,21 @@ class FormatMetadata {
if ( $lang === false ) {
if ( $noHtml ) {
- return wfMsg( 'metadata-langitem-default',
- $wrappedValue ) . "\n\n";
+ return wfMessage( 'metadata-langitem-default',
+ $wrappedValue )->text() . "\n\n";
} /* else */
return '<li class="mw-metadata-lang-default">'
- . wfMsg( 'metadata-langitem-default',
- $wrappedValue )
+ . wfMessage( 'metadata-langitem-default',
+ $wrappedValue )->text()
. "</li>\n";
}
$lowLang = strtolower( $lang );
- $langName = $wgContLang->getLanguageName( $lowLang );
+ $langName = Language::fetchLanguageName( $lowLang );
if ( $langName === '' ) {
//try just the base language name. (aka en-US -> en ).
list( $langPrefix ) = explode( '-', $lowLang, 2 );
- $langName = $wgContLang->getLanguageName( $langPrefix );
+ $langName = Language::fetchLanguageName( $langPrefix );
if ( $langName === '' ) {
// give up.
$langName = $lang;
@@ -979,8 +987,8 @@ class FormatMetadata {
// else we have a language specified
if ( $noHtml ) {
- return '*' . wfMsg( 'metadata-langitem',
- $wrappedValue, $langName, $lang );
+ return '*' . wfMessage( 'metadata-langitem',
+ $wrappedValue, $langName, $lang )->text();
} /* else: */
$item = '<li class="mw-metadata-lang-code-'
@@ -989,8 +997,8 @@ class FormatMetadata {
$item .= ' mw-metadata-lang-default';
}
$item .= '" lang="' . $lang . '">';
- $item .= wfMsg( 'metadata-langitem',
- $wrappedValue, $langName, $lang );
+ $item .= wfMessage( 'metadata-langitem',
+ $wrappedValue, $langName, $lang )->text();
$item .= "</li>\n";
return $item;
}
@@ -1004,24 +1012,22 @@ class FormatMetadata {
* @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)
- * @return string A wfMsg of "exif-$tag-$val" in lower case
+ * @return string A wfMessage of "exif-$tag-$val" in lower case
*/
static function msg( $tag, $val, $arg = null, $arg2 = null ) {
global $wgContLang;
if ($val === '')
$val = 'value';
- return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 );
+ return wfMessage( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text();
}
/**
* Format a number, convert numbers from fractions into floating point
* numbers, joins arrays of numbers with commas.
*
- * @private
- *
* @param $num Mixed: the value to format
- * @param $round digits to round to or false.
+ * @param $round float|int|bool digits to round to or false.
* @return mixed A floating point number or whatever we were fed
*/
static function formatNum( $num, $round = false ) {
@@ -1102,8 +1108,9 @@ class FormatMetadata {
return $a;
}
- /** Fetch the human readable version of a news code.
- * A news code is an 8 digit code. The first two
+ /**
+ * Fetch the human readable version of a news code.
+ * A news code is an 8 digit code. The first two
* digits are a general classification, so we just
* translate that.
*
@@ -1111,7 +1118,7 @@ class FormatMetadata {
* a string, not an int.
*
* @param $val String: The 8 digit news code.
- * @return The human readable form
+ * @return string The human readable form
*/
static private function convertNewsCode( $val ) {
if ( !preg_match( '/^\d{8}$/D', $val ) ) {
@@ -1183,7 +1190,7 @@ class FormatMetadata {
* Format a coordinate value, convert numbers from floating point
* into degree minute second representation.
*
- * @param $coord Array: degrees, minutes and seconds
+ * @param $coord int degrees, minutes and seconds
* @param $type String: latitude or longitude (for if its a NWS or E)
* @return mixed A floating point number or whatever we were fed
*/
@@ -1193,17 +1200,14 @@ class FormatMetadata {
$nCoord = -$coord;
if ( $type === 'latitude' ) {
$ref = 'S';
- }
- elseif ( $type === 'longitude' ) {
+ } elseif ( $type === 'longitude' ) {
$ref = 'W';
}
- }
- else {
+ } else {
$nCoord = $coord;
if ( $type === 'latitude' ) {
$ref = 'N';
- }
- elseif ( $type === 'longitude' ) {
+ } elseif ( $type === 'longitude' ) {
$ref = 'E';
}
}
@@ -1216,7 +1220,7 @@ class FormatMetadata {
$min = self::formatNum( $min );
$sec = self::formatNum( $sec );
- return wfMsg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord );
+ return wfMessage( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text();
}
/**
@@ -1274,7 +1278,7 @@ class FormatMetadata {
// Todo: This can potentially be multi-line.
// Need to check how that works in XMP.
$street = '<span class="extended-address">'
- . htmlspecialchars(
+ . htmlspecialchars(
$vals['CiAdrExtadr'] )
. '</span>';
}
@@ -1321,7 +1325,7 @@ class FormatMetadata {
}
if ( isset( $vals['CiAdrPcode'] ) ) {
$postal = '<span class="postal-code">'
- . htmlspecialchars(
+ . htmlspecialchars(
$vals['CiAdrPcode'] )
. '</span>';
}
@@ -1337,9 +1341,9 @@ class FormatMetadata {
. htmlspecialchars( $vals['CiUrlWork'] )
. '</span>';
}
- return wfMsg( 'exif-contact-value', $email, $url,
+ return wfMessage( 'exif-contact-value', $email, $url,
$street, $city, $region, $postal, $country,
- $tel );
+ $tel )->text();
}
}
}
@@ -1352,12 +1356,19 @@ class FormatMetadata {
**/
class FormatExif {
var $meta;
- function FormatExif ( $meta ) {
+
+ /**
+ * @param $meta array
+ */
+ function FormatExif( $meta ) {
wfDeprecated(__METHOD__);
$this->meta = $meta;
}
- function getFormattedData ( ) {
+ /**
+ * @return array
+ */
+ function getFormattedData() {
return FormatMetadata::getFormattedData( $this->meta );
}
}
diff --git a/includes/media/GIF.php b/includes/media/GIF.php
index 32618e94..84b9b8ca 100644
--- a/includes/media/GIF.php
+++ b/includes/media/GIF.php
@@ -2,6 +2,21 @@
/**
* Handler for GIF images.
*
+ * 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 Media
*/
@@ -78,6 +93,17 @@ class GIFHandler extends BitmapHandler {
return false;
}
+ /**
+ * We cannot animate thumbnails that are bigger than a particular size
+ * @param File $file
+ * @return bool
+ */
+ function canAnimateThumbnail( $file ) {
+ global $wgMaxAnimatedGifArea;
+ $answer = $this->getImageArea( $file ) <= $wgMaxAnimatedGifArea;
+ return $answer;
+ }
+
function getMetadataType( $image ) {
return 'parsed-gif';
}
@@ -127,11 +153,11 @@ class GIFHandler extends BitmapHandler {
$info[] = $original;
if ( $metadata['looped'] ) {
- $info[] = wfMsgExt( 'file-info-gif-looped', 'parseinline' );
+ $info[] = wfMessage( 'file-info-gif-looped' )->parse();
}
if ( $metadata['frameCount'] > 1 ) {
- $info[] = wfMsgExt( 'file-info-gif-frames', 'parseinline', $metadata['frameCount'] );
+ $info[] = wfMessage( 'file-info-gif-frames' )->numParams( $metadata['frameCount'] )->parse();
}
if ( $metadata['duration'] ) {
diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php
index 5dbeb8f8..5fc5c1a7 100644
--- a/includes/media/GIFMetadataExtractor.php
+++ b/includes/media/GIFMetadataExtractor.php
@@ -7,6 +7,21 @@
* Deliberately not using MWExceptions to avoid external dependencies, encouraging
* redistribution.
*
+ * 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 Media
*/
@@ -286,7 +301,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.
- * @return The data.
+ * @return string The data.
*/
static function readBlock( $fh, $includeLengths = false ) {
$data = '';
diff --git a/includes/media/IPTC.php b/includes/media/IPTC.php
index 1d19791c..8fd3552f 100644
--- a/includes/media/IPTC.php
+++ b/includes/media/IPTC.php
@@ -1,8 +1,31 @@
<?php
/**
-*Class for some IPTC functions.
+ * Class for some IPTC functions.
+ *
+ * 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 Media
+ */
-*/
+/**
+ * Class for some IPTC functions.
+ *
+ * @ingroup Media
+ */
class IPTC {
/**
@@ -395,10 +418,10 @@ class IPTC {
/**
* Helper function to convert charset for iptc values.
- * @param $data Mixed String or Array: The iptc string
+ * @param $data string|array The iptc string
* @param $charset String: The charset
*
- * @return string
+ * @return string|array
*/
private static function convIPTC ( $data, $charset ) {
if ( is_array( $data ) ) {
diff --git a/includes/media/ImageHandler.php b/includes/media/ImageHandler.php
new file mode 100644
index 00000000..61759074
--- /dev/null
+++ b/includes/media/ImageHandler.php
@@ -0,0 +1,249 @@
+<?php
+/**
+ * Media-handling base classes and generic functionality.
+ *
+ * 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 Media
+ */
+
+/**
+ * Media handler abstract base class for images
+ *
+ * @ingroup Media
+ */
+abstract class ImageHandler extends MediaHandler {
+
+ /**
+ * @param $file File
+ * @return bool
+ */
+ function canRender( $file ) {
+ return ( $file->getWidth() && $file->getHeight() );
+ }
+
+ function getParamMap() {
+ return array( 'img_width' => 'width' );
+ }
+
+ function validateParam( $name, $value ) {
+ if ( in_array( $name, array( 'width', 'height' ) ) ) {
+ if ( $value <= 0 ) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ function makeParamString( $params ) {
+ if ( isset( $params['physicalWidth'] ) ) {
+ $width = $params['physicalWidth'];
+ } elseif ( isset( $params['width'] ) ) {
+ $width = $params['width'];
+ } else {
+ throw new MWException( 'No width specified to '.__METHOD__ );
+ }
+ # Removed for ProofreadPage
+ #$width = intval( $width );
+ return "{$width}px";
+ }
+
+ function parseParamString( $str ) {
+ $m = false;
+ if ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
+ return array( 'width' => $m[1] );
+ } else {
+ return false;
+ }
+ }
+
+ function getScriptParams( $params ) {
+ return array( 'width' => $params['width'] );
+ }
+
+ /**
+ * @param $image File
+ * @param $params
+ * @return bool
+ */
+ function normaliseParams( $image, &$params ) {
+ $mimeType = $image->getMimeType();
+
+ if ( !isset( $params['width'] ) ) {
+ return false;
+ }
+
+ if ( !isset( $params['page'] ) ) {
+ $params['page'] = 1;
+ } else {
+ if ( $params['page'] > $image->pageCount() ) {
+ $params['page'] = $image->pageCount();
+ }
+
+ if ( $params['page'] < 1 ) {
+ $params['page'] = 1;
+ }
+ }
+
+ $srcWidth = $image->getWidth( $params['page'] );
+ $srcHeight = $image->getHeight( $params['page'] );
+
+ if ( isset( $params['height'] ) && $params['height'] != -1 ) {
+ # Height & width were both set
+ if ( $params['width'] * $srcHeight > $params['height'] * $srcWidth ) {
+ # Height is the relative smaller dimension, so scale width accordingly
+ $params['width'] = self::fitBoxWidth( $srcWidth, $srcHeight, $params['height'] );
+
+ if ( $params['width'] == 0 ) {
+ # Very small image, so we need to rely on client side scaling :(
+ $params['width'] = 1;
+ }
+
+ $params['physicalWidth'] = $params['width'];
+ } else {
+ # Height was crap, unset it so that it will be calculated later
+ unset( $params['height'] );
+ }
+ }
+
+ if ( !isset( $params['physicalWidth'] ) ) {
+ # Passed all validations, so set the physicalWidth
+ $params['physicalWidth'] = $params['width'];
+ }
+
+ # Because thumbs are only referred to by width, the height always needs
+ # to be scaled by the width to keep the thumbnail sizes consistent,
+ # even if it was set inside the if block above
+ $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight,
+ $params['physicalWidth'] );
+
+ # Set the height if it was not validated in the if block higher up
+ if ( !isset( $params['height'] ) || $params['height'] == -1 ) {
+ $params['height'] = $params['physicalHeight'];
+ }
+
+
+ if ( !$this->validateThumbParams( $params['physicalWidth'],
+ $params['physicalHeight'], $srcWidth, $srcHeight, $mimeType ) ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Validate thumbnail parameters and fill in the correct height
+ *
+ * @param $width Integer: specified width (input/output)
+ * @param $height Integer: height (output only)
+ * @param $srcWidth Integer: width of the source image
+ * @param $srcHeight Integer: height of the source image
+ * @param $mimeType
+ * @return bool False to indicate that an error should be returned to the user.
+ */
+ function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) {
+ $width = intval( $width );
+
+ # Sanity check $width
+ if( $width <= 0) {
+ wfDebug( __METHOD__.": Invalid destination width: $width\n" );
+ return false;
+ }
+ if ( $srcWidth <= 0 ) {
+ wfDebug( __METHOD__.": Invalid source width: $srcWidth\n" );
+ return false;
+ }
+
+ $height = File::scaleHeight( $srcWidth, $srcHeight, $width );
+ if ( $height == 0 ) {
+ # Force height to be at least 1 pixel
+ $height = 1;
+ }
+ return true;
+ }
+
+ /**
+ * @param $image File
+ * @param $script
+ * @param $params
+ * @return bool|ThumbnailImage
+ */
+ function getScriptedTransform( $image, $script, $params ) {
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return false;
+ }
+ $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) );
+
+ if( $image->mustRender() || $params['width'] < $image->getWidth() ) {
+ return new ThumbnailImage( $image, $url, false, $params );
+ }
+ }
+
+ function getImageSize( $image, $path ) {
+ wfSuppressWarnings();
+ $gis = getimagesize( $path );
+ wfRestoreWarnings();
+ return $gis;
+ }
+
+ /**
+ * @param $file File
+ * @return string
+ */
+ function getShortDesc( $file ) {
+ global $wgLang;
+ $nbytes = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) );
+ $widthheight = wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->escaped();
+
+ return "$widthheight ($nbytes)";
+ }
+
+ /**
+ * @param $file File
+ * @return string
+ */
+ function getLongDesc( $file ) {
+ global $wgLang;
+ $pages = $file->pageCount();
+ $size = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) );
+ if ( $pages === false || $pages <= 1 ) {
+ $msg = wfMessage( 'file-info-size' )->numParams( $file->getWidth(),
+ $file->getHeight() )->params( $size,
+ $file->getMimeType() )->parse();
+ } else {
+ $msg = wfMessage( 'file-info-size-pages' )->numParams( $file->getWidth(),
+ $file->getHeight() )->params( $size,
+ $file->getMimeType() )->numParams( $pages )->parse();
+ }
+ return $msg;
+ }
+
+ /**
+ * @param $file File
+ * @return string
+ */
+ function getDimensionsString( $file ) {
+ $pages = $file->pageCount();
+ if ( $pages > 1 ) {
+ return wfMessage( 'widthheightpage' )->numParams( $file->getWidth(), $file->getHeight(), $pages )->text();
+ } else {
+ return wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->text();
+ }
+ }
+}
diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php
index 7033409b..a15b6524 100644
--- a/includes/media/Jpeg.php
+++ b/includes/media/Jpeg.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Handler for JPEG images.
+ *
+ * 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 Media
*/
diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php
index 224b4a2b..8d7e43b9 100644
--- a/includes/media/JpegMetadataExtractor.php
+++ b/includes/media/JpegMetadataExtractor.php
@@ -1,10 +1,34 @@
<?php
/**
-* Class for reading jpegs and extracting metadata.
-* see also BitmapMetadataHandler.
-*
-* Based somewhat on GIFMetadataExtrator.
-*/
+ * Extraction of JPEG image metadata.
+ *
+ * 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 Media
+ */
+
+/**
+ * Class for reading jpegs and extracting metadata.
+ * see also BitmapMetadataHandler.
+ *
+ * Based somewhat on GIFMetadataExtrator.
+ *
+ * @ingroup Media
+ */
class JpegMetadataExtractor {
const MAX_JPEG_SEGMENTS = 200;
@@ -143,13 +167,17 @@ class JpegMetadataExtractor {
/**
* Helper function for jpegSegmentSplitter
* @param &$fh FileHandle for jpeg file
- * @return data content of segment.
+ * @return string data content of segment.
*/
private static function jpegExtractMarker( &$fh ) {
$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" );
+ }
$segment = fread( $fh, $size['int'] - 2 );
- if ( strlen( $segment ) !== $size['int'] - 2 ) throw new MWException( "Segment shorter than expected" );
+ if ( strlen( $segment ) !== $size['int'] - 2 ) {
+ throw new MWException( "Segment shorter than expected" );
+ }
return $segment;
}
diff --git a/includes/media/Generic.php b/includes/media/MediaHandler.php
index 271d3a8d..965099fd 100644
--- a/includes/media/Generic.php
+++ b/includes/media/MediaHandler.php
@@ -1,6 +1,21 @@
<?php
/**
- * Media-handling base classes and generic functionality
+ * Media-handling base classes and generic functionality.
+ *
+ * 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 Media
@@ -160,6 +175,7 @@ abstract class MediaHandler {
* MediaHandler::METADATA_GOOD for if the metadata is a-ok,
* MediaHanlder::METADATA_COMPATIBLE if metadata is old but backwards
* compatible (which may or may not trigger a metadata reload).
+ * @return bool
*/
function isMetadataValid( $image, $metadata ) {
return self::METADATA_GOOD;
@@ -173,6 +189,7 @@ abstract class MediaHandler {
* Used when the repository has a thumbnailScriptUrl option configured.
*
* Return false to fall back to the regular getTransform().
+ * @return bool
*/
function getScriptedTransform( $image, $script, $params ) {
return false;
@@ -186,6 +203,7 @@ abstract class MediaHandler {
* @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()
+ * @return MediaTransformOutput
*/
final function getTransform( $image, $dstPath, $dstUrl, $params ) {
return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER );
@@ -227,27 +245,46 @@ abstract class MediaHandler {
/**
* True if the handled types can be transformed
+ * @return bool
*/
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; }
/**
* True if the type has multi-page capabilities
+ * @return bool
*/
function isMultiPage( $file ) { return false; }
/**
* Page count for a multi-page document, false if unsupported or unknown
+ * @return bool
*/
function pageCount( $file ) { return false; }
/**
* The material is vectorized and thus scaling is lossless
+ * @return bool
*/
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; }
+ /**
+ * 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; }
+ /**
* False if the handler is disabled for all files
+ * @return bool
*/
function isEnabled() { return true; }
@@ -258,6 +295,8 @@ abstract class MediaHandler {
* Returns false if unknown or if the document is not multi-page.
*
* @param $image File
+ * @param $page Unused, left for backcompatibility?
+ * @return array
*/
function getPageDimensions( $image, $page ) {
$gis = $this->getImageSize( $image, $image->getLocalRefPath() );
@@ -270,6 +309,7 @@ abstract class MediaHandler {
/**
* Generic getter for text layer.
* Currently overloaded by PDF and DjVu handlers
+ * @return bool
*/
function getPageText( $image, $page ) {
return false;
@@ -300,6 +340,7 @@ abstract class MediaHandler {
* all the formatting according to some standard. That makes it possible
* to do things like visual indication of grouped and chained streams
* in ogg container files.
+ * @return bool
*/
function formatMetadata( $image ) {
return false;
@@ -344,7 +385,7 @@ abstract class MediaHandler {
*/
function visibleMetadataFields() {
$fields = array();
- $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
+ $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() );
foreach( $lines as $line ) {
$matches = array();
if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
@@ -471,7 +512,7 @@ abstract class MediaHandler {
* match the handler class, a Status object should be returned containing
* relevant errors.
*
- * @param $fileName The local path to the file.
+ * @param $fileName string The local path to the file.
* @return Status object
*/
function verifyUpload( $fileName ) {
@@ -482,9 +523,9 @@ 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 The location of the suspect file
- * @param $retval Return value of some shell process, file will be deleted if this is non-zero
- * @return true if removed, false otherwise
+ * @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
+ * @return bool True if removed, false otherwise
*/
function removeBadFile( $dstPath, $retval = 0 ) {
if( file_exists( $dstPath ) ) {
@@ -509,7 +550,7 @@ abstract class MediaHandler {
/**
* Remove files from the purge list
- *
+ *
* @param array $files
* @param array $options
*/
@@ -517,235 +558,3 @@ abstract class MediaHandler {
// Do nothing
}
}
-
-/**
- * Media handler abstract base class for images
- *
- * @ingroup Media
- */
-abstract class ImageHandler extends MediaHandler {
-
- /**
- * @param $file File
- * @return bool
- */
- function canRender( $file ) {
- return ( $file->getWidth() && $file->getHeight() );
- }
-
- function getParamMap() {
- return array( 'img_width' => 'width' );
- }
-
- function validateParam( $name, $value ) {
- if ( in_array( $name, array( 'width', 'height' ) ) ) {
- if ( $value <= 0 ) {
- return false;
- } else {
- return true;
- }
- } else {
- return false;
- }
- }
-
- function makeParamString( $params ) {
- if ( isset( $params['physicalWidth'] ) ) {
- $width = $params['physicalWidth'];
- } elseif ( isset( $params['width'] ) ) {
- $width = $params['width'];
- } else {
- throw new MWException( 'No width specified to '.__METHOD__ );
- }
- # Removed for ProofreadPage
- #$width = intval( $width );
- return "{$width}px";
- }
-
- function parseParamString( $str ) {
- $m = false;
- if ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
- return array( 'width' => $m[1] );
- } else {
- return false;
- }
- }
-
- function getScriptParams( $params ) {
- return array( 'width' => $params['width'] );
- }
-
- /**
- * @param $image File
- * @param $params
- * @return bool
- */
- function normaliseParams( $image, &$params ) {
- $mimeType = $image->getMimeType();
-
- if ( !isset( $params['width'] ) ) {
- return false;
- }
-
- if ( !isset( $params['page'] ) ) {
- $params['page'] = 1;
- } else {
- if ( $params['page'] > $image->pageCount() ) {
- $params['page'] = $image->pageCount();
- }
-
- if ( $params['page'] < 1 ) {
- $params['page'] = 1;
- }
- }
-
- $srcWidth = $image->getWidth( $params['page'] );
- $srcHeight = $image->getHeight( $params['page'] );
-
- if ( isset( $params['height'] ) && $params['height'] != -1 ) {
- # Height & width were both set
- if ( $params['width'] * $srcHeight > $params['height'] * $srcWidth ) {
- # Height is the relative smaller dimension, so scale width accordingly
- $params['width'] = self::fitBoxWidth( $srcWidth, $srcHeight, $params['height'] );
-
- if ( $params['width'] == 0 ) {
- # Very small image, so we need to rely on client side scaling :(
- $params['width'] = 1;
- }
-
- $params['physicalWidth'] = $params['width'];
- } else {
- # Height was crap, unset it so that it will be calculated later
- unset( $params['height'] );
- }
- }
-
- if ( !isset( $params['physicalWidth'] ) ) {
- # Passed all validations, so set the physicalWidth
- $params['physicalWidth'] = $params['width'];
- }
-
- # Because thumbs are only referred to by width, the height always needs
- # to be scaled by the width to keep the thumbnail sizes consistent,
- # even if it was set inside the if block above
- $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight,
- $params['physicalWidth'] );
-
- # Set the height if it was not validated in the if block higher up
- if ( !isset( $params['height'] ) || $params['height'] == -1 ) {
- $params['height'] = $params['physicalHeight'];
- }
-
-
- if ( !$this->validateThumbParams( $params['physicalWidth'],
- $params['physicalHeight'], $srcWidth, $srcHeight, $mimeType ) ) {
- return false;
- }
- return true;
- }
-
- /**
- * Validate thumbnail parameters and fill in the correct height
- *
- * @param $width Integer: specified width (input/output)
- * @param $height Integer: height (output only)
- * @param $srcWidth Integer: width of the source image
- * @param $srcHeight Integer: height of the source image
- * @param $mimeType Unused
- * @return false to indicate that an error should be returned to the user.
- */
- function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) {
- $width = intval( $width );
-
- # Sanity check $width
- if( $width <= 0) {
- wfDebug( __METHOD__.": Invalid destination width: $width\n" );
- return false;
- }
- if ( $srcWidth <= 0 ) {
- wfDebug( __METHOD__.": Invalid source width: $srcWidth\n" );
- return false;
- }
-
- $height = File::scaleHeight( $srcWidth, $srcHeight, $width );
- if ( $height == 0 ) {
- # Force height to be at least 1 pixel
- $height = 1;
- }
- return true;
- }
-
- /**
- * @param $image File
- * @param $script
- * @param $params
- * @return bool|ThumbnailImage
- */
- function getScriptedTransform( $image, $script, $params ) {
- if ( !$this->normaliseParams( $image, $params ) ) {
- return false;
- }
- $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) );
- $page = isset( $params['page'] ) ? $params['page'] : false;
-
- if( $image->mustRender() || $params['width'] < $image->getWidth() ) {
- return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page );
- }
- }
-
- function getImageSize( $image, $path ) {
- wfSuppressWarnings();
- $gis = getimagesize( $path );
- wfRestoreWarnings();
- return $gis;
- }
-
- function isAnimatedImage( $image ) {
- return false;
- }
-
- /**
- * @param $file File
- * @return string
- */
- function getShortDesc( $file ) {
- global $wgLang;
- $nbytes = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) );
- $widthheight = wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->escaped();
-
- return "$widthheight ($nbytes)";
- }
-
- /**
- * @param $file File
- * @return string
- */
- function getLongDesc( $file ) {
- global $wgLang;
- $pages = $file->pageCount();
- $size = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) );
- if ( $pages === false || $pages <= 1 ) {
- $msg = wfMessage( 'file-info-size' )->numParams( $file->getWidth(),
- $file->getHeight() )->params( $size,
- $file->getMimeType() )->parse();
- } else {
- $msg = wfMessage( 'file-info-size-pages' )->numParams( $file->getWidth(),
- $file->getHeight() )->params( $size,
- $file->getMimeType() )->numParams( $pages )->parse();
- }
- return $msg;
- }
-
- /**
- * @param $file File
- * @return string
- */
- function getDimensionsString( $file ) {
- $pages = $file->pageCount();
- if ( $pages > 1 ) {
- return wfMessage( 'widthheightpage' )->numParams( $file->getWidth(), $file->getHeight(), $pages )->text();
- } else {
- return wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->text();
- }
- }
-}
diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php
index fcfb2f45..773824cb 100644
--- a/includes/media/MediaTransformOutput.php
+++ b/includes/media/MediaTransformOutput.php
@@ -2,6 +2,21 @@
/**
* Base class for the output of file transformation methods.
*
+ * 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 Media
*/
@@ -21,28 +36,37 @@ abstract class MediaTransformOutput {
protected $storagePath = false;
/**
- * Get the width of the output box
+ * @return integer Width of the output box
*/
public function getWidth() {
return $this->width;
}
/**
- * Get the height of the output box
+ * @return integer Height of the output box
*/
public function getHeight() {
return $this->height;
}
/**
- * @return string The thumbnail URL
+ * Get the final extension of the thumbnail.
+ * Returns false for scripted transformations.
+ * @return string|false
+ */
+ public function getExtension() {
+ return $this->path ? FileBackend::extensionFromPath( $this->path ) : false;
+ }
+
+ /**
+ * @return string|false The thumbnail URL
*/
public function getUrl() {
return $this->url;
}
/**
- * @return string|false The permanent thumbnail storage path
+ * @return string|bool The permanent thumbnail storage path
*/
public function getStoragePath() {
return $this->storagePath;
@@ -69,7 +93,7 @@ abstract class MediaTransformOutput {
* custom-url-link Custom URL to link to
* custom-title-link Custom Title object to link to
* valign vertical-align property, if the output is an inline element
- * img-class Class applied to the <img> tag, if there is such a tag
+ * img-class Class applied to the "<img>" tag, if there is such a tag
*
* For images, desc-link and file-link are implemented as a click-through. For
* sounds and videos, they may be displayed in other ways.
@@ -80,6 +104,7 @@ abstract class MediaTransformOutput {
/**
* This will be overridden to return true in error classes
+ * @return bool
*/
public function isError() {
return false;
@@ -90,7 +115,7 @@ abstract class MediaTransformOutput {
* This will return false if there was an error, the
* thumbnail is to be handled client-side only, or if
* transformation was deferred via TRANSFORM_LATER.
- *
+ *
* @return Bool
*/
public function hasFile() {
@@ -113,7 +138,7 @@ abstract class MediaTransformOutput {
* Get the path of a file system copy of the thumbnail.
* Callers should never write to this path.
*
- * @return string|false Returns false if there isn't one
+ * @return string|bool Returns false if there isn't one
*/
public function getLocalCopyPath() {
if ( $this->isError() ) {
@@ -132,7 +157,14 @@ abstract class MediaTransformOutput {
* @return Bool success
*/
public function streamFile( $headers = array() ) {
- return $this->path && StreamFile::stream( $this->getLocalCopyPath(), $headers );
+ if ( !$this->path ) {
+ return false;
+ } elseif ( FileBackend::isStoragePath( $this->path ) ) {
+ $be = $this->file->getRepo()->getBackend();
+ return $be->streamFile( array( 'src' => $this->path, 'headers' => $headers ) )->isOK();
+ } else { // FS-file
+ return StreamFile::stream( $this->getLocalCopyPath(), $headers );
+ }
}
/**
@@ -182,25 +214,46 @@ class ThumbnailImage extends MediaTransformOutput {
* Get a thumbnail object from a file and parameters.
* If $path is set to null, the output file is treated as a source copy.
* If $path is set to false, no output file will be created.
- *
+ * $parameters should include, as a minimum, (file) 'width' and 'height'.
+ * It may also include a 'page' parameter for multipage files.
+ *
* @param $file File object
* @param $url String: URL path to the thumb
- * @param $width Integer: file's width
- * @param $height Integer: file's height
- * @param $path String|false|null: filesystem path to the thumb
- * @param $page Integer: page number, for multipage files
+ * @param $path String|bool|null: filesystem path to the thumb
+ * @param $parameters Array: Associative array of parameters
* @private
*/
- function __construct( $file, $url, $width, $height, $path = false, $page = false ) {
+ 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
+ );
+ $actualParams = $parameters + $defaults;
+ } else {
+ # Using old format, should convert. Later a warning could be added here.
+ $numArgs = func_num_args();
+ $actualParams = array(
+ 'width' => $path,
+ 'height' => $parameters,
+ 'page' => ( $numArgs > 5 ) ? func_get_arg( 5 ) : false
+ );
+ $path = ( $numArgs > 4 ) ? func_get_arg( 4 ) : false;
+ }
+
$this->file = $file;
$this->url = $url;
+ $this->path = $path;
+
# These should be integers when they get here.
# If not, there's a bug somewhere. But let's at
# least produce valid HTML code regardless.
- $this->width = round( $width );
- $this->height = round( $height );
- $this->path = $path;
- $this->page = $page;
+ $this->width = round( $actualParams['width'] );
+ $this->height = round( $actualParams['height'] );
+
+ $this->page = $actualParams['page'];
}
/**
@@ -221,6 +274,9 @@ class ThumbnailImage extends MediaTransformOutput {
* 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
+ * parser-extlink-* Attributes added by parser for external links:
+ * parser-extlink-rel: add rel="nofollow"
+ * parser-extlink-target: link target, but overridden by custom-target-link
*
* For images, desc-link and file-link are implemented as a click-through. For
* sounds and videos, they may be displayed in other ways.
@@ -243,6 +299,11 @@ class ThumbnailImage extends MediaTransformOutput {
}
if ( !empty( $options['custom-target-link'] ) ) {
$linkAttribs['target'] = $options['custom-target-link'];
+ } elseif ( !empty( $options['parser-extlink-target'] ) ) {
+ $linkAttribs['target'] = $options['parser-extlink-target'];
+ }
+ if ( !empty( $options['parser-extlink-rel'] ) ) {
+ $linkAttribs['rel'] = $options['parser-extlink-rel'];
}
} elseif ( !empty( $options['custom-title-link'] ) ) {
$title = $options['custom-title-link'];
@@ -326,6 +387,6 @@ class TransformParameterError extends MediaTransformError {
parent::__construct( 'thumbnail_error',
max( isset( $params['width'] ) ? $params['width'] : 0, 120 ),
max( isset( $params['height'] ) ? $params['height'] : 0, 120 ),
- wfMsg( 'thumbnail_invalid_params' ) );
+ wfMessage( 'thumbnail_invalid_params' )->text() );
}
}
diff --git a/includes/media/PNG.php b/includes/media/PNG.php
index 8fe9ecb4..1b329e57 100644
--- a/includes/media/PNG.php
+++ b/includes/media/PNG.php
@@ -2,6 +2,21 @@
/**
* Handler for PNG images.
*
+ * 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 Media
*/
@@ -65,6 +80,14 @@ class PNGHandler extends BitmapHandler {
}
return false;
}
+ /**
+ * We do not support making APNG thumbnails, so always false
+ * @param $image File
+ * @return bool false
+ */
+ function canAnimateThumbnail( $image ) {
+ return false;
+ }
function getMetadataType( $image ) {
return 'parsed-png';
@@ -113,13 +136,13 @@ class PNGHandler extends BitmapHandler {
$info[] = $original;
if ( $metadata['loopCount'] == 0 ) {
- $info[] = wfMsgExt( 'file-info-png-looped', 'parseinline' );
+ $info[] = wfMessage( 'file-info-png-looped' )->parse();
} elseif ( $metadata['loopCount'] > 1 ) {
- $info[] = wfMsgExt( 'file-info-png-repeat', 'parseinline', $metadata['loopCount'] );
+ $info[] = wfMessage( 'file-info-png-repeat' )->numParams( $metadata['loopCount'] )->parse();
}
if ( $metadata['frameCount'] > 0 ) {
- $info[] = wfMsgExt( 'file-info-png-frames', 'parseinline', $metadata['frameCount'] );
+ $info[] = wfMessage( 'file-info-png-frames' )->numParams( $metadata['frameCount'] )->parse();
}
if ( $metadata['duration'] ) {
diff --git a/includes/media/PNGMetadataExtractor.php b/includes/media/PNGMetadataExtractor.php
index d3c44d4f..9dcde406 100644
--- a/includes/media/PNGMetadataExtractor.php
+++ b/includes/media/PNGMetadataExtractor.php
@@ -1,10 +1,26 @@
<?php
/**
* PNG frame counter and metadata extractor.
+ *
* Slightly derived from GIFMetadataExtractor.php
* Deliberately not using MWExceptions to avoid external dependencies, encouraging
* redistribution.
*
+ * 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 Media
*/
diff --git a/includes/media/SVG.php b/includes/media/SVG.php
index aac838e1..55fa5547 100644
--- a/includes/media/SVG.php
+++ b/includes/media/SVG.php
@@ -2,6 +2,21 @@
/**
* Handler for SVG images.
*
+ * 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 Media
*/
@@ -49,6 +64,13 @@ class SvgHandler extends ImageHandler {
}
/**
+ * We do not support making animated svg thumbnails
+ */
+ function canAnimateThumb( $file ) {
+ return false;
+ }
+
+ /**
* @param $image File
* @param $params
* @return bool
@@ -93,20 +115,20 @@ class SvgHandler extends ImageHandler {
$clientHeight = $params['height'];
$physicalWidth = $params['physicalWidth'];
$physicalHeight = $params['physicalHeight'];
- $srcPath = $image->getLocalRefPath();
if ( $flags & self::TRANSFORM_LATER ) {
- return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
}
if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
- wfMsg( 'thumbnail_dest_directory' ) );
+ wfMessage( 'thumbnail_dest_directory' )->text() );
}
+ $srcPath = $image->getLocalRefPath();
$status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight );
if( $status === true ) {
- return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
} else {
return $status; // MediaTransformError
}
@@ -119,7 +141,7 @@ class SvgHandler extends ImageHandler {
* @param string $dstPath
* @param string $width
* @param string $height
- * @return true|MediaTransformError
+ * @return bool|MediaTransformError
*/
public function rasterize( $srcPath, $dstPath, $width, $height ) {
global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
@@ -199,15 +221,30 @@ class SvgHandler extends ImageHandler {
}
/**
+ * Subtitle for the image. Different from the base
+ * class so it can be denoted that SVG's have
+ * a "nominal" resolution, and not a fixed one,
+ * as well as so animation can be denoted.
+ *
* @param $file File
* @return string
*/
function getLongDesc( $file ) {
global $wgLang;
- return wfMsgExt( 'svg-long-desc', 'parseinline',
- $wgLang->formatNum( $file->getWidth() ),
- $wgLang->formatNum( $file->getHeight() ),
- $wgLang->formatSize( $file->getSize() ) );
+ $size = $wgLang->formatSize( $file->getSize() );
+
+ if ( $this->isAnimatedImage( $file ) ) {
+ $msg = wfMessage( 'svg-long-desc-animated' );
+ } else {
+ $msg = wfMessage( 'svg-long-desc' );
+ }
+
+ $msg->numParams(
+ $file->getWidth(),
+ $file->getHeight()
+ );
+ $msg->Params( $size );
+ return $msg->parse();
}
function getMetadata( $file, $filename ) {
@@ -238,11 +275,19 @@ class SvgHandler extends ImageHandler {
}
function isMetadataValid( $image, $metadata ) {
- return $this->unpackMetadata( $metadata ) !== false;
+ $meta = $this->unpackMetadata( $metadata );
+ if ( $meta === false ) {
+ return self::METADATA_BAD;
+ }
+ if ( !isset( $meta['originalWidth'] ) ) {
+ // Old but compatible
+ return self::METADATA_COMPATIBLE;
+ }
+ return self::METADATA_GOOD;
}
function visibleMetadataFields() {
- $fields = array( 'title', 'description', 'animated' );
+ $fields = array( 'objectname', 'imagedescription' );
return $fields;
}
@@ -263,8 +308,6 @@ class SvgHandler extends ImageHandler {
if ( !$metadata ) {
return false;
}
- unset( $metadata['version'] );
- unset( $metadata['metadata'] ); /* non-formatted XML */
/* TODO: add a formatter
$format = new FormatSVG( $metadata );
@@ -275,9 +318,10 @@ class SvgHandler extends ImageHandler {
$visibleFields = $this->visibleMetadataFields();
// Rename fields to be compatible with exif, so that
- // the labels for these fields work.
- $conversion = array( 'width' => 'imagewidth',
- 'height' => 'imagelength',
+ // the labels for these fields work and reuse existing messages.
+ $conversion = array(
+ 'originalwidth' => 'imagewidth',
+ 'originalheight' => 'imagelength',
'description' => 'imagedescription',
'title' => 'objectname',
);
@@ -285,6 +329,9 @@ class SvgHandler extends ImageHandler {
$tag = strtolower( $name );
if ( isset( $conversion[$tag] ) ) {
$tag = $conversion[$tag];
+ } else {
+ // Do not output other metadata not in list
+ continue;
}
self::addMeta( $result,
in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php
index db9f05fd..851fe428 100644
--- a/includes/media/SVGMetadataExtractor.php
+++ b/includes/media/SVGMetadataExtractor.php
@@ -1,6 +1,6 @@
<?php
/**
- * SVGMetadataExtractor.php
+ * Extraction of SVG image metadata.
*
* 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
@@ -19,12 +19,15 @@
*
* @file
* @ingroup Media
- * @author Derk-Jan Hartman <hartman _at_ videolan d0t org>
+ * @author "Derk-Jan Hartman <hartman _at_ videolan d0t org>"
* @author Brion Vibber
* @copyright Copyright © 2010-2010 Brion Vibber, Derk-Jan Hartman
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
*/
+/**
+ * @ingroup Media
+ */
class SVGMetadataExtractor {
static function getMetadata( $filename ) {
$svg = new SVGReader( $filename );
@@ -32,6 +35,9 @@ class SVGMetadataExtractor {
}
}
+/**
+ * @ingroup Media
+ */
class SVGReader {
const DEFAULT_WIDTH = 512;
const DEFAULT_HEIGHT = 512;
@@ -77,6 +83,12 @@ class SVGReader {
$this->metadata['width'] = self::DEFAULT_WIDTH;
$this->metadata['height'] = self::DEFAULT_HEIGHT;
+ // The size in the units specified by the SVG file
+ // (for the metadata box)
+ // Per the SVG spec, if unspecified, default to '100%'
+ $this->metadata['originalWidth'] = '100%';
+ $this->metadata['originalHeight'] = '100%';
+
// Because we cut off the end of the svg making an invalid one. Complicated
// try catch thing to make sure warnings get restored. Seems like there should
// be a better way.
@@ -84,6 +96,8 @@ class SVGReader {
try {
$this->read();
} 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();
throw $e;
}
@@ -99,6 +113,7 @@ class SVGReader {
/**
* Read the SVG
+ * @return bool
*/
public function read() {
$keepReading = $this->reader->read();
@@ -132,6 +147,11 @@ class SVGReader {
$this->readField( $tag, 'description' );
} elseif ( $isSVG && $tag == 'metadata' && $type == XmlReader::ELEMENT ) {
$this->readXml( $tag, 'metadata' );
+ } elseif ( $isSVG && $tag == 'script' ) {
+ // We normally do not allow scripted svgs.
+ // However its possible to configure MW to let them
+ // in, and such files should be considered animated.
+ $this->metadata['animated'] = true;
} elseif ( $tag !== '#text' ) {
$this->debug( "Unhandled top-level XML tag $tag" );
@@ -212,6 +232,11 @@ class SVGReader {
break;
} elseif ( $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::ELEMENT ) {
switch( $this->reader->localName ) {
+ case 'script':
+ // Normally we disallow files with
+ // <script>, but its possible
+ // to configure MW to disable
+ // such checks.
case 'animate':
case 'set':
case 'animateMotion':
@@ -248,7 +273,7 @@ class SVGReader {
/**
* Parse the attributes of an SVG element
*
- * The parser has to be in the start element of <svg>
+ * The parser has to be in the start element of "<svg>"
*/
private function handleSVGAttribs( ) {
$defaultWidth = self::DEFAULT_WIDTH;
@@ -271,9 +296,11 @@ class SVGReader {
}
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 );
+ $this->metadata['originalHeight'] = $this->reader->getAttribute( 'height' );
}
if( !isset( $width ) && !isset( $height ) ) {
diff --git a/includes/media/Tiff.php b/includes/media/Tiff.php
index 0f317e1a..d95c9074 100644
--- a/includes/media/Tiff.php
+++ b/includes/media/Tiff.php
@@ -2,6 +2,21 @@
/**
* Handler for Tiff images.
*
+ * 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 Media
*/
diff --git a/includes/media/XCF.php b/includes/media/XCF.php
index 806db73c..555fa1fb 100644
--- a/includes/media/XCF.php
+++ b/includes/media/XCF.php
@@ -7,6 +7,21 @@
* Specification in Gnome repository:
* http://svn.gnome.org/viewvc/gimp/trunk/devel-docs/xcf.txt?view=markup
*
+ * 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 Media
*/
@@ -58,7 +73,7 @@ class XCFHandler extends BitmapHandler {
* @author Hashar
*
* @param $filename String Full path to a XCF file
- * @return false|metadata array just like PHP getimagesize()
+ * @return bool|array metadata array just like PHP getimagesize()
*/
static function getXCFMetaData( $filename ) {
# Decode master structure
diff --git a/includes/media/XMP.php b/includes/media/XMP.php
index 0dbf5632..36660b3d 100644
--- a/includes/media/XMP.php
+++ b/includes/media/XMP.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Reader for XMP data containing properties relevant to images.
+ *
+ * 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 Media
+ */
+
+/**
* Class for reading xmp data containing properties relevant to
* images, and spitting out an array that FormatExif accepts.
*
@@ -191,10 +213,16 @@ class XMPReader {
unset( $data['xmp-special'] );
// Convert GPSAltitude to negative if below sea level.
- if ( isset( $data['xmp-exif']['GPSAltitudeRef'] ) ) {
- if ( $data['xmp-exif']['GPSAltitudeRef'] == '1'
- && isset( $data['xmp-exif']['GPSAltitude'] )
- ) {
+ if ( isset( $data['xmp-exif']['GPSAltitudeRef'] )
+ && isset( $data['xmp-exif']['GPSAltitude'] )
+ ) {
+
+ // Must convert to a real before multiplying by -1
+ // XMPValidate guarantees there will always be a '/' in this value.
+ list( $nom, $denom ) = explode( '/', $data['xmp-exif']['GPSAltitude'] );
+ $data['xmp-exif']['GPSAltitude'] = $nom / $denom;
+
+ if ( $data['xmp-exif']['GPSAltitudeRef'] == '1' ) {
$data['xmp-exif']['GPSAltitude'] *= -1;
}
unset( $data['xmp-exif']['GPSAltitudeRef'] );
@@ -439,13 +467,15 @@ class XMPReader {
* 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>.
+ * Note this means we hit the closing element not the "</rdf:Seq>".
*
- * For example, when processing:
+ * @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.
+ * This method is called when we hit the "</exif:ISOSpeedRatings>" tag.
*
* @param $elm String namespace . space . tag name.
*/
@@ -501,15 +531,17 @@ 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>
+ * Note we still have to hit the outer "</property>"
*
- * For example, when processing:
+ * @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>.
+ * This method is called when we hit the "</rdf:Seq>".
* (For comparison, we call endElementModeSimple when we
- * hit the </rdf:li>)
+ * hit the "</rdf:li>")
*
* @param $elm String namespace . ' ' . element name
*/
@@ -988,7 +1020,7 @@ class XMPReader {
* Also does some initial set up for the wrapper element
*
* @param $parser XMLParser
- * @param $elm String namespace <space> element
+ * @param $elm String namespace "<space>" element
* @param $attribs Array attribute name => value
*/
function startElement( $parser, $elm, $attribs ) {
@@ -1071,11 +1103,13 @@ class XMPReader {
* Process attributes.
* Simple values can be stored as either a tag or attribute
*
- * Often the initial <rdf:Description> tag just has all the simple
+ * Often the initial "<rdf:Description>" tag just has all the simple
* properties as attributes.
*
- * Example:
+ * @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.
*/
diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php
index 156d9b50..83b8a102 100644
--- a/includes/media/XMPInfo.php
+++ b/includes/media/XMPInfo.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Definitions for XMPReader class.
+ *
+ * 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 Media
+ */
+
+/**
* This class is just a container for a big array
* used by XMPReader to determine which XMP items to
* extract.
diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php
index 600d99de..5ce3c00b 100644
--- a/includes/media/XMPValidate.php
+++ b/includes/media/XMPValidate.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Methods for validating XMP properties.
+ *
+ * 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 Media
+ */
+
+/**
* This contains some static methods for
* validating XMP properties. See XMPInfo and XMPReader classes.
*
diff --git a/includes/mobile/DeviceDetection.php b/includes/mobile/DeviceDetection.php
new file mode 100644
index 00000000..262665be
--- /dev/null
+++ b/includes/mobile/DeviceDetection.php
@@ -0,0 +1,459 @@
+<?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/RandomTest.php b/includes/normal/RandomTest.php
index d96cb09a..23471e94 100644
--- a/includes/normal/RandomTest.php
+++ b/includes/normal/RandomTest.php
@@ -57,10 +57,6 @@ function donorm( $str ) {
return rtrim( utf8_normalize( $str . "\x01", UtfNormal::UNORM_NFC ), "\x01" );
}
-function wfMsg($x) {
- return $x;
-}
-
function showDiffs( $a, $b ) {
$ota = explode( "\n", str_replace( "\r\n", "\n", $a ) );
$nta = explode( "\n", str_replace( "\r\n", "\n", $b ) );
diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php
index b5aad301..08f85bd3 100644
--- a/includes/normal/UtfNormal.php
+++ b/includes/normal/UtfNormal.php
@@ -190,7 +190,7 @@ class UtfNormal {
*/
static function loadData() {
if( !isset( self::$utfCombiningClass ) ) {
- require_once( dirname(__FILE__) . '/UtfNormalData.inc' );
+ require_once( __DIR__ . '/UtfNormalData.inc' );
}
}
@@ -238,6 +238,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.
+ * @return bool
*/
static function quickIsNFCVerify( &$string ) {
# Screen out some characters that eg won't be allowed in XML
diff --git a/includes/normal/UtfNormalDefines.php b/includes/normal/UtfNormalDefines.php
index 6c4d8b76..5142a414 100644
--- a/includes/normal/UtfNormalDefines.php
+++ b/includes/normal/UtfNormalDefines.php
@@ -6,6 +6,21 @@
* since this file will not be executed during request startup for a compiled
* MediaWiki.
*
+ * 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 UtfNormal
*/
diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php
index e5ae7f72..5872ec34 100644
--- a/includes/normal/UtfNormalTest.php
+++ b/includes/normal/UtfNormalTest.php
@@ -37,6 +37,7 @@ if( defined( 'PRETTY_UTF8' ) ) {
} else {
/**
* @ignore
+ * @return string
*/
function pretty( $string ) {
return trim( preg_replace( '/(.)/use',
diff --git a/includes/normal/UtfNormalTest2.php b/includes/normal/UtfNormalTest2.php
index 28be4838..691bfaa7 100644
--- a/includes/normal/UtfNormalTest2.php
+++ b/includes/normal/UtfNormalTest2.php
@@ -1,7 +1,22 @@
#!/usr/bin/php
<?php
/**
- * Other tests for the unicode normalization module
+ * Other tests for the unicode normalization module.
+ *
+ * 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 UtfNormal
@@ -61,6 +76,7 @@ function normalize_form_kd($c) { return UtfNormal::toNFKD($c); }
* following functions to force pure PHP usage. I decided not to
* commit that code since might produce a slowdown in the UTF
* normalization code just for the sake of these tests. -- hexmode
+ * @return string
*/
function normalize_form_c_php($c) { return UtfNormal::toNFC($c, "php"); }
function normalize_form_d_php($c) { return UtfNormal::toNFD($c, "php"); }
diff --git a/includes/objectcache/APCBagOStuff.php b/includes/objectcache/APCBagOStuff.php
index dd4a76e1..1a0de218 100644
--- a/includes/objectcache/APCBagOStuff.php
+++ b/includes/objectcache/APCBagOStuff.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Object caching using PHP's APC accelerator.
+ *
+ * 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
+ */
/**
* This is a wrapper for APC's shared memory functions
@@ -6,28 +27,62 @@
* @ingroup Cache
*/
class APCBagOStuff extends BagOStuff {
+ /**
+ * @param $key string
+ * @return mixed
+ */
public function get( $key ) {
$val = apc_fetch( $key );
if ( is_string( $val ) ) {
- $val = unserialize( $val );
+ if ( $this->isInteger( $val ) ) {
+ $val = intval( $val );
+ } else {
+ $val = unserialize( $val );
+ }
}
return $val;
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
public function set( $key, $value, $exptime = 0 ) {
- apc_store( $key, serialize( $value ), $exptime );
+ if ( !$this->isInteger( $value ) ) {
+ $value = serialize( $value );
+ }
+
+ apc_store( $key, $value, $exptime );
return true;
}
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
public function delete( $key, $time = 0 ) {
apc_delete( $key );
return true;
}
+ public function incr( $key, $value = 1 ) {
+ return apc_inc( $key, $value );
+ }
+
+ 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'];
@@ -40,4 +95,3 @@ class APCBagOStuff extends BagOStuff {
return $keys;
}
}
-
diff --git a/includes/objectcache/BagOStuff.php b/includes/objectcache/BagOStuff.php
index 81ad6621..7bbaff93 100644
--- a/includes/objectcache/BagOStuff.php
+++ b/includes/objectcache/BagOStuff.php
@@ -56,8 +56,7 @@ abstract class BagOStuff {
/**
* Get an item with the given key. Returns false if it does not exist.
* @param $key string
- *
- * @return bool|Object
+ * @return mixed Returns false on failure
*/
abstract public function get( $key );
@@ -66,6 +65,7 @@ abstract class BagOStuff {
* @param $key string
* @param $value mixed
* @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ * @return bool success
*/
abstract public function set( $key, $value, $exptime = 0 );
@@ -73,19 +73,33 @@ abstract class BagOStuff {
* Delete an item.
* @param $key string
* @param $time int 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 );
+ /**
+ * @param $key string
+ * @param $timeout integer
+ * @return bool success
+ */
public function lock( $key, $timeout = 0 ) {
/* stub */
return true;
}
+ /**
+ * @param $key string
+ * @return bool success
+ */
public function unlock( $key ) {
/* stub */
return true;
}
+ /**
+ * @todo: what is this?
+ * @return Array
+ */
public function keys() {
/* stub */
return array();
@@ -93,12 +107,12 @@ abstract class BagOStuff {
/**
* Delete all objects expiring before a certain date.
- * @param $date The reference date in MW format
- * @param $progressCallback Optional, a function which will be called
+ * @param $date string 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.
*
- * @return true on success, false if unimplemented
+ * @return bool on success, false if unimplemented
*/
public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) {
// stub
@@ -107,45 +121,83 @@ abstract class BagOStuff {
/* *** Emulated functions *** */
- public function add( $key, $value, $exptime = 0 ) {
- if ( !$this->get( $key ) ) {
- $this->set( $key, $value, $exptime );
+ /**
+ * Get an associative array containing the item for each of the keys that have items.
+ * @param $keys Array List of strings
+ * @return Array
+ */
+ public function getMulti( array $keys ) {
+ $res = array();
+ foreach ( $keys as $key ) {
+ $val = $this->get( $key );
+ if ( $val !== false ) {
+ $res[$key] = $val;
+ }
+ }
+ return $res;
+ }
- return true;
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime integer
+ * @return bool success
+ */
+ public function add( $key, $value, $exptime = 0 ) {
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime );
}
+ return false; // key already set
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool success
+ */
public function replace( $key, $value, $exptime = 0 ) {
if ( $this->get( $key ) !== false ) {
- $this->set( $key, $value, $exptime );
+ return $this->set( $key, $value, $exptime );
}
+ return false; // key not already set
}
/**
+ * Increase stored value of $key by $value while preserving its TTL
* @param $key String: Key to increase
* @param $value Integer: Value to add to $key (Default 1)
- * @return null if lock is not possible else $key value increased by $value
+ * @return integer|bool New value or false on failure
*/
public function incr( $key, $value = 1 ) {
if ( !$this->lock( $key ) ) {
- return null;
+ return false;
}
-
- $value = intval( $value );
-
- if ( ( $n = $this->get( $key ) ) !== false ) {
- $n += $value;
- $this->set( $key, $n ); // exptime?
+ $n = $this->get( $key );
+ if ( $this->isInteger( $n ) ) { // key exists?
+ $n += intval( $value );
+ $this->set( $key, max( 0, $n ) ); // exptime?
+ } else {
+ $n = false;
}
$this->unlock( $key );
return $n;
}
+ /**
+ * Decrease stored value of $key by $value while preserving its TTL
+ * @param $key String
+ * @param $value Integer
+ * @return integer
+ */
public function decr( $key, $value = 1 ) {
return $this->incr( $key, - $value );
}
+ /**
+ * @param $text string
+ */
public function debug( $text ) {
if ( $this->debugMode ) {
$class = get_class( $this );
@@ -155,6 +207,8 @@ abstract class BagOStuff {
/**
* Convert an optionally relative time to an absolute time
+ * @param $exptime integer
+ * @return int
*/
protected function convertExpiry( $exptime ) {
if ( ( $exptime != 0 ) && ( $exptime < 86400 * 3650 /* 10 years */ ) ) {
@@ -163,6 +217,33 @@ abstract class BagOStuff {
return $exptime;
}
}
-}
+ /**
+ * Convert an optionally absolute expiry time to a relative time. If an
+ * absolute time is specified which is in the past, use a short expiry time.
+ *
+ * @param $exptime integer
+ * @return integer
+ */
+ protected function convertToRelative( $exptime ) {
+ if ( $exptime >= 86400 * 3650 /* 10 years */ ) {
+ $exptime -= time();
+ if ( $exptime <= 0 ) {
+ $exptime = 1;
+ }
+ return $exptime;
+ } else {
+ return $exptime;
+ }
+ }
+ /**
+ * Check if a value is an integer
+ *
+ * @param $value mixed
+ * @return bool
+ */
+ protected function isInteger( $value ) {
+ return ( is_int( $value ) || ctype_digit( $value ) );
+ }
+}
diff --git a/includes/objectcache/DBABagOStuff.php b/includes/objectcache/DBABagOStuff.php
index ade8c0a9..36ced496 100644
--- a/includes/objectcache/DBABagOStuff.php
+++ b/includes/objectcache/DBABagOStuff.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Object caching using DBA 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
+ * @ingroup Cache
+ */
/**
* Cache that uses DBA as a backend.
@@ -7,23 +28,24 @@
* for systems that don't have it.
*
* On construction you can pass array( 'dir' => '/some/path' ); as a parameter
- * to override the default DBA files directory (wgTmpDirectory).
+ * to override the default DBA files directory (wfTempDir()).
*
* @ingroup Cache
*/
class DBABagOStuff extends BagOStuff {
var $mHandler, $mFile, $mReader, $mWriter, $mDisabled;
+ /**
+ * @param $params array
+ */
public function __construct( $params ) {
global $wgDBAhandler;
if ( !isset( $params['dir'] ) ) {
- global $wgTmpDirectory;
- $params['dir'] = $wgTmpDirectory;
+ $params['dir'] = wfTempDir();
}
- $this->mFile = $params['dir']."/mw-cache-" . wfWikiID();
- $this->mFile .= '.db';
+ $this->mFile = $params['dir'] . '/mw-cache-' . wfWikiID() . '.db';
wfDebug( __CLASS__ . ": using cache file {$this->mFile}\n" );
$this->mHandler = $wgDBAhandler;
}
@@ -35,7 +57,7 @@ class DBABagOStuff extends BagOStuff {
*
* @return string
*/
- function encode( $value, $expiry ) {
+ protected function encode( $value, $expiry ) {
# Convert to absolute time
$expiry = $this->convertExpiry( $expiry );
@@ -43,11 +65,12 @@ class DBABagOStuff extends BagOStuff {
}
/**
+ * @param $blob string
* @return array list containing value first and expiry second
*/
- function decode( $blob ) {
+ protected function decode( $blob ) {
if ( !is_string( $blob ) ) {
- return array( null, 0 );
+ return array( false, 0 );
} else {
return array(
unserialize( substr( $blob, 11 ) ),
@@ -56,7 +79,10 @@ class DBABagOStuff extends BagOStuff {
}
}
- function getReader() {
+ /**
+ * @return resource
+ */
+ protected function getReader() {
if ( file_exists( $this->mFile ) ) {
$handle = dba_open( $this->mFile, 'rl', $this->mHandler );
} else {
@@ -70,7 +96,10 @@ class DBABagOStuff extends BagOStuff {
return $handle;
}
- function getWriter() {
+ /**
+ * @return resource
+ */
+ protected function getWriter() {
$handle = dba_open( $this->mFile, 'cl', $this->mHandler );
if ( !$handle ) {
@@ -80,14 +109,18 @@ class DBABagOStuff extends BagOStuff {
return $handle;
}
- function get( $key ) {
+ /**
+ * @param $key string
+ * @return mixed
+ */
+ public function get( $key ) {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__ . "($key)\n" );
$handle = $this->getReader();
if ( !$handle ) {
wfProfileOut( __METHOD__ );
- return null;
+ return false;
}
$val = dba_fetch( $key, $handle );
@@ -96,20 +129,26 @@ class DBABagOStuff extends BagOStuff {
# Must close ASAP because locks are held
dba_close( $handle );
- if ( !is_null( $val ) && $expiry && $expiry < time() ) {
+ if ( $val !== false && $expiry && $expiry < time() ) {
# Key is expired, delete it
$handle = $this->getWriter();
dba_delete( $key, $handle );
dba_close( $handle );
wfDebug( __METHOD__ . ": $key expired\n" );
- $val = null;
+ $val = false;
}
wfProfileOut( __METHOD__ );
return $val;
}
- function set( $key, $value, $exptime = 0 ) {
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function set( $key, $value, $exptime = 0 ) {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__ . "($key)\n" );
@@ -128,7 +167,12 @@ class DBABagOStuff extends BagOStuff {
return $ret;
}
- function delete( $key, $time = 0 ) {
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
+ public function delete( $key, $time = 0 ) {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__ . "($key)\n" );
@@ -138,14 +182,20 @@ class DBABagOStuff extends BagOStuff {
return false;
}
- $ret = dba_delete( $key, $handle );
+ $ret = !dba_exists( $key, $handle ) || dba_delete( $key, $handle );
dba_close( $handle );
wfProfileOut( __METHOD__ );
return $ret;
}
- function add( $key, $value, $exptime = 0 ) {
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
+ public function add( $key, $value, $exptime = 0 ) {
wfProfileIn( __METHOD__ );
$blob = $this->encode( $value, $exptime );
@@ -163,7 +213,7 @@ class DBABagOStuff extends BagOStuff {
if ( !$ret ) {
list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) );
- if ( $expiry < time() ) {
+ if ( $expiry && $expiry < time() ) {
# Yes expired, delete and try again
dba_delete( $key, $handle );
$ret = dba_insert( $key, $blob, $handle );
@@ -177,6 +227,44 @@ class DBABagOStuff extends BagOStuff {
return $ret;
}
+ /**
+ * @param $key string
+ * @param $step integer
+ * @return integer|bool
+ */
+ public function incr( $key, $step = 1 ) {
+ wfProfileIn( __METHOD__ );
+
+ $handle = $this->getWriter();
+
+ if ( !$handle ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) );
+ if ( $value !== false ) {
+ if ( $expiry && $expiry < time() ) {
+ # Key is expired, delete it
+ dba_delete( $key, $handle );
+ wfDebug( __METHOD__ . ": $key expired\n" );
+ $value = false;
+ } else {
+ $value += $step;
+ $blob = $this->encode( $value, $expiry );
+
+ $ret = dba_replace( $key, $blob, $handle );
+ $value = $ret ? $value : false;
+ }
+ }
+
+ dba_close( $handle );
+
+ wfProfileOut( __METHOD__ );
+
+ return ( $value === false ) ? false : (int)$value;
+ }
+
function keys() {
$reader = $this->getReader();
$k1 = dba_firstkey( $reader );
@@ -196,4 +284,3 @@ class DBABagOStuff extends BagOStuff {
return $result;
}
}
-
diff --git a/includes/objectcache/EhcacheBagOStuff.php b/includes/objectcache/EhcacheBagOStuff.php
index 75aad27a..f86cf157 100644
--- a/includes/objectcache/EhcacheBagOStuff.php
+++ b/includes/objectcache/EhcacheBagOStuff.php
@@ -1,8 +1,31 @@
<?php
+/**
+ * Object caching using the Ehcache RESTful web service.
+ *
+ * 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
+ */
/**
* Client for the Ehcache RESTful web service - http://ehcache.org/documentation/cache_server.html
* TODO: Simplify configuration and add to the installer.
+ *
+ * @ingroup Cache
*/
class EhcacheBagOStuff extends BagOStuff {
var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions,
@@ -10,6 +33,9 @@ class EhcacheBagOStuff extends BagOStuff {
var $curls = array();
+ /**
+ * @param $params array
+ */
function __construct( $params ) {
if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) {
throw new MWException( __CLASS__.' requires curl version 7.16.2 or later.' );
@@ -36,6 +62,10 @@ class EhcacheBagOStuff extends BagOStuff {
);
}
+ /**
+ * @param $key string
+ * @return bool|mixed
+ */
public function get( $key ) {
wfProfileIn( __METHOD__ );
$response = $this->doItemRequest( $key );
@@ -70,6 +100,12 @@ class EhcacheBagOStuff extends BagOStuff {
return $data;
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $expiry int
+ * @return bool
+ */
public function set( $key, $value, $expiry = 0 ) {
wfProfileIn( __METHOD__ );
$expiry = $this->convertExpiry( $expiry );
@@ -107,6 +143,11 @@ class EhcacheBagOStuff extends BagOStuff {
return $result;
}
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
public function delete( $key, $time = 0 ) {
wfProfileIn( __METHOD__ );
$response = $this->doItemRequest( $key,
@@ -122,6 +163,10 @@ class EhcacheBagOStuff extends BagOStuff {
return $result;
}
+ /**
+ * @param $key string
+ * @return string
+ */
protected function getCacheUrl( $key ) {
if ( count( $this->servers ) == 1 ) {
$server = reset( $this->servers );
@@ -149,6 +194,13 @@ class EhcacheBagOStuff extends BagOStuff {
return $this->curls[$cacheUrl];
}
+ /**
+ * @param $key string
+ * @param $data
+ * @param $type
+ * @param $ttl
+ * @return int
+ */
protected function attemptPut( $key, $data, $type, $ttl ) {
// In initial benchmarking, it was 30 times faster to use CURLOPT_POST
// than CURLOPT_UPLOAD with CURLOPT_READFUNCTION. This was because
@@ -173,6 +225,10 @@ class EhcacheBagOStuff extends BagOStuff {
}
}
+ /**
+ * @param $key string
+ * @return bool
+ */
protected function createCache( $key ) {
wfDebug( __METHOD__.": creating cache for $key\n" );
$response = $this->doCacheRequest( $key,
@@ -185,21 +241,26 @@ class EhcacheBagOStuff extends BagOStuff {
wfDebug( __CLASS__.": failed to create cache for $key\n" );
return false;
}
- if ( $response['http_code'] == 201 /* created */
- || $response['http_code'] == 409 /* already there */ )
- {
- return true;
- } else {
- return false;
- }
+ return ( $response['http_code'] == 201 /* created */
+ || $response['http_code'] == 409 /* already there */ );
}
+ /**
+ * @param $key string
+ * @param $curlOptions array
+ * @return array|bool|mixed
+ */
protected function doCacheRequest( $key, $curlOptions = array() ) {
$cacheUrl = $this->getCacheUrl( $key );
$curl = $this->getCurl( $cacheUrl );
return $this->doRequest( $curl, $cacheUrl, $curlOptions );
}
+ /**
+ * @param $key string
+ * @param $curlOptions array
+ * @return array|bool|mixed
+ */
protected function doItemRequest( $key, $curlOptions = array() ) {
$cacheUrl = $this->getCacheUrl( $key );
$curl = $this->getCurl( $cacheUrl );
@@ -207,6 +268,13 @@ class EhcacheBagOStuff extends BagOStuff {
return $this->doRequest( $curl, $url, $curlOptions );
}
+ /**
+ * @param $curl
+ * @param $url string
+ * @param $curlOptions array
+ * @return array|bool|mixed
+ * @throws MWException
+ */
protected function doRequest( $curl, $url, $curlOptions = array() ) {
if ( array_diff_key( $curlOptions, $this->curlOptions ) ) {
// var_dump( array_diff_key( $curlOptions, $this->curlOptions ) );
diff --git a/includes/objectcache/EmptyBagOStuff.php b/includes/objectcache/EmptyBagOStuff.php
index 2aee6b12..bd28b241 100644
--- a/includes/objectcache/EmptyBagOStuff.php
+++ b/includes/objectcache/EmptyBagOStuff.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Dummy object caching.
+ *
+ * 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
+ */
/**
* A BagOStuff object with no objects in it. Used to provide a no-op object to calling code.
@@ -6,14 +27,30 @@
* @ingroup Cache
*/
class EmptyBagOStuff extends BagOStuff {
+
+ /**
+ * @param $key string
+ * @return bool
+ */
function get( $key ) {
return false;
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exp int
+ * @return bool
+ */
function set( $key, $value, $exp = 0 ) {
return true;
}
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
function delete( $key, $time = 0 ) {
return true;
}
diff --git a/includes/objectcache/HashBagOStuff.php b/includes/objectcache/HashBagOStuff.php
index 36773306..799f26a3 100644
--- a/includes/objectcache/HashBagOStuff.php
+++ b/includes/objectcache/HashBagOStuff.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Object caching using PHP arrays.
+ *
+ * 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
+ */
/**
* This is a test of the interface, mainly. It stores things in an associative
@@ -13,6 +34,10 @@ class HashBagOStuff extends BagOStuff {
$this->bag = array();
}
+ /**
+ * @param $key string
+ * @return bool
+ */
protected function expire( $key ) {
$et = $this->bag[$key][1];
@@ -25,6 +50,10 @@ class HashBagOStuff extends BagOStuff {
return true;
}
+ /**
+ * @param $key string
+ * @return bool|mixed
+ */
function get( $key ) {
if ( !isset( $this->bag[$key] ) ) {
return false;
@@ -37,10 +66,22 @@ class HashBagOStuff extends BagOStuff {
return $this->bag[$key][0];
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
function set( $key, $value, $exptime = 0 ) {
$this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) );
+ return true;
}
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
function delete( $key, $time = 0 ) {
if ( !isset( $this->bag[$key] ) ) {
return false;
@@ -51,6 +92,9 @@ 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
new file mode 100644
index 00000000..813c2727
--- /dev/null
+++ b/includes/objectcache/MemcachedBagOStuff.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Base class for memcached clients.
+ *
+ * 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
+ */
+
+/**
+ * Base class for memcached clients.
+ *
+ * @ingroup Cache
+ */
+class MemcachedBagOStuff extends BagOStuff {
+ protected $client;
+
+ /**
+ * Fill in the defaults for any parameters missing from $params, using the
+ * backwards-compatible global variables
+ */
+ protected function applyDefaultParams( $params ) {
+ if ( !isset( $params['servers'] ) ) {
+ $params['servers'] = $GLOBALS['wgMemCachedServers'];
+ }
+ if ( !isset( $params['debug'] ) ) {
+ $params['debug'] = $GLOBALS['wgMemCachedDebug'];
+ }
+ if ( !isset( $params['persistent'] ) ) {
+ $params['persistent'] = $GLOBALS['wgMemCachedPersistent'];
+ }
+ if ( !isset( $params['compress_threshold'] ) ) {
+ $params['compress_threshold'] = 1500;
+ }
+ if ( !isset( $params['timeout'] ) ) {
+ $params['timeout'] = $GLOBALS['wgMemCachedTimeout'];
+ }
+ if ( !isset( $params['connect_timeout'] ) ) {
+ $params['connect_timeout'] = 0.5;
+ }
+ return $params;
+ }
+
+ /**
+ * @param $key string
+ * @return Mixed
+ */
+ public function get( $key ) {
+ return $this->client->get( $this->encodeKey( $key ) );
+ }
+
+ /**
+ * @param $key string
+ * @param $value
+ * @param $exptime int
+ * @return bool
+ */
+ public function set( $key, $value, $exptime = 0 ) {
+ return $this->client->set( $this->encodeKey( $key ), $value,
+ $this->fixExpiry( $exptime ) );
+ }
+
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
+ public function delete( $key, $time = 0 ) {
+ return $this->client->delete( $this->encodeKey( $key ), $time );
+ }
+
+ /**
+ * @param $key string
+ * @param $value int
+ * @param $exptime int (default 0)
+ * @return Mixed
+ */
+ public function add( $key, $value, $exptime = 0 ) {
+ return $this->client->add( $this->encodeKey( $key ), $value,
+ $this->fixExpiry( $exptime ) );
+ }
+
+ /**
+ * @param $key string
+ * @param $value int
+ * @param $exptime
+ * @return Mixed
+ */
+ public function replace( $key, $value, $exptime = 0 ) {
+ return $this->client->replace( $this->encodeKey( $key ), $value,
+ $this->fixExpiry( $exptime ) );
+ }
+
+ /**
+ * Get the underlying client object. This is provided for debugging
+ * purposes.
+ */
+ public function getClient() {
+ return $this->client;
+ }
+
+ /**
+ * Encode a key for use on the wire inside the memcached protocol.
+ *
+ * We encode spaces and line breaks to avoid protocol errors. We encode
+ * the other control characters for compatibility with libmemcached
+ * verify_key. We leave other punctuation alone, to maximise backwards
+ * compatibility.
+ * @param $key string
+ * @return string
+ */
+ public function encodeKey( $key ) {
+ return preg_replace_callback( '/[\x00-\x20\x25\x7f]+/',
+ array( $this, 'encodeKeyCallback' ), $key );
+ }
+
+ /**
+ * @param $m array
+ * @return string
+ */
+ protected function encodeKeyCallback( $m ) {
+ return rawurlencode( $m[0] );
+ }
+
+ /**
+ * TTLs higher than 30 days will be detected as absolute TTLs
+ * (UNIX timestamps), and will result in the cache entry being
+ * discarded immediately because the expiry is in the past.
+ * Clamp expiries >30d at 30d, unless they're >=1e9 in which
+ * case they are likely to really be absolute (1e9 = 2011-09-09)
+ */
+ function fixExpiry( $expiry ) {
+ if ( $expiry > 2592000 && $expiry < 1000000000 ) {
+ $expiry = 2592000;
+ }
+ return $expiry;
+ }
+
+ /**
+ * Decode a key encoded with encodeKey(). This is provided as a convenience
+ * function for debugging.
+ *
+ * @param $key string
+ *
+ * @return string
+ */
+ public function decodeKey( $key ) {
+ return urldecode( $key );
+ }
+
+ /**
+ * 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 868ad69f..536ba6ea 100644
--- a/includes/objectcache/MemcachedClient.php
+++ b/includes/objectcache/MemcachedClient.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Memcached client for PHP.
+ *
* +---------------------------------------------------------------------------+
* | memcached client, PHP |
* +---------------------------------------------------------------------------+
@@ -257,7 +259,7 @@ class MWMemcached {
$this->_host_dead = array();
$this->_timeout_seconds = 0;
- $this->_timeout_microseconds = isset( $args['timeout'] ) ? $args['timeout'] : 100000;
+ $this->_timeout_microseconds = isset( $args['timeout'] ) ? $args['timeout'] : 500000;
$this->_connect_timeout = isset( $args['connect_timeout'] ) ? $args['connect_timeout'] : 0.1;
$this->_connect_attempts = 2;
@@ -328,27 +330,36 @@ class MWMemcached {
$this->stats['delete'] = 1;
}
$cmd = "delete $key $time\r\n";
- if( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
- $this->_dead_sock( $sock );
+ if( !$this->_fwrite( $sock, $cmd ) ) {
return false;
}
- $res = trim( fgets( $sock ) );
+ $res = $this->_fgets( $sock );
if ( $this->_debug ) {
$this->_debugprint( sprintf( "MemCache: delete %s (%s)\n", $key, $res ) );
}
- if ( $res == "DELETED" ) {
+ if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
return true;
}
+
return false;
}
+ /**
+ * @param $key
+ * @param $timeout int
+ * @return bool
+ */
public function lock( $key, $timeout = 0 ) {
/* stub */
return true;
}
+ /**
+ * @param $key
+ * @return bool
+ */
public function unlock( $key ) {
/* stub */
return true;
@@ -427,8 +438,7 @@ class MWMemcached {
}
$cmd = "get $key\r\n";
- if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
- $this->_dead_sock( $sock );
+ if ( !$this->_fwrite( $sock, $cmd ) ) {
wfProfileOut( __METHOD__ );
return false;
}
@@ -471,7 +481,7 @@ class MWMemcached {
$this->stats['get_multi'] = 1;
}
$sock_keys = array();
-
+ $socks = array();
foreach ( $keys as $key ) {
$sock = $this->get_sock( $key );
if ( !is_resource( $sock ) ) {
@@ -479,24 +489,23 @@ class MWMemcached {
}
$key = is_array( $key ) ? $key[1] : $key;
if ( !isset( $sock_keys[$sock] ) ) {
- $sock_keys[$sock] = array();
+ $sock_keys[ intval( $sock ) ] = array();
$socks[] = $sock;
}
- $sock_keys[$sock][] = $key;
+ $sock_keys[ intval( $sock ) ][] = $key;
}
+ $gather = array();
// Send out the requests
foreach ( $socks as $sock ) {
$cmd = 'get';
- foreach ( $sock_keys[$sock] as $key ) {
+ foreach ( $sock_keys[ intval( $sock ) ] as $key ) {
$cmd .= ' ' . $key;
}
$cmd .= "\r\n";
- if ( $this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
+ if ( $this->_fwrite( $sock, $cmd ) ) {
$gather[] = $sock;
- } else {
- $this->_dead_sock( $sock );
}
}
@@ -559,12 +568,6 @@ class MWMemcached {
* Passes through $cmd to the memcache server connected by $sock; returns
* output as an array (null array if no output)
*
- * NOTE: due to a possible bug in how PHP reads while using fgets(), each
- * line may not be terminated by a \r\n. More specifically, my testing
- * has shown that, on FreeBSD at least, each line is terminated only
- * with a \n. This is with the PHP flag auto_detect_line_endings set
- * to falase (the default).
- *
* @param $sock Resource: socket to send command on
* @param $cmd String: command to run
*
@@ -575,12 +578,13 @@ class MWMemcached {
return array();
}
- if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
+ if ( !$this->_fwrite( $sock, $cmd ) ) {
return array();
}
+ $ret = array();
while ( true ) {
- $res = fgets( $sock );
+ $res = $this->_fgets( $sock );
$ret[] = $res;
if ( preg_match( '/^END/', $res ) ) {
break;
@@ -717,15 +721,19 @@ class MWMemcached {
wfRestoreWarnings();
}
if ( !$sock ) {
- if ( $this->_debug ) {
- $this->_debugprint( "Error connecting to $host: $errstr\n" );
- }
+ $this->_error_log( "Error connecting to $host: $errstr\n" );
+ $this->_dead_host( $host );
return false;
}
// Initialise timeout
stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
+ // If the connection was persistent, flush the read buffer in case there
+ // was a previous incomplete request on this connection
+ if ( $this->_persistent ) {
+ $this->_flush_read_buffer( $sock );
+ }
return true;
}
@@ -744,6 +752,9 @@ class MWMemcached {
$this->_dead_host( $host );
}
+ /**
+ * @param $host
+ */
function _dead_host( $host ) {
$parts = explode( ':', $host );
$ip = $parts[0];
@@ -769,13 +780,12 @@ class MWMemcached {
}
if ( $this->_single_sock !== null ) {
- $this->_flush_read_buffer( $this->_single_sock );
return $this->sock_to_host( $this->_single_sock );
}
$hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
-
if ( $this->_buckets === null ) {
+ $bu = array();
foreach ( $this->_servers as $v ) {
if ( is_array( $v ) ) {
for( $i = 0; $i < $v[1]; $i++ ) {
@@ -794,7 +804,6 @@ class MWMemcached {
$host = $this->_buckets[$hv % $this->_bucketcount];
$sock = $this->sock_to_host( $host );
if ( is_resource( $sock ) ) {
- $this->_flush_read_buffer( $sock );
return $sock;
}
$hv = $this->_hashfunc( $hv . $realkey );
@@ -815,7 +824,7 @@ class MWMemcached {
* @access private
*/
function _hashfunc( $key ) {
- # Hash function must on [0,0x7ffffff]
+ # Hash function must be in [0,0x7ffffff]
# We take the first 31 bits of the MD5 hash, which unlike the hash
# function used in a previous version of this client, works
return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
@@ -850,11 +859,11 @@ class MWMemcached {
} else {
$this->stats[$cmd] = 1;
}
- if ( !$this->_safe_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
- return $this->_dead_sock( $sock );
+ if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
+ return null;
}
- $line = fgets( $sock );
+ $line = $this->_fgets( $sock );
$match = array();
if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
return null;
@@ -870,58 +879,42 @@ class MWMemcached {
*
* @param $sock Resource: socket to read from
* @param $ret Array: returned values
+ * @return boolean True for success, false for failure
*
* @access private
*/
function _load_items( $sock, &$ret ) {
while ( 1 ) {
- $decl = fgets( $sock );
- if ( $decl == "END\r\n" ) {
+ $decl = $this->_fgets( $sock );
+ if( $decl === false ) {
+ return false;
+ } elseif ( $decl == "END" ) {
return true;
- } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) {
+ } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)$/', $decl, $match ) ) {
list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] );
- $bneed = $len + 2;
- $offset = 0;
-
- while ( $bneed > 0 ) {
- $data = fread( $sock, $bneed );
- $n = strlen( $data );
- if ( $n == 0 ) {
- break;
- }
- $offset += $n;
- $bneed -= $n;
- if ( isset( $ret[$rkey] ) ) {
- $ret[$rkey] .= $data;
- } else {
- $ret[$rkey] = $data;
- }
+ $data = $this->_fread( $sock, $len + 2 );
+ if ( $data === false ) {
+ return false;
}
-
- if ( $offset != $len + 2 ) {
- // Something is borked!
- if ( $this->_debug ) {
- $this->_debugprint( sprintf( "Something is borked! key %s expecting %d got %d length\n", $rkey, $len + 2, $offset ) );
- }
-
- unset( $ret[$rkey] );
- $this->_close_sock( $sock );
+ 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] );
}
- $ret[$rkey] = rtrim( $ret[$rkey] );
-
if ( $flags & self::SERIALIZED ) {
$ret[$rkey] = unserialize( $ret[$rkey] );
}
} else {
- $this->_debugprint( "Error parsing memcached response\n" );
- return 0;
+ $this->_handle_error( $sock, 'Error parsing response from $1' );
+ return false;
}
}
}
@@ -960,15 +953,6 @@ class MWMemcached {
$this->stats[$cmd] = 1;
}
- // TTLs higher than 30 days will be detected as absolute TTLs
- // (UNIX timestamps), and will result in the cache entry being
- // discarded immediately because the expiry is in the past.
- // Clamp expiries >30d at 30d, unless they're >=1e9 in which
- // case they are likely to really be absolute (1e9 = 2011-09-09)
- if ( $exp > 2592000 && $exp < 1000000000 ) {
- $exp = 2592000;
- }
-
$flags = 0;
if ( !is_scalar( $val ) ) {
@@ -996,11 +980,11 @@ class MWMemcached {
$flags |= self::COMPRESSED;
}
}
- if ( !$this->_safe_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) {
- return $this->_dead_sock( $sock );
+ if ( !$this->_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) {
+ return false;
}
- $line = trim( fgets( $sock ) );
+ $line = $this->_fgets( $sock );
if ( $this->_debug ) {
$this->_debugprint( sprintf( "%s %s (%s)\n", $cmd, $key, $line ) );
@@ -1037,7 +1021,7 @@ class MWMemcached {
}
if ( !$this->_connect_sock( $sock, $host ) ) {
- return $this->_dead_host( $host );
+ return null;
}
// Do not buffer writes
@@ -1048,49 +1032,136 @@ class MWMemcached {
return $this->_cache_sock[$host];
}
- function _debugprint( $str ) {
- print( $str );
+ /**
+ * @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 );
+ }
+
+ /**
+ * @param $text string
+ */
+ function _error_log( $text ) {
+ wfDebugLog( 'memcached-serious', "Memcached error: $text" );
}
/**
- * Write to a stream, timing out after the correct amount of time
+ * Write to a stream. If there is an error, mark the socket dead.
*
- * @return Boolean: false on failure, true on success
+ * @param $sock The socket
+ * @param $buf The string to write
+ * @return bool True on success, false on failure
*/
- /*
- function _safe_fwrite( $f, $buf, $len = false ) {
- stream_set_blocking( $f, 0 );
+ function _fwrite( $sock, $buf ) {
+ $bytesWritten = 0;
+ $bufSize = strlen( $buf );
+ while ( $bytesWritten < $bufSize ) {
+ $result = fwrite( $sock, $buf );
+ $data = stream_get_meta_data( $sock );
+ if ( $data['timed_out'] ) {
+ $this->_handle_error( $sock, 'timeout writing to $1' );
+ return false;
+ }
+ // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
+ if ( $result === false || $result === 0 ) {
+ $this->_handle_error( $sock, 'error writing to $1' );
+ return false;
+ }
+ $bytesWritten += $result;
+ }
- if ( $len === false ) {
- wfDebug( "Writing " . strlen( $buf ) . " bytes\n" );
- $bytesWritten = fwrite( $f, $buf );
- } else {
- wfDebug( "Writing $len bytes\n" );
- $bytesWritten = fwrite( $f, $buf, $len );
+ return true;
+ }
+
+ /**
+ * Handle an I/O error. Mark the socket dead and log an error.
+ */
+ function _handle_error( $sock, $msg ) {
+ $peer = stream_socket_get_name( $sock, true /** remote **/ );
+ if ( strval( $peer ) === '' ) {
+ $peer = array_search( $sock, $this->_cache_sock );
+ if ( $peer === false ) {
+ $peer = '[unknown host]';
+ }
}
- $n = stream_select( $r = null, $w = array( $f ), $e = null, 10, 0 );
- # $this->_timeout_seconds, $this->_timeout_microseconds );
+ $msg = str_replace( '$1', $peer, $msg );
+ $this->_error_log( "$msg\n" );
+ $this->_dead_sock( $sock );
+ }
- wfDebug( "stream_select returned $n\n" );
- stream_set_blocking( $f, 1 );
- return $n == 1;
- return $bytesWritten;
- }*/
+ /**
+ * Read the specified number of bytes from a stream. If there is an error,
+ * mark the socket dead.
+ *
+ * @param $sock The socket
+ * @param $len The number of bytes to read
+ * @return The string on success, false on failure.
+ */
+ function _fread( $sock, $len ) {
+ $buf = '';
+ while ( $len > 0 ) {
+ $result = fread( $sock, $len );
+ $data = stream_get_meta_data( $sock );
+ if ( $data['timed_out'] ) {
+ $this->_handle_error( $sock, 'timeout reading from $1' );
+ return false;
+ }
+ if ( $result === false ) {
+ $this->_handle_error( $sock, 'error reading buffer from $1' );
+ return false;
+ }
+ if ( $result === '' ) {
+ // This will happen if the remote end of the socket is shut down
+ $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
+ return false;
+ }
+ $len -= strlen( $result );
+ $buf .= $result;
+ }
+ return $buf;
+ }
/**
- * Original behaviour
+ * Read a line from a stream. If there is an error, mark the socket dead.
+ * The \r\n line ending is stripped from the response.
+ *
+ * @param $sock The socket
+ * @return The string on success, false on failure
*/
- function _safe_fwrite( $f, $buf, $len = false ) {
- if ( $len === false ) {
- $bytesWritten = fwrite( $f, $buf );
+ 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
+ // got a string response.
+ $data = stream_get_meta_data( $sock );
+ if ( $data['timed_out'] ) {
+ $this->_handle_error( $sock, 'timeout reading line from $1' );
+ return false;
+ }
+ if ( $result === false ) {
+ $this->_handle_error( $sock, 'error reading line from $1' );
+ return false;
+ }
+ if ( substr( $result, -2 ) === "\r\n" ) {
+ $result = substr( $result, 0, -2 );
+ } elseif ( substr( $result, -1 ) === "\n" ) {
+ $result = substr( $result, 0, -1 );
} else {
- $bytesWritten = fwrite( $f, $buf, $len );
+ $this->_handle_error( $sock, 'line ending missing in response from $1' );
+ return false;
}
- return $bytesWritten;
+ return $result;
}
/**
* Flush the read buffer of a stream
+ * @param $f Resource
*/
function _flush_read_buffer( $f ) {
if ( !is_resource( $f ) ) {
@@ -1108,12 +1179,8 @@ class MWMemcached {
// }}}
}
-// vim: sts=3 sw=3 et
// }}}
class MemCachedClientforWiki extends MWMemcached {
- function _debugprint( $text ) {
- wfDebug( "memcached: $text" );
- }
}
diff --git a/includes/objectcache/MemcachedPeclBagOStuff.php b/includes/objectcache/MemcachedPeclBagOStuff.php
new file mode 100644
index 00000000..76886ebb
--- /dev/null
+++ b/includes/objectcache/MemcachedPeclBagOStuff.php
@@ -0,0 +1,237 @@
+<?php
+/**
+ * Object caching using memcached.
+ *
+ * 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
+ */
+
+/**
+ * A wrapper class for the PECL memcached client
+ *
+ * @ingroup Cache
+ */
+class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
+
+ /**
+ * Constructor
+ *
+ * Available parameters are:
+ * - servers: The list of IP:port combinations holding the memcached servers.
+ * - persistent: Whether to use a persistent connection
+ * - 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
+ * - 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.
+ */
+ function __construct( $params ) {
+ $params = $this->applyDefaultParams( $params );
+
+ 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.
+ $this->client = new Memcached( md5( serialize( $params ) ) );
+ if ( count( $this->client->getServerList() ) ) {
+ wfDebug( __METHOD__ . ": persistent Memcached object already loaded.\n" );
+ return; // already initialized; don't add duplicate servers
+ }
+ } else {
+ $this->client = new Memcached;
+ }
+
+ if ( !isset( $params['serializer'] ) ) {
+ $params['serializer'] = 'php';
+ }
+
+ // 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.
+ ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
+
+ // Set timeouts
+ $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
+ $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
+ $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
+ $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
+
+ // Set libketama mode since it's recommended by the documentation and
+ // is as good as any. There's no way to configure libmemcached to use
+ // hashes identical to the ones currently in use by the PHP client, and
+ // even implementing one of the libmemcached hashes in pure PHP for
+ // forwards compatibility would require MWMemcached::get_sock() to be
+ // rewritten.
+ $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
+
+ // Set the serializer
+ switch ( $params['serializer'] ) {
+ case 'php':
+ $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
+ break;
+ case 'igbinary':
+ if ( !Memcached::HAVE_IGBINARY ) {
+ 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' );
+ }
+ $servers = array();
+ foreach ( $params['servers'] as $host ) {
+ $servers[] = IP::splitHostAndPort( $host ); // (ip, port)
+ }
+ $this->client->addServers( $servers );
+ }
+
+ /**
+ * @param $key string
+ * @return Mixed
+ */
+ public function get( $key ) {
+ $this->debugLog( "get($key)" );
+ return $this->checkResult( $key, parent::get( $key ) );
+ }
+
+ /**
+ * @param $key string
+ * @param $value
+ * @param $exptime int
+ * @return bool
+ */
+ public function set( $key, $value, $exptime = 0 ) {
+ $this->debugLog( "set($key)" );
+ return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
+ }
+
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
+ public function delete( $key, $time = 0 ) {
+ $this->debugLog( "delete($key)" );
+ $result = parent::delete( $key, $time );
+ if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
+ // "Not found" is counted as success in our interface
+ return true;
+ } else {
+ return $this->checkResult( $key, $result );
+ }
+ }
+
+ /**
+ * @param $key string
+ * @param $value int
+ * @param $exptime int
+ * @return Mixed
+ */
+ public function add( $key, $value, $exptime = 0 ) {
+ $this->debugLog( "add($key)" );
+ return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
+ }
+
+ /**
+ * @param $key string
+ * @param $value int
+ * @param $exptime
+ * @return Mixed
+ */
+ public function replace( $key, $value, $exptime = 0 ) {
+ $this->debugLog( "replace($key)" );
+ return $this->checkResult( $key, parent::replace( $key, $value, $exptime ) );
+ }
+
+ /**
+ * @param $key string
+ * @param $value int
+ * @return Mixed
+ */
+ public function incr( $key, $value = 1 ) {
+ $this->debugLog( "incr($key)" );
+ $result = $this->client->increment( $key, $value );
+ return $this->checkResult( $key, $result );
+ }
+
+ /**
+ * @param $key string
+ * @param $value int
+ * @return Mixed
+ */
+ public function decr( $key, $value = 1 ) {
+ $this->debugLog( "decr($key)" );
+ $result = $this->client->decrement( $key, $value );
+ return $this->checkResult( $key, $result );
+ }
+
+ /**
+ * Check the return value from a client method call and take any necessary
+ * action. Returns the value that the wrapper function should return. At
+ * present, the return value is always the same as the return value from
+ * 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 $result Mixed The return value
+ * @return Mixed
+ */
+ protected function checkResult( $key, $result ) {
+ if ( $result !== false ) {
+ return $result;
+ }
+ switch ( $this->client->getResultCode() ) {
+ case Memcached::RES_SUCCESS:
+ break;
+ case Memcached::RES_DATA_EXISTS:
+ case Memcached::RES_NOTSTORED:
+ case Memcached::RES_NOTFOUND:
+ $this->debugLog( "result: " . $this->client->getResultMessage() );
+ break;
+ default:
+ $msg = $this->client->getResultMessage();
+ if ( $key !== false ) {
+ $server = $this->client->getServerByKey( $key );
+ $serverName = "{$server['host']}:{$server['port']}";
+ $msg = "Memcached error for key \"$key\" on server \"$serverName\": $msg";
+ } else {
+ $msg = "Memcached error: $msg";
+ }
+ wfDebugLog( 'memcached-serious', $msg );
+ }
+ return $result;
+ }
+
+ /**
+ * @param $keys Array
+ * @return Array
+ */
+ public function getMulti( array $keys ) {
+ $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' );
+ $callback = array( $this, 'encodeKey' );
+ $result = $this->client->getMulti( array_map( $callback, $keys ) );
+ return $this->checkResult( false, $result );
+ }
+
+ /* NOTE: there is no cas() method here because it is currently not supported
+ * by the BagOStuff interface and other BagOStuff subclasses, such as
+ * SqlBagOStuff.
+ */
+}
diff --git a/includes/objectcache/MemcachedPhpBagOStuff.php b/includes/objectcache/MemcachedPhpBagOStuff.php
index 14016683..a46dc716 100644
--- a/includes/objectcache/MemcachedPhpBagOStuff.php
+++ b/includes/objectcache/MemcachedPhpBagOStuff.php
@@ -1,14 +1,32 @@
<?php
+/**
+ * Object caching using memcached.
+ *
+ * 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
+ */
/**
* A wrapper class for the pure-PHP memcached client, exposing a BagOStuff interface.
+ *
+ * @ingroup Cache
*/
-class MemcachedPhpBagOStuff extends BagOStuff {
-
- /**
- * @var MemCachedClientforWiki
- */
- protected $client;
+class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
/**
* Constructor.
@@ -24,24 +42,7 @@ class MemcachedPhpBagOStuff extends BagOStuff {
* @param $params array
*/
function __construct( $params ) {
- if ( !isset( $params['servers'] ) ) {
- $params['servers'] = $GLOBALS['wgMemCachedServers'];
- }
- if ( !isset( $params['debug'] ) ) {
- $params['debug'] = $GLOBALS['wgMemCachedDebug'];
- }
- if ( !isset( $params['persistent'] ) ) {
- $params['persistent'] = $GLOBALS['wgMemCachedPersistent'];
- }
- if ( !isset( $params['compress_threshold'] ) ) {
- $params['compress_threshold'] = 1500;
- }
- if ( !isset( $params['timeout'] ) ) {
- $params['timeout'] = $GLOBALS['wgMemCachedTimeout'];
- }
- if ( !isset( $params['connect_timeout'] ) ) {
- $params['connect_timeout'] = 0.1;
- }
+ $params = $this->applyDefaultParams( $params );
$this->client = new MemCachedClientforWiki( $params );
$this->client->set_servers( $params['servers'] );
@@ -56,36 +57,18 @@ class MemcachedPhpBagOStuff extends BagOStuff {
}
/**
- * @param $key string
- * @return Mixed
- */
- public function get( $key ) {
- return $this->client->get( $this->encodeKey( $key ) );
- }
-
- /**
- * @param $key string
- * @param $value
- * @param $exptime int
- * @return bool
+ * @param $keys Array
+ * @return Array
*/
- public function set( $key, $value, $exptime = 0 ) {
- return $this->client->set( $this->encodeKey( $key ), $value, $exptime );
- }
-
- /**
- * @param $key string
- * @param $time int
- * @return bool
- */
- public function delete( $key, $time = 0 ) {
- return $this->client->delete( $this->encodeKey( $key ), $time );
+ public function getMulti( array $keys ) {
+ $callback = array( $this, 'encodeKey' );
+ return $this->client->get_multi( array_map( $callback, $keys ) );
}
/**
* @param $key
* @param $timeout int
- * @return
+ * @return bool
*/
public function lock( $key, $timeout = 0 ) {
return $this->client->lock( $this->encodeKey( $key ), $timeout );
@@ -98,26 +81,7 @@ class MemcachedPhpBagOStuff extends BagOStuff {
public function unlock( $key ) {
return $this->client->unlock( $this->encodeKey( $key ) );
}
-
- /**
- * @param $key string
- * @param $value int
- * @return Mixed
- */
- public function add( $key, $value, $exptime = 0 ) {
- return $this->client->add( $this->encodeKey( $key ), $value, $exptime );
- }
-
- /**
- * @param $key string
- * @param $value int
- * @param $exptime
- * @return Mixed
- */
- public function replace( $key, $value, $exptime = 0 ) {
- return $this->client->replace( $this->encodeKey( $key ), $value, $exptime );
- }
-
+
/**
* @param $key string
* @param $value int
@@ -135,44 +99,5 @@ class MemcachedPhpBagOStuff extends BagOStuff {
public function decr( $key, $value = 1 ) {
return $this->client->decr( $this->encodeKey( $key ), $value );
}
-
- /**
- * Get the underlying client object. This is provided for debugging
- * purposes.
- *
- * @return MemCachedClientforWiki
- */
- public function getClient() {
- return $this->client;
- }
-
- /**
- * Encode a key for use on the wire inside the memcached protocol.
- *
- * We encode spaces and line breaks to avoid protocol errors. We encode
- * the other control characters for compatibility with libmemcached
- * verify_key. We leave other punctuation alone, to maximise backwards
- * compatibility.
- */
- public function encodeKey( $key ) {
- return preg_replace_callback( '/[\x00-\x20\x25\x7f]+/',
- array( $this, 'encodeKeyCallback' ), $key );
- }
-
- protected function encodeKeyCallback( $m ) {
- return rawurlencode( $m[0] );
- }
-
- /**
- * Decode a key encoded with encodeKey(). This is provided as a convenience
- * function for debugging.
- *
- * @param $key string
- *
- * @return string
- */
- public function decodeKey( $key ) {
- return urldecode( $key );
- }
}
diff --git a/includes/objectcache/MultiWriteBagOStuff.php b/includes/objectcache/MultiWriteBagOStuff.php
index 0d95a846..e496ddd8 100644
--- a/includes/objectcache/MultiWriteBagOStuff.php
+++ b/includes/objectcache/MultiWriteBagOStuff.php
@@ -1,9 +1,32 @@
<?php
+/**
+ * Wrapper for object caching in different caches.
+ *
+ * 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
+ */
/**
* 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
*/
class MultiWriteBagOStuff extends BagOStuff {
var $caches;
@@ -11,11 +34,12 @@ class MultiWriteBagOStuff extends BagOStuff {
/**
* Constructor. Parameters are:
*
- * - caches: This should have a numbered array of cache parameter
+ * - caches: This should have a numbered array of cache parameter
* structures, in the style required by $wgObjectCaches. See
* the documentation of $wgObjectCaches for more detail.
*
* @param $params array
+ * @throws MWException
*/
public function __construct( $params ) {
if ( !isset( $params['caches'] ) ) {
@@ -28,10 +52,17 @@ class MultiWriteBagOStuff extends BagOStuff {
}
}
+ /**
+ * @param $debug bool
+ */
public function setDebug( $debug ) {
$this->doWrite( 'setDebug', $debug );
}
+ /**
+ * @param $key string
+ * @return bool|mixed
+ */
public function get( $key ) {
foreach ( $this->caches as $cache ) {
$value = $cache->get( $key );
@@ -42,30 +73,68 @@ class MultiWriteBagOStuff extends BagOStuff {
return false;
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
public function set( $key, $value, $exptime = 0 ) {
return $this->doWrite( 'set', $key, $value, $exptime );
}
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
public function delete( $key, $time = 0 ) {
return $this->doWrite( 'delete', $key, $time );
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
public function add( $key, $value, $exptime = 0 ) {
return $this->doWrite( 'add', $key, $value, $exptime );
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
public function replace( $key, $value, $exptime = 0 ) {
return $this->doWrite( 'replace', $key, $value, $exptime );
}
+ /**
+ * @param $key string
+ * @param $value int
+ * @return bool|null
+ */
public function incr( $key, $value = 1 ) {
return $this->doWrite( 'incr', $key, $value );
}
+ /**
+ * @param $key string
+ * @param $value int
+ * @return bool
+ */
public function decr( $key, $value = 1 ) {
return $this->doWrite( 'decr', $key, $value );
- }
+ }
+ /**
+ * @param $key string
+ * @param $timeout int
+ * @return bool
+ */
public function lock( $key, $timeout = 0 ) {
// Lock only the first cache, to avoid deadlocks
if ( isset( $this->caches[0] ) ) {
@@ -75,6 +144,10 @@ class MultiWriteBagOStuff extends BagOStuff {
}
}
+ /**
+ * @param $key string
+ * @return bool
+ */
public function unlock( $key ) {
if ( isset( $this->caches[0] ) ) {
return $this->caches[0]->unlock( $key );
@@ -83,6 +156,10 @@ class MultiWriteBagOStuff extends BagOStuff {
}
}
+ /**
+ * @param $method string
+ * @return bool
+ */
protected function doWrite( $method /*, ... */ ) {
$ret = true;
$args = func_get_args();
@@ -97,9 +174,12 @@ class MultiWriteBagOStuff extends BagOStuff {
}
/**
- * Delete objects expiring before a certain date.
+ * Delete objects expiring before a certain date.
*
* Succeed if any of the child caches succeed.
+ * @param $date string
+ * @param $progressCallback bool|callback
+ * @return bool
*/
public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) {
$ret = false;
diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php
index 77ca8371..9b360f32 100644
--- a/includes/objectcache/ObjectCache.php
+++ b/includes/objectcache/ObjectCache.php
@@ -1,19 +1,40 @@
<?php
/**
- * Functions to get cache objects
+ * Functions to get cache 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 Cache
*/
+
+/**
+ * Functions to get cache objects
+ *
+ * @ingroup Cache
+ */
class ObjectCache {
static $instances = array();
/**
* Get a cached instance of the specified type of cache object.
*
- * @param $id
+ * @param $id string
*
- * @return object
+ * @return ObjectCache
*/
static function getInstance( $id ) {
if ( isset( self::$instances[$id] ) ) {
@@ -35,8 +56,9 @@ class ObjectCache {
/**
* Create a new cache object of the specified type.
*
- * @param $id
+ * @param $id string
*
+ * @throws MWException
* @return ObjectCache
*/
static function newFromId( $id ) {
@@ -55,6 +77,7 @@ class ObjectCache {
*
* @param $params array
*
+ * @throws MWException
* @return ObjectCache
*/
static function newFromParams( $params ) {
@@ -71,6 +94,15 @@ class ObjectCache {
/**
* Factory function referenced from DefaultSettings.php for CACHE_ANYTHING
+ *
+ * CACHE_ANYTHING means that stuff has to be cached, not caching is not an option.
+ * If a caching method is configured for any of the main caches ($wgMainCacheType,
+ * $wgMessageCacheType, $wgParserCacheType), then CACHE_ANYTHING will effectively
+ * be an alias to the configured cache choice for that.
+ * 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
*/
static function newAnything( $params ) {
global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType;
@@ -86,6 +118,8 @@ class ObjectCache {
/**
* Factory function referenced from DefaultSettings.php for CACHE_ACCEL.
*
+ * @param $params array
+ * @throws MWException
* @return ObjectCache
*/
static function newAccelerator( $params ) {
@@ -104,8 +138,10 @@ class ObjectCache {
/**
* Factory function that creates a memcached client object.
- * The idea of this is that it might eventually detect and automatically
- * support the PECL extension, assuming someone can get it to compile.
+ *
+ * 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.
*
* @param $params array
*
diff --git a/includes/objectcache/ObjectCacheSessionHandler.php b/includes/objectcache/ObjectCacheSessionHandler.php
new file mode 100644
index 00000000..f55da94d
--- /dev/null
+++ b/includes/objectcache/ObjectCacheSessionHandler.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Session storage in object cache.
+ *
+ * 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
+ */
+
+/**
+ * Session storage in object cache.
+ * Used if $wgSessionsInObjectCache is true.
+ *
+ * @ingroup Cache
+ */
+class ObjectCacheSessionHandler {
+ /**
+ * Install a session handler for the current web request
+ */
+ static function install() {
+ session_set_save_handler(
+ array( __CLASS__, 'open' ),
+ array( __CLASS__, 'close' ),
+ array( __CLASS__, 'read' ),
+ array( __CLASS__, 'write' ),
+ array( __CLASS__, 'destroy' ),
+ array( __CLASS__, 'gc' ) );
+
+ // It's necessary to register a shutdown function to call session_write_close(),
+ // because by the time the request shutdown function for the session module is
+ // called, $wgMemc has already been destroyed. Shutdown functions registered
+ // this way are called before object destruction.
+ register_shutdown_function( array( __CLASS__, 'handleShutdown' ) );
+ }
+
+ /**
+ * Get the cache storage object to use for session storage
+ */
+ static function getCache() {
+ global $wgSessionCacheType;
+ return ObjectCache::getInstance( $wgSessionCacheType );
+ }
+
+ /**
+ * Get a cache key for the given session id.
+ *
+ * @param $id String: session id
+ * @return String: cache key
+ */
+ static function getKey( $id ) {
+ return wfMemcKey( 'session', $id );
+ }
+
+ /**
+ * Callback when opening a session.
+ *
+ * @param $save_path String: path used to store session files, unused
+ * @param $session_name String: session name
+ * @return Boolean: success
+ */
+ static function open( $save_path, $session_name ) {
+ return true;
+ }
+
+ /**
+ * Callback when closing a session.
+ * NOP.
+ *
+ * @return Boolean: success
+ */
+ static function close() {
+ return true;
+ }
+
+ /**
+ * Callback when reading session data.
+ *
+ * @param $id String: session id
+ * @return Mixed: session data
+ */
+ static function read( $id ) {
+ $data = self::getCache()->get( self::getKey( $id ) );
+ if( $data === false ) {
+ return '';
+ }
+ return $data;
+ }
+
+ /**
+ * Callback when writing session data.
+ *
+ * @param $id String: session id
+ * @param $data Mixed: session data
+ * @return Boolean: success
+ */
+ static function write( $id, $data ) {
+ global $wgObjectCacheSessionExpiry;
+ self::getCache()->set( self::getKey( $id ), $data, $wgObjectCacheSessionExpiry );
+ return true;
+ }
+
+ /**
+ * Callback to destroy a session when calling session_destroy().
+ *
+ * @param $id String: session id
+ * @return Boolean: success
+ */
+ static function destroy( $id ) {
+ self::getCache()->delete( self::getKey( $id ) );
+ return true;
+ }
+
+ /**
+ * Callback to execute garbage collection.
+ * NOP: Object caches perform garbage collection implicitly
+ *
+ * @param $maxlifetime Integer: maximum session life time
+ * @return Boolean: success
+ */
+ static function gc( $maxlifetime ) {
+ return true;
+ }
+
+ /**
+ * Shutdown function. See the comment inside ObjectCacheSessionHandler::install
+ * for rationale.
+ */
+ static function handleShutdown() {
+ session_write_close();
+ }
+}
diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php
new file mode 100644
index 00000000..c5966cdb
--- /dev/null
+++ b/includes/objectcache/RedisBagOStuff.php
@@ -0,0 +1,413 @@
+<?php
+/**
+ * Object caching using Redis (http://redis.io/).
+ *
+ * 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 RedisBagOStuff extends BagOStuff {
+ protected $connectTimeout, $persistent, $password, $automaticFailover;
+
+ /**
+ * A list of server names, from $params['servers']
+ */
+ 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();
+
+ /**
+ * Construct a RedisBagOStuff object. Parameters are:
+ *
+ * - servers: An array of server names. A server name may be a hostname,
+ * 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.
+ *
+ * - 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.
+ *
+ * - automaticFailover: If this is false, then each key will be mapped to
+ * a single server, and if that server is down, any requests for that key
+ * will fail. If this is true, a connection failure will cause the client
+ * to immediately try the next server in the list (as determined by a
+ * consistent hashing algorithm). True by default. This has the
+ * potential to create consistency issues if a server is slow enough to
+ * 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' );
+ }
+
+ $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 {
+ $this->automaticFailover = true;
+ }
+ }
+
+ public function get( $key ) {
+ wfProfileIn( __METHOD__ );
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ try {
+ $result = $conn->get( $key );
+ } catch ( RedisException $e ) {
+ $result = false;
+ $this->handleException( $server, $e );
+ }
+ $this->logRequest( 'get', $key, $server, $result );
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ public function set( $key, $value, $expiry = 0 ) {
+ wfProfileIn( __METHOD__ );
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ $expiry = $this->convertToRelative( $expiry );
+ try {
+ if ( !$expiry ) {
+ // No expiry, that is very different from zero expiry in Redis
+ $result = $conn->set( $key, $value );
+ } else {
+ $result = $conn->setex( $key, $expiry, $value );
+ }
+ } catch ( RedisException $e ) {
+ $result = false;
+ $this->handleException( $server, $e );
+ }
+
+ $this->logRequest( 'set', $key, $server, $result );
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ public function delete( $key, $time = 0 ) {
+ wfProfileIn( __METHOD__ );
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ try {
+ $conn->delete( $key );
+ // Return true even if the key didn't exist
+ $result = true;
+ } catch ( RedisException $e ) {
+ $result = false;
+ $this->handleException( $server, $e );
+ }
+ $this->logRequest( 'delete', $key, $server, $result );
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ public function getMulti( array $keys ) {
+ wfProfileIn( __METHOD__ );
+ $batches = array();
+ $conns = array();
+ foreach ( $keys as $key ) {
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ continue;
+ }
+ $conns[$server] = $conn;
+ $batches[$server][] = $key;
+ }
+ $result = array();
+ foreach ( $batches as $server => $batchKeys ) {
+ $conn = $conns[$server];
+ try {
+ $conn->multi( Redis::PIPELINE );
+ foreach ( $batchKeys as $key ) {
+ $conn->get( $key );
+ }
+ $batchResult = $conn->exec();
+ if ( $batchResult === false ) {
+ $this->debug( "multi request to $server failed" );
+ continue;
+ }
+ foreach ( $batchResult as $i => $value ) {
+ if ( $value !== false ) {
+ $result[$batchKeys[$i]] = $value;
+ }
+ }
+ } catch ( RedisException $e ) {
+ $this->handleException( $server, $e );
+ }
+ }
+
+ $this->debug( "getMulti for " . count( $keys ) . " keys " .
+ "returned " . count( $result ) . " results" );
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ public function add( $key, $value, $expiry = 0 ) {
+ wfProfileIn( __METHOD__ );
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ $expiry = $this->convertToRelative( $expiry );
+ try {
+ $result = $conn->setnx( $key, $value );
+ if ( $result && $expiry ) {
+ $conn->expire( $key, $expiry );
+ }
+ } catch ( RedisException $e ) {
+ $result = false;
+ $this->handleException( $server, $e );
+ }
+ $this->logRequest( 'add', $key, $server, $result );
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ /**
+ * Non-atomic implementation of replace(). Could perhaps be done atomically
+ * with WATCH or scripting, but this function is rarely used.
+ */
+ public function replace( $key, $value, $expiry = 0 ) {
+ wfProfileIn( __METHOD__ );
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ if ( !$conn->exists( $key ) ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $expiry = $this->convertToRelative( $expiry );
+ try {
+ if ( !$expiry ) {
+ $result = $conn->set( $key, $value );
+ } else {
+ $result = $conn->setex( $key, $expiry, $value );
+ }
+ } catch ( RedisException $e ) {
+ $result = false;
+ $this->handleException( $server, $e );
+ }
+
+ $this->logRequest( 'replace', $key, $server, $result );
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ /**
+ * 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
+ */
+ 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 );
+ if ( !$this->automaticFailover ) {
+ reset( $hashes );
+ $candidates = array( key( $hashes ) );
+ } else {
+ $candidates = array_keys( $hashes );
+ }
+ }
+
+ foreach ( $candidates as $server ) {
+ $conn = $this->getConnectionToServer( $server );
+ if ( $conn ) {
+ return array( $server, $conn );
+ }
+ }
+ return array( false, false );
+ }
+
+ /**
+ * 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 ) {
+ wfDebugLog( 'redis', "Redis error: $msg\n" );
+ }
+
+ /**
+ * 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.
+ */
+ protected function handleException( $server, $e ) {
+ wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" );
+ unset( $this->conns[$server] );
+ }
+
+ /**
+ * Send information about a single request to the debug log
+ */
+ public function logRequest( $method, $key, $server, $result ) {
+ $this->debug( "$method $key on $server: " .
+ ( $result === false ? "failure" : "success" ) );
+ }
+}
+
diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php
index 93d22f11..54051dc1 100644
--- a/includes/objectcache/SqlBagOStuff.php
+++ b/includes/objectcache/SqlBagOStuff.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Object caching using a 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
+ * @ingroup Cache
+ */
/**
* Class to store objects in the database
@@ -6,7 +27,6 @@
* @ingroup Cache
*/
class SqlBagOStuff extends BagOStuff {
-
/**
* @var LoadBalancer
*/
@@ -22,6 +42,9 @@ class SqlBagOStuff extends BagOStuff {
var $shards = 1;
var $tableName = 'objectcache';
+ protected $connFailureTime = 0; // UNIX timestamp
+ protected $connFailureError; // exception
+
/**
* Constructor. Parameters are:
* - server: A server info structure in the format required by each
@@ -66,25 +89,40 @@ class SqlBagOStuff extends BagOStuff {
* @return DatabaseBase
*/
protected function getDB() {
+ 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->db ) ) {
# If server connection info was given, use that
if ( $this->serverInfo ) {
+ if ( $wgDebugDBTransactions ) {
+ wfDebug( sprintf( "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 );
} else {
- # We must keep a separate connection to MySQL in order to avoid deadlocks
- # However, SQLite has an opposite behaviour.
- # @todo Investigate behaviour for other databases
- if ( wfGetDB( DB_MASTER )->getType() == 'sqlite' ) {
- $this->db = wfGetDB( DB_MASTER );
- } 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
+ * 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 );
+ $this->db->clearFlag( DBO_TRX ); // auto-commit mode
+ } else {
+ $this->db = wfGetDB( DB_MASTER );
}
}
+ if ( $wgDebugDBTransactions ) {
+ wfDebug( sprintf( "Connection %s will be used for SqlBagOStuff\n", $this->db ) );
+ }
}
return $this->db;
@@ -92,6 +130,8 @@ class SqlBagOStuff extends BagOStuff {
/**
* Get the table name for a given key
+ * @param $key string
+ * @return string
*/
protected function getTableByKey( $key ) {
if ( $this->shards > 1 ) {
@@ -104,6 +144,8 @@ class SqlBagOStuff extends BagOStuff {
/**
* Get the table name for a given shard index
+ * @param $index int
+ * @return string
*/
protected function getTableByShard( $index ) {
if ( $this->shards > 1 ) {
@@ -115,61 +157,103 @@ class SqlBagOStuff extends BagOStuff {
}
}
+ /**
+ * @param $key string
+ * @return mixed
+ */
public function get( $key ) {
- # expire old entries if any
- $this->garbageCollect();
- $db = $this->getDB();
- $tableName = $this->getTableByKey( $key );
- $row = $db->selectRow( $tableName, array( 'value', 'exptime' ),
- array( 'keyname' => $key ), __METHOD__ );
+ $values = $this->getMulti( array( $key ) );
+ return array_key_exists( $key, $values ) ? $values[$key] : false;
+ }
- if ( !$row ) {
- $this->debug( 'get: no matching rows' );
- return false;
- }
+ /**
+ * @param $keys array
+ * @return Array
+ */
+ public function getMulti( array $keys ) {
+ $values = array(); // array of (key => value)
- $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
+ try {
+ $db = $this->getDB();
+ $keysByTableName = array();
+ foreach ( $keys as $key ) {
+ $tableName = $this->getTableByKey( $key );
+ if ( !isset( $keysByTableName[$tableName] ) ) {
+ $keysByTableName[$tableName] = array();
+ }
+ $keysByTableName[$tableName][] = $key;
+ }
- if ( $this->isExpired( $row->exptime ) ) {
- $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( $tableName,
- array(
- 'keyname' => $key,
- 'exptime' => $row->exptime
- ), __METHOD__ );
- $db->commit( __METHOD__ );
- } catch ( DBQueryError $e ) {
- $this->handleWriteError( $e );
+ $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;
+ }
}
- return false;
- }
+ 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
+ $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 );
+ }
+ $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 ( DBError $e ) {
+ $this->handleReadError( $e );
+ };
- return $this->unserialize( $db->decodeBlob( $row->value ) );
+ return $values;
}
+ /**
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int
+ * @return bool
+ */
public function set( $key, $value, $exptime = 0 ) {
- $db = $this->getDB();
- $exptime = intval( $exptime );
-
- if ( $exptime < 0 ) {
- $exptime = 0;
- }
+ try {
+ $db = $this->getDB();
+ $exptime = intval( $exptime );
- if ( $exptime == 0 ) {
- $encExpiry = $this->getMaxDateTime();
- } else {
- if ( $exptime < 3.16e8 ) { # ~10 years
- $exptime += time();
+ if ( $exptime < 0 ) {
+ $exptime = 0;
}
- $encExpiry = $db->timestamp( $exptime );
- }
- try {
+ if ( $exptime == 0 ) {
+ $encExpiry = $this->getMaxDateTime();
+ } 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
@@ -182,40 +266,46 @@ class SqlBagOStuff extends BagOStuff {
'exptime' => $encExpiry
), __METHOD__ );
$db->commit( __METHOD__ );
- } catch ( DBQueryError $e ) {
+ } catch ( DBError $e ) {
$this->handleWriteError( $e );
-
return false;
}
return true;
}
+ /**
+ * @param $key string
+ * @param $time int
+ * @return bool
+ */
public function delete( $key, $time = 0 ) {
- $db = $this->getDB();
-
try {
+ $db = $this->getDB();
$db->begin( __METHOD__ );
$db->delete(
$this->getTableByKey( $key ),
array( 'keyname' => $key ),
__METHOD__ );
$db->commit( __METHOD__ );
- } catch ( DBQueryError $e ) {
+ } catch ( DBError $e ) {
$this->handleWriteError( $e );
-
return false;
}
return true;
}
+ /**
+ * @param $key string
+ * @param $step int
+ * @return int|null
+ */
public function incr( $key, $step = 1 ) {
- $db = $this->getDB();
- $tableName = $this->getTableByKey( $key );
- $step = intval( $step );
-
try {
+ $db = $this->getDB();
+ $tableName = $this->getTableByKey( $key );
+ $step = intval( $step );
$db->begin( __METHOD__ );
$row = $db->selectRow(
$tableName,
@@ -251,34 +341,47 @@ class SqlBagOStuff extends BagOStuff {
$newValue = null;
}
$db->commit( __METHOD__ );
- } catch ( DBQueryError $e ) {
+ } catch ( DBError $e ) {
$this->handleWriteError( $e );
-
return null;
}
return $newValue;
}
+ /**
+ * @return Array
+ */
public function keys() {
- $db = $this->getDB();
$result = array();
- for ( $i = 0; $i < $this->shards; $i++ ) {
- $res = $db->select( $this->getTableByShard( $i ),
- array( 'keyname' ), false, __METHOD__ );
- foreach ( $res as $row ) {
- $result[] = $row->keyname;
+ 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();
}
+ /**
+ * @return string
+ */
protected function getMaxDateTime() {
if ( time() > 0x7fffffff ) {
return $this->getDB()->timestamp( 1 << 62 );
@@ -310,14 +413,16 @@ class SqlBagOStuff extends BagOStuff {
/**
* Delete objects from the database which expire before a certain date.
+ * @param $timestamp string
+ * @param $progressCallback bool|callback
+ * @return bool
*/
public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) {
- $db = $this->getDB();
- $dbTimestamp = $db->timestamp( $timestamp );
- $totalSeconds = false;
- $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) );
-
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 ) {
@@ -325,7 +430,7 @@ class SqlBagOStuff extends BagOStuff {
if ( $maxExpTime !== false ) {
$conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime );
}
- $rows = $db->select(
+ $rows = $db->select(
$this->getTableByShard( $i ),
array( 'keyname', 'exptime' ),
$conds,
@@ -349,7 +454,7 @@ class SqlBagOStuff extends BagOStuff {
$db->begin( __METHOD__ );
$db->delete(
$this->getTableByShard( $i ),
- array(
+ array(
'exptime >= ' . $db->addQuotes( $minExpTime ),
'exptime < ' . $db->addQuotes( $dbTimestamp ),
'keyname' => $keys
@@ -361,36 +466,40 @@ class SqlBagOStuff extends BagOStuff {
if ( intval( $totalSeconds ) === 0 ) {
$percent = 0;
} else {
- $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp )
+ $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp )
- wfTimestamp( TS_UNIX, $maxExpTime );
if ( $remainingSeconds > $totalSeconds ) {
$totalSeconds = $remainingSeconds;
}
- $percent = ( $i + $remainingSeconds / $totalSeconds )
+ $percent = ( $i + $remainingSeconds / $totalSeconds )
/ $this->shards * 100;
}
call_user_func( $progressCallback, $percent );
}
}
}
- } catch ( DBQueryError $e ) {
+ } catch ( DBError $e ) {
$this->handleWriteError( $e );
+ return false;
}
+
return true;
}
public function deleteAll() {
- $db = $this->getDB();
-
try {
+ $db = $this->getDB();
for ( $i = 0; $i < $this->shards; $i++ ) {
$db->begin( __METHOD__ );
$db->delete( $this->getTableByShard( $i ), '*', __METHOD__ );
$db->commit( __METHOD__ );
}
- } catch ( DBQueryError $e ) {
+ } catch ( DBError $e ) {
$this->handleWriteError( $e );
+ return false;
}
+
+ return true;
}
/**
@@ -433,23 +542,40 @@ class SqlBagOStuff extends BagOStuff {
}
/**
- * Handle a DBQueryError which occurred during a write operation.
- * Ignore errors which are due to a read-only database, rethrow others.
+ * Handle a DBError which occurred during a read operation.
*/
- protected function handleWriteError( $exception ) {
- $db = $this->getDB();
-
- if ( !$db->wasReadOnlyError() ) {
- throw $exception;
+ protected function handleReadError( DBError $exception ) {
+ if ( $exception instanceof DBConnectionError ) {
+ $this->connFailureTime = time();
+ $this->connFailureError = $exception;
}
-
- try {
- $db->rollback();
- } catch ( DBQueryError $e ) {
+ wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" );
+ if ( $this->db ) {
+ wfDebug( __METHOD__ . ": ignoring query error\n" );
+ } else {
+ wfDebug( __METHOD__ . ": ignoring connection error\n" );
}
+ }
- wfDebug( __METHOD__ . ": ignoring query error\n" );
- $db->ignoreErrors( false );
+ /**
+ * Handle a DBQueryError which occurred during a write operation.
+ */
+ protected function handleWriteError( DBError $exception ) {
+ if ( $exception instanceof DBConnectionError ) {
+ $this->connFailureTime = time();
+ $this->connFailureError = $exception;
+ }
+ if ( $this->db && $this->db->wasReadOnlyError() ) {
+ try {
+ $this->db->rollback( __METHOD__ );
+ } catch ( DBError $e ) {}
+ }
+ wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" );
+ if ( $this->db ) {
+ wfDebug( __METHOD__ . ": ignoring query error\n" );
+ } else {
+ wfDebug( __METHOD__ . ": ignoring connection error\n" );
+ }
}
/**
diff --git a/includes/objectcache/WinCacheBagOStuff.php b/includes/objectcache/WinCacheBagOStuff.php
index 7f464946..21aa39e7 100644
--- a/includes/objectcache/WinCacheBagOStuff.php
+++ b/includes/objectcache/WinCacheBagOStuff.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Object caching using WinCache.
+ *
+ * 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
+ */
/**
* Wrapper for WinCache object caching functions; identical interface
@@ -53,6 +74,9 @@ class WinCacheBagOStuff extends BagOStuff {
return true;
}
+ /**
+ * @return Array
+ */
public function keys() {
$info = wincache_ucache_info();
$list = $info['ucache_entries'];
diff --git a/includes/objectcache/XCacheBagOStuff.php b/includes/objectcache/XCacheBagOStuff.php
index 0ddf1245..bc68b596 100644
--- a/includes/objectcache/XCacheBagOStuff.php
+++ b/includes/objectcache/XCacheBagOStuff.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Object caching using XCache.
+ *
+ * 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
+ */
/**
* Wrapper for XCache object caching functions; identical interface
@@ -17,7 +38,13 @@ class XCacheBagOStuff extends BagOStuff {
$val = xcache_get( $key );
if ( is_string( $val ) ) {
- $val = unserialize( $val );
+ if ( $this->isInteger( $val ) ) {
+ $val = intval( $val );
+ } else {
+ $val = unserialize( $val );
+ }
+ } elseif ( is_null( $val ) ) {
+ return false;
}
return $val;
@@ -32,7 +59,11 @@ class XCacheBagOStuff extends BagOStuff {
* @return bool
*/
public function set( $key, $value, $expire = 0 ) {
- xcache_set( $key, serialize( $value ), $expire );
+ if ( !$this->isInteger( $value ) ) {
+ $value = serialize( $value );
+ }
+
+ xcache_set( $key, $value, $expire );
return true;
}
@@ -47,5 +78,12 @@ class XCacheBagOStuff extends BagOStuff {
xcache_unset( $key );
return true;
}
-}
+ public function incr( $key, $value = 1 ) {
+ return xcache_inc( $key, $value );
+ }
+
+ public function decr( $key, $value = 1 ) {
+ return xcache_dec( $key, $value );
+ }
+}
diff --git a/includes/parser/CacheTime.php b/includes/parser/CacheTime.php
new file mode 100644
index 00000000..881dded7
--- /dev/null
+++ b/includes/parser/CacheTime.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Parser cache specific expiry check.
+ *
+ * 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 cache specific expiry check.
+ *
+ * @ingroup Parser
+ */
+class CacheTime {
+
+ var $mVersion = Parser::VERSION, # Compatibility check
+ $mCacheTime = '', # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
+ $mCacheExpiry = null, # Seconds after which the object should expire, use 0 for uncachable. Used in ParserCache.
+ $mContainsOldMagic; # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
+
+ function getCacheTime() { return $this->mCacheTime; }
+
+ function containsOldMagic() { return $this->mContainsOldMagic; }
+ function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
+
+ /**
+ * setCacheTime() sets the timestamp expressing when the page has been rendered.
+ * This doesn not control expiry, see updateCacheExpiry() for that!
+ * @param $t string
+ * @return string
+ */
+ function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
+
+ /**
+ * Sets the number of seconds after which this object should expire.
+ * This value is used with the ParserCache.
+ * If called with a value greater than the value provided at any previous call,
+ * the new call has no effect. The value returned by getCacheExpiry is smaller
+ * or equal to the smallest number that was provided as an argument to
+ * updateCacheExpiry().
+ *
+ * @param $seconds number
+ */
+ function updateCacheExpiry( $seconds ) {
+ $seconds = (int)$seconds;
+
+ if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) {
+ $this->mCacheExpiry = $seconds;
+ }
+
+ // hack: set old-style marker for uncacheable entries.
+ if ( $this->mCacheExpiry !== null && $this->mCacheExpiry <= 0 ) {
+ $this->mCacheTime = -1;
+ }
+ }
+
+ /**
+ * Returns the number of seconds after which this object should expire.
+ * This method is used by ParserCache to determine how long the ParserOutput can be cached.
+ * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime().
+ * The value returned by getCacheExpiry is smaller or equal to the smallest number
+ * that was provided to a call of updateCacheExpiry(), and smaller or equal to the
+ * value of $wgParserCacheExpireTime.
+ * @return int|mixed|null
+ */
+ function getCacheExpiry() {
+ global $wgParserCacheExpireTime;
+
+ if ( $this->mCacheTime < 0 ) {
+ return 0;
+ } // old-style marker for "not cachable"
+
+ $expire = $this->mCacheExpiry;
+
+ if ( $expire === null ) {
+ $expire = $wgParserCacheExpireTime;
+ } else {
+ $expire = min( $expire, $wgParserCacheExpireTime );
+ }
+
+ if( $this->containsOldMagic() ) { //compatibility hack
+ $expire = min( $expire, 3600 ); # 1 hour
+ }
+
+ if ( $expire <= 0 ) {
+ return 0; // not cachable
+ } else {
+ return $expire;
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ function isCacheable() {
+ return $this->getCacheExpiry() > 0;
+ }
+
+ /**
+ * Return true if this cached output object predates the global or
+ * per-article cache invalidation timestamps, or if it comes from
+ * an incompatible older version.
+ *
+ * @param $touched String: the affected article's last touched timestamp
+ * @return Boolean
+ */
+ public function expired( $touched ) {
+ global $wgCacheEpoch;
+ return !$this->isCacheable() || // parser says it's uncacheable
+ $this->getCacheTime() < $touched ||
+ $this->getCacheTime() <= $wgCacheEpoch ||
+ $this->getCacheTime() < wfTimestamp( TS_MW, time() - $this->getCacheExpiry() ) || // expiry period has passed
+ !isset( $this->mVersion ) ||
+ version_compare( $this->mVersion, Parser::VERSION, "lt" );
+ }
+
+}
diff --git a/includes/parser/CoreLinkFunctions.php b/includes/parser/CoreLinkFunctions.php
index 8de13278..4bfa9d35 100644
--- a/includes/parser/CoreLinkFunctions.php
+++ b/includes/parser/CoreLinkFunctions.php
@@ -2,7 +2,23 @@
/**
* 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
*/
/**
diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php
index 0e5702b7..8917b6d0 100644
--- a/includes/parser/CoreParserFunctions.php
+++ b/includes/parser/CoreParserFunctions.php
@@ -2,7 +2,23 @@
/**
* Parser functions provided by MediaWiki core
*
+ * 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
*/
/**
@@ -55,6 +71,7 @@ class CoreParserFunctions {
$parser->setFunctionHook( 'padright', array( __CLASS__, 'padright' ), SFH_NO_HASH );
$parser->setFunctionHook( 'anchorencode', array( __CLASS__, 'anchorencode' ), SFH_NO_HASH );
$parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) );
+ $parser->setFunctionHook( 'speciale', array( __CLASS__, 'speciale' ) );
$parser->setFunctionHook( 'defaultsort', array( __CLASS__, 'defaultsort' ), SFH_NO_HASH );
$parser->setFunctionHook( 'filepath', array( __CLASS__, 'filepath' ), SFH_NO_HASH );
$parser->setFunctionHook( 'pagesincategory', array( __CLASS__, 'pagesincategory' ), SFH_NO_HASH );
@@ -62,6 +79,7 @@ class CoreParserFunctions {
$parser->setFunctionHook( 'protectionlevel', array( __CLASS__, 'protectionlevel' ), SFH_NO_HASH );
$parser->setFunctionHook( 'namespace', array( __CLASS__, 'mwnamespace' ), SFH_NO_HASH );
$parser->setFunctionHook( 'namespacee', array( __CLASS__, 'namespacee' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'namespacenumber', array( __CLASS__, 'namespacenumber' ), SFH_NO_HASH );
$parser->setFunctionHook( 'talkspace', array( __CLASS__, 'talkspace' ), SFH_NO_HASH );
$parser->setFunctionHook( 'talkspacee', array( __CLASS__, 'talkspacee' ), SFH_NO_HASH );
$parser->setFunctionHook( 'subjectspace', array( __CLASS__, 'subjectspace' ), SFH_NO_HASH );
@@ -111,7 +129,8 @@ class CoreParserFunctions {
* @return mixed|string
*/
static function formatDate( $parser, $date, $defaultPref = null ) {
- $df = DateFormatter::getInstance();
+ $lang = $parser->getFunctionLang();
+ $df = DateFormatter::getInstance( $lang );
$date = trim( $date );
@@ -158,6 +177,7 @@ class CoreParserFunctions {
* @param $parser Parser object
* @param $s String: The text to encode.
* @param $arg String (optional): The type of encoding.
+ * @return string
*/
static function urlencode( $parser, $s = '', $arg = null ) {
static $magicWords = null;
@@ -283,8 +303,10 @@ class CoreParserFunctions {
// Some shortcuts to avoid loading user data unnecessarily
if ( count( $forms ) === 0 ) {
+ wfProfileOut( __METHOD__ );
return '';
} elseif ( count( $forms ) === 1 ) {
+ wfProfileOut( __METHOD__ );
return $forms[0];
}
@@ -303,9 +325,9 @@ class CoreParserFunctions {
// check parameter, or use the ParserOptions if in interface message
$user = User::newFromName( $username );
if ( $user ) {
- $gender = $user->getOption( 'gender' );
+ $gender = GenderCache::singleton()->getGenderOf( $user, __METHOD__ );
} elseif ( $username === '' && $parser->getOptions()->getInterfaceMessage() ) {
- $gender = $parser->getOptions()->getUser()->getOption( 'gender' );
+ $gender = GenderCache::singleton()->getGenderOf( $parser->getOptions()->getUser(), __METHOD__ );
}
$ret = $parser->getFunctionLang()->gender( $gender, $forms );
wfProfileOut( __METHOD__ );
@@ -320,6 +342,7 @@ class CoreParserFunctions {
static function plural( $parser, $text = '' ) {
$forms = array_slice( func_get_args(), 2 );
$text = $parser->getFunctionLang()->parseFormattedNumber( $text );
+ settype( $text, ctype_digit( $text ) ? 'int' : 'float' );
return $parser->getFunctionLang()->convertPlural( $text, $forms );
}
@@ -420,6 +443,7 @@ class CoreParserFunctions {
* corresponding magic word
* Note: function name changed to "mwnamespace" rather than "namespace"
* to not break PHP 5.3
+ * @return mixed|string
*/
static function mwnamespace( $parser, $title = null ) {
$t = Title::newFromText( $title );
@@ -433,6 +457,12 @@ class CoreParserFunctions {
return '';
return wfUrlencode( $t->getNsText() );
}
+ static function namespacenumber( $parser, $title = null ) {
+ $t = Title::newFromText( $title );
+ if ( is_null( $t ) )
+ return '';
+ return $t->getNamespace();
+ }
static function talkspace( $parser, $title = null ) {
$t = Title::newFromText( $title );
if ( is_null( $t ) || !$t->canTalk() )
@@ -461,6 +491,7 @@ class CoreParserFunctions {
/**
* Functions to get and normalize pagenames, corresponding to the magic words
* of the same names
+ * @return String
*/
static function pagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
@@ -536,28 +567,64 @@ class CoreParserFunctions {
}
/**
- * Return the number of pages in the given category, or 0 if it's nonexis-
- * tent. This is an expensive parser function and can't be called too many
- * times per page.
+ * Return the number of pages, files or subcats in the given category,
+ * or 0 if it's nonexistent. This is an expensive parser function and
+ * can't be called too many times per page.
+ * @return string
*/
- static function pagesincategory( $parser, $name = '', $raw = null ) {
+ static function pagesincategory( $parser, $name = '', $arg1 = null, $arg2 = null ) {
+ static $magicWords = null;
+ if ( is_null( $magicWords ) ) {
+ $magicWords = new MagicWordArray( array(
+ 'pagesincategory_all',
+ 'pagesincategory_pages',
+ 'pagesincategory_subcats',
+ 'pagesincategory_files'
+ ) );
+ }
static $cache = array();
- $category = Category::newFromName( $name );
- if( !is_object( $category ) ) {
- $cache[$name] = 0;
+ // split the given option to its variable
+ if( self::isRaw( $arg1 ) ) {
+ //{{pagesincategory:|raw[|type]}}
+ $raw = $arg1;
+ $type = $magicWords->matchStartToEnd( $arg2 );
+ } else {
+ //{{pagesincategory:[|type[|raw]]}}
+ $type = $magicWords->matchStartToEnd( $arg1 );
+ $raw = $arg2;
+ }
+ if( !$type ) { //backward compatibility
+ $type = 'pagesincategory_all';
+ }
+
+ $title = Title::makeTitleSafe( NS_CATEGORY, $name );
+ if( !$title ) { # invalid title
return self::formatRaw( 0, $raw );
}
- # Normalize name for cache
- $name = $category->getName();
+ // Normalize name for cache
+ $name = $title->getDBkey();
- $count = 0;
- if( isset( $cache[$name] ) ) {
- $count = $cache[$name];
- } elseif( $parser->incrementExpensiveFunctionCount() ) {
- $count = $cache[$name] = (int)$category->getPageCount();
+ if( !isset( $cache[$name] ) ) {
+ $category = Category::newFromTitle( $title );
+
+ $allCount = $subcatCount = $fileCount = $pagesCount = 0;
+ 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();
+ $subcatCount = (int)$category->getSubcatCount();
+ $fileCount = (int)$category->getFileCount();
+ $pagesCount = $allCount - $subcatCount - $fileCount;
+ }
+ $cache[$name]['pagesincategory_all'] = $allCount;
+ $cache[$name]['pagesincategory_pages'] = $pagesCount;
+ $cache[$name]['pagesincategory_subcats'] = $subcatCount;
+ $cache[$name]['pagesincategory_files'] = $fileCount;
}
+
+ $count = $cache[$name][$type];
return self::formatRaw( $count, $raw );
}
@@ -576,6 +643,7 @@ class CoreParserFunctions {
* @param $parser Parser
* @param $page String TODO DOCUMENT (Default: empty string)
* @param $raw TODO DOCUMENT (Default: null)
+ * @return string
*/
static function pagesize( $parser, $page = '', $raw = null ) {
static $cache = array();
@@ -593,7 +661,7 @@ class CoreParserFunctions {
if( isset( $cache[$page] ) ) {
$length = $cache[$page];
} elseif( $parser->incrementExpensiveFunctionCount() ) {
- $rev = Revision::newFromTitle( $title );
+ $rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
$id = $rev ? $rev->getPage() : 0;
$length = $cache[$page] = $rev ? $rev->getSize() : 0;
@@ -605,7 +673,8 @@ class CoreParserFunctions {
/**
* Returns the requested protection level for the current page
- */
+ * @return string
+ */
static function protectionlevel( $parser, $type = '' ) {
$restrictions = $parser->mTitle->getRestrictions( strtolower( $type ) );
# Title::getRestrictions returns an array, its possible it may have
@@ -616,26 +685,20 @@ class CoreParserFunctions {
/**
* Gives language names.
* @param $parser Parser
- * @param $code String Language code
- * @param $language String Language code
+ * @param $code String Language code (of which to get name)
+ * @param $inLanguage String Language code (in which to get name)
* @return String
*/
- static function language( $parser, $code = '', $language = '' ) {
- global $wgContLang;
+ static function language( $parser, $code = '', $inLanguage = '' ) {
$code = strtolower( $code );
- $language = strtolower( $language );
-
- if ( $language !== '' ) {
- $names = Language::getTranslatedLanguageNames( $language );
- return isset( $names[$code] ) ? $names[$code] : wfBCP47( $code );
- }
-
- $lang = $wgContLang->getLanguageName( $code );
+ $inLanguage = strtolower( $inLanguage );
+ $lang = Language::fetchLanguageName( $code, $inLanguage );
return $lang !== '' ? $lang : wfBCP47( $code );
}
/**
* Unicode-safe str_pad with the restriction that $length is forced to be <= 500
+ * @return string
*/
static function pad( $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT ) {
$padding = $parser->killMarkers( $padding );
@@ -683,12 +746,16 @@ class CoreParserFunctions {
list( $page, $subpage ) = SpecialPageFactory::resolveAlias( $text );
if ( $page ) {
$title = SpecialPage::getTitleFor( $page, $subpage );
- return $title;
+ return $title->getPrefixedText();
} else {
- return wfMsgForContent( 'nosuchspecialpage' );
+ return wfMessage( 'nosuchspecialpage' )->inContentLanguage()->text();
}
}
+ static function speciale( $parser, $text ) {
+ return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) );
+ }
+
/**
* @param $parser Parser
* @param $text String The sortkey to use
@@ -716,48 +783,39 @@ class CoreParserFunctions {
return '';
} else {
return( '<span class="error">' .
- wfMsgForContent( 'duplicate-defaultsort',
- htmlspecialchars( $old ),
- htmlspecialchars( $text ) ) .
+ wfMessage( 'duplicate-defaultsort', $old, $text )->inContentLanguage()->escaped() .
'</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='' ) {
$file = wfFindFile( $name );
- $size = '';
- $argA_int = intval( $argA );
- $argB_int = intval( $argB );
-
- if ( $argB_int > 0 ) {
- // {{filepath: | option | size }}
- $size = $argB_int;
- $option = $argA;
-
- } elseif ( $argA_int > 0 ) {
- // {{filepath: | size [|option] }}
- $size = $argA_int;
- $option = $argB;
+ if( $argA == 'nowiki' ) {
+ // {{filepath: | option [| size] }}
+ $isNowiki = true;
+ $parsedWidthParam = $parser->parseWidthParam( $argB );
} else {
- // {{filepath: [|option] }}
- $option = $argA;
+ // {{filepath: [| size [|option]] }}
+ $parsedWidthParam = $parser->parseWidthParam( $argA );
+ $isNowiki = ($argB == 'nowiki');
}
if ( $file ) {
$url = $file->getFullUrl();
// If a size is requested...
- if ( is_integer( $size ) ) {
- $mto = $file->transform( array( 'width' => $size ) );
+ if ( count( $parsedWidthParam ) ) {
+ $mto = $file->transform( $parsedWidthParam );
// ... and we can
if ( $mto && !$mto->isError() ) {
// ... change the URL to point to a thumbnail.
$url = wfExpandUrl( $mto->getUrl(), PROTO_RELATIVE );
}
}
- if ( $option == 'nowiki' ) {
+ if ( $isNowiki ) {
return array( $url, 'nowiki' => true );
}
return $url;
@@ -768,6 +826,7 @@ class CoreParserFunctions {
/**
* Parser function to extension tag adaptor
+ * @return string
*/
public static function tagObj( $parser, $frame, $args ) {
if ( !count( $args ) ) {
@@ -784,7 +843,7 @@ class CoreParserFunctions {
$stripList = $parser->getStripList();
if ( !in_array( $tagName, $stripList ) ) {
return '<span class="error">' .
- wfMsgForContent( 'unknown_extension_tag', $tagName ) .
+ wfMessage( 'unknown_extension_tag', $tagName )->inContentLanguage()->text() .
'</span>';
}
diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php
index 7d488c4b..296be66f 100644
--- a/includes/parser/CoreTagHooks.php
+++ b/includes/parser/CoreTagHooks.php
@@ -2,7 +2,23 @@
/**
* Tag hooks provided by MediaWiki core
*
+ * 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
*/
/**
diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php
index 6559e886..2917b4a7 100644
--- a/includes/parser/DateFormatter.php
+++ b/includes/parser/DateFormatter.php
@@ -2,7 +2,23 @@
/**
* Date formatter
*
+ * 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
*/
/**
@@ -10,14 +26,15 @@
* @todo preferences, OutputPage
* @ingroup Parser
*/
-class DateFormatter
-{
+class DateFormatter {
var $mSource, $mTarget;
var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD;
var $regexes, $pDays, $pMonths, $pYears;
var $rules, $xMonths, $preferences;
+ protected $lang;
+
const ALL = -1;
const NONE = 0;
const MDY = 1;
@@ -32,15 +49,15 @@ class DateFormatter
const LAST = 8;
/**
- * @todo document
+ * @param $lang Language In which language to format the date
*/
- function __construct() {
- global $wgContLang;
+ function __construct( Language $lang ) {
+ $this->lang = $lang;
$this->monthNames = $this->getMonthRegex();
for ( $i=1; $i<=12; $i++ ) {
- $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i;
- $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i;
+ $this->xMonths[$this->lang->lc( $this->lang->getMonthName( $i ) )] = $i;
+ $this->xMonths[$this->lang->lc( $this->lang->getMonthAbbreviation( $i ) )] = $i;
}
$this->regexTrail = '(?![a-z])/iu';
@@ -103,16 +120,20 @@ class DateFormatter
/**
* Get a DateFormatter object
*
+ * @param $lang Language|string|null In which language to format the date
+ * Defaults to the site content language
* @return DateFormatter object
*/
- public static function &getInstance() {
- global $wgMemc;
+ public static function &getInstance( $lang = null ) {
+ global $wgMemc, $wgContLang;
static $dateFormatter = false;
+ $lang = $lang ? wfGetLangObj( $lang ) : $wgContLang;
+ $key = wfMemcKey( 'dateformatter', $lang->getCode() );
if ( !$dateFormatter ) {
- $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) );
+ $dateFormatter = $wgMemc->get( $key );
if ( !$dateFormatter ) {
- $dateFormatter = new DateFormatter;
- $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 );
+ $dateFormatter = new DateFormatter( $lang );
+ $wgMemc->set( $key, $dateFormatter, 3600 );
}
}
return $dateFormatter;
@@ -122,12 +143,12 @@ class DateFormatter
* @param $preference String: User preference
* @param $text String: Text to reformat
* @param $options Array: can contain 'linked' and/or 'match-whole'
+ * @return mixed|String
*/
function reformat( $preference, $text, $options = array('linked') ) {
-
$linked = in_array( 'linked', $options );
$match_whole = in_array( 'match-whole', $options );
-
+
if ( isset( $this->preferences[$preference] ) ) {
$preference = $this->preferences[$preference];
} else {
@@ -149,19 +170,19 @@ class DateFormatter
$this->mTarget = $i;
}
$regex = $this->regexes[$i];
-
+
// Horrible hack
if (!$linked) {
$regex = str_replace( array( '\[\[', '\]\]' ), '', $regex );
}
-
+
if ($match_whole) {
// Let's hope this works
$regex = preg_replace( '!^/!', '/^', $regex );
$regex = str_replace( $this->regexTrail,
'$'.$this->regexTrail, $regex );
}
-
+
// Another horrible hack
$this->mLinked = $linked;
$text = preg_replace_callback( $regex, array( &$this, 'replace' ), $text );
@@ -172,6 +193,7 @@ class DateFormatter
/**
* @param $matches
+ * @return string
*/
function replace( $matches ) {
# Extract information from $matches
@@ -186,10 +208,15 @@ class DateFormatter
$bits[$key[$p]] = $matches[$p+1];
}
}
-
+
return $this->formatDate( $bits, $linked );
}
-
+
+ /**
+ * @param $bits array
+ * @param $link bool
+ * @return string
+ */
function formatDate( $bits, $link = true ) {
$format = $this->targets[$this->mTarget];
@@ -203,13 +230,13 @@ class DateFormatter
# Construct new date
$text = '';
$fail = false;
-
+
// Pre-generate y/Y stuff because we need the year for the <span> title.
if ( !isset( $bits['y'] ) && isset( $bits['Y'] ) )
$bits['y'] = $this->makeIsoYear( $bits['Y'] );
if ( !isset( $bits['Y'] ) && isset( $bits['y'] ) )
$bits['Y'] = $this->makeNormalYear( $bits['y'] );
-
+
if ( !isset( $bits['m'] ) ) {
$m = $this->makeIsoMonth( $bits['F'] );
if ( !$m || $m == '00' ) {
@@ -218,7 +245,7 @@ class DateFormatter
$bits['m'] = $m;
}
}
-
+
if ( !isset($bits['d']) ) {
$bits['d'] = sprintf( '%02d', $bits['j'] );
}
@@ -248,8 +275,7 @@ class DateFormatter
if ( $m > 12 || $m < 1 ) {
$fail = true;
} else {
- global $wgContLang;
- $text .= $wgContLang->getMonthName( $m );
+ $text .= $this->lang->getMonthName( $m );
}
} else {
$text .= ucfirst( $bits['F'] );
@@ -265,30 +291,30 @@ class DateFormatter
if ( $fail ) {
$text = $matches[0];
}
-
+
$isoBits = array();
if ( isset($bits['y']) )
$isoBits[] = $bits['y'];
$isoBits[] = $bits['m'];
$isoBits[] = $bits['d'];
$isoDate = implode( '-', $isoBits );
-
+
// Output is not strictly HTML (it's wikitext), but <span> is whitelisted.
$text = Html::rawElement( 'span',
array( 'class' => 'mw-formatted-date', 'title' => $isoDate ), $text );
-
+
return $text;
}
/**
* @todo document
+ * @return string
*/
function getMonthRegex() {
- global $wgContLang;
$names = array();
for( $i = 1; $i <= 12; $i++ ) {
- $names[] = $wgContLang->getMonthName( $i );
- $names[] = $wgContLang->getMonthAbbreviation( $i );
+ $names[] = $this->lang->getMonthName( $i );
+ $names[] = $this->lang->getMonthAbbreviation( $i );
}
return implode( '|', $names );
}
@@ -299,9 +325,7 @@ class DateFormatter
* @return string ISO month name
*/
function makeIsoMonth( $monthName ) {
- global $wgContLang;
-
- $n = $this->xMonths[$wgContLang->lc( $monthName )];
+ $n = $this->xMonths[$this->lang->lc( $monthName )];
return sprintf( '%02d', $n );
}
@@ -325,6 +349,7 @@ class DateFormatter
/**
* @todo document
+ * @return int|string
*/
function makeNormalYear( $iso ) {
if ( $iso[0] == '-' ) {
diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php
index fb013047..d9356b48 100644
--- a/includes/parser/LinkHolderArray.php
+++ b/includes/parser/LinkHolderArray.php
@@ -2,7 +2,23 @@
/**
* Holder of replacement pairs for wiki links
*
+ * 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
*/
/**
@@ -33,7 +49,8 @@ class LinkHolderArray {
* serializing at present.
*
* Compact the titles, only serialize the text form.
- */
+ * @return array
+ */
function __sleep() {
foreach ( $this->internals as &$nsLinks ) {
foreach ( $nsLinks as &$entry ) {
@@ -134,6 +151,7 @@ class LinkHolderArray {
/**
* Get a subset of the current LinkHolderArray which is sufficient to
* interpret the given text.
+ * @return LinkHolderArray
*/
function getSubArray( $text ) {
$sub = new LinkHolderArray( $this->parent );
@@ -167,6 +185,7 @@ class LinkHolderArray {
/**
* Returns true if the memory requirements of this object are getting large
+ * @return bool
*/
function isBig() {
global $wgLinkHolderBatchSize;
@@ -190,6 +209,11 @@ class LinkHolderArray {
* article length checks (for stub links) to be bundled into a single query.
*
* @param $nt Title
+ * @param $text String
+ * @param $query Array [optional]
+ * @param $trail String [optional]
+ * @param $prefix String [optional]
+ * @return string
*/
function makeHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
wfProfileIn( __METHOD__ );
@@ -433,7 +457,7 @@ class LinkHolderArray {
foreach ( $entries as $index => $entry ) {
$pdbk = $entry['pdbk'];
// we only deal with new links (in its first query)
- if ( !isset( $colours[$pdbk] ) ) {
+ if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
$title = $entry['title'];
$titleText = $title->getText();
$titlesAttrs[] = array(
@@ -449,7 +473,7 @@ class LinkHolderArray {
}
// Now do the conversion and explode string to text of titles
- $titlesAllVariants = $wgContLang->autoConvertToAllVariants( $titlesToBeConverted );
+ $titlesAllVariants = $wgContLang->autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
$allVariantsName = array_keys( $titlesAllVariants );
foreach ( $titlesAllVariants as &$titlesVariant ) {
$titlesVariant = explode( "\0", $titlesVariant );
@@ -517,7 +541,7 @@ class LinkHolderArray {
$entry =& $this->internals[$ns][$index];
$pdbk = $entry['pdbk'];
- if(!isset($colours[$pdbk])){
+ if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
// found link in some of the variants, replace the link holder data
$entry['title'] = $variantTitle;
$entry['pdbk'] = $varPdbk;
diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php
index d4f167c9..2a24bee7 100644
--- a/includes/parser/Parser.php
+++ b/includes/parser/Parser.php
@@ -1,12 +1,29 @@
<?php
/**
- * @defgroup Parser Parser
+ * PHP parser that converts wiki markup to HTML.
+ *
+ * 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
- * File for Parser and related classes
*/
+/**
+ * @defgroup Parser Parser
+ */
/**
* PHP Parser - Processes wiki markup (which uses a more user-friendly
@@ -14,36 +31,38 @@
* transformation of that wiki markup it into XHTML output / markup
* (which in turn the browser understands, and can display).
*
- * <pre>
- * There are five main entry points into the Parser class:
- * parse()
+ * There are seven main entry points into the Parser class:
+ *
+ * - Parser::parse()
* produces HTML output
- * preSaveTransform().
+ * - Parser::preSaveTransform().
* produces altered wiki markup.
- * preprocess()
+ * - Parser::preprocess()
* removes HTML comments and expands templates
- * cleanSig() / cleanSigInSig()
+ * - Parser::cleanSig() and Parser::cleanSigInSig()
* Cleans a signature before saving it to preferences
- * getSection()
+ * - Parser::getSection()
* Return the content of a section from an article for section editing
- * replaceSection()
+ * - Parser::replaceSection()
* Replaces a section by number inside an article
- * getPreloadText()
+ * - Parser::getPreloadText()
* Removes <noinclude> sections, and <includeonly> tags.
*
* Globals used:
* object: $wgContLang
*
- * NOT $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
+ * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
*
- * settings:
- * $wgUseDynamicDates*, $wgInterwikiMagic*,
- * $wgNamespacesWithSubpages, $wgAllowExternalImages*,
- * $wgLocaltimezone, $wgAllowSpecialInclusion*,
- * $wgMaxArticleSize*
+ * @par Settings:
+ * $wgLocaltimezone
+ * $wgNamespacesWithSubpages
*
- * * only within ParserOptions
- * </pre>
+ * @par Settings only within ParserOptions:
+ * $wgAllowExternalImages
+ * $wgAllowSpecialInclusion
+ * $wgInterwikiMagic
+ * $wgMaxArticleSize
+ * $wgUseDynamicDates
*
* @ingroup Parser
*/
@@ -144,7 +163,8 @@ class Parser {
var $mLinkHolders;
var $mLinkID;
- var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
+ var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
+ var $mDefaultSort;
var $mTplExpandCache; # empty-frame expansion cache
var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
var $mExpensiveFunctionCount; # number of expensive parser function calls
@@ -188,7 +208,7 @@ class Parser {
public function __construct( $conf = array() ) {
$this->mConf = $conf;
$this->mUrlProtocols = wfUrlProtocols();
- $this->mExtLinkBracketedRegex = '/\[((' . wfUrlProtocols() . ')'.
+ $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'];
@@ -273,8 +293,6 @@ class Parser {
* Must not consist of all title characters, or else it will change
* the behaviour of <nowiki> in a link.
*/
- # $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
- # Changed to \x7f to allow XML double-parsing -- TS
$this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
$this->mStripState = new StripState( $this->mUniqPrefix );
@@ -289,6 +307,8 @@ class Parser {
'arg' => 0,
);
$this->mPPNodeCount = 0;
+ $this->mGeneratedPPNodeCount = 0;
+ $this->mHighestExpansionDepth = 0;
$this->mDefaultSort = false;
$this->mHeadings = array();
$this->mDoubleUnderscores = array();
@@ -321,13 +341,18 @@ class Parser {
* to internalParse() which does all the real work.
*/
- global $wgUseTidy, $wgAlwaysUseTidy, $wgDisableLangConversion, $wgDisableTitleConversion;
+ global $wgUseTidy, $wgAlwaysUseTidy;
$fname = __METHOD__.'-' . wfGetCaller();
wfProfileIn( __METHOD__ );
wfProfileIn( $fname );
$this->startParse( $title, $options, self::OT_HTML, $clearState );
+ # Remove the strip marker tag prefix from the input, if present.
+ if ( $clearState ) {
+ $text = str_replace( $this->mUniqPrefix, '', $text );
+ }
+
$oldRevisionId = $this->mRevisionId;
$oldRevisionObject = $this->mRevisionObject;
$oldRevisionTimestamp = $this->mRevisionTimestamp;
@@ -343,6 +368,7 @@ class Parser {
# No more strip!
wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
$text = $this->internalParse( $text );
+ wfRunHooks( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) );
$text = $this->mStripState->unstripGeneral( $text );
@@ -368,9 +394,8 @@ class Parser {
* c) It's a conversion table
* d) it is an interface message (which is in the user language)
*/
- if ( !( $wgDisableLangConversion
- || isset( $this->mDoubleUnderscores['nocontentconvert'] )
- || $this->mTitle->isConversionTable() ) )
+ if ( !( $options->getDisableContentConversion()
+ || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) )
{
# Run convert unconditionally in 1.18-compatible mode
global $wgBug34832TransitionalRollback;
@@ -389,8 +414,7 @@ class Parser {
* {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
* automatic link conversion.
*/
- if ( !( $wgDisableLangConversion
- || $wgDisableTitleConversion
+ if ( !( $options->getDisableTitleConversion()
|| isset( $this->mDoubleUnderscores['nocontentconvert'] )
|| isset( $this->mDoubleUnderscores['notitleconvert'] )
|| $this->mOutput->getDisplayTitle() !== false ) )
@@ -442,9 +466,12 @@ class Parser {
array_values( $tidyregs ),
$text );
}
- global $wgExpensiveParserFunctionLimit;
- if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
- $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
+
+ if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
+ $this->limitationWarn( 'expensive-parserfunction',
+ $this->mExpensiveFunctionCount,
+ $this->mOptions->getExpensiveParserFunctionLimit()
+ );
}
wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
@@ -452,12 +479,15 @@ 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}/$wgExpensiveParserFunctionLimit\n";
+ $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/{$this->mOptions->getExpensiveParserFunctionLimit()}\n";
$limitReport =
"NewPP limit report\n" .
- "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\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;
wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
$text .= "\n<!-- \n$limitReport-->\n";
@@ -497,6 +527,7 @@ class Parser {
/**
* Expand templates and variables in the text, producing valid, static wikitext.
* Also removes comments.
+ * @return mixed|string
*/
function preprocess( $text, Title $title, ParserOptions $options, $revid = null ) {
wfProfileIn( __METHOD__ );
@@ -530,10 +561,11 @@ class Parser {
}
/**
- * Process the wikitext for the ?preload= feature. (bug 5210)
+ * Process the wikitext for the "?preload=" feature. (bug 5210)
*
- * <noinclude>, <includeonly> etc. are parsed as for template transclusion,
- * comments, templates, arguments, tags hooks and parser functions are untouched.
+ * "<noinclude>", "<includeonly>" etc. are parsed as for template
+ * transclusion, comments, templates, arguments, tags hooks and parser
+ * functions are untouched.
*
* @param $text String
* @param $title Title
@@ -557,7 +589,7 @@ class Parser {
* @return string
*/
static public function getRandomString() {
- return dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) );
+ return wfRandomString( 16 );
}
/**
@@ -619,7 +651,7 @@ class Parser {
/**
* Accessor/mutator for the Title object
*
- * @param $x New Title object or null to just get the current one
+ * @param $x Title object or null to just get the current one
* @return Title object
*/
function Title( $x = null ) {
@@ -645,7 +677,7 @@ class Parser {
/**
* Accessor/mutator for the output type
*
- * @param $x New value or null to just get the current one
+ * @param $x int|null New value or null to just get the current one
* @return Integer
*/
function OutputType( $x = null ) {
@@ -673,8 +705,8 @@ class Parser {
/**
* Accessor/mutator for the ParserOptions object
*
- * @param $x New value or null to just get the current one
- * @return Current ParserOptions object
+ * @param $x ParserOptions New value or null to just get the current one
+ * @return ParserOptions Current ParserOptions object
*/
function Options( $x = null ) {
return wfSetVar( $this->mOptions, $x );
@@ -703,18 +735,24 @@ class Parser {
}
/**
- * Get the target language for the content being parsed. This is usually the
- * language that the content is in.
+ * Get the target language for the content being parsed. This is usually the
+ * language that the content is in.
+ *
+ * @since 1.19
+ *
+ * @return Language|null
*/
- function getTargetLanguage() {
+ public function getTargetLanguage() {
$target = $this->mOptions->getTargetLanguage();
+
if ( $target !== null ) {
return $target;
} elseif( $this->mOptions->getInterfaceMessage() ) {
return $this->mOptions->getUserLangObj();
} elseif( is_null( $this->mTitle ) ) {
- throw new MWException( __METHOD__.': $this->mTitle is null' );
+ throw new MWException( __METHOD__ . ': $this->mTitle is null' );
}
+
return $this->mTitle->getPageLanguage();
}
@@ -761,11 +799,14 @@ class Parser {
* in the text with a random marker and returns the next text. The output
* parameter $matches will be an associative array filled with data in
* the form:
+ *
+ * @code
* 'UNIQ-xxxxx' => array(
* 'element',
* 'tag content',
* array( 'param' => 'x' ),
* '<element param="x">tag content</element>' ) )
+ * @endcode
*
* @param $elements array list of element names. Comments are always extracted.
* @param $text string Source text string.
@@ -864,6 +905,7 @@ class Parser {
* parse the wiki syntax used to render tables
*
* @private
+ * @return string
*/
function doTableStuff( $text ) {
wfProfileIn( __METHOD__ );
@@ -1094,6 +1136,7 @@ class Parser {
$text = $this->replaceVariables( $text );
}
+ wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
$text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
@@ -1146,7 +1189,7 @@ class Parser {
'!(?: # Start cases
(<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
(<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
- (\\b(?:$prots)$urlChar+) | # m[3]: Free external links" . '
+ (\\b(?i:$prots)$urlChar+) | # m[3]: Free external links" . '
(?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
ISBN\s+(\b # m[5]: ISBN, capture number
(?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
@@ -1189,7 +1232,7 @@ class Parser {
throw new MWException( __METHOD__.': unrecognised match type "' .
substr( $m[0], 0, 20 ) . '"' );
}
- $url = wfMsgForContent( $urlmsg, $id );
+ $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $CssClass );
} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
# ISBN
@@ -1249,7 +1292,7 @@ class Parser {
$text = $this->maybeMakeExternalImage( $url );
if ( $text === false ) {
# Not an image, make a link
- $text = Linker::makeExternalLink( $url,
+ $text = Linker::makeExternalLink( $url,
$this->getConverterLanguage()->markNoConversion($url), true, 'free',
$this->getExternalLinkAttribs( $url ) );
# Register it in the output object...
@@ -1646,7 +1689,7 @@ class Parser {
}
if ( !$text && $this->mOptions->getEnableImageWhitelist()
&& preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
- $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) );
+ $whitelist = explode( "\n", wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() );
foreach ( $whitelist as $entry ) {
# Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
@@ -1698,7 +1741,7 @@ class Parser {
$holders = new LinkHolderArray( $this );
- # split the entire text string on occurences of [[
+ # split the entire text string on occurrences of [[
$a = StringUtils::explode( '[[', ' ' . $s );
# get the first element (all text up to first [[), and remove the space we added
$s = $a->current();
@@ -1711,7 +1754,7 @@ class Parser {
if ( $useLinkPrefixExtension ) {
# Match the end of a line for a word that's not followed by whitespace,
# e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
- $e2 = wfMsgForContent( 'linkprefix' );
+ $e2 = wfMessage( 'linkprefix' )->inContentLanguage()->text();
}
if ( is_null( $this->mTitle ) ) {
@@ -1733,7 +1776,7 @@ class Parser {
}
if ( $this->getConverterLanguage()->hasVariants() ) {
- $selflink = $this->getConverterLanguage()->autoConvertToAllVariants(
+ $selflink = $this->getConverterLanguage()->autoConvertToAllVariants(
$this->mTitle->getPrefixedText() );
} else {
$selflink = array( $this->mTitle->getPrefixedText() );
@@ -1812,7 +1855,7 @@ class Parser {
# Don't allow internal links to pages containing
# PROTO: where PROTO is a valid URL protocol; these
# should be external links.
- if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
+ if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $m[1] ) ) {
$s .= $prefix . '[[' . $line ;
wfProfileOut( __METHOD__."-misc" );
continue;
@@ -1902,11 +1945,9 @@ class Parser {
# Link not escaped by : , create the various objects
if ( $noforce ) {
- global $wgContLang;
-
# Interwikis
wfProfileIn( __METHOD__."-interwiki" );
- if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
+ if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
$this->mOutput->addLanguageLink( $nt->getFullText() );
$s = rtrim( $s . $prefix );
$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
@@ -2051,7 +2092,7 @@ class Parser {
* @return String: less-or-more HTML with NOPARSE bits
*/
function armorLinks( $text ) {
- return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
+ return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
"{$this->mUniqPrefix}NOPARSE$1", $text );
}
@@ -2122,7 +2163,7 @@ class Parser {
* element appropriate to the prefix character passed into them.
* @private
*
- * @param $char char
+ * @param $char string
*
* @return string
*/
@@ -2384,7 +2425,7 @@ class Parser {
}
/**
- * Split up a string on ':', ignoring any occurences inside tags
+ * Split up a string on ':', ignoring any occurrences inside tags
* to prevent illegal overlapping.
*
* @param $str String the string to split
@@ -2698,6 +2739,18 @@ class Parser {
$subjPage = $this->mTitle->getSubjectPage();
$value = wfEscapeWikiText( $subjPage->getPrefixedUrl() );
break;
+ case 'pageid': // requested in bug 23427
+ $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
+ # will have a different value once the page is saved.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
+ }
+ $value = $pageid ? $pageid : null;
+ break;
case 'revisionid':
# Let the edit saving system know we should parse the page
# *after* a revision ID has been assigned.
@@ -2760,6 +2813,9 @@ class Parser {
case 'namespacee':
$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
break;
+ case 'namespacenumber':
+ $value = $this->mTitle->getNamespace();
+ break;
case 'talkspace':
$value = $this->mTitle->canTalk() ? str_replace( '_',' ',$this->mTitle->getTalkNsText() ) : '';
break;
@@ -2834,7 +2890,8 @@ class Parser {
$value = $pageLang->formatNum( SiteStats::edits() );
break;
case 'numberofviews':
- $value = $pageLang->formatNum( SiteStats::views() );
+ global $wgDisableCounters;
+ $value = !$wgDisableCounters ? $pageLang->formatNum( SiteStats::views() ) : '';
break;
case 'currenttimestamp':
$value = wfTimestamp( TS_MW, $ts );
@@ -2900,7 +2957,7 @@ class Parser {
*
* @param $text String: The text to parse
* @param $flags Integer: bitwise combination of:
- * self::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
+ * self::PTD_FOR_INCLUSION Handle "<noinclude>" and "<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.
@@ -3027,13 +3084,14 @@ class Parser {
* 'post-expand-template-inclusion' (corresponding messages:
* 'post-expand-template-inclusion-warning',
* 'post-expand-template-inclusion-category')
- * @param $current Current value
- * @param $max Maximum allowed, when an explicit limit has been
+ * @param $current int|null Current value
+ * @param $max int|null Maximum allowed, when an explicit limit has been
* exceeded, provide the values (optional)
*/
- function limitationWarn( $limitationType, $current=null, $max=null) {
+ function limitationWarn( $limitationType, $current = '', $max = '' ) {
# does no harm if $current and $max are present but are unnecessary for the message
- $warning = wfMsgExt( "$limitationType-warning", array( 'parsemag', 'escape' ), $current, $max );
+ $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
+ ->inContentLanguage()->escaped();
$this->mOutput->addWarning( $warning );
$this->addTrackingCategory( "$limitationType-category" );
}
@@ -3051,7 +3109,7 @@ class Parser {
* @private
*/
function braceSubstitution( $piece, $frame ) {
- global $wgNonincludableNamespaces, $wgContLang;
+ global $wgContLang;
wfProfileIn( __METHOD__ );
wfProfileIn( __METHOD__.'-setup' );
@@ -3238,7 +3296,8 @@ class Parser {
if ( $frame->depth >= $limit ) {
$found = true;
$text = '<span class="error">'
- . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit )
+ . wfMessage( 'parser-template-recursion-depth-warning' )
+ ->numParams( $limit )->inContentLanguage()->text()
. '</span>';
}
}
@@ -3246,8 +3305,11 @@ class Parser {
# Load from database
if ( !$found && $title ) {
- $titleProfileIn = __METHOD__ . "-title-" . $title->getDBKey();
- wfProfileIn( $titleProfileIn ); // template in
+ if ( !Profiler::instance()->isPersistent() ) {
+ # Too many unique items can kill profiling DBs/collectors
+ $titleProfileIn = __METHOD__ . "-title-" . $title->getDBKey();
+ wfProfileIn( $titleProfileIn ); // template in
+ }
wfProfileIn( __METHOD__ . '-loadtpl' );
if ( !$title->isExternal() ) {
if ( $title->isSpecialPage()
@@ -3281,7 +3343,7 @@ class Parser {
$isHTML = true;
$this->disableCache();
}
- } elseif ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
+ } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
$found = false; # access denied
wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
} else {
@@ -3315,7 +3377,9 @@ class Parser {
# This has to be done after redirect resolution to avoid infinite loops via redirects
if ( !$frame->loopCheck( $title ) ) {
$found = true;
- $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>';
+ $text = '<span class="error">'
+ . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
+ . '</span>';
wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
}
wfProfileOut( __METHOD__ . '-loadtpl' );
@@ -3362,10 +3426,8 @@ class Parser {
}
# Replace raw HTML by a placeholder
- # Add a blank line preceding, to prevent it from mucking up
- # immediately preceding headings
if ( $isHTML ) {
- $text = "\n\n" . $this->insertStripItem( $text );
+ $text = $this->insertStripItem( $text );
} elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
# Escape nowiki-style return values
$text = wfEscapeWikiText( $text );
@@ -3476,7 +3538,7 @@ class Parser {
* Static function to get a template
* Can be overridden via ParserOptions::setTemplateCallback().
*
- * @parma $title Title
+ * @param $title Title
* @param $parser Parser
*
* @return array
@@ -3505,7 +3567,7 @@ class Parser {
# Get the revision
$rev = $id
? Revision::newFromId( $id )
- : Revision::newFromTitle( $title );
+ : Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
$rev_id = $rev ? $rev->getId() : 0;
# If there is no current revision, there is no page
if ( $id === false && !$rev ) {
@@ -3556,7 +3618,7 @@ class Parser {
* 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
- * @return File|false
+ * @return File|bool
*/
function fetchFile( $title, $options = array() ) {
$res = $this->fetchFileAndTitle( $title, $options );
@@ -3607,13 +3669,13 @@ class Parser {
global $wgEnableScaryTranscluding;
if ( !$wgEnableScaryTranscluding ) {
- return wfMsgForContent('scarytranscludedisabled');
+ return wfMessage('scarytranscludedisabled')->inContentLanguage()->text();
}
$url = $title->getFullUrl( "action=$action" );
if ( strlen( $url ) > 255 ) {
- return wfMsgForContent( 'scarytranscludetoolong' );
+ return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
}
return $this->fetchScaryTemplateMaybeFromCache( $url );
}
@@ -3634,7 +3696,7 @@ class Parser {
$text = Http::get( $url );
if ( !$text ) {
- return wfMsgForContent( 'scarytranscludefailed', $url );
+ return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
}
$dbw = wfGetDB( DB_MASTER );
@@ -3650,7 +3712,7 @@ class Parser {
* Triple brace replacement -- used for template arguments
* @private
*
- * @param $peice array
+ * @param $piece array
* @param $frame PPFrame
*
* @return array
@@ -3700,7 +3762,7 @@ class Parser {
* Return the text to be used for a given extension tag.
* This is the ghost of strip().
*
- * @param $params Associative array of parameters:
+ * @param $params array 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
@@ -3808,12 +3870,8 @@ class Parser {
* @return Boolean: false if the limit has been exceeded
*/
function incrementExpensiveFunctionCount() {
- global $wgExpensiveParserFunctionLimit;
$this->mExpensiveFunctionCount++;
- if ( $this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit ) {
- return true;
- }
- return false;
+ return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
}
/**
@@ -3921,6 +3979,7 @@ class Parser {
* @param $text String
* @param $origText String: original, untouched wikitext
* @param $isMain Boolean
+ * @return mixed|string
* @private
*/
function formatHeadings( $text, $origText, $isMain=true ) {
@@ -4147,7 +4206,7 @@ class Parser {
# Don't number the heading if it is the only one (looks silly)
if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
# the two are different if the line contains a link
- $headline = $numbering . ' ' . $headline;
+ $headline = Html::element( 'span', array( 'class' => 'mw-headline-number' ), $numbering ) . ' ' . $headline;
}
# Create the anchor for linking from the TOC to the section
@@ -4286,7 +4345,7 @@ class Parser {
}
/**
- * Transform wiki markup when saving a page by doing \r\n -> \n
+ * 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
@@ -4362,7 +4421,7 @@ class Parser {
$text = $this->replaceVariables( $text );
# This works almost by chance, as the replaceVariables are done before the getUserSig(),
- # which may corrupt this parser instance via its wfMsgExt( parsemag ) call-
+ # which may corrupt this parser instance via its wfMessage()->text() call-
# Signatures
$sigText = $this->getUserSig( $user );
@@ -4373,13 +4432,12 @@ class Parser {
) );
# Context links: [[|name]] and [[name (context)|]]
- global $wgLegalTitleChars;
- $tc = "[$wgLegalTitleChars]";
+ $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|]]
+ $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]]
$p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
@@ -4583,7 +4641,7 @@ class Parser {
}
/**
- * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
+ * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>"
* The callback should have the following form:
* function myParserHook( $text, $params, $parser, $frame ) { ... }
*
@@ -4601,13 +4659,15 @@ class Parser {
* this interface, as it is not documented and injudicious use could smash
* private variables.**
*
- * @param $tag Mixed: the tag to use, e.g. 'hook' for <hook>
+ * @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
- * @return The old value of the mTagHooks array associated with the hook
+ * @return Mixed|null The old value of the mTagHooks array associated with the hook
*/
public function setHook( $tag, $callback ) {
$tag = strtolower( $tag );
- if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
+ if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
+ throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
+ }
$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
$this->mTagHooks[$tag] = $callback;
if ( !in_array( $tag, $this->mStripList ) ) {
@@ -4629,13 +4689,15 @@ class Parser {
* @since 1.10
* @todo better document or deprecate this
*
- * @param $tag Mixed: the tag to use, e.g. 'hook' for <hook>
+ * @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
- * @return The old value of the mTagHooks array associated with the hook
+ * @return Mixed|null The old value of the mTagHooks array associated with the hook
*/
function setTransparentTagHook( $tag, $callback ) {
$tag = strtolower( $tag );
- if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
+ if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
+ throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
+ }
$oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
$this->mTransparentTagHooks[$tag] = $callback;
@@ -4691,7 +4753,7 @@ class Parser {
* Please read the documentation in includes/parser/Preprocessor.php for more information
* about the methods available in PPFrame and PPNode.
*
- * @return The old callback function for this name, if any
+ * @return string|callback The old callback function for this name, if any
*/
public function setFunctionHook( $id, $callback, $flags = 0 ) {
global $wgContLang;
@@ -4735,9 +4797,10 @@ class Parser {
}
/**
- * Create a tag function, e.g. <test>some stuff</test>.
+ * 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.
+ * @return null
*/
function setFunctionTagHook( $tag, $callback, $flags ) {
$tag = strtolower( $tag );
@@ -4755,7 +4818,7 @@ class Parser {
/**
* @todo FIXME: Update documentation. makeLinkObj() is deprecated.
- * Replace <!--LINK--> link placeholders with actual links, in the buffer
+ * Replace "<!--LINK-->" link placeholders with actual links, in the buffer
* Placeholders created in Skin::makeLinkObj()
*
* @param $text string
@@ -4768,7 +4831,7 @@ class Parser {
}
/**
- * Replace <!--LINK--> link placeholders with plain text of links
+ * Replace "<!--LINK-->" link placeholders with plain text of links
* (not HTML-formatted).
*
* @param $text String
@@ -4845,30 +4908,41 @@ class Parser {
$label = '';
$alt = '';
+ $link = '';
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
$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
- $altmatches = StringUtils::explode('|', $matches[3]);
+ $parameterMatches = StringUtils::explode('|', $matches[3]);
$magicWordAlt = MagicWord::get( 'img_alt' );
+ $magicWordLink = MagicWord::get( 'img_link' );
- foreach ( $altmatches as $altmatch ) {
- $match = $magicWordAlt->matchVariableStartToEnd( $altmatch );
- if ( $match ) {
+ 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();
+ }
+ }
else {
// concatenate all other pipes
- $label .= '|' . $altmatch;
+ $label .= '|' . $parameterMatch;
}
}
// remove the first pipe
$label = substr( $label, 1 );
}
- $ig->add( $title, $label, $alt );
+ $ig->add( $title, $label, $alt ,$link);
}
return $ig->toHTML();
}
@@ -4890,7 +4964,7 @@ class Parser {
'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
'bottom', 'text-bottom' ),
'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
- 'upright', 'border', 'link', 'alt' ),
+ 'upright', 'border', 'link', 'alt', 'class' ),
);
static $internalParamMap;
if ( !$internalParamMap ) {
@@ -4922,7 +4996,7 @@ class Parser {
*
* @param $title Title
* @param $options String
- * @param $holders LinkHolderArray|false
+ * @param $holders LinkHolderArray|bool
* @return string HTML
*/
function makeImage( $title, $options, $holders = false ) {
@@ -4940,6 +5014,7 @@ class Parser {
# * upright reduce width for upright images, rounded to full __0 px
# * border draw a 1px border around the image
# * alt Text for HTML alt attribute (defaults to empty)
+ # * class Set a class for img node
# * link Set the target of the image link. Can be external, interwiki, or local
# vertical-align values (no % or length right now):
# * baseline
@@ -4983,27 +5058,22 @@ class Parser {
# Special case; width and height come in one variable together
if ( $type === 'handler' && $paramName === 'width' ) {
- $m = array();
- # (bug 13500) In both cases (width/height and width only),
- # permit trailing "px" for backward compatibility.
- if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
- $width = intval( $m[1] );
- $height = intval( $m[2] );
+ $parsedWidthParam = $this->parseWidthParam( $value );
+ if( isset( $parsedWidthParam['width'] ) ) {
+ $width = $parsedWidthParam['width'];
if ( $handler->validateParam( 'width', $width ) ) {
$params[$type]['width'] = $width;
$validated = true;
}
+ }
+ if( isset( $parsedWidthParam['height'] ) ) {
+ $height = $parsedWidthParam['height'];
if ( $handler->validateParam( 'height', $height ) ) {
$params[$type]['height'] = $height;
$validated = true;
}
- } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
- $width = intval( $value );
- if ( $handler->validateParam( 'width', $width ) ) {
- $params[$type]['width'] = $width;
- $validated = true;
- }
- } # else no validation -- bug 13436
+ }
+ # else no validation -- bug 13436
} else {
if ( $type === 'handler' ) {
# Validate handler parameter
@@ -5013,6 +5083,7 @@ class Parser {
switch( $paramName ) {
case 'manualthumb':
case 'alt':
+ case 'class':
# @todo FIXME: Possibly check validity here for
# manualthumb? downstream behavior seems odd with
# missing manual thumbs.
@@ -5026,8 +5097,8 @@ class Parser {
$paramName = 'no-link';
$value = true;
$validated = true;
- } elseif ( preg_match( "/^$prots/", $value ) ) {
- if ( preg_match( "/^($prots)$chars+$/u", $value, $m ) ) {
+ } elseif ( preg_match( "/^(?i)$prots/", $value ) ) {
+ if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) {
$paramName = 'link-url';
$this->mOutput->addExternalLink( $value );
if ( $this->mOptions->getExternalLinkTarget() ) {
@@ -5116,11 +5187,11 @@ class Parser {
$params['frame']['title'] = $this->stripAltText( $caption, $holders );
}
- wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
+ wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params, $this ) );
# Linker does the rest
$time = isset( $options['time'] ) ? $options['time'] : false;
- $ret = Linker::makeImageLink2( $title, $file, $params['frame'], $params['handler'],
+ $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
$time, $descQuery, $this->mOptions->getThumbSize() );
# Give the handler a chance to modify the parser object
@@ -5229,13 +5300,13 @@ class Parser {
*
* @param $text String: Page wikitext
* @param $section String: a section identifier string of the form:
- * <flag1> - <flag2> - ... - <section number>
+ * "<flag1> - <flag2> - ... - <section number>"
*
* Currently the only recognised flag is "T", which means the target section number
* was derived during a template inclusion parse, in other words this is a template
* section edit link. If no flags are given, it was an ordinary section edit link.
* This flag is required to avoid a section numbering mismatch when a section is
- * enclosed by <includeonly> (bug 6563).
+ * enclosed by "<includeonly>" (bug 6563).
*
* The section number 0 pulls the text before the first heading; other numbers will
* pull the given section along with its lower-level subsections. If the section is
@@ -5381,7 +5452,7 @@ class Parser {
* section does not exist, $oldtext is returned unchanged.
*
* @param $oldtext String: former text of the article
- * @param $section Numeric: section identifier
+ * @param $section int section identifier
* @param $text String: replacing text
* @return String: modified text
*/
@@ -5464,7 +5535,7 @@ class Parser {
/**
* Mutator for $mDefaultSort
*
- * @param $sort New value
+ * @param $sort string New value
*/
public function setDefaultSort( $sort ) {
$this->mDefaultSort = $sort;
@@ -5542,7 +5613,7 @@ class Parser {
*
* @param $text String: text string to be stripped of wikitext
* for use in a Section anchor
- * @return Filtered text string
+ * @return string Filtered text string
*/
public function stripSectionName( $text ) {
# Strip internal link markup
@@ -5553,7 +5624,7 @@ class Parser {
# @todo FIXME: Not tolerant to blank link text
# I.E. [http://www.mediawiki.org] will render as [1] or something depending
# on how many empty links there are on the page - need to figure that out.
- $text = preg_replace( '/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
+ $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
# Parse wikitext quotes (italics & bold)
$text = $this->doQuotes( $text );
@@ -5691,7 +5762,7 @@ 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 Serialized data
+ * @param $data array Serialized data
* @return String
*/
function unserializeHalfParsedText( $data ) {
@@ -5722,4 +5793,32 @@ class Parser {
function isValidHalfParsedText( $data ) {
return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
}
+
+ /**
+ * Parsed a width param of imagelink like 300px or 200x300px
+ *
+ * @param $value String
+ *
+ * @return array
+ * @since 1.20
+ */
+ public function parseWidthParam( $value ) {
+ $parsedWidthParam = array();
+ if( $value === '' ) {
+ return $parsedWidthParam;
+ }
+ $m = array();
+ # (bug 13500) In both cases (width/height and width only),
+ # permit trailing "px" for backward compatibility.
+ if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
+ $width = intval( $m[1] );
+ $height = intval( $m[2] );
+ $parsedWidthParam['width'] = $width;
+ $parsedWidthParam['height'] = $height;
+ } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
+ $width = intval( $value );
+ $parsedWidthParam['width'] = $width;
+ }
+ return $parsedWidthParam;
+ }
}
diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php
index 8b043290..6a4ef0c5 100644
--- a/includes/parser/ParserCache.php
+++ b/includes/parser/ParserCache.php
@@ -2,7 +2,23 @@
/**
* Cache for outputs of the PHP parser
*
+ * 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 Parser
*/
/**
@@ -77,6 +93,7 @@ class ParserCache {
*
* @param $article Article
* @param $popts ParserOptions
+ * @return string
*/
function getETag( $article, $popts ) {
return 'W/"' . $this->getParserOutputKey( $article,
@@ -88,7 +105,7 @@ class ParserCache {
* Retrieve the ParserOutput from ParserCache, even if it's outdated.
* @param $article Article
* @param $popts ParserOptions
- * @return ParserOutput|false
+ * @return ParserOutput|bool False on failure
*/
public function getDirty( $article, $popts ) {
$value = $this->get( $article, $popts, true );
@@ -102,8 +119,10 @@ class ParserCache {
*
* @todo Document parameter $useOutdated
*
- * @param $article Article
- * @param $popts ParserOptions
+ * @param $article Article
+ * @param $popts ParserOptions
+ * @param $useOutdated Boolean (default true)
+ * @return bool|mixed|string
*/
public function getKey( $article, $popts, $useOutdated = true ) {
global $wgCacheEpoch;
@@ -139,11 +158,11 @@ class ParserCache {
* Retrieve the ParserOutput from ParserCache.
* false if not found or outdated.
*
- * @param $article Article
- * @param $popts ParserOptions
- * @param $useOutdated
+ * @param $article Article
+ * @param $popts ParserOptions
+ * @param $useOutdated Boolean (default false)
*
- * @return ParserOutput|false
+ * @return ParserOutput|bool False on failure
*/
public function get( $article, $popts, $useOutdated = false ) {
global $wgCacheEpoch;
diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php
index 57d3a7eb..009b18a1 100644
--- a/includes/parser/ParserOptions.php
+++ b/includes/parser/ParserOptions.php
@@ -1,6 +1,21 @@
<?php
/**
- * \brief Options for the PHP parser
+ * Options for the PHP parser
+ *
+ * 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
@@ -79,6 +94,11 @@ class ParserOptions {
* Maximum number of nodes touched by PPFrame::expand()
*/
var $mMaxPPNodeCount;
+
+ /**
+ * Maximum number of nodes generated by Preprocessor::preprocessToObj()
+ */
+ var $mMaxGeneratedPPNodeCount;
/**
* Maximum recursion depth in PPFrame::expand()
@@ -91,6 +111,11 @@ class ParserOptions {
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;
@@ -130,6 +155,16 @@ class ParserOptions {
var $mPreSaveTransform = true;
/**
+ * Whether content conversion should be disabled
+ */
+ var $mDisableContentConversion;
+
+ /**
+ * Whether title conversion should be disabled
+ */
+ var $mDisableTitleConversion;
+
+ /**
* Automatically number headings?
*/
var $mNumberHeadings;
@@ -199,13 +234,18 @@ class ParserOptions {
function getTargetLanguage() { return $this->mTargetLanguage; }
function getMaxIncludeSize() { return $this->mMaxIncludeSize; }
function getMaxPPNodeCount() { return $this->mMaxPPNodeCount; }
+ function getMaxGeneratedPPNodeCount() { return $this->mMaxGeneratedPPNodeCount; }
function getMaxPPExpandDepth() { return $this->mMaxPPExpandDepth; }
function getMaxTemplateDepth() { return $this->mMaxTemplateDepth; }
+ /* @since 1.20 */
+ function getExpensiveParserFunctionLimit() { return $this->mExpensiveParserFunctionLimit; }
function getRemoveComments() { return $this->mRemoveComments; }
function getTemplateCallback() { return $this->mTemplateCallback; }
function getEnableLimitReport() { return $this->mEnableLimitReport; }
function getCleanSignatures() { return $this->mCleanSignatures; }
function getExternalLinkTarget() { return $this->mExternalLinkTarget; }
+ function getDisableContentConversion() { return $this->mDisableContentConversion; }
+ function getDisableTitleConversion() { return $this->mDisableTitleConversion; }
function getMath() { $this->optionUsed( 'math' );
return $this->mMath; }
function getThumbSize() { $this->optionUsed( 'thumbsize' );
@@ -285,13 +325,18 @@ class ParserOptions {
function setTargetLanguage( $x ) { return wfSetVar( $this->mTargetLanguage, $x, true ); }
function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); }
function setMaxPPNodeCount( $x ) { return wfSetVar( $this->mMaxPPNodeCount, $x ); }
+ function setMaxGeneratedPPNodeCount( $x ) { return wfSetVar( $this->mMaxGeneratedPPNodeCount, $x ); }
function setMaxTemplateDepth( $x ) { return wfSetVar( $this->mMaxTemplateDepth, $x ); }
+ /* @since 1.20 */
+ function setExpensiveParserFunctionLimit( $x ) { return wfSetVar( $this->mExpensiveParserFunctionLimit, $x ); }
function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); }
function setTemplateCallback( $x ) { return wfSetVar( $this->mTemplateCallback, $x ); }
function enableLimitReport( $x = true ) { return wfSetVar( $this->mEnableLimitReport, $x ); }
function setTimestamp( $x ) { return wfSetVar( $this->mTimestamp, $x ); }
function setCleanSignatures( $x ) { return wfSetVar( $this->mCleanSignatures, $x ); }
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 ); }
function setMath( $x ) { return wfSetVar( $this->mMath, $x ); }
function setUserLang( $x ) {
if ( is_string( $x ) ) {
@@ -380,7 +425,8 @@ class ParserOptions {
global $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages,
$wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion,
$wgMaxArticleSize, $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth,
- $wgCleanSignatures, $wgExternalLinkTarget;
+ $wgCleanSignatures, $wgExternalLinkTarget, $wgExpensiveParserFunctionLimit,
+ $wgMaxGeneratedPPNodeCount, $wgDisableLangConversion, $wgDisableTitleConversion;
wfProfileIn( __METHOD__ );
@@ -392,10 +438,14 @@ class ParserOptions {
$this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
$this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
$this->mMaxPPNodeCount = $wgMaxPPNodeCount;
+ $this->mMaxGeneratedPPNodeCount = $wgMaxGeneratedPPNodeCount;
$this->mMaxPPExpandDepth = $wgMaxPPExpandDepth;
$this->mMaxTemplateDepth = $wgMaxTemplateDepth;
+ $this->mExpensiveParserFunctionLimit = $wgExpensiveParserFunctionLimit;
$this->mCleanSignatures = $wgCleanSignatures;
$this->mExternalLinkTarget = $wgExternalLinkTarget;
+ $this->mDisableContentConversion = $wgDisableLangConversion;
+ $this->mDisableTitleConversion = $wgDisableLangConversion || $wgDisableTitleConversion;
$this->mUser = $user;
$this->mNumberHeadings = $user->getOption( 'numberheadings' );
@@ -428,6 +478,7 @@ class ParserOptions {
* Returns the full array of options that would have been used by
* in 1.16.
* Used to get the old parser cache entries when available.
+ * @return array
*/
public static function legacyOptions() {
global $wgUseDynamicDates;
diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php
index 2d99a3b5..41b4a385 100644
--- a/includes/parser/ParserOutput.php
+++ b/includes/parser/ParserOutput.php
@@ -1,118 +1,26 @@
<?php
+
/**
- * Output of the PHP parser
+ * Output of the PHP parser.
+ *
+ * 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
*/
-
-/**
- * @todo document
- * @ingroup Parser
- */
-
-class CacheTime {
- var $mVersion = Parser::VERSION, # Compatibility check
- $mCacheTime = '', # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
- $mCacheExpiry = null, # Seconds after which the object should expire, use 0 for uncachable. Used in ParserCache.
- $mContainsOldMagic; # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
-
- function getCacheTime() { return $this->mCacheTime; }
-
- function containsOldMagic() { return $this->mContainsOldMagic; }
- function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
-
- /**
- * setCacheTime() sets the timestamp expressing when the page has been rendered.
- * This doesn not control expiry, see updateCacheExpiry() for that!
- * @param $t string
- * @return string
- */
- function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
-
- /**
- * Sets the number of seconds after which this object should expire.
- * This value is used with the ParserCache.
- * If called with a value greater than the value provided at any previous call,
- * the new call has no effect. The value returned by getCacheExpiry is smaller
- * or equal to the smallest number that was provided as an argument to
- * updateCacheExpiry().
- *
- * @param $seconds number
- */
- function updateCacheExpiry( $seconds ) {
- $seconds = (int)$seconds;
-
- if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) {
- $this->mCacheExpiry = $seconds;
- }
-
- // hack: set old-style marker for uncacheable entries.
- if ( $this->mCacheExpiry !== null && $this->mCacheExpiry <= 0 ) {
- $this->mCacheTime = -1;
- }
- }
-
- /**
- * Returns the number of seconds after which this object should expire.
- * This method is used by ParserCache to determine how long the ParserOutput can be cached.
- * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime().
- * The value returned by getCacheExpiry is smaller or equal to the smallest number
- * that was provided to a call of updateCacheExpiry(), and smaller or equal to the
- * value of $wgParserCacheExpireTime.
- */
- function getCacheExpiry() {
- global $wgParserCacheExpireTime;
-
- if ( $this->mCacheTime < 0 ) {
- return 0;
- } // old-style marker for "not cachable"
-
- $expire = $this->mCacheExpiry;
-
- if ( $expire === null ) {
- $expire = $wgParserCacheExpireTime;
- } else {
- $expire = min( $expire, $wgParserCacheExpireTime );
- }
-
- if( $this->containsOldMagic() ) { //compatibility hack
- $expire = min( $expire, 3600 ); # 1 hour
- }
-
- if ( $expire <= 0 ) {
- return 0; // not cachable
- } else {
- return $expire;
- }
- }
-
- /**
- * @return bool
- */
- function isCacheable() {
- return $this->getCacheExpiry() > 0;
- }
-
- /**
- * Return true if this cached output object predates the global or
- * per-article cache invalidation timestamps, or if it comes from
- * an incompatible older version.
- *
- * @param $touched String: the affected article's last touched timestamp
- * @return Boolean
- */
- public function expired( $touched ) {
- global $wgCacheEpoch;
- return !$this->isCacheable() || // parser says it's uncacheable
- $this->getCacheTime() < $touched ||
- $this->getCacheTime() <= $wgCacheEpoch ||
- $this->getCacheTime() < wfTimestamp( TS_MW, time() - $this->getCacheExpiry() ) || // expiry period has passed
- !isset( $this->mVersion ) ||
- version_compare( $this->mVersion, Parser::VERSION, "lt" );
- }
-}
-
class ParserOutput extends CacheTime {
var $mText, # The output text
$mLanguageLinks, # List of the full text of language links, in the order they appear
@@ -140,8 +48,9 @@ class ParserOutput extends CacheTime {
$mProperties = array(), # Name/value pairs to be cached in the DB
$mTOCHTML = '', # HTML of the TOC
$mTimestamp; # Timestamp of the revision
- private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change.
- private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
+ 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.
const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
@@ -166,6 +75,7 @@ class ParserOutput extends CacheTime {
/**
* callback used by getText to replace editsection tokens
* @private
+ * @return mixed
*/
function replaceEditSectionLinksCallback( $m ) {
global $wgOut, $wgLang;
@@ -331,7 +241,7 @@ class ParserOutput extends CacheTime {
}
/**
- * Add some text to the <head>.
+ * Add some text to the "<head>".
* If $tag is set, the section with that tag will only be included once
* in a given page.
*/
@@ -447,4 +357,45 @@ class ParserOutput extends CacheTime {
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
+ * store any secondary information extracted from the page's content.
+ *
+ * @since 1.20
+ *
+ * @param DataUpdate $update
+ */
+ public function addSecondaryDataUpdate( DataUpdate $update ) {
+ $this->mSecondaryDataUpdates[] = $update;
+ }
+
+ /**
+ * Returns any DataUpdate jobs to be executed in order to store secondary information
+ * extracted from the page's content, including a LinksUpdate object for all links stored in
+ * this ParserOutput object.
+ *
+ * @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 $recursive Boolean: queue jobs for recursive updates?
+ *
+ * @return Array. An array of instances of DataUpdate
+ */
+ public function getSecondaryDataUpdates( Title $title = null, $recursive = true ) {
+ if ( is_null( $title ) ) {
+ $title = Title::newFromText( $this->getTitleText() );
+ }
+
+ $linksUpdate = new LinksUpdate( $title, $this, $recursive );
+
+ if ( $this->mSecondaryDataUpdates === array() ) {
+ return array( $linksUpdate );
+ } else {
+ $updates = array_merge( $this->mSecondaryDataUpdates, array( $linksUpdate ) );
+ }
+
+ return $updates;
+ }
+
}
diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php
index efad33f9..f25340fa 100644
--- a/includes/parser/Parser_DiffTest.php
+++ b/includes/parser/Parser_DiffTest.php
@@ -2,7 +2,23 @@
/**
* Fake parser that output the difference of two different parsers
*
+ * 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
*/
/**
diff --git a/includes/parser/Parser_LinkHooks.php b/includes/parser/Parser_LinkHooks.php
index 90e44943..6bcc324d 100644
--- a/includes/parser/Parser_LinkHooks.php
+++ b/includes/parser/Parser_LinkHooks.php
@@ -2,7 +2,23 @@
/**
* 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
*/
/**
@@ -84,7 +100,7 @@ class Parser_LinkHooks extends Parser {
* @param $flags Integer: a combination of the following flags:
* SLH_PATTERN Use a regex link pattern rather than a namespace
*
- * @return The old callback function for this name, if any
+ * @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) )
@@ -210,7 +226,7 @@ class Parser_LinkHooks extends Parser {
# Don't allow internal links to pages containing
# PROTO: where PROTO is a valid URL protocol; these
# should be external links.
- if( preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $titleText) ) {
+ if( preg_match('/^\b(?i:' . wfUrlProtocols() . ')/', $titleText) ) {
wfProfileOut( __METHOD__ );
return $wt;
}
diff --git a/includes/parser/Preprocessor.php b/includes/parser/Preprocessor.php
index ae088fdb..bd13f9ae 100644
--- a/includes/parser/Preprocessor.php
+++ b/includes/parser/Preprocessor.php
@@ -2,7 +2,23 @@
/**
* Interfaces for preprocessors
*
+ * 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
*/
/**
@@ -62,15 +78,19 @@ interface PPFrame {
const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet
+ /** This constant exists when $indexOffset is supported in newChild() */
+ const SUPPORTS_INDEX_OFFSET = 1;
+
/**
* Create a child frame
*
* @param $args array
* @param $title Title
+ * @param $indexOffset A number subtracted from the index attributes of the arguments
*
* @return PPFrame
*/
- function newChild( $args = false, $title = false );
+ function newChild( $args = false, $title = false, $indexOffset = 0 );
/**
* Expand a document tree node
@@ -211,7 +231,7 @@ interface PPNode {
function getName();
/**
- * Split a <part> node into an associative array containing:
+ * Split a "<part>" node into an associative array containing:
* name PPNode name
* index String index
* value PPNode value
@@ -219,13 +239,13 @@ interface PPNode {
function splitArg();
/**
- * Split an <ext> node into an associative array containing name, attr, inner and close
+ * Split an "<ext>" node into an associative array containing name, attr, inner and close
* All values in the resulting array are PPNodes. Inner and close are optional.
*/
function splitExt();
/**
- * Split an <h> node
+ * Split an "<h>" node
*/
function splitHeading();
}
diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php
index 066589f6..34de0ba5 100644
--- a/includes/parser/Preprocessor_DOM.php
+++ b/includes/parser/Preprocessor_DOM.php
@@ -2,6 +2,21 @@
/**
* Preprocessor using PHP's dom extension
*
+ * 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
*/
@@ -41,7 +56,7 @@ class Preprocessor_DOM implements Preprocessor {
}
/**
- * @param $args
+ * @param $args array
* @return PPCustomFrame_DOM
*/
function newCustomFrame( $args ) {
@@ -97,7 +112,7 @@ class Preprocessor_DOM implements Preprocessor {
*
* @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
+ * Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<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.
@@ -147,6 +162,15 @@ class Preprocessor_DOM implements Preprocessor {
}
}
+
+ // Fail if the number of elements exceeds acceptable limits
+ // 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' );
+ }
+
wfProfileIn( __METHOD__.'-loadXML' );
$dom = new DOMDocument;
wfSuppressWarnings();
@@ -220,6 +244,7 @@ class Preprocessor_DOM implements Preprocessor {
$searchBase = "[{<\n"; #}
$revText = strrev( $text ); // For fast reverse searches
+ $lengthText = strlen( $text );
$i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
$accum =& $stack->getAccum(); # Current accumulator
@@ -275,7 +300,7 @@ class Preprocessor_DOM implements Preprocessor {
$accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
$i += $literalLength;
}
- if ( $i >= strlen( $text ) ) {
+ if ( $i >= $lengthText ) {
if ( $currentClosing == "\n" ) {
// Do a past-the-end run to finish off the heading
$curChar = '';
@@ -339,10 +364,10 @@ class Preprocessor_DOM implements Preprocessor {
// Unclosed comment in input, runs to end
$inner = substr( $text, $i );
$accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
- $i = strlen( $text );
+ $i = $lengthText;
} else {
// Search backwards for leading whitespace
- $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+ $wsStart = $i ? ( $i - strspn( $revText, ' ', $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 );
@@ -423,7 +448,7 @@ class Preprocessor_DOM implements Preprocessor {
} else {
// No end tag -- let it run out to the end of the text.
$inner = substr( $text, $tagEndPos + 1 );
- $i = strlen( $text );
+ $i = $lengthText;
$close = '';
}
}
@@ -479,20 +504,20 @@ class Preprocessor_DOM implements Preprocessor {
} elseif ( $found == 'line-end' ) {
$piece = $stack->top;
// A heading must be open, otherwise \n wouldn't have been in the search list
- assert( $piece->open == "\n" );
+ assert( '$piece->open == "\n"' );
$part = $piece->getCurrentPart();
// Search back through the input to see if it has a proper close
// Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
- $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+ $wsLength = 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 = $part->visualEnd;
- $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+ $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
}
$count = $piece->count;
- $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+ $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
if ( $equalsLength > 0 ) {
if ( $searchStart - $equalsLength == $piece->startPos ) {
// This is just a single string of equals signs on its own line
@@ -911,7 +936,7 @@ class PPFrame_DOM implements PPFrame {
*
* @return PPTemplateFrame_DOM
*/
- function newChild( $args = false, $title = false ) {
+ function newChild( $args = false, $title = false, $indexOffset = 0 ) {
$namedArgs = array();
$numberedArgs = array();
if ( $title === false ) {
@@ -923,6 +948,9 @@ class PPFrame_DOM implements PPFrame {
$args = $args->node;
}
foreach ( $args as $arg ) {
+ if ( $arg instanceof PPNode ) {
+ $arg = $arg->node;
+ }
if ( !$xpath ) {
$xpath = new DOMXPath( $arg->ownerDocument );
}
@@ -932,6 +960,7 @@ class PPFrame_DOM implements PPFrame {
if ( $nameNodes->item( 0 )->hasAttributes() ) {
// Numbered parameter
$index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
+ $index = $index - $indexOffset;
$numberedArgs[$index] = $value->item( 0 );
unset( $namedArgs[$index] );
} else {
@@ -958,14 +987,25 @@ class PPFrame_DOM implements PPFrame {
}
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>';
}
wfProfileIn( __METHOD__ );
++$expansionDepth;
+ if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
+ $this->parser->mHighestExpansionDepth = $expansionDepth;
+ }
if ( $root instanceof PPNode_DOM ) {
$root = $root->node;
@@ -1250,6 +1290,7 @@ class PPFrame_DOM implements PPFrame {
/**
* Virtual implode with brackets
+ * @return array
*/
function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
$args = array_slice( func_get_args(), 3 );
@@ -1522,6 +1563,10 @@ class PPCustomFrame_DOM extends PPFrame_DOM {
}
return $this->args[$index];
}
+
+ function getArguments() {
+ return $this->args;
+ }
}
/**
@@ -1623,10 +1668,10 @@ class PPNode_DOM implements PPNode {
}
/**
- * Split a <part> node into an associative array containing:
- * name PPNode name
- * index String index
- * value PPNode value
+ * Split a "<part>" node into an associative array containing:
+ * - name PPNode name
+ * - index String index
+ * - value PPNode value
*
* @return array
*/
@@ -1646,7 +1691,7 @@ class PPNode_DOM implements PPNode {
}
/**
- * Split an <ext> node into an associative array containing name, attr, inner and close
+ * 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.
*
* @return array
@@ -1673,7 +1718,8 @@ class PPNode_DOM implements PPNode {
}
/**
- * Split a <h> node
+ * Split a "<h>" node
+ * @return array
*/
function splitHeading() {
if ( $this->getName() !== 'h' ) {
diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php
index 2934181a..4f04c865 100644
--- a/includes/parser/Preprocessor_Hash.php
+++ b/includes/parser/Preprocessor_Hash.php
@@ -2,6 +2,21 @@
/**
* Preprocessor using PHP arrays
*
+ * 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
*/
@@ -9,7 +24,7 @@
/**
* Differences from DOM schema:
* * attribute nodes are children
- * * <h> nodes that aren't at the top are replaced with <possible-h>
+ * * "<h>" nodes that aren't at the top are replaced with <possible-h>
* @ingroup Parser
*/
class Preprocessor_Hash implements Preprocessor {
@@ -32,7 +47,7 @@ class Preprocessor_Hash implements Preprocessor {
}
/**
- * @param $args
+ * @param $args array
* @return PPCustomFrame_Hash
*/
function newCustomFrame( $args ) {
@@ -76,7 +91,7 @@ class Preprocessor_Hash implements Preprocessor {
*
* @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
+ * Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<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.
@@ -162,6 +177,7 @@ class Preprocessor_Hash implements Preprocessor {
$searchBase = "[{<\n";
$revText = strrev( $text ); // For fast reverse searches
+ $lengthText = strlen( $text );
$i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
$accum =& $stack->getAccum(); # Current accumulator
@@ -216,7 +232,7 @@ class Preprocessor_Hash implements Preprocessor {
$accum->addLiteral( substr( $text, $i, $literalLength ) );
$i += $literalLength;
}
- if ( $i >= strlen( $text ) ) {
+ if ( $i >= $lengthText ) {
if ( $currentClosing == "\n" ) {
// Do a past-the-end run to finish off the heading
$curChar = '';
@@ -280,10 +296,10 @@ class Preprocessor_Hash implements Preprocessor {
// Unclosed comment in input, runs to end
$inner = substr( $text, $i );
$accum->addNodeWithText( 'comment', $inner );
- $i = strlen( $text );
+ $i = $lengthText;
} else {
// Search backwards for leading whitespace
- $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+ $wsStart = $i ? ( $i - strspn( $revText, ' ', $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 );
@@ -368,7 +384,7 @@ class Preprocessor_Hash implements Preprocessor {
} else {
// No end tag -- let it run out to the end of the text.
$inner = substr( $text, $tagEndPos + 1 );
- $i = strlen( $text );
+ $i = $lengthText;
$close = null;
}
}
@@ -428,20 +444,20 @@ class Preprocessor_Hash implements Preprocessor {
} elseif ( $found == 'line-end' ) {
$piece = $stack->top;
// A heading must be open, otherwise \n wouldn't have been in the search list
- assert( $piece->open == "\n" );
+ assert( '$piece->open == "\n"' );
$part = $piece->getCurrentPart();
// Search back through the input to see if it has a proper close
// Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
- $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+ $wsLength = 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 = $part->visualEnd;
- $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+ $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
}
$count = $piece->count;
- $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+ $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
if ( $equalsLength > 0 ) {
if ( $searchStart - $equalsLength == $piece->startPos ) {
// This is just a single string of equals signs on its own line
@@ -869,11 +885,11 @@ class PPFrame_Hash implements PPFrame {
* $args is optionally a multi-root PPNode or array containing the template arguments
*
* @param $args PPNode_Hash_Array|array
- * @param $title Title|false
+ * @param $title Title|bool
*
* @return PPTemplateFrame_Hash
*/
- function newChild( $args = false, $title = false ) {
+ function newChild( $args = false, $title = false, $indexOffset = 0 ) {
$namedArgs = array();
$numberedArgs = array();
if ( $title === false ) {
@@ -889,8 +905,9 @@ class PPFrame_Hash implements PPFrame {
$bits = $arg->splitArg();
if ( $bits['index'] !== '' ) {
// Numbered parameter
- $numberedArgs[$bits['index']] = $bits['value'];
- unset( $namedArgs[$bits['index']] );
+ $index = $bits['index'] - $indexOffset;
+ $numberedArgs[$index] = $bits['value'];
+ unset( $namedArgs[$index] );
} else {
// Named parameter
$name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
@@ -915,12 +932,23 @@ class PPFrame_Hash implements PPFrame {
}
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 );
@@ -1470,6 +1498,10 @@ class PPCustomFrame_Hash extends PPFrame_Hash {
}
return $this->args[$index];
}
+
+ function getArguments() {
+ return $this->args;
+ }
}
/**
@@ -1543,7 +1575,7 @@ class PPNode_Hash_Tree implements PPNode {
$children = array();
for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
if ( isset( $child->name ) && $child->name === $name ) {
- $children[] = $name;
+ $children[] = $child;
}
}
return $children;
@@ -1572,10 +1604,10 @@ class PPNode_Hash_Tree implements PPNode {
}
/**
- * Split a <part> node into an associative array containing:
- * name PPNode name
- * index String index
- * value PPNode value
+ * Split a "<part>" node into an associative array containing:
+ * - name PPNode name
+ * - index String index
+ * - value PPNode value
*
* @return array
*/
@@ -1607,7 +1639,7 @@ class PPNode_Hash_Tree implements PPNode {
}
/**
- * Split an <ext> node into an associative array containing name, attr, inner and close
+ * 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.
*
* @return array
@@ -1635,7 +1667,7 @@ class PPNode_Hash_Tree implements PPNode {
}
/**
- * Split an <h> node
+ * Split an "<h>" node
*
* @return array
*/
@@ -1661,7 +1693,7 @@ class PPNode_Hash_Tree implements PPNode {
}
/**
- * Split a <template> or <tplarg> node
+ * Split a "<template>" or "<tplarg>" node
*
* @return array
*/
diff --git a/includes/parser/Preprocessor_HipHop.hphp b/includes/parser/Preprocessor_HipHop.hphp
index f5af0154..8b71a1b5 100644
--- a/includes/parser/Preprocessor_HipHop.hphp
+++ b/includes/parser/Preprocessor_HipHop.hphp
@@ -3,6 +3,21 @@
* 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
*/
@@ -18,6 +33,9 @@ class Preprocessor_HipHop implements Preprocessor {
const CACHE_VERSION = 1;
+ /**
+ * @param $parser Parser
+ */
function __construct( $parser ) {
$this->parser = $parser;
}
@@ -30,10 +48,10 @@ class Preprocessor_HipHop implements Preprocessor {
}
/**
- * @param $args
+ * @param $args array
* @return PPCustomFrame_HipHop
*/
- function newCustomFrame( array $args ) {
+ function newCustomFrame( $args ) {
return new PPCustomFrame_HipHop( $this, $args );
}
@@ -88,15 +106,18 @@ class Preprocessor_HipHop implements Preprocessor {
* 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( string $text, int $flags = 0 ) {
+ function preprocessToObj( $text, $flags = 0 ) {
wfProfileIn( __METHOD__ );
// Check cache.
global $wgMemc, $wgPreprocessorCacheThreshold;
- $cacheable = ($wgPreprocessorCacheThreshold !== false && strlen( $text ) > $wgPreprocessorCacheThreshold);
+ $lengthText = strlen( $text );
+
+ $cacheable = ($wgPreprocessorCacheThreshold !== false && $lengthText > $wgPreprocessorCacheThreshold);
if ( $cacheable ) {
wfProfileIn( __METHOD__.'-cacheable' );
@@ -220,7 +241,7 @@ class Preprocessor_HipHop implements Preprocessor {
$accum->addLiteral( strval( substr( $text, $i, $literalLength ) ) );
$i += $literalLength;
}
- if ( $i >= strlen( $text ) ) {
+ if ( $i >= $lengthText ) {
if ( $currentClosing === "\n" ) {
// Do a past-the-end run to finish off the heading
$curChar = '';
@@ -286,12 +307,12 @@ class Preprocessor_HipHop implements Preprocessor {
// Unclosed comment in input, runs to end
$inner = strval( substr( $text, $i ) );
$accum->addNodeWithText( 'comment', $inner );
- $i = strlen( $text );
+ $i = $lengthText;
} else {
$endPos = intval( $variantEndPos );
// Search backwards for leading whitespace
if ( $i ) {
- $wsStart = $i - intval( strspn( $revText, ' ', strlen( $text ) - $i ) );
+ $wsStart = $i - intval( strspn( $revText, ' ', $lengthText - $i ) );
} else {
$wsStart = 0;
}
@@ -384,7 +405,7 @@ class Preprocessor_HipHop implements Preprocessor {
} else {
// No end tag -- let it run out to the end of the text.
$inner = strval( substr( $text, $tagEndPos + 1 ) );
- $i = strlen( $text );
+ $i = $lengthText;
$haveClose = false;
}
}
@@ -444,20 +465,21 @@ class Preprocessor_HipHop implements Preprocessor {
} 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" );
+ 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", strlen( $text ) - $i ) );
+ $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", strlen( $text ) - $searchStart ) );
+ $searchStart -= intval( strspn( $revText, " \t", $lengthText - $searchStart ) );
}
$count = intval( $piece->count );
- $equalsLength = intval( strspn( $revText, '=', strlen( $text ) - $searchStart ) );
+ $equalsLength = intval( strspn( $revText, '=', $lengthText - $searchStart ) );
$isTreeNode = false;
$resultAccum = $accum;
if ( $equalsLength > 0 ) {
@@ -814,16 +836,23 @@ class PPDStack_HipHop {
* @ingroup Parser
*/
class PPDStackElement_HipHop {
- 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.
+ /**
+ * @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 );
@@ -832,14 +861,23 @@ class PPDStackElement_HipHop {
}
}
+ /**
+ * @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] );
}
@@ -860,6 +898,7 @@ class PPDStackElement_HipHop {
/**
* Get the accumulator that would result if the close is not found.
*
+ * @param $openingCount bool
* @return PPDAccum_HipHop
*/
function breakSyntax( $openingCount = false ) {
@@ -1025,12 +1064,14 @@ class PPFrame_HipHop implements PPFrame {
* Create a new child frame
* $args is optionally a multi-root PPNode or array containing the template arguments
*
- * @param $args PPNode_HipHop_Array|array
- * @param $title Title|false
+ * @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 ) {
+ function newChild( $args = false, $title = false, $indexOffset = 0 ) {
$namedArgs = array();
$numberedArgs = array();
if ( $title === false ) {
@@ -1072,12 +1113,23 @@ class PPFrame_HipHop implements PPFrame {
}
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 );
@@ -1266,6 +1318,7 @@ class PPFrame_HipHop implements PPFrame {
/**
* 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 /*, ... */ ) {
@@ -1296,6 +1349,7 @@ class PPFrame_HipHop implements PPFrame {
* 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 /*, ... */ ) {
@@ -1325,6 +1379,9 @@ class PPFrame_HipHop implements PPFrame {
/**
* Virtual implode with brackets
*
+ * @param $start
+ * @param $sep
+ * @param $end
* @return PPNode_HipHop_Array
*/
function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
@@ -1445,11 +1502,11 @@ class PPTemplateFrame_HipHop extends PPFrame_HipHop {
var $numberedExpansionCache, $namedExpansionCache;
/**
- * @param $preprocessor
- * @param $parent
+ * @param $preprocessor Preprocessor_HipHop
+ * @param $parent bool
* @param $numberedArgs array
* @param $namedArgs array
- * @param $title Title
+ * @param $title Title|bool
*/
function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
parent::__construct( $preprocessor );
@@ -1696,11 +1753,15 @@ class PPNode_HipHop_Tree implements PPNode {
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[] = $name;
+ $children[] = $child;
}
}
return $children;
@@ -1734,6 +1795,7 @@ class PPNode_HipHop_Tree implements PPNode {
* index String index
* value PPNode value
*
+ * @throws MWException
* @return array
*/
function splitArg() {
@@ -1767,6 +1829,7 @@ class PPNode_HipHop_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() {
@@ -1794,6 +1857,7 @@ class PPNode_HipHop_Tree implements PPNode {
/**
* Split an <h> node
*
+ * @throws MWException
* @return array
*/
function splitHeading() {
diff --git a/includes/parser/StripState.php b/includes/parser/StripState.php
index 7ad80fa1..ad95d5f7 100644
--- a/includes/parser/StripState.php
+++ b/includes/parser/StripState.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Holder for stripped items when parsing wiki markup.
+ *
+ * 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
+ */
/**
* @todo document, briefly.
@@ -10,6 +31,10 @@ class StripState {
protected $regex;
protected $tempType, $tempMergePrefix;
+ protected $circularRefGuard;
+ protected $recursionLevel = 0;
+
+ const UNSTRIP_RECURSION_LIMIT = 20;
/**
* @param $prefix string
@@ -21,6 +46,7 @@ class StripState {
'general' => array()
);
$this->regex = "/{$this->prefix}([^\x7f]+)" . Parser::MARKER_SUFFIX . '/';
+ $this->circularRefGuard = array();
}
/**
@@ -92,12 +118,10 @@ class StripState {
}
wfProfileIn( __METHOD__ );
+ $oldType = $this->tempType;
$this->tempType = $type;
- do {
- $oldText = $text;
- $text = preg_replace_callback( $this->regex, array( $this, 'unstripCallback' ), $text );
- } while ( $text !== $oldText );
- $this->tempType = null;
+ $text = preg_replace_callback( $this->regex, array( $this, 'unstripCallback' ), $text );
+ $this->tempType = $oldType;
wfProfileOut( __METHOD__ );
return $text;
}
@@ -107,8 +131,25 @@ class StripState {
* @return array
*/
protected function unstripCallback( $m ) {
- if ( isset( $this->data[$this->tempType][$m[1]] ) ) {
- return $this->data[$this->tempType][$m[1]];
+ $marker = $m[1];
+ if ( isset( $this->data[$this->tempType][$marker] ) ) {
+ if ( isset( $this->circularRefGuard[$marker] ) ) {
+ return '<span class="error">'
+ . wfMessage( 'parser-unstrip-loop-warning' )->inContentLanguage()->text()
+ . '</span>';
+ }
+ if ( $this->recursionLevel >= self::UNSTRIP_RECURSION_LIMIT ) {
+ return '<span class="error">' .
+ wfMessage( 'parser-unstrip-recursion-limit' )
+ ->numParams( self::UNSTRIP_RECURSION_LIMIT )->inContentLanguage()->text() .
+ '</span>';
+ }
+ $this->circularRefGuard[$marker] = true;
+ $this->recursionLevel++;
+ $ret = $this->unstripType( $this->tempType, $this->data[$this->tempType][$marker] );
+ $this->recursionLevel--;
+ unset( $this->circularRefGuard[$marker] );
+ return $ret;
} else {
return $m[0];
}
diff --git a/includes/parser/Tidy.php b/includes/parser/Tidy.php
index 2b98f01d..ed2d436d 100644
--- a/includes/parser/Tidy.php
+++ b/includes/parser/Tidy.php
@@ -2,7 +2,23 @@
/**
* HTML validation and correction
*
+ * 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
*/
/**
@@ -14,6 +30,8 @@
*
* This re-uses some of the parser's UNIQ tricks, though some of it is private so it's
* duplicated. Perhaps we should create an abstract marker hiding class.
+ *
+ * @ingroup Parser
*/
class MWTidyWrapper {
@@ -143,7 +161,7 @@ class MWTidy {
*
* @param $text String: HTML to check
* @param $stderr Boolean: Whether to read result from STDERR rather than STDOUT
- * @param &$retval Exit code (-1 on internal error)
+ * @param &$retval int Exit code (-1 on internal error)
* @return mixed String or null
*/
private static function execExternalTidy( $text, $stderr = false, &$retval = null ) {
@@ -207,7 +225,7 @@ class MWTidy {
*
* @param $text String: HTML to check
* @param $stderr Boolean: Whether to read result from error status instead of output
- * @param &$retval Exit code (-1 on internal error)
+ * @param &$retval int Exit code (-1 on internal error)
* @return mixed String or null
*/
private static function execInternalTidy( $text, $stderr = false, &$retval = null ) {
diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php
index 0fe18c25..62be39e4 100644
--- a/includes/profiler/Profiler.php
+++ b/includes/profiler/Profiler.php
@@ -1,6 +1,21 @@
<?php
/**
- * @defgroup Profiler Profiler
+ * Base class and functions for profiling.
+ *
+ * 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 Profiler
@@ -8,6 +23,10 @@
*/
/**
+ * @defgroup Profiler Profiler
+ */
+
+/**
* Begin profiling of a function
* @param $functionname String: name of the function we will profile
*/
@@ -48,14 +67,7 @@ class Profiler {
$this->mProfileID = $params['profileID'];
}
- // Push an entry for the pre-profile setup time onto the stack
- $initial = $this->getInitialTime();
- if ( $initial !== null ) {
- $this->mWorkStack[] = array( '-total', 0, $initial, 0 );
- $this->mStack[] = array( '-setup', 1, $initial, 0, $this->getTime(), 0 );
- } else {
- $this->profileIn( '-total' );
- }
+ $this->addInitialStack();
}
/**
@@ -102,6 +114,16 @@ class Profiler {
return false;
}
+ /**
+ * Return whether this profiler stores data
+ *
+ * @see Profiler::logData()
+ * @return Boolean
+ */
+ public function isPersistent() {
+ return true;
+ }
+
public function setProfileID( $id ) {
$this->mProfileID = $id;
}
@@ -115,6 +137,20 @@ class Profiler {
}
/**
+ * Add the inital item in the stack.
+ */
+ protected function addInitialStack() {
+ // Push an entry for the pre-profile setup time onto the stack
+ $initial = $this->getInitialTime();
+ if ( $initial !== null ) {
+ $this->mWorkStack[] = array( '-total', 0, $initial, 0 );
+ $this->mStack[] = array( '-setup', 1, $initial, 0, $this->getTime(), 0 );
+ } else {
+ $this->profileIn( '-total' );
+ }
+ }
+
+ /**
* Called by wfProfieIn()
*
* @param $functionname String
@@ -205,6 +241,7 @@ class Profiler {
/**
* Returns a tree of function call instead of a list of functions
+ * @return string
*/
function getCallTree() {
return implode( '', array_map( array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack ) ) );
@@ -213,7 +250,8 @@ class Profiler {
/**
* Recursive function the format the current profiling array into a tree
*
- * @param $stack profiling array
+ * @param $stack array profiling array
+ * @return array
*/
function remapCallTree( $stack ) {
if( count( $stack ) < 2 ){
@@ -252,6 +290,7 @@ class Profiler {
/**
* Callback to get a formatted line for the call tree
+ * @return string
*/
function getCallTreeLine( $entry ) {
list( $fname, $level, $start, /* $x */, $end) = $entry;
@@ -262,28 +301,69 @@ class Profiler {
return sprintf( "%10s %s %s\n", trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname );
}
- function getTime() {
- if ( $this->mTimeMetric === 'user' ) {
- return $this->getUserTime();
+ /**
+ * 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:
+ * - user: User CPU time (without system calls)
+ * - cpu: Total CPU time (user and system calls)
+ * - wall (or any other string): elapsed time
+ * - false (default): will fall back to default metric
+ * @return float|null
+ */
+ function getTime( $metric = false ) {
+ if ( $metric === false ) {
+ $metric = $this->mTimeMetric;
+ }
+
+ if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) {
+ if ( !function_exists( 'getrusage' ) ) {
+ return 0;
+ }
+ $ru = getrusage();
+ $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
+ if ( $metric === 'cpu' ) {
+ # This is the time of system calls, added to the user time
+ # it gives the total CPU time
+ $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
+ }
+ return $time;
} else {
return microtime( true );
}
}
- function getUserTime() {
- $ru = getrusage();
- return $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
- }
-
- private function getInitialTime() {
+ /**
+ * 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:
+ * - user: User CPU time (without system calls)
+ * - cpu: Total CPU time (user and system calls)
+ * - wall (or any other string): elapsed time
+ * - false (default): will fall back to default metric
+ * @return float|null
+ */
+ protected function getInitialTime( $metric = false ) {
global $wgRequestTime, $wgRUstart;
- if ( $this->mTimeMetric === 'user' ) {
- if ( count( $wgRUstart ) ) {
- return $wgRUstart['ru_utime.tv_sec'] + $wgRUstart['ru_utime.tv_usec'] / 1e6;
- } else {
+ if ( $metric === false ) {
+ $metric = $this->mTimeMetric;
+ }
+
+ if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) {
+ if ( !count( $wgRUstart ) ) {
return null;
}
+
+ $time = $wgRUstart['ru_utime.tv_sec'] + $wgRUstart['ru_utime.tv_usec'] / 1e6;
+ if ( $metric === 'cpu' ) {
+ # This is the time of system calls, added to the user time
+ # it gives the total CPU time
+ $time += $wgRUstart['ru_stime.tv_sec'] + $wgRUstart['ru_stime.tv_usec'] / 1e6;
+ }
+ return $time;
} else {
if ( empty( $wgRequestTime ) ) {
return null;
@@ -409,7 +489,7 @@ class Profiler {
}
wfProfileOut( '-overhead-total' );
}
-
+
/**
* Counts the number of profiled function calls sitting under
* the given point in the call graph. Not the most efficient algo.
@@ -479,7 +559,7 @@ class Profiler {
$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 ),
+ 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ),
__METHOD__, array ('IGNORE'));
}
// When we upgrade to mysql 4.1, the insert+update
@@ -494,6 +574,7 @@ class Profiler {
/**
* Get the function name of the current profiling section
+ * @return
*/
function getCurrentSection() {
$elt = end( $this->mWorkStack );
diff --git a/includes/profiler/ProfilerSimple.php b/includes/profiler/ProfilerSimple.php
index 055a0ea0..d1d1c5d9 100644
--- a/includes/profiler/ProfilerSimple.php
+++ b/includes/profiler/ProfilerSimple.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Base class for simple profiling.
+ *
+ * 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 Profiler
*/
@@ -15,32 +32,24 @@ class ProfilerSimple extends Profiler {
var $zeroEntry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0);
var $errorEntry;
- function __construct( $params ) {
- global $wgRequestTime, $wgRUstart;
- parent::__construct( $params );
+ public function isPersistent() {
+ /* Implement in output subclasses */
+ return false;
+ }
+ protected function addInitialStack() {
$this->errorEntry = $this->zeroEntry;
$this->errorEntry['count'] = 1;
- if (!empty($wgRequestTime) && !empty($wgRUstart)) {
- # Remove the -total entry from parent::__construct
- $this->mWorkStack = array();
-
- $this->mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart));
-
- $elapsedcpu = $this->getCpuTime() - $this->getCpuTime($wgRUstart);
- $elapsedreal = microtime(true) - $wgRequestTime;
+ $initialTime = $this->getInitialTime();
+ $initialCpu = $this->getInitialTime( 'cpu' );
+ if ( $initialTime !== null && $initialCpu !== null ) {
+ $this->mWorkStack[] = array( '-total', 0, $initialTime, $initialCpu );
+ $this->mWorkStack[] = array( '-setup', 1, $initialTime, $initialCpu );
- $entry =& $this->mCollated["-setup"];
- if (!is_array($entry)) {
- $entry = $this->zeroEntry;
- $this->mCollated["-setup"] =& $entry;
- }
- $entry['cpu'] += $elapsedcpu;
- $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu;
- $entry['real'] += $elapsedreal;
- $entry['real_sq'] += $elapsedreal*$elapsedreal;
- $entry['count']++;
+ $this->profileOut( '-setup' );
+ } else {
+ $this->profileIn( '-total' );
}
}
@@ -53,7 +62,7 @@ class ProfilerSimple extends Profiler {
if ($wgDebugFunctionEntry) {
$this->debug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n");
}
- $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime());
+ $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), $this->getTime( 'cpu' ) );
}
function profileOut($functionname) {
@@ -80,8 +89,8 @@ class ProfilerSimple extends Profiler {
$this->mCollated[$message] = $this->errorEntry;
}
$entry =& $this->mCollated[$functionname];
- $elapsedcpu = $this->getCpuTime() - $octime;
- $elapsedreal = microtime(true) - $ortime;
+ $elapsedcpu = $this->getTime( 'cpu' ) - $octime;
+ $elapsedreal = $this->getTime() - $ortime;
if (!is_array($entry)) {
$entry = $this->zeroEntry;
$this->mCollated[$functionname] =& $entry;
@@ -104,15 +113,20 @@ class ProfilerSimple extends Profiler {
/* Implement in subclasses */
}
- function getCpuTime($ru=null) {
- if ( function_exists( 'getrusage' ) ) {
- if ( $ru == null ) {
- $ru = getrusage();
- }
- return ($ru['ru_utime.tv_sec'] + $ru['ru_stime.tv_sec'] + ($ru['ru_utime.tv_usec'] +
- $ru['ru_stime.tv_usec']) * 1e-6);
+ /**
+ * Get the actual CPU time or the initial one if $ru is set.
+ *
+ * @deprecated in 1.20
+ * @return float|null
+ */
+ function getCpuTime( $ru = null ) {
+ wfDeprecated( __METHOD__, '1.20' );
+
+ if ( $ru === null ) {
+ return $this->getTime( 'cpu' );
} else {
- return 0;
+ # It theory we should use $ru here, but it always $wgRUstart that is passed here
+ return $this->getInitialTime( 'cpu' );
}
}
}
diff --git a/includes/profiler/ProfilerSimpleText.php b/includes/profiler/ProfilerSimpleText.php
index 3621a41f..3e7d6fa4 100644
--- a/includes/profiler/ProfilerSimpleText.php
+++ b/includes/profiler/ProfilerSimpleText.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Profiler showing output in page source.
+ *
+ * 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 Profiler
*/
diff --git a/includes/profiler/ProfilerSimpleTrace.php b/includes/profiler/ProfilerSimpleTrace.php
index 784609f5..822e9fe4 100644
--- a/includes/profiler/ProfilerSimpleTrace.php
+++ b/includes/profiler/ProfilerSimpleTrace.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Profiler showing execution trace.
+ *
+ * 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 Profiler
*/
@@ -10,20 +27,11 @@
* @ingroup Profiler
*/
class ProfilerSimpleTrace extends ProfilerSimple {
- var $trace = "";
+ var $trace = "Beginning trace: \n";
var $memory = 0;
- function __construct( $params ) {
- global $wgRequestTime, $wgRUstart;
- parent::__construct( $params );
- if ( !empty( $wgRequestTime ) && !empty( $wgRUstart ) ) {
- $this->mWorkStack[] = array( '-total', 0, $wgRequestTime, $this->getCpuTime( $wgRUstart ) );
- }
- $this->trace .= "Beginning trace: \n";
- }
-
- function profileIn($functionname) {
- $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime());
+ function profileIn( $functionname ) {
+ parent::profileIn( $functionname );
$this->trace .= " " . sprintf("%6.1f",$this->memoryDiff()) .
str_repeat( " ", count($this->mWorkStack)) . " > " . $functionname . "\n";
}
@@ -48,12 +56,12 @@ class ProfilerSimpleTrace extends ProfilerSimple {
elseif ( $ofname != $functionname ) {
$this->trace .= "Profiling error: in({$ofname}), out($functionname)";
}
- $elapsedreal = microtime( true ) - $ortime;
+ $elapsedreal = $this->getTime() - $ortime;
$this->trace .= sprintf( "%03.6f %6.1f", $elapsedreal, $this->memoryDiff() ) .
str_repeat(" ", count( $this->mWorkStack ) + 1 ) . " < " . $functionname . "\n";
}
}
-
+
function memoryDiff() {
$diff = memory_get_usage() - $this->memory;
$this->memory = memory_get_usage();
diff --git a/includes/profiler/ProfilerSimpleUDP.php b/includes/profiler/ProfilerSimpleUDP.php
index ae607aa6..a95ccb0d 100644
--- a/includes/profiler/ProfilerSimpleUDP.php
+++ b/includes/profiler/ProfilerSimpleUDP.php
@@ -1,5 +1,22 @@
<?php
/**
+ * Profiler sending messages over UDP.
+ *
+ * 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 Profiler
*/
@@ -10,6 +27,10 @@
* @ingroup Profiler
*/
class ProfilerSimpleUDP extends ProfilerSimple {
+ public function isPersistent() {
+ return true;
+ }
+
public function logData() {
global $wgUDPProfilerHost, $wgUDPProfilerPort;
diff --git a/includes/profiler/ProfilerStub.php b/includes/profiler/ProfilerStub.php
index 1a0933c4..c0eb0fb4 100644
--- a/includes/profiler/ProfilerStub.php
+++ b/includes/profiler/ProfilerStub.php
@@ -1,13 +1,38 @@
<?php
/**
- * Stub profiling functions
+ * Stub profiling functions.
+ *
+ * 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 Profiler
*/
+
+/**
+ * Stub profiler that does nothing
+ *
+ * @ingroup Profiler
+ */
class ProfilerStub extends Profiler {
public function isStub() {
return true;
}
+ public function isPersistent() {
+ return false;
+ }
public function profileIn( $fn ) {}
public function profileOut( $fn ) {}
public function getOutput() {}
diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php
index 9175b10d..3b48a266 100644
--- a/includes/resourceloader/ResourceLoader.php
+++ b/includes/resourceloader/ResourceLoader.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Base class for resource loading system.
+ *
* 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
@@ -161,11 +163,11 @@ class ResourceLoader {
$wgResourceLoaderMinifierStatementsOnOwnLine,
$wgResourceLoaderMinifierMaxLineLength
);
- $result .= "\n\n/* cache key: $key */\n";
+ $result .= "\n/* cache key: $key */";
break;
case 'minify-css':
$result = CSSMin::minify( $data );
- $result .= "\n\n/* cache key: $key */\n";
+ $result .= "\n/* cache key: $key */";
break;
}
@@ -215,7 +217,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 Module info array. For backwards compatibility with 1.17alpha,
+ * @param $info array 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
@@ -239,9 +241,9 @@ class ResourceLoader {
);
}
- // Check $name for illegal characters
- if ( preg_match( '/[|,!]/', $name ) ) {
- throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|), commas (,) or exclamation marks (!)" );
+ // Check $name for validity
+ if ( !self::isValidModuleName( $name ) ) {
+ throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
}
// Attach module
@@ -354,6 +356,7 @@ class ResourceLoader {
* @return Array
*/
public function getTestModuleNames( $framework = 'all' ) {
+ /// @TODO: api siteinfo prop testmodulenames modulenames
if ( $framework == 'all' ) {
return $this->testModuleNames;
} elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
@@ -499,10 +502,6 @@ class ResourceLoader {
$response = $this->makeComment( $warnings ) . $response;
}
- // Remove the output buffer and output the response
- ob_end_clean();
- echo $response;
-
// Save response to file cache unless there are errors
if ( isset( $fileCache ) && !$errors && !$missing ) {
// Cache single modules...and other requests if there are enough hits
@@ -515,6 +514,10 @@ class ResourceLoader {
}
}
+ // Remove the output buffer and output the response
+ ob_end_clean();
+ echo $response;
+
wfProfileOut( __METHOD__ );
}
@@ -598,7 +601,7 @@ class ResourceLoader {
/**
* Send out code for a response from file cache if possible
*
- * @param $fileCache ObjectFileCache: Cache object for this request URL
+ * @param $fileCache ResourceFileCache: Cache object for this request URL
* @param $context ResourceLoaderContext: Context in which to generate a response
* @return bool If this found a cache file and handled the response
*/
@@ -682,6 +685,7 @@ class ResourceLoader {
}
// Generate output
+ $isRaw = false;
foreach ( $modules as $name => $module ) {
/**
* @var $module ResourceLoaderModule
@@ -699,7 +703,7 @@ class ResourceLoader {
$scripts = $module->getScriptURLsForDebug( $context );
} else {
$scripts = $module->getScript( $context );
- if ( is_string( $scripts ) ) {
+ if ( is_string( $scripts ) && strlen( $scripts ) && substr( $scripts, -1 ) !== ';' ) {
// bug 27054: Append semicolon to prevent weird bugs
// caused by files not terminating their statements right
$scripts .= ";\n";
@@ -709,12 +713,39 @@ class ResourceLoader {
// Styles
$styles = array();
if ( $context->shouldIncludeStyles() ) {
- // If we are in debug mode, we'll want to return an array of URLs
- // See comment near shouldIncludeScripts() for more details
- if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
- $styles = $module->getStyleURLsForDebug( $context );
- } else {
- $styles = $module->getStyles( $context );
+ // Don't create empty stylesheets like array( '' => '' ) for modules
+ // that don't *have* any stylesheets (bug 38024).
+ $stylePairs = $module->getStyles( $context );
+ if ( count ( $stylePairs ) ) {
+ // If we are in debug mode without &only= set, we'll want to return an array of URLs
+ // See comment near shouldIncludeScripts() for more details
+ if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
+ $styles = array(
+ 'url' => $module->getStyleURLsForDebug( $context )
+ );
+ } else {
+ // Minify CSS before embedding in mw.loader.implement call
+ // (unless in debug mode)
+ if ( !$context->getDebug() ) {
+ foreach ( $stylePairs as $media => $style ) {
+ // Can be either a string or an array of strings.
+ if ( is_array( $style ) ) {
+ $stylePairs[$media] = array();
+ foreach ( $style as $cssText ) {
+ if ( is_string( $cssText ) ) {
+ $stylePairs[$media][] = $this->filter( 'minify-css', $cssText );
+ }
+ }
+ } elseif ( is_string( $style ) ) {
+ $stylePairs[$media] = $this->filter( 'minify-css', $style );
+ }
+ }
+ }
+ // Wrap styles into @media groups as needed and flatten into a numerical array
+ $styles = array(
+ 'css' => self::makeCombinedStyles( $stylePairs )
+ );
+ }
}
}
@@ -733,23 +764,21 @@ class ResourceLoader {
}
break;
case 'styles':
- $out .= self::makeCombinedStyles( $styles );
+ // We no longer seperate into media, they are all combined now with
+ // custom media type groups into @media .. {} sections as part of the css string.
+ // Module returns either an empty array or a numerical array with css strings.
+ $out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
break;
case 'messages':
$out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
break;
default:
- // Minify CSS before embedding in mw.loader.implement call
- // (unless in debug mode)
- if ( !$context->getDebug() ) {
- foreach ( $styles as $media => $style ) {
- if ( is_string( $style ) ) {
- $styles[$media] = $this->filter( 'minify-css', $style );
- }
- }
- }
- $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
- new XmlJsCode( $messagesBlob ) );
+ $out .= self::makeLoaderImplementScript(
+ $name,
+ $scripts,
+ $styles,
+ new XmlJsCode( $messagesBlob )
+ );
break;
}
} catch ( Exception $e ) {
@@ -760,15 +789,14 @@ class ResourceLoader {
$missing[] = $name;
unset( $modules[$name] );
}
+ $isRaw |= $module->isRaw();
wfProfileOut( __METHOD__ . '-' . $name );
}
// Update module states
- if ( $context->shouldIncludeScripts() ) {
+ if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
// Set the state of modules loaded as only scripts to ready
- if ( count( $modules ) && $context->getOnly() === 'scripts'
- && !isset( $modules['startup'] ) )
- {
+ if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
$out .= self::makeLoaderStateScript(
array_fill_keys( array_keys( $modules ), 'ready' ) );
}
@@ -796,9 +824,9 @@ class ResourceLoader {
* Returns JS code to call to mw.loader.implement for a module with
* given properties.
*
- * @param $name Module name
+ * @param $name string Module name
* @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code
- * @param $styles Mixed: List of CSS strings keyed by media type, or list of lists of URLs to
+ * @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
* @param $messages Mixed: List of messages associated with this module. May either be an
* associative array mapping message key to value, or a JSON-encoded message blob containing
@@ -808,7 +836,7 @@ class ResourceLoader {
*/
public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
if ( is_string( $scripts ) ) {
- $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" );
+ $scripts = new XmlJsCode( "function () {\n{$scripts}\n}" );
} elseif ( !is_array( $scripts ) ) {
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
}
@@ -817,6 +845,11 @@ class ResourceLoader {
array(
$name,
$scripts,
+ // Force objects. mw.loader.implement requires them to be javascript objects.
+ // Although these variables are associative arrays, which become javascript
+ // objects through json_encode. In many cases they will be empty arrays, and
+ // PHP/json_encode() consider empty arrays to be numerical arrays and
+ // output javascript "[]" instead of "{}". This fixes that.
(object)$styles,
(object)$messages
) );
@@ -836,26 +869,34 @@ class ResourceLoader {
/**
* Combines an associative array mapping media type to CSS into a
- * single stylesheet with @media blocks.
+ * single stylesheet with "@media" blocks.
*
- * @param $styles Array: List of CSS strings keyed by media type
+ * @param $styles Array: Array keyed by media type containing (arrays of) CSS strings.
*
- * @return string
+ * @return Array
*/
- public static function makeCombinedStyles( array $styles ) {
- $out = '';
- foreach ( $styles as $media => $style ) {
- // Transform the media type based on request params and config
- // The way that this relies on $wgRequest to propagate request params is slightly evil
- $media = OutputPage::transformCssMedia( $media );
-
- if ( $media === null ) {
- // Skip
- } elseif ( $media === '' || $media == 'all' ) {
- // Don't output invalid or frivolous @media statements
- $out .= "$style\n";
- } else {
- $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n";
+ private static function makeCombinedStyles( array $stylePairs ) {
+ $out = array();
+ foreach ( $stylePairs as $media => $styles ) {
+ // 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;
+ foreach ( $styles as $style ) {
+ $style = trim( $style );
+ // Don't output an empty "@media print { }" block (bug 40498)
+ if ( $style !== '' ) {
+ // Transform the media type based on request params and config
+ // The way that this relies on $wgRequest to propagate request params is slightly evil
+ $media = OutputPage::transformCssMedia( $media );
+
+ if ( $media === '' || $media == 'all' ) {
+ $out[] = $style;
+ } else if ( is_string( $media ) ) {
+ $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
+ }
+ // else: skip
+ }
}
}
return $out;
@@ -902,7 +943,7 @@ class ResourceLoader {
public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
$script = str_replace( "\n", "\n\t", trim( $script ) );
return Xml::encodeJsCall(
- "( function( name, version, dependencies, group, source ) {\n\t$script\n} )",
+ "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
array( $name, $version, $dependencies, $group, $source ) );
}
@@ -975,7 +1016,7 @@ class ResourceLoader {
* @return string
*/
public static function makeLoaderConditionalScript( $script ) {
- return "if(window.mw){\n".trim( $script )."\n}";
+ return "if(window.mw){\n" . trim( $script ) . "\n}";
}
/**
@@ -1091,4 +1132,17 @@ class ResourceLoader {
ksort( $query );
return $query;
}
+
+ /**
+ * Check a module name for validity.
+ *
+ * Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be
+ * at most 255 bytes.
+ *
+ * @param $moduleName string 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;
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php
index dd69bb01..0e96c6c8 100644
--- a/includes/resourceloader/ResourceLoaderContext.php
+++ b/includes/resourceloader/ResourceLoaderContext.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Context for resource loader modules.
+ *
* 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
@@ -39,6 +41,7 @@ class ResourceLoaderContext {
protected $only;
protected $version;
protected $hash;
+ protected $raw;
/* Methods */
@@ -62,6 +65,7 @@ class ResourceLoaderContext {
$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
@@ -157,7 +161,7 @@ class ResourceLoaderContext {
$this->direction = $this->request->getVal( 'dir' );
if ( !$this->direction ) {
# directionality based on user language (see bug 6100)
- $this->direction = Language::factory( $this->language )->getDir();
+ $this->direction = Language::factory( $this->getLanguage() )->getDir();
}
}
return $this->direction;
@@ -201,6 +205,13 @@ class ResourceLoaderContext {
/**
* @return bool
*/
+ public function getRaw() {
+ return $this->raw;
+ }
+
+ /**
+ * @return bool
+ */
public function shouldIncludeScripts() {
return is_null( $this->only ) || $this->only === 'scripts';
}
diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php
index 3d657e1c..d0c56ae8 100644
--- a/includes/resourceloader/ResourceLoaderFileModule.php
+++ b/includes/resourceloader/ResourceLoaderFileModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader module based on local JavaScript/CSS files.
+ *
* 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
@@ -109,6 +111,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
protected $position = 'bottom';
/** Boolean: Link to raw files in debug mode */
protected $debugRaw = true;
+ /** Boolean: Whether mw.loader.state() call should be omitted */
+ protected $raw = false;
/**
* Array: Cache for mtime
* @par Usage:
@@ -238,6 +242,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
break;
// Single booleans
case 'debugRaw':
+ case 'raw':
$this->{$member} = (bool) $option;
break;
}
@@ -366,6 +371,13 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
+ * @return bool
+ */
+ public function isRaw() {
+ return $this->raw;
+ }
+
+ /**
* Get the last modified timestamp of this module.
*
* Last modified timestamps are calculated from the highest last modified
@@ -622,7 +634,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
// Get and register local file references
$this->localFileRefs = array_merge(
$this->localFileRefs,
- CSSMin::getLocalFileReferences( $style, $dir ) );
+ CSSMin::getLocalFileReferences( $style, $dir )
+ );
return CSSMin::remap(
$style, $dir, $remoteDir, true
);
diff --git a/includes/resourceloader/ResourceLoaderFilePageModule.php b/includes/resourceloader/ResourceLoaderFilePageModule.php
index e3b719bb..61ed5206 100644
--- a/includes/resourceloader/ResourceLoaderFilePageModule.php
+++ b/includes/resourceloader/ResourceLoaderFilePageModule.php
@@ -1,5 +1,26 @@
<?php
/**
+ * Resource loader module for MediaWiki:Filepage.css
+ *
+ * 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
+ */
+
+/**
* ResourceLoader definition for MediaWiki:Filepage.css
*/
class ResourceLoaderFilePageModule extends ResourceLoaderWikiModule {
diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
new file mode 100644
index 00000000..c916c4a5
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Resource loader module for populating language specific data.
+ *
+ * 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 Santhosh Thottingal
+ * @author Timo Tijhof
+ */
+
+/**
+ * ResourceLoader module for populating language specific data.
+ */
+class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
+
+ protected $language;
+ /**
+ * Get the grammar forms for the site content language.
+ *
+ * @return array
+ */
+ protected function getSiteLangGrammarForms() {
+ return $this->language->getGrammarForms();
+ }
+
+ /**
+ * Get the plural forms for the site content language.
+ *
+ * @return array
+ */
+ protected function getPluralRules() {
+ return $this->language->getPluralRules();
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Get all the dynamic data for the content language to an array
+ *
+ * @return array
+ */
+ protected function getData() {
+ return array(
+ 'digitTransformTable' => $this->getDigitTransformTable(),
+ 'grammarForms' => $this->getSiteLangGrammarForms(),
+ 'pluralRules' => $this->getPluralRules(),
+ );
+ }
+
+ /**
+ * @param $context ResourceLoaderContext
+ * @return string: JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $this->language = Language::factory( $context->getLanguage() );
+ return Xml::encodeJsCall( 'mw.language.setData', array(
+ $this->language->getCode(),
+ $this->getData()
+ ) );
+ }
+
+ /**
+ * @param $context ResourceLoaderContext
+ * @return array|int|Mixed
+ */
+ public function getModifiedTime( ResourceLoaderContext $context ) {
+ $this->language = Language::factory( $context ->getLanguage() );
+ $cache = wfGetCache( CACHE_ANYTHING );
+ $key = wfMemcKey( 'resourceloader', 'langdatamodule', 'changeinfo' );
+
+ $data = $this->getData();
+ $hash = md5( serialize( $data ) );
+
+ $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 array
+ */
+ public function getDependencies() {
+ return array( 'mediawiki.language.init' );
+ }
+}
diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php
index 1a232ec2..9c49c45f 100644
--- a/includes/resourceloader/ResourceLoaderModule.php
+++ b/includes/resourceloader/ResourceLoaderModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Abstraction for resource loader modules.
+ *
* 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
@@ -170,7 +172,9 @@ abstract class ResourceLoaderModule {
* Get all CSS for this module for a given skin.
*
* @param $context ResourceLoaderContext: Context object
- * @return Array: List of CSS strings keyed by media type
+ * @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 }' ) );
*/
public function getStyles( ResourceLoaderContext $context ) {
// Stub, override expected
@@ -235,8 +239,8 @@ abstract class ResourceLoaderModule {
/**
* Where on the HTML page should this module's JS be loaded?
- * 'top': in the <head>
- * 'bottom': at the bottom of the <body>
+ * - 'top': in the "<head>"
+ * - 'bottom': at the bottom of the "<body>"
*
* @return string
*/
@@ -245,6 +249,17 @@ abstract class ResourceLoaderModule {
}
/**
+ * Whether this module's JS expects to work without the client-side ResourceLoader module.
+ * Returning true from this function will prevent mw.loader.state() call from being
+ * appended to the bottom of the script.
+ *
+ * @return bool
+ */
+ public function isRaw() {
+ return false;
+ }
+
+ /**
* 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
diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderNoscriptModule.php
index 28f629a2..8e81c8d9 100644
--- a/includes/resourceloader/ResourceLoaderNoscriptModule.php
+++ b/includes/resourceloader/ResourceLoaderNoscriptModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader for site customizations for users without JavaScript enabled.
+ *
* 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
diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php
index 2527a0a3..03fe1fe5 100644
--- a/includes/resourceloader/ResourceLoaderSiteModule.php
+++ b/includes/resourceloader/ResourceLoaderSiteModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader module for site customizations.
+ *
* 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
diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php
index 5dbce439..20ee83f9 100644
--- a/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Module for resource loader initialization.
+ *
* 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
@@ -35,8 +37,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
protected function getConfig( $context ) {
global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
$wgArticlePath, $wgScriptPath, $wgServer, $wgContLang,
- $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
- $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
+ $wgVariantArticlePath, $wgActionPaths, $wgVersion,
+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname,
$wgSitename, $wgFileExtensions, $wgExtensionAssetsPath,
$wgCookiePrefix, $wgResourceLoaderMaxQueryLength;
@@ -77,10 +79,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgVersion' => $wgVersion,
'wgEnableAPI' => $wgEnableAPI,
'wgEnableWriteAPI' => $wgEnableWriteAPI,
- 'wgDefaultDateFormat' => $wgContLang->getDefaultDateFormat(),
- 'wgMonthNames' => $wgContLang->getMonthNamesArray(),
- 'wgMonthNamesShort' => $wgContLang->getMonthAbbreviationsArray(),
- 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
+ 'wgMainPageTitle' => $mainPage->getPrefixedText(),
'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
'wgNamespaceIds' => $namespaceIds,
'wgSiteName' => $wgSitename,
@@ -96,9 +95,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength,
'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
);
- if ( $wgUseAjax && $wgEnableMWSuggest ) {
- $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
- }
wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) );
@@ -125,12 +121,12 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
// Register modules
foreach ( $resourceLoader->getModuleNames() as $name ) {
$module = $resourceLoader->getModule( $name );
+ $deps = $module->getDependencies();
+ $group = $module->getGroup();
+ $source = $module->getSource();
// Support module loader scripts
$loader = $module->getLoaderScript();
if ( $loader !== false ) {
- $deps = $module->getDependencies();
- $group = $module->getGroup();
- $source = $module->getSource();
$version = wfTimestamp( TS_ISO_8601_BASIC,
$module->getModifiedTime( $context ) );
$out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $source, $loader );
@@ -143,26 +139,23 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
$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( $module->getDependencies() && $module->getGroup() === null && $module->getSource() === 'local' ) ) {
+ 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 ( $module->getGroup() === null && $module->getSource() === 'local' ) {
- $registrations[] = array(
- $name, $mtime, $module->getDependencies() );
+ 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 ( $module->getSource() === 'local' ) {
- $registrations[] = array(
- $name, $mtime, $module->getDependencies(), $module->getGroup() );
+ 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, $module->getDependencies(), $module->getGroup(), $module->getSource() );
+ $registrations[] = array( $name, $mtime, $deps, $group, $source );
}
}
}
@@ -175,6 +168,13 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
/* Methods */
/**
+ * @return bool
+ */
+ public function isRaw() {
+ return true;
+ }
+
+ /**
* @param $context ResourceLoaderContext
* @return string
*/
@@ -185,19 +185,20 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
if ( $context->getOnly() === 'scripts' ) {
// The core modules:
- $modules = array( 'jquery', 'mediawiki' );
- wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$modules ) );
+ $moduleNames = array( 'jquery', 'mediawiki' );
+ wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$moduleNames ) );
// Get the latest version
+ $loader = $context->getResourceLoader();
$version = 0;
- foreach ( $modules as $moduleName ) {
+ foreach ( $moduleNames as $moduleName ) {
$version = max( $version,
- $context->getResourceLoader()->getModule( $moduleName )->getModifiedTime( $context )
+ $loader->getModule( $moduleName )->getModifiedTime( $context )
);
}
// Build load query for StartupModules
$query = array(
- 'modules' => ResourceLoader::makePackedModulesString( $modules ),
+ 'modules' => ResourceLoader::makePackedModulesString( $moduleNames ),
'only' => 'scripts',
'lang' => $context->getLanguage(),
'skin' => $context->getSkin(),
@@ -210,6 +211,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
// Startup function
$configuration = $this->getConfig( $context );
$registrations = self::getModuleRegistrations( $context );
+ $registrations = str_replace( "\n", "\n\t", trim( $registrations ) ); // fix indentation
$out .= "var startUp = function() {\n" .
"\tmw.config = new " . Xml::encodeJsCall( 'mw.Map', array( $wgLegacyJavaScriptGlobals ) ) . "\n" .
"\t$registrations\n" .
diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
index 02693d3e..0e95d964 100644
--- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader module for user preference customizations.
+ *
* 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
@@ -69,13 +71,6 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
$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['highlightbroken'] ) {
- $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
- } else {
- $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
- $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
- $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
- }
if ( $options['justify'] ) {
$rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
}
@@ -86,7 +81,10 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
$rules[] = ".editsection { display: none; }\n";
}
if ( $options['editfont'] !== 'default' ) {
- $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
+ // 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 ) ) {
diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
index 733dfa04..1316f423 100644
--- a/includes/resourceloader/ResourceLoaderUserGroupsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader module for user customizations.
+ *
* 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
@@ -31,21 +33,32 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
* @return array
*/
protected function getPages( ResourceLoaderContext $context ) {
- if ( $context->getUser() ) {
- $user = User::newFromName( $context->getUser() );
- if ( $user instanceof User ) {
- $pages = array();
- 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' );
- }
- return $pages;
+ global $wgUser;
+
+ $userName = $context->getUser();
+ if ( $userName === null ) {
+ return array();
+ }
+
+ // Use $wgUser is possible; allows to skip a lot of code
+ if ( is_object( $wgUser ) && $wgUser->getName() == $userName ) {
+ $user = $wgUser;
+ } else {
+ $user = User::newFromName( $userName );
+ if ( !$user instanceof User ) {
+ return array();
+ }
+ }
+
+ $pages = array();
+ 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' );
}
- return array();
+ return $pages;
}
/* Methods */
diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php
index 338b6322..177302c5 100644
--- a/includes/resourceloader/ResourceLoaderUserModule.php
+++ b/includes/resourceloader/ResourceLoaderUserModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader module for user customizations.
+ *
* 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,33 +35,40 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
* @return array
*/
protected function getPages( ResourceLoaderContext $context ) {
- if ( $context->getUser() ) {
- // Get the normalized title of the user's user page
- $username = $context->getUser();
- $userpageTitle = Title::makeTitleSafe( NS_USER, $username );
- $userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works
+ $username = $context->getUser();
+
+ if ( $username === null ) {
+ return array();
+ }
+
+ // Get the normalized title of the user's user page
+ $userpageTitle = Title::makeTitleSafe( NS_USER, $username );
+
+ if ( !$userpageTitle instanceof Title ) {
+ return array();
+ }
+
+ $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(
+ "$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' ),
+ );
- // 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
- // parameter will be set to the name of the page we need to exclude.
- $excludepage = $context->getRequest()->getVal( 'excludepage' );
- if ( isset( $pages[$excludepage] ) ) {
- // This works because $excludepage is generated with getPrefixedDBkey(),
- // just like the keys in $pages[] above
- unset( $pages[$excludepage] );
- }
- return $pages;
+ // 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
+ // parameter will be set to the name of the page we need to exclude.
+ $excludepage = $context->getRequest()->getVal( 'excludepage' );
+ if ( isset( $pages[$excludepage] ) ) {
+ // This works because $excludepage is generated with getPrefixedDBkey(),
+ // just like the keys in $pages[] above
+ unset( $pages[$excludepage] );
}
- return array();
+ return $pages;
}
/* Methods */
diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
index 7b162205..4624cbce 100644
--- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader module for user preference customizations.
+ *
* 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
@@ -42,7 +44,7 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
if ( isset( $this->modifiedTime[$hash] ) ) {
return $this->modifiedTime[$hash];
}
-
+
global $wgUser;
return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() );
}
@@ -58,6 +60,13 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
}
/**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
* @return string
*/
public function getGroup() {
diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php
index e1a52388..62d096a6 100644
--- a/includes/resourceloader/ResourceLoaderUserTokensModule.php
+++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Resource loader module for user tokens.
+ *
* 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
@@ -55,6 +57,13 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
}
/**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
* @return string
*/
public function getGroup() {
diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php
index 91a51f89..ee8dd1e5 100644
--- a/includes/resourceloader/ResourceLoaderWikiModule.php
+++ b/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Abstraction for resource loader modules which pull from 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
@@ -20,8 +22,6 @@
* @author Roan Kattouw
*/
-defined( 'MEDIAWIKI' ) || die( 1 );
-
/**
* Abstraction for resource loader modules which pull from wiki pages
*
@@ -42,7 +42,6 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
/* Abstract Protected Methods */
/**
- * @abstract
* @param $context ResourceLoaderContext
*/
abstract protected function getPages( ResourceLoaderContext $context );
@@ -69,14 +68,10 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
* @return null|string
*/
protected function getContent( $title ) {
- if ( $title->getNamespace() === NS_MEDIAWIKI ) {
- $message = wfMessage( $title->getDBkey() )->inContentLanguage();
- return $message->exists() ? $message->plain() : '';
- }
if ( !$title->isCssJsSubpage() && !$title->isCssOrJsPage() ) {
return null;
}
- $revision = Revision::newFromTitle( $title );
+ $revision = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
if ( !$revision ) {
return null;
}
@@ -137,12 +132,12 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
}
$style = CSSMin::remap( $style, false, $wgScriptPath, true );
if ( !isset( $styles[$media] ) ) {
- $styles[$media] = '';
+ $styles[$media] = array();
}
if ( strpos( $titleText, '*/' ) === false ) {
- $styles[$media] .= "/* $titleText */\n";
+ $style = "/* $titleText */\n" . $style;
}
- $styles[$media] .= $style . "\n";
+ $styles[$media][] = $style;
}
return $styles;
}
@@ -181,7 +176,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
// We're dealing with a subclass that doesn't have a DB
return array();
}
-
+
$hash = $context->getHash();
if ( isset( $this->titleMtimes[$hash] ) ) {
return $this->titleMtimes[$hash];
diff --git a/includes/revisiondelete/RevisionDelete.php b/includes/revisiondelete/RevisionDelete.php
index 6cee6246..6ceadff4 100644
--- a/includes/revisiondelete/RevisionDelete.php
+++ b/includes/revisiondelete/RevisionDelete.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Base implementations for deletable items.
+ *
+ * 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 RevisionDelete
+ */
+
+/**
* List for revision table items
*
* This will check both the 'revision' table for live revisions and the
@@ -191,13 +213,16 @@ class RevDel_RevisionItem extends RevDel_Item {
/**
* Get the HTML link to the revision text.
* Overridden by RevDel_ArchiveItem.
+ * @return string
*/
protected function getRevisionLink() {
- $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true );
+ $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
+ $this->revision->getTimestamp(), $this->list->getUser() ) );
+
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $date;
}
- return Linker::link(
+ return Linker::linkKnown(
$this->list->title,
$date,
array(),
@@ -211,38 +236,36 @@ class RevDel_RevisionItem extends RevDel_Item {
/**
* Get the HTML link to the diff.
* Overridden by RevDel_ArchiveItem
+ * @return string
*/
protected function getDiffLink() {
if ( $this->isDeleted() && !$this->canViewContent() ) {
- return wfMsgHtml('diff');
+ return $this->list->msg( 'diff' )->escaped();
} else {
return
- Linker::link(
+ Linker::linkKnown(
$this->list->title,
- wfMsgHtml('diff'),
+ $this->list->msg( 'diff' )->escaped(),
array(),
array(
'diff' => $this->revision->getId(),
'oldid' => 'prev',
'unhide' => 1
- ),
- array(
- 'known',
- 'noclasses'
)
);
}
}
public function getHTML() {
- $difflink = $this->getDiffLink();
+ $difflink = $this->list->msg( 'parentheses' )
+ ->rawParams( $this->getDiffLink() )->escaped();
$revlink = $this->getRevisionLink();
$userlink = Linker::revUserLink( $this->revision );
$comment = Linker::revComment( $this->revision );
if ( $this->isDeleted() ) {
$revlink = "<span class=\"history-deleted\">$revlink</span>";
}
- return "<li>($difflink) $revlink $userlink $comment</li>";
+ return "<li>$difflink $revlink $userlink $comment</li>";
}
}
@@ -298,7 +321,7 @@ class RevDel_ArchiveItem extends RevDel_RevisionItem {
public function __construct( $list, $row ) {
RevDel_Item::__construct( $list, $row );
$this->revision = Revision::newFromArchiveRow( $row,
- array( 'page' => $this->list->title->getArticleId() ) );
+ array( 'page' => $this->list->title->getArticleID() ) );
}
public function getIdField() {
@@ -339,29 +362,39 @@ class RevDel_ArchiveItem extends RevDel_RevisionItem {
}
protected function getRevisionLink() {
- $undelete = SpecialPage::getTitleFor( 'Undelete' );
- $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true );
+ $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
+ $this->revision->getTimestamp(), $this->list->getUser() ) );
+
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $date;
}
- return Linker::link( $undelete, $date, array(),
+
+ return Linker::link(
+ SpecialPage::getTitleFor( 'Undelete' ),
+ $date,
+ array(),
array(
'target' => $this->list->title->getPrefixedText(),
'timestamp' => $this->revision->getTimestamp()
- ) );
+ )
+ );
}
protected function getDiffLink() {
if ( $this->isDeleted() && !$this->canViewContent() ) {
- return wfMsgHtml( 'diff' );
+ return $this->list->msg( 'diff' )->escaped();
}
- $undelete = SpecialPage::getTitleFor( 'Undelete' );
- return Linker::link( $undelete, wfMsgHtml('diff'), array(),
+
+ return Linker::link(
+ SpecialPage::getTitleFor( 'Undelete' ),
+ $this->list->msg( 'diff' )->escaped(),
+ array(),
array(
'target' => $this->list->title->getPrefixedText(),
'diff' => 'prev',
'timestamp' => $this->revision->getTimestamp()
- ) );
+ )
+ );
}
}
@@ -375,7 +408,7 @@ class RevDel_ArchivedRevisionItem extends RevDel_ArchiveItem {
RevDel_Item::__construct( $list, $row );
$this->revision = Revision::newFromArchiveRow( $row,
- array( 'page' => $this->list->title->getArticleId() ) );
+ array( 'page' => $this->list->title->getArticleID() ) );
}
public function getIdField() {
@@ -569,31 +602,34 @@ class RevDel_FileItem extends RevDel_Item {
/**
* Get the link to the file.
* Overridden by RevDel_ArchivedFileItem.
+ * @return string
*/
protected function getLink() {
- $date = $this->list->getLanguage()->timeanddate( $this->file->getTimestamp(), true );
- if ( $this->isDeleted() ) {
- # Hidden files...
- if ( !$this->canViewContent() ) {
- $link = $date;
- } else {
- $revdelete = SpecialPage::getTitleFor( 'Revisiondelete' );
- $link = Linker::link(
- $revdelete,
- $date, array(),
- array(
- 'target' => $this->list->title->getPrefixedText(),
- 'file' => $this->file->getArchiveName(),
- 'token' => $this->list->getUser()->getEditToken(
- $this->file->getArchiveName() )
- )
- );
- }
- return '<span class="history-deleted">' . $link . '</span>';
- } else {
+ $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
+ $this->file->getTimestamp(), $this->list->getUser() ) );
+
+ if ( !$this->isDeleted() ) {
# Regular files...
- return Xml::element( 'a', array( 'href' => $this->file->getUrl() ), $date );
+ return Html::rawElement( 'a', array( 'href' => $this->file->getUrl() ), $date );
}
+
+ # Hidden files...
+ if ( !$this->canViewContent() ) {
+ $link = $date;
+ } else {
+ $link = Linker::link(
+ SpecialPage::getTitleFor( 'Revisiondelete' ),
+ $date,
+ array(),
+ array(
+ 'target' => $this->list->title->getPrefixedText(),
+ 'file' => $this->file->getArchiveName(),
+ 'token' => $this->list->getUser()->getEditToken(
+ $this->file->getArchiveName() )
+ )
+ );
+ }
+ return '<span class="history-deleted">' . $link . '</span>';
}
/**
* Generate a user tool link cluster if the current user is allowed to view it
@@ -604,7 +640,7 @@ class RevDel_FileItem extends RevDel_Item {
$link = Linker::userLink( $this->file->user, $this->file->user_text ) .
Linker::userToolLinks( $this->file->user, $this->file->user_text );
} else {
- $link = wfMsgHtml( 'rev-deleted-user' );
+ $link = $this->list->msg( 'rev-deleted-user' )->escaped();
}
if( $this->file->isDeleted( Revision::DELETED_USER ) ) {
return '<span class="history-deleted">' . $link . '</span>';
@@ -622,7 +658,7 @@ class RevDel_FileItem extends RevDel_Item {
if( $this->file->userCan( File::DELETED_COMMENT, $this->list->getUser() ) ) {
$block = Linker::commentBlock( $this->file->description );
} else {
- $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+ $block = ' ' . $this->list->msg( 'rev-deleted-comment' )->escaped();
}
if( $this->file->isDeleted( File::DELETED_COMMENT ) ) {
return "<span class=\"history-deleted\">$block</span>";
@@ -632,14 +668,9 @@ class RevDel_FileItem extends RevDel_Item {
public function getHTML() {
$data =
- wfMsg(
- 'widthheight',
- $this->list->getLanguage()->formatNum( $this->file->getWidth() ),
- $this->list->getLanguage()->formatNum( $this->file->getHeight() )
- ) .
- ' (' .
- wfMsgExt( 'nbytes', 'parsemag', $this->list->getLanguage()->formatNum( $this->file->getSize() ) ) .
- ')';
+ $this->list->msg( 'widthheight' )->numParams(
+ $this->file->getWidth(), $this->file->getHeight() )->text() .
+ ' (' . $this->list->msg( 'nbytes' )->numParams( $this->file->getSize() )->text() . ')';
return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' .
$data . ' ' . $this->getComment(). '</li>';
@@ -722,13 +753,15 @@ class RevDel_ArchivedFileItem extends RevDel_FileItem {
}
protected function getLink() {
- $date = $this->list->getLanguage()->timeanddate( $this->file->getTimestamp(), true );
- $undelete = SpecialPage::getTitleFor( 'Undelete' );
- $key = $this->file->getKey();
+ $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
+ $this->file->getTimestamp(), $this->list->getUser() ) );
+
# Hidden files...
if( !$this->canViewContent() ) {
$link = $date;
} else {
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $key = $this->file->getKey();
$link = Linker::link( $undelete, $date, array(),
array(
'target' => $this->list->title->getPrefixedText(),
@@ -847,18 +880,21 @@ class RevDel_LogItem extends RevDel_Item {
}
public function getHTML() {
- $date = htmlspecialchars( $this->list->getLanguage()->timeanddate( $this->row->log_timestamp ) );
+ $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
+ $this->row->log_timestamp, $this->list->getUser() ) );
$title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title );
$formatter = LogFormatter::newFromRow( $this->row );
+ $formatter->setContext( $this->list->getContext() );
$formatter->setAudience( LogFormatter::FOR_THIS_USER );
// Log link for this page
$loglink = Linker::link(
SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'log' ),
+ $this->list->msg( 'log' )->escaped(),
array(),
array( 'page' => $title->getPrefixedText() )
);
+ $loglink = $this->list->msg( 'parentheses' )->rawParams( $loglink )->escaped();
// User links and action text
$action = $formatter->getActionText();
// Comment
@@ -867,6 +903,6 @@ class RevDel_LogItem extends RevDel_Item {
$comment = '<span class="history-deleted">' . $comment . '</span>';
}
- return "<li>($loglink) $date $action $comment</li>";
+ return "<li>$loglink $date $action $comment</li>";
}
}
diff --git a/includes/revisiondelete/RevisionDeleteAbstracts.php b/includes/revisiondelete/RevisionDeleteAbstracts.php
index dc7af194..4f58099f 100644
--- a/includes/revisiondelete/RevisionDeleteAbstracts.php
+++ b/includes/revisiondelete/RevisionDeleteAbstracts.php
@@ -1,4 +1,25 @@
<?php
+/**
+ * Interface definition for deletable items.
+ *
+ * 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 RevisionDelete
+ */
/**
* Abstract base class for a list of deletable items. The list class
@@ -16,6 +37,7 @@ 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
*/
public static function getRelationType() {
return null;
@@ -25,7 +47,7 @@ abstract class RevDel_List extends RevisionListBase {
* Set the visibility for the revisions in this list. Logging and
* transactions are done here.
*
- * @param $params Associative array of parameters. Members are:
+ * @param $params array Associative array of parameters. Members are:
* value: The integer value to set the visibility to
* comment: The log comment.
* @return Status
@@ -37,7 +59,7 @@ abstract class RevDel_List extends RevisionListBase {
$this->res = false;
$dbw = wfGetDB( DB_MASTER );
$this->doQuery( $dbw );
- $dbw->begin();
+ $dbw->begin( __METHOD__ );
$status = Status::newGood();
$missing = array_flip( $this->ids );
$this->clearFileOps();
@@ -110,7 +132,7 @@ abstract class RevDel_List extends RevisionListBase {
if ( $status->successCount == 0 ) {
$status->ok = false;
- $dbw->rollback();
+ $dbw->rollback( __METHOD__ );
return $status;
}
@@ -121,7 +143,7 @@ abstract class RevDel_List extends RevisionListBase {
$status->merge( $this->doPreCommitUpdates() );
if ( !$status->isOK() ) {
// Fatal error, such as no configured archive directory
- $dbw->rollback();
+ $dbw->rollback( __METHOD__ );
return $status;
}
@@ -136,7 +158,7 @@ abstract class RevDel_List extends RevisionListBase {
'authorIds' => $authorIds,
'authorIPs' => $authorIPs
) );
- $dbw->commit();
+ $dbw->commit( __METHOD__ );
// Clear caches
$status->merge( $this->doPostCommitUpdates() );
@@ -154,7 +176,7 @@ abstract class RevDel_List extends RevisionListBase {
/**
* Record a log entry on the action
- * @param $params Associative array of parameters:
+ * @param $params array Associative array of parameters:
* newBits: The new value of the *_deleted bitfield
* oldBits: The old value of the *_deleted bitfield.
* title: The target title
@@ -189,6 +211,7 @@ abstract class RevDel_List extends RevisionListBase {
/**
* Get the log action for this list type
+ * @return string
*/
public function getLogAction() {
return 'revision';
@@ -196,7 +219,7 @@ abstract class RevDel_List extends RevisionListBase {
/**
* Get log parameter array.
- * @param $params Associative array of log parameters, same as updateLog()
+ * @param $params array Associative array of log parameters, same as updateLog()
* @return array
*/
public function getLogParams( $params ) {
@@ -247,6 +270,7 @@ abstract class RevDel_Item extends RevisionItemBase {
* Returns true if the item is "current", and the operation to set the given
* bits can't be executed for that reason
* STUB
+ * @return bool
*/
public function isHideCurrentOp( $newBits ) {
return false;
diff --git a/includes/revisiondelete/RevisionDeleteUser.php b/includes/revisiondelete/RevisionDeleteUser.php
index c88b4d91..c02e9c76 100644
--- a/includes/revisiondelete/RevisionDeleteUser.php
+++ b/includes/revisiondelete/RevisionDeleteUser.php
@@ -1,9 +1,6 @@
<?php
/**
- * Backend functions for suppressing and unsuppressing all references to a given user,
- * used when blocking with HideUser enabled. This was spun out of SpecialBlockip.php
- * in 1.18; at some point it needs to be rewritten to either use RevisionDelete abstraction,
- * or at least schema abstraction.
+ * Backend functions for suppressing and unsuppressing all references to a given user.
*
* 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 +20,15 @@
* @file
* @ingroup RevisionDelete
*/
+
+/**
+ * Backend functions for suppressing and unsuppressing all references to a given user,
+ * used when blocking with HideUser enabled. This was spun out of SpecialBlockip.php
+ * in 1.18; at some point it needs to be rewritten to either use RevisionDelete abstraction,
+ * or at least schema abstraction.
+ *
+ * @ingroup RevisionDelete
+ */
class RevisionDeleteUser {
/**
@@ -30,14 +36,14 @@ class RevisionDeleteUser {
* @param $name String username
* @param $userId Int user id
* @param $op String operator '|' or '&'
- * @param $dbw null|Database, if you happen to have one lying around
+ * @param $dbw null|DatabaseBase, if you happen to have one lying around
* @return bool
*/
private static function setUsernameBitfields( $name, $userId, $op, $dbw ) {
- if( $op !== '|' && $op !== '&' ){
+ if ( !$userId || ( $op !== '|' && $op !== '&' ) ) {
return false; // sanity check
}
- if( !$dbw instanceof DatabaseBase ){
+ if ( !$dbw instanceof DatabaseBase ) {
$dbw = wfGetDB( DB_MASTER );
}
@@ -127,4 +133,4 @@ class RevisionDeleteUser {
public static function unsuppressUserName( $name, $userId, $dbw = null ) {
return self::setUsernameBitfields( $name, $userId, '&', $dbw );
}
-} \ No newline at end of file
+}
diff --git a/includes/revisiondelete/RevisionDeleter.php b/includes/revisiondelete/RevisionDeleter.php
index 59a9fa82..c59edc2a 100644
--- a/includes/revisiondelete/RevisionDeleter.php
+++ b/includes/revisiondelete/RevisionDeleter.php
@@ -2,12 +2,29 @@
/**
* Revision/log/file deletion 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
+ * @ingroup RevisionDelete
*/
/**
* Temporary b/c interface, collection of static functions.
* @ingroup SpecialPage
+ * @ingroup RevisionDelete
*/
class RevisionDeleter {
/**
@@ -42,7 +59,7 @@ class RevisionDeleter {
*
* @param $n Integer: the new bitfield.
* @param $o Integer: the old bitfield.
- * @return An array as described above.
+ * @return array An array as described above.
* @since 1.19 public
*/
public static function getChanges( $n, $o ) {
@@ -107,69 +124,4 @@ class RevisionDeleter {
return $timestamp;
}
-
- /**
- * Creates utility links for log entries.
- *
- * @param $title Title
- * @param $paramArray Array
- * @param $messages
- * @return String
- */
- public static function getLogLinks( $title, $paramArray, $messages ) {
- global $wgLang;
-
- if ( count( $paramArray ) >= 2 ) {
- // Different revision types use different URL params...
- $key = $paramArray[0];
- // $paramArray[1] is a CSV of the IDs
- $Ids = explode( ',', $paramArray[1] );
-
- $revert = array();
-
- // Diff link for single rev deletions
- if ( count( $Ids ) == 1 ) {
- // Live revision diffs...
- if ( in_array( $key, array( 'oldid', 'revision' ) ) ) {
- $revert[] = Linker::linkKnown(
- $title,
- $messages['diff'],
- array(),
- array(
- 'diff' => intval( $Ids[0] ),
- 'unhide' => 1
- )
- );
- // Deleted revision diffs...
- } elseif ( in_array( $key, array( 'artimestamp','archive' ) ) ) {
- $revert[] = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Undelete' ),
- $messages['diff'],
- array(),
- array(
- 'target' => $title->getPrefixedDBKey(),
- 'diff' => 'prev',
- 'timestamp' => $Ids[0]
- )
- );
- }
- }
-
- // View/modify link...
- $revert[] = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Revisiondelete' ),
- $messages['revdel-restore'],
- array(),
- array(
- 'target' => $title->getPrefixedText(),
- 'type' => $key,
- 'ids' => implode(',', $Ids),
- )
- );
-
- // Pipe links
- return wfMsg( 'parentheses', $wgLang->pipeList( $revert ) );
- }
- return '';
- }
}
diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php
index 2f7dfd7e..27a321ac 100644
--- a/includes/search/SearchEngine.php
+++ b/includes/search/SearchEngine.php
@@ -2,6 +2,21 @@
/**
* Basic search engine
*
+ * 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
*/
@@ -65,6 +80,7 @@ class SearchEngine {
/**
* If this search backend can list/unlist redirects
* @deprecated since 1.18 Call supports( 'list-redirects' );
+ * @return bool
*/
function acceptListRedirects() {
wfDeprecated( __METHOD__, '1.18' );
@@ -91,7 +107,7 @@ class SearchEngine {
* @since 1.18
* @param $feature String
* @param $data Mixed
- * @return Noolean
+ * @return bool
*/
public function setFeatureData( $feature, $data ) {
$this->features[$feature] = $data;
@@ -147,6 +163,7 @@ class SearchEngine {
/**
* Really find the title match.
+ * @return null|\Title
*/
private static function getNearMatchInternal( $searchterm ) {
global $wgContLang, $wgEnableSearchContributorsByIP;
@@ -287,6 +304,7 @@ class SearchEngine {
* or namespace names
*
* @param $query String
+ * @return string
*/
function replacePrefixes( $query ) {
global $wgContLang;
@@ -297,7 +315,7 @@ class SearchEngine {
return $parsed;
}
- $allkeyword = wfMsgForContent( 'searchall' ) . ":";
+ $allkeyword = wfMessage( 'searchall' )->inContentLanguage()->text() . ":";
if ( strncmp( $query, $allkeyword, strlen( $allkeyword ) ) == 0 ) {
$this->namespaces = null;
$parsed = substr( $query, strlen( $allkeyword ) );
@@ -391,6 +409,7 @@ class SearchEngine {
* and preferences
*
* @param $namespaces Array
+ * @return array
*/
public static function namespacesAsText( $namespaces ) {
global $wgContLang;
@@ -398,7 +417,7 @@ class SearchEngine {
$formatted = array_map( array( $wgContLang, 'getFormattedNsText' ), $namespaces );
foreach ( $formatted as $key => $ns ) {
if ( empty( $ns ) )
- $formatted[$key] = wfMsg( 'blanknamespace' );
+ $formatted[$key] = wfMessage( 'blanknamespace' )->text();
}
return $formatted;
}
@@ -486,19 +505,6 @@ class SearchEngine {
return $wgCanonicalServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
}
}
-
- /**
- * Get internal MediaWiki Suggest template
- *
- * @return String
- */
- public static function getMWSuggestTemplate() {
- global $wgMWSuggestTemplate, $wgServer;
- if ( $wgMWSuggestTemplate )
- return $wgMWSuggestTemplate;
- else
- return $wgServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace={namespaces}&suggest';
- }
}
/**
@@ -737,7 +743,10 @@ class SearchResult {
protected function initFromTitle( $title ) {
$this->mTitle = $title;
if ( !is_null( $this->mTitle ) ) {
- $this->mRevision = Revision::newFromTitle( $this->mTitle );
+ $id = false;
+ wfRunHooks( 'SearchResultInitFromTitle', array( $title, &$id ) );
+ $this->mRevision = Revision::newFromTitle(
+ $this->mTitle, $id, Revision::READ_NORMAL );
if ( $this->mTitle->getNamespace() === NS_FILE )
$this->mImage = wfFindFile( $this->mTitle );
}
@@ -771,7 +780,7 @@ class SearchResult {
}
/**
- * @return Double or null if not supported
+ * @return float|null if not supported
*/
function getScore() {
return null;
@@ -1185,6 +1194,7 @@ class SearchHighlighter {
* Do manual case conversion for non-ascii chars
*
* @param $matches Array
+ * @return string
*/
function caseCallback( $matches ) {
global $wgContLang;
@@ -1305,6 +1315,7 @@ class SearchHighlighter {
/**
* Basic wikitext removal
* @protected
+ * @return mixed
*/
function removeWiki( $text ) {
$fname = __METHOD__;
diff --git a/includes/search/SearchIBM_DB2.php b/includes/search/SearchIBM_DB2.php
index c02a009d..51ed000f 100644
--- a/includes/search/SearchIBM_DB2.php
+++ b/includes/search/SearchIBM_DB2.php
@@ -111,6 +111,7 @@ class SearchIBM_DB2 extends SearchEngine {
* 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) . ' ' .
@@ -145,7 +146,9 @@ class SearchIBM_DB2 extends SearchEngine {
'WHERE page_id=si_page AND ' . $match;
}
- /** @todo document */
+ /** @todo document
+ * @return string
+ */
function parseQuery($filteredText, $fulltext) {
global $wgContLang;
$lc = SearchEngine::legalSearchChars();
diff --git a/includes/search/SearchMssql.php b/includes/search/SearchMssql.php
index ebf19d3a..69c92ba3 100644
--- a/includes/search/SearchMssql.php
+++ b/includes/search/SearchMssql.php
@@ -115,6 +115,7 @@ class SearchMssql extends SearchEngine {
*
* @param $filteredTerm String
* @param $fulltext Boolean
+ * @return String
*/
function getQuery( $filteredTerm, $fulltext ) {
return $this->queryLimit( $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
@@ -151,7 +152,9 @@ class SearchMssql extends SearchEngine {
'WHERE page_id=ftindex.[KEY] ';
}
- /** @todo document */
+ /** @todo document
+ * @return string
+ */
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
$lc = SearchEngine::legalSearchChars();
@@ -189,6 +192,7 @@ class SearchMssql extends SearchEngine {
* @param $id Integer
* @param $title String
* @param $text String
+ * @return bool|\ResultWrapper
*/
function update( $id, $title, $text ) {
// We store the column data as UTF-8 byte order marked binary stream
@@ -211,6 +215,7 @@ class SearchMssql extends SearchEngine {
*
* @param $id Integer
* @param $title String
+ * @return bool|\ResultWrapper
*/
function updateTitle( $id, $title ) {
$table = $this->db->tableName( 'searchindex' );
diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php
index af8f3875..5cee03e0 100644
--- a/includes/search/SearchMySQL.php
+++ b/includes/search/SearchMySQL.php
@@ -290,7 +290,7 @@ class SearchMySQL extends SearchEngine {
/**
* Get the base part of the search query.
*
- * @param &$query Search query array
+ * @param &$query array Search query array
* @param $filteredTerm String
* @param $fulltext Boolean
* @since 1.18 (changed)
@@ -308,6 +308,7 @@ class SearchMySQL extends SearchEngine {
/**
* @since 1.18 (changed)
+ * @return array
*/
function getCountQuery( $filteredTerm, $fulltext ) {
$match = $this->parseQuery( $filteredTerm, $fulltext );
@@ -365,6 +366,7 @@ class SearchMySQL extends SearchEngine {
/**
* Converts some characters for MySQL's indexing to grok it correctly,
* and pads short words to overcome limitations.
+ * @return mixed|string
*/
function normalizeText( $string ) {
global $wgContLang;
@@ -412,6 +414,7 @@ class SearchMySQL extends SearchEngine {
* Armor a case-folded UTF-8 string to get through MySQL's
* fulltext search without being mucked up by funny charset
* settings or anything else of the sort.
+ * @return string
*/
protected function stripForSearchCallback( $matches ) {
return 'u8' . bin2hex( $matches[1] );
diff --git a/includes/search/SearchOracle.php b/includes/search/SearchOracle.php
index 2d6fc3e2..a2db52f3 100644
--- a/includes/search/SearchOracle.php
+++ b/includes/search/SearchOracle.php
@@ -124,7 +124,7 @@ class SearchOracle extends SearchEngine {
/**
* Return a LIMIT clause to limit results on the query.
*
- * @param string
+ * @param $sql string
*
* @return String
*/
@@ -147,6 +147,7 @@ class SearchOracle extends SearchEngine {
* 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) . ' ' .
@@ -184,6 +185,7 @@ class SearchOracle extends SearchEngine {
/**
* Parse a user input search string, and return an SQL fragment to be used
* as part of a WHERE clause
+ * @return string
*/
function parseQuery($filteredText, $fulltext) {
global $wgContLang;
diff --git a/includes/search/SearchPostgres.php b/includes/search/SearchPostgres.php
index cfe283b2..68648894 100644
--- a/includes/search/SearchPostgres.php
+++ b/includes/search/SearchPostgres.php
@@ -146,6 +146,7 @@ class SearchPostgres extends SearchEngine {
* @param $term String
* @param $fulltext String
* @param $colname
+ * @return string
*/
function searchQuery( $term, $fulltext, $colname ) {
# Get the SQL fragment for the given term
diff --git a/includes/search/SearchSqlite.php b/includes/search/SearchSqlite.php
index cd59eea9..e52e4fe3 100644
--- a/includes/search/SearchSqlite.php
+++ b/includes/search/SearchSqlite.php
@@ -238,6 +238,7 @@ class SearchSqlite extends SearchEngine {
* The guts shoulds be constructed in queryMain()
* @param $filteredTerm String
* @param $fulltext Boolean
+ * @return String
*/
function getQuery( $filteredTerm, $fulltext ) {
return $this->limitResult(
diff --git a/includes/search/SearchUpdate.php b/includes/search/SearchUpdate.php
index a162d2b3..40dd36c2 100644
--- a/includes/search/SearchUpdate.php
+++ b/includes/search/SearchUpdate.php
@@ -4,6 +4,21 @@
*
* See deferred.txt
*
+ * 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
*/
diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php
index 617a8026..c5aa2389 100644
--- a/includes/specials/SpecialActiveusers.php
+++ b/includes/specials/SpecialActiveusers.php
@@ -40,7 +40,12 @@ class ActiveUsersPager extends UsersPager {
/**
* @var Array
*/
- protected $groups;
+ protected $hideGroups = array();
+
+ /**
+ * @var Array
+ */
+ protected $hideRights = array();
/**
* @param $context IContextSource
@@ -73,12 +78,11 @@ class ActiveUsersPager extends UsersPager {
$this->opts->fetchValuesFromRequest( $this->getRequest() );
- $this->groups = array();
if ( $this->opts->getValue( 'hidebots' ) == 1 ) {
- $this->groups['bot'] = true;
+ $this->hideRights[] = 'bot';
}
if ( $this->opts->getValue( 'hidesysops' ) == 1 ) {
- $this->groups['sysop'] = true;
+ $this->hideGroups[] = 'sysop';
}
}
@@ -90,8 +94,8 @@ class ActiveUsersPager extends UsersPager {
$dbr = wfGetDB( DB_SLAVE );
$conds = array( 'rc_user > 0' ); // Users - no anons
$conds[] = 'ipb_deleted IS NULL'; // don't show hidden names
- $conds[] = "rc_log_type IS NULL OR rc_log_type != 'newusers'";
- $conds[] = "rc_timestamp >= '{$dbr->timestamp( wfTimestamp( TS_UNIX ) - $this->RCMaxAge*24*3600 )}'";
+ $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 ) );
if( $this->requestedUser != '' ) {
$conds[] = 'rc_user_text >= ' . $dbr->addQuotes( $this->requestedUser );
@@ -99,19 +103,23 @@ class ActiveUsersPager extends UsersPager {
$query = array(
'tables' => array( 'recentchanges', 'user', 'ipblocks' ),
- 'fields' => array( 'rc_user_text AS user_name', // inheritance
+ 'fields' => array( 'user_name' => 'rc_user_text', // inheritance
'rc_user_text', // for Pager
'user_id',
- 'COUNT(*) AS recentedits',
- 'MAX(ipb_user) AS blocked'
+ 'recentedits' => 'COUNT(*)',
+ 'blocked' => 'MAX(ipb_user)'
),
'options' => array(
- 'GROUP BY' => 'rc_user_text, user_id',
+ 'GROUP BY' => array( 'rc_user_text', 'user_id' ),
'USE INDEX' => array( 'recentchanges' => 'rc_user_text' )
),
'join_conds' => array(
'user' => array( 'INNER JOIN', 'rc_user_text=user_name' ),
- 'ipblocks' => array( 'LEFT JOIN', 'user_id=ipb_user AND ipb_auto=0 AND ipb_deleted=1' ),
+ 'ipblocks' => array( 'LEFT JOIN', array(
+ 'user_id=ipb_user',
+ 'ipb_auto' => 0,
+ 'ipb_deleted' => 1
+ )),
),
'conds' => $conds
);
@@ -127,12 +135,30 @@ class ActiveUsersPager extends UsersPager {
$lang = $this->getLanguage();
$list = array();
- foreach( self::getGroups( $row->user_id ) as $group ) {
- if ( isset( $this->groups[$group] ) ) {
- return;
+ $user = User::newFromId( $row->user_id );
+
+ // User right filter
+ 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
+ // User object
+ if ( in_array( $right, $user->getRights() ) ) {
+ return '';
+ }
+ }
+
+ // User group filter
+ // 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 ) {
+ if ( in_array( $group, $this->hideGroups ) ) {
+ return '';
}
$list[] = self::buildGroupLink( $group, $userName );
}
+
$groups = $lang->commaList( $list );
$item = $lang->specialList( $ulinks, $groups );
diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php
index 2bfea4c3..fe9d41e5 100644
--- a/includes/specials/SpecialAllmessages.php
+++ b/includes/specials/SpecialAllmessages.php
@@ -146,8 +146,9 @@ class AllmessagesTablePager extends TablePager {
function buildForm() {
global $wgScript;
- $languages = Language::getLanguageNames( false );
- ksort( $languages );
+ $attrs = array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' );
+ $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' ) ) .
Xml::fieldset( $this->msg( 'allmessages-filter-legend' )->text() ) .
@@ -187,18 +188,8 @@ class AllmessagesTablePager extends TablePager {
"</td>\n
</tr>
<tr>\n
- <td class=\"mw-label\">" .
- Xml::label( $this->msg( 'allmessages-language' )->text(), 'mw-allmessages-form-lang' ) .
- "</td>\n
- <td class=\"mw-input\">" .
- Xml::openElement( 'select', array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ) );
-
- foreach( $languages as $lang => $name ) {
- $selected = $lang == $this->langcode;
- $out .= Xml::option( $lang . ' - ' . $name, $lang, $selected ) . "\n";
- }
- $out .= Xml::closeElement( 'select' ) .
- "</td>\n
+ <td class=\"mw-label\">" . $langSelect[0] . "</td>\n
+ <td class=\"mw-input\">" . $langSelect[1] . "</td>\n
</tr>" .
'<tr>
@@ -290,6 +281,7 @@ 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.
+ * @return FakeResultWrapper
*/
function reallyDoQuery( $offset, $limit, $descending ) {
$result = new FakeResultWrapper( array() );
@@ -369,7 +361,7 @@ class AllmessagesTablePager extends TablePager {
array( 'broken' )
);
}
- return $title . ' (' . $talk . ')';
+ return $title . ' ' . $this->msg( 'parentheses' )->rawParams( $talk )->escaped();
case 'am_default' :
case 'am_actual' :
diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php
index 960a327a..0f8b2557 100644
--- a/includes/specials/SpecialAllpages.php
+++ b/includes/specials/SpecialAllpages.php
@@ -30,24 +30,37 @@ class SpecialAllpages extends IncludableSpecialPage {
/**
* Maximum number of pages to show on single subpage.
+ *
+ * @var int $maxPerPage
*/
protected $maxPerPage = 345;
/**
* Maximum number of pages to show on single index subpage.
+ *
+ * @var int $maxLineCount
*/
protected $maxLineCount = 100;
/**
* Maximum number of chars to show for an entry.
+ *
+ * @var int $maxPageLength
*/
protected $maxPageLength = 70;
/**
* Determines, which message describes the input field 'nsfrom'.
+ *
+ * @var string $nsfromMsg
*/
protected $nsfromMsg = 'allpagesfrom';
+ /**
+ * Constructor
+ *
+ * @param $name string: name of the special page, as seen in links and URLs (default: 'Allpages')
+ */
function __construct( $name = 'Allpages' ){
parent::__construct( $name );
}
@@ -70,6 +83,7 @@ class SpecialAllpages extends IncludableSpecialPage {
$from = $request->getVal( 'from', null );
$to = $request->getVal( 'to', null );
$namespace = $request->getInt( 'namespace' );
+ $hideredirects = $request->getBool( 'hideredirects', false );
$namespaces = $wgContLang->getNamespaces();
@@ -81,11 +95,11 @@ class SpecialAllpages extends IncludableSpecialPage {
$out->addModuleStyles( 'mediawiki.special' );
if( $par !== null ) {
- $this->showChunk( $namespace, $par, $to );
+ $this->showChunk( $namespace, $par, $to, $hideredirects );
} elseif( $from !== null && $to === null ) {
- $this->showChunk( $namespace, $from, $to );
+ $this->showChunk( $namespace, $from, $to, $hideredirects );
} else {
- $this->showToplevel( $namespace, $from, $to );
+ $this->showToplevel( $namespace, $from, $to, $hideredirects );
}
}
@@ -95,8 +109,10 @@ class SpecialAllpages extends IncludableSpecialPage {
* @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)
+ * @return string
*/
- function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '' ) {
+ function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) {
global $wgScript;
$t = $this->getTitle();
@@ -131,6 +147,12 @@ class SpecialAllpages extends IncludableSpecialPage {
array( 'selected' => $namespace ),
array( 'name' => 'namespace', 'id' => 'namespace' )
) . ' ' .
+ Xml::checkLabel(
+ $this->msg( 'allpages-hide-redirects' )->text(),
+ 'hideredirects',
+ 'hideredirects',
+ $hideredirects
+ ) . ' ' .
Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) .
" </td>
</tr>";
@@ -145,8 +167,9 @@ class SpecialAllpages extends IncludableSpecialPage {
* @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)
*/
- function showToplevel( $namespace = NS_MAIN, $from = '', $to = '' ) {
+ function showToplevel( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) {
$output = $this->getOutput();
# TODO: Either make this *much* faster or cache the title index points
@@ -156,6 +179,10 @@ class SpecialAllpages extends IncludableSpecialPage {
$out = "";
$where = array( 'page_namespace' => $namespace );
+ if ( $hideredirects ) {
+ $where[ 'page_is_redirect' ] = 0;
+ }
+
$from = Title::makeTitleSafe( $namespace, $from );
$to = Title::makeTitleSafe( $namespace, $to );
$from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null;
@@ -224,9 +251,9 @@ class SpecialAllpages extends IncludableSpecialPage {
// Instead, display the first section directly.
if( count( $lines ) <= 2 ) {
if( !empty($lines) ) {
- $this->showChunk( $namespace, $from, $to );
+ $this->showChunk( $namespace, $from, $to, $hideredirects );
} else {
- $output->addHTML( $this->namespaceForm( $namespace, $from, $to ) );
+ $output->addHTML( $this->namespaceForm( $namespace, $from, $to, $hideredirects ) );
}
return;
}
@@ -236,10 +263,10 @@ class SpecialAllpages extends IncludableSpecialPage {
while( count ( $lines ) > 0 ) {
$inpoint = array_shift( $lines );
$outpoint = array_shift( $lines );
- $out .= $this->showline( $inpoint, $outpoint, $namespace );
+ $out .= $this->showline( $inpoint, $outpoint, $namespace, $hideredirects );
}
$out .= Xml::closeElement( 'table' );
- $nsForm = $this->namespaceForm( $namespace, $from, $to );
+ $nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects );
# Is there more?
if( $this->including() ) {
@@ -270,8 +297,10 @@ class SpecialAllpages extends IncludableSpecialPage {
* @param $inpoint String: lower limit of pagenames
* @param $outpoint String: upper limit of pagenames
* @param $namespace Integer (Default NS_MAIN)
+ * @param $hideredirects Bool: dont show redirects (default FALSE)
+ * @return string
*/
- function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
+ function showline( $inpoint, $outpoint, $namespace = NS_MAIN, $hideredirects ) {
global $wgContLang;
$inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
$outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
@@ -280,8 +309,14 @@ class SpecialAllpages extends IncludableSpecialPage {
$outpointf = $wgContLang->truncate( $outpointf, $this->maxPageLength );
$queryparams = $namespace ? "namespace=$namespace&" : '';
+
+ $queryhideredirects = array();
+ if ($hideredirects) {
+ $queryhideredirects[ 'hideredirects' ] = 1;
+ }
+
$special = $this->getTitle();
- $link = htmlspecialchars( $special->getLocalUrl( $queryparams . 'from=' . urlencode($inpoint) . '&to=' . urlencode($outpoint) ) );
+ $link = htmlspecialchars( $special->getLocalUrl( $queryparams . 'from=' . urlencode($inpoint) . '&to=' . urlencode($outpoint), $queryhideredirects ) );
$out = $this->msg( 'alphaindexline' )->rawParams(
"<a href=\"$link\">$inpointf</a></td><td>",
@@ -294,8 +329,9 @@ class SpecialAllpages extends IncludableSpecialPage {
* @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)
*/
- function showChunk( $namespace = NS_MAIN, $from = false, $to = false ) {
+ function showChunk( $namespace = NS_MAIN, $from = false, $to = false, $hideredirects = false ) {
global $wgContLang;
$output = $this->getOutput();
@@ -319,6 +355,11 @@ class SpecialAllpages extends IncludableSpecialPage {
'page_namespace' => $namespace,
'page_title >= ' . $dbr->addQuotes( $fromKey )
);
+
+ if ( $hideredirects ) {
+ $conds[ 'page_is_redirect' ] = 0;
+ }
+
if( $toKey !== "" ) {
$conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey );
}
@@ -406,7 +447,7 @@ class SpecialAllpages extends IncludableSpecialPage {
$self = $this->getTitle();
- $nsForm = $this->namespaceForm( $namespace, $from, $to );
+ $nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects );
$out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ).
'<tr>
<td>' .
@@ -422,6 +463,9 @@ class SpecialAllpages extends IncludableSpecialPage {
if( $namespace )
$query['namespace'] = $namespace;
+ if( $hideredirects )
+ $query['hideredirects'] = $hideredirects;
+
$prevLink = Linker::linkKnown(
$self,
$this->msg( 'prevpage', $pt )->escaped(),
@@ -433,12 +477,15 @@ class SpecialAllpages extends IncludableSpecialPage {
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 )
$query['namespace'] = $namespace;
+ if( $hideredirects )
+ $query['hideredirects'] = $hideredirects;
+
$nextLink = Linker::linkKnown(
$self,
$this->msg( 'nextpage', $t->getText() )->escaped(),
diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php
index 1203e1fd..6e3d49bd 100644
--- a/includes/specials/SpecialAncientpages.php
+++ b/includes/specials/SpecialAncientpages.php
@@ -41,9 +41,9 @@ class AncientPagesPage extends QueryPage {
function getQueryInfo() {
return array(
'tables' => array( 'page', 'revision' ),
- 'fields' => array( 'page_namespace AS namespace',
- 'page_title AS title',
- 'rev_timestamp AS value' ),
+ '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' )
diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php
index da8eed1b..1d6656ab 100644
--- a/includes/specials/SpecialBlock.php
+++ b/includes/specials/SpecialBlock.php
@@ -106,9 +106,9 @@ class SpecialBlock extends FormSpecialPage {
$form->setSubmitTextMsg( $msg );
# Don't need to do anything if the form has been posted
- if( !$this->getRequest()->wasPosted() && $this->preErrors ){
+ if ( !$this->getRequest()->wasPosted() && $this->preErrors ) {
$s = HTMLForm::formatErrors( $this->preErrors );
- if( $s ){
+ if ( $s ) {
$form->addHeaderText( Html::rawElement(
'div',
array( 'class' => 'error' ),
@@ -122,7 +122,7 @@ class SpecialBlock extends FormSpecialPage {
* Get the HTMLForm descriptor array for the block form
* @return Array
*/
- protected function getFormFields(){
+ protected function getFormFields() {
global $wgBlockAllowsUTEdit;
$user = $this->getUser();
@@ -144,6 +144,7 @@ class SpecialBlock extends FormSpecialPage {
'tabindex' => '2',
'options' => self::getSuggestedDurations(),
'other' => $this->msg( 'ipbother' )->text(),
+ 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(),
),
'Reason' => array(
'type' => 'selectandother',
@@ -157,14 +158,14 @@ class SpecialBlock extends FormSpecialPage {
),
);
- if( self::canBlockEmail( $user ) ) {
+ if ( self::canBlockEmail( $user ) ) {
$a['DisableEmail'] = array(
'type' => 'check',
'label-message' => 'ipbemailban',
);
}
- if( $wgBlockAllowsUTEdit ){
+ if ( $wgBlockAllowsUTEdit ) {
$a['DisableUTEdit'] = array(
'type' => 'check',
'label-message' => 'ipb-disableusertalk',
@@ -179,7 +180,7 @@ class SpecialBlock extends FormSpecialPage {
);
# Allow some users to hide name from block log, blocklist and listusers
- if( $user->isAllowed( 'hideuser' ) ) {
+ if ( $user->isAllowed( 'hideuser' ) ) {
$a['HideUser'] = array(
'type' => 'check',
'label-message' => 'ipbhidename',
@@ -188,7 +189,7 @@ class SpecialBlock extends FormSpecialPage {
}
# Watchlist their user page? (Only if user is logged in)
- if( $user->isLoggedIn() ) {
+ if ( $user->isLoggedIn() ) {
$a['Watch'] = array(
'type' => 'check',
'label-message' => 'ipbwatchuser',
@@ -227,7 +228,7 @@ class SpecialBlock extends FormSpecialPage {
* @return Bool whether fields were altered (that is, whether the target is
* already blocked)
*/
- protected function maybeAlterFormDefaults( &$fields ){
+ protected function maybeAlterFormDefaults( &$fields ) {
# This will be overwritten by request data
$fields['Target']['default'] = (string)$this->target;
@@ -236,7 +237,7 @@ class SpecialBlock extends FormSpecialPage {
$block = Block::newFromTarget( $this->target );
- if( $block instanceof Block && !$block->mAuto # The block exists and isn't an autoblock
+ 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
)
@@ -245,15 +246,15 @@ class SpecialBlock extends FormSpecialPage {
$fields['CreateAccount']['default'] = $block->prevents( 'createaccount' );
$fields['AutoBlock']['default'] = $block->isAutoblocking();
- if( isset( $fields['DisableEmail'] ) ){
+ if ( isset( $fields['DisableEmail'] ) ) {
$fields['DisableEmail']['default'] = $block->prevents( 'sendemail' );
}
- if( isset( $fields['HideUser'] ) ){
+ if ( isset( $fields['HideUser'] ) ) {
$fields['HideUser']['default'] = $block->mHideName;
}
- if( isset( $fields['DisableUTEdit'] ) ){
+ if ( isset( $fields['DisableUTEdit'] ) ) {
$fields['DisableUTEdit']['default'] = $block->prevents( 'editownusertalk' );
}
@@ -265,7 +266,7 @@ class SpecialBlock extends FormSpecialPage {
$fields['Reason']['default'] = '';
}
- if( $this->getRequest()->wasPosted() ){
+ if ( $this->getRequest()->wasPosted() ) {
# Ok, so we got a POST submission asking us to reblock a user. So show the
# confirm checkbox; the user will only see it if they haven't previously
$fields['Confirm']['type'] = 'check';
@@ -276,25 +277,25 @@ class SpecialBlock extends FormSpecialPage {
$fields['Confirm']['default'] = 1;
}
- if( $block->mExpiry == 'infinity' ) {
- $fields['Expiry']['default'] = 'indefinite';
+ if ( $block->mExpiry == 'infinity' ) {
+ $fields['Expiry']['default'] = 'infinite';
} else {
$fields['Expiry']['default'] = wfTimestamp( TS_RFC2822, $block->mExpiry );
}
$this->alreadyBlocked = true;
- $this->preErrors[] = array( 'ipb-needreblock', (string)$block->getTarget() );
+ $this->preErrors[] = array( 'ipb-needreblock', wfEscapeWikiText( (string)$block->getTarget() ) );
}
# We always need confirmation to do HideUser
- if( $this->requestedHideUser ){
+ if ( $this->requestedHideUser ) {
$fields['Confirm']['type'] = 'check';
unset( $fields['Confirm']['default'] );
$this->preErrors[] = 'ipb-confirmhideuser';
}
# Or if the user is trying to block themselves
- if( (string)$this->target === $this->getUser()->getName() ){
+ if ( (string)$this->target === $this->getUser()->getName() ) {
$fields['Confirm']['type'] = 'check';
unset( $fields['Confirm']['default'] );
$this->preErrors[] = 'ipb-blockingself';
@@ -303,16 +304,19 @@ class SpecialBlock extends FormSpecialPage {
/**
* Add header elements like block log entries, etc.
+ * @return String
*/
- protected function preText(){
+ protected function preText() {
+ $this->getOutput()->addModules( 'mediawiki.special.block' );
+
$text = $this->msg( 'blockiptext' )->parse();
$otherBlockMessages = array();
- if( $this->target !== null ) {
+ if ( $this->target !== null ) {
# Get other blocks, i.e. from GlobalBlocking or TorBlock extension
wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockMessages, $this->target ) );
- if( count( $otherBlockMessages ) ) {
+ if ( count( $otherBlockMessages ) ) {
$s = Html::rawElement(
'h2',
array(),
@@ -321,7 +325,7 @@ class SpecialBlock extends FormSpecialPage {
$list = '';
- foreach( $otherBlockMessages as $link ) {
+ foreach ( $otherBlockMessages as $link ) {
$list .= Html::rawElement( 'li', array(), $link ) . "\n";
}
@@ -342,9 +346,11 @@ class SpecialBlock extends FormSpecialPage {
* Add footer elements to the form
* @return string
*/
- protected function postText(){
+ protected function postText() {
+ $links = array();
+
# Link to the user's contributions, if applicable
- if( $this->target instanceof User ){
+ if ( $this->target instanceof User ) {
$contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->target->getName() );
$links[] = Linker::link(
$contribsPage,
@@ -353,8 +359,8 @@ class SpecialBlock extends FormSpecialPage {
}
# Link to unblock the specified user, or to a blank unblock form
- if( $this->target instanceof User ) {
- $message = $this->msg( 'ipb-unblock-addr', $this->target->getName() )->parse();
+ if ( $this->target instanceof User ) {
+ $message = $this->msg( 'ipb-unblock-addr', wfEscapeWikiText( $this->target->getName() ) )->parse();
$list = SpecialPage::getTitleFor( 'Unblock', $this->target->getName() );
} else {
$message = $this->msg( 'ipb-unblock' )->parse();
@@ -386,35 +392,35 @@ class SpecialBlock extends FormSpecialPage {
$this->getLanguage()->pipeList( $links )
);
- if( $this->target instanceof User ){
+ $userTitle = self::getTargetUserTitle( $this->target );
+ if ( $userTitle ) {
# Get relevant extracts from the block and suppression logs, if possible
- $userpage = $this->target->getUserPage();
$out = '';
LogEventsList::showLogExtract(
$out,
'block',
- $userpage,
+ $userTitle,
'',
array(
'lim' => 10,
- 'msgKey' => array( 'blocklog-showlog', $userpage->getText() ),
+ 'msgKey' => array( 'blocklog-showlog', $userTitle->getText() ),
'showIfEmpty' => false
)
);
$text .= $out;
# Add suppression block entries if allowed
- if( $user->isAllowed( 'suppressionlog' ) ) {
+ if ( $user->isAllowed( 'suppressionlog' ) ) {
LogEventsList::showLogExtract(
$out,
'suppress',
- $userpage,
+ $userTitle,
'',
array(
'lim' => 10,
'conds' => array( 'log_action' => array( 'block', 'reblock', 'unblock' ) ),
- 'msgKey' => array( 'blocklog-showsuppresslog', $userpage->getText() ),
+ 'msgKey' => array( 'blocklog-showsuppresslog', $userTitle->getText() ),
'showIfEmpty' => false
)
);
@@ -427,6 +433,21 @@ class SpecialBlock extends FormSpecialPage {
}
/**
+ * Get a user page target for things like logs.
+ * This handles account and IP range targets.
+ * @param $target User|string
+ * @return Title|null
+ */
+ protected static function getTargetUserTitle( $target ) {
+ if ( $target instanceof User ) {
+ return $target->getUserPage();
+ } 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
@@ -434,18 +455,18 @@ class SpecialBlock extends FormSpecialPage {
* @param $request WebRequest optionally try and get data from a request too
* @return array( User|string|null, Block::TYPE_ constant|null )
*/
- public static function getTargetAndType( $par, WebRequest $request = null ){
+ public static function getTargetAndType( $par, WebRequest $request = null ) {
$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
# below; so this has to have a higher precedence here than $par, or
# we could end up with different values in $this->target and the HTMLForm!
- if( $request instanceof WebRequest ){
+ if ( $request instanceof WebRequest ) {
$target = $request->getText( 'wpTarget', null );
}
break;
@@ -453,13 +474,13 @@ class SpecialBlock extends FormSpecialPage {
$target = $par;
break;
case 2:
- if( $request instanceof WebRequest ){
+ if ( $request instanceof WebRequest ) {
$target = $request->getText( 'ip', null );
}
break;
case 3:
# B/C @since 1.18
- if( $request instanceof WebRequest ){
+ if ( $request instanceof WebRequest ) {
$target = $request->getText( 'wpBlockAddress', null );
}
break;
@@ -469,7 +490,7 @@ class SpecialBlock extends FormSpecialPage {
list( $target, $type ) = Block::parseTarget( $target );
- if( $type !== null ){
+ if ( $type !== null ) {
return array( $target, $type );
}
}
@@ -490,9 +511,9 @@ class SpecialBlock extends FormSpecialPage {
list( $target, $type ) = self::getTargetAndType( $value );
- if( $type == Block::TYPE_USER ){
+ if ( $type == Block::TYPE_USER ) {
# TODO: why do we not have a User->exists() method?
- if( !$target->getId() ){
+ if ( !$target->getId() ) {
return $form->msg( 'nosuchusershort',
wfEscapeWikiText( $target->getName() ) );
}
@@ -502,31 +523,31 @@ class SpecialBlock extends FormSpecialPage {
return $form->msg( 'badaccess', $status );
}
- } elseif( $type == Block::TYPE_RANGE ){
+ } elseif ( $type == Block::TYPE_RANGE ) {
list( $ip, $range ) = explode( '/', $target, 2 );
- if( ( IP::isIPv4( $ip ) && $wgBlockCIDRLimit['IPv4'] == 32 )
+ 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 ) && $range > 32 )
+ if ( ( IP::isIPv4( $ip ) && $range > 32 )
|| ( IP::isIPv6( $ip ) && $range > 128 ) )
{
# Dodgy range
return $form->msg( 'ip_range_invalid' );
}
- if( IP::isIPv4( $ip ) && $range < $wgBlockCIDRLimit['IPv4'] ) {
+ if ( IP::isIPv4( $ip ) && $range < $wgBlockCIDRLimit['IPv4'] ) {
return $form->msg( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv4'] );
}
- if( IP::isIPv6( $ip ) && $range < $wgBlockCIDRLimit['IPv6'] ) {
+ if ( IP::isIPv6( $ip ) && $range < $wgBlockCIDRLimit['IPv6'] ) {
return $form->msg( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv6'] );
}
- } elseif( $type == Block::TYPE_IP ){
+ } elseif ( $type == Block::TYPE_IP ) {
# All is well
} else {
return $form->msg( 'badipaddress' );
@@ -551,7 +572,7 @@ class SpecialBlock extends FormSpecialPage {
* @param $context IContextSource
* @return Bool|String
*/
- public static function processForm( array $data, IContextSource $context ){
+ public static function processForm( array $data, IContextSource $context ) {
global $wgBlockAllowsUTEdit;
$performer = $context->getUser();
@@ -564,7 +585,7 @@ class SpecialBlock extends FormSpecialPage {
$data['Confirm'] = !in_array( $data['Confirm'], array( '', '0', null, false ), true );
list( $target, $type ) = self::getTargetAndType( $data['Target'] );
- if( $type == Block::TYPE_USER ){
+ if ( $type == Block::TYPE_USER ) {
$user = $target;
$target = $user->getName();
$userId = $user->getId();
@@ -576,14 +597,14 @@ class SpecialBlock extends FormSpecialPage {
# since both $data['PreviousTarget'] and $target are normalized
# but $data['target'] gets overriden by (non-normalized) request variable
# from previous request.
- if( $target === $performer->getName() &&
+ if ( $target === $performer->getName() &&
( $data['PreviousTarget'] !== $target || !$data['Confirm'] ) )
{
return array( 'ipb-blockingself' );
}
- } elseif( $type == Block::TYPE_RANGE ){
+ } elseif ( $type == Block::TYPE_RANGE ) {
$userId = 0;
- } elseif( $type == Block::TYPE_IP ){
+ } elseif ( $type == Block::TYPE_IP ) {
$target = $target->getName();
$userId = 0;
} else {
@@ -591,24 +612,24 @@ class SpecialBlock extends FormSpecialPage {
return array( 'badipaddress' );
}
- if( ( strlen( $data['Expiry'] ) == 0) || ( strlen( $data['Expiry'] ) > 50 )
+ if ( ( strlen( $data['Expiry'] ) == 0) || ( strlen( $data['Expiry'] ) > 50 )
|| !self::parseExpiryInput( $data['Expiry'] ) )
{
return array( 'ipb_expiry_invalid' );
}
- if( !isset( $data['DisableEmail'] ) ){
+ if ( !isset( $data['DisableEmail'] ) ) {
$data['DisableEmail'] = false;
}
# If the user has done the form 'properly', they won't even have been given the
# option to suppress-block unless they have the 'hideuser' permission
- if( !isset( $data['HideUser'] ) ){
+ if ( !isset( $data['HideUser'] ) ) {
$data['HideUser'] = false;
}
- if( $data['HideUser'] ) {
- if( !$performer->isAllowed('hideuser') ){
+ if ( $data['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
@@ -617,16 +638,16 @@ class SpecialBlock extends FormSpecialPage {
}
# Recheck params here...
- if( $type != Block::TYPE_USER ) {
+ if ( $type != Block::TYPE_USER ) {
$data['HideUser'] = false; # IP users should not be hidden
- } elseif( !in_array( $data['Expiry'], array( 'infinite', 'infinity', 'indefinite' ) ) ) {
+ } elseif ( !in_array( $data['Expiry'], array( 'infinite', 'infinity', 'indefinite' ) ) ) {
# Bad expiry.
return array( 'ipb_expiry_temp' );
- } elseif( $user->getEditCount() > self::HIDEUSER_CONTRIBLIMIT ) {
+ } elseif ( $user->getEditCount() > self::HIDEUSER_CONTRIBLIMIT ) {
# Typically, the user should have a handful of edits.
# Disallow hiding users with many edits for performance.
return array( 'ipb_hide_invalid' );
- } elseif( !$data['Confirm'] ){
+ } elseif ( !$data['Confirm'] ) {
return array( 'ipb-confirmhideuser' );
}
}
@@ -644,15 +665,15 @@ class SpecialBlock extends FormSpecialPage {
$block->isAutoblocking( $data['AutoBlock'] );
$block->mHideName = $data['HideUser'];
- if( !wfRunHooks( 'BlockIp', array( &$block, &$performer ) ) ) {
+ if ( !wfRunHooks( 'BlockIp', array( &$block, &$performer ) ) ) {
return array( 'hookaborted' );
}
# Try to insert block. Is there a conflicting block?
$status = $block->insert();
- if( !$status ) {
+ if ( !$status ) {
# Show form unless the user is already aware of this...
- if( !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data )
+ if ( !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data )
&& $data['PreviousTarget'] !== $target ) )
{
return array( array( 'ipb_already_blocked', $block->getTarget() ) );
@@ -662,13 +683,13 @@ class SpecialBlock extends FormSpecialPage {
# be sure the user is blocked by now it should work for our purposes
$currentBlock = Block::newFromTarget( $target );
- if( $block->equals( $currentBlock ) ) {
+ if ( $block->equals( $currentBlock ) ) {
return array( array( 'ipb_already_blocked', $block->getTarget() ) );
}
# If the name was hidden and the blocking user cannot hide
# names, then don't allow any block changes...
- if( $currentBlock->mHideName && !$performer->isAllowed( 'hideuser' ) ) {
+ if ( $currentBlock->mHideName && !$performer->isAllowed( 'hideuser' ) ) {
return array( 'cant-see-hidden-user' );
}
@@ -677,12 +698,12 @@ class SpecialBlock extends FormSpecialPage {
$logaction = 'reblock';
# Unset _deleted fields if requested
- if( $currentBlock->mHideName && !$data['HideUser'] ) {
+ if ( $currentBlock->mHideName && !$data['HideUser'] ) {
RevisionDeleteUser::unsuppressUserName( $target, $userId );
}
# If hiding/unhiding a name, this should go in the private logs
- if( (bool)$currentBlock->mHideName ){
+ if ( (bool)$currentBlock->mHideName ) {
$data['HideUser'] = true;
}
}
@@ -693,12 +714,12 @@ class SpecialBlock extends FormSpecialPage {
wfRunHooks( 'BlockIpComplete', array( $block, $performer ) );
# Set *_deleted fields if requested
- if( $data['HideUser'] ) {
+ if ( $data['HideUser'] ) {
RevisionDeleteUser::suppressUserName( $target, $userId );
}
# Can't watch a rangeblock
- if( $type != Block::TYPE_RANGE && $data['Watch'] ) {
+ if ( $type != Block::TYPE_RANGE && $data['Watch'] ) {
$performer->addWatch( Title::makeTitle( NS_USER, $target ) );
}
@@ -736,18 +757,18 @@ class SpecialBlock extends FormSpecialPage {
* the wiki's content language
* @return Array
*/
- public static function getSuggestedDurations( $lang = null ){
+ public static function getSuggestedDurations( $lang = null ) {
$a = array();
$msg = $lang === null
? wfMessage( 'ipboptions' )->inContentLanguage()->text()
: wfMessage( 'ipboptions' )->inLanguage( $lang )->text();
- if( $msg == '-' ){
+ if ( $msg == '-' ) {
return array();
}
- foreach( explode( ',', $msg ) as $option ) {
- if( strpos( $option, ':' ) === false ){
+ foreach ( explode( ',', $msg ) as $option ) {
+ if ( strpos( $option, ':' ) === false ) {
$option = "$option:$option";
}
@@ -766,7 +787,7 @@ class SpecialBlock extends FormSpecialPage {
*/
public static function parseExpiryInput( $expiry ) {
static $infinity;
- if( $infinity == null ){
+ if ( $infinity == null ) {
$infinity = wfGetDB( DB_SLAVE )->getInfinity();
}
@@ -811,8 +832,8 @@ class SpecialBlock extends FormSpecialPage {
$user = User::newFromName( $user );
}
- if( $performer->isBlocked() ){
- if( $user instanceof User && $user->getId() == $performer->getId() ) {
+ if ( $performer->isBlocked() ) {
+ if ( $user instanceof User && $user->getId() == $performer->getId() ) {
# User is trying to unblock themselves
if ( $performer->isAllowed( 'unblockself' ) ) {
return true;
@@ -836,40 +857,41 @@ class SpecialBlock extends FormSpecialPage {
* reader for this block, to provide more information in the logs
* @param $data Array from HTMLForm data
* @param $type Block::TYPE_ constant (USER, RANGE, or IP)
- * @return array
+ * @return string
*/
protected static function blockLogFlags( array $data, $type ) {
global $wgBlockAllowsUTEdit;
$flags = array();
- # when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log
- if( !$data['HardBlock'] && $type != Block::TYPE_USER ){
+ # when blocking a user the option 'anononly' is not available/has no effect
+ # -> do not write this into log
+ if ( !$data['HardBlock'] && $type != Block::TYPE_USER ) {
// For grepping: message block-log-flags-anononly
$flags[] = 'anononly';
}
- if( $data['CreateAccount'] ){
+ if ( $data['CreateAccount'] ) {
// For grepping: message block-log-flags-nocreate
$flags[] = 'nocreate';
}
# Same as anononly, this is not displayed when blocking an IP address
- if( !$data['AutoBlock'] && $type == Block::TYPE_USER ){
+ if ( !$data['AutoBlock'] && $type == Block::TYPE_USER ) {
// For grepping: message block-log-flags-noautoblock
$flags[] = 'noautoblock';
}
- if( $data['DisableEmail'] ){
+ if ( $data['DisableEmail'] ) {
// For grepping: message block-log-flags-noemail
$flags[] = 'noemail';
}
- if( $wgBlockAllowsUTEdit && $data['DisableUTEdit'] ){
+ if ( $wgBlockAllowsUTEdit && $data['DisableUTEdit'] ) {
// For grepping: message block-log-flags-nousertalk
$flags[] = 'nousertalk';
}
- if( $data['HideUser'] ){
+ if ( $data['HideUser'] ) {
// For grepping: message block-log-flags-hiddenname
$flags[] = 'hiddenname';
}
@@ -894,7 +916,7 @@ class SpecialBlock extends FormSpecialPage {
public function onSuccess() {
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'blockipsuccesssub' ) );
- $out->addWikiMsg( 'blockipsuccesstext', $this->target );
+ $out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( $this->target ) );
}
}
diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php
index 0a3a28fe..7143d5bc 100644
--- a/includes/specials/SpecialBlockList.php
+++ b/includes/specials/SpecialBlockList.php
@@ -372,7 +372,7 @@ class BlockListPager extends TablePager {
'ipb_user',
'ipb_by',
'ipb_by_text',
- 'user_name AS by_user_name',
+ 'by_user_name' => 'user_name',
'ipb_reason',
'ipb_timestamp',
'ipb_auto',
diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php
index 48ca4f05..bf7de3f5 100644
--- a/includes/specials/SpecialBooksources.php
+++ b/includes/specials/SpecialBooksources.php
@@ -46,7 +46,7 @@ class SpecialBookSources extends SpecialPage {
/**
* Show the special page
*
- * @param $isbn ISBN passed as a subpage parameter
+ * @param $isbn string ISBN passed as a subpage parameter
*/
public function execute( $isbn ) {
$this->setHeaders();
@@ -63,7 +63,8 @@ class SpecialBookSources extends SpecialPage {
/**
* Returns whether a given ISBN (10 or 13) is valid. True indicates validity.
- * @param isbn ISBN passed for check
+ * @param isbn string ISBN passed for check
+ * @return bool
*/
public static function isValidISBN( $isbn ) {
$isbn = self::cleanIsbn( $isbn );
@@ -100,7 +101,7 @@ class SpecialBookSources extends SpecialPage {
/**
* Trim ISBN and remove characters which aren't required
*
- * @param $isbn Unclean ISBN
+ * @param $isbn string Unclean ISBN
* @return string
*/
private static function cleanIsbn( $isbn ) {
@@ -142,7 +143,7 @@ class SpecialBookSources extends SpecialPage {
$page = $this->msg( 'booksources' )->inContentLanguage()->text();
$title = Title::makeTitleSafe( NS_PROJECT, $page ); # Show list in content language
if( is_object( $title ) && $title->exists() ) {
- $rev = Revision::newFromTitle( $title );
+ $rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
$this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
return true;
}
@@ -160,8 +161,8 @@ class SpecialBookSources extends SpecialPage {
/**
* Format a book source list item
*
- * @param $label Book source label
- * @param $url Book source URL
+ * @param $label string Book source label
+ * @param $url string Book source URL
* @return string
*/
private function makeListItem( $label, $url ) {
diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php
index b8dbe9e8..8119e6d1 100644
--- a/includes/specials/SpecialBrokenRedirects.php
+++ b/includes/specials/SpecialBrokenRedirects.php
@@ -27,7 +27,7 @@
*
* @ingroup SpecialPage
*/
-class BrokenRedirectsPage extends PageQueryPage {
+class BrokenRedirectsPage extends QueryPage {
function __construct( $name = 'BrokenRedirects' ) {
parent::__construct( $name );
@@ -45,9 +45,9 @@ class BrokenRedirectsPage extends PageQueryPage {
return array(
'tables' => array( 'redirect', 'p1' => 'page',
'p2' => 'page' ),
- 'fields' => array( 'p1.page_namespace AS namespace',
- 'p1.page_title AS title',
- 'p1.page_title AS value',
+ 'fields' => array( 'namespace' => 'p1.page_namespace',
+ 'title' => 'p1.page_title',
+ 'value' => 'p1.page_title',
'rd_namespace',
'rd_title'
),
diff --git a/includes/specials/SpecialCachedPage.php b/includes/specials/SpecialCachedPage.php
new file mode 100644
index 00000000..b3f6c720
--- /dev/null
+++ b/includes/specials/SpecialCachedPage.php
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * Abstract special page 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
+ * (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 Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @since 1.20
+ */
+abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper {
+
+ /**
+ * CacheHelper object to which we forward the non-SpecialPage specific caching work.
+ * Initialized in startCache.
+ *
+ * @since 1.20
+ * @var CacheHelper
+ */
+ protected $cacheHelper;
+
+ /**
+ * If the cache is enabled or not.
+ *
+ * @since 1.20
+ * @var boolean
+ */
+ protected $cacheEnabled = true;
+
+ /**
+ * Gets called after @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param $subPage string|null
+ */
+ protected function afterExecute( $subPage ) {
+ $this->saveCache();
+
+ parent::afterExecute( $subPage );
+ }
+
+ /**
+ * Sets if the cache should be enabled or not.
+ *
+ * @since 1.20
+ * @param boolean $cacheEnabled
+ */
+ public function setCacheEnabled( $cacheEnabled ) {
+ $this->cacheHelper->setCacheEnabled( $cacheEnabled );
+ }
+
+ /**
+ * Initializes the caching.
+ * Should be called before the first time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ *
+ * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp.
+ * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not.
+ */
+ public function startCache( $cacheExpiry = null, $cacheEnabled = null ) {
+ if ( !isset( $this->cacheHelper ) ) {
+ $this->cacheHelper = new CacheHelper();
+
+ $this->cacheHelper->setCacheEnabled( $this->cacheEnabled );
+ $this->cacheHelper->setOnInitializedHandler( array( $this, 'onCacheInitialized' ) );
+
+ $keyArgs = $this->getCacheKey();
+
+ if ( array_key_exists( 'action', $keyArgs ) && $keyArgs['action'] === 'purge' ) {
+ unset( $keyArgs['action'] );
+ }
+
+ $this->cacheHelper->setCacheKey( $keyArgs );
+
+ if ( $this->getRequest()->getText( 'action' ) === 'purge' ) {
+ $this->cacheHelper->rebuildOnDemand();
+ }
+ }
+
+ $this->cacheHelper->startCache( $cacheExpiry, $cacheEnabled );
+ }
+
+ /**
+ * Get a cached value if available or compute it if not and then cache it if possible.
+ * The provided $computeFunction is only called when the computation needs to happen
+ * and should return a result value. $args are arguments that will be passed to the
+ * compute function when called.
+ *
+ * @since 1.20
+ *
+ * @param {function} $computeFunction
+ * @param array|mixed $args
+ * @param string|null $key
+ *
+ * @return mixed
+ */
+ public function getCachedValue( $computeFunction, $args = array(), $key = null ) {
+ return $this->cacheHelper->getCachedValue( $computeFunction, $args, $key );
+ }
+
+ /**
+ * Add some HTML to be cached.
+ * This is done by providing a callback function that should
+ * return the HTML to be added. It will only be called if the
+ * item is not in the cache yet or when the cache has been invalidated.
+ *
+ * @since 1.20
+ *
+ * @param {function} $computeFunction
+ * @param array $args
+ * @param string|null $key
+ */
+ public function addCachedHTML( $computeFunction, $args = array(), $key = null ) {
+ $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ) );
+ }
+
+ /**
+ * Saves the HTML to the cache in case it got recomputed.
+ * Should be called after the last time anything is added via addCachedHTML.
+ *
+ * @since 1.20
+ */
+ public function saveCache() {
+ if ( isset( $this->cacheHelper ) ) {
+ $this->cacheHelper->saveCache();
+ }
+ }
+
+ /**
+ * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry.
+ *
+ * @since 1.20
+ *
+ * @param integer $cacheExpiry
+ */
+ public function setExpiry( $cacheExpiry ) {
+ $this->cacheHelper->setExpiry( $cacheExpiry );
+ }
+
+ /**
+ * Returns the variables used to constructed the cache key in an array.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ protected function getCacheKey() {
+ return array(
+ $this->mName,
+ $this->getLanguage()->getCode()
+ );
+ }
+
+ /**
+ * Gets called after the cache got initialized.
+ *
+ * @since 1.20
+ *
+ * @param boolean $hasCached
+ */
+ public function onCacheInitialized( $hasCached ) {
+ if ( $hasCached ) {
+ $this->getOutput()->setSubtitle( $this->cacheHelper->getCachedNotice( $this->getContext() ) );
+ }
+ }
+
+}
diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php
index 338cd706..1232e3fa 100644
--- a/includes/specials/SpecialCategories.php
+++ b/includes/specials/SpecialCategories.php
@@ -59,16 +59,13 @@ class SpecialCategories extends SpecialPage {
* @ingroup SpecialPage Pager
*/
class CategoryPager extends AlphabeticPager {
- private $conds = array( 'cat_pages > 0' );
-
function __construct( IContextSource $context, $from ) {
parent::__construct( $context );
$from = str_replace( ' ', '_', $from );
if( $from !== '' ) {
$from = Title::capitalize( $from, NS_CATEGORY );
- $dbr = wfGetDB( DB_SLAVE );
- $this->conds[] = 'cat_title >= ' . $dbr->addQuotes( $from );
- $this->setOffset( '' );
+ $this->setOffset( $from );
+ $this->setIncludeOffset( true );
}
}
@@ -76,7 +73,7 @@ class CategoryPager extends AlphabeticPager {
return array(
'tables' => array( 'category' ),
'fields' => array( 'cat_title','cat_pages' ),
- 'conds' => $this->conds,
+ 'conds' => array( 'cat_pages > 0' ),
'options' => array( 'USE INDEX' => 'cat_title' ),
);
}
diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php
index 0f85f516..fc726106 100644
--- a/includes/specials/SpecialChangeEmail.php
+++ b/includes/specials/SpecialChangeEmail.php
@@ -27,10 +27,26 @@
* @ingroup SpecialPage
*/
class SpecialChangeEmail extends UnlistedSpecialPage {
+
+ /**
+ * Users password
+ * @var string
+ */
+ protected $mPassword;
+
+ /**
+ * Users new email address
+ * @var string
+ */
+ protected $mNewEmail;
+
public function __construct() {
parent::__construct( 'ChangeEmail' );
}
+ /**
+ * @return Bool
+ */
function isListed() {
global $wgAuth;
return $wgAuth->allowPropChange( 'emailaddress' );
@@ -42,11 +58,13 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
function execute( $par ) {
global $wgAuth;
- $this->checkReadOnly();
-
$this->setHeaders();
$this->outputHeader();
+ $out = $this->getOutput();
+ $out->disallowUserJs();
+ $out->addModules( 'mediawiki.special.changeemail' );
+
if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) {
$this->error( 'cannotchangeemail' );
return;
@@ -65,9 +83,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
return;
}
- $out = $this->getOutput();
- $out->disallowUserJs();
- $out->addModules( 'mediawiki.special.changeemail' );
+ $this->checkReadOnly();
$this->mPassword = $request->getVal( 'wpPassword' );
$this->mNewEmail = $request->getVal( 'wpNewEmail' );
@@ -90,6 +106,9 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
$this->showForm();
}
+ /**
+ * @param $type string
+ */
protected function doReturnTo( $type = 'hard' ) {
$titleObj = Title::newFromText( $this->getRequest()->getVal( 'returnto' ) );
if ( !$titleObj instanceof Title ) {
@@ -102,11 +121,15 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
}
}
+ /**
+ * @param $msg string
+ */
protected function error( $msg ) {
$this->getOutput()->wrapWikiMsg( "<p class='error'>\n$1\n</p>", $msg );
}
protected function showForm() {
+ global $wgRequirePasswordforEmailChange;
$user = $this->getUser();
$oldEmailText = $user->getEmail()
@@ -123,13 +146,20 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
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" .
- $this->pretty( array(
- array( 'wpName', 'username', 'text', $user->getName() ),
- array( 'wpOldEmail', 'changeemail-oldemail', 'text', $oldEmailText ),
- array( 'wpNewEmail', 'changeemail-newemail', 'input', $this->mNewEmail ),
- array( 'wpPassword', 'yourpassword', 'password', $this->mPassword ),
- ) ) . "\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 ),
+ );
+ if ( $wgRequirePasswordforEmailChange ) {
+ $items[] = array( 'wpPassword', 'yourpassword', 'password', $this->mPassword );
+ }
+
+ $this->getOutput()->addHTML(
+ $this->pretty( $items ) .
+ "\n" .
"<tr>\n" .
"<td></td>\n" .
'<td class="mw-input">' .
@@ -143,6 +173,10 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
);
}
+ /**
+ * @param $fields array
+ * @return string
+ */
protected function pretty( $fields ) {
$out = '';
foreach ( $fields as $list ) {
@@ -173,6 +207,9 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
}
/**
+ * @param $user User
+ * @param $pass string
+ * @param $newaddr string
* @return bool|string true or string on success, false on failure
*/
protected function attemptChange( User $user, $pass, $newaddr ) {
@@ -187,7 +224,8 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
return false;
}
- if ( !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) {
+ global $wgRequirePasswordforEmailChange;
+ if ( $wgRequirePasswordforEmailChange && !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) {
$this->error( 'wrongpassword' );
return false;
}
@@ -196,18 +234,20 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
LoginForm::clearLoginThrottle( $user->getName() );
}
- list( $status, $info ) = Preferences::trySetUserEmail( $user, $newaddr );
- if ( $status !== true ) {
- if ( $status instanceof Status ) {
- $this->getOutput()->addHTML(
- '<p class="error">' .
- $this->getOutput()->parseInline( $status->getWikiText( $info ) ) .
- '</p>' );
- }
+ $oldaddr = $user->getEmail();
+ $status = $user->setEmailWithConfirmation( $newaddr );
+ if ( !$status->isGood() ) {
+ $this->getOutput()->addHTML(
+ '<p class="error">' .
+ $this->getOutput()->parseInline( $status->getWikiText( 'mailerror' ) ) .
+ '</p>' );
return false;
}
+ wfRunHooks( 'PrefsEmailAudit', array( $user, $oldaddr, $newaddr ) );
+
$user->saveSettings();
- return $info ? $info : true;
+
+ return $status->value;
}
}
diff --git a/includes/specials/SpecialChangePassword.php b/includes/specials/SpecialChangePassword.php
index f6482ef5..41b3b255 100644
--- a/includes/specials/SpecialChangePassword.php
+++ b/includes/specials/SpecialChangePassword.php
@@ -37,7 +37,9 @@ class SpecialChangePassword extends UnlistedSpecialPage {
function execute( $par ) {
global $wgAuth;
- $this->checkReadOnly();
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->getOutput()->disallowUserJs();
$request = $this->getRequest();
$this->mUserName = trim( $request->getVal( 'wpName' ) );
@@ -46,10 +48,6 @@ class SpecialChangePassword extends UnlistedSpecialPage {
$this->mRetype = $request->getVal( 'wpRetype' );
$this->mDomain = $request->getVal( 'wpDomain' );
- $this->setHeaders();
- $this->outputHeader();
- $this->getOutput()->disallowUserJs();
-
$user = $this->getUser();
if( !$request->wasPosted() && !$user->isLoggedIn() ) {
$this->error( $this->msg( 'resetpass-no-info' )->text() );
@@ -61,12 +59,11 @@ class SpecialChangePassword extends UnlistedSpecialPage {
return;
}
+ $this->checkReadOnly();
+
if( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'token' ) ) ) {
try {
- if ( isset( $_SESSION['wsDomain'] ) ) {
- $this->mDomain = $_SESSION['wsDomain'];
- }
- $wgAuth->setDomain( $this->mDomain );
+ $this->mDomain = $wgAuth->getDomain();
if( !$wgAuth->allowPasswordChange() ) {
$this->error( $this->msg( 'resetpass_forbidden' )->text() );
return;
@@ -136,6 +133,15 @@ class SpecialChangePassword extends UnlistedSpecialPage {
$oldpassMsg = 'oldpassword';
$submitMsg = 'resetpass-submit-loggedin';
}
+ $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 ),
+ );
+ $prettyFields = array_merge( $prettyFields, $extraFields );
$this->getOutput()->addHTML(
Xml::fieldset( $this->msg( 'resetpass_header' )->text() ) .
Xml::openElement( 'form',
@@ -149,12 +155,7 @@ class SpecialChangePassword extends UnlistedSpecialPage {
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( array(
- array( 'wpName', 'username', 'text', $this->mUserName ),
- array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ),
- array( 'wpNewPassword', 'newpassword', 'password', null ),
- array( 'wpRetype', 'retypenew', 'password', null ),
- ) ) . "\n" .
+ $this->pretty( $prettyFields ) . "\n" .
$rememberMe .
"<tr>\n" .
"<td></td>\n" .
diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php
index 912f7733..3e9ce128 100644
--- a/includes/specials/SpecialConfirmemail.php
+++ b/includes/specials/SpecialConfirmemail.php
@@ -110,7 +110,7 @@ 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 Confirmation code
+ * @param $code string Confirmation code
*/
function attemptConfirm( $code ) {
$user = User::newFromConfirmationCode( $code );
@@ -156,7 +156,7 @@ 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 Confirmation code
+ * @param $code string Confirmation code
*/
function attemptInvalidate( $code ) {
$user = User::newFromConfirmationCode( $code );
diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
index 31df4a9b..54f8e261 100644
--- a/includes/specials/SpecialContributions.php
+++ b/includes/specials/SpecialContributions.php
@@ -44,10 +44,7 @@ class SpecialContributions extends SpecialPage {
$this->opts = array();
$request = $this->getRequest();
- if ( $par == 'newbies' ) {
- $target = 'newbies';
- $this->opts['contribs'] = 'newbie';
- } elseif ( $par !== null ) {
+ if ( $par !== null ) {
$target = $par;
} else {
$target = $request->getVal( 'target' );
@@ -57,6 +54,9 @@ class SpecialContributions extends SpecialPage {
if ( $request->getVal( 'contribs' ) == 'newbie' ) {
$target = 'newbies';
$this->opts['contribs'] = 'newbie';
+ } elseif ( $par === 'newbies' ) { // b/c for WMF
+ $target = 'newbies';
+ $this->opts['contribs'] = 'newbie';
} else {
$this->opts['contribs'] = 'user';
}
@@ -192,18 +192,20 @@ class SpecialContributions extends SpecialPage {
}
$out->preventClickjacking( $pager->getPreventClickjacking() );
+
# Show the appropriate "footer" message - WHOIS tools, etc.
- if ( $this->opts['contribs'] != 'newbie' ) {
+ if ( $this->opts['contribs'] == 'newbie' ) {
+ $message = 'sp-contributions-footer-newbies';
+ } elseif( IP::isIPAddress( $target ) ) {
+ $message = 'sp-contributions-footer-anon';
+ } elseif( $userObj->isAnon() ) {
+ // No message for non-existing users
+ $message = '';
+ } else {
$message = 'sp-contributions-footer';
- if ( IP::isIPAddress( $target ) ) {
- $message = 'sp-contributions-footer-anon';
- } else {
- if ( $userObj->isAnon() ) {
- // No message for non-existing users
- return;
- }
- }
+ }
+ if( $message ) {
if ( !$this->msg( $message, $target )->isDisabled() ) {
$out->wrapWikiMsg(
"<div class='mw-contributions-footer'>\n$1\n</div>",
@@ -227,12 +229,15 @@ class SpecialContributions extends SpecialPage {
}
$nt = $userObj->getUserPage();
$talk = $userObj->getTalkPage();
+ $links = '';
if ( $talk ) {
$tools = $this->getUserLinks( $nt, $talk, $userObj );
$links = $this->getLanguage()->pipeList( $tools );
// Show a note if the user is blocked and display the last block log entry.
- if ( $userObj->isBlocked() ) {
+ // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
+ // and also this will display a totally irrelevant log entry as a current block.
+ if ( $userObj->isBlocked() && $userObj->getBlock()->getType() != Block::TYPE_AUTO ) {
$out = $this->getOutput(); // showLogExtract() wants first parameter by reference
LogEventsList::showLogExtract(
$out,
@@ -258,9 +263,11 @@ class SpecialContributions extends SpecialPage {
// 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() ) {
- return $oldMsg->rawParams( "$user ($links)" );
+ $linksWithParentheses = $this->msg( 'parentheses' )->rawParams( $links )->escaped();
+ return $oldMsg->rawParams( "$user $linksWithParentheses" );
} else {
return $this->msg( 'contribsub2' )->rawParams( $user, $links );
}
@@ -331,7 +338,7 @@ class SpecialContributions extends SpecialPage {
# Add a link to change user rights for privileged users
$userrightsPage = new UserrightsPage();
$userrightsPage->setContext( $this->getContext() );
- if ( $id !== null && $userrightsPage->userCanChangeRights( $target ) ) {
+ if ( $userrightsPage->userCanChangeRights( $target ) ) {
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Userrights', $username ),
$this->msg( 'sp-contributions-userrights' )->escaped()
@@ -434,7 +441,7 @@ class SpecialContributions extends SpecialPage {
'target',
$this->opts['target'],
'text',
- array( 'size' => '20', 'required' => '', 'class' => 'mw-input' ) +
+ array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) +
( $this->opts['target'] ? array() : array( 'autofocus' )
)
) . ' '
@@ -449,7 +456,15 @@ class SpecialContributions extends SpecialPage {
)
) .
Xml::tags( 'td', null,
- Xml::namespaceSelector( $this->opts['namespace'], '' ) . '&#160;' .
+ Html::namespaceSelector( array(
+ 'selected' => $this->opts['namespace'],
+ 'all' => '',
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ ) ) .
+ '&#160;' .
Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ),
Xml::checkLabel(
$this->msg( 'invert' )->text(),
@@ -542,6 +557,11 @@ class ContribsPager extends ReverseChronologicalPager {
var $namespace = '', $mDb;
var $preventClickjacking = false;
+ /**
+ * @var array
+ */
+ protected $mParentLens;
+
function __construct( IContextSource $context, array $options ) {
parent::__construct( $context );
@@ -574,6 +594,66 @@ class ContribsPager extends ReverseChronologicalPager {
return $query;
}
+ /**
+ * 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 $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 );
+ $pager = $this;
+
+ /*
+ * This hook will allow extensions to add in additional queries, so they can get their data
+ * in My Contributions as well. Extensions should append their results to the $data array.
+ *
+ * Extension queries have to implement the navbar requirement as well. They should
+ * - have a column aliased as $pager->getIndexField()
+ * - have LIMIT set
+ * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
+ * - have the ORDER BY specified based upon the details provided by the navbar
+ *
+ * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
+ *
+ * &$data: an array of results of all contribs queries
+ * $pager: the ContribsPager object hooked into
+ * $offset: see phpdoc above
+ * $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 ) );
+
+ $result = array();
+
+ // loop all results and collect them in an array
+ foreach ( $data as $j => $query ) {
+ foreach ( $query as $i => $row ) {
+ // use index column as key, allowing us to easily sort in PHP
+ $result[$row->{$this->getIndexField()} . "-$i"] = $row;
+ }
+ }
+
+ // sort results
+ if ( $descending ) {
+ ksort( $result );
+ } else {
+ krsort( $result );
+ }
+
+ // enforce limit
+ $result = array_slice( $result, 0, $limit );
+
+ // get rid of array keys
+ $result = array_values( $result );
+
+ return new FakeResultWrapper( $result );
+ }
+
function getQueryInfo() {
list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond();
@@ -624,20 +704,30 @@ class ContribsPager extends ReverseChronologicalPager {
$join_conds = array();
$tables = array( 'revision', 'page', 'user' );
if ( $this->contribs == 'newbie' ) {
- $tables[] = 'user_groups';
$max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
$condition[] = 'rev_user >' . (int)( $max - $max / 100 );
- $condition[] = 'ug_group IS NULL';
$index = 'user_timestamp';
- # @todo FIXME: Other groups may have 'bot' rights
- $join_conds['user_groups'] = array( 'LEFT JOIN', "ug_user = rev_user AND ug_group = 'bot'" );
+ # ignore local groups with the bot right
+ # @todo FIXME: Global groups may have 'bot' rights
+ $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
+ if( count( $groupsWithBotPermission ) ) {
+ $tables[] = 'user_groups';
+ $condition[] = 'ug_group IS NULL';
+ $join_conds['user_groups'] = array(
+ 'LEFT JOIN', array(
+ 'ug_user = rev_user',
+ 'ug_group' => $groupsWithBotPermission
+ )
+ );
+ }
} else {
- if ( IP::isIPAddress( $this->target ) ) {
+ $uid = User::idFromName( $this->target );
+ if ( $uid ) {
+ $condition['rev_user'] = $uid;
+ $index = 'user_timestamp';
+ } else {
$condition['rev_user_text'] = $this->target;
$index = 'usertext_timestamp';
- } else {
- $condition['rev_user'] = User::idFromName( $this->target );
- $index = 'user_timestamp';
}
}
if ( $this->deletedOnly ) {
@@ -678,52 +768,29 @@ class ContribsPager extends ReverseChronologicalPager {
}
function doBatchLookups() {
- $this->mResult->rewind();
- $revIds = array();
- foreach ( $this->mResult as $row ) {
- if( $row->rev_parent_id ) {
- $revIds[] = $row->rev_parent_id;
- }
- }
- $this->mParentLens = $this->getParentLengths( $revIds );
- $this->mResult->rewind(); // reset
-
# Do a link batch query
$this->mResult->seek( 0 );
+ $revIds = array();
$batch = new LinkBatch();
# Give some pointers to make (last) links
foreach ( $this->mResult as $row ) {
- if ( $this->contribs === 'newbie' ) { // multiple users
- $batch->add( NS_USER, $row->user_name );
- $batch->add( NS_USER_TALK, $row->user_name );
+ if( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
+ $revIds[] = $row->rev_parent_id;
+ }
+ if ( isset( $row->rev_id ) ) {
+ if ( $this->contribs === 'newbie' ) { // multiple users
+ $batch->add( NS_USER, $row->user_name );
+ $batch->add( NS_USER_TALK, $row->user_name );
+ }
+ $batch->add( $row->page_namespace, $row->page_title );
}
- $batch->add( $row->page_namespace, $row->page_title );
}
+ $this->mParentLens = Revision::getParentLengths( $this->getDatabase(), $revIds );
$batch->execute();
$this->mResult->seek( 0 );
}
/**
- * Do a batched query to get the parent revision lengths
- */
- private function getParentLengths( array $revIds ) {
- $revLens = array();
- if ( !$revIds ) {
- return $revLens; // empty
- }
- wfProfileIn( __METHOD__ );
- $res = $this->getDatabase()->select( 'revision',
- array( 'rev_id', 'rev_len' ),
- array( 'rev_id' => $revIds ),
- __METHOD__ );
- foreach ( $res as $row ) {
- $revLens[$row->rev_id] = $row->rev_len;
- }
- wfProfileOut( __METHOD__ );
- return $revLens;
- }
-
- /**
* @return string
*/
function getStartBody() {
@@ -746,142 +813,153 @@ class ContribsPager extends ReverseChronologicalPager {
* was not written by the target user.
*
* @todo This would probably look a lot nicer in a table.
+ * @param $row
+ * @return string
*/
function formatRow( $row ) {
wfProfileIn( __METHOD__ );
- $rev = new Revision( $row );
+ $ret = '';
$classes = array();
- $page = Title::newFromRow( $row );
- $link = Linker::link(
- $page,
- htmlspecialchars( $page->getPrefixedText() ),
- array(),
- $page->isRedirect() ? array( 'redirect' => 'no' ) : array()
- );
- # Mark current revisions
- $topmarktext = '';
- if ( $row->rev_id == $row->page_latest ) {
- $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
- # Add rollback link
- if ( !$row->page_is_new && $page->quickUserCan( 'rollback' )
- && $page->quickUserCan( 'edit' ) )
- {
- $this->preventClickjacking();
- $topmarktext .= ' ' . Linker::generateRollback( $rev );
+ /*
+ * There may be more than just revision rows. To make sure that we'll only be processing
+ * revisions here, let's _try_ to build a revision out of our row (without displaying
+ * notices though) and then trying to grab data from the built object. If we succeed,
+ * we're definitely dealing with revision data and we may proceed, if not, we'll leave it
+ * to extensions to subscribe to the hook to parse the row.
+ */
+ wfSuppressWarnings();
+ $rev = new Revision( $row );
+ $validRevision = $rev->getParentId() !== null;
+ wfRestoreWarnings();
+
+ if ( $validRevision ) {
+ $classes = array();
+
+ $page = Title::newFromRow( $row );
+ $link = Linker::link(
+ $page,
+ htmlspecialchars( $page->getPrefixedText() ),
+ array( 'class' => 'mw-contributions-title' ),
+ $page->isRedirect() ? array( 'redirect' => 'no' ) : array()
+ );
+ # Mark current revisions
+ $topmarktext = '';
+ $user = $this->getUser();
+ if ( $row->rev_id == $row->page_latest ) {
+ $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 ) )
+ {
+ $this->preventClickjacking();
+ $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() );
+ }
}
- }
- $user = $this->getUser();
- # Is there a visible previous revision?
- if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) {
- $difftext = Linker::linkKnown(
+ # Is there a visible previous revision?
+ if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) {
+ $difftext = Linker::linkKnown(
+ $page,
+ $this->messages['diff'],
+ array(),
+ array(
+ 'diff' => 'prev',
+ 'oldid' => $row->rev_id
+ )
+ );
+ } else {
+ $difftext = $this->messages['diff'];
+ }
+ $histlink = Linker::linkKnown(
$page,
- $this->messages['diff'],
+ $this->messages['hist'],
array(),
- array(
- 'diff' => 'prev',
- 'oldid' => $row->rev_id
- )
+ array( 'action' => 'history' )
);
- } else {
- $difftext = $this->messages['diff'];
- }
- $histlink = Linker::linkKnown(
- $page,
- $this->messages['hist'],
- array(),
- array( 'action' => 'history' )
- );
- if ( $row->rev_parent_id === null ) {
- // 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 = ' . . ' . Linker::formatRevisionSize( $row->rev_len ) . ' . . ';
- } else {
- $parentLen = isset( $this->mParentLens[$row->rev_parent_id] ) ? $this->mParentLens[$row->rev_parent_id] : 0;
- $chardiff = ' . . ' . ChangesList::showCharacterDifference(
- $parentLen, $row->rev_len ) . ' . . ';
- }
+ if ( $row->rev_parent_id === null ) {
+ // 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> ';
+ } 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> ';
+ }
- $lang = $this->getLanguage();
- $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true );
- $date = $lang->userTimeAndDate( $row->rev_timestamp, $user );
- if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
- $d = Linker::linkKnown(
- $page,
- htmlspecialchars( $date ),
- array(),
- array( 'oldid' => intval( $row->rev_id ) )
- );
- } else {
- $d = htmlspecialchars( $date );
- }
- if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $d = '<span class="history-deleted">' . $d . '</span>';
- }
+ $lang = $this->getLanguage();
+ $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true );
+ $date = $lang->userTimeAndDate( $row->rev_timestamp, $user );
+ if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
+ $d = Linker::linkKnown(
+ $page,
+ htmlspecialchars( $date ),
+ array( 'class' => 'mw-changeslist-date' ),
+ array( 'oldid' => intval( $row->rev_id ) )
+ );
+ } else {
+ $d = htmlspecialchars( $date );
+ }
+ if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $d = '<span class="history-deleted">' . $d . '</span>';
+ }
- # 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 .= ' ' . $this->msg( 'parentheses' )->rawParams(
- Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' ';
- } else {
- $userlink = '';
- }
+ # 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 .= ' ' . $this->msg( 'parentheses' )->rawParams(
+ Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' ';
+ } else {
+ $userlink = '';
+ }
- if ( $rev->getParentId() === 0 ) {
- $nflag = ChangesList::flag( 'newpage' );
- } else {
- $nflag = '';
- }
+ if ( $rev->getParentId() === 0 ) {
+ $nflag = ChangesList::flag( 'newpage' );
+ } else {
+ $nflag = '';
+ }
- if ( $rev->isMinor() ) {
- $mflag = ChangesList::flag( 'minor' );
- } else {
- $mflag = '';
- }
+ if ( $rev->isMinor() ) {
+ $mflag = ChangesList::flag( 'minor' );
+ } else {
+ $mflag = '';
+ }
- $del = Linker::getRevDeleteLink( $user, $rev, $page );
- if ( $del !== '' ) {
- $del .= ' ';
- }
+ $del = Linker::getRevDeleteLink( $user, $rev, $page );
+ if ( $del !== '' ) {
+ $del .= ' ';
+ }
- $diffHistLinks = '(' . $difftext . $this->messages['pipe-separator'] . $histlink . ')';
- $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} {$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>";
- }
+ # Denote if username is redacted for this edit
+ if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>";
+ }
- # Tags, if any.
- list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' );
- $classes = array_merge( $classes, $newClasses );
- $ret .= " $tagSummary";
+ # Tags, if any.
+ list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' );
+ $classes = array_merge( $classes, $newClasses );
+ $ret .= " $tagSummary";
+ }
// Let extensions add data
- wfRunHooks( 'ContributionsLineEnding', array( &$this, &$ret, $row ) );
+ wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) );
$classes = implode( ' ', $classes );
$ret = "<li class=\"$classes\">$ret</li>\n";
+
wfProfileOut( __METHOD__ );
return $ret;
}
/**
- * Get the Database object in use
- *
- * @return DatabaseBase
- */
- public function getDatabase() {
- return $this->mDb;
- }
-
- /**
* Overwrite Pager function and return a helpful comment
+ * @return string
*/
function getSqlComment() {
if ( $this->namespace || $this->deletedOnly ) {
@@ -895,6 +973,9 @@ class ContribsPager extends ReverseChronologicalPager {
$this->preventClickjacking = true;
}
+ /**
+ * @return bool
+ */
public function getPreventClickjacking() {
return $this->preventClickjacking;
}
diff --git a/includes/specials/SpecialDeadendpages.php b/includes/specials/SpecialDeadendpages.php
index 1266a0ce..f4904a50 100644
--- a/includes/specials/SpecialDeadendpages.php
+++ b/includes/specials/SpecialDeadendpages.php
@@ -39,7 +39,7 @@ class DeadendPagesPage extends PageQueryPage {
/**
* LEFT JOIN is expensive
*
- * @return true
+ * @return bool
*/
function isExpensive() {
return true;
@@ -50,7 +50,7 @@ class DeadendPagesPage extends PageQueryPage {
}
/**
- * @return false
+ * @return bool
*/
function sortDescending() {
return false;
@@ -59,9 +59,9 @@ class DeadendPagesPage extends PageQueryPage {
function getQueryInfo() {
return array(
'tables' => array( 'page', 'pagelinks' ),
- 'fields' => array( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_title AS value'
+ 'fields' => array( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_title'
),
'conds' => array( 'pl_from IS NULL',
'page_namespace' => MWNamespace::getContentNamespaces(),
diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php
index 3498a16d..c880b617 100644
--- a/includes/specials/SpecialDeletedContributions.php
+++ b/includes/specials/SpecialDeletedContributions.php
@@ -25,7 +25,6 @@
* Implements Special:DeletedContributions to display archived revisions
* @ingroup SpecialPage
*/
-
class DeletedContribsPager extends IndexPager {
public $mDefaultDirection = true;
var $messages, $target;
@@ -54,9 +53,9 @@ class DeletedContribsPager extends IndexPager {
$user = $this->getUser();
// Paranoia: avoid brute force searches (bug 17792)
if( !$user->isAllowed( 'deletedhistory' ) ) {
- $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::DELETED_USER) . ' = 0';
+ $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::DELETED_USER ) . ' = 0';
} elseif( !$user->isAllowed( 'suppressrevision' ) ) {
- $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::SUPPRESSED_USER) .
+ $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::SUPPRESSED_USER ) .
' != ' . Revision::SUPPRESSED_USER;
}
return array(
@@ -95,17 +94,17 @@ class DeletedContribsPager extends IndexPager {
if ( isset( $this->mNavigationBar ) ) {
return $this->mNavigationBar;
}
- $lang = $this->getLanguage();
- $fmtLimit = $lang->formatNum( $this->mLimit );
+
$linkTexts = array(
- 'prev' => $this->msg( 'pager-newer-n', $fmtLimit )->escaped(),
- 'next' => $this->msg( 'pager-older-n', $fmtLimit )->escaped(),
+ 'prev' => $this->msg( 'pager-newer-n' )->numParams( $this->mLimit )->escaped(),
+ 'next' => $this->msg( 'pager-older-n' )->numParams( $this->mLimit )->escaped(),
'first' => $this->msg( 'histlast' )->escaped(),
'last' => $this->msg( 'histfirst' )->escaped()
);
$pagingLinks = $this->getPagingLinks( $linkTexts );
$limitLinks = $this->getLimitLinks();
+ $lang = $this->getLanguage();
$limits = $lang->pipeList( $limitLinks );
$this->mNavigationBar = "(" . $lang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " .
@@ -130,6 +129,8 @@ class DeletedContribsPager extends IndexPager {
* written by the target user.
*
* @todo This would probably look a lot nicer in a table.
+ * @param $row
+ * @return string
*/
function formatRow( $row ) {
wfProfileIn( __METHOD__ );
@@ -190,7 +191,7 @@ class DeletedContribsPager extends IndexPager {
$link = Linker::linkKnown(
$undelete,
$date,
- array(),
+ array( 'class' => 'mw-changeslist-date' ),
array(
'target' => $page->getPrefixedText(),
'timestamp' => $rev->getTimestamp()
@@ -202,7 +203,11 @@ class DeletedContribsPager extends IndexPager {
$link = '<span class="history-deleted">' . $link . '</span>';
}
- $pagelink = Linker::link( $page );
+ $pagelink = Linker::link(
+ $page,
+ null,
+ array( 'class' => 'mw-changeslist-title' )
+ );
if( $rev->isMinor() ) {
$mflag = ChangesList::flag( 'minor' );
@@ -221,7 +226,8 @@ class DeletedContribsPager extends IndexPager {
array( $last, $dellog, $reviewlink ) ) )->escaped()
);
- $ret = "{$del}{$link} {$tools} . . {$mflag} {$pagelink} {$comment}";
+ $separator = '<span class="mw-changeslist-separator">. .</span>';
+ $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment}";
# Denote if username is redacted for this edit
if( $rev->isDeleted( Revision::DELETED_USER ) ) {
@@ -237,7 +243,7 @@ class DeletedContribsPager extends IndexPager {
/**
* Get the Database object in use
*
- * @return Database
+ * @return DatabaseBase
*/
public function getDatabase() {
return $this->mDb;
@@ -254,12 +260,12 @@ class DeletedContributionsPage extends SpecialPage {
* Special page "deleted user contributions".
* Shows a list of the deleted contributions of a user.
*
- * @return none
* @param $par String: (optional) user name of the user for which to show the contributions
*/
function execute( $par ) {
global $wgQueryPageDefaultLimit;
$this->setHeaders();
+ $this->outputHeader();
$user = $this->getUser();
@@ -293,6 +299,7 @@ class DeletedContributionsPage extends SpecialPage {
$out->addHTML( $this->getForm( '' ) );
return;
}
+ $this->getSkin()->setRelevantUser( $userObj );
$target = $userObj->getName();
$out->addSubtitle( $this->getSubTitle( $userObj ) );
@@ -346,6 +353,7 @@ class DeletedContributionsPage extends SpecialPage {
} else {
$user = Linker::link( $userObj->getUserPage(), htmlspecialchars( $userObj->getName() ) );
}
+ $links = '';
$nt = $userObj->getUserPage();
$id = $userObj->getID();
$talk = $nt->getTalkPage();
@@ -387,6 +395,13 @@ class DeletedContributionsPage extends SpecialPage {
)
);
}
+
+ # Uploads
+ $tools[] = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Listfiles', $userObj->getName() ),
+ $this->msg( 'sp-contributions-uploads' )->escaped()
+ );
+
# Other logs link
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Log' ),
@@ -403,7 +418,7 @@ class DeletedContributionsPage extends SpecialPage {
# Add a link to change user rights for privileged users
$userrightsPage = new UserrightsPage();
$userrightsPage->setContext( $this->getContext() );
- if( $id !== null && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) {
+ if( $userrightsPage->userCanChangeRights( $userObj ) ) {
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ),
$this->msg( 'sp-contributions-userrights' )->escaped()
@@ -450,6 +465,7 @@ class DeletedContributionsPage extends SpecialPage {
/**
* Generates the namespace selector form with hidden attributes.
* @param $options Array: the options to be included.
+ * @return string
*/
function getForm( $options ) {
global $wgScript;
@@ -489,8 +505,17 @@ class DeletedContributionsPage extends SpecialPage {
'size' => '20',
'required' => ''
) + ( $options['target'] ? array() : array( 'autofocus' ) ) ) . ' '.
- Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . ' ' .
- Xml::namespaceSelector( $options['namespace'], '' ) . ' ' .
+ 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' );
diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php
index 2b05fb6b..48180a77 100644
--- a/includes/specials/SpecialDisambiguations.php
+++ b/includes/specials/SpecialDisambiguations.php
@@ -26,27 +26,35 @@
*
* @ingroup SpecialPage
*/
-class DisambiguationsPage extends PageQueryPage {
+class DisambiguationsPage extends QueryPage {
function __construct( $name = 'Disambiguations' ) {
parent::__construct( $name );
}
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
function getPageHeader() {
return $this->msg( 'disambiguations-text' )->parseAsBlock();
}
- function getQueryInfo() {
+ /**
+ * @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);
+ $dp = Title::newFromText( $dMsgText );
if( $dp ) {
if( $dp->getNamespace() != NS_TEMPLATE ) {
# @todo FIXME: We assume the disambiguation message is a template but
@@ -71,25 +79,38 @@ class DisambiguationsPage extends PageQueryPage {
}
}
$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");
+ 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( 'p1.page_namespace AS namespace',
- 'p1.page_title AS title',
- 'pl_from AS value' ),
- 'conds' => array( $set,
- '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() )
+ '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()
+ )
);
}
@@ -108,17 +129,17 @@ class DisambiguationsPage extends PageQueryPage {
* @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();
- // Back to start for display
- if ( $db->numRows( $res ) > 0 ) {
- // If there are no rows we get an error seeking.
- $db->dataSeek( $res, 0 );
- }
+ $res->seek( 0 );
}
function formatResult( $skin, $result ) {
@@ -126,10 +147,14 @@ class DisambiguationsPage extends PageQueryPage {
$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 );
+ $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 a6df66f6..5864ca9f 100644
--- a/includes/specials/SpecialDoubleRedirects.php
+++ b/includes/specials/SpecialDoubleRedirects.php
@@ -27,7 +27,7 @@
*
* @ingroup SpecialPage
*/
-class DoubleRedirectsPage extends PageQueryPage {
+class DoubleRedirectsPage extends QueryPage {
function __construct( $name = 'DoubleRedirects' ) {
parent::__construct( $name );
@@ -47,13 +47,13 @@ class DoubleRedirectsPage extends PageQueryPage {
'tables' => array ( 'ra' => 'redirect',
'rb' => 'redirect', 'pa' => 'page',
'pb' => 'page', 'pc' => 'page' ),
- 'fields' => array ( 'pa.page_namespace AS namespace',
- 'pa.page_title AS title',
- 'pa.page_title AS value',
- 'pb.page_namespace AS nsb',
- 'pb.page_title AS tb',
- 'pc.page_namespace AS nsc',
- 'pc.page_title AS tc' ),
+ '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',
diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php
index 9c9689ae..23cd9aa6 100644
--- a/includes/specials/SpecialEditWatchlist.php
+++ b/includes/specials/SpecialEditWatchlist.php
@@ -1,9 +1,36 @@
<?php
+/**
+ * @defgroup Watchlist Users watchlist handling
+ */
+
+/**
+ * Implements Special:EditWatchlist
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @ingroup Watchlist
+ */
/**
* Provides the UI through which users can perform editing
* operations on their watchlist
*
+ * @ingroup SpecialPage
* @ingroup Watchlist
* @author Rob Church <robchur@gmail.com>
*/
@@ -76,7 +103,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$form = $this->getRawForm();
if( $form->show() ){
$out->addHTML( $this->successMessage );
- $out->returnToMain();
+ $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
}
break;
@@ -86,7 +113,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$form = $this->getNormalForm();
if( $form->show() ){
$out->addHTML( $this->successMessage );
- $out->returnToMain();
+ $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
} elseif ( $this->toc !== false ) {
$out->prependHTML( $this->toc );
}
@@ -102,21 +129,28 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* @return array
*/
private function extractTitles( $list ) {
- $titles = array();
$list = explode( "\n", trim( $list ) );
if( !is_array( $list ) ) {
return array();
}
+ $titles = array();
foreach( $list as $text ) {
$text = trim( $text );
if( strlen( $text ) > 0 ) {
$title = Title::newFromText( $text );
if( $title instanceof Title && $title->isWatchable() ) {
- $titles[] = $title->getPrefixedText();
+ $titles[] = $title;
}
}
}
- return array_unique( $titles );
+
+ GenderCache::singleton()->doTitlesArray( $titles );
+
+ $list = array();
+ foreach( $titles as $title ) {
+ $list[] = $title->getPrefixedText();
+ }
+ return array_unique( $list );
}
public function submitRaw( $data ){
@@ -214,22 +248,30 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$dbr = wfGetDB( DB_MASTER );
$res = $dbr->select(
'watchlist',
- '*',
array(
+ 'wl_namespace', 'wl_title'
+ ), array(
'wl_user' => $this->getUser()->getId(),
),
__METHOD__
);
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()
) {
- $list[] = $title->getPrefixedText();
+ $titles[] = $title;
}
}
$res->free();
+
+ GenderCache::singleton()->doTitlesArray( $titles );
+
+ foreach( $titles as $title ) {
+ $list[] = $title->getPrefixedText();
+ }
}
$this->cleanupWatchlist();
return $list;
@@ -250,7 +292,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
array( 'wl_namespace', 'wl_title' ),
array( 'wl_user' => $this->getUser()->getId() ),
__METHOD__,
- array( 'ORDER BY' => 'wl_namespace, wl_title' )
+ array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' ) )
);
$lb = new LinkBatch();
@@ -270,7 +312,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 ) {
@@ -294,18 +336,20 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* Attempts to clean up broken items
*/
private function cleanupWatchlist() {
- if ( count( $this->badItems ) ) {
- $dbw = wfGetDB( DB_MASTER );
+ 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 {$this->getUser()} has broken watchlist item ns($namespace):$dbKey, "
+ wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, "
. ( $title ? 'cleaning up' : 'deleting' ) . ".\n"
);
$dbw->delete( 'watchlist',
array(
- 'wl_user' => $this->getUser()->getId(),
+ 'wl_user' => $user->getId(),
'wl_namespace' => $namespace,
'wl_title' => $dbKey,
),
@@ -314,7 +358,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
// Can't just do an UPDATE instead of DELETE/INSERT due to unique index
if ( $title ) {
- $this->getUser()->addWatch( $title );
+ $user->addWatch( $title );
}
}
}
@@ -408,7 +452,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
foreach( $data as $titles ) {
$this->unwatchTitles( $titles );
- $removed += $titles;
+ $removed = array_merge( $removed, $titles );
}
if( count( $removed ) > 0 ) {
@@ -445,7 +489,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$title = Title::makeTitleSafe( $namespace, $dbkey );
if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
$text = $this->buildRemoveLine( $title );
- $fields['TitlesNs'.$namespace]['options'][$text] = $title->getEscapedText();
+ $fields['TitlesNs'.$namespace]['options'][$text] = htmlspecialchars( $title->getPrefixedText() );
$count++;
}
}
@@ -455,7 +499,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
if ( count( $fields ) > 1 && $count > 30 ) {
$this->toc = Linker::tocIndent();
$tocLength = 0;
- foreach( $fields as $key => $data ) {
+ foreach( $fields as $data ) {
# strip out the 'ns' prefix from the section name:
$ns = substr( $data['section'], 2 );
@@ -572,7 +616,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* Build a set of links for convenient navigation
* between watchlist viewing and editing modes
*
- * @param $unused Unused
+ * @param $unused
* @return string
*/
public static function buildTools( $unused ) {
@@ -588,12 +632,12 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
// can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
$tools[] = Linker::linkKnown(
SpecialPage::getTitleFor( $arr[0], $arr[1] ),
- wfMsgHtml( "watchlisttools-{$mode}" )
+ wfMessage( "watchlisttools-{$mode}" )->escaped()
);
}
return Html::rawElement( 'span',
array( 'class' => 'mw-watchlist-toollinks' ),
- wfMsg( 'parentheses', $wgLang->pipeList( $tools ) ) );
+ wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text() );
}
}
diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php
index 314da727..4d875e6e 100644
--- a/includes/specials/SpecialEmailuser.php
+++ b/includes/specials/SpecialEmailuser.php
@@ -33,6 +33,15 @@ class SpecialEmailUser extends UnlistedSpecialPage {
parent::__construct( 'Emailuser' );
}
+ public function getDescription() {
+ $target = self::getTarget( $this->mTarget );
+ if( !$target instanceof User ) {
+ return $this->msg( 'emailuser-title-notarget' )->text();
+ }
+
+ return $this->msg( 'emailuser-title-target', $target->getName() )->text();
+ }
+
protected function getFormFields() {
return array(
'From' => array(
@@ -61,18 +70,19 @@ class SpecialEmailUser extends UnlistedSpecialPage {
),
'Subject' => array(
'type' => 'text',
- 'default' => wfMsgExt( 'defemailsubject', array( 'content', 'parsemag' ), $this->getUser()->getName() ),
+ 'default' => $this->msg( 'defemailsubject',
+ $this->getUser()->getName() )->inContentLanguage()->text(),
'label-message' => 'emailsubject',
'maxlength' => 200,
'size' => 60,
- 'required' => 1,
+ 'required' => true,
),
'Text' => array(
'type' => 'textarea',
'rows' => 20,
'cols' => 80,
'label-message' => 'emailmessage',
- 'required' => 1,
+ 'required' => true,
),
'CCMe' => array(
'type' => 'check',
@@ -83,13 +93,18 @@ class SpecialEmailUser extends UnlistedSpecialPage {
}
public function execute( $par ) {
- $this->setHeaders();
- $this->outputHeader();
$out = $this->getOutput();
$out->addModuleStyles( 'mediawiki.special' );
+
$this->mTarget = is_null( $par )
? $this->getRequest()->getVal( 'wpTarget', $this->getRequest()->getVal( 'target', '' ) )
: $par;
+
+ // This needs to be below assignment of $this->mTarget because
+ // getDescription() needs it to determine the correct page title.
+ $this->setHeaders();
+ $this->outputHeader();
+
// error out if sending user cannot do this
$error = self::getPermissionsError( $this->getUser(), $this->getRequest()->getVal( 'wpEditToken' ) );
switch ( $error ) {
@@ -124,18 +139,17 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$this->mTargetObj = $ret;
$form = new HTMLForm( $this->getFormFields(), $this->getContext() );
- $form->addPreText( wfMsgExt( 'emailpagetext', 'parseinline' ) );
- $form->setSubmitText( wfMsg( 'emailsend' ) );
+ $form->addPreText( $this->msg( 'emailpagetext' )->parse() );
+ $form->setSubmitTextMsg( 'emailsend' );
$form->setTitle( $this->getTitle() );
- $form->setSubmitCallback( array( __CLASS__, 'submit' ) );
- $form->setWrapperLegend( wfMsgExt( 'email-legend', 'parsemag' ) );
+ $form->setSubmitCallback( array( __CLASS__, 'uiSubmit' ) );
+ $form->setWrapperLegendMsg( 'email-legend' );
$form->loadData();
if( !wfRunHooks( 'EmailUserForm', array( &$form ) ) ) {
return false;
}
- $out->setPageTitle( $this->msg( 'emailpage' ) );
$result = $form->show();
if( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
@@ -224,15 +238,27 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$string = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'askusername' ) ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
Xml::openElement( 'fieldset' ) .
- Html::rawElement( 'legend', null, wfMessage( 'emailtarget' )->parse() ) .
- Xml::inputLabel( wfMessage( 'emailusername' )->text(), 'target', 'emailusertarget', 30, $name ) . ' ' .
- Xml::submitButton( wfMessage( 'emailusernamesubmit' )->text() ) .
+ Html::rawElement( 'legend', null, $this->msg( 'emailtarget' )->parse() ) .
+ 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;
}
/**
+ * Submit callback for an HTMLForm object, will simply call submit().
+ *
+ * @since 1.20
+ * @param $data array
+ * @param $form HTMLForm object
+ * @return Status|string|bool
+ */
+ public static function uiSubmit( array $data, HTMLForm $form ) {
+ return self::submit( $data, $form->getContext() );
+ }
+
+ /**
* Really send a mail. Permissions should have been checked using
* getPermissionsError(). It is probably also a good
* idea to check the edit token and ping limiter in advance.
@@ -240,25 +266,22 @@ class SpecialEmailUser extends UnlistedSpecialPage {
* @return Mixed: Status object, or potentially a String on error
* or maybe even true on success if anything uses the EmailUser hook.
*/
- public static function submit( $data ) {
- global $wgUser, $wgUserEmailUseReplyTo;
+ public static function submit( array $data, IContextSource $context ) {
+ global $wgUserEmailUseReplyTo;
$target = self::getTarget( $data['Target'] );
if( !$target instanceof User ) {
- return wfMsgExt( $target . 'text', 'parse' );
+ return $context->msg( $target . 'text' )->parseAsBlock();
}
$to = new MailAddress( $target );
- $from = new MailAddress( $wgUser );
+ $from = new MailAddress( $context->getUser() );
$subject = $data['Subject'];
$text = $data['Text'];
// Add a standard footer and trim up trailing newlines
$text = rtrim( $text ) . "\n\n-- \n";
- $text .= wfMsgExt(
- 'emailuserfooter',
- array( 'content', 'parsemag' ),
- array( $from->name, $to->name )
- );
+ $text .= $context->msg( 'emailuserfooter',
+ $from->name, $to->name )->inContentLanguage()->text();
$error = '';
if( !wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$text, &$error ) ) ) {
@@ -302,11 +325,8 @@ class SpecialEmailUser extends UnlistedSpecialPage {
// unless they are emailing themselves, in which case one
// copy of the message is sufficient.
if ( $data['CCMe'] && $to != $from ) {
- $cc_subject = wfMsg(
- 'emailccsubject',
- $target->getName(),
- $subject
- );
+ $cc_subject = $context->msg( 'emailccsubject' )->rawParams(
+ $target->getName(), $subject )->text();
wfRunHooks( 'EmailUserCC', array( &$from, &$from, &$cc_subject, &$text ) );
$ccStatus = UserMailer::send( $from, $from, $cc_subject, $text );
$status->merge( $ccStatus );
diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php
index d061389e..b4294b32 100644
--- a/includes/specials/SpecialExport.php
+++ b/includes/specials/SpecialExport.php
@@ -93,6 +93,13 @@ class SpecialExport extends SpecialPage {
elseif( $request->getCheck( 'exportall' ) && $wgExportAllowAll ) {
$this->doExport = true;
$exportall = true;
+
+ /* Although $page and $history are not used later on, we
+ nevertheless set them to avoid that PHP notices about using
+ undefined variables foul up our XML output (see call to
+ doExport(...) further down) */
+ $page = '';
+ $history = '';
}
elseif( $request->wasPosted() && $par == '' ) {
$page = $request->getText( 'pages' );
@@ -181,17 +188,26 @@ class SpecialExport extends SpecialPage {
$form = Xml::openElement( 'form', array( 'method' => 'post',
'action' => $this->getTitle()->getLocalUrl( 'action=submit' ) ) );
- $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . '&#160;';
- $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
+ $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 .= Xml::namespaceSelector( $nsindex, null, 'nsindex', wfMsg( 'export-addnstext' ) ) . '&#160;';
- $form .= Xml::submitButton( wfMsg( 'export-addns' ), array( 'name' => 'addns' ) ) . '<br />';
+ $form .= Html::namespaceSelector(
+ array(
+ 'selected' => $nsindex,
+ 'label' => $this->msg( 'export-addnstext' )->text()
+ ), array(
+ 'name' => 'nsindex',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ ) . '&#160;';
+ $form .= Xml::submitButton( $this->msg( 'export-addns' )->text(), array( 'name' => 'addns' ) ) . '<br />';
}
if ( $wgExportAllowAll ) {
$form .= Xml::checkLabel(
- wfMsg( 'exportall' ),
+ $this->msg( 'exportall' )->text(),
'exportall',
'exportall',
$request->wasPosted() ? $request->getCheck( 'exportall' ) : false
@@ -203,29 +219,29 @@ class SpecialExport extends SpecialPage {
if( $wgExportAllowHistory ) {
$form .= Xml::checkLabel(
- wfMsg( 'exportcuronly' ),
+ $this->msg( 'exportcuronly' )->text(),
'curonly',
'curonly',
$request->wasPosted() ? $request->getCheck( 'curonly' ) : true
) . '<br />';
} else {
- $out->addHTML( wfMsgExt( 'exportnohistory', 'parse' ) );
+ $out->addWikiMsg( 'exportnohistory' );
}
$form .= Xml::checkLabel(
- wfMsg( 'export-templates' ),
+ $this->msg( 'export-templates' )->text(),
'templates',
'wpExportTemplates',
$request->wasPosted() ? $request->getCheck( 'templates' ) : false
) . '<br />';
if( $wgExportMaxLinkDepth || $this->userCanOverrideExportDepth() ) {
- $form .= Xml::inputLabel( wfMsg( 'export-pagelinks' ), 'pagelink-depth', 'pagelink-depth', 20, 0 ) . '<br />';
+ $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( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />';
+ //$form .= Xml::checkLabel( $this->msg( 'export-images' )->text(), 'images', 'wpExportImages', false ) . '<br />';
$form .= Xml::checkLabel(
- wfMsg( 'export-download' ),
+ $this->msg( 'export-download' )->text(),
'wpDownload',
'wpDownload',
$request->wasPosted() ? $request->getCheck( 'wpDownload' ) : true
@@ -233,14 +249,14 @@ class SpecialExport extends SpecialPage {
if ( $wgExportAllowListContributors ) {
$form .= Xml::checkLabel(
- wfMsg( 'exportlistauthors' ),
+ $this->msg( 'exportlistauthors' )->text(),
'listauthors',
'listauthors',
$request->wasPosted() ? $request->getCheck( 'listauthors' ) : false
) . '<br />';
}
- $form .= Xml::submitButton( wfMsg( 'export-submit' ), Linker::tooltipAndAccesskeyAttribs( 'export' ) );
+ $form .= Xml::submitButton( $this->msg( 'export-submit' )->text(), Linker::tooltipAndAccesskeyAttribs( 'export' ) );
$form .= Xml::closeElement( 'form' );
$out->addHTML( $form );
@@ -439,7 +455,7 @@ class SpecialExport extends SpecialPage {
private function getTemplates( $inputPages, $pageSet ) {
return $this->getLinks( $inputPages, $pageSet,
'templatelinks',
- array( 'tl_namespace AS namespace', 'tl_title AS title' ),
+ array( 'namespace' => 'tl_namespace', 'title' => 'tl_title' ),
array( 'page_id=tl_from' )
);
}
@@ -481,7 +497,7 @@ class SpecialExport extends SpecialPage {
for( ; $depth > 0; --$depth ) {
$pageSet = $this->getLinks(
$inputPages, $pageSet, 'pagelinks',
- array( 'pl_namespace AS namespace', 'pl_title AS title' ),
+ array( 'namespace' => 'pl_namespace', 'title' => 'pl_title' ),
array( 'page_id=pl_from' )
);
$inputPages = array_keys( $pageSet );
@@ -503,13 +519,14 @@ class SpecialExport extends SpecialPage {
$inputPages,
$pageSet,
'imagelinks',
- array( NS_FILE . ' AS namespace', 'il_to AS title' ),
+ array( 'namespace' => NS_FILE, 'title' => 'il_to' ),
array( 'page_id=il_from' )
);
}
/**
* Expand a list of pages to include items used in those pages.
+ * @return array
*/
private function getLinks( $inputPages, $pageSet, $table, $fields, $join ) {
$dbr = wfGetDB( DB_SLAVE );
diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php
index 27d17f63..7e4bc9ce 100644
--- a/includes/specials/SpecialFewestrevisions.php
+++ b/includes/specials/SpecialFewestrevisions.php
@@ -44,10 +44,10 @@ class FewestrevisionsPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'revision', 'page' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'COUNT(*) AS value',
- 'page_is_redirect AS redirect' ),
+ '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',
@@ -56,7 +56,7 @@ class FewestrevisionsPage extends QueryPage {
// useful to remove this. People _do_ create pages
// and never revise them, they aren't necessarily
// redirects.
- 'GROUP BY' => 'page_namespace, page_title, page_is_redirect' )
+ 'GROUP BY' => array( 'page_namespace', 'page_title', 'page_is_redirect' ) )
);
}
@@ -68,13 +68,15 @@ class FewestrevisionsPage extends QueryPage {
/**
* @param $skin Skin object
* @param $result Object: database row
+ * @return String
*/
function formatResult( $skin, $result ) {
global $wgContLang;
$nt = Title::makeTitleSafe( $result->namespace, $result->title );
if( !$nt ) {
- return '<!-- bad title -->';
+ return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
}
$text = htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) );
@@ -82,7 +84,7 @@ class FewestrevisionsPage extends QueryPage {
$nl = $this->msg( 'nrevisions' )->numParams( $result->value )->escaped();
$redirect = isset( $result->redirect ) && $result->redirect ?
- ' - ' . wfMsgHtml( 'isredirect' ) : '';
+ ' - ' . $this->msg( 'isredirect' )->escaped() : '';
$nlink = Linker::linkKnown(
$nt,
$nl,
diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php
index 18d19db8..ccf8ba17 100644
--- a/includes/specials/SpecialFileDuplicateSearch.php
+++ b/includes/specials/SpecialFileDuplicateSearch.php
@@ -78,8 +78,8 @@ class FileDuplicateSearchPage extends QueryPage {
return array(
'tables' => array( 'image' ),
'fields' => array(
- 'img_name AS title',
- 'img_sha1 AS value',
+ 'title' => 'img_name',
+ 'value' => 'img_sha1',
'img_user_text',
'img_timestamp'
),
@@ -157,10 +157,24 @@ class FileDuplicateSearchPage extends QueryPage {
);
}
+ $this->doBatchLookups( $dupes );
$this->showList( $dupes );
}
}
+ function doBatchLookups( $list ) {
+ $batch = new LinkBatch();
+ foreach( $list as $file ) {
+ $batch->addObj( $file->getTitle() );
+ if( $file->isLocal() ) {
+ $userName = $file->getUser( 'text' );
+ $batch->add( NS_USER, $userName );
+ $batch->add( NS_USER_TALK, $userName );
+ }
+ }
+ $batch->execute();
+ }
+
/**
*
* @param Skin $skin
@@ -178,7 +192,17 @@ class FileDuplicateSearchPage extends QueryPage {
);
$userText = $result->getUser( 'text' );
- $user = Linker::link( Title::makeTitle( NS_USER, $userText ), $userText );
+ if ( $result->isLocal() ) {
+ $userId = $result->getUser( 'id' );
+ $user = Linker::userLink( $userId, $userText );
+ $user .= $this->getContext()->msg( 'word-separator' )->plain();
+ $user .= '<span style="white-space: nowrap;">';
+ $user .= Linker::userToolLinks( $userId, $userText );
+ $user .= '</span>';
+ } else {
+ $user = htmlspecialchars( $userText );
+ }
+
$time = $this->getLanguage()->userTimeAndDate( $result->getTimestamp(), $this->getUser() );
return "$plink . . $user . . $time";
diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php
index 101a33f4..e0866504 100644
--- a/includes/specials/SpecialFilepath.php
+++ b/includes/specials/SpecialFilepath.php
@@ -78,10 +78,10 @@ class SpecialFilepath extends SpecialPage {
$this->getOutput()->addHTML(
Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'specialfilepath' ) ) .
Html::openElement( 'fieldset' ) .
- Html::element( 'legend', null, wfMsg( 'filepath' ) ) .
+ Html::element( 'legend', null, $this->msg( 'filepath' )->text() ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
- Xml::inputLabel( wfMsg( 'filepath-page' ), 'file', 'file', 25, is_object( $title ) ? $title->getText() : '' ) . ' ' .
- Xml::submitButton( wfMsg( 'filepath-submit' ) ) . "\n" .
+ 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' )
);
diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php
index a2380fbe..362fc5cf 100644
--- a/includes/specials/SpecialImport.php
+++ b/includes/specials/SpecialImport.php
@@ -33,6 +33,7 @@ class SpecialImport extends SpecialPage {
private $interwiki = false;
private $namespace;
+ private $rootpage = '';
private $frompage = '';
private $logcomment= false;
private $history = true;
@@ -100,6 +101,7 @@ class SpecialImport extends SpecialPage {
$this->logcomment = $request->getText( 'log-comment' );
$this->pageLinkDepth = $wgExportMaxLinkDepth == 0 ? 0 : $request->getIntOrNull( 'pagelink-depth' );
+ $this->rootpage = $request->getText( 'rootpage' );
$user = $this->getUser();
if ( !$user->matchEditToken( $request->getVal( 'editToken' ) ) ) {
@@ -137,12 +139,20 @@ class SpecialImport extends SpecialPage {
if( !$source->isGood() ) {
$out->wrapWikiMsg( "<p class=\"error\">\n$1\n</p>", array( 'importfailed', $source->getWikiText() ) );
} else {
- $out->addWikiMsg( "importstart" );
-
$importer = new WikiImporter( $source->value );
if( !is_null( $this->namespace ) ) {
$importer->setTargetNamespace( $this->namespace );
}
+ 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() ) ) );
+ return;
+ }
+ }
+
+ $out->addWikiMsg( "importstart" );
+
$reporter = new ImportReporter( $importer, $isUpload, $this->interwiki , $this->logcomment);
$reporter->setContext( $this->getContext() );
$exception = false;
@@ -177,18 +187,18 @@ class SpecialImport extends SpecialPage {
$out = $this->getOutput();
if( $user->isAllowed( 'importupload' ) ) {
- $out->addWikiMsg( "importtext" );
$out->addHTML(
- Xml::fieldset( wfMsg( 'import-upload' ) ).
+ 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>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'import-upload-filename' ), 'xmlimport' ) .
+ Xml::label( $this->msg( 'import-upload-filename' )->text(), 'xmlimport' ) .
"</td>
<td class='mw-input'>" .
Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' .
@@ -196,7 +206,7 @@ class SpecialImport extends SpecialPage {
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'import-comment' ), 'mw-import-comment' ) .
+ Xml::label( $this->msg( 'import-comment' )->text(), 'mw-import-comment' ) .
"</td>
<td class='mw-input'>" .
Xml::input( 'log-comment', 50, '',
@@ -204,9 +214,18 @@ class SpecialImport extends SpecialPage {
"</td>
</tr>
<tr>
+ <td class='mw-label'>" .
+ Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'rootpage', 50, $this->rootpage,
+ array( 'id' => 'mw-interwiki-rootpage', 'type' => 'text' ) ) . ' ' .
+ "</td>
+ </tr>
+ <tr>
<td></td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
+ Xml::submitButton( $this->msg( 'uploadbtn' )->text() ) .
"</td>
</tr>" .
Xml::closeElement( 'table' ).
@@ -226,7 +245,7 @@ class SpecialImport extends SpecialPage {
if( $wgExportMaxLinkDepth > 0 ) {
$importDepth = "<tr>
<td class='mw-label'>" .
- wfMsgExt( 'export-pagelinks', 'parseinline' ) .
+ $this->msg( 'export-pagelinks' )->parse() .
"</td>
<td class='mw-input'>" .
Xml::input( 'pagelink-depth', 3, 0 ) .
@@ -235,16 +254,16 @@ class SpecialImport extends SpecialPage {
}
$out->addHTML(
- Xml::fieldset( wfMsg( 'importinterwiki' ) ) .
+ Xml::fieldset( $this->msg( 'importinterwiki' )->text() ) .
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'mw-import-interwiki-form' ) ) .
- wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
+ $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>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'import-interwiki-source' ), 'interwiki' ) .
+ Xml::label( $this->msg( 'import-interwiki-source' )->text(), 'interwiki' ) .
"</td>
<td class='mw-input'>" .
Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
@@ -263,28 +282,37 @@ class SpecialImport extends SpecialPage {
<td>
</td>
<td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'import-interwiki-history' ), '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( wfMsg( 'import-interwiki-templates' ), '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( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) .
+ Xml::label( $this->msg( 'import-interwiki-namespace' )->text(), 'namespace' ) .
"</td>
<td class='mw-input'>" .
- Xml::namespaceSelector( $this->namespace, '' ) .
+ Html::namespaceSelector(
+ array(
+ 'selected' => $this->namespace,
+ 'all' => '',
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ ) .
"</td>
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'import-comment' ), 'mw-interwiki-comment' ) .
+ Xml::label( $this->msg( 'import-comment' )->text(), 'mw-interwiki-comment' ) .
"</td>
<td class='mw-input'>" .
Xml::input( 'log-comment', 50, '',
@@ -292,10 +320,19 @@ class SpecialImport extends SpecialPage {
"</td>
</tr>
<tr>
+ <td class='mw-label'>" .
+ Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'rootpage', 50, $this->rootpage,
+ array( 'id' => 'mw-interwiki-rootpage', 'type' => 'text' ) ) . ' ' .
+ "</td>
+ </tr>
+ <tr>
<td>
</td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'import-interwiki-submit' ), Linker::tooltipAndAccesskeyAttribs( 'import' ) ) .
+ Xml::submitButton( $this->msg( 'import-interwiki-submit' )->text(), Linker::tooltipAndAccesskeyAttribs( 'import' ) ) .
"</td>
</tr>" .
Xml::closeElement( 'table' ).
@@ -352,8 +389,6 @@ class ImportReporter extends ContextSource {
* @return void
*/
function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) {
- global $wgContLang;
-
$args = func_get_args();
call_user_func_array( $this->mOriginalPageOutCallback, $args );
@@ -364,30 +399,27 @@ class ImportReporter extends ContextSource {
$this->mPageCount++;
- $localCount = $this->getLanguage()->formatNum( $successCount );
- $contentCount = $wgContLang->formatNum( $successCount );
-
if( $successCount > 0 ) {
$this->getOutput()->addHTML( "<li>" . Linker::linkKnown( $title ) . " " .
- wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
+ $this->msg( 'import-revision-count' )->numParams( $successCount )->escaped() .
"</li>\n"
);
$log = new LogPage( 'import' );
if( $this->mIsUpload ) {
- $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
- $contentCount );
+ $detail = $this->msg( 'import-logentry-upload-detail' )->numParams(
+ $successCount )->inContentLanguage()->text();
if ( $this->reason ) {
- $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
+ $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason;
}
$log->addEntry( 'upload', $title, $detail );
} else {
$interwiki = '[[:' . $this->mInterwiki . ':' .
$origTitle->getPrefixedText() . ']]';
- $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
- $contentCount, $interwiki );
+ $detail = $this->msg( 'import-logentry-interwiki-detail' )->numParams(
+ $successCount )->params( $interwiki )->inContentLanguage()->text();
if ( $this->reason ) {
- $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
+ $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason;
}
$log->addEntry( 'interwiki', $title, $detail );
}
@@ -395,7 +427,7 @@ class ImportReporter extends ContextSource {
$comment = $detail; // quick
$dbw = wfGetDB( DB_MASTER );
$latest = $title->getLatestRevID();
- $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true );
+ $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleID(), $comment, true );
if (!is_null($nullRevision)) {
$nullRevision->insertOn( $dbw );
$page = WikiPage::factory( $title );
@@ -405,15 +437,14 @@ class ImportReporter extends ContextSource {
}
} else {
$this->getOutput()->addHTML( "<li>" . Linker::linkKnown( $title ) . " " .
- wfMsgHtml( 'import-nonewrevisions' ) . "</li>\n" );
+ $this->msg( 'import-nonewrevisions' )->escaped() . "</li>\n" );
}
}
function close() {
$out = $this->getOutput();
if ( $this->mLogItemCount > 0 ) {
- $msg = wfMsgExt( 'imported-log-entries', 'parseinline',
- $this->getLanguage()->formatNum( $this->mLogItemCount ) );
+ $msg = $this->msg( 'imported-log-entries' )->numParams( $this->mLogItemCount )->parse();
$out->addHTML( Xml::tags( 'li', null, $msg ) );
} elseif( $this->mPageCount == 0 && $this->mLogItemCount == 0 ) {
$out->addHTML( "</ul>\n" );
diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php
index d7e1655f..c217eccb 100644
--- a/includes/specials/SpecialJavaScriptTest.php
+++ b/includes/specials/SpecialJavaScriptTest.php
@@ -1,5 +1,29 @@
<?php
-
+/**
+ * Implements Special:JavaScriptTest
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
class SpecialJavaScriptTest extends SpecialPage {
/**
@@ -37,26 +61,24 @@ class SpecialJavaScriptTest extends SpecialPage {
// No framework specified
if ( $par == '' ) {
- $out->setPagetitle( wfMsgHtml( 'javascripttest' ) );
+ $out->setPageTitle( $this->msg( 'javascripttest' ) );
$summary = $this->wrapSummaryHtml(
- wfMsgHtml( 'javascripttest-pagetext-noframework' ) . $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( wfMsgHtml( 'javascripttest-title', wfMsgHtml( "javascripttest-$framework-name" ) ) );
- $out->setSubtitle(
- wfMessage( 'javascripttest-backlink' )->rawParams( Linker::linkKnown( $this->getTitle() ) )->escaped()
- );
+ $out->setPageTitle( $this->msg( 'javascripttest-title', $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 {
- $out->setPagetitle( wfMsgHtml( 'javascripttest' ) );
+ $out->setPageTitle( $this->msg( 'javascripttest' ) );
$summary = $this->wrapSummaryHtml( '<p class="error">'
- . wfMsgHtml( 'javascripttest-pagetext-unknownframework', $par )
+ . $this->msg( 'javascripttest-pagetext-unknownframework', $par )->escaped()
. '</p>'
. $this->getFrameworkListHtml(),
'unknownframework'
@@ -75,11 +97,11 @@ class SpecialJavaScriptTest extends SpecialPage {
$list .= Html::rawElement(
'li',
array(),
- Linker::link( $this->getTitle( $framework ), wfMsgHtml( "javascripttest-$framework-name" ) )
+ Linker::link( $this->getTitle( $framework ), $this->msg( "javascripttest-$framework-name" )->escaped() )
);
}
$list .= '</ul>';
- $msg = wfMessage( 'javascripttest-pagetext-frameworks' )->rawParams( $list )->parseAsBlock();
+ $msg = $this->msg( 'javascripttest-pagetext-frameworks' )->rawParams( $list )->parseAsBlock();
return $msg;
}
@@ -90,6 +112,7 @@ class SpecialJavaScriptTest extends SpecialPage {
* be thrown.
* @param $html String: The raw HTML.
* @param $state String: State, one of 'noframework', 'unknownframework' or 'frameworkfound'
+ * @return string
*/
private function wrapSummaryHtml( $html, $state ) {
$validStates = array( 'noframework', 'unknownframework', 'frameworkfound' );
@@ -106,7 +129,7 @@ class SpecialJavaScriptTest extends SpecialPage {
* Initialize the page for QUnit.
*/
private function initQUnitTesting() {
- global $wgJavaScriptTestConfig, $wgLang;
+ global $wgJavaScriptTestConfig;
$out = $this->getOutput();
@@ -114,11 +137,11 @@ class SpecialJavaScriptTest extends SpecialPage {
$qunitTestModules = $out->getResourceLoader()->getTestModuleNames( 'qunit' );
$out->addModules( $qunitTestModules );
- $summary = wfMessage( 'javascripttest-qunit-intro' )
+ $summary = $this->msg( 'javascripttest-qunit-intro' )
->params( $wgJavaScriptTestConfig['qunit']['documentation'] )
->parseAsBlock();
- $header = wfMessage( 'javascripttest-qunit-heading' )->escaped();
- $userDir = $wgLang->getDir();
+ $header = $this->msg( 'javascripttest-qunit-heading' )->escaped();
+ $userDir = $this->getLanguage()->getDir();
$baseHtml = <<<HTML
<div class="mw-content-ltr">
@@ -132,6 +155,14 @@ class SpecialJavaScriptTest extends SpecialPage {
HTML;
$out->addHtml( $this->wrapSummaryHtml( $summary, 'frameworkfound' ) . $baseHtml );
+ // This special page is disabled by default ($wgEnableJavaScriptTest), and contains
+ // no sensitive data. In order to allow TestSwarm to embed it into a test client window,
+ // we need to allow iframing of this page.
+ $out->allowClickjacking();
+
+ // Used in ./tests/qunit/data/testrunner.js, see also documentation of
+ // $wgJavaScriptTestConfig in DefaultSettings.php
+ $out->addJsConfigVars( 'QUnitTestSwarmInjectJSPath', $wgJavaScriptTestConfig['qunit']['testswarm-injectjs'] );
}
public function isListed(){
diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php
index d3ab2f04..0810ee77 100644
--- a/includes/specials/SpecialLinkSearch.php
+++ b/includes/specials/SpecialLinkSearch.php
@@ -88,13 +88,22 @@ class LinkSearchPage extends QueryPage {
$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(), wfMsg( 'linksearch' ) ) .
- Xml::inputLabel( wfMsg( 'linksearch-pat' ), 'target', 'target', 50, $target ) . ' ';
+ Xml::element( 'legend', array(), $this->msg( 'linksearch' )->text() ) .
+ Xml::inputLabel( $this->msg( 'linksearch-pat' )->text(), 'target', 'target', 50, $target ) . ' ';
if ( !$wgMiserMode ) {
- $s .= Xml::label( wfMsg( 'linksearch-ns' ), 'namespace' ) . ' ' .
- Xml::namespaceSelector( $namespace, '' );
+ $s .= Html::namespaceSelector(
+ array(
+ 'selected' => $namespace,
+ 'all' => '',
+ 'label' => $this->msg( 'linksearch-ns' )->text()
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ );
}
- $s .= Xml::submitButton( wfMsg( 'linksearch-ok' ) ) .
+ $s .= Xml::submitButton( $this->msg( 'linksearch-ok' )->text() ) .
'</fieldset>' .
Xml::closeElement( 'form' );
$out->addHTML( $s );
@@ -112,6 +121,7 @@ class LinkSearchPage extends QueryPage {
/**
* Disable RSS/Atom feeds
+ * @return bool
*/
function isSyndicated() {
return false;
@@ -161,9 +171,9 @@ class LinkSearchPage extends QueryPage {
$like = $dbr->buildLike( $stripped );
$retval = array (
'tables' => array ( 'page', 'externallinks' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'el_index AS value', 'el_to AS url' ),
+ '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 )
@@ -180,7 +190,7 @@ class LinkSearchPage extends QueryPage {
$pageLink = Linker::linkKnown( $title );
$urlLink = Linker::makeExternalLink( $url, $url );
- return wfMsgHtml( 'linksearch-line', $urlLink, $pageLink );
+ return $this->msg( 'linksearch-line' )->rawParams( $urlLink, $pageLink )->escaped();
}
/**
@@ -203,6 +213,7 @@ class LinkSearchPage extends QueryPage {
* We do a truncated index search, so the optimizer won't trust
* it as good enough for optimizing sort. The implicit ordering
* from the scan will usually do well enough for our needs.
+ * @return array
*/
function getOrderFields() {
return array();
diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php
index b5754991..cc055221 100644
--- a/includes/specials/SpecialListfiles.php
+++ b/includes/specials/SpecialListfiles.php
@@ -107,15 +107,15 @@ class ImageListPager extends TablePager {
if ( !$this->mFieldNames ) {
global $wgMiserMode;
$this->mFieldNames = array(
- 'img_timestamp' => wfMsg( 'listfiles_date' ),
- 'img_name' => wfMsg( 'listfiles_name' ),
- 'thumb' => wfMsg( 'listfiles_thumb' ),
- 'img_size' => wfMsg( 'listfiles_size' ),
- 'img_user_text' => wfMsg( 'listfiles_user' ),
- 'img_description' => wfMsg( 'listfiles_description' ),
+ 'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
+ 'img_name' => $this->msg( 'listfiles_name' )->text(),
+ 'thumb' => $this->msg( 'listfiles_thumb' )->text(),
+ 'img_size' => $this->msg( 'listfiles_size' )->text(),
+ 'img_user_text' => $this->msg( 'listfiles_user' )->text(),
+ 'img_description' => $this->msg( 'listfiles_description' )->text(),
);
if( !$wgMiserMode ) {
- $this->mFieldNames['count'] = wfMsg( 'listfiles_count' );
+ $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
}
}
return $this->mFieldNames;
@@ -156,9 +156,8 @@ class ImageListPager extends TablePager {
if( $dbr->implicitGroupby() ) {
$options = array( 'GROUP BY' => 'img_name' );
} else {
- $columnlist = implode( ',',
- preg_grep( '/^img/', array_keys( $this->getFieldNames() ) ) );
- $options = array( 'GROUP BY' => "img_user, $columnlist" );
+ $columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) );
+ $options = array( 'GROUP BY' => array_merge( array( 'img_user' ), $columnlist ) );
}
$join_conds = array( 'oldimage' => array( 'LEFT JOIN', 'oi_name = img_name' ) );
}
@@ -175,20 +174,14 @@ class ImageListPager extends TablePager {
return 'img_timestamp';
}
- function getStartBody() {
- # Do a link batch query for user pages
- if ( $this->mResult->numRows() ) {
- $lb = new LinkBatch;
- $this->mResult->seek( 0 );
- foreach ( $this->mResult as $row ) {
- if ( $row->img_user ) {
- $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) );
- }
- }
- $lb->execute();
+ function doBatchLookups() {
+ $userIds = array();
+ $this->mResult->seek( 0 );
+ foreach ( $this->mResult as $row ) {
+ $userIds[] = $row->img_user;
}
-
- return parent::getStartBody();
+ # Do a link batch query for names and userpages
+ UserCache::singleton()->doQuery( $userIds, array( 'userpage' ), __METHOD__ );
}
function formatValue( $field, $value ) {
@@ -198,10 +191,10 @@ class ImageListPager extends TablePager {
$thumb = $file->transform( array( 'width' => 180, 'height' => 360 ) );
return $thumb->toHtml( array( 'desc-link' => true ) );
case 'img_timestamp':
- return htmlspecialchars( $this->getLanguage()->timeanddate( $value, true ) );
+ return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
case 'img_name':
static $imgfile = null;
- if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' );
+ if ( $imgfile === null ) $imgfile = $this->msg( 'imgfile' )->text();
// Weird files can maybe exist? Bug 22227
$filePage = Title::makeTitleSafe( NS_FILE, $value );
@@ -211,15 +204,17 @@ class ImageListPager extends TablePager {
array( 'href' => wfLocalFile( $filePage )->getURL() ),
$imgfile
);
- return "$link ($download)";
+ $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
+ return "$link $download";
} else {
return htmlspecialchars( $value );
}
case 'img_user_text':
if ( $this->mCurrentRow->img_user ) {
+ $name = User::whoIs( $this->mCurrentRow->img_user );
$link = Linker::link(
- Title::makeTitle( NS_USER, $value ),
- htmlspecialchars( $value )
+ Title::makeTitle( NS_USER, $name ),
+ htmlspecialchars( $name )
);
} else {
$link = htmlspecialchars( $value );
@@ -253,9 +248,10 @@ class ImageListPager extends TablePager {
) );
return Html::openElement( 'form',
array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' ) ) .
- Xml::fieldset( wfMsg( 'listfiles' ) ) .
+ 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' ) ) .
+ $this->getHiddenFields( array( 'limit', 'ilsearch', 'user', 'title' ) ) .
Html::closeElement( 'fieldset' ) .
Html::closeElement( 'form' ) . "\n";
}
@@ -290,4 +286,8 @@ class ImageListPager extends TablePager {
}
return $queries;
}
+
+ function getTitle() {
+ return SpecialPage::getTitleFor( 'Listfiles' );
+ }
}
diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php
index 91d8ed87..1f95c225 100644
--- a/includes/specials/SpecialListgrouprights.php
+++ b/includes/specials/SpecialListgrouprights.php
@@ -54,8 +54,8 @@ class SpecialListGroupRights extends SpecialPage {
$out->addHTML(
Xml::openElement( 'table', array( 'class' => 'wikitable mw-listgrouprights-table' ) ) .
'<tr>' .
- Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) .
- Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) .
+ Xml::element( 'th', null, $this->msg( 'listgrouprights-group' )->text() ) .
+ Xml::element( 'th', null, $this->msg( 'listgrouprights-rights' )->text() ) .
'</tr>'
);
@@ -77,10 +77,10 @@ class SpecialListGroupRights extends SpecialPage {
? 'all'
: $group;
- $msg = wfMessage( 'group-' . $groupname );
+ $msg = $this->msg( 'group-' . $groupname );
$groupnameLocalized = !$msg->isBlank() ? $msg->text() : $groupname;
- $msg = wfMessage( 'grouppage-' . $groupname )->inContentLanguage();
+ $msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage();
$grouppageLocalized = !$msg->isBlank() ?
$msg->text() :
MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
@@ -99,12 +99,12 @@ class SpecialListGroupRights extends SpecialPage {
// Link to Special:listusers for implicit group 'user'
$grouplink = '<br />' . Linker::linkKnown(
SpecialPage::getTitleFor( 'Listusers' ),
- wfMsgHtml( 'listgrouprights-members' )
+ $this->msg( 'listgrouprights-members' )->escaped()
);
} elseif ( !in_array( $group, $wgImplicitGroups ) ) {
$grouplink = '<br />' . Linker::linkKnown(
SpecialPage::getTitleFor( 'Listusers' ),
- wfMsgHtml( 'listgrouprights-members' ),
+ $this->msg( 'listgrouprights-members' )->escaped(),
array(),
array( 'group' => $group )
);
@@ -152,59 +152,59 @@ class SpecialListGroupRights extends SpecialPage {
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] ) ) {
- $description = wfMsgExt( 'listgrouprights-right-display', array( 'parseinline' ),
+ $description = $this->msg( 'listgrouprights-right-display',
User::getRightDescription( $permission ),
'<span class="mw-listgrouprights-right-name">' . $permission . '</span>'
- );
+ )->parse();
$r[] = $description;
}
}
foreach( $revoke as $permission => $revoked ) {
if( $revoked ) {
- $description = wfMsgExt( 'listgrouprights-right-revoked', array( 'parseinline' ),
+ $description = $this->msg( 'listgrouprights-right-revoked',
User::getRightDescription( $permission ),
'<span class="mw-listgrouprights-right-name">' . $permission . '</span>'
- );
+ )->parse();
$r[] = $description;
}
}
sort( $r );
$lang = $this->getLanguage();
if( $add === true ){
- $r[] = wfMsgExt( 'listgrouprights-addgroup-all', array( 'escape' ) );
+ $r[] = $this->msg( 'listgrouprights-addgroup-all' )->escaped();
} elseif( is_array( $add ) && count( $add ) ) {
$add = array_values( array_unique( $add ) );
- $r[] = wfMsgExt( 'listgrouprights-addgroup', array( 'parseinline' ),
+ $r[] = $this->msg( 'listgrouprights-addgroup',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ),
count( $add )
- );
+ )->parse();
}
if( $remove === true ){
- $r[] = wfMsgExt( 'listgrouprights-removegroup-all', array( 'escape' ) );
+ $r[] = $this->msg( 'listgrouprights-removegroup-all' )->escaped();
} elseif( is_array( $remove ) && count( $remove ) ) {
$remove = array_values( array_unique( $remove ) );
- $r[] = wfMsgExt( 'listgrouprights-removegroup', array( 'parseinline' ),
+ $r[] = $this->msg( 'listgrouprights-removegroup',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ),
count( $remove )
- );
+ )->parse();
}
if( $addSelf === true ){
- $r[] = wfMsgExt( 'listgrouprights-addgroup-self-all', array( 'escape' ) );
+ $r[] = $this->msg( 'listgrouprights-addgroup-self-all' )->escaped();
} elseif( is_array( $addSelf ) && count( $addSelf ) ) {
$addSelf = array_values( array_unique( $addSelf ) );
- $r[] = wfMsgExt( 'listgrouprights-addgroup-self', array( 'parseinline' ),
+ $r[] = $this->msg( 'listgrouprights-addgroup-self',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ),
count( $addSelf )
- );
+ )->parse();
}
if( $removeSelf === true ){
- $r[] = wfMsgExt( 'listgrouprights-removegroup-self-all', array( 'escape' ) );
+ $r[] = $this->msg( 'listgrouprights-removegroup-self-all' )->parse();
} elseif( is_array( $removeSelf ) && count( $removeSelf ) ) {
$removeSelf = array_values( array_unique( $removeSelf ) );
- $r[] = wfMsgExt( 'listgrouprights-removegroup-self', array( 'parseinline' ),
+ $r[] = $this->msg( 'listgrouprights-removegroup-self',
$lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ),
count( $removeSelf )
- );
+ )->parse();
}
if( empty( $r ) ) {
return '';
diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php
index f9cf3e6e..fe338a08 100644
--- a/includes/specials/SpecialListredirects.php
+++ b/includes/specials/SpecialListredirects.php
@@ -41,14 +41,14 @@ class ListredirectsPage extends QueryPage {
function getQueryInfo() {
return array(
'tables' => array( 'p1' => 'page', 'redirect', 'p2' => 'page' ),
- 'fields' => array( 'p1.page_namespace AS namespace',
- 'p1.page_title AS title',
- 'p1.page_title AS value',
+ 'fields' => array( 'namespace' => 'p1.page_namespace',
+ 'title' => 'p1.page_title',
+ 'value' => 'p1.page_title',
'rd_namespace',
'rd_title',
'rd_fragment',
'rd_interwiki',
- 'p2.page_id AS redirid' ),
+ 'redirid' => 'p2.page_id' ),
'conds' => array( 'p1.page_is_redirect' => 1 ),
'join_conds' => array( 'redirect' => array(
'LEFT JOIN', 'rd_from=p1.page_id' ),
diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php
index d743712d..1089fbbe 100644
--- a/includes/specials/SpecialListusers.php
+++ b/includes/specials/SpecialListusers.php
@@ -34,7 +34,11 @@
*/
class UsersPager extends AlphabeticPager {
- function __construct( IContextSource $context = null, $par = null ) {
+ /**
+ * @param $context IContextSource
+ * @param $par null|array
+ */
+ function __construct( IContextSource $context = null, $par = null, $including = null ) {
if ( $context ) {
$this->setContext( $context );
}
@@ -58,6 +62,7 @@ class UsersPager extends AlphabeticPager {
}
$this->editsOnly = $request->getBool( 'editsOnly' );
$this->creationSort = $request->getBool( 'creationSort' );
+ $this->including = $including;
$this->requestedUser = '';
if ( $un != '' ) {
@@ -69,15 +74,21 @@ class UsersPager extends AlphabeticPager {
parent::__construct();
}
+ /**
+ * @return string
+ */
function getIndexField() {
return $this->creationSort ? 'user_id' : 'user_name';
}
+ /**
+ * @return Array
+ */
function getQueryInfo() {
$dbr = wfGetDB( DB_SLAVE );
$conds = array();
// Don't show hidden names
- if( !$this->getUser()->isAllowed('hideuser') ) {
+ if( !$this->getUser()->isAllowed( 'hideuser' ) ) {
$conds[] = 'ipb_deleted IS NULL';
}
@@ -105,18 +116,22 @@ class UsersPager extends AlphabeticPager {
$query = array(
'tables' => array( 'user', 'user_groups', 'ipblocks'),
'fields' => array(
- $this->creationSort ? 'MAX(user_name) AS user_name' : 'user_name',
- $this->creationSort ? 'user_id' : 'MAX(user_id) AS user_id',
- 'MAX(user_editcount) AS edits',
- 'COUNT(ug_group) AS numgroups',
- 'MAX(ug_group) AS singlegroup', // the usergroup if there is only one
- 'MIN(user_registration) AS creation',
- 'MAX(ipb_deleted) AS ipb_deleted' // block/hide status
+ 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name',
+ 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)',
+ 'edits' => 'MAX(user_editcount)',
+ 'numgroups' => 'COUNT(ug_group)',
+ 'singlegroup' => 'MAX(ug_group)', // the usergroup if there is only one
+ 'creation' => 'MIN(user_registration)',
+ 'ipb_deleted' => 'MAX(ipb_deleted)' // block/hide status
),
'options' => $options,
'join_conds' => array(
'user_groups' => array( 'LEFT JOIN', 'user_id=ug_user' ),
- 'ipblocks' => array( 'LEFT JOIN', 'user_id=ipb_user AND ipb_deleted=1 AND ipb_auto=0' ),
+ 'ipblocks' => array( 'LEFT JOIN', array(
+ 'user_id=ipb_user',
+ 'ipb_deleted' => 1,
+ 'ipb_auto' => 0
+ )),
),
'conds' => $conds
);
@@ -125,95 +140,101 @@ class UsersPager extends AlphabeticPager {
return $query;
}
+ /**
+ * @param $row Object
+ * @return String
+ */
function formatRow( $row ) {
- if ($row->user_id == 0) #Bug 16487
+ if ( $row->user_id == 0 ) { #Bug 16487
return '';
+ }
- $userPage = Title::makeTitle( NS_USER, $row->user_name );
- $name = Linker::link( $userPage, htmlspecialchars( $userPage->getText() ) );
+ $userName = $row->user_name;
+
+ $ulinks = Linker::userLink( $row->user_id, $userName );
+ $ulinks .= Linker::userToolLinks( $row->user_id, $userName );
$lang = $this->getLanguage();
+ $groups = '';
$groups_list = self::getGroups( $row->user_id );
- if( count( $groups_list ) > 0 ) {
+ if( !$this->including && count( $groups_list ) > 0 ) {
$list = array();
foreach( $groups_list as $group )
- $list[] = self::buildGroupLink( $group, $userPage->getText() );
+ $list[] = self::buildGroupLink( $group, $userName );
$groups = $lang->commaList( $list );
- } else {
- $groups = '';
}
- $item = $lang->specialList( $name, $groups );
+ $item = $lang->specialList( $ulinks, $groups );
if( $row->ipb_deleted ) {
$item = "<span class=\"deleted\">$item</span>";
}
+ $edits = '';
global $wgEdititis;
- if ( $wgEdititis ) {
- $editCount = $lang->formatNum( $row->edits );
- $edits = ' [' . wfMsgExt( 'usereditcount', array( 'parsemag', 'escape' ), $editCount ) . ']';
- } else {
- $edits = '';
+ if ( !$this->including && $wgEdititis ) {
+ $edits = ' [' . $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped() . ']';
}
$created = '';
# Some rows may be NULL
- if( $row->creation ) {
- $d = $lang->date( wfTimestamp( TS_MW, $row->creation ), true );
- $t = $lang->time( wfTimestamp( TS_MW, $row->creation ), true );
- $created = ' (' . wfMsgExt( 'usercreated', array( 'parsemag', 'escape' ), $d, $t, $row->user_name ) . ')';
+ 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();
}
wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
- return "<li>{$item}{$edits}{$created}</li>";
+ return Html::rawElement( 'li', array(), "{$item}{$edits}{$created}" );
}
- function getBody() {
- if( !$this->mQueryDone ) {
- $this->doQuery();
- }
- $this->mResult->rewind();
- $batch = new LinkBatch;
+ function doBatchLookups() {
+ $batch = new LinkBatch();
+ # Give some pointers to make user links
foreach ( $this->mResult as $row ) {
- $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+ $batch->add( NS_USER, $row->user_name );
+ $batch->add( NS_USER_TALK, $row->user_name );
}
$batch->execute();
$this->mResult->rewind();
- return parent::getBody();
}
+ /**
+ * @return string
+ */
function getPageHeader( ) {
global $wgScript;
- // @todo Add a PrefixedBaseDBKey
+
list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() );
# Form tag
$out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listusers-form' ) ) .
- Xml::fieldset( wfMsg( 'listusers' ) ) .
+ Xml::fieldset( $this->msg( 'listusers' )->text() ) .
Html::hidden( 'title', $self );
# Username field
- $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' .
+ $out .= Xml::label( $this->msg( 'listusersfrom' )->text(), 'offset' ) . ' ' .
Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' ';
# Group drop-down list
- $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' .
+ $out .= Xml::label( $this->msg( 'group' )->text(), 'group' ) . ' ' .
Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) .
- Xml::option( wfMsg( 'group-all' ), '' );
+ Xml::option( $this->msg( 'group-all' )->text(), '' );
foreach( $this->getAllGroups() as $group => $groupText )
$out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
$out .= Xml::closeElement( 'select' ) . '<br />';
- $out .= Xml::checkLabel( wfMsg('listusers-editsonly'), 'editsOnly', 'editsOnly', $this->editsOnly );
+ $out .= Xml::checkLabel( $this->msg( 'listusers-editsonly' )->text(), 'editsOnly', 'editsOnly', $this->editsOnly );
$out .= '&#160;';
- $out .= Xml::checkLabel( wfMsg('listusers-creationsort'), 'creationSort', 'creationSort', $this->creationSort );
+ $out .= Xml::checkLabel( $this->msg( 'listusers-creationsort' )->text(), 'creationSort', 'creationSort', $this->creationSort );
$out .= '<br />';
wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
# Submit button and form bottom
$out .= Html::hidden( 'limit', $this->mLimit );
- $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+ $out .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() );
wfRunHooks( 'SpecialListusersHeader', array( $this, &$out ) );
$out .= Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' );
@@ -240,10 +261,12 @@ 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;
}
@@ -282,6 +305,7 @@ class SpecialListUsers extends SpecialPage {
*/
public function __construct() {
parent::__construct( 'Listusers' );
+ $this->mIncludable = true;
}
/**
@@ -293,18 +317,22 @@ class SpecialListUsers extends SpecialPage {
$this->setHeaders();
$this->outputHeader();
- $up = new UsersPager( $this->getContext(), $par );
+ $up = new UsersPager( $this->getContext(), $par, $this->including() );
# getBody() first to check, if empty
$usersbody = $up->getBody();
- $s = $up->getPageHeader();
+ $s = '';
+ if ( !$this->including() ) {
+ $s = $up->getPageHeader();
+ }
+
if( $usersbody ) {
$s .= $up->getNavigationBar();
$s .= Html::rawElement( 'ul', array(), $usersbody );
$s .= $up->getNavigationBar();
} else {
- $s .= wfMessage( 'listusers-noresult' )->parseAsBlock();
+ $s .= $this->msg( 'listusers-noresult' )->parseAsBlock();
}
$this->getOutput()->addHTML( $s );
diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php
index c1453518..d71ac6e1 100644
--- a/includes/specials/SpecialLockdb.php
+++ b/includes/specials/SpecialLockdb.php
@@ -87,13 +87,11 @@ class SpecialLockdb extends FormSpecialPage {
}
fwrite( $fp, $data['Reason'] );
$timestamp = wfTimestampNow();
- fwrite( $fp, "\n<p>" . wfMsgExt(
- 'lockedbyandtime',
- array( 'content', 'parsemag' ),
+ fwrite( $fp, "\n<p>" . $this->msg( 'lockedbyandtime',
$this->getUser()->getName(),
- $wgContLang->date( $timestamp ),
- $wgContLang->time( $timestamp )
- ) . "</p>\n" );
+ $wgContLang->date( $timestamp, false, false ),
+ $wgContLang->time( $timestamp, false, false )
+ )->inContentLanguage()->text() . "</p>\n" );
fclose( $fp );
return Status::newGood();
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
index 64190df1..7800e566 100644
--- a/includes/specials/SpecialLog.php
+++ b/includes/specials/SpecialLog.php
@@ -33,7 +33,7 @@ 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
- * Title user instead.
+ * Title user instead.
*/
private $typeOnUser = array(
'block',
@@ -47,7 +47,7 @@ class SpecialLog extends SpecialPage {
public function execute( $par ) {
global $wgLogRestrictions;
-
+
$this->setHeaders();
$this->outputHeader();
@@ -65,7 +65,7 @@ class SpecialLog extends SpecialPage {
// Set values
$opts->fetchValuesFromRequest( $this->getRequest() );
- if ( $par ) {
+ if ( $par !== null ) {
$this->parseParams( $opts, (string)$par );
}
@@ -131,7 +131,7 @@ 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->getSkin(), $this->getOutput(), 0 );
+ $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' ) );
@@ -152,9 +152,7 @@ class SpecialLog extends SpecialPage {
if ( $logBody ) {
$this->getOutput()->addHTML(
$pager->getNavigationBar() .
- $loglist->beginLogEventsList() .
- $logBody .
- $loglist->endLogEventsList() .
+ $this->getRevisionButton( $loglist->beginLogEventsList() . $logBody . $loglist->endLogEventsList() ) .
$pager->getNavigationBar()
);
} else {
@@ -162,6 +160,29 @@ 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 ( !$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::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" ),
+ $this->msg( 'showhideselectedlogentries' )->text()
+ ) . "\n";
+ $s .= $button . $formcontents . $button;
+ $s .= Html::closeElement( 'form' );
+
+ return $s;
+ }
+
+
/**
* Set page title and show header for this log type
* @param $type string
diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php
index 0800e43c..763bbdb1 100644
--- a/includes/specials/SpecialLonelypages.php
+++ b/includes/specials/SpecialLonelypages.php
@@ -34,7 +34,7 @@ class LonelyPagesPage extends PageQueryPage {
}
function getPageHeader() {
- return wfMsgExt( 'lonelypagestext', array( 'parse' ) );
+ return $this->msg( 'lonelypagestext' )->parseAsBlock();
}
function sortDescending() {
@@ -50,9 +50,9 @@ class LonelyPagesPage extends PageQueryPage {
return array (
'tables' => array ( 'page', 'pagelinks',
'templatelinks' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_title AS value' ),
+ '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,
diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php
index 2213ffa4..104c653f 100644
--- a/includes/specials/SpecialMIMEsearch.php
+++ b/includes/specials/SpecialMIMEsearch.php
@@ -45,9 +45,9 @@ class MIMEsearchPage extends QueryPage {
public function getQueryInfo() {
return array(
'tables' => array( 'image' ),
- 'fields' => array( "'" . NS_FILE . "' AS namespace",
- 'img_name AS title',
- 'img_major_mime AS value',
+ 'fields' => array( 'namespace' => NS_FILE,
+ 'title' => 'img_name',
+ 'value' => 'img_major_mime',
'img_size',
'img_width',
'img_height',
@@ -59,17 +59,19 @@ class MIMEsearchPage extends QueryPage {
}
function execute( $par ) {
+ global $wgScript;
+
$mime = $par ? $par : $this->getRequest()->getText( 'mime' );
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addHTML(
- Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => SpecialPage::getTitleFor( 'MIMEsearch' )->getLocalUrl() ) ) .
+ Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgScript ) ) .
Xml::openElement( 'fieldset' ) .
- Html::hidden( 'title', SpecialPage::getTitleFor( 'MIMEsearch' )->getPrefixedText() ) .
- Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
- Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
- Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
+ 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' )
);
@@ -93,17 +95,16 @@ class MIMEsearchPage extends QueryPage {
htmlspecialchars( $text )
);
- $download = Linker::makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) );
+ $download = Linker::makeMediaLinkObj( $nt, $this->msg( 'download' )->escaped() );
+ $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
$lang = $this->getLanguage();
$bytes = htmlspecialchars( $lang->formatSize( $result->img_size ) );
- $dimensions = htmlspecialchars( wfMsg( 'widthheight',
- $lang->formatNum( $result->img_width ),
- $lang->formatNum( $result->img_height )
- ) );
+ $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->timeanddate( $result->img_timestamp ) );
+ $time = htmlspecialchars( $lang->userTimeAndDate( $result->img_timestamp, $this->getUser() ) );
- return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
+ return "$download $plink . . $dimensions . . $bytes . . $user . . $time";
}
/**
diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php
index 19650da9..1f057499 100644
--- a/includes/specials/SpecialMergeHistory.php
+++ b/includes/specials/SpecialMergeHistory.php
@@ -76,7 +76,7 @@ class SpecialMergeHistory extends SpecialPage {
function preCacheMessages() {
// Precache various messages
if( !isset( $this->message ) ) {
- $this->message['last'] = wfMsgExt( 'last', array( 'escape' ) );
+ $this->message['last'] = $this->msg( 'last' )->escaped();
}
}
@@ -90,7 +90,8 @@ class SpecialMergeHistory extends SpecialPage {
$this->outputHeader();
if( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
- return $this->merge();
+ $this->merge();
+ return;
}
if ( !$this->mSubmitted ) {
@@ -100,23 +101,23 @@ class SpecialMergeHistory extends SpecialPage {
$errors = array();
if ( !$this->mTargetObj instanceof Title ) {
- $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
+ $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
} elseif( !$this->mTargetObj->exists() ) {
- $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
+ $errors[] = $this->msg( 'mergehistory-no-source', array( 'parse' ),
wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
- );
+ )->parseAsBlock();
}
if ( !$this->mDestObj instanceof Title ) {
- $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
+ $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
} elseif( !$this->mDestObj->exists() ) {
- $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
+ $errors[] = $this->msg( 'mergehistory-no-destination', array( 'parse' ),
wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
- );
+ )->parseAsBlock();
}
if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
- $errors[] = wfMsgExt( 'mergehistory-same-destination', array( 'parse' ) );
+ $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock();
}
if ( count( $errors ) ) {
@@ -139,19 +140,19 @@ class SpecialMergeHistory extends SpecialPage {
'action' => $wgScript ) ) .
'<fieldset>' .
Xml::element( 'legend', array(),
- wfMsg( 'mergehistory-box' ) ) .
+ $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( wfMsg( 'mergehistory-from' ), 'target' ) . '</td>
+ <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( wfMsg( 'mergehistory-into' ), 'dest' ) . '</td>
+ <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( wfMsg( 'mergehistory-go' ) ) .
+ Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
'</td></tr>' .
Xml::closeElement( 'table' ) .
'</fieldset>' .
@@ -187,12 +188,12 @@ class SpecialMergeHistory extends SpecialPage {
# in a nice little table
$table =
Xml::openElement( 'fieldset' ) .
- wfMsgExt( 'mergehistory-merge', array( 'parseinline' ),
- $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .
+ $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( wfMsg( 'mergehistory-reason' ), 'wpComment' ) .
+ Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
'</td>
<td class="mw-input">' .
Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
@@ -201,7 +202,7 @@ class SpecialMergeHistory extends SpecialPage {
<tr>
<td>&#160;</td>
<td class="mw-submit">' .
- Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
+ Xml::submitButton( $this->msg( 'mergehistory-submit' )->text(), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
'</td>
</tr>' .
Xml::closeElement( 'table' ) .
@@ -212,7 +213,7 @@ class SpecialMergeHistory extends SpecialPage {
$out->addHTML(
'<h2 id="mw-mergehistory">' .
- wfMsgHtml( 'mergehistory-list' ) . "</h2>\n"
+ $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
);
if( $haveRevisions ) {
@@ -225,8 +226,9 @@ class SpecialMergeHistory extends SpecialPage {
$out->addWikiMsg( 'mergehistory-empty' );
}
- # Show relevant lines from the deletion log:
- $out->addHTML( '<h2>' . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );
+ # Show relevant lines from the merge log:
+ $mergeLogPage = new LogPage( 'merge' );
+ $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
# When we submit, go by page ID to avoid some nasty but unlikely collisions.
@@ -251,9 +253,11 @@ class SpecialMergeHistory extends SpecialPage {
$ts = wfTimestamp( TS_MW, $row->rev_timestamp );
$checkBox = Xml::radio( 'mergepoint', $ts, false );
+ $user = $this->getUser();
+
$pageLink = Linker::linkKnown(
$rev->getTitle(),
- htmlspecialchars( $this->getLanguage()->timeanddate( $ts ) ),
+ htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ),
array(),
array( 'oldid' => $rev->getId() )
);
@@ -262,7 +266,7 @@ class SpecialMergeHistory extends SpecialPage {
}
# Last link
- if( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+ if( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
$last = $this->message['last'];
} elseif( isset( $this->prevId[$row->rev_id] ) ) {
$last = Linker::linkKnown(
@@ -284,7 +288,8 @@ class SpecialMergeHistory extends SpecialPage {
}
$comment = Linker::revComment( $rev );
- return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";
+ return Html::rawElement( 'li', array(),
+ $this->msg( 'mergehistory-revisionrow' )->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() );
}
function merge() {
@@ -296,7 +301,7 @@ class SpecialMergeHistory extends SpecialPage {
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
@@ -355,18 +360,18 @@ class SpecialMergeHistory extends SpecialPage {
);
if( !$haveRevisions ) {
if( $this->mComment ) {
- $comment = wfMsgForContent(
+ $comment = $this->msg(
'mergehistory-comment',
$targetTitle->getPrefixedText(),
$destTitle->getPrefixedText(),
$this->mComment
- );
+ )->inContentLanguage()->text();
} else {
- $comment = wfMsgForContent(
+ $comment = $this->msg(
'mergehistory-autocomment',
$targetTitle->getPrefixedText(),
$destTitle->getPrefixedText()
- );
+ )->inContentLanguage()->text();
}
$mwRedir = MagicWord::get( 'redirect' );
$redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destTitle->getPrefixedText() . "]]\n";
@@ -404,9 +409,8 @@ class SpecialMergeHistory extends SpecialPage {
array( $destTitle->getPrefixedText(), $timestampLimit )
);
- $this->getOutput()->addHTML(
- wfMsgExt( 'mergehistory-success', array('parseinline'),
- $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
+ $this->getOutput()->addWikiMsg( 'mergehistory-success',
+ $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count );
wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php
index 98b73675..3f0bafa3 100644
--- a/includes/specials/SpecialMostcategories.php
+++ b/includes/specials/SpecialMostcategories.php
@@ -41,27 +41,57 @@ class MostcategoriesPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'categorylinks', 'page' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'COUNT(*) AS value' ),
+ 'fields' => array ( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'COUNT(*)' ),
'conds' => array ( 'page_namespace' => MWNamespace::getContentNamespaces() ),
'options' => array ( 'HAVING' => 'COUNT(*) > 1',
- 'GROUP BY' => 'page_namespace, page_title' ),
+ 'GROUP BY' => array( 'page_namespace', 'page_title' ) ),
'join_conds' => array ( 'page' => array ( 'LEFT JOIN',
'page_id = cl_from' ) )
);
}
/**
+ * @param $db DatabaseBase
+ * @param $res
+ */
+ function preprocessResults( $db, $res ) {
+ # There's no point doing a batch check if we aren't caching results;
+ # the page must exist for it to have been pulled out of the table
+ if ( !$this->isCached() || !$res->numRows() ) {
+ return;
+ }
+
+ $batch = new LinkBatch();
+ foreach ( $res as $row ) {
+ $batch->add( $row->namespace, $row->title );
+ }
+ $batch->execute();
+
+ $res->seek( 0 );
+ }
+
+ /**
* @param $skin Skin
* @param $result
* @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 ) );
+ }
+
+ if ( $this->isCached() ) {
+ $link = Linker::link( $title );
+ } else {
+ $link = Linker::linkKnown( $title );
+ }
$count = $this->msg( 'ncategories' )->numParams( $result->value )->escaped();
- $link = Linker::link( $title );
+
return $this->getLanguage()->specialList( $link, $count );
}
}
diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php
index 7805e53e..3d797908 100644
--- a/includes/specials/SpecialMostimages.php
+++ b/includes/specials/SpecialMostimages.php
@@ -41,9 +41,9 @@ class MostimagesPage extends ImageQueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'imagelinks' ),
- 'fields' => array ( "'" . NS_FILE . "' AS namespace",
- 'il_to AS title',
- 'COUNT(*) AS value' ),
+ 'fields' => array ( 'namespace' => NS_FILE,
+ 'title' => 'il_to',
+ 'value' => 'COUNT(*)' ),
'options' => array ( 'GROUP BY' => 'il_to',
'HAVING' => 'COUNT(*) > 1' )
);
diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php
new file mode 100644
index 00000000..894d697b
--- /dev/null
+++ b/includes/specials/SpecialMostinterwikis.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Implements Special:Mostinterwikis
+ *
+ * Copyright © 2012 Umherirrender
+ *
+ * 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 Umherirrender
+ */
+
+/**
+ * A special page that listed pages that have highest interwiki count
+ *
+ * @ingroup SpecialPage
+ */
+class MostinterwikisPage extends QueryPage {
+
+ function __construct( $name = 'Mostinterwikis' ) {
+ parent::__construct( $name );
+ }
+
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getQueryInfo() {
+ return array (
+ 'tables' => array (
+ 'langlinks',
+ '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 = ll_from'
+ )
+ )
+ );
+ }
+
+ /**
+ * Pre-fill the link cache
+ *
+ * @param $db DatabaseBase
+ * @param $res
+ */
+ function preprocessResults( $db, $res ) {
+ # There's no point doing a batch check if we aren't caching results;
+ # the page must exist for it to have been pulled out of the table
+ if ( !$this->isCached() || !$res->numRows() ) {
+ return;
+ }
+
+ $batch = new LinkBatch;
+ foreach ( $res as $row ) {
+ $batch->add( $row->namespace, $row->title );
+ }
+ $batch->execute();
+
+ // Back to start for display
+ $res->seek( 0 );
+ }
+
+ /**
+ * @param $skin Skin
+ * @param $result
+ * @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 ) );
+ }
+
+ if ( $this->isCached() ) {
+ $link = Linker::link( $title );
+ } else {
+ $link = Linker::linkKnown( $title );
+ }
+
+ $count = $this->msg( 'ninterwikis' )->numParams( $result->value )->escaped();
+
+ return $this->getLanguage()->specialList( $link, $count );
+ }
+}
diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php
index a16f0872..89c43509 100644
--- a/includes/specials/SpecialMostlinked.php
+++ b/includes/specials/SpecialMostlinked.php
@@ -42,13 +42,13 @@ class MostlinkedPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'pagelinks', 'page' ),
- 'fields' => array ( 'pl_namespace AS namespace',
- 'pl_title AS title',
- 'COUNT(*) AS value',
+ 'fields' => array ( 'namespace' => 'pl_namespace',
+ 'title' => 'pl_title',
+ 'value' => 'COUNT(*)',
'page_namespace' ),
'options' => array ( 'HAVING' => 'COUNT(*) > 1',
- 'GROUP BY' => 'pl_namespace, pl_title, '.
- 'page_namespace' ),
+ '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' ) ) )
@@ -62,12 +62,12 @@ class MostlinkedPage extends QueryPage {
* @param $res
*/
function preprocessResults( $db, $res ) {
- if( $db->numRows( $res ) > 0 ) {
+ if ( $res->numRows() > 0 ) {
$linkBatch = new LinkBatch();
foreach ( $res as $row ) {
$linkBatch->add( $row->namespace, $row->title );
}
- $db->dataSeek( $res, 0 );
+ $res->seek( 0 );
$linkBatch->execute();
}
}
@@ -94,7 +94,8 @@ class MostlinkedPage extends QueryPage {
function formatResult( $skin, $result ) {
$title = Title::makeTitleSafe( $result->namespace, $result->title );
if ( !$title ) {
- return '<!-- ' . htmlspecialchars( "Invalid title: [[$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,
diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php
index 7fb9dea9..dadef8bf 100644
--- a/includes/specials/SpecialMostlinkedcategories.php
+++ b/includes/specials/SpecialMostlinkedcategories.php
@@ -40,9 +40,9 @@ class MostlinkedCategoriesPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'category' ),
- 'fields' => array ( 'cat_title AS title',
- NS_CATEGORY . ' AS namespace',
- 'cat_pages AS value' ),
+ 'fields' => array ( 'title' => 'cat_title',
+ 'namespace' => NS_CATEGORY,
+ 'value' => 'cat_pages' ),
);
}
@@ -55,6 +55,10 @@ class MostlinkedCategoriesPage extends QueryPage {
* @param $res DatabaseResult
*/
function preprocessResults( $db, $res ) {
+ if ( !$res->numRows() ) {
+ return;
+ }
+
$batch = new LinkBatch;
foreach ( $res as $row ) {
$batch->add( NS_CATEGORY, $row->title );
@@ -62,10 +66,7 @@ class MostlinkedCategoriesPage extends QueryPage {
$batch->execute();
// Back to start for display
- if ( $db->numRows( $res ) > 0 ) {
- // If there are no rows we get an error seeking.
- $db->dataSeek( $res, 0 );
- }
+ $res->seek( 0 );
}
/**
@@ -76,7 +77,12 @@ class MostlinkedCategoriesPage extends QueryPage {
function formatResult( $skin, $result ) {
global $wgContLang;
- $nt = Title::makeTitle( NS_CATEGORY, $result->title );
+ $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 ) );
+ }
+
$text = $wgContLang->convert( $nt->getText() );
$plink = Linker::link( $nt, htmlspecialchars( $text ) );
diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php
index 6fb09426..22932e5c 100644
--- a/includes/specials/SpecialMostlinkedtemplates.php
+++ b/includes/specials/SpecialMostlinkedtemplates.php
@@ -64,28 +64,32 @@ class MostlinkedTemplatesPage extends QueryPage {
public function getQueryInfo() {
return array (
'tables' => array ( 'templatelinks' ),
- 'fields' => array ( 'tl_namespace AS namespace',
- 'tl_title AS title',
- 'COUNT(*) AS value' ),
+ 'fields' => array ( 'namespace' => 'tl_namespace',
+ 'title' => 'tl_title',
+ 'value' => 'COUNT(*)' ),
'conds' => array ( 'tl_namespace' => NS_TEMPLATE ),
- 'options' => array( 'GROUP BY' => 'tl_namespace, tl_title' )
+ 'options' => array( 'GROUP BY' => array( 'tl_namespace', 'tl_title' ) )
);
}
/**
* Pre-cache page existence to speed up link generation
*
- * @param $db Database connection
+ * @param $db DatabaseBase connection
* @param $res ResultWrapper
*/
public function preprocessResults( $db, $res ) {
+ if ( !$res->numRows() ) {
+ return;
+ }
+
$batch = new LinkBatch();
foreach ( $res as $row ) {
$batch->add( $row->namespace, $row->title );
}
$batch->execute();
- if( $db->numRows( $res ) > 0 )
- $db->dataSeek( $res, 0 );
+
+ $res->seek( 0 );
}
/**
@@ -96,7 +100,11 @@ class MostlinkedTemplatesPage extends QueryPage {
* @return String
*/
public function formatResult( $skin, $result ) {
- $title = Title::makeTitle( $result->namespace, $result->title );
+ $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 $this->getLanguage()->specialList(
Linker::link( $title ),
diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
index 5536fbc9..af3dbf3e 100644
--- a/includes/specials/SpecialMovepage.php
+++ b/includes/specials/SpecialMovepage.php
@@ -141,13 +141,13 @@ class MovePageForm extends UnlistedSpecialPage {
&& $newTitle->quickUserCan( 'delete', $user )
) {
$out->addWikiMsg( 'delete_and_move_text', $newTitle->getPrefixedText() );
- $movepagebtn = wfMsg( 'delete_and_move' );
+ $movepagebtn = $this->msg( 'delete_and_move' )->text();
$submitVar = 'wpDeleteAndMove';
$confirm = "
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) .
+ Xml::checkLabel( $this->msg( 'delete_and_move_confirm' )->text(), 'wpConfirm', 'wpConfirm' ) .
"</td>
</tr>";
$err = array();
@@ -157,7 +157,7 @@ class MovePageForm extends UnlistedSpecialPage {
}
$out->addWikiMsg( $wgFixDoubleRedirects ? 'movepagetext' :
'movepagetext-noredirectfixer' );
- $movepagebtn = wfMsg( 'movepagebtn' );
+ $movepagebtn = $this->msg( 'movepagebtn' )->text();
$submitVar = 'wpMove';
$confirm = false;
}
@@ -246,14 +246,22 @@ class MovePageForm extends UnlistedSpecialPage {
// Byte limit (not string length limit) for wpReason and wpNewTitleMain
// is enforced in the mediawiki.special.movePage module
+ $immovableNamespaces = array();
+
+ foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) {
+ if ( !MWNamespace::isMovable( $nsId ) ) {
+ $immovableNamespaces[] = $nsId;
+ }
+ }
+
$out->addHTML(
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'move-page-legend' ) ) .
- Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-movepage-table' ) ) .
+ Xml::element( 'legend', null, $this->msg( 'move-page-legend' )->text() ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-movepage-table' ) ) .
"<tr>
<td class='mw-label'>" .
- wfMsgHtml( 'movearticle' ) .
+ $this->msg( 'movearticle' )->escaped() .
"</td>
<td class='mw-input'>
<strong>{$oldTitleLink}</strong>
@@ -261,11 +269,14 @@ class MovePageForm extends UnlistedSpecialPage {
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'newtitle' ), 'wpNewTitleMain' ) .
+ Xml::label( $this->msg( 'newtitle' )->text(), 'wpNewTitleMain' ) .
"</td>
<td class='mw-input'>" .
Html::namespaceSelector(
- array( 'selected' => $newTitle->getNamespace() ),
+ array(
+ 'selected' => $newTitle->getNamespace(),
+ 'exclude' => $immovableNamespaces
+ ),
array( 'name' => 'wpNewTitleNs', 'id' => 'wpNewTitleNs' )
) .
Xml::input( 'wpNewTitleMain', 60, $wgContLang->recodeForEdit( $newTitle->getText() ), array(
@@ -278,7 +289,7 @@ class MovePageForm extends UnlistedSpecialPage {
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'movereason' ), '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,
@@ -292,7 +303,7 @@ class MovePageForm extends UnlistedSpecialPage {
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
+ Xml::checkLabel( $this->msg( 'movetalk' )->text(), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
"</td>
</tr>"
);
@@ -303,7 +314,7 @@ class MovePageForm extends UnlistedSpecialPage {
<tr>
<td></td>
<td class='mw-input' >" .
- Xml::checkLabel( wfMsg( 'move-leave-redirect' ), 'wpLeaveRedirect',
+ Xml::checkLabel( $this->msg( 'move-leave-redirect' )->text(), 'wpLeaveRedirect',
'wpLeaveRedirect', $this->leaveRedirect ) .
"</td>
</tr>"
@@ -315,7 +326,7 @@ class MovePageForm extends UnlistedSpecialPage {
<tr>
<td></td>
<td class='mw-input' >" .
- Xml::checkLabel( wfMsg( 'fix-double-redirects' ), 'wpFixRedirects',
+ Xml::checkLabel( $this->msg( 'fix-double-redirects' )->text(), 'wpFixRedirects',
'wpFixRedirects', $this->fixRedirects ) .
"</td>
</tr>"
@@ -335,15 +346,11 @@ class MovePageForm extends UnlistedSpecialPage {
array( 'id' => 'wpMovesubpages' )
) . '&#160;' .
Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ),
- wfMsgExt(
+ $this->msg(
( $this->oldTitle->hasSubpages()
? 'move-subpages'
- : 'move-talk-subpages' ),
- array( 'parseinline' ),
- $this->getLanguage()->formatNum( $wgMaximumMovedPages ),
- # $2 to allow use of PLURAL in message.
- $wgMaximumMovedPages
- )
+ : 'move-talk-subpages' )
+ )->numParams( $wgMaximumMovedPages )->params( $wgMaximumMovedPages )->parse()
) .
"</td>
</tr>"
@@ -351,14 +358,14 @@ class MovePageForm extends UnlistedSpecialPage {
}
$watchChecked = $user->isLoggedIn() && ($this->watch || $user->getBoolOption( 'watchmoves' )
- || $this->oldTitle->userIsWatching());
+ || $user->isWatched( $this->oldTitle ) );
# Don't allow watching if user is not logged in
if( $user->isLoggedIn() ) {
$out->addHTML( "
<tr>
<td></td>
<td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) .
+ Xml::checkLabel( $this->msg( 'move-watch' )->text(), 'wpWatch', 'watch', $watchChecked ) .
"</td>
</tr>");
}
@@ -384,7 +391,7 @@ class MovePageForm extends UnlistedSpecialPage {
}
function doSubmit() {
- global $wgMaximumMovedPages, $wgFixDoubleRedirects, $wgDeleteRevisionsLimit;
+ global $wgMaximumMovedPages, $wgFixDoubleRedirects;
$user = $this->getUser();
@@ -421,7 +428,7 @@ class MovePageForm extends UnlistedSpecialPage {
return;
}
- $reason = wfMessage( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
+ $reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
// Delete an associated image if there is
if ( $nt->getNamespace() == NS_FILE ) {
@@ -433,8 +440,9 @@ class MovePageForm extends UnlistedSpecialPage {
$error = ''; // passed by ref
$page = WikiPage::factory( $nt );
- if ( !$page->doDeleteArticle( $reason, false, 0, true, $error, $user ) ) {
- $this->showForm( array( array( 'cannotdelete', wfEscapeWikiText( $nt->getPrefixedText() ) ) ) );
+ $deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
+ if ( !$deleteStatus->isGood() ) {
+ $this->showForm( $deleteStatus->getErrorsArray() );
return;
}
}
@@ -459,7 +467,7 @@ class MovePageForm extends UnlistedSpecialPage {
wfRunHooks( 'SpecialMovepageAfterMove', array( &$this, &$ot, &$nt ) );
$out = $this->getOutput();
- $out->setPagetitle( wfMsg( 'pagemovedsub' ) );
+ $out->setPageTitle( $this->msg( 'pagemovedsub' ) );
$oldLink = Linker::link(
$ot,
@@ -472,7 +480,7 @@ class MovePageForm extends UnlistedSpecialPage {
$newText = $nt->getPrefixedText();
$msgName = $createRedirect ? 'movepage-moved-redirect' : 'movepage-moved-noredirect';
- $out->addHTML( wfMessage( 'movepage-moved' )->rawParams( $oldLink,
+ $out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink,
$newLink )->params( $oldText, $newText )->parseAsBlock() );
$out->addWikiMsg( $msgName );
@@ -562,15 +570,15 @@ class MovePageForm extends UnlistedSpecialPage {
$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
if( !$newSubpage ) {
$oldLink = Linker::linkKnown( $oldSubpage );
- $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink,
- htmlspecialchars(Title::makeName( $newNs, $newPageName )));
+ $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 []= wfMsgHtml( 'movepage-page-exists', $link );
+ $extraOutput []= $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
} else {
$success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect );
if( $success === true ) {
@@ -584,16 +592,16 @@ class MovePageForm extends UnlistedSpecialPage {
array( 'redirect' => 'no' )
);
$newLink = Linker::linkKnown( $newSubpage );
- $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink );
+ $extraOutput []= $this->msg( 'movepage-page-moved' )->rawParams( $oldLink, $newLink )->escaped();
++$count;
if( $count >= $wgMaximumMovedPages ) {
- $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $wgMaximumMovedPages ) );
+ $extraOutput []= $this->msg( 'movepage-max-pages' )->numParams( $wgMaximumMovedPages )->escaped();
break;
}
} else {
$oldLink = Linker::linkKnown( $oldSubpage );
$newLink = Linker::link( $newSubpage );
- $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, $newLink );
+ $extraOutput []= $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink, $newLink )->escaped();
}
}
@@ -621,8 +629,9 @@ class MovePageForm extends UnlistedSpecialPage {
}
function showLogFragment( $title ) {
+ $moveLogPage = new LogPage( 'move' );
$out = $this->getOutput();
- $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'move' ) ) );
+ $out->addHTML( Xml::element( 'h2', null, $moveLogPage->getName()->text() ) );
LogEventsList::showLogExtract( $out, 'move', $title );
}
diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php
index b88123dc..350aac63 100644
--- a/includes/specials/SpecialNewimages.php
+++ b/includes/specials/SpecialNewimages.php
@@ -58,6 +58,9 @@ class NewFilesPager extends ReverseChronologicalPager {
function __construct( IContextSource $context, $par = null ) {
$this->like = $context->getRequest()->getText( 'like' );
$this->showbots = $context->getRequest()->getBool( 'showbots' , 0 );
+ if ( is_numeric( $par ) ) {
+ $this->setLimit( $par );
+ }
parent::__construct( $context );
}
@@ -68,15 +71,18 @@ class NewFilesPager extends ReverseChronologicalPager {
$tables = array( 'image' );
if( !$this->showbots ) {
- $tables[] = 'user_groups';
- $conds[] = 'ug_group IS NULL';
- $jconds['user_groups'] = array(
- 'LEFT JOIN',
- array(
- 'ug_group' => User::getGroupsWithPermission( 'bot' ),
- 'ug_user = img_user'
- )
- );
+ $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
+ if( count( $groupsWithBotPermission ) ) {
+ $tables[] = 'user_groups';
+ $conds[] = 'ug_group IS NULL';
+ $jconds['user_groups'] = array(
+ 'LEFT JOIN',
+ array(
+ 'ug_group' => $groupsWithBotPermission,
+ 'ug_user = img_user'
+ )
+ );
+ }
}
if( !$wgMiserMode && $this->like !== null ){
@@ -123,7 +129,7 @@ class NewFilesPager extends ReverseChronologicalPager {
$this->gallery->add(
$title,
"$ul<br />\n<i>"
- . htmlspecialchars( $this->getLanguage()->timeanddate( $row->img_timestamp, true ) )
+ . htmlspecialchars( $this->getLanguage()->userTimeAndDate( $row->img_timestamp, $this->getUser() ) )
. "</i><br />\n"
);
}
@@ -139,13 +145,13 @@ class NewFilesPager extends ReverseChronologicalPager {
),
'showbots' => array(
'type' => 'check',
- 'label' => wfMessage( 'showhidebots', wfMsg( 'show' ) ),
+ 'label' => $this->msg( 'showhidebots', $this->msg( 'show' )->plain() )->escaped(),
'name' => 'showbots',
# 'default' => $this->getRequest()->getBool( 'showbots', 0 ),
),
'limit' => array(
'type' => 'hidden',
- 'default' => $this->getRequest()->getText( 'limit' ),
+ 'default' => $this->mLimit,
'name' => 'limit',
),
'offset' => array(
@@ -161,9 +167,9 @@ class NewFilesPager extends ReverseChronologicalPager {
$form = new HTMLForm( $fields, $this->getContext() );
$form->setTitle( $this->getTitle() );
- $form->setSubmitText( wfMsg( 'ilsubmit' ) );
+ $form->setSubmitTextMsg( 'ilsubmit' );
$form->setMethod( 'get' );
- $form->setWrapperLegend( wfMsg( 'newimages-legend' ) );
+ $form->setWrapperLegendMsg( 'newimages-legend' );
return $form;
}
diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php
index 54bcb97f..8e15d554 100644
--- a/includes/specials/SpecialNewpages.php
+++ b/includes/specials/SpecialNewpages.php
@@ -143,7 +143,9 @@ class SpecialNewpages extends IncludableSpecialPage {
return $this->feed( $feedType );
}
- $out->setFeedAppendQuery( wfArrayToCGI( $this->opts->getAllValues() ) );
+ $allValues = $this->opts->getAllValues();
+ unset( $allValues['feed'] );
+ $out->setFeedAppendQuery( wfArrayToCGI( $allValues ) );
}
$pager = new NewPagesPager( $this, $this->opts );
@@ -165,7 +167,7 @@ class SpecialNewpages extends IncludableSpecialPage {
global $wgGroupPermissions;
// show/hide links
- $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
+ $showhide = array( $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() );
// Option value -> message mapping
$filters = array(
@@ -197,7 +199,7 @@ class SpecialNewpages extends IncludableSpecialPage {
$link = Linker::link( $self, $showhide[$onoff], array(),
array( $key => $onoff ) + $changed
);
- $links[$key] = wfMsgHtml( $msg, $link );
+ $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
}
return $this->getLanguage()->pipeList( $links );
@@ -230,14 +232,23 @@ class SpecialNewpages extends IncludableSpecialPage {
$form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
- Xml::fieldset( wfMsg( 'newpages' ) ) .
+ Xml::fieldset( $this->msg( 'newpages' )->text() ) .
Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) .
'<tr>
<td class="mw-label">' .
- Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) .
'</td>
<td class="mw-input">' .
- Xml::namespaceSelector( $namespace, 'all' ) .
+ Html::namespaceSelector(
+ array(
+ 'selected' => $namespace,
+ 'all' => 'all',
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ ) .
'</td>
</tr>' . ( $tagFilter ? (
'<tr>
@@ -251,7 +262,7 @@ class SpecialNewpages extends IncludableSpecialPage {
( $wgEnableNewpagesUserFilter ?
'<tr>
<td class="mw-label">' .
- Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) .
+ Xml::label( $this->msg( 'newpages-username' )->text(), 'mw-np-username' ) .
'</td>
<td class="mw-input">' .
Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) .
@@ -259,7 +270,7 @@ class SpecialNewpages extends IncludableSpecialPage {
</tr>' : '' ) .
'<tr> <td></td>
<td class="mw-submit">' .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) .
'</td>
</tr>' .
'<tr>
@@ -283,6 +294,8 @@ class SpecialNewpages extends IncludableSpecialPage {
* @return String
*/
public function formatRow( $result ) {
+ $title = Title::newFromRow( $result );
+
# Revision deletion works on revisions, so we should cast one
$row = array(
'comment' => $result->rc_comment,
@@ -291,15 +304,15 @@ class SpecialNewpages extends IncludableSpecialPage {
'user' => $result->rc_user,
);
$rev = new Revision( $row );
+ $rev->setTitle( $title );
$classes = array();
$lang = $this->getLanguage();
$dm = $lang->getDirMark();
- $title = Title::newFromRow( $result );
$spanTime = Html::element( 'span', array( 'class' => 'mw-newpages-time' ),
- $lang->timeanddate( $result->rc_timestamp, true )
+ $lang->userTimeAndDate( $result->rc_timestamp, $this->getUser() )
);
$time = Linker::linkKnown(
$title,
@@ -324,14 +337,15 @@ class SpecialNewpages extends IncludableSpecialPage {
);
$histLink = Linker::linkKnown(
$title,
- wfMsgHtml( 'hist' ),
+ $this->msg( 'hist' )->escaped(),
array(),
array( 'action' => 'history' )
);
- $hist = Html::rawElement( 'span', array( 'class' => 'mw-newpages-history' ), wfMsg( 'parentheses', $histLink ) );
+ $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( 'nbytes' )->numParams( $result->length )->text() . ']'
+ $this->msg( 'brackets' )->params( $this->msg( 'nbytes' )->numParams( $result->length )->text() )
);
$ulink = Linker::revUserTools( $rev );
@@ -346,8 +360,8 @@ class SpecialNewpages extends IncludableSpecialPage {
$classes[] = 'mw-newpages-zero-byte-page';
}
- # Tags, if any. check for including due to bug 23293
- if ( !$this->including() ) {
+ # Tags, if any.
+ if( isset( $result->ts_tags ) ) {
list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $result->ts_tags, 'newpages' );
$classes = array_merge( $classes, $newClasses );
} else {
@@ -356,11 +370,11 @@ class SpecialNewpages extends IncludableSpecialPage {
$css = count( $classes ) ? ' class="' . implode( ' ', $classes ) . '"' : '';
- # Display the old title if the namespace has been changed
+ # Display the old title if the namespace/title has been changed
$oldTitleText = '';
- if ( $result->page_namespace !== $result->rc_namespace ) {
- $oldTitleText = wfMessage( 'rc-old-title' )->params( Title::makeTitle( $result->rc_namespace, $result->rc_title )
- ->getPrefixedText() )->escaped();
+ $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title );
+ if ( !$title->equals( $oldTitle ) ) {
+ $oldTitleText = $this->msg( 'rc-old-title' )->params( $oldTitle->getPrefixedText() )->escaped();
}
return "<li{$css}>{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} {$tagDisplay} {$oldTitleText}</li>\n";
@@ -396,7 +410,7 @@ class SpecialNewpages extends IncludableSpecialPage {
$feed = new $wgFeedClasses[$type](
$this->feedTitle(),
- wfMsgExt( 'tagline', 'parsemag' ),
+ $this->msg( 'tagline' )->text(),
$this->getTitle()->getFullUrl()
);
@@ -420,7 +434,7 @@ class SpecialNewpages extends IncludableSpecialPage {
}
protected function feedItem( $row ) {
- $title = Title::MakeTitle( intval( $row->rc_namespace ), $row->rc_title );
+ $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
if( $title ) {
$date = $row->rc_timestamp;
$comments = $title->getTalkPage()->getFullURL();
@@ -445,7 +459,8 @@ class SpecialNewpages extends IncludableSpecialPage {
protected function feedItemDesc( $row ) {
$revision = Revision::newFromId( $row->rev_id );
if( $revision ) {
- return '<p>' . htmlspecialchars( $revision->getUserText() ) . wfMsgForContent( 'colon-separator' ) .
+ 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>";
@@ -515,7 +530,7 @@ class NewPagesPager extends ReverseChronologicalPager {
$fields = array(
'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text',
'rc_comment', 'rc_timestamp', 'rc_patrolled','rc_id', 'rc_deleted',
- 'page_len AS length', 'page_latest AS rev_id', 'ts_tags', 'rc_this_oldid',
+ 'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid',
'page_namespace', 'page_title'
);
$join_conds = array( 'page' => array( 'INNER JOIN', 'page_id=rc_cur_id' ) );
@@ -531,13 +546,10 @@ class NewPagesPager extends ReverseChronologicalPager {
'join_conds' => $join_conds
);
- // Empty array for fields, it'll be set by us anyway.
- $fields = array();
-
// Modify query for tags
ChangeTags::modifyDisplayQuery(
$info['tables'],
- $fields,
+ $info['fields'],
$info['conds'],
$info['join_conds'],
$info['options'],
diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php
index 62731e98..efb57657 100644
--- a/includes/specials/SpecialPasswordReset.php
+++ b/includes/specials/SpecialPasswordReset.php
@@ -65,6 +65,9 @@ class SpecialPasswordReset extends FormSpecialPage {
'type' => 'text',
'label-message' => 'passwordreset-username',
);
+ if( $this->getUser()->isLoggedIn() ) {
+ $a['Username']['default'] = $this->getUser()->getName();
+ }
}
if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) {
@@ -95,7 +98,7 @@ class SpecialPasswordReset extends FormSpecialPage {
}
public function alterForm( HTMLForm $form ) {
- $form->setSubmitText( wfMessage( "mailmypassword" ) );
+ $form->setSubmitTextMsg( 'mailmypassword' );
}
protected function preText() {
@@ -110,7 +113,7 @@ class SpecialPasswordReset extends FormSpecialPage {
if ( isset( $wgPasswordResetRoutes['domain'] ) && $wgPasswordResetRoutes['domain'] ) {
$i++;
}
- return wfMessage( 'passwordreset-pretext', $i )->parseAsBlock();
+ return $this->msg( 'passwordreset-pretext', $i )->parseAsBlock();
}
/**
@@ -151,7 +154,7 @@ class SpecialPasswordReset extends FormSpecialPage {
$method = 'email';
$res = wfGetDB( DB_SLAVE )->select(
'user',
- '*',
+ User::selectFields(),
array( 'user_email' => $data['Email'] ),
__METHOD__
);
@@ -234,12 +237,12 @@ class SpecialPasswordReset extends FormSpecialPage {
$password = $user->randomPassword();
$user->setNewpassword( $password );
$user->saveSettings();
- $passwords[] = wfMessage( 'passwordreset-emailelement', $user->getName(), $password
- )->inLanguage( $userLanguage )->plain(); // 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 );
- $this->email = wfMessage( $msg )->inLanguage( $userLanguage );
+ $this->email = $this->msg( $msg )->inLanguage( $userLanguage );
$this->email->params(
$username,
$passwordBlock,
@@ -248,7 +251,7 @@ class SpecialPasswordReset extends FormSpecialPage {
round( $wgNewPasswordExpiry / 86400 )
);
- $title = wfMessage( 'passwordreset-emailtitle' );
+ $title = $this->msg( 'passwordreset-emailtitle' );
$this->result = $firstUser->sendMail( $title->escaped(), $this->email->escaped() );
diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php
index 803f03e7..448d1799 100644
--- a/includes/specials/SpecialPopularpages.php
+++ b/includes/specials/SpecialPopularpages.php
@@ -42,9 +42,9 @@ class PopularPagesPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array( 'page' ),
- 'fields' => array( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_counter AS value'),
+ 'fields' => array( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_counter'),
'conds' => array( 'page_is_redirect' => 0,
'page_namespace' => MWNamespace::getContentNamespaces() ) );
}
@@ -56,7 +56,13 @@ class PopularPagesPage extends QueryPage {
*/
function formatResult( $skin, $result ) {
global $wgContLang;
- $title = Title::makeTitle( $result->namespace, $result->title );
+
+ $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 ) );
+ }
+
$link = Linker::linkKnown(
$title,
htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) )
diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php
index 946112bf..c6b2bb6b 100644
--- a/includes/specials/SpecialPreferences.php
+++ b/includes/specials/SpecialPreferences.php
@@ -39,8 +39,7 @@ class SpecialPreferences extends SpecialPage {
$user = $this->getUser();
if ( $user->isAnon() ) {
- $out->showErrorPage( 'prefsnologin', 'prefsnologintext', array( $this->getTitle()->getPrefixedDBkey() ) );
- return;
+ throw new ErrorPageError( 'prefsnologin', 'prefsnologintext', array( $this->getTitle()->getPrefixedDBkey() ) );
}
$this->checkReadOnly();
@@ -69,7 +68,7 @@ class SpecialPreferences extends SpecialPage {
$htmlForm = new HTMLForm( array(), $this->getContext(), 'prefs-restore' );
- $htmlForm->setSubmitText( wfMsg( 'restoreprefs' ) );
+ $htmlForm->setSubmitTextMsg( 'restoreprefs' );
$htmlForm->setTitle( $this->getTitle( 'reset' ) );
$htmlForm->setSubmitCallback( array( $this, 'submitReset' ) );
$htmlForm->suppressReset();
@@ -82,7 +81,7 @@ class SpecialPreferences extends SpecialPage {
$user->resetOptions();
$user->saveSettings();
- $url = SpecialPage::getTitleFor( 'Preferences' )->getFullURL( 'success' );
+ $url = $this->getTitle()->getFullURL( 'success' );
$this->getOutput()->redirect( $url );
diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php
index 495f15f7..7740b320 100644
--- a/includes/specials/SpecialPrefixindex.php
+++ b/includes/specials/SpecialPrefixindex.php
@@ -52,6 +52,7 @@ 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 );
$namespaces = $wgContLang->getNamespaces();
$out->setPageTitle(
@@ -73,29 +74,31 @@ 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 );
+ $this->showPrefixChunk( $namespace, $showme, $from, $hideredirects );
} else {
- $out->addHTML( $this->namespacePrefixForm( $namespace, null ) );
+ $out->addHTML( $this->namespacePrefixForm( $namespace, null, $hideredirects ) );
}
}
/**
- * HTML for the top form
- * @param $namespace Integer: a namespace constant (default NS_MAIN).
- * @param $from String: dbKey we are starting listing at.
- */
- function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ) {
+ * 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)
+ * @return string
+ */
+ function namespacePrefixForm( $namespace = NS_MAIN, $from = '', $hideredirects = false ) {
global $wgScript;
$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' );
- $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
+ $out .= Xml::element( 'legend', null, $this->msg( 'allpages' )->text() );
$out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
$out .= "<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'allpagesprefix' ), 'nsfrom' ) .
+ Xml::label( $this->msg( 'allpagesprefix' )->text(), 'nsfrom' ) .
"</td>
<td class='mw-input'>" .
Xml::input( 'prefix', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
@@ -103,13 +106,25 @@ class SpecialPrefixindex extends SpecialAllpages {
</tr>
<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) .
"</td>
<td class='mw-input'>" .
- Xml::namespaceSelector( $namespace, null ) . ' ' .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ 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>
- </tr>";
+ </tr>";
$out .= Xml::closeElement( 'table' );
$out .= Xml::closeElement( 'fieldset' );
$out .= Xml::closeElement( 'form' );
@@ -121,8 +136,9 @@ class SpecialPrefixindex extends SpecialAllpages {
* @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)
*/
- function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null ) {
+ function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null, $hideredirects = false ) {
global $wgContLang;
if ( $from === null ) {
@@ -134,10 +150,10 @@ class SpecialPrefixindex extends SpecialAllpages {
$namespaces = $wgContLang->getNamespaces();
if ( !$prefixList || !$fromList ) {
- $out = wfMsgExt( 'allpagesbadtitle', 'parse' );
+ $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock();
} elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
// Show errormessage and reset to NS_MAIN
- $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+ $out = $this->msg( 'allpages-bad-ns', $namespace )->parse();
$namespace = NS_MAIN;
} else {
list( $namespace, $prefixKey, $prefix ) = $prefixList;
@@ -147,13 +163,19 @@ class SpecialPrefixindex extends SpecialAllpages {
$dbr = wfGetDB( DB_SLAVE );
+ $conds = array(
+ 'page_namespace' => $namespace,
+ 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ),
+ 'page_title >= ' . $dbr->addQuotes( $fromKey ),
+ );
+
+ if ( $hideredirects ) {
+ $conds['page_is_redirect'] = 0;
+ }
+
$res = $dbr->select( 'page',
array( 'page_namespace', 'page_title', 'page_is_redirect' ),
- array(
- 'page_namespace' => $namespace,
- 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ),
- 'page_title >= ' . $dbr->addQuotes( $fromKey ),
- ),
+ $conds,
__METHOD__,
array(
'ORDER BY' => 'page_title',
@@ -166,7 +188,7 @@ class SpecialPrefixindex extends SpecialAllpages {
$n = 0;
if( $res->numRows() > 0 ) {
- $out = Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-prefixindex-list-table' ) );
+ $out = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-list-table' ) );
while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
$t = Title::makeTitle( $s->page_namespace, $s->page_title );
@@ -174,7 +196,8 @@ class SpecialPrefixindex extends SpecialAllpages {
$link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
Linker::linkKnown(
$t,
- htmlspecialchars( $t->getText() )
+ htmlspecialchars( $t->getText() ),
+ $s->page_is_redirect ? array( 'class' => 'mw-redirect' ) : array()
) .
($s->page_is_redirect ? '</div>' : '' );
} else {
@@ -202,9 +225,9 @@ class SpecialPrefixindex extends SpecialAllpages {
if ( $this->including() ) {
$out2 = '';
} else {
- $nsForm = $this->namespacePrefixForm( $namespace, $prefix );
+ $nsForm = $this->namespacePrefixForm( $namespace, $prefix, $hideredirects );
$self = $this->getTitle();
- $out2 = Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-prefixindex-nav-table' ) ) .
+ $out2 = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-nav-table' ) ) .
'<tr>
<td>' .
$nsForm .
@@ -214,7 +237,8 @@ class SpecialPrefixindex extends SpecialAllpages {
if( isset( $res ) && $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
$query = array(
'from' => $s->page_title,
- 'prefix' => $prefix
+ 'prefix' => $prefix,
+ 'hideredirects' => $hideredirects,
);
if( $namespace || ($prefix == '')) {
@@ -224,7 +248,7 @@ class SpecialPrefixindex extends SpecialAllpages {
}
$nextLink = Linker::linkKnown(
$self,
- wfMsgHtml( 'nextpage', str_replace( '_',' ', htmlspecialchars( $s->page_title ) ) ),
+ $this->msg( 'nextpage', str_replace( '_',' ', $s->page_title ) )->escaped(),
array(),
$query
);
diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php
index eec974fe..74ed5378 100644
--- a/includes/specials/SpecialProtectedpages.php
+++ b/includes/specials/SpecialProtectedpages.php
@@ -58,21 +58,20 @@ class SpecialProtectedpages extends SpecialPage {
$this->getOutput()->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly, $cascadeOnly ) );
if( $pager->getNumRows() ) {
- $s = $pager->getNavigationBar();
- $s .= "<ul>" .
- $pager->getBody() .
- "</ul>";
- $s .= $pager->getNavigationBar();
+ $this->getOutput()->addHTML(
+ $pager->getNavigationBar() .
+ '<ul>' . $pager->getBody() . '</ul>' .
+ $pager->getNavigationBar()
+ );
} else {
- $s = '<p>' . wfMsgHtml( 'protectedpagesempty' ) . '</p>';
+ $this->getOutput()->addWikiMsg( 'protectedpagesempty' );
}
- $this->getOutput()->addHTML( $s );
}
/**
* Callback function to output a restriction
- * @param $row object Protected title
- * @return string Formatted <li> element
+ * @param Title $row Protected title
+ * @return string Formatted "<li>" element
*/
public function formatRow( $row ) {
wfProfileIn( __METHOD__ );
@@ -88,12 +87,12 @@ class SpecialProtectedpages extends SpecialPage {
$description_items = array ();
- $protType = wfMsgHtml( 'restriction-level-' . $row->pr_level );
+ $protType = $this->msg( 'restriction-level-' . $row->pr_level )->escaped();
$description_items[] = $protType;
if( $row->pr_cascade ) {
- $description_items[] = wfMsg( 'protect-summary-cascade' );
+ $description_items[] = $this->msg( 'protect-summary-cascade' )->text();
}
$stxt = '';
@@ -101,15 +100,13 @@ class SpecialProtectedpages extends SpecialPage {
$expiry = $lang->formatExpiry( $row->pr_expiry, TS_MW );
if( $expiry != $infinity ) {
-
- $expiry_description = wfMsg(
+ $user = $this->getUser();
+ $description_items[] = $this->msg(
'protect-expiring-local',
- $lang->timeanddate( $expiry, true ),
- $lang->date( $expiry, true ),
- $lang->time( $expiry, true )
- );
-
- $description_items[] = htmlspecialchars($expiry_description);
+ $lang->userTimeAndDate( $expiry, $user ),
+ $lang->userDate( $expiry, $user ),
+ $lang->userTime( $expiry, $user )
+ )->escaped();
}
if(!is_null($size = $row->page_len)) {
@@ -118,25 +115,27 @@ class SpecialProtectedpages extends SpecialPage {
# 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(
+ $changeProtection = Linker::linkKnown(
$title,
- wfMsgHtml( 'protect_change' ),
+ $this->msg( 'protect_change' )->escaped(),
array(),
array( 'action' => 'unprotect' )
- ) . ')';
+ );
} else {
$ltitle = SpecialPage::getTitleFor( 'Log' );
- $changeProtection = ' (' . Linker::linkKnown(
+ $changeProtection = Linker::linkKnown(
$ltitle,
- wfMsgHtml( 'protectlogpage' ),
+ $this->msg( 'protectlogpage' )->escaped(),
array(),
array(
'type' => 'protect',
'page' => $title->getPrefixedText()
)
- ) . ')';
+ );
}
+ $changeProtection = ' ' . $this->msg( 'parentheses' )->rawParams( $changeProtection )->escaped();
+
wfProfileOut( __METHOD__ );
return Html::rawElement(
@@ -160,7 +159,7 @@ class SpecialProtectedpages extends SpecialPage {
$title = $this->getTitle();
return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) .
+ Xml::element( 'legend', array(), $this->msg( 'protectedpages' )->text() ) .
Html::hidden( 'title', $title->getPrefixedDBkey() ) . "\n" .
$this->getNamespaceMenu( $namespace ) . "&#160;\n" .
$this->getTypeMenu( $type ) . "&#160;\n" .
@@ -171,7 +170,7 @@ class SpecialProtectedpages extends SpecialPage {
"</span><br /><span style='white-space: nowrap'>" .
$this->getSizeLimit( $sizetype, $size ) . "&#160;\n" .
"</span>" .
- "&#160;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ "&#160;" . Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n" .
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' );
}
@@ -184,9 +183,19 @@ class SpecialProtectedpages extends SpecialPage {
* @return String
*/
protected function getNamespaceMenu( $namespace = null ) {
- return "<span style='white-space: nowrap'>" .
- Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&#160;'
- . Xml::namespaceSelector( $namespace, '' ) . "</span>";
+ return Html::rawElement( 'span', array( 'style' => 'white-space: nowrap;' ),
+ Html::namespaceSelector(
+ array(
+ 'selected' => $namespace,
+ 'all' => '',
+ 'label' => $this->msg( 'namespace' )->text()
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ )
+ );
}
/**
@@ -194,7 +203,7 @@ class SpecialProtectedpages extends SpecialPage {
*/
protected function getExpiryCheck( $indefOnly ) {
return
- Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n";
+ Xml::checkLabel( $this->msg( 'protectedpages-indef' )->text(), 'indefonly', 'indefonly', $indefOnly ) . "\n";
}
/**
@@ -202,7 +211,7 @@ class SpecialProtectedpages extends SpecialPage {
*/
protected function getCascadeCheck( $cascadeOnly ) {
return
- Xml::checkLabel( wfMsg('protectedpages-cascade'), 'cascadeonly', 'cascadeonly', $cascadeOnly ) . "\n";
+ Xml::checkLabel( $this->msg( 'protectedpages-cascade' )->text(), 'cascadeonly', 'cascadeonly', $cascadeOnly ) . "\n";
}
/**
@@ -212,13 +221,13 @@ class SpecialProtectedpages extends SpecialPage {
$max = $sizetype === 'max';
return
- Xml::radioLabel( wfMsg('minimum-size'), 'sizetype', 'min', 'wpmin', !$max ) .
+ Xml::radioLabel( $this->msg( 'minimum-size' )->text(), 'sizetype', 'min', 'wpmin', !$max ) .
'&#160;' .
- Xml::radioLabel( wfMsg('maximum-size'), '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;' .
- Xml::label( wfMsg('pagesize'), 'wpsize' );
+ Xml::label( $this->msg( 'pagesize' )->text(), 'wpsize' );
}
/**
@@ -232,7 +241,7 @@ class SpecialProtectedpages extends SpecialPage {
// First pass to load the log names
foreach( Title::getFilteredRestrictionTypes( true ) as $type ) {
- $text = wfMsg("restriction-$type");
+ $text = $this->msg( "restriction-$type" )->text();
$m[$text] = $type;
}
@@ -243,7 +252,7 @@ class SpecialProtectedpages extends SpecialPage {
}
return "<span style='white-space: nowrap'>" .
- Xml::label( wfMsg('restriction-type') , $this->IdType ) . '&#160;' .
+ Xml::label( $this->msg( 'restriction-type' )->text(), $this->IdType ) . '&#160;' .
Xml::tags( 'select',
array( 'id' => $this->IdType, 'name' => $this->IdType ),
implode( "\n", $options ) ) . "</span>";
@@ -257,14 +266,14 @@ class SpecialProtectedpages extends SpecialPage {
protected function getLevelMenu( $pr_level ) {
global $wgRestrictionLevels;
- $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); // Temporary array
$options = array();
// First pass to load the log names
foreach( $wgRestrictionLevels as $type ) {
// Messages used can be 'restriction-level-sysop' and 'restriction-level-autoconfirmed'
if( $type !='' && $type !='*') {
- $text = wfMsg("restriction-level-$type");
+ $text = $this->msg( "restriction-level-$type" )->text();
$m[$text] = $type;
}
}
@@ -276,7 +285,7 @@ class SpecialProtectedpages extends SpecialPage {
}
return "<span style='white-space: nowrap'>" .
- Xml::label( wfMsg( 'restriction-level' ) , $this->IdLevel ) . ' ' .
+ Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . ' ' .
Xml::tags( 'select',
array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
implode( "\n", $options ) ) . "</span>";
diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php
index 982feb66..a80f0d0a 100644
--- a/includes/specials/SpecialProtectedtitles.php
+++ b/includes/specials/SpecialProtectedtitles.php
@@ -56,15 +56,14 @@ class SpecialProtectedtitles extends SpecialPage {
$this->getOutput()->addHTML( $this->showOptions( $NS, $type, $level ) );
if ( $pager->getNumRows() ) {
- $s = $pager->getNavigationBar();
- $s .= "<ul>" .
- $pager->getBody() .
- "</ul>";
- $s .= $pager->getNavigationBar();
+ $this->getOutput()->addHTML(
+ $pager->getNavigationBar() .
+ '<ul>' . $pager->getBody() . '</ul>' .
+ $pager->getNavigationBar()
+ );
} else {
- $s = '<p>' . wfMsgHtml( 'protectedtitlesempty' ) . '</p>';
+ $this->getOutput()->addWikiMsg( 'protectedtitlesempty' );
}
- $this->getOutput()->addHTML( $s );
}
/**
@@ -86,21 +85,20 @@ class SpecialProtectedtitles extends SpecialPage {
$description_items = array ();
- $protType = wfMsgHtml( 'restriction-level-' . $row->pt_create_perm );
+ $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_description = wfMsg(
+ $user = $this->getUser();
+ $description_items[] = $this->msg(
'protect-expiring-local',
- $lang->timeanddate( $expiry, true ),
- $lang->date( $expiry, true ),
- $lang->time( $expiry, true )
- );
-
- $description_items[] = htmlspecialchars($expiry_description);
+ $lang->userTimeAndDate( $expiry, $user ),
+ $lang->userDate( $expiry, $user ),
+ $lang->userTime( $expiry, $user )
+ )->escaped();
}
wfProfileOut( __METHOD__ );
@@ -112,6 +110,7 @@ class SpecialProtectedtitles extends SpecialPage {
* @param $namespace Integer:
* @param $type string
* @param $level string
+ * @return string
* @private
*/
function showOptions( $namespace, $type='edit', $level ) {
@@ -121,11 +120,11 @@ class SpecialProtectedtitles extends SpecialPage {
$special = htmlspecialchars( $title->getPrefixedDBkey() );
return "<form action=\"$action\" method=\"get\">\n" .
'<fieldset>' .
- Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) .
+ Xml::element( 'legend', array(), $this->msg( 'protectedtitles' )->text() ) .
Html::hidden( 'title', $special ) . "&#160;\n" .
$this->getNamespaceMenu( $namespace ) . "&#160;\n" .
$this->getLevelMenu( $level ) . "&#160;\n" .
- "&#160;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ "&#160;" . Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n" .
"</fieldset></form>";
}
@@ -137,9 +136,17 @@ class SpecialProtectedtitles extends SpecialPage {
* @return string
*/
function getNamespaceMenu( $namespace = null ) {
- return Xml::label( wfMsg( 'namespace' ), 'namespace' )
- . '&#160;'
- . Xml::namespaceSelector( $namespace, '' );
+ return Html::namespaceSelector(
+ array(
+ 'selected' => $namespace,
+ 'all' => '',
+ 'label' => $this->msg( 'namespace' )->text()
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ );
}
/**
@@ -149,13 +156,13 @@ class SpecialProtectedtitles extends SpecialPage {
function getLevelMenu( $pr_level ) {
global $wgRestrictionLevels;
- $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); // Temporary array
$options = array();
// First pass to load the log names
foreach( $wgRestrictionLevels as $type ) {
if ( $type !='' && $type !='*') {
- $text = wfMsg("restriction-level-$type");
+ $text = $this->msg( "restriction-level-$type" )->text();
$m[$text] = $type;
}
}
@@ -170,7 +177,7 @@ class SpecialProtectedtitles extends SpecialPage {
}
return
- Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&#160;' .
+ Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . '&#160;' .
Xml::tags( 'select',
array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
implode( "\n", $options ) );
@@ -212,7 +219,7 @@ class ProtectedTitlesPager extends AlphabeticPager {
* @return Title
*/
function getTitle() {
- return SpecialPage::getTitleFor( 'Protectedtitles' );
+ return $this->mForm->getTitle();
}
function formatRow( $row ) {
diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php
index 0b6239bb..307088ed 100644
--- a/includes/specials/SpecialRandompage.php
+++ b/includes/specials/SpecialRandompage.php
@@ -85,7 +85,7 @@ class RandomPage extends SpecialPage {
$nsNames = array();
foreach( $this->namespaces as $n ) {
if( $n === NS_MAIN ) {
- $nsNames[] = wfMsgNoTrans( 'blanknamespace' );
+ $nsNames[] = $this->msg( 'blanknamespace' )->plain();
} else {
$nsNames[] = $wgContLang->getNsText( $n );
}
diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php
index daf47f62..2bd8b0a9 100644
--- a/includes/specials/SpecialRecentchanges.php
+++ b/includes/specials/SpecialRecentchanges.php
@@ -190,8 +190,8 @@ class SpecialRecentChanges extends IncludableSpecialPage {
public function getFeedObject( $feedFormat ){
$changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' );
$formatter = $changesFeed->getFeedObject(
- wfMsgForContent( 'recentchanges' ),
- wfMsgForContent( 'recentchanges-feed-description' ),
+ $this->msg( 'recentchanges' )->inContentLanguage()->text(),
+ $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(),
$this->getTitle()->getFullURL()
);
return array( $changesFeed, $formatter );
@@ -366,7 +366,7 @@ class SpecialRecentChanges extends IncludableSpecialPage {
*
* @param $conds Array
* @param $opts FormOptions
- * @return database result or false (for Recentchangeslinked only)
+ * @return bool|ResultWrapper result or false (for Recentchangeslinked only)
*/
public function doMainQuery( $conds, $opts ) {
$tables = array( 'recentchanges' );
@@ -396,14 +396,15 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$fields[] = 'page_latest';
$join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id');
}
- if ( !$this->including() ) {
- // Tag stuff.
- // Doesn't work when transcluding. See bug 23293
- ChangeTags::modifyDisplayQuery(
- $tables, $fields, $conds, $join_conds, $query_options,
- $opts['tagfilter']
- );
- }
+ // Tag stuff.
+ ChangeTags::modifyDisplayQuery(
+ $tables,
+ $fields,
+ $conds,
+ $join_conds,
+ $query_options,
+ $opts['tagfilter']
+ );
if ( !wfRunHooks( 'SpecialRecentChangesQuery',
array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ) ) )
@@ -534,6 +535,7 @@ class SpecialRecentChanges extends IncludableSpecialPage {
/**
* Get the query string to append to feed link URLs.
* This is overridden by RCL to add the target parameter
+ * @return bool
*/
public function getFeedQuery() {
return false;
@@ -564,17 +566,17 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$extraOpts = $this->getExtraOptions( $opts );
$extraOptsCount = count( $extraOpts );
$count = 0;
- $submit = ' ' . Xml::submitbutton( wfMsg( 'allpagessubmit' ) );
+ $submit = ' ' . Xml::submitbutton( $this->msg( 'allpagessubmit' )->text() );
$out = Xml::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) );
- foreach( $extraOpts as $optionRow ) {
+ foreach( $extraOpts as $name => $optionRow ) {
# Add submit button to the last row only
++$count;
- $addSubmit = $count === $extraOptsCount ? $submit : '';
+ $addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
$out .= Xml::openElement( 'tr' );
if( is_array( $optionRow ) ) {
- $out .= Xml::tags( 'td', array( 'class' => 'mw-label' ), $optionRow[0] );
+ $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 );
@@ -595,7 +597,7 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$panelString = implode( "\n", $panel );
$this->getOutput()->addHTML(
- Xml::fieldset( wfMsg( 'recentchanges-legend' ), $panelString, array( 'class' => 'rcoptions' ) )
+ Xml::fieldset( $this->msg( 'recentchanges-legend' )->text(), $panelString, array( 'class' => 'rcoptions' ) )
);
$this->setBottomText( $opts );
@@ -632,14 +634,18 @@ class SpecialRecentChanges extends IncludableSpecialPage {
*/
function setTopText( FormOptions $opts ) {
global $wgContLang;
- $this->getOutput()->addWikiText(
- Html::rawElement( 'p',
- array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ),
- "\n" . wfMsgForContentNoTrans( 'recentchangestext' ) . "\n"
- ),
- /* $lineStart */ false,
- /* $interface */ false
- );
+
+ $message = $this->msg( 'recentchangestext' )->inContentLanguage();
+ if ( !$message->isDisabled() ) {
+ $this->getOutput()->addWikiText(
+ Html::rawElement( 'p',
+ array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ),
+ "\n" . $message->plain() . "\n"
+ ),
+ /* $lineStart */ false,
+ /* $interface */ false
+ );
+ }
}
/**
@@ -662,16 +668,16 @@ class SpecialRecentChanges extends IncludableSpecialPage {
array( 'selected' => $opts['namespace'], 'all' => '' ),
array( 'name' => 'namespace', 'id' => 'namespace' )
);
- $nsLabel = Xml::label( wfMsg( 'namespace' ), 'namespace' );
+ $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
$invert = Xml::checkLabel(
- wfMsg( 'invert' ), 'invert', 'nsinvert',
+ $this->msg( 'invert' )->text(), 'invert', 'nsinvert',
$opts['invert'],
- array( 'title' => wfMsg( 'tooltip-invert' ) )
+ array( 'title' => $this->msg( 'tooltip-invert' )->text() )
);
$associated = Xml::checkLabel(
- wfMsg( 'namespace_association' ), 'associated', 'nsassociated',
+ $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
$opts['associated'],
- array( 'title' => wfMsg( 'tooltip-namespace_association' ) )
+ array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() )
);
return array( $nsLabel, "$nsSelect $invert $associated" );
}
@@ -683,10 +689,10 @@ class SpecialRecentChanges extends IncludableSpecialPage {
* @return Array
*/
protected function categoryFilterForm( FormOptions $opts ) {
- list( $label, $input ) = Xml::inputLabelSep( wfMsg( 'rc_categories' ),
+ list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(),
'categories', 'mw-categories', false, $opts['categories'] );
- $input .= ' ' . Xml::checkLabel( wfMsg( 'rc_categories_any' ),
+ $input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(),
'categories_any', 'mw-categories_any', $opts['categories_any'] );
return array( $label, $input );
@@ -763,9 +769,20 @@ class SpecialRecentChanges extends IncludableSpecialPage {
* @param $override Array: options to override
* @param $options Array: current options
* @param $active Boolean: whether to show the link in bold
+ * @return string
*/
function makeOptionsLink( $title, $override, $options, $active = false ) {
$params = $override + $options;
+
+ // Bug 36524: false values have be converted to "0" otherwise
+ // wfArrayToCgi() will omit it them.
+ foreach ( $params as &$value ) {
+ if ( $value === false ) {
+ $value = '0';
+ }
+ }
+ unset( $value );
+
$text = htmlspecialchars( $title );
if ( $active ) {
$text = '<strong>' . $text . '</strong>';
@@ -778,6 +795,7 @@ class SpecialRecentChanges extends IncludableSpecialPage {
*
* @param $defaults Array
* @param $nondefaults Array
+ * @return string
*/
function optionsPanel( $defaults, $nondefaults ) {
global $wgRCLinkLimits, $wgRCLinkDays;
@@ -785,16 +803,18 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$options = $nondefaults + $defaults;
$note = '';
- if( !wfEmptyMsg( 'rclegend' ) ) {
- $note .= '<div class="mw-rclegend">' .
- wfMsgExt( 'rclegend', array( 'parseinline' ) ) . "</div>\n";
+ $msg = $this->msg( 'rclegend' );
+ if( !$msg->isDisabled() ) {
+ $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
}
+
+ $lang = $this->getLanguage();
+ $user = $this->getUser();
if( $options['from'] ) {
- $note .= wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
- $this->getLanguage()->formatNum( $options['limit'] ),
- $this->getLanguage()->timeanddate( $options['from'], true ),
- $this->getLanguage()->date( $options['from'], true ),
- $this->getLanguage()->time( $options['from'], true ) ) . '<br />';
+ $note .= $this->msg( 'rcnotefrom' )->numParams( $options['limit'] )->params(
+ $lang->userTimeAndDate( $options['from'], $user ),
+ $lang->userDate( $options['from'], $user ),
+ $lang->userTime( $options['from'], $user ) )->parse() . '<br />';
}
# Sort data for display and make sure it's unique after we've added user data.
@@ -807,21 +827,21 @@ class SpecialRecentChanges extends IncludableSpecialPage {
// limit links
foreach( $wgRCLinkLimits as $value ) {
- $cl[] = $this->makeOptionsLink( $this->getLanguage()->formatNum( $value ),
+ $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
array( 'limit' => $value ), $nondefaults, $value == $options['limit'] );
}
- $cl = $this->getLanguage()->pipeList( $cl );
+ $cl = $lang->pipeList( $cl );
// day links, reset 'from' to none
foreach( $wgRCLinkDays as $value ) {
- $dl[] = $this->makeOptionsLink( $this->getLanguage()->formatNum( $value ),
+ $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] );
}
- $dl = $this->getLanguage()->pipeList( $dl );
+ $dl = $lang->pipeList( $dl );
// show/hide links
- $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) );
+ $showhide = array( $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() );
$filters = array(
'hideminor' => 'rcshowhideminor',
'hidebots' => 'rcshowhidebots',
@@ -834,7 +854,7 @@ class SpecialRecentChanges extends IncludableSpecialPage {
$filters[$key] = $params['msg'];
}
// Disable some if needed
- if ( !$this->getUser()->useRCPatrol() ) {
+ if ( !$user->useRCPatrol() ) {
unset( $filters['hidepatrolled'] );
}
@@ -842,19 +862,18 @@ class SpecialRecentChanges extends IncludableSpecialPage {
foreach ( $filters as $key => $msg ) {
$link = $this->makeOptionsLink( $showhide[1 - $options[$key]],
array( $key => 1-$options[$key] ), $nondefaults );
- $links[] = wfMsgHtml( $msg, $link );
+ $links[] = $this->msg( $msg )->rawParams( $link )->escaped();
}
// show from this onward link
$timestamp = wfTimestampNow();
- $now = $this->getLanguage()->timeanddate( $timestamp, true );
+ $now = $lang->userTimeAndDate( $timestamp, $user );
$tl = $this->makeOptionsLink(
$now, array( 'from' => $timestamp ), $nondefaults
);
- $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter' ),
- $cl, $dl, $this->getLanguage()->pipeList( $links ) );
- $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter' ), $tl );
+ $rclinks = $this->msg( 'rclinks' )->rawParams( $cl, $dl, $lang->pipeList( $links ) )->parse();
+ $rclistfrom = $this->msg( 'rclistfrom' )->rawParams( $tl )->parse();
return "{$note}$rclinks<br />$rclistfrom";
}
diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php
index 1f556f89..862736d3 100644
--- a/includes/specials/SpecialRecentchangeslinked.php
+++ b/includes/specials/SpecialRecentchangeslinked.php
@@ -54,8 +54,9 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
public function getFeedObject( $feedFormat ){
$feed = new ChangesFeed( $feedFormat, false );
$feedObj = $feed->getFeedObject(
- wfMsgForContent( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() ),
- wfMsgForContent( 'recentchangeslinked-feed' ),
+ $this->msg( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() )
+ ->inContentLanguage()->text(),
+ $this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(),
$this->getTitle()->getFullUrl()
);
return array( $feed, $feedObj );
@@ -88,7 +89,7 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
*/
$dbr = wfGetDB( DB_SLAVE, 'recentchangeslinked' );
- $id = $title->getArticleId();
+ $id = $title->getArticleID();
$ns = $title->getNamespace();
$dbkey = $title->getDBkey();
@@ -109,10 +110,14 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
$join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id');
$select[] = 'page_latest';
}
- if ( !$this->including() ) { // bug 23293
- ChangeTags::modifyDisplayQuery( $tables, $select, $conds, $join_conds,
- $query_options, $opts['tagfilter'] );
- }
+ ChangeTags::modifyDisplayQuery(
+ $tables,
+ $select,
+ $conds,
+ $join_conds,
+ $query_options,
+ $opts['tagfilter']
+ );
if ( !wfRunHooks( 'SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$select ) ) ) {
return false;
@@ -224,10 +229,10 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges {
$opts->consumeValues( array( 'showlinkedto', 'target', 'tagfilter' ) );
$extraOpts = array();
$extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
- $extraOpts['target'] = array( wfMsgHtml( 'recentchangeslinked-page' ),
+ $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::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) );
+ Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) );
$tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
if ($tagFilter) {
$extraOpts['tagfilter'] = $tagFilter;
diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php
index df60a26a..aba90cf8 100644
--- a/includes/specials/SpecialRevisiondelete.php
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -66,6 +66,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
'list-class' => 'RevDel_RevisionList',
+ 'permission' => 'deleterevision',
),
'archive' => array(
'check-label' => 'revdelete-hide-text',
@@ -73,6 +74,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
'list-class' => 'RevDel_ArchiveList',
+ 'permission' => 'deleterevision',
),
'oldimage'=> array(
'check-label' => 'revdelete-hide-image',
@@ -80,6 +82,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
'list-class' => 'RevDel_FileList',
+ 'permission' => 'deleterevision',
),
'filearchive' => array(
'check-label' => 'revdelete-hide-image',
@@ -87,6 +90,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
'success' => 'revdelete-success',
'failure' => 'revdelete-failure',
'list-class' => 'RevDel_ArchivedFileList',
+ 'permission' => 'deleterevision',
),
'logging' => array(
'check-label' => 'revdelete-hide-name',
@@ -94,6 +98,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
'success' => 'logdelete-success',
'failure' => 'logdelete-failure',
'list-class' => 'RevDel_LogList',
+ 'permission' => 'deletelogentry',
),
);
@@ -117,7 +122,6 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$output = $this->getOutput();
$user = $this->getUser();
- $this->mIsAllowed = $user->isAllowed('deleterevision'); // for changes
$this->setHeaders();
$this->outputHeader();
$request = $this->getRequest();
@@ -143,6 +147,24 @@ 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...
@@ -159,10 +181,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
# No targets?
if( !isset( self::$allowedTypes[$this->typeName] ) || count( $this->ids ) == 0 ) {
- $output->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
- return;
+ 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
@@ -201,12 +223,14 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$qc = $this->getLogQueryCond();
# Show relevant lines from the deletion log
- $output->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ $deleteLogPage = new LogPage( 'delete' );
+ $output->addHTML( "<h2>" . $deleteLogPage->getName()->escaped() . "</h2>\n" );
LogEventsList::showLogExtract( $output, 'delete',
$this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) );
# Show relevant lines from the suppression log
if( $user->isAllowed( 'suppressionlog' ) ) {
- $output->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
+ $suppressLogPage = new LogPage( 'suppress' );
+ $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" );
LogEventsList::showLogExtract( $output, 'suppress',
$this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) );
}
@@ -221,7 +245,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$links = array();
$links[] = Linker::linkKnown(
SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'viewpagelogs' ),
+ $this->msg( 'viewpagelogs' )->escaped(),
array(),
array( 'page' => $this->targetObj->getPrefixedText() )
);
@@ -229,7 +253,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
# Give a link to the page history
$links[] = Linker::linkKnown(
$this->targetObj,
- wfMsgHtml( 'pagehist' ),
+ $this->msg( 'pagehist' )->escaped(),
array(),
array( 'action' => 'history' )
);
@@ -238,7 +262,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$undelete = SpecialPage::getTitleFor( 'Undelete' );
$links[] = Linker::linkKnown(
$undelete,
- wfMsgHtml( 'deletedhist' ),
+ $this->msg( 'deletedhist' )->escaped(),
array(),
array( 'target' => $this->targetObj->getPrefixedDBkey() )
);
@@ -251,6 +275,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
/**
* Get the condition used for fetching log snippets
+ * @return array
*/
protected function getLogQueryCond() {
$conds = array();
@@ -275,29 +300,30 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$this->getOutput()->addWikiMsg( 'revdelete-no-file' );
return;
}
- if( !$oimage->userCan( File::DELETED_FILE, $this->getUser() ) ) {
+ $user = $this->getUser();
+ if( !$oimage->userCan( File::DELETED_FILE, $user ) ) {
if( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
- $this->getOutput()->permissionRequired( 'suppressrevision' );
+ throw new PermissionsError( 'suppressrevision' );
} else {
- $this->getOutput()->permissionRequired( 'deletedtext' );
+ throw new PermissionsError( 'deletedtext' );
}
- return;
}
- if ( !$this->getUser()->matchEditToken( $this->token, $archiveName ) ) {
+ if ( !$user->matchEditToken( $this->token, $archiveName ) ) {
+ $lang = $this->getLanguage();
$this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm',
$this->targetObj->getText(),
- $this->getLanguage()->date( $oimage->getTimestamp() ),
- $this->getLanguage()->time( $oimage->getTimestamp() ) );
+ $lang->userDate( $oimage->getTimestamp(), $user ),
+ $lang->userTime( $oimage->getTimestamp(), $user ) );
$this->getOutput()->addHTML(
Xml::openElement( 'form', array(
'method' => 'POST',
'action' => $this->getTitle()->getLocalUrl(
- 'target=' . urlencode( $oimage->getName() ) .
+ 'target=' . urlencode( $this->targetObj->getPrefixedDBkey() ) .
'&file=' . urlencode( $archiveName ) .
- '&token=' . urlencode( $this->getUser()->getEditToken( $archiveName ) ) )
+ '&token=' . urlencode( $user->getEditToken( $archiveName ) ) )
)
) .
- Xml::submitButton( wfMsg( 'revdelete-show-file-submit' ) ) .
+ Xml::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) .
'</form>'
);
return;
@@ -350,8 +376,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$item = $list->current();
if ( !$item->canView() ) {
if( !$this->submitClicked ) {
- $this->getOutput()->permissionRequired( 'suppressrevision' );
- return;
+ throw new PermissionsError( 'suppressrevision' );
}
$UserAllowed = false;
}
@@ -360,8 +385,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
}
if( !$numRevisions ) {
- $this->getOutput()->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
- return;
+ throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
}
$this->getOutput()->addHTML( "</ul>" );
@@ -376,22 +400,23 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$out = Xml::openElement( 'form', array( 'method' => 'post',
'action' => $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ),
'id' => 'mw-revdel-form-revisions' ) ) .
- Xml::fieldset( wfMsg( 'revdelete-legend' ) ) .
+ Xml::fieldset( $this->msg( 'revdelete-legend' )->text() ) .
$this->buildCheckBoxes() .
Xml::openElement( 'table' ) .
"<tr>\n" .
'<td class="mw-label">' .
- Xml::label( wfMsg( 'revdelete-log' ), 'wpRevDeleteReasonList' ) .
+ Xml::label( $this->msg( 'revdelete-log' )->text(), 'wpRevDeleteReasonList' ) .
'</td>' .
'<td class="mw-input">' .
Xml::listDropDown( 'wpRevDeleteReasonList',
- wfMsgForContent( 'revdelete-reason-dropdown' ),
- wfMsgForContent( 'revdelete-reasonotherlist' ), '', 'wpReasonDropDown', 1
+ $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->text(),
+ $this->msg( 'revdelete-reasonotherlist' )->inContentLanguage()->text(),
+ '', 'wpReasonDropDown', 1
) .
'</td>' .
"</tr><tr>\n" .
'<td class="mw-label">' .
- Xml::label( wfMsg( 'revdelete-otherreason' ), 'wpReason' ) .
+ Xml::label( $this->msg( 'revdelete-otherreason' )->text(), 'wpReason' ) .
'</td>' .
'<td class="mw-input">' .
Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason', 'maxlength' => 100 ) ) .
@@ -399,7 +424,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
"</tr><tr>\n" .
'<td></td>' .
'<td class="mw-submit">' .
- Xml::submitButton( wfMsgExt('revdelete-submit','parsemag',$numRevisions),
+ Xml::submitButton( $this->msg( 'revdelete-submit', $numRevisions )->text(),
array( 'name' => 'wpSubmit' ) ) .
'</td>' .
"</tr>\n" .
@@ -416,10 +441,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$out .= Xml::closeElement( 'form' ) . "\n";
// Show link to edit the dropdown reasons
if( $this->getUser()->isAllowed( 'editinterface' ) ) {
- $title = Title::makeTitle( NS_MEDIAWIKI, 'revdelete-reason-dropdown' );
+ $title = Title::makeTitle( NS_MEDIAWIKI, 'Revdelete-reason-dropdown' );
$link = Linker::link(
$title,
- wfMsgHtml( 'revdelete-edit-reasonlist' ),
+ $this->msg( 'revdelete-edit-reasonlist' )->escaped(),
array(),
array( 'action' => 'edit' )
);
@@ -458,7 +483,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
}
foreach( $this->checks as $item ) {
list( $message, $name, $field ) = $item;
- $innerHTML = Xml::checkLabel( wfMsg($message), $name, $name, $bitfield & $field );
+ $innerHTML = Xml::checkLabel( $this->msg( $message )->text(), $name, $name, $bitfield & $field );
if( $field == Revision::DELETED_RESTRICTED )
$innerHTML = "<b>$innerHTML</b>";
$line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML );
@@ -467,9 +492,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
// Otherwise, use tri-state radios
} else {
$html .= '<tr>';
- $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-same').'</th>';
- $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-unset').'</th>';
- $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-set').'</th>';
+ $html .= '<th class="mw-revdel-checkbox">' . $this->msg( 'revdelete-radio-same' )->escaped() . '</th>';
+ $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 ) {
list( $message, $name, $field ) = $item;
@@ -482,7 +507,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$line = '<td class="mw-revdel-checkbox">' . Xml::radio( $name, -1, $selected == -1 ) . '</td>';
$line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>';
$line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>';
- $label = wfMsgHtml($message);
+ $label = $this->msg( $message )->escaped();
if( $field == Revision::DELETED_RESTRICTED ) {
$label = "<b>$label</b>";
}
@@ -497,6 +522,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
/**
* UI entry point for form submission.
+ * @return bool
*/
protected function submit() {
# Check edit token on submission
@@ -510,14 +536,13 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$comment = $listReason;
if( $comment != 'other' && $this->otherReason != '' ) {
// Entry from drop down menu + additional comment
- $comment .= wfMsgForContent( 'colon-separator' ) . $this->otherReason;
+ $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->otherReason;
} elseif( $comment == 'other' ) {
$comment = $this->otherReason;
}
# Can the user set this field?
if( $bitParams[Revision::DELETED_RESTRICTED]==1 && !$this->getUser()->isAllowed('suppressrevision') ) {
- $this->getOutput()->permissionRequired( 'suppressrevision' );
- return false;
+ throw new PermissionsError( 'suppressrevision' );
}
# If the save went through, go to success message...
$status = $this->save( $bitParams, $comment, $this->targetObj );
@@ -592,6 +617,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
/**
* Do the write operations. Simple wrapper for RevDel_*List::setVisibility().
+ * @param $bitfield
+ * @param $reason
+ * @param $title
+ * @return
*/
protected function save( $bitfield, $reason, $title ) {
return $this->getList()->setVisibility(
diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php
index 3fa86875..5f5b6b4d 100644
--- a/includes/specials/SpecialSearch.php
+++ b/includes/specials/SpecialSearch.php
@@ -174,7 +174,8 @@ class SpecialSearch extends SpecialPage {
$t = Title::newFromText( $term );
# If the string cannot be used to create a title
if( is_null( $t ) ) {
- return $this->showResults( $term );
+ $this->showResults( $term );
+ return;
}
# If there's an exact or very near match, jump right there.
$t = SearchEngine::getNearMatch( $term );
@@ -201,7 +202,7 @@ class SpecialSearch extends SpecialPage {
return;
}
}
- return $this->showResults( $term );
+ $this->showResults( $term );
}
/**
@@ -232,13 +233,13 @@ class SpecialSearch extends SpecialPage {
} else {
$out->addHTML(
Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
- Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
- wfMsg( 'googlesearch',
+ Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) .
+ Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), $this->msg( 'searchdisabled' )->text() ) .
+ $this->msg( 'googlesearch' )->rawParams(
htmlspecialchars( $term ),
- htmlspecialchars( 'UTF-8' ),
- htmlspecialchars( wfMsg( 'searchbutton' ) )
- ) .
+ 'UTF-8',
+ $this->msg( 'searchbutton' )->escaped()
+ )->text() .
Xml::closeElement( 'fieldset' )
);
}
@@ -285,7 +286,7 @@ class SpecialSearch extends SpecialPage {
$stParams
);
- $this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>';
+ $this->didYouMeanHtml = '<div class="searchdidyoumean">' . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>';
}
// start rendering the page
$out->addHtml(
@@ -299,7 +300,7 @@ class SpecialSearch extends SpecialPage {
)
);
$out->addHtml(
- Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-search-top-table', 'cellpadding' => 0, 'cellspacing' => 0 ) ) .
Xml::openElement( 'tr' ) .
Xml::openElement( 'td' ) . "\n" .
$this->shortDialog( $term ) .
@@ -583,13 +584,8 @@ class SpecialSearch extends SpecialPage {
$redirectText = null;
$redirect = "<span class='searchalttitle'>" .
- wfMsg(
- 'search-redirect',
- Linker::linkKnown(
- $redirectTitle,
- $redirectText
- )
- ) .
+ $this->msg( 'search-redirect' )->rawParams(
+ Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
"</span>";
}
@@ -600,12 +596,8 @@ class SpecialSearch extends SpecialPage {
$sectionText = null;
$section = "<span class='searchalttitle'>" .
- wfMsg(
- 'search-section', Linker::linkKnown(
- $sectionTitle,
- $sectionText
- )
- ) .
+ $this->msg( 'search-section' )->rawParams(
+ Linker::linkKnown( $sectionTitle, $sectionText ) )->text() .
"</span>";
}
@@ -620,7 +612,7 @@ class SpecialSearch extends SpecialPage {
$score = '';
} else {
$percent = sprintf( '%2.1f', $result->getScore() * 100 );
- $score = wfMsg( 'search-result-score', $lang->formatNum( $percent ) )
+ $score = $this->msg( 'search-result-score' )->numParams( $percent )->text()
. ' - ';
}
@@ -628,25 +620,17 @@ class SpecialSearch extends SpecialPage {
$byteSize = $result->getByteSize();
$wordCount = $result->getWordCount();
$timestamp = $result->getTimestamp();
- $size = wfMsgExt(
- 'search-result-size',
- array( 'parsemag', 'escape' ),
- $lang->formatSize( $byteSize ),
- $lang->formatNum( $wordCount )
- );
+ $size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) )
+ ->numParams( $wordCount )->escaped();
if( $t->getNamespace() == NS_CATEGORY ) {
$cat = Category::newFromTitle( $t );
- $size = wfMsgExt(
- 'search-result-category-size',
- array( 'parsemag', 'escape' ),
- $lang->formatNum( $cat->getPageCount() ),
- $lang->formatNum( $cat->getSubcatCount() ),
- $lang->formatNum( $cat->getFileCount() )
- );
+ $size = $this->msg( 'search-result-category-size' )
+ ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
+ ->escaped();
}
- $date = $lang->timeanddate( $timestamp );
+ $date = $lang->userTimeAndDate( $timestamp, $this->getUser() );
// link to related articles if supported
$related = '';
@@ -655,14 +639,15 @@ class SpecialSearch extends SpecialPage {
$stParams = array_merge(
$this->powerSearchOptions(),
array(
- 'search' => wfMsgForContent( 'searchrelated' ) . ':' . $t->getPrefixedText(),
- 'fulltext' => wfMsg( 'search' )
+ 'search' => $this->msg( 'searchrelated' )->inContentLanguage()->text() .
+ ':' . $t->getPrefixedText(),
+ 'fulltext' => $this->msg( 'search' )->text()
)
);
$related = ' -- ' . Linker::linkKnown(
$st,
- wfMsg('search-relatedarticle'),
+ $this->msg( 'search-relatedarticle' )->text(),
array(),
$stParams
);
@@ -674,7 +659,7 @@ class SpecialSearch extends SpecialPage {
if( $img ) {
$thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
if( $thumb ) {
- $desc = wfMsg( 'parentheses', $img->getShortDesc() );
+ $desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped();
wfProfileOut( __METHOD__ );
// Float doesn't seem to interact well with the bullets.
// Table messes up vertical alignment of the bullets.
@@ -682,10 +667,10 @@ class SpecialSearch extends SpecialPage {
return "<li>" .
'<table class="searchResultImage">' .
'<tr>' .
- '<td width="120" align="center" valign="top">' .
+ '<td width="120" style="text-align: center; vertical-align: top;">' .
$thumb->toHtml( array( 'desc-link' => true ) ) .
'</td>' .
- '<td valign="top">' .
+ '<td style="vertical-align: top;">' .
$link .
$extract .
"<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
@@ -718,12 +703,12 @@ class SpecialSearch extends SpecialPage {
$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
$out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
- wfMsg('search-interwiki-caption')."</div>\n";
+ $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",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
+ $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
@@ -786,13 +771,8 @@ class SpecialSearch extends SpecialPage {
$redirectText = null;
$redirect = "<span class='searchalttitle'>" .
- wfMsg(
- 'search-redirect',
- Linker::linkKnown(
- $redirectTitle,
- $redirectText
- )
- ) .
+ $this->msg( 'search-redirect' )->rawParams(
+ Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
"</span>";
}
@@ -806,13 +786,13 @@ class SpecialSearch extends SpecialPage {
// default is to show the hostname of the other wiki which might suck
// if there are many wikis on one hostname
$parsed = wfParseUrl( $t->getFullURL() );
- $caption = wfMsg('search-interwiki-default', $parsed['host']);
+ $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");
$searchLink = Linker::linkKnown(
$searchTitle,
- wfMsg('search-interwiki-more'),
+ $this->msg( 'search-interwiki-more' )->text(),
array(),
array(
'search' => $query,
@@ -865,7 +845,7 @@ class SpecialSearch extends SpecialPage {
}
$name = str_replace( '_', ' ', $name );
if( $name == '' ) {
- $name = wfMsg( 'blanknamespace' );
+ $name = $this->msg( 'blanknamespace' )->text();
}
$rows[$subject] .=
Xml::openElement(
@@ -888,7 +868,7 @@ class SpecialSearch extends SpecialPage {
for( $i = 0; $i < $numRows; $i += 4 ) {
$namespaceTables .= Xml::openElement(
'table',
- array( 'cellpadding' => 0, 'cellspacing' => 0, 'border' => 0 )
+ array( 'cellpadding' => 0, 'cellspacing' => 0 )
);
for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
$namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
@@ -901,7 +881,7 @@ class SpecialSearch extends SpecialPage {
// Show redirects check only if backend supports it
if( $this->getSearchEngine()->supports( 'list-redirects' ) ) {
$showSections['redirects'] =
- Xml::checkLabel( wfMsg( 'powersearch-redir' ), 'redirs', 'redirs', $this->searchRedirects );
+ Xml::checkLabel( $this->msg( 'powersearch-redir' )->text(), 'redirs', 'redirs', $this->searchRedirects );
}
wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) );
@@ -917,29 +897,9 @@ class SpecialSearch extends SpecialPage {
'fieldset',
array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
) .
- Xml::element( 'legend', null, wfMsg('powersearch-legend') ) .
- Xml::tags( 'h4', null, wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) ) .
- Xml::tags(
- 'div',
- array( 'id' => 'mw-search-togglebox' ),
- Xml::label( wfMsg( 'powersearch-togglelabel' ), 'mw-search-togglelabel' ) .
- Xml::element(
- 'input',
- array(
- 'type'=>'button',
- 'id' => 'mw-search-toggleall',
- 'value' => wfMsg( 'powersearch-toggleall' )
- )
- ) .
- Xml::element(
- 'input',
- array(
- 'type'=>'button',
- 'id' => 'mw-search-togglenone',
- 'value' => wfMsg( 'powersearch-togglenone' )
- )
- )
- ) .
+ 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 ) .
implode( Xml::element( 'div', array( 'class' => 'divider' ), '', false ), $showSections ) .
$hidden .
@@ -1034,8 +994,8 @@ class SpecialSearch extends SpecialPage {
$this->makeSearchLink(
$bareterm,
array(),
- wfMsg( $profile['message'] ),
- wfMsg( $profile['tooltip'], $tooltipParam ),
+ $this->msg( $profile['message'] )->text(),
+ $this->msg( $profile['tooltip'], $tooltipParam )->text(),
$profile['parameters']
)
);
@@ -1046,24 +1006,19 @@ class SpecialSearch extends SpecialPage {
// Results-info
if ( $resultsShown > 0 ) {
if ( $totalNum > 0 ){
- $top = wfMsgExt( 'showingresultsheader', array( 'parseinline' ),
- $lang->formatNum( $this->offset + 1 ),
- $lang->formatNum( $this->offset + $resultsShown ),
- $lang->formatNum( $totalNum ),
- wfEscapeWikiText( $term ),
- $lang->formatNum( $resultsShown )
- );
+ $top = $this->msg( 'showingresultsheader' )
+ ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
+ ->params( wfEscapeWikiText( $term ) )
+ ->numParams( $resultsShown )
+ ->parse();
} elseif ( $resultsShown >= $this->limit ) {
- $top = wfMsgExt( 'showingresults', array( 'parseinline' ),
- $lang->formatNum( $this->limit ),
- $lang->formatNum( $this->offset + 1 )
- );
+ $top = $this->msg( 'showingresults' )
+ ->numParams( $this->limit, $this->offset + 1 )
+ ->parse();
} else {
- $top = wfMsgExt( 'showingresultsnum', array( 'parseinline' ),
- $lang->formatNum( $this->limit ),
- $lang->formatNum( $this->offset + 1 ),
- $lang->formatNum( $resultsShown )
- );
+ $top = $this->msg( 'showingresultsnum' )
+ ->numParams( $this->limit, $this->offset + 1, $resultsShown )
+ ->parse();
}
$out .= Xml::tags( 'div', array( 'class' => 'results-info' ),
Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) )
@@ -1090,7 +1045,7 @@ class SpecialSearch extends SpecialPage {
'autofocus'
) ) . "\n";
$out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
- $out .= Xml::submitButton( wfMsg( 'searchbutton' ) ) . "\n";
+ $out .= Xml::submitButton( $this->msg( 'searchbutton' )->text() ) . "\n";
return $out . $this->didYouMeanHtml;
}
@@ -1114,7 +1069,7 @@ class SpecialSearch extends SpecialPage {
$stParams = array_merge(
array(
'search' => $term,
- 'fulltext' => wfMsg( 'search' )
+ 'fulltext' => $this->msg( 'search' )->text()
),
$opt
);
@@ -1152,7 +1107,7 @@ class SpecialSearch extends SpecialPage {
*/
protected function startsWithAll( $term ) {
- $allkeyword = wfMsgForContent('searchall');
+ $allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text();
$p = explode( ':', $term );
if( count( $p ) > 1 ) {
diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php
index c176f913..5a4e8f03 100644
--- a/includes/specials/SpecialShortpages.php
+++ b/includes/specials/SpecialShortpages.php
@@ -40,10 +40,11 @@ class ShortPagesPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'page' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_len AS value' ),
- 'conds' => array ( 'page_namespace' => NS_MAIN,
+ 'fields' => array ( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_len' ),
+ 'conds' => array ( 'page_namespace' =>
+ MWNamespace::getContentNamespaces(),
'page_is_redirect' => 0 ),
'options' => array ( 'USE INDEX' => 'page_redirect_namespace_len' )
);
@@ -61,16 +62,17 @@ class ShortPagesPage extends QueryPage {
function preprocessResults( $db, $res ) {
# There's no point doing a batch check if we aren't caching results;
# the page must exist for it to have been pulled out of the table
- if( $this->isCached() ) {
- $batch = new LinkBatch();
- foreach ( $res as $row ) {
- $batch->add( $row->namespace, $row->title );
- }
- $batch->execute();
- if ( $db->numRows( $res ) > 0 ) {
- $db->dataSeek( $res, 0 );
- }
+ if ( !$this->isCached() || !$res->numRows() ) {
+ return;
}
+
+ $batch = new LinkBatch();
+ foreach ( $res as $row ) {
+ $batch->add( $row->namespace, $row->title );
+ }
+ $batch->execute();
+
+ $res->seek( 0 );
}
function sortDescending() {
@@ -80,23 +82,32 @@ class ShortPagesPage extends QueryPage {
function formatResult( $skin, $result ) {
$dm = $this->getLanguage()->getDirMark();
- $title = Title::makeTitle( $result->namespace, $result->title );
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
if ( !$title ) {
- return '<!-- Invalid title ' . htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->';
+ return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) );
}
+
$hlink = Linker::linkKnown(
$title,
- wfMsgHtml( 'hist' ),
+ $this->msg( 'hist' )->escaped(),
array(),
array( 'action' => 'history' )
);
- $plink = $this->isCached()
- ? Linker::link( $title )
- : Linker::linkKnown( $title );
+ $hlinkInParentheses = $this->msg( 'parentheses' )->rawParams( $hlink )->escaped();
+
+ if ( $this->isCached() ) {
+ $plink = Linker::link( $title );
+ $exists = $title->exists();
+ } else {
+ $plink = Linker::linkKnown( $title );
+ $exists = true;
+ }
+
$size = $this->msg( 'nbytes' )->numParams( $result->value )->escaped();
- return $title->exists()
- ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]"
- : "<del>({$hlink}) {$dm}{$plink} {$dm}[{$size}]</del>";
+ return $exists
+ ? "${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]"
+ : "<del>${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]</del>";
}
}
diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php
index b9c092b6..46881ec4 100644
--- a/includes/specials/SpecialStatistics.php
+++ b/includes/specials/SpecialStatistics.php
@@ -97,7 +97,7 @@ class SpecialStatistics extends SpecialPage {
$text .= Xml::closeElement( 'table' );
# Customizable footer
- $footer = wfMessage( 'statistics-footer' );
+ $footer = $this->msg( 'statistics-footer' );
if ( !$footer->isBlank() ) {
$text .= "\n" . $footer->parse();
}
@@ -116,11 +116,11 @@ class SpecialStatistics extends SpecialPage {
*/
private function formatRow( $text, $number, $trExtraParams = array(), $descMsg = '', $descMsgParam = '' ) {
if( $descMsg ) {
- $msg = wfMessage( $descMsg, $descMsgParam );
+ $msg = $this->msg( $descMsg, $descMsgParam );
if ( $msg->exists() ) {
- $descriptionText = $msg->parse();
+ $descriptionText = $this->msg( 'parentheses' )->rawParams( $msg->parse() )->escaped();
$text .= "<br />" . Xml::element( 'small', array( 'class' => 'mw-statistic-desc'),
- " ($descriptionText)" );
+ " $descriptionText" );
}
}
return Html::rawElement( 'tr', $trExtraParams,
@@ -136,29 +136,29 @@ class SpecialStatistics extends SpecialPage {
*/
private function getPageStats() {
return Xml::openElement( 'tr' ) .
- Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-pages', array( 'parseinline' ) ) ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-pages' )->parse() ) .
Xml::closeElement( 'tr' ) .
$this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'Allpages' ),
- wfMsgExt( 'statistics-articles', array( 'parseinline' ) ) ),
+ $this->msg( 'statistics-articles' )->parse() ),
$this->getLanguage()->formatNum( $this->good ),
array( 'class' => 'mw-statistics-articles' ) ) .
- $this->formatRow( wfMsgExt( 'statistics-pages', array( 'parseinline' ) ),
+ $this->formatRow( $this->msg( 'statistics-pages' )->parse(),
$this->getLanguage()->formatNum( $this->total ),
array( 'class' => 'mw-statistics-pages' ),
'statistics-pages-desc' ) .
$this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'Listfiles' ),
- wfMsgExt( 'statistics-files', array( 'parseinline' ) ) ),
+ $this->msg( 'statistics-files' )->parse() ),
$this->getLanguage()->formatNum( $this->images ),
array( 'class' => 'mw-statistics-files' ) );
}
private function getEditStats() {
return Xml::openElement( 'tr' ) .
- Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-edits', array( 'parseinline' ) ) ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-edits' )->parse() ) .
Xml::closeElement( 'tr' ) .
- $this->formatRow( wfMsgExt( 'statistics-edits', array( 'parseinline' ) ),
+ $this->formatRow( $this->msg( 'statistics-edits' )->parse(),
$this->getLanguage()->formatNum( $this->edits ),
array( 'class' => 'mw-statistics-edits' ) ) .
- $this->formatRow( wfMsgExt( 'statistics-edits-average', array( 'parseinline' ) ),
+ $this->formatRow( $this->msg( 'statistics-edits-average' )->parse(),
$this->getLanguage()->formatNum( sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) ),
array( 'class' => 'mw-statistics-edits-average' ) );
}
@@ -166,15 +166,15 @@ class SpecialStatistics extends SpecialPage {
private function getUserStats() {
global $wgActiveUserDays;
return Xml::openElement( 'tr' ) .
- Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-users', array( 'parseinline' ) ) ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-users' )->parse() ) .
Xml::closeElement( 'tr' ) .
- $this->formatRow( wfMsgExt( 'statistics-users', array( 'parseinline' ) ),
+ $this->formatRow( $this->msg( 'statistics-users' )->parse(),
$this->getLanguage()->formatNum( $this->users ),
array( 'class' => 'mw-statistics-users' ) ) .
- $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ) . ' ' .
+ $this->formatRow( $this->msg( 'statistics-users-active' )->parse() . ' ' .
Linker::linkKnown(
SpecialPage::getTitleFor( 'Activeusers' ),
- wfMsgHtml( 'listgrouprights-members' )
+ $this->msg( 'listgrouprights-members' )->escaped()
),
$this->getLanguage()->formatNum( $this->activeUsers ),
array( 'class' => 'mw-statistics-users-active' ),
@@ -191,13 +191,13 @@ class SpecialStatistics extends SpecialPage {
continue;
}
$groupname = htmlspecialchars( $group );
- $msg = wfMessage( 'group-' . $groupname );
+ $msg = $this->msg( 'group-' . $groupname );
if ( $msg->isBlank() ) {
$groupnameLocalized = $groupname;
} else {
$groupnameLocalized = $msg->text();
}
- $msg = wfMessage( 'grouppage-' . $groupname )->inContentLanguage();
+ $msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage();
if ( $msg->isBlank() ) {
$grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
} else {
@@ -210,7 +210,7 @@ class SpecialStatistics extends SpecialPage {
);
$grouplink = Linker::linkKnown(
SpecialPage::getTitleFor( 'Listusers' ),
- wfMsgHtml( 'listgrouprights-members' ),
+ $this->msg( 'listgrouprights-members' )->escaped(),
array(),
array( 'group' => $group )
);
@@ -229,12 +229,12 @@ class SpecialStatistics extends SpecialPage {
private function getViewsStats() {
return Xml::openElement( 'tr' ) .
- Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-views', array( 'parseinline' ) ) ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-views' )->parse() ) .
Xml::closeElement( 'tr' ) .
- $this->formatRow( wfMsgExt( 'statistics-views-total', array( 'parseinline' ) ),
+ $this->formatRow( $this->msg( 'statistics-views-total' )->parse(),
$this->getLanguage()->formatNum( $this->views ),
array ( 'class' => 'mw-statistics-views-total' ), 'statistics-views-total-desc' ) .
- $this->formatRow( wfMsgExt( 'statistics-views-peredit', array( 'parseinline' ) ),
+ $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' ) );
@@ -262,7 +262,7 @@ class SpecialStatistics extends SpecialPage {
);
if( $res->numRows() > 0 ) {
$text .= Xml::openElement( 'tr' );
- $text .= Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-mostpopular', array( 'parseinline' ) ) );
+ $text .= Xml::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 );
@@ -282,7 +282,7 @@ class SpecialStatistics extends SpecialPage {
return '';
$return = Xml::openElement( 'tr' ) .
- Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-hooks', array( 'parseinline' ) ) ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-hooks' )->parse() ) .
Xml::closeElement( 'tr' );
foreach( $stats as $name => $number ) {
diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php
index adfc7441..4036ebb2 100644
--- a/includes/specials/SpecialTags.php
+++ b/includes/specials/SpecialTags.php
@@ -21,9 +21,6 @@
* @ingroup SpecialPage
*/
-if (!defined('MEDIAWIKI'))
- die;
-
/**
* A special page that lists tags for edits
*
@@ -44,13 +41,13 @@ class SpecialTags extends SpecialPage {
$out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
// Write the headers
- $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, wfMsgExt( 'tags-tag', 'parseinline' ) ) .
- Xml::tags( 'th', null, wfMsgExt( 'tags-display-header', 'parseinline' ) ) .
- Xml::tags( 'th', null, wfMsgExt( 'tags-description-header', 'parseinline' ) ) .
- Xml::tags( 'th', null, wfMsgExt( 'tags-hitcount-header', 'parseinline' ) )
+ $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-hitcount-header' )->parse() )
);
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'change_tag', array( 'ct_tag', 'count(*) AS hitcount' ),
+ $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 ) {
@@ -72,18 +69,22 @@ class SpecialTags extends SpecialPage {
}
$newRow = '';
- $newRow .= Xml::tags( 'td', null, Xml::element( 'tt', null, $tag ) );
+ $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
$disp = ChangeTags::tagDescription( $tag );
- $disp .= ' (' . Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsgHtml( 'tags-edit' ) ) . ')';
+ $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 = wfMessage( "tag-$tag-description" );
+ $msg = $this->msg( "tag-$tag-description" );
$desc = !$msg->exists() ? '' : $msg->parse();
- $desc .= ' (' . Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsgHtml( 'tags-edit' ) ) . ')';
+ $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 = wfMsgExt( 'tags-hitcount', array( 'parsemag' ), $this->getLanguage()->formatNum( $hitcount ) );
+ $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 );
diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php
index 47944309..fb2005b5 100644
--- a/includes/specials/SpecialUnblock.php
+++ b/includes/specials/SpecialUnblock.php
@@ -1,5 +1,7 @@
<?php
/**
+ * Implements Special:Unblock
+ *
* 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
@@ -49,23 +51,23 @@ class SpecialUnblock extends SpecialPage {
$out->addModules( 'mediawiki.special' );
$form = new HTMLForm( $this->getFields(), $this->getContext() );
- $form->setWrapperLegend( wfMsg( 'unblockip' ) );
+ $form->setWrapperLegendMsg( 'unblockip' );
$form->setSubmitCallback( array( __CLASS__, 'processUIUnblock' ) );
- $form->setSubmitText( wfMsg( 'ipusubmit' ) );
- $form->addPreText( wfMsgExt( 'unblockiptext', 'parse' ) );
+ $form->setSubmitTextMsg( 'ipusubmit' );
+ $form->addPreText( $this->msg( 'unblockiptext' )->parseAsBlock() );
if( $form->show() ){
switch( $this->type ){
case Block::TYPE_USER:
case Block::TYPE_IP:
- $out->addWikiMsg( 'unblocked', $this->target );
+ $out->addWikiMsg( 'unblocked', wfEscapeWikiText( $this->target ) );
break;
case Block::TYPE_RANGE:
- $out->addWikiMsg( 'unblocked-range', $this->target );
+ $out->addWikiMsg( 'unblocked-range', wfEscapeWikiText( $this->target ) );
break;
case Block::TYPE_ID:
case Block::TYPE_AUTO:
- $out->addWikiMsg( 'unblocked-id', $this->target );
+ $out->addWikiMsg( 'unblocked-id', wfEscapeWikiText( $this->target ) );
break;
}
}
@@ -136,6 +138,7 @@ class SpecialUnblock extends SpecialPage {
/**
* Submit callback for an HTMLForm object
+ * @return Array( Array(message key, parameters)
*/
public static function processUIUnblock( array $data, HTMLForm $form ) {
return self::processUnblock( $data, $form->getContext() );
diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php
index 3efed747..5865bf62 100644
--- a/includes/specials/SpecialUncategorizedimages.php
+++ b/includes/specials/SpecialUncategorizedimages.php
@@ -49,9 +49,9 @@ class UncategorizedImagesPage extends ImageQueryPage {
function getQueryInfo() {
return array (
'tables' => array( 'page', 'categorylinks' ),
- 'fields' => array( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_title AS value' ),
+ 'fields' => array( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_title' ),
'conds' => array( 'cl_from IS NULL',
'page_namespace' => NS_FILE,
'page_is_redirect' => 0 ),
diff --git a/includes/specials/SpecialUncategorizedpages.php b/includes/specials/SpecialUncategorizedpages.php
index 08a69448..1226a6ca 100644
--- a/includes/specials/SpecialUncategorizedpages.php
+++ b/includes/specials/SpecialUncategorizedpages.php
@@ -46,9 +46,9 @@ class UncategorizedPagesPage extends PageQueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'page', 'categorylinks' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_title AS value' ),
+ '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',
diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php
index 5d8b17b7..d8e0b97c 100644
--- a/includes/specials/SpecialUndelete.php
+++ b/includes/specials/SpecialUndelete.php
@@ -92,13 +92,13 @@ class PageArchive {
array(
'ar_namespace',
'ar_title',
- 'COUNT(*) AS count'
+ 'count' => 'COUNT(*)'
),
$condition,
__METHOD__,
array(
- 'GROUP BY' => 'ar_namespace,ar_title',
- 'ORDER BY' => 'ar_namespace,ar_title',
+ 'GROUP BY' => array( 'ar_namespace', 'ar_title' ),
+ 'ORDER BY' => array( 'ar_namespace', 'ar_title' ),
'LIMIT' => 100,
)
)
@@ -120,7 +120,7 @@ class PageArchive {
),
array( 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey() ),
- 'PageArchive::listRevisions',
+ __METHOD__,
array( 'ORDER BY' => 'ar_timestamp DESC' ) );
$ret = $dbr->resultObject( $res );
return $ret;
@@ -195,7 +195,7 @@ class PageArchive {
'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
__METHOD__ );
if( $row ) {
- return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleId() ) );
+ return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleID() ) );
} else {
return null;
}
@@ -308,7 +308,9 @@ class PageArchive {
$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 );
}
@@ -321,11 +323,14 @@ class PageArchive {
* @param $comment String
* @param $fileVersions Array
* @param $unsuppress Boolean
+ * @param $user User doing 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 ) {
+ 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 );
@@ -354,28 +359,35 @@ class PageArchive {
}
// Touch the log!
- global $wgContLang;
- $log = new LogPage( 'delete' );
if( $textRestored && $filesRestored ) {
- $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
- $wgContLang->formatNum( $textRestored ),
- $wgContLang->formatNum( $filesRestored ) );
+ $reason = wfMessage( 'undeletedrevisions-files' )
+ ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
} elseif( $textRestored ) {
- $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ),
- $wgContLang->formatNum( $textRestored ) );
+ $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
+ ->inContentLanguage()->text();
} elseif( $filesRestored ) {
- $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ),
- $wgContLang->formatNum( $filesRestored ) );
+ $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
+ ->inContentLanguage()->text();
} else {
wfDebug( "Undelete: nothing undeleted...\n" );
return false;
}
if( trim( $comment ) != '' ) {
- $reason .= wfMsgForContent( 'colon-separator' ) . $comment;
+ $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
}
- $log->addEntry( 'restore', $this->title, $reason );
+
+ if ( $user === null ) {
+ $user = $wgUser;
+ }
+
+ $logEntry = new ManualLogEntry( 'delete', 'restore' );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $this->title );
+ $logEntry->setComment( $reason );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
return array( $textRestored, $filesRestored, $reason );
}
@@ -745,14 +757,20 @@ class SpecialUndelete extends SpecialPage {
$out->addHTML( "<ul>\n" );
foreach ( $result as $row ) {
$title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
- $link = Linker::linkKnown(
- $undelete,
- htmlspecialchars( $title->getPrefixedText() ),
- array(),
- array( 'target' => $title->getPrefixedText() )
- );
+ if ( $title !== null ) {
+ $item = Linker::linkKnown(
+ $undelete,
+ htmlspecialchars( $title->getPrefixedText() ),
+ array(),
+ array( 'target' => $title->getPrefixedText() )
+ );
+ } 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 ) );
+ }
$revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
- $out->addHTML( "<li>{$link} ({$revs})</li>\n" );
+ $out->addHTML( "<li>{$item} ({$revs})</li>\n" );
}
$result->free();
$out->addHTML( "</ul>\n" );
@@ -890,21 +908,22 @@ class SpecialUndelete extends SpecialPage {
$diffEngine->showDiffStyle();
$this->getOutput()->addHTML(
"<div>" .
- "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+ "<table width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
"<col class='diff-marker' />" .
"<col class='diff-content' />" .
"<col class='diff-marker' />" .
"<col class='diff-content' />" .
"<tr>" .
- "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
+ "<td colspan='2' width='50%' style='text-align: center' class='diff-otitle'>" .
$this->diffHeader( $previousRev, 'o' ) .
"</td>\n" .
- "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
+ "<td colspan='2' width='50%' style='text-align: center' class='diff-ntitle'>" .
$this->diffHeader( $currentRev, 'n' ) .
"</td>\n" .
"</tr>" .
$diffEngine->generateDiffBody(
- $previousRev->getText(), $currentRev->getText() ) .
+ $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ),
+ $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) .
"</table>" .
"</div>\n"
);
@@ -1009,7 +1028,7 @@ class SpecialUndelete extends SpecialPage {
}
$out->wrapWikiMsg(
"<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
- array( 'undeletepagetitle', $this->mTargetObj->getPrefixedText() )
+ array( 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) )
);
$archive = new PageArchive( $this->mTargetObj );
@@ -1065,11 +1084,13 @@ class SpecialUndelete extends SpecialPage {
}
# Show relevant lines from the deletion log:
- $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" );
+ $deleteLogPage = new LogPage( 'delete' );
+ $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
# Show relevant lines from the suppression log:
+ $suppressLogPage = new LogPage( 'suppress' );
if( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
- $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'suppress' ) ) . "\n" );
+ $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
}
@@ -1159,8 +1180,8 @@ class SpecialUndelete extends SpecialPage {
private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
$rev = Revision::newFromArchiveRow( $row,
- array( 'page' => $this->mTargetObj->getArticleId() ) );
- $stxt = '';
+ array( 'page' => $this->mTargetObj->getArticleID() ) );
+ $revTextSize = '';
$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
// Build checkboxen...
if( $this->mAllowed ) {
@@ -1209,13 +1230,15 @@ class SpecialUndelete extends SpecialPage {
// Revision text size
$size = $row->ar_len;
if( !is_null( $size ) ) {
- $stxt = Linker::formatRevisionSize( $size );
+ $revTextSize = Linker::formatRevisionSize( $size );
}
// Edit summary
$comment = Linker::revComment( $rev );
// Revision delete links
$revdlink = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
- return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
+
+ $revisionRow = $this->msg( 'undelete-revisionrow' )->rawParams( $checkBox, $revdlink, $last, $pageLink , $userLink, $revTextSize, $comment )->escaped();
+ return "<li>$revisionRow</li>";
}
private function formatFileRow( $row ) {
@@ -1232,9 +1255,9 @@ class SpecialUndelete extends SpecialPage {
$pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user );
}
$userLink = $this->getFileUser( $file );
- $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text() .
- ' (' . $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() . ')';
- $data = htmlspecialchars( $data );
+ $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();
+ $data = htmlspecialchars( $data . ' ' . $bytes );
$comment = $this->getFileComment( $file );
// Add show/hide deletion links if available
@@ -1263,7 +1286,7 @@ class SpecialUndelete extends SpecialPage {
*
* @param $rev Revision
* @param $titleObj Title
- * @param $ts Timestamp
+ * @param $ts string Timestamp
* @return string
*/
function getPageLink( $rev, $titleObj, $ts ) {
@@ -1294,7 +1317,7 @@ class SpecialUndelete extends SpecialPage {
*
* @param $file File
* @param $titleObj Title
- * @param $ts A timestamp
+ * @param $ts string A timestamp
* @param $key String: a storage key
*
* @return String: HTML fragment
@@ -1379,7 +1402,9 @@ class SpecialUndelete extends SpecialPage {
$this->mTargetTimestamp,
$this->mComment,
$this->mFileVersions,
- $this->mUnsuppress );
+ $this->mUnsuppress,
+ $this->getUser()
+ );
if( is_array( $ok ) ) {
if ( $ok[1] ) { // Undeleted file count
@@ -1399,7 +1424,7 @@ class SpecialUndelete extends SpecialPage {
// Show file deletion warnings and errors
$status = $archive->getFileStatus();
if( $status && !$status->isGood() ) {
- $out->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
+ $out->addWikiText( '<div class="error">' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '</div>' );
}
}
}
diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php
index 48a93e8d..1bd38e17 100644
--- a/includes/specials/SpecialUnusedcategories.php
+++ b/includes/specials/SpecialUnusedcategories.php
@@ -39,9 +39,9 @@ class UnusedCategoriesPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'page', 'categorylinks' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_title AS value' ),
+ 'fields' => array ( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_title' ),
'conds' => array ( 'cl_from IS NULL',
'page_namespace' => NS_CATEGORY,
'page_is_redirect' => 0 ),
@@ -52,6 +52,7 @@ class UnusedCategoriesPage extends QueryPage {
/**
* A should come before Z (bug 30907)
+ * @return bool
*/
function sortDescending() {
return false;
diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php
index 6407de44..cdab557e 100644
--- a/includes/specials/SpecialUnusedimages.php
+++ b/includes/specials/SpecialUnusedimages.php
@@ -47,9 +47,9 @@ class UnusedimagesPage extends ImageQueryPage {
global $wgCountCategorizedImagesAsUsed;
$retval = array (
'tables' => array ( 'image', 'imagelinks' ),
- 'fields' => array ( "'" . NS_FILE . "' AS namespace",
- 'img_name AS title',
- 'img_timestamp AS value',
+ 'fields' => array ( 'namespace' => NS_FILE,
+ 'title' => 'img_name',
+ 'value' => 'img_timestamp',
'img_user', 'img_user_text',
'img_description' ),
'conds' => array ( 'il_to IS NULL' ),
@@ -77,7 +77,7 @@ class UnusedimagesPage extends ImageQueryPage {
}
function getPageHeader() {
- return wfMsgExt( 'unusedimagestext', array( 'parse' ) );
+ return $this->msg( 'unusedimagestext' )->parseAsBlock();
}
}
diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php
index e5c55b83..06077d1f 100644
--- a/includes/specials/SpecialUnusedtemplates.php
+++ b/includes/specials/SpecialUnusedtemplates.php
@@ -42,9 +42,9 @@ class UnusedtemplatesPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'page', 'templatelinks' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_title AS value' ),
+ 'fields' => array ( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_title' ),
'conds' => array ( 'page_namespace' => NS_TEMPLATE,
'tl_from IS NULL',
'page_is_redirect' => 0 ),
@@ -68,17 +68,13 @@ class UnusedtemplatesPage extends QueryPage {
array( 'redirect' => 'no' )
);
$wlhLink = Linker::linkKnown(
- SpecialPage::getTitleFor( 'Whatlinkshere' ),
- wfMsgHtml( 'unusedtemplateswlh' ),
- array(),
- array( 'target' => $title->getPrefixedText() )
+ SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ),
+ $this->msg( 'unusedtemplateswlh' )->escaped()
);
return $this->getLanguage()->specialList( $pageLink, $wlhLink );
}
function getPageHeader() {
- return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) );
+ return $this->msg( 'unusedtemplatestext' )->parseAsBlock();
}
-
}
-
diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php
index 22c64858..e5a79413 100644
--- a/includes/specials/SpecialUnwatchedpages.php
+++ b/includes/specials/SpecialUnwatchedpages.php
@@ -41,9 +41,9 @@ class UnwatchedpagesPage extends QueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'page', 'watchlist' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_namespace AS value' ),
+ 'fields' => array ( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_namespace' ),
'conds' => array ( 'wl_title IS NULL',
'page_is_redirect' => 0,
"page_namespace != '" . NS_MEDIAWIKI .
@@ -68,17 +68,19 @@ class UnwatchedpagesPage extends QueryPage {
function formatResult( $skin, $result ) {
global $wgContLang;
- $nt = Title::makeTitle( $result->namespace, $result->title );
+ $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 ) );
+ }
+
$text = $wgContLang->convert( $nt->getPrefixedText() );
- $plink = Linker::linkKnown(
- $nt,
- htmlspecialchars( $text )
- );
+ $plink = Linker::linkKnown( $nt, htmlspecialchars( $text ) );
$token = WatchAction::getWatchToken( $nt, $this->getUser() );
$wlink = Linker::linkKnown(
$nt,
- wfMsgHtml( 'watch' ),
+ $this->msg( 'watch' )->escaped(),
array(),
array( 'action' => 'watch', 'token' => $token )
);
diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php
index d6a76d02..43ea345b 100644
--- a/includes/specials/SpecialUpload.php
+++ b/includes/specials/SpecialUpload.php
@@ -150,7 +150,7 @@ class SpecialUpload extends SpecialPage {
# Check blocks
if( $user->isBlocked() ) {
- throw new UserBlockedError( $user->mBlock );
+ throw new UserBlockedError( $user->getBlock() );
}
# Check whether we actually want to allow changing stuff
@@ -235,7 +235,7 @@ class SpecialUpload extends SpecialPage {
!$this->mTokenOk && !$this->mCancelUpload &&
( $this->mUpload && $this->mUploadClicked )
) {
- $form->addPreText( wfMsgExt( 'session_fail_preview', 'parseinline' ) );
+ $form->addPreText( $this->msg( 'session_fail_preview' )->parse() );
}
# Give a notice if the user is uploading a file that has been deleted or moved
@@ -255,16 +255,16 @@ class SpecialUpload extends SpecialPage {
# Add text to form
$form->addPreText( '<div id="uploadtext">' .
- wfMsgExt( 'uploadtext', 'parse', array( $this->mDesiredDestName ) ) .
+ $this->msg( 'uploadtext', array( $this->mDesiredDestName ) )->parseAsBlock() .
'</div>' );
# Add upload error message
$form->addPreText( $message );
# Add footer to form
- $uploadFooter = wfMessage( 'uploadfooter' );
+ $uploadFooter = $this->msg( 'uploadfooter' );
if ( !$uploadFooter->isDisabled() ) {
$form->addPostText( '<div id="mw-upload-footer-message">'
- . $this->getOutput()->parse( $uploadFooter->plain() ) . "</div>\n" );
+ . $uploadFooter->parseAsBlock() . "</div>\n" );
}
return $form;
@@ -280,14 +280,12 @@ class SpecialUpload extends SpecialPage {
if( $title instanceof Title ) {
$count = $title->isDeleted();
if ( $count > 0 && $user->isAllowed( 'deletedhistory' ) ) {
- $link = wfMsgExt(
- $user->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
- array( 'parse', 'replaceafter' ),
- Linker::linkKnown(
- SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
- wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count )
- )
+ $restorelink = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
+ $this->msg( 'restorelink' )->numParams( $count )->escaped()
);
+ $link = $this->msg( $user->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted' )
+ ->rawParams( $restorelink )->parseAsBlock();
$this->getOutput()->addHTML( "<div id=\"contentSub2\">{$link}</div>" );
}
}
@@ -306,11 +304,11 @@ class SpecialUpload extends SpecialPage {
*/
protected function showRecoverableUploadError( $message ) {
$sessionKey = $this->mUpload->stashSession();
- $message = '<h2>' . wfMsgHtml( 'uploaderror' ) . "</h2>\n" .
+ $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . "</h2>\n" .
'<div class="error">' . $message . "</div>\n";
$form = $this->getUploadForm( $message, $sessionKey );
- $form->setSubmitText( wfMsg( 'upload-tryagain' ) );
+ $form->setSubmitText( $this->msg( 'upload-tryagain' )->escaped() );
$this->showUploadForm( $form );
}
/**
@@ -335,7 +333,7 @@ class SpecialUpload extends SpecialPage {
$sessionKey = $this->mUpload->stashSession();
- $warningHtml = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n"
+ $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n"
. '<ul class="warning">';
foreach( $warnings as $warning => $args ) {
if( $warning == 'exists' ) {
@@ -343,8 +341,8 @@ class SpecialUpload extends SpecialPage {
} elseif( $warning == 'duplicate' ) {
$msg = self::getDupeWarning( $args );
} elseif( $warning == 'duplicate-archive' ) {
- $msg = "\t<li>" . wfMsgExt( 'file-deleted-duplicate', 'parseinline',
- array( Title::makeTitle( NS_FILE, $args )->getPrefixedText() ) )
+ $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate',
+ Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse()
. "</li>\n";
} else {
if ( $args === true ) {
@@ -352,17 +350,17 @@ class SpecialUpload extends SpecialPage {
} elseif ( !is_array( $args ) ) {
$args = array( $args );
}
- $msg = "\t<li>" . wfMsgExt( $warning, 'parseinline', $args ) . "</li>\n";
+ $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n";
}
$warningHtml .= $msg;
}
$warningHtml .= "</ul>\n";
- $warningHtml .= wfMsgExt( 'uploadwarning-text', 'parse' );
+ $warningHtml .= $this->msg( 'uploadwarning-text' )->parseAsBlock();
$form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true );
- $form->setSubmitText( wfMsg( 'upload-tryagain' ) );
- $form->addButton( 'wpUploadIgnoreWarning', wfMsg( 'ignorewarning' ) );
- $form->addButton( 'wpCancelUpload', wfMsg( 'reuploaddesc' ) );
+ $form->setSubmitText( $this->msg( 'upload-tryagain' )->text() );
+ $form->addButton( 'wpUploadIgnoreWarning', $this->msg( 'ignorewarning' )->text() );
+ $form->addButton( 'wpCancelUpload', $this->msg( 'reuploaddesc' )->text() );
$this->showUploadForm( $form );
@@ -376,7 +374,7 @@ class SpecialUpload extends SpecialPage {
* @param $message string HTML string
*/
protected function showUploadError( $message ) {
- $message = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" .
+ $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" .
'<div class="error">' . $message . "</div>\n";
$this->showUploadForm( $this->getUploadForm( $message ) );
}
@@ -414,8 +412,7 @@ class SpecialUpload extends SpecialPage {
$permErrors = $this->mUpload->verifyTitlePermissions( $this->getUser() );
if( $permErrors !== true ) {
$code = array_shift( $permErrors[0] );
- $this->showRecoverableUploadError( wfMsgExt( $code,
- 'parseinline', $permErrors[0] ) );
+ $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() );
return;
}
@@ -469,7 +466,7 @@ class SpecialUpload extends SpecialPage {
if ( in_array( $msgName, $wgForceUIMsgAsContentMsg ) ) {
$msg[$msgName] = "{{int:$msgName}}";
} else {
- $msg[$msgName] = wfMsgForContent( $msgName );
+ $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
}
}
@@ -516,7 +513,7 @@ class SpecialUpload extends SpecialPage {
if( $local && $local->exists() ) {
// We're uploading a new version of an existing file.
// No creation, so don't watch it if we're not already.
- return $local->getTitle()->userIsWatching();
+ return $this->getUser()->isWatched( $local->getTitle() );
} else {
// New page should get watched if that's our option.
return $this->getUser()->getOption( 'watchcreations' );
@@ -536,33 +533,31 @@ class SpecialUpload extends SpecialPage {
/** Statuses that only require name changing **/
case UploadBase::MIN_LENGTH_PARTNAME:
- $this->showRecoverableUploadError( wfMsgHtml( 'minlength1' ) );
+ $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() );
break;
case UploadBase::ILLEGAL_FILENAME:
- $this->showRecoverableUploadError( wfMsgExt( 'illegalfilename',
- 'parseinline', $details['filtered'] ) );
+ $this->showRecoverableUploadError( $this->msg( 'illegalfilename',
+ $details['filtered'] )->parse() );
break;
case UploadBase::FILENAME_TOO_LONG:
- $this->showRecoverableUploadError( wfMsgHtml( 'filename-toolong' ) );
+ $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() );
break;
case UploadBase::FILETYPE_MISSING:
- $this->showRecoverableUploadError( wfMsgExt( 'filetype-missing',
- 'parseinline' ) );
+ $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() );
break;
case UploadBase::WINDOWS_NONASCII_FILENAME:
- $this->showRecoverableUploadError( wfMsgExt( 'windows-nonascii-filename',
- 'parseinline' ) );
+ $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() );
break;
/** Statuses that require reuploading **/
case UploadBase::EMPTY_FILE:
- $this->showUploadError( wfMsgHtml( 'emptyfile' ) );
+ $this->showUploadError( $this->msg( 'emptyfile' )->escaped() );
break;
case UploadBase::FILE_TOO_LARGE:
- $this->showUploadError( wfMsgHtml( 'largefileserver' ) );
+ $this->showUploadError( $this->msg( 'largefileserver' )->escaped() );
break;
case UploadBase::FILETYPE_BADTYPE:
- $msg = wfMessage( 'filetype-banned-type' );
+ $msg = $this->msg( 'filetype-banned-type' );
if ( isset( $details['blacklistedExt'] ) ) {
$msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) );
} else {
@@ -585,7 +580,7 @@ class SpecialUpload extends SpecialPage {
case UploadBase::VERIFICATION_ERROR:
unset( $details['status'] );
$code = array_shift( $details['details'] );
- $this->showUploadError( wfMsgExt( $code, 'parseinline', $details['details'] ) );
+ $this->showUploadError( $this->msg( $code, $details['details'] )->parse() );
break;
case UploadBase::HOOK_ABORTED:
if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array
@@ -596,7 +591,7 @@ class SpecialUpload extends SpecialPage {
$args = null;
}
- $this->showUploadError( wfMsgExt( $error, 'parseinline', $args ) );
+ $this->showUploadError( $this->msg( $error, $args )->parse() );
break;
default:
throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" );
@@ -641,37 +636,37 @@ class SpecialUpload extends SpecialPage {
if( $exists['warning'] == 'exists' ) {
// Exact match
- $warning = wfMsgExt( 'fileexists', 'parseinline', $filename );
+ $warning = wfMessage( 'fileexists', $filename )->parse();
} elseif( $exists['warning'] == 'page-exists' ) {
// Page exists but file does not
- $warning = wfMsgExt( 'filepageexists', 'parseinline', $filename );
+ $warning = wfMessage( 'filepageexists', $filename )->parse();
} elseif ( $exists['warning'] == 'exists-normalized' ) {
- $warning = wfMsgExt( 'fileexists-extension', 'parseinline', $filename,
- $exists['normalizedFile']->getTitle()->getPrefixedText() );
+ $warning = wfMessage( 'fileexists-extension', $filename,
+ $exists['normalizedFile']->getTitle()->getPrefixedText() )->parse();
} elseif ( $exists['warning'] == 'thumb' ) {
// Swapped argument order compared with other messages for backwards compatibility
- $warning = wfMsgExt( 'fileexists-thumbnail-yes', 'parseinline',
- $exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
+ $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
$name = $file->getName();
$badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
- $warning = wfMsgExt( 'file-thumbnail-no', 'parseinline', $badPart );
+ $warning = wfMessage( 'file-thumbnail-no', $badPart )->parse();
} elseif ( $exists['warning'] == 'bad-prefix' ) {
- $warning = wfMsgExt( 'filename-bad-prefix', 'parseinline', $exists['prefix'] );
+ $warning = wfMessage( 'filename-bad-prefix', $exists['prefix'] )->parse();
} elseif ( $exists['warning'] == 'was-deleted' ) {
# If the file existed before and was deleted, warn the user of this
$ltitle = SpecialPage::getTitleFor( 'Log' );
$llink = Linker::linkKnown(
$ltitle,
- wfMsgHtml( 'deletionlog' ),
+ wfMessage( 'deletionlog' )->escaped(),
array(),
array(
'type' => 'delete',
'page' => $filename
)
);
- $warning = wfMsgExt( 'filewasdeleted', array( 'parse', 'replaceafter' ), $llink );
+ $warning = wfMessage( 'filewasdeleted' )->rawParams( $llink )->parseAsBlock();
}
return $warning;
@@ -707,22 +702,18 @@ class SpecialUpload extends SpecialPage {
* @return string
*/
public static function getDupeWarning( $dupes ) {
- global $wgOut;
- if( $dupes ) {
- $msg = '<gallery>';
- foreach( $dupes as $file ) {
- $title = $file->getTitle();
- $msg .= $title->getPrefixedText() .
- '|' . $title->getText() . "\n";
- }
- $msg .= '</gallery>';
- return '<li>' .
- wfMsgExt( 'file-exists-duplicate', array( 'parse' ), count( $dupes ) ) .
- $wgOut->parse( $msg ) .
- "</li>\n";
- } else {
+ if ( !$dupes ) {
return '';
}
+
+ $gallery = new ImageGallery;
+ $gallery->setShowBytes( false );
+ foreach( $dupes as $file ) {
+ $gallery->add( $file->getTitle() );
+ }
+ return '<li>' .
+ wfMessage( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() .
+ $gallery->toHtml() . "</li>\n";
}
}
@@ -775,7 +766,7 @@ class UploadForm extends HTMLForm {
parent::__construct( $descriptor, $context, 'upload' );
# Set some form properties
- $this->setSubmitText( wfMsg( 'uploadbtn' ) );
+ $this->setSubmitText( $this->msg( 'uploadbtn' )->text() );
$this->setSubmitName( 'wpUpload' );
# Used message keys: 'accesskey-upload', 'tooltip-upload'
$this->setSubmitTooltip( 'upload' );
@@ -830,7 +821,9 @@ class UploadForm extends HTMLForm {
# that setting doesn't exist
if ( !wfIsHipHop() ) {
$this->mMaxUploadSize['file'] = min( $this->mMaxUploadSize['file'],
- wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ) );
+ wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ),
+ wfShorthandToInteger( ini_get( 'post_max_size' ) )
+ );
}
$descriptor['UploadFile'] = array(
@@ -841,10 +834,9 @@ class UploadForm extends HTMLForm {
'label-message' => 'sourcefilename',
'upload-type' => 'File',
'radio' => &$radio,
- 'help' => wfMsgExt( 'upload-maxfilesize',
- array( 'parseinline', 'escapenoentities' ),
+ 'help' => $this->msg( 'upload-maxfilesize',
$this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] )
- ) . ' ' . wfMsgHtml( 'upload_source_file' ),
+ )->parse() . ' ' . $this->msg( 'upload_source_file' )->escaped(),
'checked' => $selectedSourceType == 'file',
);
if ( $canUploadByUrl ) {
@@ -856,10 +848,9 @@ class UploadForm extends HTMLForm {
'label-message' => 'sourceurl',
'upload-type' => 'url',
'radio' => &$radio,
- 'help' => wfMsgExt( 'upload-maxfilesize',
- array( 'parseinline', 'escapenoentities' ),
+ 'help' => $this->msg( 'upload-maxfilesize',
$this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] )
- ) . ' ' . wfMsgHtml( 'upload_source_url' ),
+ )->parse() . ' ' . $this->msg( 'upload_source_url' )->escaped(),
'checked' => $selectedSourceType == 'url',
);
}
@@ -890,16 +881,16 @@ class UploadForm extends HTMLForm {
# Everything not permitted is banned
$extensionsList =
'<div id="mw-upload-permitted">' .
- wfMsgExt( 'upload-permitted', 'parse', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) ) .
+ $this->msg( 'upload-permitted', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) )->parseAsBlock() .
"</div>\n";
} else {
# We have to list both preferred and prohibited
$extensionsList =
'<div id="mw-upload-preferred">' .
- wfMsgExt( 'upload-preferred', 'parse', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) ) .
+ $this->msg( 'upload-preferred', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) )->parseAsBlock() .
"</div>\n" .
'<div id="mw-upload-prohibited">' .
- wfMsgExt( 'upload-prohibited', 'parse', $this->getContext()->getLanguage()->commaList( $wgFileBlacklist ) ) .
+ $this->msg( 'upload-prohibited', $this->getContext()->getLanguage()->commaList( $wgFileBlacklist ) )->parseAsBlock() .
"</div>\n";
}
} else {
diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php
index 121b6a44..1a00d731 100644
--- a/includes/specials/SpecialUploadStash.php
+++ b/includes/specials/SpecialUploadStash.php
@@ -1,7 +1,28 @@
<?php
/**
- * Implements Special:UploadStash
+ * Implements Special:UploadStash.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @ingroup Upload
+ */
+
+/**
* Web access for files temporarily stored by UploadStash.
*
* For example -- files that were uploaded with the UploadWizard extension are stored temporarily
@@ -10,12 +31,7 @@
*
* Since this is based on the user's session, in effect this creates a private temporary file area.
* However, the URLs for the files cannot be shared.
- *
- * @file
- * @ingroup SpecialPage
- * @ingroup Upload
*/
-
class SpecialUploadStash extends UnlistedSpecialPage {
// UploadStash
private $stash;
@@ -58,6 +74,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
* n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward.
*
* @param $key String: the key of a particular requested file
+ * @return bool
*/
public function showUpload( $key ) {
// prevent callers from doing standard HTML output -- we'll take it from here
@@ -241,6 +258,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!)
+ * @return bool
*/
private function outputLocalFile( File $file ) {
if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
@@ -255,8 +273,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
/**
* Output HTTP response of raw content
* Side effect: writes HTTP response to STDOUT.
- * @param String $content: content
- * @param String $mimeType: mime type
+ * @param $content String content
+ * @param $contentType String mime type
+ * @return bool
*/
private function outputContents( $content, $contentType ) {
$size = strlen( $content );
@@ -303,7 +322,8 @@ 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 : $status - the result of processRequest
+ * @param $status [optional] Status: the result of processRequest
+ * @return bool
*/
private function showUploads( $status = null ) {
if ( $status === null ) {
@@ -326,7 +346,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
), $this->getContext(), 'clearStashedUploads' );
$form->setSubmitCallback( array( __CLASS__ , 'tryClearStashedUploads' ) );
$form->setTitle( $this->getTitle() );
- $form->setSubmitText( wfMsg( 'uploadstash-clear' ) );
+ $form->setSubmitTextMsg( 'uploadstash-clear' );
$form->prepareForm();
$formResult = $form->tryAuthorizedSubmit();
@@ -334,7 +354,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
// show the files + form, if there are any, or just say there are none
$refreshHtml = Html::element( 'a',
array( 'href' => $this->getTitle()->getLocalURL() ),
- wfMsg( 'uploadstash-refresh' ) );
+ $this->msg( 'uploadstash-refresh' )->text() );
$files = $this->stash->listFiles();
if ( $files && count( $files ) ) {
sort( $files );
@@ -351,7 +371,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
$this->getOutput()->addHtml( Html::rawElement( 'p', array(), $refreshHtml ) );
} else {
$this->getOutput()->addHtml( Html::rawElement( 'p', array(),
- Html::element( 'span', array(), wfMsg( 'uploadstash-nofiles' ) )
+ Html::element( 'span', array(), $this->msg( 'uploadstash-nofiles' )->text() )
. ' '
. $refreshHtml
) );
diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php
index 4c5a2376..58da77da 100644
--- a/includes/specials/SpecialUserlogin.php
+++ b/includes/specials/SpecialUserlogin.php
@@ -74,7 +74,7 @@ class LoginForm extends SpecialPage {
* Loader
*/
function load() {
- global $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin;
+ global $wgAuth, $wgHiddenPrefs, $wgEnableEmail;
if ( $this->mLoaded ) {
return;
@@ -93,8 +93,6 @@ class LoginForm extends SpecialPage {
$this->mRetype = $request->getText( 'wpRetype' );
$this->mDomain = $request->getText( 'wpDomain' );
$this->mReason = $request->getText( 'wpReason' );
- $this->mReturnTo = $request->getVal( 'returnto' );
- $this->mReturnToQuery = $request->getVal( 'returntoquery' );
$this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
$this->mPosted = $request->wasPosted();
$this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
@@ -107,11 +105,8 @@ class LoginForm extends SpecialPage {
$this->mLanguage = $request->getText( 'uselang' );
$this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
$this->mToken = ( $this->mType == 'signup' ) ? $request->getVal( 'wpCreateaccountToken' ) : $request->getVal( 'wpLoginToken' );
-
- if ( $wgRedirectOnLogin ) {
- $this->mReturnTo = $wgRedirectOnLogin;
- $this->mReturnToQuery = '';
- }
+ $this->mReturnTo = $request->getVal( 'returnto', '' );
+ $this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
if( $wgEnableEmail ) {
$this->mEmail = $request->getText( 'wpEmail' );
@@ -125,17 +120,17 @@ class LoginForm extends SpecialPage {
}
if( !$wgAuth->validDomain( $this->mDomain ) ) {
- if ( isset( $_SESSION['wsDomain'] ) ) {
- $this->mDomain = $_SESSION['wsDomain'];
- } else {
- $this->mDomain = 'invaliddomain';
- }
+ $this->mDomain = $wgAuth->getDomain();
}
$wgAuth->setDomain( $this->mDomain );
- # When switching accounts, it sucks to get automatically logged out
+ # 1. When switching accounts, it sucks to get automatically logged out
+ # 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 ) && $returnToTitle->isSpecial( 'Userlogout' ) ) {
+ if( is_object( $returnToTitle ) && (
+ $returnToTitle->isSpecial( 'Userlogout' )
+ || $returnToTitle->isSpecial( 'PasswordReset' ) ) ) {
$this->mReturnTo = '';
$this->mReturnToQuery = '';
}
@@ -163,11 +158,14 @@ class LoginForm extends SpecialPage {
return;
} elseif( $this->mPosted ) {
if( $this->mCreateaccount ) {
- return $this->addNewAccount();
+ $this->addNewAccount();
+ return;
} elseif ( $this->mCreateaccountMail ) {
- return $this->addNewAccountMailPassword();
+ $this->addNewAccountMailPassword();
+ return;
} elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
- return $this->processLogin();
+ $this->processLogin();
+ return;
}
}
$this->mainLoginForm( '' );
@@ -203,12 +201,13 @@ class LoginForm extends SpecialPage {
$this->mainLoginForm( $this->msg( 'mailerror', $result->getWikiText() )->text() );
} else {
$out->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
- $out->returnToMain( false );
+ $this->executeReturnTo( 'success' );
}
}
/**
* @private
+ * @return bool
*/
function addNewAccount() {
global $wgUser, $wgEmailAuthentication, $wgLoginLanguageSelector;
@@ -216,7 +215,7 @@ class LoginForm extends SpecialPage {
# Create the account and abort if there's a problem doing so
$u = $this->addNewAccountInternal();
if( $u == null ) {
- return;
+ return false;
}
# If we showed up language selection links, and one was in use, be
@@ -253,23 +252,24 @@ class LoginForm extends SpecialPage {
wfRunHooks( 'AddNewAccount', array( $u, false ) );
$u->addNewUserLogEntry();
if( $this->hasSessionCookie() ) {
- return $this->successfulCreation();
+ $this->successfulCreation();
} else {
- return $this->cookieRedirectCheck( 'new' );
+ $this->cookieRedirectCheck( 'new' );
}
} else {
# Confirm that the account was created
$out->setPageTitle( $this->msg( 'accountcreated' ) );
$out->addWikiMsg( 'accountcreatedtext', $u->getName() );
- $out->returnToMain( false, $this->getTitle() );
+ $out->addReturnTo( $this->getTitle() );
wfRunHooks( 'AddNewAccount', array( $u, false ) );
$u->addNewUserLogEntry( false, $this->mReason );
- return true;
}
+ return true;
}
/**
* @private
+ * @return bool|User
*/
function addNewAccountInternal() {
global $wgAuth, $wgMemc, $wgAccountCreationThrottle,
@@ -334,7 +334,7 @@ class LoginForm extends SpecialPage {
$ip = $this->getRequest()->getIP();
if ( $currentUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
- $this->mainLoginForm( $this->msg( 'sorbs_create_account_reason' )->text() . ' (' . htmlspecialchars( $ip ) . ')' );
+ $this->mainLoginForm( $this->msg( 'sorbs_create_account_reason' )->text() . ' ' . $this->msg( 'parentheses', $ip )->escaped() );
return false;
}
@@ -476,6 +476,7 @@ class LoginForm extends SpecialPage {
* This may create a local account as a side effect if the
* authentication plugin allows transparent local account
* creation.
+ * @return int
*/
public function authenticateUserData() {
global $wgUser, $wgAuth;
@@ -747,9 +748,9 @@ class LoginForm extends SpecialPage {
$this->getContext()->setLanguage( $userLang );
// Reset SessionID on Successful login (bug 40995)
$this->renewSessionId();
- return $this->successfulLogin();
+ $this->successfulLogin();
} else {
- return $this->cookieRedirectCheck( 'login' );
+ $this->cookieRedirectCheck( 'login' );
}
break;
@@ -769,7 +770,7 @@ class LoginForm extends SpecialPage {
case self::NOT_EXISTS:
if( $this->getUser()->isAllowed( 'createaccount' ) ) {
$this->mainLoginForm( $this->msg( 'nosuchuser',
- wfEscapeWikiText( $this->mUsername ) )->parse() );
+ wfEscapeWikiText( $this->mUsername ) )->parse() );
} else {
$this->mainLoginForm( $this->msg( 'nosuchusershort',
wfEscapeWikiText( $this->mUsername ) )->text() );
@@ -861,16 +862,7 @@ class LoginForm extends SpecialPage {
if( $injected_html !== '' ) {
$this->displaySuccessfulLogin( 'loginsuccess', $injected_html );
} else {
- $titleObj = Title::newFromText( $this->mReturnTo );
- if ( !$titleObj instanceof Title ) {
- $titleObj = Title::newMainPage();
- }
- $redirectUrl = $titleObj->getFullURL( $this->mReturnToQuery );
- global $wgSecureLogin;
- if( $wgSecureLogin && !$this->mStickHTTPS ) {
- $redirectUrl = preg_replace( '/^https:/', 'http:', $redirectUrl );
- }
- $this->getOutput()->redirect( $redirectUrl );
+ $this->executeReturnTo( 'successredirect' );
}
}
@@ -900,6 +892,8 @@ class LoginForm extends SpecialPage {
/**
* Display a "login successful" page.
+ * @param $msgname string
+ * @param $injected_html string
*/
private function displaySuccessfulLogin( $msgname, $injected_html ) {
$out = $this->getOutput();
@@ -910,11 +904,7 @@ class LoginForm extends SpecialPage {
$out->addHTML( $injected_html );
- if ( !empty( $this->mReturnTo ) ) {
- $out->returnToMain( null, $this->mReturnTo, $this->mReturnToQuery );
- } else {
- $out->returnToMain( null );
- }
+ $this->executeReturnTo( 'success' );
}
/**
@@ -948,7 +938,42 @@ class LoginForm extends SpecialPage {
$block->getByName()
);
- $out->returnToMain( false );
+ $this->executeReturnTo( 'error' );
+ }
+
+ /**
+ * Add a "return to" link or redirect to it.
+ *
+ * @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
+ */
+ private function executeReturnTo( $type ) {
+ global $wgRedirectOnLogin, $wgSecureLogin;
+
+ if ( $type != 'error' && $wgRedirectOnLogin !== null ) {
+ $returnTo = $wgRedirectOnLogin;
+ $returnToQuery = array();
+ } else {
+ $returnTo = $this->mReturnTo;
+ $returnToQuery = wfCgiToArray( $this->mReturnToQuery );
+ }
+
+ $returnToTitle = Title::newFromText( $returnTo );
+ if ( !$returnToTitle ) {
+ $returnToTitle = Title::newMainPage();
+ }
+
+ if ( $type == 'successredirect' ) {
+ $redirectUrl = $returnToTitle->getFullURL( $returnToQuery );
+ if( $wgSecureLogin && !$this->mStickHTTPS ) {
+ $redirectUrl = preg_replace( '/^https:/', 'http:', $redirectUrl );
+ }
+ $this->getOutput()->redirect( $redirectUrl );
+ } else {
+ $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery );
+ }
}
/**
@@ -998,9 +1023,9 @@ class LoginForm extends SpecialPage {
$linkmsg = 'nologin';
}
- if ( !empty( $this->mReturnTo ) ) {
+ if ( $this->mReturnTo !== '' ) {
$returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
- if ( !empty( $this->mReturnToQuery ) ) {
+ if ( $this->mReturnToQuery !== '' ) {
$returnto .= '&returntoquery=' .
wfUrlencode( $this->mReturnToQuery );
}
@@ -1125,6 +1150,7 @@ class LoginForm extends SpecialPage {
* previous pass through the system.
*
* @private
+ * @return bool
*/
function hasSessionCookie() {
global $wgDisableCookieCheck;
@@ -1133,6 +1159,7 @@ class LoginForm extends SpecialPage {
/**
* Get the login token from the current session
+ * @return Mixed
*/
public static function getLoginToken() {
global $wgRequest;
@@ -1159,6 +1186,7 @@ class LoginForm extends SpecialPage {
/**
* Get the createaccount token from the current session
+ * @return Mixed
*/
public static function getCreateaccountToken() {
global $wgRequest;
@@ -1181,7 +1209,7 @@ class LoginForm extends SpecialPage {
$wgRequest->setSessionData( 'wsCreateaccountToken', null );
}
- /**
+ /**
* Renew the user's session id, using strong entropy
*/
private function renewSessionId() {
@@ -1204,12 +1232,13 @@ class LoginForm extends SpecialPage {
function cookieRedirectCheck( $type ) {
$titleObj = SpecialPage::getTitleFor( 'Userlogin' );
$query = array( 'wpCookieCheck' => $type );
- if ( $this->mReturnTo ) {
+ if ( $this->mReturnTo !== '' ) {
$query['returnto'] = $this->mReturnTo;
+ $query['returntoquery'] = $this->mReturnToQuery;
}
$check = $titleObj->getFullURL( $query );
- return $this->getOutput()->redirect( $check );
+ $this->getOutput()->redirect( $check );
}
/**
@@ -1218,15 +1247,15 @@ class LoginForm extends SpecialPage {
function onCookieRedirectCheck( $type ) {
if ( !$this->hasSessionCookie() ) {
if ( $type == 'new' ) {
- return $this->mainLoginForm( $this->msg( 'nocookiesnew' )->parse() );
+ $this->mainLoginForm( $this->msg( 'nocookiesnew' )->parse() );
} elseif ( $type == 'login' ) {
- return $this->mainLoginForm( $this->msg( 'nocookieslogin' )->parse() );
+ $this->mainLoginForm( $this->msg( 'nocookieslogin' )->parse() );
} else {
# shouldn't happen
- return $this->mainLoginForm( $this->msg( 'error' )->text() );
+ $this->mainLoginForm( $this->msg( 'error' )->text() );
}
} else {
- return $this->successfulLogin();
+ $this->successfulLogin();
}
}
@@ -1268,20 +1297,31 @@ class LoginForm extends SpecialPage {
*
* @param $text Link text
* @param $lang Language code
+ * @return string
*/
function makeLanguageSelectorLink( $text, $lang ) {
- $attr = array( 'uselang' => $lang );
+ if( $this->getLanguage()->getCode() == $lang ) {
+ // no link for currently used language
+ return htmlspecialchars( $text );
+ }
+ $query = array( 'uselang' => $lang );
if( $this->mType == 'signup' ) {
- $attr['type'] = 'signup';
+ $query['type'] = 'signup';
}
- if( $this->mReturnTo ) {
- $attr['returnto'] = $this->mReturnTo;
+ if( $this->mReturnTo !== '' ) {
+ $query['returnto'] = $this->mReturnTo;
+ $query['returntoquery'] = $this->mReturnToQuery;
}
+
+ $attr = array();
+ $targetLanguage = Language::factory( $lang );
+ $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
+
return Linker::linkKnown(
$this->getTitle(),
htmlspecialchars( $text ),
- array(),
- $attr
+ $attr,
+ $query
);
}
}
diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php
index d747448f..ab2bf0ac 100644
--- a/includes/specials/SpecialUserlogout.php
+++ b/includes/specials/SpecialUserlogout.php
@@ -39,7 +39,7 @@ class SpecialUserlogout extends UnlistedSpecialPage {
*/
if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&amp;' ) !== false ) {
wfDebug( "Special:Userlogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" );
- throw new HttpError( 400, wfMessage( 'suspicious-userlogout' ), wfMessage( 'loginerror' ) );
+ throw new HttpError( 400, $this->msg( 'suspicious-userlogout' ), $this->msg( 'loginerror' ) );
}
$this->setHeaders();
diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php
index e2e0f38b..9f5a48a5 100644
--- a/includes/specials/SpecialUserrights.php
+++ b/includes/specials/SpecialUserrights.php
@@ -47,6 +47,9 @@ class UserrightsPage extends SpecialPage {
public function userCanChangeRights( $user, $checkIfSelf = true ) {
$available = $this->changeableGroups();
+ if ( $user->getId() == 0 ) {
+ return false;
+ }
return !empty( $available['add'] )
|| !empty( $available['remove'] )
|| ( ( $this->isself || !$checkIfSelf ) &&
@@ -72,7 +75,7 @@ class UserrightsPage extends SpecialPage {
* allow them to use Special:UserRights.
*/
if( $user->isBlocked() && !$user->isAllowed( 'userrights' ) ) {
- throw new UserBlockedError( $user->mBlock );
+ throw new UserBlockedError( $user->getBlock() );
}
$request = $this->getRequest();
@@ -345,7 +348,7 @@ class UserrightsPage extends SpecialPage {
function makeGroupNameList( $ids ) {
if( empty( $ids ) ) {
- return wfMsgForContent( 'rightsnone' );
+ return $this->msg( 'rightsnone' )->inContentLanguage()->text();
} else {
return implode( ', ', $ids );
}
@@ -367,9 +370,9 @@ class UserrightsPage extends SpecialPage {
$this->getOutput()->addHTML(
Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) .
Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
- Xml::fieldset( wfMsg( 'userrights-lookup-user' ) ) .
- Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ) ) . ' ' .
- Xml::submitButton( wfMsg( 'editusergroup' ) ) .
+ Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
+ Xml::inputLabel( $this->msg( 'userrights-user-editname' )->text(), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ) ) . ' ' .
+ Xml::submitButton( $this->msg( 'editusergroup' )->text() ) .
Html::closeElement( 'fieldset' ) .
Html::closeElement( 'form' ) . "\n"
);
@@ -420,12 +423,12 @@ class UserrightsPage extends SpecialPage {
$grouplist = '';
$count = count( $list );
if( $count > 0 ) {
- $grouplist = wfMessage( 'userrights-groupsmember', $count)->parse();
+ $grouplist = $this->msg( 'userrights-groupsmember', $count, $user->getName() )->parse();
$grouplist = '<p>' . $grouplist . ' ' . $this->getLanguage()->listToText( $list ) . "</p>\n";
}
$count = count( $autolist );
if( $count > 0 ) {
- $autogrouplistintro = wfMessage( 'userrights-groupsmember-auto', $count)->parse();
+ $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto', $count, $user->getName() )->parse();
$grouplist .= '<p>' . $autogrouplistintro . ' ' . $this->getLanguage()->listToText( $autolist ) . "</p>\n";
}
@@ -441,15 +444,15 @@ class UserrightsPage extends SpecialPage {
Html::hidden( 'user', $this->mTarget ) .
Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', array(), wfMessage( 'userrights-editusergroup', $user->getName() )->text() ) .
- wfMessage( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )->rawParams( $userToolLinks )->parse() .
- wfMessage( 'userrights-groups-help', $user->getName() )->parse() .
+ Xml::element( 'legend', array(), $this->msg( 'userrights-editusergroup', $user->getName() )->text() ) .
+ $this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )->rawParams( $userToolLinks )->parse() .
+ $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
$grouplist .
Xml::tags( 'p', null, $this->groupCheckboxes( $groups, $user ) ) .
- Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-userrights-table-outer' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-userrights-table-outer' ) ) .
"<tr>
<td class='mw-label'>" .
- Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
+ Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
"</td>
<td class='mw-input'>" .
Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ),
@@ -459,7 +462,7 @@ class UserrightsPage extends SpecialPage {
<tr>
<td></td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'saveusergroups' ),
+ Xml::submitButton( $this->msg( 'saveusergroups' )->text(),
array( 'name' => 'saveusergroups' ) + Linker::tooltipAndAccesskeyAttribs( 'userrights-set' ) ) .
"</td>
</tr>" .
@@ -531,12 +534,12 @@ class UserrightsPage extends SpecialPage {
}
# Build the HTML table
- $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) .
+ $ret .= Xml::openElement( 'table', array( 'class' => 'mw-userrights-groups' ) ) .
"<tr>\n";
foreach( $columns as $name => $column ) {
if( $column === array() )
continue;
- $ret .= Xml::element( 'th', null, wfMessage( 'userrights-' . $name . '-col', count( $column ) )->text() );
+ $ret .= Xml::element( 'th', null, $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text() );
}
$ret.= "</tr>\n<tr>\n";
foreach( $columns as $column ) {
@@ -548,7 +551,7 @@ class UserrightsPage extends SpecialPage {
$member = User::getGroupMember( $group, $user->getName() );
if ( $checkbox['irreversible'] ) {
- $text = wfMessage( 'userrights-irreversible-marker', $member )->escaped();
+ $text = $this->msg( 'userrights-irreversible-marker', $member )->escaped();
} else {
$text = htmlspecialchars( $member );
}
@@ -602,7 +605,8 @@ class UserrightsPage extends SpecialPage {
* @param $output OutputPage to use
*/
protected function showLogFragment( $user, $output ) {
- $output->addHTML( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) );
+ $rightsLogPage = new LogPage( 'rights' );
+ $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
}
}
diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php
index 8185fe88..4e5b6bf5 100644
--- a/includes/specials/SpecialVersion.php
+++ b/includes/specials/SpecialVersion.php
@@ -37,7 +37,7 @@ class SpecialVersion extends SpecialPage {
protected static $viewvcUrls = array(
'svn+ssh://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
'http://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
- 'https://svn.wikimedia.org/viewvc/mediawiki' => 'https://svn.wikimedia.org/viewvc/mediawiki',
+ 'https://svn.wikimedia.org/svnroot/mediawiki' => 'https://svn.wikimedia.org/viewvc/mediawiki',
);
public function __construct(){
@@ -58,6 +58,7 @@ class SpecialVersion extends SpecialPage {
$text =
$this->getMediaWikiCredits() .
$this->softwareInformation() .
+ $this->getEntryPointInfo() .
$this->getExtensionCredits();
if ( $wgSpecialVersionShowHooks ) {
$text .= $this->getWgHooks();
@@ -79,13 +80,13 @@ class SpecialVersion extends SpecialPage {
* @return string
*/
private static function getMediaWikiCredits() {
- $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) );
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMessage( 'version-license' )->text() );
// This text is always left-to-right.
- $ret .= '<div>';
+ $ret .= '<div class="plainlinks">';
$ret .= "__NOTOC__
" . self::getCopyrightAndAuthorList() . "\n
- " . wfMsg( 'version-license-info' );
+ " . wfMessage( 'version-license-info' )->text();
$ret .= '</div>';
return str_replace( "\t\t", '', $ret ) . "\n";
@@ -107,11 +108,14 @@ 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',
- wfMsg( 'version-poweredby-others' )
+ 'Timo Tijhof',
+ '[{{SERVER}}{{SCRIPTPATH}}/CREDITS ' .
+ wfMessage( 'version-poweredby-others' )->text() .
+ ']'
);
- return wfMsg( 'version-poweredby-credits', date( 'Y' ),
- $wgLang->listToText( $authorList ) );
+ return wfMessage( 'version-poweredby-credits', date( 'Y' ),
+ $wgLang->listToText( $authorList ) )->text();
}
/**
@@ -123,8 +127,8 @@ class SpecialVersion extends SpecialPage {
$dbr = wfGetDB( DB_SLAVE );
// Put the software in an array of form 'name' => 'version'. All messages should
- // be loaded here, so feel free to use wfMsg*() in the 'name'. Raw HTML or wikimarkup
- // can be used.
+ // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
+ // wikimarkup can be used.
$software = array();
$software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
$software['[http://www.php.net/ PHP]'] = phpversion() . " (" . php_sapi_name() . ")";
@@ -133,11 +137,11 @@ class SpecialVersion extends SpecialPage {
// Allow a hook to add/remove items.
wfRunHooks( 'SoftwareInfo', array( &$software ) );
- $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
- Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-software' ) ) .
+ $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMessage( 'version-software' )->text() ) .
+ Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ) ) .
"<tr>
- <th>" . wfMsg( 'version-software-product' ) . "</th>
- <th>" . wfMsg( 'version-software-version' ) . "</th>
+ <th>" . wfMessage( 'version-software-product' )->text() . "</th>
+ <th>" . wfMessage( 'version-software-version' )->text() . "</th>
</tr>\n";
foreach( $software as $name => $version ) {
@@ -160,18 +164,26 @@ class SpecialVersion extends SpecialPage {
global $wgVersion, $IP;
wfProfileIn( __METHOD__ );
- $info = self::getSvnInfo( $IP );
- if ( !$info ) {
+ $gitInfo = self::getGitHeadSha1( $IP );
+ $svnInfo = self::getSvnInfo( $IP );
+ if ( !$svnInfo && !$gitInfo ) {
$version = $wgVersion;
- } elseif( $flags === 'nodb' ) {
- $version = "$wgVersion (r{$info['checkout-rev']})";
+ } elseif ( $gitInfo && $flags === 'nodb' ) {
+ $shortSha1 = substr( $gitInfo, 0, 7 );
+ $version = "$wgVersion ($shortSha1)";
+ } elseif ( $gitInfo ) {
+ $shortSha1 = substr( $gitInfo, 0, 7 );
+ $shortSha1 = wfMessage( 'parentheses' )->params( $shortSha1 )->escaped();
+ $version = "$wgVersion $shortSha1";
+ } elseif ( $flags === 'nodb' ) {
+ $version = "$wgVersion (r{$svnInfo['checkout-rev']})";
} else {
$version = $wgVersion . ' ' .
- wfMsg(
+ wfMessage(
'version-svn-revision',
isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
$info['checkout-rev']
- );
+ )->text();
}
wfProfileOut( __METHOD__ );
@@ -180,37 +192,79 @@ class SpecialVersion extends SpecialPage {
/**
* Return a wikitext-formatted string of the MediaWiki version with a link to
- * the SVN revision if available.
+ * the SVN revision or the git SHA1 of head if available.
+ * Git is prefered over Svn
+ * The fallback is just $wgVersion
*
* @return mixed
*/
public static function getVersionLinked() {
- global $wgVersion, $IP;
+ global $wgVersion;
wfProfileIn( __METHOD__ );
+ $gitVersion = self::getVersionLinkedGit();
+ if( $gitVersion ) {
+ $v = $gitVersion;
+ } else {
+ $svnVersion = self::getVersionLinkedSvn();
+ if( $svnVersion ) {
+ $v = $svnVersion;
+ } else {
+ $v = $wgVersion; // fallback
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $v;
+ }
+
+ /**
+ * @return string wgVersion + a link to subversion revision of svn BASE
+ */
+ private static function getVersionLinkedSvn() {
+ global $wgVersion, $IP;
+
$info = self::getSvnInfo( $IP );
+ if( !isset( $info['checkout-rev'] ) ) {
+ return false;
+ }
- if ( isset( $info['checkout-rev'] ) ) {
- $linkText = wfMsg(
- 'version-svn-revision',
- isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
- $info['checkout-rev']
- );
+ $linkText = wfMessage(
+ 'version-svn-revision',
+ isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
+ $info['checkout-rev']
+ )->text();
- if ( isset( $info['viewvc-url'] ) ) {
- $version = "$wgVersion [{$info['viewvc-url']} $linkText]";
- } else {
- $version = "$wgVersion $linkText";
- }
+ if ( isset( $info['viewvc-url'] ) ) {
+ $version = "$wgVersion [{$info['viewvc-url']} $linkText]";
} else {
- $version = $wgVersion;
+ $version = "$wgVersion $linkText";
}
- wfProfileOut( __METHOD__ );
return $version;
}
/**
+ * @return bool|string wgVersion + HEAD sha1 stripped to the first 7 chars. False on failure
+ */
+ private static function getVersionLinkedGit() {
+ global $wgVersion, $IP;
+
+ $gitInfo = new GitInfo( $IP );
+ $headSHA1 = $gitInfo->getHeadSHA1();
+ if( !$headSHA1 ) {
+ return false;
+ }
+
+ $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
+ $viewerUrl = $gitInfo->getHeadViewUrl();
+ if ( $viewerUrl !== false ) {
+ $shortSHA1 = "[$viewerUrl $shortSHA1]";
+ }
+ return "$wgVersion $shortSHA1";
+ }
+
+ /**
* Returns an array with the base extension types.
* Type is stored as array key, the message as array value.
*
@@ -225,14 +279,14 @@ class SpecialVersion extends SpecialPage {
public static function getExtensionTypes() {
if ( self::$extensionTypes === false ) {
self::$extensionTypes = array(
- 'specialpage' => wfMsg( 'version-specialpages' ),
- 'parserhook' => wfMsg( 'version-parserhooks' ),
- 'variable' => wfMsg( 'version-variables' ),
- 'media' => wfMsg( 'version-mediahandlers' ),
- 'antispam' => wfMsg( 'version-antispam' ),
- 'skin' => wfMsg( 'version-skins' ),
- 'api' => wfMsg( 'version-api' ),
- 'other' => wfMsg( 'version-other' ),
+ 'specialpage' => wfMessage( 'version-specialpages' )->text(),
+ 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
+ 'variable' => wfMessage( 'version-variables' )->text(),
+ 'media' => wfMessage( 'version-mediahandlers' )->text(),
+ 'antispam' => wfMessage( 'version-antispam' )->text(),
+ 'skin' => wfMessage( 'version-skins' )->text(),
+ 'api' => wfMessage( 'version-api' )->text(),
+ 'other' => wfMessage( 'version-other' )->text(),
);
wfRunHooks( 'ExtensionTypes', array( &self::$extensionTypes ) );
@@ -274,8 +328,8 @@ class SpecialVersion extends SpecialPage {
*/
wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
- $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
- Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-ext' ) );
+ $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), $this->msg( 'version-extensions' )->text() ) .
+ Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ) );
// Make sure the 'other' type is set to an array.
if ( !array_key_exists( 'other', $wgExtensionCredits ) ) {
@@ -300,7 +354,7 @@ class SpecialVersion extends SpecialPage {
$out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
if ( count( $wgExtensionFunctions ) ) {
- $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' );
+ $out .= $this->openExtType( $this->msg( 'version-extension-functions' )->text(), 'extension-functions' );
$out .= '<tr><td colspan="4">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
}
@@ -311,13 +365,13 @@ class SpecialVersion extends SpecialPage {
for ( $i = 0; $i < $cnt; ++$i ) {
$tags[$i] = "&lt;{$tags[$i]}&gt;";
}
- $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' );
+ $out .= $this->openExtType( $this->msg( 'version-parser-extensiontags' )->text(), 'parser-tags' );
$out .= '<tr><td colspan="4">' . $this->listToText( $tags ). "</td></tr>\n";
}
$fhooks = $wgParser->getFunctionHooks();
if( count( $fhooks ) ) {
- $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' );
+ $out .= $this->openExtType( $this->msg( 'version-parser-function-hooks' )->text(), 'parser-function-hooks' );
$out .= '<tr><td colspan="4">' . $this->listToText( $fhooks ) . "</td></tr>\n";
}
@@ -356,6 +410,9 @@ class SpecialVersion extends SpecialPage {
/**
* Callback to sort extensions by type.
+ * @param $a array
+ * @param $b array
+ * @return int
*/
function compare( $a, $b ) {
if( $a['name'] === $b['name'] ) {
@@ -377,15 +434,26 @@ class SpecialVersion extends SpecialPage {
function getCreditsForExtension( array $extension ) {
$name = isset( $extension['name'] ) ? $extension['name'] : '[no name]';
+ $vcsText = false;
+
if ( isset( $extension['path'] ) ) {
- $svnInfo = self::getSvnInfo( dirname($extension['path']) );
- $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null;
- $checkoutRev = isset( $svnInfo['checkout-rev'] ) ? $svnInfo['checkout-rev'] : null;
- $viewvcUrl = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : null;
- } else {
- $directoryRev = null;
- $checkoutRev = null;
- $viewvcUrl = null;
+ $gitInfo = new GitInfo( dirname( $extension['path'] ) );
+ $gitHeadSHA1 = $gitInfo->getHeadSHA1();
+ if ( $gitHeadSHA1 !== false ) {
+ $vcsText = '(' . substr( $gitHeadSHA1, 0, 7 ) . ')';
+ $gitViewerUrl = $gitInfo->getHeadViewUrl();
+ if ( $gitViewerUrl !== false ) {
+ $vcsText = "[$gitViewerUrl $vcsText]";
+ }
+ } else {
+ $svnInfo = self::getSvnInfo( dirname( $extension['path'] ) );
+ # Make subversion text/link.
+ if ( $svnInfo !== false ) {
+ $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null;
+ $vcsText = $this->msg( 'version-svn-revision', $directoryRev, $svnInfo['checkout-rev'] )->text();
+ $vcsText = isset( $svnInfo['viewvc-url'] ) ? '[' . $svnInfo['viewvc-url'] . " $vcsText]" : $vcsText;
+ }
+ }
}
# Make main link (or just the name if there is no URL).
@@ -397,20 +465,12 @@ class SpecialVersion extends SpecialPage {
if ( isset( $extension['version'] ) ) {
$versionText = '<span class="mw-version-ext-version">' .
- wfMsg( 'version-version', $extension['version'] ) .
+ $this->msg( 'version-version', $extension['version'] )->text() .
'</span>';
} else {
$versionText = '';
}
- # Make subversion text/link.
- if ( $checkoutRev ) {
- $svnText = wfMsg( 'version-svn-revision', $directoryRev, $checkoutRev );
- $svnText = isset( $viewvcUrl ) ? "[$viewvcUrl $svnText]" : $svnText;
- } else {
- $svnText = false;
- }
-
# Make description text.
$description = isset ( $extension['description'] ) ? $extension['description'] : '';
@@ -422,16 +482,16 @@ class SpecialVersion extends SpecialPage {
$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
- $description = wfMsg( $descriptionMsgKey, $descriptionMsg );
+ $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
} else {
- $description = wfMsg( $descriptionMsg );
+ $description = $this->msg( $descriptionMsg )->text();
}
}
- if ( $svnText !== false ) {
+ if ( $vcsText !== false ) {
$extNameVer = "<tr>
<td><em>$mainLink $versionText</em></td>
- <td><em>$svnText</em></td>";
+ <td><em>$vcsText</em></td>";
} else {
$extNameVer = "<tr>
<td colspan=\"2\"><em>$mainLink $versionText</em></td>";
@@ -457,11 +517,11 @@ class SpecialVersion extends SpecialPage {
$myWgHooks = $wgHooks;
ksort( $myWgHooks );
- $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), $this->msg( 'version-hooks' )->text() ) .
Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) ) .
"<tr>
- <th>" . wfMsg( 'version-hook-name' ) . "</th>
- <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
+ <th>" . $this->msg( 'version-hook-name' )->text() . "</th>
+ <th>" . $this->msg( 'version-hook-subscribedby' )->text() . "</th>
</tr>\n";
foreach ( $myWgHooks as $hook => $hooks ) {
@@ -517,7 +577,7 @@ class SpecialVersion extends SpecialPage {
$list = array();
foreach( (array)$authors as $item ) {
if( $item == '...' ) {
- $list[] = wfMsg( 'version-poweredby-others' );
+ $list[] = $this->msg( 'version-poweredby-others' )->text();
} else {
$list[] = $item;
}
@@ -562,8 +622,8 @@ class SpecialVersion extends SpecialPage {
$list = $list[0];
}
if( is_object( $list ) ) {
- $class = get_class( $list );
- return "($class)";
+ $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
+ return $class;
} elseif ( !is_array( $list ) ) {
return $list;
} else {
@@ -572,7 +632,7 @@ class SpecialVersion extends SpecialPage {
} else {
$class = $list[0];
}
- return "($class, {$list[1]})";
+ return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
}
}
@@ -589,6 +649,8 @@ class SpecialVersion extends SpecialPage {
* url The subversion URL of the directory
* repo-url The base URL of the repository
* viewvc-url A ViewVC URL pointing to the checked-out revision
+ * @param $dir string
+ * @return array|bool
*/
public static function getSvnInfo( $dir ) {
// http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
@@ -676,6 +738,50 @@ class SpecialVersion extends SpecialPage {
}
}
+ /**
+ * @param $dir String: directory of the git checkout
+ * @return bool|String sha1 of commit HEAD points to
+ */
+ public static function getGitHeadSha1( $dir ) {
+ $repo = new GitInfo( $dir );
+ return $repo->getHeadSHA1();
+ }
+
+ /**
+ * Get the list of entry points and their URLs
+ * @return string Wikitext
+ */
+ public function getEntryPointInfo() {
+ global $wgArticlePath, $wgScriptPath;
+ $entryPoints = array(
+ 'version-entrypoints-articlepath' => $wgArticlePath,
+ 'version-entrypoints-scriptpath' => $wgScriptPath,
+ 'version-entrypoints-index-php' => wfScript( 'index' ),
+ 'version-entrypoints-api-php' => wfScript( 'api' ),
+ 'version-entrypoints-load-php' => wfScript( 'load' ),
+ );
+
+ $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( 'tr' ) .
+ Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-entrypoint' )->text() ) .
+ Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-url' )->text() ) .
+ Html::closeElement( 'tr' );
+
+ foreach ( $entryPoints as $message => $value ) {
+ $url = wfExpandUrl( $value, PROTO_RELATIVE );
+ $out .= Html::openElement( 'tr' ) .
+ // ->text() looks like it should be ->parse(), but this function
+ // returns wikitext, not HTML, boo
+ Html::rawElement( 'td', array(), $this->msg( $message )->text() ) .
+ Html::rawElement( 'td', array(), Html::rawElement( 'code', array(), "[$url $value]" ) ) .
+ Html::closeElement( 'tr' );
+ }
+
+ $out .= Html::closeElement( 'table' );
+ return $out;
+ }
+
function showEasterEgg() {
$rx = $rp = $xe = '';
$alpha = array("", "kbQW", "\$\n()");
diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php
index f497e4e2..0b1fb251 100644
--- a/includes/specials/SpecialWantedcategories.php
+++ b/includes/specials/SpecialWantedcategories.php
@@ -37,9 +37,9 @@ class WantedCategoriesPage extends WantedQueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'categorylinks', 'page' ),
- 'fields' => array ( "'" . NS_CATEGORY . "' AS namespace",
- 'cl_to AS title',
- 'COUNT(*) AS value' ),
+ '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',
diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php
index ec0912df..f52f7bb9 100644
--- a/includes/specials/SpecialWantedfiles.php
+++ b/includes/specials/SpecialWantedfiles.php
@@ -39,7 +39,7 @@ class WantedFilesPage extends WantedQueryPage {
# Specifically setting to use "Wanted Files" (NS_MAIN) as title, so as to get what
# category would be used on main namespace pages, for those tricky wikipedia
# admins who like to do {{#ifeq:{{NAMESPACE}}|foo|bar|....}}.
- $catMessage = wfMessage( 'broken-file-category' )
+ $catMessage = $this->msg( 'broken-file-category' )
->title( Title::newFromText( "Wanted Files", NS_MAIN ) )
->inContentLanguage();
@@ -66,6 +66,7 @@ class WantedFilesPage extends WantedQueryPage {
* that exist e.g. in a shared repo. Setting this at least
* keeps them from showing up as redlinks in the output, even
* if it doesn't fix the real problem (bug 6220).
+ * @return bool
*/
function forceExistenceCheck() {
return true;
@@ -74,9 +75,9 @@ class WantedFilesPage extends WantedQueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'imagelinks', 'image' ),
- 'fields' => array ( "'" . NS_FILE . "' AS namespace",
- 'il_to AS title',
- 'COUNT(*) AS value' ),
+ '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' =>
diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php
index 4624b355..7673305d 100644
--- a/includes/specials/SpecialWantedpages.php
+++ b/includes/specials/SpecialWantedpages.php
@@ -60,9 +60,9 @@ class WantedPagesPage extends WantedQueryPage {
'pg2' => 'page'
),
'fields' => array(
- 'pl_namespace AS namespace',
- 'pl_title AS title',
- 'COUNT(*) AS value'
+ 'namespace' => 'pl_namespace',
+ 'title' => 'pl_title',
+ 'value' => 'COUNT(*)'
),
'conds' => array(
'pg1.page_namespace IS NULL',
@@ -72,7 +72,7 @@ class WantedPagesPage extends WantedQueryPage {
),
'options' => array(
'HAVING' => "COUNT(*) > $count",
- 'GROUP BY' => 'pl_namespace, pl_title'
+ 'GROUP BY' => array( 'pl_namespace', 'pl_title' )
),
'join_conds' => array(
'pg1' => array(
diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php
index ab9d6046..f3e33698 100644
--- a/includes/specials/SpecialWantedtemplates.php
+++ b/includes/specials/SpecialWantedtemplates.php
@@ -40,13 +40,13 @@ class WantedTemplatesPage extends WantedQueryPage {
function getQueryInfo() {
return array (
'tables' => array ( 'templatelinks', 'page' ),
- 'fields' => array ( 'tl_namespace AS namespace',
- 'tl_title AS title',
- 'COUNT(*) AS value' ),
+ 'fields' => array ( 'namespace' => 'tl_namespace',
+ 'title' => 'tl_title',
+ 'value' => 'COUNT(*)' ),
'conds' => array ( 'page_title IS NULL',
'tl_namespace' => NS_TEMPLATE ),
'options' => array (
- 'GROUP BY' => 'tl_namespace, tl_title' ),
+ 'GROUP BY' => array( 'tl_namespace', 'tl_title' ) ),
'join_conds' => array ( 'page' => array ( 'LEFT JOIN',
array ( 'page_namespace = tl_namespace',
'page_title = tl_title' ) ) )
diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php
index fef54911..5dfc1133 100644
--- a/includes/specials/SpecialWatchlist.php
+++ b/includes/specials/SpecialWatchlist.php
@@ -91,14 +91,6 @@ class SpecialWatchlist extends SpecialPage {
return;
}
- if( ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) && $request->getVal( 'reset' ) &&
- $request->wasPosted() )
- {
- $user->clearAllNotifications();
- $output->redirect( $this->getTitle()->getFullUrl() );
- return;
- }
-
$nitems = $this->countItems();
if ( $nitems == 0 ) {
$output->addWikiMsg( 'nowatchlist' );
@@ -116,6 +108,7 @@ class SpecialWatchlist extends SpecialPage {
/* bool */ 'hideOwn' => (int)$user->getBoolOption( 'watchlisthideown' ),
/* ? */ 'namespace' => 'all',
/* ? */ 'invert' => false,
+ /* bool */ 'associated' => false,
);
$this->customFilters = array();
wfRunHooks( 'SpecialWatchlistFilters', array( $this, &$this->customFilters ) );
@@ -148,13 +141,20 @@ class SpecialWatchlist extends SpecialPage {
# Get namespace value, if supplied, and prepare a WHERE fragment
$nameSpace = $request->getIntOrNull( 'namespace' );
- $invert = $request->getIntOrNull( 'invert' );
+ $invert = $request->getBool( 'invert' );
+ $associated = $request->getBool( 'associated' );
if ( !is_null( $nameSpace ) ) {
+ $eq_op = $invert ? '!=' : '=';
+ $bool_op = $invert ? 'AND' : 'OR';
$nameSpace = intval( $nameSpace ); // paranioa
- if ( $invert ) {
- $nameSpaceClause = "rc_namespace != $nameSpace";
+ if ( !$associated ) {
+ $nameSpaceClause = "rc_namespace $eq_op $nameSpace";
} else {
- $nameSpaceClause = "rc_namespace = $nameSpace";
+ $associatedNS = MWNamespace::getAssociated( $nameSpace );
+ $nameSpaceClause =
+ "rc_namespace $eq_op $nameSpace " .
+ $bool_op .
+ " rc_namespace $eq_op $associatedNS";
}
} else {
$nameSpace = '';
@@ -162,6 +162,7 @@ class SpecialWatchlist extends SpecialPage {
}
$values['namespace'] = $nameSpace;
$values['invert'] = $invert;
+ $values['associated'] = $associated;
if( is_null( $values['days'] ) || !is_numeric( $values['days'] ) ) {
$big = 1000; /* The magical big */
@@ -181,6 +182,14 @@ class SpecialWatchlist extends SpecialPage {
wfAppendToArrayIfNotDefault( $name, $values[$name], $defaults, $nondefaults );
}
+ if( ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) && $request->getVal( 'reset' ) &&
+ $request->wasPosted() )
+ {
+ $user->clearAllNotifications();
+ $output->redirect( $this->getTitle()->getFullUrl( $nondefaults ) );
+ return;
+ }
+
$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
# Possible where conditions
@@ -254,15 +263,25 @@ class SpecialWatchlist extends SpecialPage {
'id' => 'mw-watchlist-resetbutton' ) ) .
$this->msg( 'wlheader-showupdated' )->parse() . ' ' .
Xml::submitButton( $this->msg( 'enotif_reset' )->text(), array( 'name' => 'dummy' ) ) .
- Html::hidden( 'reset', 'all' ) .
- Xml::closeElement( 'form' );
+ Html::hidden( 'reset', 'all' );
+ foreach ( $nondefaults as $key => $value ) {
+ $form .= Html::hidden( $key, $value );
+ }
+ $form .= Xml::closeElement( 'form' );
}
$form .= '<hr />';
$tables = array( 'recentchanges', 'watchlist' );
$fields = array( $dbr->tableName( 'recentchanges' ) . '.*' );
$join_conds = array(
- 'watchlist' => array('INNER JOIN',"wl_user='{$user->getId()}' AND wl_namespace=rc_namespace AND wl_title=rc_title"),
+ 'watchlist' => array(
+ 'INNER JOIN',
+ array(
+ 'wl_user' => $user->getId(),
+ 'wl_namespace=rc_namespace',
+ 'wl_title=rc_title'
+ ),
+ ),
);
$options = array( 'ORDER BY' => 'rc_timestamp DESC' );
if( $wgShowUpdatedMarker ) {
@@ -285,7 +304,7 @@ class SpecialWatchlist extends SpecialPage {
wfRunHooks('SpecialWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) );
$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
- $numRows = $dbr->numRows( $res );
+ $numRows = $res->numRows();
/* Start bottom header */
@@ -327,9 +346,31 @@ class SpecialWatchlist extends SpecialPage {
$form .= $lang->pipeList( $links );
$form .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl(), 'id' => 'mw-watchlist-form-namespaceselector' ) );
$form .= '<hr /><p>';
- $form .= Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . '&#160;';
- $form .= Xml::namespaceSelector( $nameSpace, '' ) . '&#160;';
- $form .= Xml::checkLabel( $this->msg( 'invert' )->text(), 'invert', 'nsinvert', $invert ) . '&#160;';
+ $form .= Html::namespaceSelector(
+ array(
+ 'selected' => $nameSpace,
+ 'all' => '',
+ 'label' => $this->msg( 'namespace' )->text()
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ ) . '&#160;';
+ $form .= Xml::checkLabel(
+ $this->msg( 'invert' )->text(),
+ 'invert',
+ 'nsinvert',
+ $invert,
+ array( 'title' => $this->msg( 'tooltip-invert' )->text() )
+ ) . '&#160;';
+ $form .= Xml::checkLabel(
+ $this->msg( 'namespace_association' )->text(),
+ 'associated',
+ 'associated',
+ $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 ) {
@@ -459,7 +500,7 @@ class SpecialWatchlist extends SpecialPage {
$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
# Fetch the raw count
- $res = $dbr->select( 'watchlist', 'COUNT(*) AS count',
+ $res = $dbr->select( 'watchlist', array( 'count' => 'COUNT(*)' ),
array( 'wl_user' => $this->getUser()->getId() ), __METHOD__ );
$row = $dbr->fetchObject( $res );
$count = $row->count;
diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php
index d5129bf6..f1356493 100644
--- a/includes/specials/SpecialWhatlinkshere.php
+++ b/includes/specials/SpecialWhatlinkshere.php
@@ -163,7 +163,7 @@ class SpecialWhatLinksHere extends SpecialPage {
'rd_from = page_id',
'rd_namespace' => $target->getNamespace(),
'rd_title' => $target->getDBkey(),
- '(rd_interwiki is NULL) or (rd_interwiki = \'\')'
+ 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'
)));
if( $fetchlinks ) {
@@ -288,7 +288,7 @@ class SpecialWhatLinksHere extends SpecialPage {
'whatlinkshere-links', 'isimage' );
$msgcache = array();
foreach ( $msgs as $msg ) {
- $msgcache[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
+ $msgcache[$msg] = $this->msg( $msg )->escaped();
}
}
@@ -316,12 +316,12 @@ class SpecialWhatLinksHere extends SpecialPage {
$props[] = $msgcache['isimage'];
if ( count( $props ) ) {
- $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')';
+ $propsText = $this->msg( 'parentheses' )->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped();
}
# Space for utilities links, with a what-links-here link provided
$wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
- $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' );
+ $wlh = Xml::wrapClass( $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(), 'mw-whatlinkshere-tools' );
return $notClose ?
Xml::openElement( 'li' ) . "$link $propsText $dirmark $wlh\n" :
@@ -356,8 +356,8 @@ class SpecialWhatLinksHere extends SpecialPage {
function getPrevNext( $prevId, $nextId ) {
$currentLimit = $this->opts->getValue( 'limit' );
- $prev = wfMessage( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped();
- $next = wfMessage( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
+ $prev = $this->msg( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped();
+ $next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
$changed = $this->opts->getChangedValues();
unset($changed['target']); // Already in the request title
@@ -381,7 +381,7 @@ class SpecialWhatLinksHere extends SpecialPage {
$nums = $lang->pipeList( $limitLinks );
- return wfMsgHtml( 'viewprevnext', $prev, $next, $nums );
+ return $this->msg( 'viewprevnext' )->rawParams( $prev, $next, $nums )->escaped();
}
function whatlinkshereForm() {
@@ -404,22 +404,31 @@ class SpecialWhatLinksHere extends SpecialPage {
$f .= Html::hidden( $name, $value );
}
- $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) );
+ $f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() );
# Target input
- $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target',
+ $f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target',
'mw-whatlinkshere-target', 40, $target );
$f .= ' ';
# Namespace selector
- $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&#160;' .
- Xml::namespaceSelector( $namespace, '' );
+ $f .= Html::namespaceSelector(
+ array(
+ 'selected' => $namespace,
+ 'all' => '',
+ 'label' => $this->msg( 'namespace' )->text()
+ ), array(
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'class' => 'namespaceselector',
+ )
+ );
$f .= ' ';
# Submit
- $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+ $f .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() );
# Close
$f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
@@ -433,8 +442,8 @@ class SpecialWhatLinksHere extends SpecialPage {
* @return string HTML fieldset and filter panel with the show/hide links
*/
function getFilterPanel() {
- $show = wfMsgHtml( 'show' );
- $hide = wfMsgHtml( 'hide' );
+ $show = $this->msg( 'show' )->escaped();
+ $hide = $this->msg( 'hide' )->escaped();
$changed = $this->opts->getChangedValues();
unset($changed['target']); // Already in the request title
@@ -445,13 +454,14 @@ class SpecialWhatLinksHere extends SpecialPage {
$types[] = 'hideimages';
// Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans', 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages'
- // To be sure they will be find by grep
+ // To be sure they will be found by grep
foreach( $types as $type ) {
$chosen = $this->opts->getValue( $type );
$msg = $chosen ? $show : $hide;
$overrides = array( $type => !$chosen );
- $links[] = wfMsgHtml( "whatlinkshere-{$type}", $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) );
+ $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
+ $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped();
}
- return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), $this->getLanguage()->pipeList( $links ) );
+ return Xml::fieldset( $this->msg( 'whatlinkshere-filters' )->text(), $this->getLanguage()->pipeList( $links ) );
}
}
diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php
index 89dae203..2988b04f 100644
--- a/includes/specials/SpecialWithoutinterwiki.php
+++ b/includes/specials/SpecialWithoutinterwiki.php
@@ -41,10 +41,10 @@ class WithoutInterwikiPage extends PageQueryPage {
}
function getPageHeader() {
- global $wgScript, $wgMiserMode;
+ global $wgScript;
- # Do not show useless input form if wiki is running in misermode
- if( $wgMiserMode ) {
+ # Do not show useless input form if special page is cached
+ if( $this->isCached() ) {
return '';
}
@@ -53,10 +53,10 @@ class WithoutInterwikiPage extends PageQueryPage {
return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'withoutinterwiki-legend' ) ) .
+ Xml::element( 'legend', null, $this->msg( 'withoutinterwiki-legend' )->text() ) .
Html::hidden( 'title', $t->getPrefixedText() ) .
- Xml::inputLabel( wfMsg( 'allpagesprefix' ), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
- Xml::submitButton( wfMsg( 'withoutinterwiki-submit' ) ) .
+ Xml::inputLabel( $this->msg( 'allpagesprefix' )->text(), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
+ Xml::submitButton( $this->msg( 'withoutinterwiki-submit' )->text() ) .
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' );
}
@@ -80,9 +80,9 @@ class WithoutInterwikiPage extends PageQueryPage {
function getQueryInfo() {
$query = array (
'tables' => array ( 'page', 'langlinks' ),
- 'fields' => array ( 'page_namespace AS namespace',
- 'page_title AS title',
- 'page_title AS value' ),
+ 'fields' => array ( 'namespace' => 'page_namespace',
+ 'title' => 'page_title',
+ 'value' => 'page_title' ),
'conds' => array ( 'll_title IS NULL',
'page_namespace' => MWNamespace::getContentNamespaces(),
'page_is_redirect' => 0 ),
diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php
index 59284af0..bf5c487a 100644
--- a/includes/templates/NoLocalSettings.php
+++ b/includes/templates/NoLocalSettings.php
@@ -1,6 +1,21 @@
<?php
/**
- * Template used when there is no LocalSettings.php file
+ * Template used when there is no LocalSettings.php file.
+ *
+ * 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 Templates
diff --git a/includes/templates/Usercreate.php b/includes/templates/Usercreate.php
index c93b02cc..98727f17 100644
--- a/includes/templates/Usercreate.php
+++ b/includes/templates/Usercreate.php
@@ -1,6 +1,21 @@
<?php
/**
- * Html form for account creation
+ * Html form for account creation.
+ *
+ * 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 Templates
@@ -154,9 +169,10 @@ class UsercreateTemplate extends QuickTemplate {
<td></td>
<td class="mw-input">
<?php
- global $wgCookieExpiration, $wgLang;
+ global $wgCookieExpiration;
+ $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
echo Xml::checkLabel(
- wfMsgExt( 'remembermypassword', 'parsemag', $wgLang->formatNum( ceil( $wgCookieExpiration / ( 3600 * 24 ) ) ) ),
+ wfMessage( 'remembermypassword' )->numParams( $expirationDays )->text(),
'wpRemember',
'wpRemember',
$this->data['remember'],
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
index efe826f4..a3f6a38b 100644
--- a/includes/templates/Userlogin.php
+++ b/includes/templates/Userlogin.php
@@ -1,6 +1,21 @@
<?php
/**
- * Html form for user login
+ * Html form for user login.
+ *
+ * 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 Templates
@@ -93,9 +108,10 @@ class UserloginTemplate extends QuickTemplate {
<td></td>
<td class="mw-input">
<?php
- global $wgCookieExpiration, $wgLang;
+ global $wgCookieExpiration;
+ $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
echo Xml::checkLabel(
- wfMsgExt( 'remembermypassword', 'parsemag', $wgLang->formatNum( ceil( $wgCookieExpiration / ( 3600 * 24 ) ) ) ),
+ wfMessage( 'remembermypassword' )->numParams( $expirationDays )->text(),
'wpRemember',
'wpRemember',
$this->data['remember'],
@@ -111,7 +127,7 @@ class UserloginTemplate extends QuickTemplate {
<td class="mw-input">
<?php
echo Xml::checkLabel(
- wfMsg( 'securelogin-stick-https' ),
+ wfMessage( 'securelogin-stick-https' )->text(),
'wpStickHTTPS',
'wpStickHTTPS',
$this->data['stickHTTPS'],
@@ -125,7 +141,7 @@ class UserloginTemplate extends QuickTemplate {
<td></td>
<td class="mw-submit">
<?php
- echo Html::input( 'wpLoginAttempt', wfMsg( 'login' ), 'submit', array(
+ echo Html::input( 'wpLoginAttempt', wfMessage( 'login' )->text(), 'submit', array(
'id' => 'wpLoginAttempt',
'tabindex' => '9'
) );
@@ -138,10 +154,14 @@ class UserloginTemplate extends QuickTemplate {
);
} elseif( $this->data['resetlink'] === null ) {
echo '&#160;';
- echo Html::input( 'wpMailmypassword', wfMsg( 'mailmypassword' ), 'submit', array(
- 'id' => 'wpMailmypassword',
- 'tabindex' => '10'
- ) );
+ echo Html::input(
+ 'wpMailmypassword',
+ wfMessage( 'mailmypassword' )->text(),
+ 'submit', array(
+ 'id' => 'wpMailmypassword',
+ 'tabindex' => '10'
+ )
+ );
}
} ?>
diff --git a/includes/tidy.conf b/includes/tidy.conf
index 09412f05..aa333fcb 100644
--- a/includes/tidy.conf
+++ b/includes/tidy.conf
@@ -16,3 +16,4 @@ quiet: yes
quote-nbsp: yes
fix-backslash: no
fix-uri: no
+new-inline-tags: video,audio,source,track,bdi
diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php
index 6cc2287a..d40b53d3 100644
--- a/includes/upload/UploadBase.php
+++ b/includes/upload/UploadBase.php
@@ -1,6 +1,28 @@
<?php
/**
- * @defgroup Upload
+ * Base class for the backend of file 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
+ */
+
+/**
+ * @defgroup Upload Upload related
*/
/**
@@ -41,6 +63,10 @@ abstract class UploadBase {
const WINDOWS_NONASCII_FILENAME = 13;
const FILENAME_TOO_LONG = 14;
+ /**
+ * @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',
@@ -64,6 +90,7 @@ abstract class UploadBase {
/**
* Returns true if uploads are enabled.
* Can be override by subclasses.
+ * @return bool
*/
public static function isEnabled() {
global $wgEnableUploads;
@@ -82,6 +109,7 @@ abstract class UploadBase {
* Can be overriden by subclasses.
*
* @param $user User
+ * @return bool
*/
public static function isAllowed( $user ) {
foreach ( array( 'upload', 'edit' ) as $permission ) {
@@ -100,6 +128,7 @@ abstract class UploadBase {
*
* @param $request WebRequest
* @param $type
+ * @return null
*/
public static function createFromRequest( &$request, $type = null ) {
$type = $type ? $type : $request->getVal( 'wpSourceType', 'File' );
@@ -140,6 +169,8 @@ abstract class UploadBase {
/**
* Check whether a request if valid for this handler
+ * @param $request
+ * @return bool
*/
public static function isValidRequest( $request ) {
return false;
@@ -161,7 +192,7 @@ abstract class UploadBase {
* @param $tempPath string the temporary path
* @param $fileSize int the file size
* @param $removeTempFile bool (false) remove the temporary file?
- * @return null
+ * @throws MWException
*/
public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
$this->mDesiredDestName = $name;
@@ -180,6 +211,7 @@ abstract class UploadBase {
/**
* Fetch the file. Usually a no-op
+ * @return Status
*/
public function fetchFile() {
return Status::newGood();
@@ -203,17 +235,20 @@ abstract class UploadBase {
/**
* @param $srcPath String: the source path
- * @return the real path if it was a virtual URL
+ * @return string the real path if it was a virtual URL
*/
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
$tmpFile = $repo->getLocalCopy( $srcPath );
$tmpFile->bind( $this ); // keep alive with $thumb
+ wfProfileOut( __METHOD__ );
return $tmpFile->getPath();
}
+ wfProfileOut( __METHOD__ );
return $srcPath;
}
@@ -222,10 +257,13 @@ abstract class UploadBase {
* @return mixed self::OK or else an array with error information
*/
public function verifyUpload() {
+ wfProfileIn( __METHOD__ );
+
/**
* If there was no filename or a zero size given, give up quick.
*/
if( $this->isEmptyFile() ) {
+ wfProfileOut( __METHOD__ );
return array( 'status' => self::EMPTY_FILE );
}
@@ -234,6 +272,7 @@ abstract class UploadBase {
*/
$maxSize = self::getMaxUploadSize( $this->getSourceType() );
if( $this->mFileSize > $maxSize ) {
+ wfProfileOut( __METHOD__ );
return array(
'status' => self::FILE_TOO_LARGE,
'max' => $maxSize,
@@ -247,6 +286,7 @@ abstract class UploadBase {
*/
$verification = $this->verifyFile();
if( $verification !== true ) {
+ wfProfileOut( __METHOD__ );
return array(
'status' => self::VERIFICATION_ERROR,
'details' => $verification
@@ -258,15 +298,19 @@ abstract class UploadBase {
*/
$result = $this->validateName();
if( $result !== true ) {
+ wfProfileOut( __METHOD__ );
return $result;
}
$error = '';
if( !wfRunHooks( 'UploadVerification',
- array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
+ array( $this->mDestName, $this->mTempPath, &$error ) ) )
+ {
+ wfProfileOut( __METHOD__ );
return array( 'status' => self::HOOK_ABORTED, 'error' => $error );
}
+ wfProfileOut( __METHOD__ );
return array( 'status' => self::OK );
}
@@ -304,15 +348,18 @@ abstract class UploadBase {
*/
protected function verifyMimeType( $mime ) {
global $wgVerifyMimeType;
+ wfProfileIn( __METHOD__ );
if ( $wgVerifyMimeType ) {
wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n");
global $wgMimeTypeBlacklist;
if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+ wfProfileOut( __METHOD__ );
return array( 'filetype-badmime', $mime );
}
# XXX: Missing extension will be caught by validateName() via getTitle()
if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) {
+ wfProfileOut( __METHOD__ );
return array( 'filetype-mime-mismatch', $this->mFinalExtension, $mime );
}
@@ -326,11 +373,13 @@ abstract class UploadBase {
$ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
foreach ( $ieTypes as $ieType ) {
if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
+ wfProfileOut( __METHOD__ );
return array( 'filetype-bad-ie-mime', $ieType );
}
}
}
+ wfProfileOut( __METHOD__ );
return true;
}
@@ -341,6 +390,8 @@ abstract class UploadBase {
*/
protected function verifyFile() {
global $wgAllowJavaUploads, $wgDisableUploadScriptChecks;
+ wfProfileIn( __METHOD__ );
+
# get the title, even though we are doing nothing with it, because
# we need to populate mFinalExtension
$this->getTitle();
@@ -351,16 +402,19 @@ abstract class UploadBase {
$mime = $this->mFileProps[ 'file-mime' ];
$status = $this->verifyMimeType( $mime );
if ( $status !== true ) {
+ wfProfileOut( __METHOD__ );
return $status;
}
# check for htmlish code and javascript
if ( !$wgDisableUploadScriptChecks ) {
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 ) ) {
+ wfProfileOut( __METHOD__ );
return array( 'uploadscripted' );
}
}
@@ -376,10 +430,12 @@ abstract class UploadBase {
$errors = $zipStatus->getErrorsArray();
$error = reset( $errors );
if ( $error[0] !== 'zip-wrong-format' ) {
+ wfProfileOut( __METHOD__ );
return $error;
}
}
if ( $this->mJavaDetected ) {
+ wfProfileOut( __METHOD__ );
return array( 'uploadjava' );
}
}
@@ -387,6 +443,7 @@ abstract class UploadBase {
# Scan the uploaded file for viruses
$virus = $this->detectVirus( $this->mTempPath );
if ( $virus ) {
+ wfProfileOut( __METHOD__ );
return array( 'uploadvirus', $virus );
}
@@ -395,16 +452,19 @@ abstract class UploadBase {
$handlerStatus = $handler->verifyUpload( $this->mTempPath );
if ( !$handlerStatus->isOK() ) {
$errors = $handlerStatus->getErrorsArray();
+ wfProfileOut( __METHOD__ );
return reset( $errors );
}
}
wfRunHooks( 'UploadVerifyFile', array( $this, $mime, &$status ) );
if ( $status !== true ) {
+ wfProfileOut( __METHOD__ );
return $status;
}
wfDebug( __METHOD__ . ": all clear; passing.\n" );
+ wfProfileOut( __METHOD__ );
return true;
}
@@ -490,6 +550,7 @@ abstract class UploadBase {
*/
public function checkWarnings() {
global $wgLang;
+ wfProfileIn( __METHOD__ );
$warnings = array();
@@ -550,6 +611,7 @@ abstract class UploadBase {
$warnings['duplicate-archive'] = $archivedImage->getName();
}
+ wfProfileOut( __METHOD__ );
return $warnings;
}
@@ -557,11 +619,16 @@ abstract class UploadBase {
* Really perform the upload. Stores the file in the local repo, watches
* if necessary and runs the UploadComplete hook.
*
+ * @param $comment
+ * @param $pageText
+ * @param $watch
* @param $user User
*
* @return Status indicating the whether the upload succeeded.
*/
public function performUpload( $comment, $pageText, $watch, $user ) {
+ wfProfileIn( __METHOD__ );
+
$status = $this->getLocalFile()->upload(
$this->mTempPath,
$comment,
@@ -576,10 +643,10 @@ abstract class UploadBase {
if ( $watch ) {
$user->addWatch( $this->getLocalFile()->getTitle() );
}
-
wfRunHooks( 'UploadComplete', array( &$this ) );
}
+ wfProfileOut( __METHOD__ );
return $status;
}
@@ -699,7 +766,7 @@ abstract class UploadBase {
/**
* Return the local file and initializes if necessary.
*
- * @return LocalFile
+ * @return LocalFile|null
*/
public function getLocalFile() {
if( is_null( $this->mLocalFile ) ) {
@@ -710,26 +777,6 @@ abstract class UploadBase {
}
/**
- * NOTE: Probably should be deprecated in favor of UploadStash, but this is sometimes
- * called outside that context.
- *
- * Stash a file in a temporary directory for later processing
- * after the user has confirmed it.
- *
- * If the user doesn't explicitly cancel or accept, these files
- * can accumulate in the temp directory.
- *
- * @param $saveName String: the destination filename
- * @param $tempSrc String: the source temporary file to save
- * @return String: full path the stashed file, or false on failure
- */
- protected function saveTempUploadedFile( $saveName, $tempSrc ) {
- $repo = RepoGroup::singleton()->getLocalRepo();
- $status = $repo->storeTemp( $saveName, $tempSrc );
- return $status;
- }
-
- /**
* If the user does not supply all necessary information in the first upload form submission (either by accident or
* by design) then we may want to stash the file temporarily, get more information, and publish the file later.
*
@@ -742,9 +789,13 @@ abstract class UploadBase {
*/
public function stashFile() {
// was stashSessionFile
+ wfProfileIn( __METHOD__ );
+
$stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
$file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
$this->mLocalFile = $file;
+
+ wfProfileOut( __METHOD__ );
return $file;
}
@@ -787,6 +838,7 @@ abstract class UploadBase {
* earlier pseudo-'extensions' to determine type and execute
* scripts, so the blacklist needs to check them all.
*
+ * @param $filename string
* @return array
*/
public static function splitExtensions( $filename ) {
@@ -870,6 +922,7 @@ abstract class UploadBase {
*/
public static function detectScript( $file, $mime, $extension ) {
global $wgAllowTitlesInSVG;
+ wfProfileIn( __METHOD__ );
# ugly hack: for text files, always look at the entire file.
# For binary field, just check the first K.
@@ -885,6 +938,7 @@ abstract class UploadBase {
$chunk = strtolower( $chunk );
if( !$chunk ) {
+ wfProfileOut( __METHOD__ );
return false;
}
@@ -908,6 +962,7 @@ abstract class UploadBase {
# check for HTML doctype
if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
+ wfProfileOut( __METHOD__ );
return true;
}
@@ -944,6 +999,7 @@ abstract class UploadBase {
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;
}
}
@@ -958,25 +1014,33 @@ abstract class UploadBase {
# look for script-types
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 ) ) {
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 ) ) {
wfDebug( __METHOD__ . ": found css-style script urls\n" );
+ wfProfileOut( __METHOD__ );
return true;
}
wfDebug( __METHOD__ . ": no scripts found\n" );
+ wfProfileOut( __METHOD__ );
return false;
}
+ /**
+ * @param $filename string
+ * @return bool
+ */
protected function detectScriptInSvg( $filename ) {
$check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) );
return $check->filterMatch;
@@ -984,6 +1048,9 @@ abstract class UploadBase {
/**
* @todo Replace this with a whitelist filter!
+ * @param $element string
+ * @param $attribs array
+ * @return bool
*/
public function checkSvgScriptCallback( $element, $attribs ) {
$strippedElement = $this->stripXmlNamespace( $element );
@@ -1054,7 +1121,7 @@ abstract class UploadBase {
}
- # use handler attribute with remote / data / script
+ # use handler attribute with remote / data / script
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;
@@ -1082,6 +1149,10 @@ abstract class UploadBase {
return false; //No scripts detected
}
+ /**
+ * @param $name string
+ * @return string
+ */
private function stripXmlNamespace( $name ) {
// 'http://www.w3.org/2000/svg:script' -> 'script'
$parts = explode( ':', strtolower( $name ) );
@@ -1100,9 +1171,11 @@ abstract class UploadBase {
*/
public static function detectVirus( $file ) {
global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
+ wfProfileIn( __METHOD__ );
if ( !$wgAntivirus ) {
wfDebug( __METHOD__ . ": virus scanner disabled\n" );
+ wfProfileOut( __METHOD__ );
return null;
}
@@ -1110,7 +1183,8 @@ abstract class UploadBase {
wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" );
$wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
array( 'virus-badscanner', $wgAntivirus ) );
- return wfMsg( 'virus-unknownscanner' ) . " $wgAntivirus";
+ wfProfileOut( __METHOD__ );
+ return wfMessage( 'virus-unknownscanner' )->text() . " $wgAntivirus";
}
# look up scanner configuration
@@ -1152,17 +1226,21 @@ abstract class UploadBase {
wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" );
if ( $wgAntivirusRequired ) {
- return wfMsg( 'virus-scanfailed', array( $exitCode ) );
+ wfProfileOut( __METHOD__ );
+ return wfMessage( 'virus-scanfailed', array( $exitCode ) )->text();
} else {
+ wfProfileOut( __METHOD__ );
return 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;
} elseif ( $mappedCode === AV_NO_VIRUS ) {
# no virus found
wfDebug( __METHOD__ . ": file passed virus scan.\n" );
+ wfProfileOut( __METHOD__ );
return false;
} else {
$output = trim( $output );
@@ -1179,6 +1257,7 @@ abstract class UploadBase {
}
wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" );
+ wfProfileOut( __METHOD__ );
return $output;
}
}
@@ -1325,6 +1404,8 @@ abstract class UploadBase {
/**
* Helper function that checks whether the filename looks like a thumbnail
+ * @param $filename string
+ * @return bool
*/
public static function isThumbName( $filename ) {
$n = strrpos( $filename, '.' );
@@ -1387,13 +1468,20 @@ abstract class UploadBase {
return $info;
}
-
+ /**
+ * @param $error array
+ * @return Status
+ */
public function convertVerifyErrorToStatus( $error ) {
$code = $error['status'];
unset( $code['status'] );
return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
}
+ /**
+ * @param $forType null|string
+ * @return int
+ */
public static function getMaxUploadSize( $forType = null ) {
global $wgMaxUploadSize;
diff --git a/includes/upload/UploadFromChunks.php b/includes/upload/UploadFromChunks.php
index ec83f7d3..0542bba5 100644
--- a/includes/upload/UploadFromChunks.php
+++ b/includes/upload/UploadFromChunks.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Backend for uploading files from chunks.
+ *
+ * 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
+ */
+
+/**
* Implements uploading from chunks
*
* @ingroup Upload
@@ -7,10 +29,10 @@
*/
class UploadFromChunks extends UploadFromFile {
protected $mOffset, $mChunkIndex, $mFileKey, $mVirtualTempPath;
-
+
/**
* Setup local pointers to stash, repo and user ( similar to UploadFromStash )
- *
+ *
* @param $user User
* @param $stash UploadStash
* @param $repo FileRepo
@@ -39,37 +61,37 @@ class UploadFromChunks extends UploadFromFile {
return true;
}
/**
- * Calls the parent stashFile and updates the uploadsession table to handle "chunks"
+ * Calls the parent stashFile and updates the uploadsession table to handle "chunks"
*
* @return UploadStashFile stashed file
*/
public function stashFile() {
- // Stash file is the called on creating a new chunk session:
+ // Stash file is the called on creating a new chunk session:
$this->mChunkIndex = 0;
$this->mOffset = 0;
// 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() );
-
- // Update db table to reflect initial "chunk" state
+
+ // Update db table to reflect initial "chunk" state
$this->updateChunkStatus();
return $this->mLocalFile;
}
-
+
/**
* Continue chunk uploading
- */
+ */
public function continueChunks( $name, $key, $webRequestUpload ) {
$this->mFileKey = $key;
$this->mUpload = $webRequestUpload;
- // Get the chunk status form the db:
+ // Get the chunk status form the db:
$this->getChunkStatus();
-
+
$metadata = $this->stash->getMetadata( $key );
$this->initializePathInfo( $name,
$this->getRealPath( $metadata['us_path'] ),
@@ -77,13 +99,13 @@ class UploadFromChunks extends UploadFromFile {
false
);
}
-
+
/**
* Append the final chunk and ready file for parent::performUpload()
* @return FileRepoStatus
*/
public function concatenateChunks() {
- wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" .
+ wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" .
$this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
// Concatenate all the chunks to mVirtualTempPath
@@ -103,10 +125,10 @@ class UploadFromChunks extends UploadFromFile {
// Concatenate the chunks at the temp file
$status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE );
if( !$status->isOk() ){
- return $status;
+ return $status;
}
// Update the mTempPath and mLocalFile
- // ( for FileUpload or normal Stash to take over )
+ // ( for FileUpload or normal Stash to take over )
$this->mTempPath = $tmpPath; // file system path
$this->mLocalFile = parent::stashFile();
@@ -127,42 +149,44 @@ class UploadFromChunks extends UploadFromFile {
}
/**
- * Returns the virtual chunk location:
- * @param unknown_type $index
+ * Returns the virtual chunk location:
+ * @param $index
+ * @return string
*/
function getVirtualChunkLocation( $index ){
- return $this->repo->getVirtualUrl( 'temp' ) .
+ return $this->repo->getVirtualUrl( 'temp' ) .
'/' .
- $this->repo->getHashPath(
+ $this->repo->getHashPath(
$this->getChunkFileKey( $index )
- ) .
+ ) .
$this->getChunkFileKey( $index );
}
+
/**
* Add a chunk to the temporary directory
*
- * @param $chunkPath path to temporary chunk file
- * @param $chunkSize size of the current chunk
- * @param $offset offset of current chunk ( mutch match database chunk offset )
+ * @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 )
* @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()) {
$status = Status::newFatal( 'file-too-large' );
} else {
// Make sure the client is uploading the correct chunk with a matching offset.
if ( $preAppendOffset == $offset ) {
- // Update local chunk index for the current chunk
+ // Update local chunk index for the current chunk
$this->mChunkIndex++;
$status = $this->outputChunk( $chunkPath );
if( $status->isGood() ){
- // Update local offset:
+ // Update local offset:
$this->mOffset = $preAppendOffset + $chunkSize;
- // Update chunk table status db
- $this->updateChunkStatus();
+ // Update chunk table status db
+ $this->updateChunkStatus();
}
} else {
$status = Status::newFatal( 'invalid-chunk-offset' );
@@ -170,18 +194,18 @@ class UploadFromChunks extends UploadFromFile {
}
return $status;
}
-
+
/**
- * Update the chunk db table with the current status:
+ * Update the chunk db table with the current status:
*/
private function updateChunkStatus(){
- wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" .
+ wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" .
$this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
-
+
$dbw = $this->repo->getMasterDb();
$dbw->update(
'uploadstash',
- array(
+ array(
'us_status' => 'chunks',
'us_chunk_inx' => $this->getChunkIndex(),
'us_size' => $this->getOffset()
@@ -190,16 +214,17 @@ class UploadFromChunks extends UploadFromFile {
__METHOD__
);
}
+
/**
* Get the chunk db state and populate update relevant local values
*/
private function getChunkStatus(){
- // get Master db to avoid race conditions.
+ // get Master db to avoid race conditions.
// Otherwise, if chunk upload time < replag there will be spurious errors
$dbw = $this->repo->getMasterDb();
$row = $dbw->selectRow(
- 'uploadstash',
- array(
+ 'uploadstash',
+ array(
'us_chunk_inx',
'us_size',
'us_path',
@@ -214,8 +239,9 @@ class UploadFromChunks extends UploadFromFile {
$this->mVirtualTempPath = $row->us_path;
}
}
+
/**
- * Get the current Chunk index
+ * Get the current Chunk index
* @return Integer index of the current chunk
*/
private function getChunkIndex(){
@@ -224,10 +250,10 @@ class UploadFromChunks extends UploadFromFile {
}
return 0;
}
-
+
/**
- * Gets the current offset in fromt the stashedupload table
- * @return Integer current byte offset of the chunk file set
+ * 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 ){
@@ -235,20 +261,23 @@ class UploadFromChunks extends UploadFromFile {
}
return 0;
}
-
+
/**
* Output the chunk to disk
- *
+ *
* @param $chunkPath string
+ * @throws UploadChunkFileException
+ * @return FileRepoStatus
*/
private function outputChunk( $chunkPath ){
// Key is fileKey + chunk index
$fileKey = $this->getChunkFileKey();
-
- // Store the chunk per its indexed fileKey:
+
+ // Store the chunk per its indexed fileKey:
$hashPath = $this->repo->getHashPath( $fileKey );
- $storeStatus = $this->repo->store( $chunkPath, 'temp', "$hashPath$fileKey" );
-
+ $storeStatus = $this->repo->quickImport( $chunkPath,
+ $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" );
+
// Check for error in stashing the chunk:
if ( ! $storeStatus->isOK() ) {
$error = $storeStatus->getErrorsArray();
@@ -264,6 +293,7 @@ class UploadFromChunks extends UploadFromFile {
}
return $storeStatus;
}
+
private function getChunkFileKey( $index = null ){
if( $index === null ){
$index = $this->getChunkIndex();
diff --git a/includes/upload/UploadFromFile.php b/includes/upload/UploadFromFile.php
index 23ec2ef4..aa0cc77b 100644
--- a/includes/upload/UploadFromFile.php
+++ b/includes/upload/UploadFromFile.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Backend for regular file 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
+ */
+
+/**
* Implements regular file uploads
*
* @ingroup Upload
@@ -16,12 +38,13 @@ class UploadFromFile extends UploadBase {
* @param $request WebRequest
*/
function initializeFromRequest( &$request ) {
- $upload = $request->getUpload( 'wpUploadFile' );
+ $upload = $request->getUpload( 'wpUploadFile' );
$desiredDestName = $request->getText( 'wpDestFile' );
- if( !$desiredDestName )
+ if( !$desiredDestName ) {
$desiredDestName = $upload->getName();
-
- return $this->initialize( $desiredDestName, $upload );
+ }
+
+ $this->initialize( $desiredDestName, $upload );
}
/**
@@ -31,7 +54,7 @@ class UploadFromFile extends UploadBase {
*/
function initialize( $name, $webRequestUpload ) {
$this->mUpload = $webRequestUpload;
- return $this->initializePathInfo( $name,
+ $this->initializePathInfo( $name,
$this->mUpload->getTempName(), $this->mUpload->getSize() );
}
diff --git a/includes/upload/UploadFromStash.php b/includes/upload/UploadFromStash.php
index f7589bd2..607965f3 100644
--- a/includes/upload/UploadFromStash.php
+++ b/includes/upload/UploadFromStash.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Backend for uploading files from previously stored file.
+ *
+ * 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
+ */
+
+/**
* Implements uploading from previously stored file.
*
* @ingroup Upload
@@ -40,8 +62,6 @@ class UploadFromStash extends UploadBase {
$this->stash = new UploadStash( $this->repo, $this->user );
}
-
- return true;
}
/**
@@ -99,7 +119,7 @@ class UploadFromStash extends UploadBase {
// chooses one of wpDestFile, wpUploadFile, filename in that order.
$desiredDestName = $request->getText( 'wpDestFile', $request->getText( 'wpUploadFile', $request->getText( 'filename' ) ) );
- return $this->initialize( $fileKey, $desiredDestName );
+ $this->initialize( $fileKey, $desiredDestName );
}
/**
@@ -140,7 +160,7 @@ class UploadFromStash extends UploadBase {
/**
* Remove a temporarily kept file stashed by saveTempUploadedFile().
- * @return success
+ * @return bool success
*/
public function unsaveUploadedFile() {
return $this->stash->removeFile( $this->mFileKey );
diff --git a/includes/upload/UploadFromUrl.php b/includes/upload/UploadFromUrl.php
index da772fe2..927c3cd9 100644
--- a/includes/upload/UploadFromUrl.php
+++ b/includes/upload/UploadFromUrl.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Backend for uploading files from a HTTP resource.
+ *
+ * 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
+ */
+
+/**
* Implements uploading from a HTTP resource.
*
* @ingroup Upload
@@ -14,11 +36,12 @@ class UploadFromUrl extends UploadBase {
/**
* Checks if the user is allowed to use the upload-by-URL feature. If the
- * user is allowed, pass on permissions checking to the parent.
+ * user is not allowed, return the name of the user right as a string. If
+ * the user is allowed, have the parent do further permissions checking.
*
* @param $user User
*
- * @return bool
+ * @return bool|string
*/
public static function isAllowed( $user ) {
if ( !$user->isAllowed( 'upload_by_url' ) ) {
@@ -37,6 +60,31 @@ class UploadFromUrl extends UploadBase {
}
/**
+ * Checks whether the URL is for an allowed host
+ *
+ * @param $url string
+ * @return bool
+ */
+ public static function isAllowedHost( $url ) {
+ global $wgCopyUploadsDomains;
+ if ( !count( $wgCopyUploadsDomains ) ) {
+ return true;
+ }
+ $parsedUrl = wfParseUrl( $url );
+ if ( !$parsedUrl ) {
+ return false;
+ }
+ $valid = false;
+ foreach( $wgCopyUploadsDomains as $domain ) {
+ if ( $parsedUrl['host'] === $domain ) {
+ $valid = true;
+ break;
+ }
+ }
+ return $valid;
+ }
+
+ /**
* Entry point for API upload
*
* @param $name string
@@ -44,6 +92,7 @@ class UploadFromUrl extends UploadBase {
* @param $async mixed Whether the download should be performed
* asynchronous. False for synchronous, async or async-leavemessage for
* asynchronous download.
+ * @throws MWException
*/
public function initialize( $name, $url, $async = false ) {
global $wgAllowAsyncCopyUploads;
@@ -68,7 +117,7 @@ class UploadFromUrl extends UploadBase {
if ( !$desiredDestName ) {
$desiredDestName = $request->getText( 'wpUploadFileURL' );
}
- return $this->initialize(
+ $this->initialize(
$desiredDestName,
trim( $request->getVal( 'wpUploadFileURL' ) ),
false
@@ -101,6 +150,9 @@ class UploadFromUrl extends UploadBase {
return Status::newFatal( 'http-invalid-url' );
}
+ if( !self::isAllowedHost( $this->mUrl ) ) {
+ return Status::newFatal( 'upload-copy-upload-invalid-domain' );
+ }
if ( !$this->mAsync ) {
return $this->reallyFetchFile();
}
@@ -155,9 +207,14 @@ class UploadFromUrl extends UploadBase {
$this->mRemoveTempFile = true;
$this->mFileSize = 0;
- $req = MWHttpRequest::factory( $this->mUrl, array(
+ $options = array(
'followRedirects' => true
- ) );
+ );
+ global $wgCopyUploadProxy;
+ if ( $wgCopyUploadProxy !== false ) {
+ $options['proxy'] = $wgCopyUploadProxy;
+ }
+ $req = MWHttpRequest::factory( $this->mUrl, $options );
$req->setCallback( array( $this, 'saveTempFileChunk' ) );
$status = $req->execute();
@@ -180,6 +237,7 @@ class UploadFromUrl extends UploadBase {
/**
* Wrapper around the parent function in order to defer verifying the
* upload until the file really has been fetched.
+ * @return array|mixed
*/
public function verifyUpload() {
if ( $this->mAsync ) {
@@ -191,6 +249,7 @@ class UploadFromUrl extends UploadBase {
/**
* Wrapper around the parent function in order to defer checking warnings
* until the file really has been fetched.
+ * @return Array
*/
public function checkWarnings() {
if ( $this->mAsync ) {
@@ -203,6 +262,8 @@ class UploadFromUrl extends UploadBase {
/**
* Wrapper around the parent function in order to defer checking protection
* until we are sure that the file can actually be uploaded
+ * @param $user User
+ * @return bool|mixed
*/
public function verifyTitlePermissions( $user ) {
if ( $this->mAsync ) {
@@ -214,6 +275,11 @@ class UploadFromUrl extends UploadBase {
/**
* Wrapper around the parent function in order to defer uploading to the
* job queue for asynchronous uploads
+ * @param $comment string
+ * @param $pageText string
+ * @param $watch bool
+ * @param $user User
+ * @return Status
*/
public function performUpload( $comment, $pageText, $watch, $user ) {
if ( $this->mAsync ) {
@@ -226,11 +292,11 @@ class UploadFromUrl extends UploadBase {
}
/**
- * @param $comment
- * @param $pageText
- * @param $watch
- * @param $user User
- * @return
+ * @param $comment
+ * @param $pageText
+ * @param $watch
+ * @param $user User
+ * @return String
*/
protected function insertJob( $comment, $pageText, $watch, $user ) {
$sessionKey = $this->stashSession();
diff --git a/includes/upload/UploadStash.php b/includes/upload/UploadStash.php
index ad153d2f..c7fd23a9 100644
--- a/includes/upload/UploadStash.php
+++ b/includes/upload/UploadStash.php
@@ -1,5 +1,27 @@
<?php
/**
+ * Temporary storage for uploaded files.
+ *
+ * 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
+ */
+
+/**
* UploadStash is intended to accomplish a few things:
* - enable applications to temporarily stash files without publishing them to the wiki.
* - Several parts of MediaWiki do this in similar ways: UploadBase, UploadWizard, and FirefoggChunkedExtension
@@ -46,9 +68,11 @@ class UploadStash {
/**
* Represents a temporary filestore, with metadata in the database.
- * Designed to be compatible with the session stashing code in UploadBase (should replace it eventually)
+ * Designed to be compatible with the session stashing code in UploadBase
+ * (should replace it eventually).
*
* @param $repo FileRepo
+ * @param $user User (default null)
*/
public function __construct( FileRepo $repo, $user = null ) {
// this might change based on wiki's configuration.
@@ -215,10 +239,14 @@ class UploadStash {
}
}
// at this point, $error should contain the single "most important" error, plus any parameters.
- throw new UploadStashFileException( "Error storing file in '$path': " . wfMessage( $error )->text() );
+ $errorMsg = array_shift( $error );
+ throw new UploadStashFileException( "Error storing file in '$path': " . wfMessage( $errorMsg, $error )->text() );
}
$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' );
@@ -391,6 +419,7 @@ 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...
+ * @return string
*/
public static function getExtensionForPath( $path ) {
// Does this have an extension?
@@ -419,6 +448,7 @@ class UploadStash {
* Helper function: do the actual database query to fetch file metadata.
*
* @param $key String: key
+ * @param $readFromDB: constant (default: DB_SLAVE)
* @return boolean
*/
protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) {
@@ -451,7 +481,6 @@ class UploadStash {
/**
* Helper function: Initialize the UploadStashFile for a given file.
*
- * @param $path String: path to file
* @param $key String: key under which to store the object
* @throws UploadStashZeroLengthFileException
* @return bool
@@ -498,7 +527,7 @@ class UploadStashFile extends UnregisteredLocalFile {
}
// check if path exists! and is a plain file.
- if ( ! $repo->fileExists( $path, FileRepo::FILES_ONLY ) ) {
+ if ( ! $repo->fileExists( $path ) ) {
wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not found\n" );
throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
}
@@ -543,16 +572,17 @@ class UploadStashFile extends UnregisteredLocalFile {
* ugly file name.
*
* @param $params Array: 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
*/
- function thumbName( $params ) {
+ function thumbName( $params, $flags = 0 ) {
return $this->generateThumbName( $this->getUrlName(), $params );
}
/**
* Helper function -- given a 'subpage', return the local URL e.g. /wiki/Special:UploadStash/subpage
- * @param {String} $subPage
- * @return {String} local URL for this subpage in the Special:UploadStash space.
+ * @param $subPage String
+ * @return String: local URL for this subpage in the Special:UploadStash space.
*/
private function getSpecialUrl( $subPage ) {
return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL();
@@ -622,7 +652,7 @@ class UploadStashFile extends UnregisteredLocalFile {
* @return Status: success
*/
public function remove() {
- if ( !$this->repo->fileExists( $this->path, FileRepo::FILES_ONLY ) ) {
+ if ( !$this->repo->fileExists( $this->path ) ) {
// Maybe the file's already been removed? This could totally happen in UploadBase.
return true;
}
@@ -631,7 +661,7 @@ class UploadStashFile extends UnregisteredLocalFile {
}
public function exists() {
- return $this->repo->fileExists( $this->path, FileRepo::FILES_ONLY );
+ return $this->repo->fileExists( $this->path );
}
}
diff --git a/includes/zhtable/Makefile.py b/includes/zhtable/Makefile.py
index 305422bd..fd603ce4 100644
--- a/includes/zhtable/Makefile.py
+++ b/includes/zhtable/Makefile.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @author Philip
import tarfile as tf
@@ -31,7 +31,7 @@ def unichr3( *args ):
# DEFINE
UNIHAN_VER = '5.2.0'
-SF_MIRROR = 'cdnetworks-kr-2'
+SF_MIRROR = 'dfn'
SCIM_TABLES_VER = '0.5.10'
SCIM_PINYIN_VER = '0.5.91'
LIBTABE_VER = '0.2.3'
@@ -39,7 +39,7 @@ LIBTABE_VER = '0.2.3'
def download( url, dest ):
if os.path.isfile( dest ):
- print( 'File %s up to date.' % dest )
+ print( 'File %s is up to date.' % dest )
return
global islinux
if islinux:
@@ -370,15 +370,15 @@ $zh2Hant = array(\n'''
+ PHPArray( toCN ) \
+ '\n);\n\n$zh2SG = array(\n' \
+ PHPArray( toSG ) \
- + '\n);'
+ + '\n);\n'
- f = open( 'ZhConversion.php', 'wb', encoding = 'utf8' )
+ f = open( os.path.join( '..', 'ZhConversion.php' ), 'wb', encoding = 'utf8' )
print ('Writing ZhConversion.php ... ')
f.write( php )
f.close()
- #Remove temp files
- print ('Deleting temp files ... ')
+ # Remove temporary files
+ print ('Deleting temporary files ... ')
os.remove('EZ-Big.txt.in')
os.remove('phrase_lib.txt')
os.remove('tsi.src')
diff --git a/includes/zhtable/simp2trad.manual b/includes/zhtable/simp2trad.manual
index eb5fa396..1b84f8e7 100644
--- a/includes/zhtable/simp2trad.manual
+++ b/includes/zhtable/simp2trad.manual
@@ -239,7 +239,6 @@ U+09E21鸡|U+096DE雞|U+09DC4鷄|
U+09E5A鹚|U+09DBF鶿|U+09DC0鷀|
U+09E6E鹮|U+04D09䴉|
U+09F44齄|U+09F47齇|
-U+0E82D|U+068E1棡|
U+20BB6𠮶|U+055F0嗰|
U+26216𦈖|U+04308䌈|
U+28C3E𨰾|U+093B7鎷|
diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual
index 41680d1f..243f61b0 100644
--- a/includes/zhtable/toCN.manual
+++ b/includes/zhtable/toCN.manual
@@ -9,7 +9,6 @@
乙太網 以太网
點陣圖 位图
常式 例程
-游標 光标
光碟 光盘
光碟機 光驱
全形 全角
diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual
index 2ebb7504..1f7fe7d0 100644
--- a/includes/zhtable/toHK.manual
+++ b/includes/zhtable/toHK.manual
@@ -2240,3 +2240,61 @@
分布于 分佈於
分布於 分佈於
想象 想像
+無線電視 無綫電視
+无线电视 無綫電視
+無線收費 無綫收費
+无线收费 無綫收費
+無線節目 無綫節目
+无线节目 無綫節目
+無線劇集 無綫劇集
+无线剧集 無綫劇集
+東鐵線 東鐵綫
+东铁线 東鐵綫
+觀塘線 觀塘綫
+观塘线 觀塘綫
+荃灣線 荃灣綫
+荃湾线 荃灣綫
+港島線 港島綫
+港岛线 港島綫
+東涌線 東涌綫
+东涌线 東涌綫
+將軍澳線 將軍澳綫
+将军澳线 將軍澳綫
+西鐵線 西鐵綫
+西铁线 西鐵綫
+馬鞍山線 馬鞍山綫
+马鞍山线 馬鞍山綫
+迪士尼線 迪士尼綫
+迪士尼线 迪士尼綫
+沙田至中環線 沙田至中環綫
+沙田至中环线 沙田至中環綫
+沙中線 沙中綫
+沙中线 沙中綫
+北環線 北環綫
+北环线 北環綫
+機場快線 機場快綫
+机场快线 機場快綫
+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/toTW.manual b/includes/zhtable/toTW.manual
index 35b62689..1a14e99a 100644
--- a/includes/zhtable/toTW.manual
+++ b/includes/zhtable/toTW.manual
@@ -408,3 +408,4 @@
想象 想像
锎 鉲
信道 信道
+綫 線
diff --git a/includes/zhtable/trad2simp.manual b/includes/zhtable/trad2simp.manual
index 692c74b5..7c3ce10d 100644
--- a/includes/zhtable/trad2simp.manual
+++ b/includes/zhtable/trad2simp.manual
@@ -43,7 +43,6 @@ U+065E3旣|U+065E2既|
U+06607昇|U+05347升|
U+0672E朮|U+0672F术|
U+068CA棊|U+068CB棋|
-U+068E1棡|U+0E82D|
U+069A6榦|U+05E72干|
U+069D3槓|U+06760杠|
U+06A11樑|U+06881梁|
diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual
index ee3bc69f..9a9534f8 100644
--- a/includes/zhtable/tradphrases.manual
+++ b/includes/zhtable/tradphrases.manual
@@ -3032,6 +3032,7 @@
細如髮
繫於一髮
膚髮
+皮膚
生華髮
蒼髮
被髮佯狂
@@ -3501,6 +3502,7 @@
藍澱
皆可作澱
澱山
+海淀山後
澱澱
掛鈎
薴悴
@@ -3982,6 +3984,7 @@
棺材裡
注釋
月面
+路面
修杰楷
修杰麟
學裡