From: Tim Starling
Date: Mon, 16 Jun 2008 20:21:26 +0000 (+0000)
Subject: * Reorganised the includes directory, creating subdirectories db, parser and specials
X-Git-Tag: 1.31.0-rc.0~46992
X-Git-Url: https://git.cyclocoop.org/%27.WWW_URL.%27admin/%22%20.%20generer_url_entite%28%24id_rubrique2%2C?a=commitdiff_plain;h=c6b902f180;p=lhc%2Fweb%2Fwiklou.git
* Reorganised the includes directory, creating subdirectories db, parser and specials
* Wrote a tool to check the integrity of the autoloader class list, fixed some issues that came up.
* Start the autoloader before LocalSettings.php, so that when an extension writer thinks an inefficient one-file special page extension is the way to go, they don't have to use explicit includes to make the class inheritance work. Should continue to work with $IP set in LocalSettings.php as long as $IP is set before extensions are included.
---
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 1e0fd3779c..a823057d14 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -4,18 +4,14 @@
ini_set('unserialize_callback_func', '__autoload' );
-function __autoload($className) {
- global $wgAutoloadClasses;
-
+class AutoLoader {
# Locations of core classes
# Extension classes are specified with $wgAutoloadClasses
static $localClasses = array(
# Includes
- 'AjaxCachePolicy' => 'includes/AjaxFunctions.php',
'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
'AjaxResponse' => 'includes/AjaxResponse.php',
'AlphabeticPager' => 'includes/Pager.php',
- 'AncientPagesPage' => 'includes/SpecialAncientpages.php',
'APCBagOStuff' => 'includes/BagOStuff.php',
'ArrayDiffFormatter' => 'includes/DifferenceEngine.php',
'Article' => 'includes/Article.php',
@@ -24,29 +20,16 @@ function __autoload($className) {
'Autopromote' => 'includes/Autopromote.php',
'BagOStuff' => 'includes/BagOStuff.php',
'Block' => 'includes/Block.php',
- 'BrokenRedirectsPage' => 'includes/SpecialBrokenRedirects.php',
+ 'CacheDependency' => 'includes/CacheDependency.php',
'Category' => 'includes/Category.php',
'Categoryfinder' => 'includes/Categoryfinder.php',
'CategoryPage' => 'includes/CategoryPage.php',
'CategoryViewer' => 'includes/CategoryPage.php',
'ChangesList' => 'includes/ChangesList.php',
'ChannelFeed' => 'includes/Feed.php',
- 'ChronologyProtector' => 'includes/LBFactory.php',
'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
- 'ContributionsPage' => 'includes/SpecialContributions.php',
- 'CoreParserFunctions' => 'includes/CoreParserFunctions.php',
- 'Database' => 'includes/Database.php',
- 'DatabaseMysql' => 'includes/Database.php',
- 'DatabaseOracle' => 'includes/DatabaseOracle.php',
- 'DatabasePostgres' => 'includes/DatabasePostgres.php',
- 'DatabaseSqlite' => 'includes/DatabaseSqlite.php',
- 'DatabaseMssql' => 'includes/DatabaseMssql.php',
- 'DateFormatter' => 'includes/DateFormatter.php',
+ 'ConstantDependency' => 'includes/CacheDependency.php',
'DBABagOStuff' => 'includes/BagOStuff.php',
- 'DBLockForm' => 'includes/SpecialLockdb.php',
- 'DBObject' => 'includes/Database.php',
- 'DBUnlockForm' => 'includes/SpecialUnlockdb.php',
- 'DeadendPagesPage' => 'includes/SpecialDeadendpages.php',
'DependencyWrapper' => 'includes/CacheDependency.php',
'_DiffEngine' => 'includes/DifferenceEngine.php',
'DifferenceEngine' => 'includes/DifferenceEngine.php',
@@ -57,9 +40,7 @@ function __autoload($className) {
'_DiffOp_Copy' => 'includes/DifferenceEngine.php',
'_DiffOp_Delete' => 'includes/DifferenceEngine.php',
'_DiffOp' => 'includes/DifferenceEngine.php',
- 'DisambiguationsPage' => 'includes/SpecialDisambiguations.php',
'DjVuImage' => 'includes/DjVuImage.php',
- 'DoubleRedirectsPage' => 'includes/SpecialDoubleRedirects.php',
'DoubleReplacer' => 'includes/StringUtils.php',
'Dump7ZipOutput' => 'includes/Export.php',
'DumpBZip2Output' => 'includes/Export.php',
@@ -74,33 +55,29 @@ function __autoload($className) {
'DumpPipeOutput' => 'includes/Export.php',
'eAccelBagOStuff' => 'includes/BagOStuff.php',
'EditPage' => 'includes/EditPage.php',
- 'EmailConfirmation' => 'includes/SpecialConfirmemail.php',
- 'EmailInvalidation' => 'includes/SpecialConfirmemail.php',
'EmaillingJob' => 'includes/EmaillingJob.php',
- 'EmaillingJob' => 'includes/JobQueue.php',
'EmailNotification' => 'includes/UserMailer.php',
- 'EmailUserForm' => 'includes/SpecialEmailuser.php',
'EnhancedChangesList' => 'includes/ChangesList.php',
'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php',
+ 'ErrorPageError' => 'includes/Exception.php',
'Exif' => 'includes/Exif.php',
'ExternalEdit' => 'includes/ExternalEdit.php',
'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php',
'ExternalStore' => 'includes/ExternalStore.php',
- 'FakeMemCachedClient' => 'includes/ObjectCache.php',
+ 'FatalError' => 'includes/Exception.php',
'FakeTitle' => 'includes/FakeTitle.php',
'FauxRequest' => 'includes/WebRequest.php',
'FeedItem' => 'includes/Feed.php',
- 'FewestrevisionsPage' => 'includes/SpecialFewestrevisions.php',
'FileDeleteForm' => 'includes/FileDeleteForm.php',
'FileDependency' => 'includes/CacheDependency.php',
- 'FileDuplicateSearch' => 'includes/SpecialFileDuplicateSearch.php',
'FileRevertForm' => 'includes/FileRevertForm.php',
'FileStore' => 'includes/FileStore.php',
'FormatExif' => 'includes/Exif.php',
'FormOptions' => 'includes/FormOptions.php',
'FSException' => 'includes/FileStore.php',
'FSTransaction' => 'includes/FileStore.php',
+ 'GlobalDependency' => 'includes/CacheDependency.php',
'HashBagOStuff' => 'includes/BagOStuff.php',
'HashtableReplacer' => 'includes/StringUtils.php',
'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
@@ -115,17 +92,10 @@ function __autoload($className) {
'ImageHistoryList' => 'includes/ImagePage.php',
'ImagePage' => 'includes/ImagePage.php',
'ImageQueryPage' => 'includes/ImageQueryPage.php',
- 'ImportStreamSource' => 'includes/SpecialImport.php',
- 'ImportStringSource' => 'includes/SpecialImport.php',
'IncludableSpecialPage' => 'includes/SpecialPage.php',
'IndexPager' => 'includes/Pager.php',
- 'IPBlockForm' => 'includes/SpecialBlockip.php',
'IP' => 'includes/IP.php',
- 'IPUnblockForm' => 'includes/SpecialIpblocklist.php',
'Job' => 'includes/JobQueue.php',
- 'LBFactory' => 'includes/LBFactory.php',
- 'LBFactory_Multi' => 'includes/LBFactory_Multi.php',
- 'LBFactory_Simple' => 'includes/LBFactory.php',
'License' => 'includes/Licenses.php',
'Licenses' => 'includes/Licenses.php',
'LinkBatch' => 'includes/LinkBatch.php',
@@ -133,15 +103,11 @@ function __autoload($className) {
'Linker' => 'includes/Linker.php',
'LinkFilter' => 'includes/LinkFilter.php',
'LinksUpdate' => 'includes/LinksUpdate.php',
- 'ListredirectsPage' => 'includes/SpecialListredirects.php',
- 'LoadBalancer' => 'includes/LoadBalancer.php',
- 'LoginForm' => 'includes/SpecialUserlogin.php',
'LogPage' => 'includes/LogPage.php',
+ 'LogPager' => 'includes/LogEventsList.php',
'LogEventsList' => 'includes/LogEventsList.php',
'LogReader' => 'includes/LogEventsList.php',
'LogViewer' => 'includes/LogEventsList.php',
- 'LonelyPagesPage' => 'includes/SpecialLonelypages.php',
- 'LongPagesPage' => 'includes/SpecialLongpages.php',
'MacBinary' => 'includes/MacBinary.php',
'MagicWordArray' => 'includes/MagicWord.php',
'MagicWord' => 'includes/MagicWord.php',
@@ -156,55 +122,29 @@ function __autoload($className) {
'memcached' => 'includes/memcached-client.php',
'MessageCache' => 'includes/MessageCache.php',
'MimeMagic' => 'includes/MimeMagic.php',
- 'MIMEsearchPage' => 'includes/SpecialMIMEsearch.php',
- 'MostcategoriesPage' => 'includes/SpecialMostcategories.php',
- 'MostimagesPage' => 'includes/SpecialMostimages.php',
- 'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php',
- 'MostlinkedPage' => 'includes/SpecialMostlinked.php',
- 'MostrevisionsPage' => 'includes/SpecialMostrevisions.php',
- 'MovePageForm' => 'includes/SpecialMovepage.php',
'MWException' => 'includes/Exception.php',
'MWNamespace' => 'includes/Namespace.php',
'MySQLSearchResultSet' => 'includes/SearchMySQL.php',
- 'MySQLMasterPos' => 'includes/Database.php',
'Namespace' => 'includes/NamespaceCompat.php', // Compat
- 'NewbieContributionsPage' => 'includes/SpecialNewbieContributions.php',
- 'NewPagesPage' => 'includes/SpecialNewpages.php',
'OldChangesList' => 'includes/ChangesList.php',
+ 'OracleSearchResultSet' => 'includes/SearchOracle.php',
'OutputPage' => 'includes/OutputPage.php',
- 'PageArchive' => 'includes/SpecialUndelete.php',
'PageHistory' => 'includes/PageHistory.php',
+ 'PageHistoryPager' => 'includes/PageHistory.php',
'PageQueryPage' => 'includes/PageQueryPage.php',
- 'ParserCache' => 'includes/ParserCache.php',
- 'Parser_DiffTest' => 'includes/Parser_DiffTest.php',
- 'Parser' => 'includes/Parser.php',
- 'Parser_OldPP' => 'includes/Parser_OldPP.php',
- 'ParserOptions' => 'includes/ParserOptions.php',
- 'ParserOutput' => 'includes/ParserOutput.php',
- 'PasswordResetForm' => 'includes/SpecialResetpass.php',
+ 'Pager' => 'includes/Pager.php',
+ 'PasswordError' => 'includes/User.php',
'PatrolLog' => 'includes/PatrolLog.php',
- 'PopularPagesPage' => 'includes/SpecialPopularpages.php',
- 'PPDStackElement' => 'includes/Preprocessor_DOM.php',
- 'PPDStack' => 'includes/Preprocessor_DOM.php',
- 'PPFrame_DOM' => 'includes/Preprocessor_DOM.php',
- 'PPFrame' => 'includes/Preprocessor.php',
- 'PPNode_DOM' => 'includes/Preprocessor_DOM.php',
- 'PPNode' => 'includes/Preprocessor.php',
- 'PPTemplateFrame_DOM' => 'includes/Preprocessor_DOM.php',
- 'PreferencesForm' => 'includes/SpecialPreferences.php',
+ 'PostgresSearchResult' => 'includes/SearchPostgres.php',
+ 'PostgresSearchResultSet' => 'includes/SearchPostgres.php',
'PrefixSearch' => 'includes/PrefixSearch.php',
- 'Preprocessor_DOM' => 'includes/Preprocessor_DOM.php',
- 'Preprocessor_Hash' => 'includes/Preprocessor_Hash.php',
- 'Preprocessor' => 'includes/Preprocessor.php',
'Profiler' => 'includes/Profiler.php',
'ProfilerSimple' => 'includes/ProfilerSimple.php',
'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php',
'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php',
'ProtectionForm' => 'includes/ProtectionForm.php',
- 'ProxyTools' => 'includes/ProxyTools.php',
'QueryPage' => 'includes/QueryPage.php',
'QuickTemplate' => 'includes/SkinTemplate.php',
- 'RandomPage' => 'includes/SpecialRandompage.php',
'RawPage' => 'includes/RawPage.php',
'RCCacheEntry' => 'includes/ChangesList.php',
'RecentChange' => 'includes/RecentChange.php',
@@ -212,38 +152,32 @@ function __autoload($className) {
'RegexlikeReplacer' => 'includes/StringUtils.php',
'ReplacementArray' => 'includes/StringUtils.php',
'Replacer' => 'includes/StringUtils.php',
- 'ResultWrapper' => 'includes/Database.php',
'ReverseChronologicalPager' => 'includes/Pager.php',
- 'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php',
- 'RevisionDeleter' => 'includes/SpecialRevisiondelete.php',
'Revision' => 'includes/Revision.php',
'RSSFeed' => 'includes/Feed.php',
'Sanitizer' => 'includes/Sanitizer.php',
'SearchEngineDummy' => 'includes/SearchEngine.php',
'SearchEngine' => 'includes/SearchEngine.php',
+ 'SearchHighlighter' => 'includes/SearchEngine.php',
'SearchMySQL4' => 'includes/SearchMySQL4.php',
'SearchMySQL' => 'includes/SearchMySQL.php',
'SearchOracle' => 'includes/SearchOracle.php',
'SearchPostgres' => 'includes/SearchPostgres.php',
'SearchResult' => 'includes/SearchEngine.php',
'SearchResultSet' => 'includes/SearchEngine.php',
+ 'SearchResultTooMany' => 'includes/SearchEngine.php',
'SearchUpdate' => 'includes/SearchUpdate.php',
'SearchUpdateMyISAM' => 'includes/SearchUpdate.php',
- 'ShortPagesPage' => 'includes/SpecialShortpages.php',
'SiteConfiguration' => 'includes/SiteConfiguration.php',
'SiteStats' => 'includes/SiteStats.php',
'SiteStatsUpdate' => 'includes/SiteStats.php',
'Skin' => 'includes/Skin.php',
'SkinTemplate' => 'includes/SkinTemplate.php',
- 'SpecialAllpages' => 'includes/SpecialAllpages.php',
- 'SpecialBookSources' => 'includes/SpecialBooksources.php',
- 'SpecialListGroupRights' => 'includes/SpecialListgrouprights.php',
- 'SpecialMostlinkedtemplates' => 'includes/SpecialMostlinkedtemplates.php',
+ 'SpecialMycontributions' => 'includes/SpecialPage.php',
+ 'SpecialMypage' => 'includes/SpecialPage.php',
+ 'SpecialMytalk' => 'includes/SpecialPage.php',
'SpecialPage' => 'includes/SpecialPage.php',
- 'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php',
- 'SpecialRandomredirect' => 'includes/SpecialRandomredirect.php',
- 'SpecialSearch' => 'includes/SpecialSearch.php',
- 'SpecialVersion' => 'includes/SpecialVersion.php',
+ 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php',
'SqlBagOStuff' => 'includes/BagOStuff.php',
'SquidUpdate' => 'includes/SquidUpdate.php',
'Status' => 'includes/Status.php',
@@ -256,38 +190,21 @@ function __autoload($className) {
'TitleListDependency' => 'includes/CacheDependency.php',
'TransformParameterError' => 'includes/MediaTransformOutput.php',
'TurckBagOStuff' => 'includes/BagOStuff.php',
- 'UncategorizedCategoriesPage' => 'includes/SpecialUncategorizedcategories.php',
- 'UncategorizedPagesPage' => 'includes/SpecialUncategorizedpages.php',
- 'UncategorizedTemplatesPage' => 'includes/SpecialUncategorizedtemplates.php',
- 'UndeleteForm' => 'includes/SpecialUndelete.php',
'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php',
'UnlistedSpecialPage' => 'includes/SpecialPage.php',
- 'UnusedCategoriesPage' => 'includes/SpecialUnusedcategories.php',
- 'UnusedimagesPage' => 'includes/SpecialUnusedimages.php',
- 'UnusedtemplatesPage' => 'includes/SpecialUnusedtemplates.php',
- 'UnwatchedpagesPage' => 'includes/SpecialUnwatchedpages.php',
- 'UploadForm' => 'includes/SpecialUpload.php',
- 'UploadFormMogile' => 'includes/SpecialUploadMogile.php',
'User' => 'includes/User.php',
'UserArray' => 'includes/UserArray.php',
'UserArrayFromResult' => 'includes/UserArray.php',
'UserMailer' => 'includes/UserMailer.php',
- 'UserrightsPage' => 'includes/SpecialUserrights.php',
'UserRightsProxy' => 'includes/UserRightsProxy.php',
- 'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php',
- 'WantedPagesPage' => 'includes/SpecialWantedpages.php',
'WatchedItem' => 'includes/WatchedItem.php',
'WatchlistEditor' => 'includes/WatchlistEditor.php',
'WebRequest' => 'includes/WebRequest.php',
'WebResponse' => 'includes/WebResponse.php',
- 'WhatLinksHerePage' => 'includes/SpecialWhatlinkshere.php',
'WikiError' => 'includes/WikiError.php',
'WikiErrorMsg' => 'includes/WikiError.php',
'WikiExporter' => 'includes/Export.php',
- 'WikiImporter' => 'includes/SpecialImport.php',
- 'WikiRevision' => 'includes/SpecialImport.php',
'WikiXmlError' => 'includes/WikiError.php',
- 'WithoutInterwikiPage' => 'includes/SpecialWithoutinterwiki.php',
'WordLevelDiff' => 'includes/DifferenceEngine.php',
'XCacheBagOStuff' => 'includes/BagOStuff.php',
'XmlDumpWriter' => 'includes/Export.php',
@@ -296,46 +213,11 @@ function __autoload($className) {
'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
'ZhClient' => 'includes/ZhClient.php',
- # filerepo
- 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
- 'File' => 'includes/filerepo/File.php',
- 'FileRepo' => 'includes/filerepo/FileRepo.php',
- 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
- 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
- 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
- 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
- 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
- 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
- 'FSRepo' => 'includes/filerepo/FSRepo.php',
- 'Image' => 'includes/filerepo/Image.php',
- 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
- 'LocalFile' => 'includes/filerepo/LocalFile.php',
- 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
- 'LocalRepo' => 'includes/filerepo/LocalRepo.php',
- 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
- 'RepoGroup' => 'includes/filerepo/RepoGroup.php',
- 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
-
- # Media
- 'BitmapHandler' => 'includes/media/Bitmap.php',
- 'BmpHandler' => 'includes/media/BMP.php',
- 'DjVuHandler' => 'includes/media/DjVu.php',
- 'ImageHandler' => 'includes/media/Generic.php',
- 'MediaHandler' => 'includes/media/Generic.php',
- 'SvgHandler' => 'includes/media/SVG.php',
-
- # Normal
- 'UtfNormal' => 'includes/normal/UtfNormal.php',
-
- # Templates
- 'UsercreateTemplate' => 'includes/templates/Userlogin.php',
- 'UserloginTemplate' => 'includes/templates/Userlogin.php',
-
- # Languages
- 'Language' => 'languages/Language.php',
-
- # API
+ # includes/api
'ApiBase' => 'includes/api/ApiBase.php',
+ 'ApiBlock' => 'includes/api/ApiBlock.php',
+ 'ApiDelete' => 'includes/api/ApiDelete.php',
+ 'ApiEditPage' => 'includes/api/ApiEditPage.php',
'ApiEmailUser' => 'includes/api/ApiEmailUser.php',
'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php',
'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php',
@@ -352,28 +234,32 @@ function __autoload($className) {
'ApiLogin' => 'includes/api/ApiLogin.php',
'ApiLogout' => 'includes/api/ApiLogout.php',
'ApiMain' => 'includes/api/ApiMain.php',
+ 'ApiMove' => 'includes/api/ApiMove.php',
'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
'ApiPageSet' => 'includes/api/ApiPageSet.php',
'ApiParamInfo' => 'includes/api/ApiParamInfo.php',
'ApiParse' => 'includes/api/ApiParse.php',
- 'ApiQueryAllImages' => 'includes/api/ApiQueryAllimages.php',
+ 'ApiProtect' => 'includes/api/ApiProtect.php',
+ 'ApiQuery' => 'includes/api/ApiQuery.php',
'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
+ 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php',
'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
+ 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.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',
+ 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php',
'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php',
- 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php',
+ 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php',
- 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
+ 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php',
+ 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php',
'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php',
'ApiQueryImages' => 'includes/api/ApiQueryImages.php',
- 'ApiQuery' => 'includes/api/ApiQuery.php',
'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php',
'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php',
'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php',
@@ -387,67 +273,247 @@ function __autoload($className) {
'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
'ApiResult' => 'includes/api/ApiResult.php',
- 'Services_JSON' => 'includes/api/ApiFormatJson_json.php',
- 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
-
- # apiedit branch
- 'ApiBlock' => 'includes/api/ApiBlock.php',
- 'ApiDelete' => 'includes/api/ApiDelete.php',
- 'ApiEditPage' => 'includes/api/ApiEditPage.php',
- 'ApiMove' => 'includes/api/ApiMove.php',
- 'ApiProtect' => 'includes/api/ApiProtect.php',
- 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php',
- 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
'ApiRollback' => 'includes/api/ApiRollback.php',
'ApiUnblock' => 'includes/api/ApiUnblock.php',
'ApiUndelete' => 'includes/api/ApiUndelete.php',
+ 'Services_JSON' => 'includes/api/ApiFormatJson_json.php',
+ 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php',
+ 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
+ 'UsageException' => 'includes/api/ApiMain.php',
+ 'YAMLNode' => 'includes/api/ApiFormatYaml_spyc.php',
+
+ # includes/db
+ 'Blob' => 'includes/db/Database.php',
+ 'ChronologyProtector' => 'includes/db/LBFactory.php',
+ 'Database' => 'includes/db/Database.php',
+ 'DatabaseMssql' => 'includes/db/DatabaseMssql.php',
+ 'DatabaseMysql' => 'includes/db/Database.php',
+ 'DatabaseOracle' => 'includes/db/DatabaseOracle.php',
+ 'DatabasePostgres' => 'includes/db/DatabasePostgres.php',
+ 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
+ 'DBConnectionError' => 'includes/db/Database.php',
+ 'DBError' => 'includes/db/Database.php',
+ 'DBObject' => 'includes/db/Database.php',
+ 'DBQueryError' => 'includes/db/Database.php',
+ 'DBUnexpectedError' => 'includes/db/Database.php',
+ 'LBFactory' => 'includes/db/LBFactory.php',
+ 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
+ 'LBFactory_Simple' => 'includes/db/LBFactory.php',
+ 'LoadBalancer' => 'includes/db/LoadBalancer.php',
+ 'MSSQLField' => 'includes/db/DatabaseMssql.php',
+ 'MySQLField' => 'includes/db/Database.php',
+ 'MySQLMasterPos' => 'includes/db/Database.php',
+ 'ORABlob' => 'includes/db/DatabaseOracle.php',
+ 'ORAResult' => 'includes/db/DatabaseOracle.php',
+ 'PostgresField' => 'includes/db/DatabasePostgres.php',
+ 'ResultWrapper' => 'includes/db/Database.php',
+ 'SQLiteField' => 'includes/db/DatabaseSqlite.php',
+
+ # includes/filerepo
+ 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
+ 'File' => 'includes/filerepo/File.php',
+ 'FileRepo' => 'includes/filerepo/FileRepo.php',
+ 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
+ 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
+ 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
+ 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
+ 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
+ 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
+ 'FSRepo' => 'includes/filerepo/FSRepo.php',
+ 'Image' => 'includes/filerepo/Image.php',
+ 'LocalFile' => 'includes/filerepo/LocalFile.php',
+ 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
+ 'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php',
+ 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
+ 'LocalRepo' => 'includes/filerepo/LocalRepo.php',
+ 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
+ 'RepoGroup' => 'includes/filerepo/RepoGroup.php',
+ 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
+
+ # includes/media
+ 'BitmapHandler' => 'includes/media/Bitmap.php',
+ 'BmpHandler' => 'includes/media/BMP.php',
+ 'DjVuHandler' => 'includes/media/DjVu.php',
+ 'ImageHandler' => 'includes/media/Generic.php',
+ 'MediaHandler' => 'includes/media/Generic.php',
+ 'SvgHandler' => 'includes/media/SVG.php',
+
+ # includes/normal
+ 'UtfNormal' => 'includes/normal/UtfNormal.php',
+
+ # includes/parser
+ 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php',
+ 'DateFormatter' => 'includes/parser/DateFormatter.php',
+ 'OnlyIncludeReplacer' => 'includes/parser/Parser.php',
+ 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPDPart' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPDStack' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPFrame' => 'includes/parser/Preprocessor.php',
+ 'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode' => 'includes/parser/Preprocessor.php',
+ 'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'Parser' => 'includes/parser/Parser.php',
+ 'ParserCache' => 'includes/parser/ParserCache.php',
+ 'ParserOptions' => 'includes/parser/ParserOptions.php',
+ 'ParserOutput' => 'includes/parser/ParserOutput.php',
+ 'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php',
+ 'Parser_OldPP' => 'includes/parser/Parser_OldPP.php',
+ 'Preprocessor' => 'includes/parser/Preprocessor.php',
+ 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'StripState' => 'includes/parser/Parser.php',
+
+ # includes/specials
+ 'AncientPagesPage' => 'includes/specials/Ancientpages.php',
+ 'BrokenRedirectsPage' => 'includes/specials/BrokenRedirects.php',
+ 'ContribsPager' => 'includes/specials/Contributions.php',
+ 'DBLockForm' => 'includes/specials/Lockdb.php',
+ 'DBUnlockForm' => 'includes/specials/Unlockdb.php',
+ 'DeadendPagesPage' => 'includes/specials/Deadendpages.php',
+ 'DisambiguationsPage' => 'includes/specials/Disambiguations.php',
+ 'DoubleRedirectsPage' => 'includes/specials/DoubleRedirects.php',
+ 'EmailConfirmation' => 'includes/specials/Confirmemail.php',
+ 'EmailInvalidation' => 'includes/specials/Confirmemail.php',
+ 'EmailUserForm' => 'includes/specials/Emailuser.php',
+ 'FewestrevisionsPage' => 'includes/specials/Fewestrevisions.php',
+ 'FileDuplicateSearchPage' => 'includes/specials/FileDuplicateSearch.php',
+ 'IPBlockForm' => 'includes/specials/Blockip.php',
+ 'IPBlocklistPager' => 'includes/specials/Ipblocklist.php',
+ 'IPUnblockForm' => 'includes/specials/Ipblocklist.php',
+ 'ImportReporter' => 'includes/specials/Import.php',
+ 'ImportStreamSource' => 'includes/specials/Import.php',
+ 'ImportStringSource' => 'includes/specials/Import.php',
+ 'ListredirectsPage' => 'includes/specials/Listredirects.php',
+ 'LoginForm' => 'includes/specials/Userlogin.php',
+ 'LonelyPagesPage' => 'includes/specials/Lonelypages.php',
+ 'LongPagesPage' => 'includes/specials/Longpages.php',
+ 'MIMEsearchPage' => 'includes/specials/MIMEsearch.php',
+ 'MostcategoriesPage' => 'includes/specials/Mostcategories.php',
+ 'MostimagesPage' => 'includes/specials/Mostimages.php',
+ 'MostlinkedCategoriesPage' => 'includes/specials/Mostlinkedcategories.php',
+ 'MostlinkedPage' => 'includes/specials/Mostlinked.php',
+ 'MostrevisionsPage' => 'includes/specials/Mostrevisions.php',
+ 'MovePageForm' => 'includes/specials/Movepage.php',
+ 'NewPagesForm' => 'includes/specials/Newpages.php',
+ 'NewPagesPager' => 'includes/specials/Newpages.php',
+ 'PageArchive' => 'includes/specials/Undelete.php',
+ 'PasswordResetForm' => 'includes/specials/Resetpass.php',
+ 'PopularPagesPage' => 'includes/specials/Popularpages.php',
+ 'PreferencesForm' => 'includes/specials/Preferences.php',
+ 'RandomPage' => 'includes/specials/Randompage.php',
+ 'RevisionDeleteForm' => 'includes/specials/Revisiondelete.php',
+ 'RevisionDeleter' => 'includes/specials/Revisiondelete.php',
+ 'ShortPagesPage' => 'includes/specials/Shortpages.php',
+ 'SpecialAllpages' => 'includes/specials/Allpages.php',
+ 'SpecialBookSources' => 'includes/specials/Booksources.php',
+ 'SpecialListGroupRights' => 'includes/specials/Listgrouprights.php',
+ 'SpecialMostlinkedtemplates' => 'includes/specials/Mostlinkedtemplates.php',
+ 'SpecialPrefixindex' => 'includes/specials/Prefixindex.php',
+ 'SpecialRandomredirect' => 'includes/specials/Randomredirect.php',
+ 'SpecialSearch' => 'includes/specials/Search.php',
+ 'SpecialVersion' => 'includes/specials/Version.php',
+ 'UncategorizedCategoriesPage' => 'includes/specials/Uncategorizedcategories.php',
+ 'UncategorizedPagesPage' => 'includes/specials/Uncategorizedpages.php',
+ 'UncategorizedTemplatesPage' => 'includes/specials/Uncategorizedtemplates.php',
+ 'UndeleteForm' => 'includes/specials/Undelete.php',
+ 'UnusedCategoriesPage' => 'includes/specials/Unusedcategories.php',
+ 'UnusedimagesPage' => 'includes/specials/Unusedimages.php',
+ 'UnusedtemplatesPage' => 'includes/specials/Unusedtemplates.php',
+ 'UnwatchedpagesPage' => 'includes/specials/Unwatchedpages.php',
+ 'UploadForm' => 'includes/specials/Upload.php',
+ 'UploadFormMogile' => 'includes/specials/UploadMogile.php',
+ 'UserrightsPage' => 'includes/specials/Userrights.php',
+ 'UsersPager' => 'includes/specials/Listusers.php',
+ 'WantedCategoriesPage' => 'includes/specials/Wantedcategories.php',
+ 'WantedPagesPage' => 'includes/specials/Wantedpages.php',
+ 'WhatLinksHerePage' => 'includes/specials/Whatlinkshere.php',
+ 'WikiImporter' => 'includes/specials/Import.php',
+ 'WikiRevision' => 'includes/specials/Import.php',
+ 'WithoutInterwikiPage' => 'includes/specials/Withoutinterwiki.php',
+
+ # includes/templates
+ 'UsercreateTemplate' => 'includes/templates/Userlogin.php',
+ 'UserloginTemplate' => 'includes/templates/Userlogin.php',
+
+ # languages
+ 'Language' => 'languages/Language.php',
+ 'FakeConverter' => 'languages/Language.php',
+
);
- wfProfileIn( __METHOD__ );
- if ( isset( $localClasses[$className] ) ) {
- $filename = $localClasses[$className];
- } elseif ( isset( $wgAutoloadClasses[$className] ) ) {
- $filename = $wgAutoloadClasses[$className];
- } else {
- # Try a different capitalisation
- # The case can sometimes be wrong when unserializing PHP 4 objects
- $filename = false;
- $lowerClass = strtolower( $className );
- foreach ( $localClasses as $class2 => $file2 ) {
- if ( strtolower( $class2 ) == $lowerClass ) {
- $filename = $file2;
+ static function autoload( $className ) {
+ global $wgAutoloadClasses;
+
+ wfProfileIn( __METHOD__ );
+ if ( isset( self::$localClasses[$className] ) ) {
+ $filename = self::$localClasses[$className];
+ } elseif ( isset( $wgAutoloadClasses[$className] ) ) {
+ $filename = $wgAutoloadClasses[$className];
+ } else {
+ # Try a different capitalisation
+ # The case can sometimes be wrong when unserializing PHP 4 objects
+ $filename = false;
+ $lowerClass = strtolower( $className );
+ foreach ( self::$localClasses as $class2 => $file2 ) {
+ if ( strtolower( $class2 ) == $lowerClass ) {
+ $filename = $file2;
+ }
+ }
+ if ( !$filename ) {
+ # Give up
+ wfProfileOut( __METHOD__ );
+ return;
}
}
- if ( !$filename ) {
- # Give up
- wfProfileOut( __METHOD__ );
- return;
+
+ # Make an absolute path, this improves performance by avoiding some stat calls
+ if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) {
+ global $IP;
+ $filename = "$IP/$filename";
}
+ require( $filename );
+ wfProfileOut( __METHOD__ );
}
- # Make an absolute path, this improves performance by avoiding some stat calls
- if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) {
- global $IP;
- $filename = "$IP/$filename";
+ static function loadAllExtensions() {
+ global $wgAutoloadClasses;
+
+ # It is crucial that SpecialPage.php is included before any special page
+ # extensions are loaded. Otherwise the parent class will not be available
+ # when APC loads the early-bound extension class. Normally this is
+ # guaranteed by entering special pages via SpecialPage members such as
+ # executePath(), but here we have to take a more explicit measure.
+
+ require_once( dirname(__FILE__) . '/SpecialPage.php' );
+
+ foreach( $wgAutoloadClasses as $class => $file ) {
+ if( !( class_exists( $class ) || interface_exists( $class ) ) ) {
+ require( $file );
+ }
+ }
}
- require( $filename );
- wfProfileOut( __METHOD__ );
}
function wfLoadAllExtensions() {
- global $wgAutoloadClasses;
-
- # It is crucial that SpecialPage.php is included before any special page
- # extensions are loaded. Otherwise the parent class will not be available
- # when APC loads the early-bound extension class. Normally this is
- # guaranteed by entering special pages via SpecialPage members such as
- # executePath(), but here we have to take a more explicit measure.
-
- require_once( dirname(__FILE__) . '/SpecialPage.php' );
+ AutoLoader::loadAllExtensions();
+}
- foreach( $wgAutoloadClasses as $class => $file ) {
- if( !( class_exists( $class ) || interface_exists( $class ) ) ) {
- require( $file );
- }
+if ( function_exists( 'spl_autoload_register' ) ) {
+ spl_autoload_register( array( 'AutoLoader', 'autoload' ) );
+} else {
+ function __autoload( $class ) {
+ AutoLoader::autoload( $class );
}
}
+
diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php
deleted file mode 100644
index d9072e9310..0000000000
--- a/includes/CoreParserFunctions.php
+++ /dev/null
@@ -1,385 +0,0 @@
-setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'ns', array( __CLASS__, 'ns' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'urlencode', array( __CLASS__, 'urlencode' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'lcfirst', array( __CLASS__, 'lcfirst' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'ucfirst', array( __CLASS__, 'ucfirst' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'lc', array( __CLASS__, 'lc' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'uc', array( __CLASS__, 'uc' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'localurl', array( __CLASS__, 'localurl' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'localurle', array( __CLASS__, 'localurle' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'fullurl', array( __CLASS__, 'fullurl' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'fullurle', array( __CLASS__, 'fullurle' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'formatnum', array( __CLASS__, 'formatnum' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'grammar', array( __CLASS__, 'grammar' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'plural', array( __CLASS__, 'plural' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'numberofpages', array( __CLASS__, 'numberofpages' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'numberofusers', array( __CLASS__, 'numberofusers' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'numberofarticles', array( __CLASS__, 'numberofarticles' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'numberoffiles', array( __CLASS__, 'numberoffiles' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'numberofadmins', array( __CLASS__, 'numberofadmins' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'numberofedits', array( __CLASS__, 'numberofedits' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'language', array( __CLASS__, 'language' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'padleft', array( __CLASS__, 'padleft' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'padright', array( __CLASS__, 'padright' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'anchorencode', array( __CLASS__, 'anchorencode' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) );
- $parser->setFunctionHook( 'defaultsort', array( __CLASS__, 'defaultsort' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'filepath', array( __CLASS__, 'filepath' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'pagesincategory', array( __CLASS__, 'pagesincategory' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'pagesize', array( __CLASS__, 'pagesize' ), SFH_NO_HASH );
- $parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), SFH_OBJECT_ARGS );
-
- if ( $wgAllowDisplayTitle ) {
- $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH );
- }
- if ( $wgAllowSlowParserFunctions ) {
- $parser->setFunctionHook( 'pagesinnamespace', array( __CLASS__, 'pagesinnamespace' ), SFH_NO_HASH );
- }
- }
-
- static function intFunction( $parser, $part1 = '' /*, ... */ ) {
- if ( strval( $part1 ) !== '' ) {
- $args = array_slice( func_get_args(), 2 );
- return wfMsgReal( $part1, $args, true );
- } else {
- return array( 'found' => false );
- }
- }
-
- static function ns( $parser, $part1 = '' ) {
- global $wgContLang;
- $found = false;
- if ( intval( $part1 ) || $part1 == "0" ) {
- $text = $wgContLang->getNsText( intval( $part1 ) );
- $found = true;
- } else {
- $param = str_replace( ' ', '_', strtolower( $part1 ) );
- $index = MWNamespace::getCanonicalIndex( strtolower( $param ) );
- if ( !is_null( $index ) ) {
- $text = $wgContLang->getNsText( $index );
- $found = true;
- }
- }
- if ( $found ) {
- return $text;
- } else {
- return array( 'found' => false );
- }
- }
-
- static function urlencode( $parser, $s = '' ) {
- return urlencode( $s );
- }
-
- static function lcfirst( $parser, $s = '' ) {
- global $wgContLang;
- return $wgContLang->lcfirst( $s );
- }
-
- static function ucfirst( $parser, $s = '' ) {
- global $wgContLang;
- return $wgContLang->ucfirst( $s );
- }
-
- static function lc( $parser, $s = '' ) {
- global $wgContLang;
- if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
- return $parser->markerSkipCallback( $s, array( $wgContLang, 'lc' ) );
- } else {
- return $wgContLang->lc( $s );
- }
- }
-
- static function uc( $parser, $s = '' ) {
- global $wgContLang;
- if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
- return $parser->markerSkipCallback( $s, array( $wgContLang, 'uc' ) );
- } else {
- return $wgContLang->uc( $s );
- }
- }
-
- static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); }
- static function localurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeLocalURL', $s, $arg ); }
- static function fullurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getFullURL', $s, $arg ); }
- static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); }
-
- static function urlFunction( $func, $s = '', $arg = null ) {
- $title = Title::newFromText( $s );
- # Due to order of execution of a lot of bits, the values might be encoded
- # before arriving here; if that's true, then the title can't be created
- # and the variable will fail. If we can't get a decent title from the first
- # attempt, url-decode and try for a second.
- if( is_null( $title ) )
- $title = Title::newFromUrl( urldecode( $s ) );
- if ( !is_null( $title ) ) {
- if ( !is_null( $arg ) ) {
- $text = $title->$func( $arg );
- } else {
- $text = $title->$func();
- }
- return $text;
- } else {
- return array( 'found' => false );
- }
- }
-
- static function formatNum( $parser, $num = '', $raw = null) {
- if ( self::israw( $raw ) ) {
- return $parser->getFunctionLang()->parseFormattedNumber( $num );
- } else {
- return $parser->getFunctionLang()->formatNum( $num );
- }
- }
-
- static function grammar( $parser, $case = '', $word = '' ) {
- return $parser->getFunctionLang()->convertGrammar( $word, $case );
- }
-
- static function plural( $parser, $text = '') {
- $forms = array_slice( func_get_args(), 2);
- $text = $parser->getFunctionLang()->parseFormattedNumber( $text );
- return $parser->getFunctionLang()->convertPlural( $text, $forms );
- }
-
- /**
- * Override the title of the page when viewed, provided we've been given a
- * title which will normalise to the canonical title
- *
- * @param Parser $parser Parent parser
- * @param string $text Desired title text
- * @return string
- */
- static function displaytitle( $parser, $text = '' ) {
- $text = trim( Sanitizer::decodeCharReferences( $text ) );
- $title = Title::newFromText( $text );
- if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
- $parser->mOutput->setDisplayTitle( $text );
- return '';
- }
-
- static function isRaw( $param ) {
- static $mwRaw;
- if ( !$mwRaw ) {
- $mwRaw =& MagicWord::get( 'rawsuffix' );
- }
- if ( is_null( $param ) ) {
- return false;
- } else {
- return $mwRaw->match( $param );
- }
- }
-
- static function formatRaw( $num, $raw ) {
- if( self::isRaw( $raw ) ) {
- return $num;
- } else {
- global $wgContLang;
- return $wgContLang->formatNum( $num );
- }
- }
- static function numberofpages( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::pages(), $raw );
- }
- static function numberofusers( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::users(), $raw );
- }
- static function numberofarticles( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::articles(), $raw );
- }
- static function numberoffiles( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::images(), $raw );
- }
- static function numberofadmins( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::admins(), $raw );
- }
- static function numberofedits( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::edits(), $raw );
- }
- static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
- return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
- }
-
- /**
- * Return the number of pages in the given category, or 0 if it's nonexis-
- * tent. This is an expensive parser function and can't be called too many
- * times per page.
- */
- static function pagesincategory( $parser, $name = '', $raw = null ) {
- static $cache = array();
- $category = Category::newFromName( $name );
-
- if( !is_object( $category ) ) {
- $cache[$name] = 0;
- return self::formatRaw( 0, $raw );
- }
-
- # Normalize name for cache
- $name = $category->getName();
-
- $count = 0;
- if( isset( $cache[$name] ) ) {
- $count = $cache[$name];
- } elseif( $parser->incrementExpensiveFunctionCount() ) {
- $count = $cache[$name] = (int)$category->getPageCount();
- }
- return self::formatRaw( $count, $raw );
- }
-
- /**
- * Return the size of the given page, or 0 if it's nonexistent. This is an
- * expensive parser function and can't be called too many times per page.
- *
- * @FIXME This doesn't work correctly on preview for getting the size of
- * the current page.
- * @FIXME Title::getLength() documentation claims that it adds things to
- * the link cache, so the local cache here should be unnecessary, but in
- * fact calling getLength() repeatedly for the same $page does seem to
- * run one query for each call?
- */
- static function pagesize( $parser, $page = '', $raw = null ) {
- static $cache = array();
- $title = Title::newFromText($page);
-
- if( !is_object( $title ) ) {
- $cache[$page] = 0;
- return self::formatRaw( 0, $raw );
- }
-
- # Normalize name for cache
- $page = $title->getPrefixedText();
-
- $length = 0;
- if( isset( $cache[$page] ) ) {
- $length = $cache[$page];
- } elseif( $parser->incrementExpensiveFunctionCount() ) {
- $length = $cache[$page] = $title->getLength();
-
- // Register dependency in templatelinks
- $id = $title->getArticleId();
- $revid = Revision::newFromTitle($title);
- $parser->mOutput->addTemplate($title, $id, $revid);
- }
- return self::formatRaw( $length, $raw );
- }
-
- static function language( $parser, $arg = '' ) {
- global $wgContLang;
- $lang = $wgContLang->getLanguageName( strtolower( $arg ) );
- return $lang != '' ? $lang : $arg;
- }
-
- static function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) {
- $length = min( max( $length, 0 ), 500 );
- $char = substr( $char, 0, 1 );
- return ( $string !== '' && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 )
- ? str_pad( $string, $length, (string)$char, $direction )
- : $string;
- }
-
- static function padleft( $parser, $string = '', $length = 0, $char = 0 ) {
- return self::pad( $string, $length, $char, STR_PAD_LEFT );
- }
-
- static function padright( $parser, $string = '', $length = 0, $char = 0 ) {
- return self::pad( $string, $length, $char );
- }
-
- static function anchorencode( $parser, $text ) {
- $a = urlencode( $text );
- $a = strtr( $a, array( '%' => '.', '+' => '_' ) );
- # leave colons alone, however
- $a = str_replace( '.3A', ':', $a );
- return $a;
- }
-
- static function special( $parser, $text ) {
- $title = SpecialPage::getTitleForAlias( $text );
- if ( $title ) {
- return $title->getPrefixedText();
- } else {
- return wfMsgForContent( 'nosuchspecialpage' );
- }
- }
-
- public static function defaultsort( $parser, $text ) {
- $text = trim( $text );
- if( strlen( $text ) > 0 )
- $parser->setDefaultSort( $text );
- return '';
- }
-
- public static function filepath( $parser, $name='', $option='' ) {
- $file = wfFindFile( $name );
- if( $file ) {
- $url = $file->getFullUrl();
- if( $option == 'nowiki' ) {
- return "$url";
- }
- return $url;
- } else {
- return '';
- }
- }
-
- /**
- * Parser function to extension tag adaptor
- */
- public static function tagObj( $parser, $frame, $args ) {
- $xpath = false;
- if ( !count( $args ) ) {
- return '';
- }
- $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) );
-
- if ( count( $args ) ) {
- $inner = $frame->expand( array_shift( $args ) );
- } else {
- $inner = null;
- }
-
- $stripList = $parser->getStripList();
- if ( !in_array( $tagName, $stripList ) ) {
- return '' .
- wfMsg( 'unknown_extension_tag', $tagName ) .
- '';
- }
-
- $attributes = array();
- foreach ( $args as $arg ) {
- $bits = $arg->splitArg();
- if ( strval( $bits['index'] ) === '' ) {
- $name = $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS );
- $value = trim( $frame->expand( $bits['value'] ) );
- if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
- $value = isset( $m[1] ) ? $m[1] : '';
- }
- $attributes[$name] = $value;
- }
- }
-
- $params = array(
- 'name' => $tagName,
- 'inner' => $inner,
- 'attributes' => $attributes,
- 'close' => "$tagName>",
- );
- return $parser->extensionSubstitution( $params, $frame );
- }
-}
diff --git a/includes/Database.php b/includes/Database.php
deleted file mode 100644
index b6a8c8bde6..0000000000
--- a/includes/Database.php
+++ /dev/null
@@ -1,2700 +0,0 @@
-mFailFunction, $function );
- }
-
- /**
- * Output page, used for reporting errors
- * FALSE means discard output
- */
- function setOutputPage( $out ) {
- $this->mOut = $out;
- }
-
- /**
- * Boolean, controls output of large amounts of debug information
- */
- function debug( $debug = NULL ) {
- return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
- }
-
- /**
- * Turns buffering of SQL result sets on (true) or off (false).
- * Default is "on" and it should not be changed without good reasons.
- */
- function bufferResults( $buffer = NULL ) {
- if ( is_null( $buffer ) ) {
- return !(bool)( $this->mFlags & DBO_NOBUFFER );
- } else {
- return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
- }
- }
-
- /**
- * Turns on (false) or off (true) the automatic generation and sending
- * of a "we're sorry, but there has been a database error" page on
- * database errors. Default is on (false). When turned off, the
- * code should use lastErrno() and lastError() to handle the
- * situation as appropriate.
- */
- function ignoreErrors( $ignoreErrors = NULL ) {
- return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
- }
-
- /**
- * The current depth of nested transactions
- * @param $level Integer: , default NULL.
- */
- function trxLevel( $level = NULL ) {
- return wfSetVar( $this->mTrxLevel, $level );
- }
-
- /**
- * Number of errors logged, only useful when errors are ignored
- */
- function errorCount( $count = NULL ) {
- return wfSetVar( $this->mErrorCount, $count );
- }
-
- function tablePrefix( $prefix = null ) {
- return wfSetVar( $this->mTablePrefix, $prefix );
- }
-
- /**
- * Properties passed down from the server info array of the load balancer
- */
- function getLBInfo( $name = NULL ) {
- if ( is_null( $name ) ) {
- return $this->mLBInfo;
- } else {
- if ( array_key_exists( $name, $this->mLBInfo ) ) {
- return $this->mLBInfo[$name];
- } else {
- return NULL;
- }
- }
- }
-
- function setLBInfo( $name, $value = NULL ) {
- if ( is_null( $value ) ) {
- $this->mLBInfo = $name;
- } else {
- $this->mLBInfo[$name] = $value;
- }
- }
-
- /**
- * Set lag time in seconds for a fake slave
- */
- function setFakeSlaveLag( $lag ) {
- $this->mFakeSlaveLag = $lag;
- }
-
- /**
- * Make this connection a fake master
- */
- function setFakeMaster( $enabled = true ) {
- $this->mFakeMaster = $enabled;
- }
-
- /**
- * Returns true if this database supports (and uses) cascading deletes
- */
- function cascadingDeletes() {
- return false;
- }
-
- /**
- * Returns true if this database supports (and uses) triggers (e.g. on the page table)
- */
- function cleanupTriggers() {
- return false;
- }
-
- /**
- * Returns true if this database is strict about what can be put into an IP field.
- * Specifically, it uses a NULL value instead of an empty string.
- */
- function strictIPs() {
- return false;
- }
-
- /**
- * Returns true if this database uses timestamps rather than integers
- */
- function realTimestamps() {
- return false;
- }
-
- /**
- * Returns true if this database does an implicit sort when doing GROUP BY
- */
- function implicitGroupby() {
- return true;
- }
-
- /**
- * Returns true if this database does an implicit order by when the column has an index
- * For example: SELECT page_title FROM page LIMIT 1
- */
- function implicitOrderby() {
- return true;
- }
-
- /**
- * Returns true if this database can do a native search on IP columns
- * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
- */
- function searchableIPs() {
- return false;
- }
-
- /**
- * Returns true if this database can use functional indexes
- */
- function functionalIndexes() {
- return false;
- }
-
- /**#@+
- * Get function
- */
- function lastQuery() { return $this->mLastQuery; }
- function isOpen() { return $this->mOpened; }
- /**#@-*/
-
- function setFlag( $flag ) {
- $this->mFlags |= $flag;
- }
-
- function clearFlag( $flag ) {
- $this->mFlags &= ~$flag;
- }
-
- function getFlag( $flag ) {
- return !!($this->mFlags & $flag);
- }
-
- /**
- * General read-only accessor
- */
- function getProperty( $name ) {
- return $this->$name;
- }
-
- function getWikiID() {
- if( $this->mTablePrefix ) {
- return "{$this->mDBname}-{$this->mTablePrefix}";
- } else {
- return $this->mDBname;
- }
- }
-
-#------------------------------------------------------------------------------
-# Other functions
-#------------------------------------------------------------------------------
-
- /**@{{
- * Constructor.
- * @param string $server database server host
- * @param string $user database user name
- * @param string $password database user password
- * @param string $dbname database name
- * @param failFunction
- * @param $flags
- * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
- */
- function __construct( $server = false, $user = false, $password = false, $dbName = false,
- $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
-
- global $wgOut, $wgDBprefix, $wgCommandLineMode;
- # Can't get a reference if it hasn't been set yet
- if ( !isset( $wgOut ) ) {
- $wgOut = NULL;
- }
- $this->mOut =& $wgOut;
-
- $this->mFailFunction = $failFunction;
- $this->mFlags = $flags;
-
- if ( $this->mFlags & DBO_DEFAULT ) {
- if ( $wgCommandLineMode ) {
- $this->mFlags &= ~DBO_TRX;
- } else {
- $this->mFlags |= DBO_TRX;
- }
- }
-
- /*
- // Faster read-only access
- if ( wfReadOnly() ) {
- $this->mFlags |= DBO_PERSISTENT;
- $this->mFlags &= ~DBO_TRX;
- }*/
-
- /** Get the default table prefix*/
- if ( $tablePrefix == 'get from global' ) {
- $this->mTablePrefix = $wgDBprefix;
- } else {
- $this->mTablePrefix = $tablePrefix;
- }
-
- if ( $server ) {
- $this->open( $server, $user, $password, $dbName );
- }
- }
-
- /**
- * @static
- * @param failFunction
- * @param $flags
- */
- static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
- {
- return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
- }
-
- /**
- * Usually aborts on failure
- * If the failFunction is set to a non-zero integer, returns success
- */
- function open( $server, $user, $password, $dbName ) {
- global $wguname, $wgAllDBsAreLocalhost;
- wfProfileIn( __METHOD__ );
-
- # Test for missing mysql.so
- # First try to load it
- if (!@extension_loaded('mysql')) {
- @dl('mysql.so');
- }
-
- # Fail now
- # Otherwise we get a suppressed fatal error, which is very hard to track down
- if ( !function_exists( 'mysql_connect' ) ) {
- throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
- }
-
- # Debugging hack -- fake cluster
- if ( $wgAllDBsAreLocalhost ) {
- $realServer = 'localhost';
- } else {
- $realServer = $server;
- }
- $this->close();
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $success = false;
-
- wfProfileIn("dbconnect-$server");
-
- # Try to connect up to three times
- # The kernel's default SYN retransmission period is far too slow for us,
- # so we use a short timeout plus a manual retry.
- $this->mConn = false;
- $max = 3;
- $this->installErrorHandler();
- for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
- if ( $i > 1 ) {
- usleep( 1000 );
- }
- if ( $this->mFlags & DBO_PERSISTENT ) {
- $this->mConn = mysql_pconnect( $realServer, $user, $password );
- } else {
- # Create a new connection...
- $this->mConn = mysql_connect( $realServer, $user, $password, true );
- }
- if ($this->mConn === false) {
- #$iplus = $i + 1;
- #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
- }
- }
- $phpError = $this->restoreErrorHandler();
-
- wfProfileOut("dbconnect-$server");
-
- if ( $dbName != '' ) {
- if ( $this->mConn !== false ) {
- $success = @/**/mysql_select_db( $dbName, $this->mConn );
- if ( !$success ) {
- $error = "Error selecting database $dbName on server {$this->mServer} " .
- "from client host {$wguname['nodename']}\n";
- wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
- wfDebug( $error );
- }
- } else {
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
- $success = false;
- }
- } else {
- # Delay USE query
- $success = (bool)$this->mConn;
- }
-
- if ( $success ) {
- $version = $this->getServerVersion();
- if ( version_compare( $version, '4.1' ) >= 0 ) {
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- global $wgDBmysql5;
- if( $wgDBmysql5 ) {
- $this->query( 'SET NAMES utf8', __METHOD__ );
- }
- // Turn off strict mode
- $this->query( "SET sql_mode = ''", __METHOD__ );
- }
-
- // Turn off strict mode if it is on
- } else {
- $this->reportConnectionError( $phpError );
- }
-
- $this->mOpened = $success;
- wfProfileOut( __METHOD__ );
- return $success;
- }
- /**@}}*/
-
- protected function installErrorHandler() {
- $this->mPHPError = false;
- set_error_handler( array( $this, 'connectionErrorHandler' ) );
- }
-
- protected function restoreErrorHandler() {
- restore_error_handler();
- return $this->mPHPError;
- }
-
- protected function connectionErrorHandler( $errno, $errstr ) {
- $this->mPHPError = $errstr;
- }
-
- /**
- * Closes a database connection.
- * if it is open : commits any open transactions
- *
- * @return bool operation success. true if already closed.
- */
- function close()
- {
- $this->mOpened = false;
- if ( $this->mConn ) {
- if ( $this->trxLevel() ) {
- $this->immediateCommit();
- }
- return mysql_close( $this->mConn );
- } else {
- return true;
- }
- }
-
- /**
- * @param string $error fallback error message, used if none is given by MySQL
- */
- function reportConnectionError( $error = 'Unknown error' ) {
- $myError = $this->lastError();
- if ( $myError ) {
- $error = $myError;
- }
-
- if ( $this->mFailFunction ) {
- # Legacy error handling method
- if ( !is_int( $this->mFailFunction ) ) {
- $ff = $this->mFailFunction;
- $ff( $this, $error );
- }
- } else {
- # New method
- wfLogDBError( "Connection error: $error\n" );
- throw new DBConnectionError( $this, $error );
- }
- }
-
- /**
- * Usually aborts on failure. If errors are explicitly ignored, returns success.
- *
- * @param $sql String: SQL query
- * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
- * comment (you can use __METHOD__ or add some extra info)
- * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
- * maybe best to catch the exception instead?
- * @return true for a successful write query, ResultWrapper object for a successful read query,
- * or false on failure if $tempIgnore set
- * @throws DBQueryError Thrown when the database returns an error of any kind
- */
- public function query( $sql, $fname = '', $tempIgnore = false ) {
- global $wgProfiler;
-
- $isMaster = !is_null( $this->getLBInfo( 'master' ) );
- if ( isset( $wgProfiler ) ) {
- # generalizeSQL will probably cut down the query to reasonable
- # logging size most of the time. The substr is really just a sanity check.
-
- # Who's been wasting my precious column space? -- TS
- #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
-
- if ( $isMaster ) {
- $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'Database::query-master';
- } else {
- $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'Database::query';
- }
- wfProfileIn( $totalProf );
- wfProfileIn( $queryProf );
- }
-
- $this->mLastQuery = $sql;
-
- # Add a comment for easy SHOW PROCESSLIST interpretation
- #if ( $fname ) {
- global $wgUser;
- if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
- $userName = $wgUser->getName();
- if ( mb_strlen( $userName ) > 15 ) {
- $userName = mb_substr( $userName, 0, 15 ) . '...';
- }
- $userName = str_replace( '/', '', $userName );
- } else {
- $userName = '';
- }
- $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
- #} else {
- # $commentedSql = $sql;
- #}
-
- # If DBO_TRX is set, start a transaction
- if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
- $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
- // avoid establishing transactions for SHOW and SET statements too -
- // that would delay transaction initializations to once connection
- // is really used by application
- $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
- if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
- $this->begin();
- }
-
- if ( $this->debug() ) {
- $sqlx = substr( $commentedSql, 0, 500 );
- $sqlx = strtr( $sqlx, "\t\n", ' ' );
- if ( $isMaster ) {
- wfDebug( "SQL-master: $sqlx\n" );
- } else {
- wfDebug( "SQL: $sqlx\n" );
- }
- }
-
- # Do the query and handle errors
- $ret = $this->doQuery( $commentedSql );
-
- # Try reconnecting if the connection was lost
- if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
- # Transaction is gone, like it or not
- $this->mTrxLevel = 0;
- wfDebug( "Connection lost, reconnecting...\n" );
- if ( $this->ping() ) {
- wfDebug( "Reconnected\n" );
- $sqlx = substr( $commentedSql, 0, 500 );
- $sqlx = strtr( $sqlx, "\t\n", ' ' );
- global $wgRequestTime;
- $elapsed = round( microtime(true) - $wgRequestTime, 3 );
- wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
- $ret = $this->doQuery( $commentedSql );
- } else {
- wfDebug( "Failed\n" );
- }
- }
-
- if ( false === $ret ) {
- $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
- }
-
- if ( isset( $wgProfiler ) ) {
- wfProfileOut( $queryProf );
- wfProfileOut( $totalProf );
- }
- return $this->resultObject( $ret );
- }
-
- /**
- * The DBMS-dependent part of query()
- * @param $sql String: SQL query.
- * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
- * @access private
- */
- /*private*/ function doQuery( $sql ) {
- if( $this->bufferResults() ) {
- $ret = mysql_query( $sql, $this->mConn );
- } else {
- $ret = mysql_unbuffered_query( $sql, $this->mConn );
- }
- return $ret;
- }
-
- /**
- * @param $error
- * @param $errno
- * @param $sql
- * @param string $fname
- * @param bool $tempIgnore
- */
- function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- global $wgCommandLineMode;
- # Ignore errors during error handling to avoid infinite recursion
- $ignore = $this->ignoreErrors( true );
- ++$this->mErrorCount;
-
- if( $ignore || $tempIgnore ) {
- wfDebug("SQL ERROR (ignored): $error\n");
- $this->ignoreErrors( $ignore );
- } else {
- $sql1line = str_replace( "\n", "\\n", $sql );
- wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
- wfDebug("SQL ERROR: " . $error . "\n");
- throw new DBQueryError( $this, $error, $errno, $sql, $fname );
- }
- }
-
-
- /**
- * Intended to be compatible with the PEAR::DB wrapper functions.
- * http://pear.php.net/manual/en/package.database.db.intro-execute.php
- *
- * ? = scalar value, quoted as necessary
- * ! = raw SQL bit (a function for instance)
- * & = filename; reads the file and inserts as a blob
- * (we don't use this though...)
- */
- function prepare( $sql, $func = 'Database::prepare' ) {
- /* MySQL doesn't support prepared statements (yet), so just
- pack up the query for reference. We'll manually replace
- the bits later. */
- return array( 'query' => $sql, 'func' => $func );
- }
-
- function freePrepared( $prepared ) {
- /* No-op for MySQL */
- }
-
- /**
- * Execute a prepared query with the various arguments
- * @param string $prepared the prepared sql
- * @param mixed $args Either an array here, or put scalars as varargs
- */
- function execute( $prepared, $args = null ) {
- if( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
- }
- $sql = $this->fillPrepared( $prepared['query'], $args );
- return $this->query( $sql, $prepared['func'] );
- }
-
- /**
- * Prepare & execute an SQL statement, quoting and inserting arguments
- * in the appropriate places.
- * @param string $query
- * @param string $args ...
- */
- function safeQuery( $query, $args = null ) {
- $prepared = $this->prepare( $query, 'Database::safeQuery' );
- if( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
- }
- $retval = $this->execute( $prepared, $args );
- $this->freePrepared( $prepared );
- return $retval;
- }
-
- /**
- * For faking prepared SQL statements on DBs that don't support
- * it directly.
- * @param string $preparedSql - a 'preparable' SQL statement
- * @param array $args - array of arguments to fill it with
- * @return string executable SQL
- */
- function fillPrepared( $preparedQuery, $args ) {
- reset( $args );
- $this->preparedArgs =& $args;
- return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
- array( &$this, 'fillPreparedArg' ), $preparedQuery );
- }
-
- /**
- * preg_callback func for fillPrepared()
- * The arguments should be in $this->preparedArgs and must not be touched
- * while we're doing this.
- *
- * @param array $matches
- * @return string
- * @private
- */
- function fillPreparedArg( $matches ) {
- switch( $matches[1] ) {
- case '\\?': return '?';
- case '\\!': return '!';
- case '\\&': return '&';
- }
- list( /* $n */ , $arg ) = each( $this->preparedArgs );
- switch( $matches[1] ) {
- case '?': return $this->addQuotes( $arg );
- case '!': return $arg;
- case '&':
- # return $this->addQuotes( file_get_contents( $arg ) );
- throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
- default:
- throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
- }
- }
-
- /**#@+
- * @param mixed $res A SQL result
- */
- /**
- * Free a result object
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- if ( !@/**/mysql_free_result( $res ) ) {
- throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
- }
- }
-
- /**
- * Fetch the next row from the given result object, in object form.
- * Fields can be retrieved with $row->fieldname, with fields acting like
- * member variables.
- *
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$row = mysql_fetch_object( $res );
- if( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * Fetch the next row from the given result object, in associative array
- * form. Fields are retrieved with $row['fieldname'].
- *
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$row = mysql_fetch_array( $res );
- if ( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * Get the number of rows in a result object
- */
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$n = mysql_num_rows( $res );
- if( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $n;
- }
-
- /**
- * Get the number of fields in a result object
- * See documentation for mysql_num_fields()
- */
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_num_fields( $res );
- }
-
- /**
- * Get a field name in a result object
- * See documentation for mysql_field_name():
- * http://www.php.net/mysql_field_name
- */
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_field_name( $res, $n );
- }
-
- /**
- * Get the inserted value of an auto-increment row
- *
- * The value inserted should be fetched from nextSequenceValue()
- *
- * Example:
- * $id = $dbw->nextSequenceValue('page_page_id_seq');
- * $dbw->insert('page',array('page_id' => $id));
- * $id = $dbw->insertId();
- */
- function insertId() { return mysql_insert_id( $this->mConn ); }
-
- /**
- * Change the position of the cursor in a result object
- * See mysql_data_seek()
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_data_seek( $res, $row );
- }
-
- /**
- * Get the last error number
- * See mysql_errno()
- */
- function lastErrno() {
- if ( $this->mConn ) {
- return mysql_errno( $this->mConn );
- } else {
- return mysql_errno();
- }
- }
-
- /**
- * Get a description of the last error
- * See mysql_error() for more details
- */
- function lastError() {
- if ( $this->mConn ) {
- # Even if it's non-zero, it can still be invalid
- wfSuppressWarnings();
- $error = mysql_error( $this->mConn );
- if ( !$error ) {
- $error = mysql_error();
- }
- wfRestoreWarnings();
- } else {
- $error = mysql_error();
- }
- if( $error ) {
- $error .= ' (' . $this->mServer . ')';
- }
- return $error;
- }
- /**
- * Get the number of rows affected by the last write query
- * See mysql_affected_rows() for more details
- */
- function affectedRows() { return mysql_affected_rows( $this->mConn ); }
- /**#@-*/ // end of template : @param $result
-
- /**
- * Simple UPDATE wrapper
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- *
- * This function exists for historical reasons, Database::update() has a more standard
- * calling convention and feature set
- */
- function set( $table, $var, $value, $cond, $fname = 'Database::set' )
- {
- $table = $this->tableName( $table );
- $sql = "UPDATE $table SET $var = '" .
- $this->strencode( $value ) . "' WHERE ($cond)";
- return (bool)$this->query( $sql, $fname );
- }
-
- /**
- * Simple SELECT wrapper, returns a single field, input must be encoded
- * Usually aborts on failure
- * If errors are explicitly ignored, returns FALSE on failure
- */
- function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
- if ( !is_array( $options ) ) {
- $options = array( $options );
- }
- $options['LIMIT'] = 1;
-
- $res = $this->select( $table, $var, $cond, $fname, $options );
- if ( $res === false || !$this->numRows( $res ) ) {
- return false;
- }
- $row = $this->fetchRow( $res );
- if ( $row !== false ) {
- $this->freeResult( $res );
- return $row[0];
- } else {
- return false;
- }
- }
-
- /**
- * Returns an optional USE INDEX clause to go after the table, and a
- * string to go at the end of the query
- *
- * @private
- *
- * @param array $options an associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = '';
-
- $noKeyOptions = array();
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
- if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
- if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
-
- //if (isset($options['LIMIT'])) {
- // $tailOpts .= $this->limitResult('', $options['LIMIT'],
- // isset($options['OFFSET']) ? $options['OFFSET']
- // : false);
- //}
-
- if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
- if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
- # Various MySQL extensions
- if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
- if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
- if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
- if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
- if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
- if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
- if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
- if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
-
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
- $useIndex = $this->useIndexClause( $options['USE INDEX'] );
- } else {
- $useIndex = '';
- }
-
- return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
- }
-
- /**
- * SELECT wrapper
- *
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
- * see Database::makeSelectOptions code for list of supported stuff
- * @param array $join_conds Associative array of table join conditions (optional)
- * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
- * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
- */
- function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
- {
- $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
- return $this->query( $sql, $fname );
- }
-
- /**
- * SELECT wrapper
- *
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
- * see Database::makeSelectOptions code for list of supported stuff
- * @param array $join_conds Associative array of table join conditions (optional)
- * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
- * @return string, the SQL text
- */
- function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
- if( is_array( $vars ) ) {
- $vars = implode( ',', $vars );
- }
- if( !is_array( $options ) ) {
- $options = array( $options );
- }
- if( is_array( $table ) ) {
- if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
- $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
- else
- $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
- } elseif ($table!='') {
- if ($table{0}==' ') {
- $from = ' FROM ' . $table;
- } else {
- $from = ' FROM ' . $this->tableName( $table );
- }
- } else {
- $from = '';
- }
-
- list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
-
- if( !empty( $conds ) ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
- }
- $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
- } else {
- $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
- }
-
- if (isset($options['LIMIT']))
- $sql = $this->limitResult($sql, $options['LIMIT'],
- isset($options['OFFSET']) ? $options['OFFSET'] : false);
- $sql = "$sql $postLimitTail";
-
- if (isset($options['EXPLAIN'])) {
- $sql = 'EXPLAIN ' . $sql;
- }
- return $sql;
- }
-
- /**
- * Single row SELECT wrapper
- * Aborts or returns FALSE on error
- *
- * $vars: the selected variables
- * $conds: a condition map, terms are ANDed together.
- * Items with numeric keys are taken to be literal conditions
- * Takes an array of selected variables, and a condition map, which is ANDed
- * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
- * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
- * $obj- >page_id is the ID of the Astronomy article
- *
- * @todo migrate documentation to phpdocumentor format
- */
- function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
- $options['LIMIT'] = 1;
- $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
- if ( $res === false )
- return false;
- if ( !$this->numRows($res) ) {
- $this->freeResult($res);
- return false;
- }
- $obj = $this->fetchObject( $res );
- $this->freeResult( $res );
- return $obj;
-
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * Takes same arguments as Database::select()
- */
-
- function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
- $options['EXPLAIN']=true;
- $res = $this->select ($table, $vars, $conds, $fname, $options );
- if ( $res === false )
- return false;
- if (!$this->numRows($res)) {
- $this->freeResult($res);
- return 0;
- }
-
- $rows=1;
-
- while( $plan = $this->fetchObject( $res ) ) {
- $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
- }
-
- $this->freeResult($res);
- return $rows;
- }
-
-
- /**
- * Removes most variables from an SQL query and replaces them with X or N for numbers.
- * It's only slightly flawed. Don't use for anything important.
- *
- * @param string $sql A SQL Query
- * @static
- */
- static function generalizeSQL( $sql ) {
- # This does the same as the regexp below would do, but in such a way
- # as to avoid crashing php on some large strings.
- # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
-
- $sql = str_replace ( "\\\\", '', $sql);
- $sql = str_replace ( "\\'", '', $sql);
- $sql = str_replace ( "\\\"", '', $sql);
- $sql = preg_replace ("/'.*'/s", "'X'", $sql);
- $sql = preg_replace ('/".*"/s', "'X'", $sql);
-
- # All newlines, tabs, etc replaced by single space
- $sql = preg_replace ( '/\s+/', ' ', $sql);
-
- # All numbers => N
- $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
-
- return $sql;
- }
-
- /**
- * Determines whether a field exists in a table
- * Usually aborts on failure
- * If errors are explicitly ignored, returns NULL on failure
- */
- function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
- $table = $this->tableName( $table );
- $res = $this->query( 'DESCRIBE '.$table, $fname );
- if ( !$res ) {
- return NULL;
- }
-
- $found = false;
-
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->Field == $field ) {
- $found = true;
- break;
- }
- }
- return $found;
- }
-
- /**
- * Determines whether an index exists
- * Usually aborts on failure
- * If errors are explicitly ignored, returns NULL on failure
- */
- function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
- $info = $this->indexInfo( $table, $index, $fname );
- if ( is_null( $info ) ) {
- return NULL;
- } else {
- return $info !== false;
- }
- }
-
-
- /**
- * Get information about an index into an object
- * Returns false if the index does not exist
- */
- function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
- # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
- # SHOW INDEX should work for 3.x and up:
- # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
- $table = $this->tableName( $table );
- $sql = 'SHOW INDEX FROM '.$table;
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return NULL;
- }
-
- $result = array();
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->Key_name == $index ) {
- $result[] = $row;
- }
- }
- $this->freeResult($res);
-
- return empty($result) ? false : $result;
- }
-
- /**
- * Query whether a given table exists
- */
- function tableExists( $table ) {
- $table = $this->tableName( $table );
- $old = $this->ignoreErrors( true );
- $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
- $this->ignoreErrors( $old );
- if( $res ) {
- $this->freeResult( $res );
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
- *
- * @param $table
- * @param $field
- */
- function fieldInfo( $table, $field ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT * FROM $table LIMIT 1" );
- $n = mysql_num_fields( $res->result );
- for( $i = 0; $i < $n; $i++ ) {
- $meta = mysql_fetch_field( $res->result, $i );
- if( $field == $meta->name ) {
- return new MySQLField($meta);
- }
- }
- return false;
- }
-
- /**
- * mysql_field_type() wrapper
- */
- function fieldType( $res, $index ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_field_type( $res, $index );
- }
-
- /**
- * Determines if a given index is unique
- */
- function indexUnique( $table, $index ) {
- $indexInfo = $this->indexInfo( $table, $index );
- if ( !$indexInfo ) {
- return NULL;
- }
- return !$indexInfo[0]->Non_unique;
- }
-
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $a may be a single associative array, or an array of these with numeric keys, for
- * multi-row insert.
- *
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- */
- function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
- # No rows to insert, easy just return now
- if ( !count( $a ) ) {
- return true;
- }
-
- $table = $this->tableName( $table );
- if ( !is_array( $options ) ) {
- $options = array( $options );
- }
- if ( isset( $a[0] ) && is_array( $a[0] ) ) {
- $multi = true;
- $keys = array_keys( $a[0] );
- } else {
- $multi = false;
- $keys = array_keys( $a );
- }
-
- $sql = 'INSERT ' . implode( ' ', $options ) .
- " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
- if ( $multi ) {
- $first = true;
- foreach ( $a as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- } else {
- $sql .= '(' . $this->makeList( $a ) . ')';
- }
- return (bool)$this->query( $sql, $fname );
- }
-
- /**
- * Make UPDATE options for the Database::update function
- *
- * @private
- * @param array $options The options passed to Database::update
- * @return string
- */
- function makeUpdateOptions( $options ) {
- if( !is_array( $options ) ) {
- $options = array( $options );
- }
- $opts = array();
- if ( in_array( 'LOW_PRIORITY', $options ) )
- $opts[] = $this->lowPriorityOption();
- if ( in_array( 'IGNORE', $options ) )
- $opts[] = 'IGNORE';
- return implode(' ', $opts);
- }
-
- /**
- * UPDATE wrapper, takes a condition array and a SET array
- *
- * @param string $table The table to UPDATE
- * @param array $values An array of values to SET
- * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
- * @param string $fname The Class::Function calling this function
- * (for the log)
- * @param array $options An array of UPDATE options, can be one or
- * more of IGNORE, LOW_PRIORITY
- * @return bool
- */
- function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
- $table = $this->tableName( $table );
- $opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
- if ( $conds != '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
- }
- return $this->query( $sql, $fname );
- }
-
- /**
- * Makes an encoded list of strings from an array
- * $mode:
- * LIST_COMMA - comma separated, no field names
- * LIST_AND - ANDed WHERE clause (without the WHERE)
- * LIST_OR - ORed WHERE clause (without the WHERE)
- * LIST_SET - comma separated with field names, like a SET clause
- * LIST_NAMES - comma separated field names
- */
- function makeList( $a, $mode = LIST_COMMA ) {
- if ( !is_array( $a ) ) {
- throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
- }
-
- $first = true;
- $list = '';
- foreach ( $a as $field => $value ) {
- if ( !$first ) {
- if ( $mode == LIST_AND ) {
- $list .= ' AND ';
- } elseif($mode == LIST_OR) {
- $list .= ' OR ';
- } else {
- $list .= ',';
- }
- } else {
- $first = false;
- }
- if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
- $list .= "($value)";
- } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
- $list .= "$value";
- } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
- if( count( $value ) == 0 ) {
- throw new MWException( __METHOD__.': empty input' );
- } elseif( count( $value ) == 1 ) {
- // Special-case single values, as IN isn't terribly efficient
- // Don't necessarily assume the single key is 0; we don't
- // enforce linear numeric ordering on other arrays here.
- $value = array_values( $value );
- $list .= $field." = ".$this->addQuotes( $value[0] );
- } else {
- $list .= $field." IN (".$this->makeList($value).") ";
- }
- } elseif( is_null($value) ) {
- if ( $mode == LIST_AND || $mode == LIST_OR ) {
- $list .= "$field IS ";
- } elseif ( $mode == LIST_SET ) {
- $list .= "$field = ";
- }
- $list .= 'NULL';
- } else {
- if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
- $list .= "$field = ";
- }
- $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
- }
- }
- return $list;
- }
-
- /**
- * Change the current database
- */
- function selectDB( $db ) {
- $this->mDBname = $db;
- return mysql_select_db( $db, $this->mConn );
- }
-
- /**
- * Get the current DB name
- */
- function getDBname() {
- return $this->mDBname;
- }
-
- /**
- * Get the server hostname or IP address
- */
- function getServer() {
- return $this->mServer;
- }
-
- /**
- * Format a table name ready for use in constructing an SQL query
- *
- * This does two important things: it quotes the table names to clean them up,
- * and it adds a table prefix if only given a table name with no quotes.
- *
- * All functions of this object which require a table name call this function
- * themselves. Pass the canonical name to such functions. This is only needed
- * when calling query() directly.
- *
- * @param string $name database table name
- * @return string full database name
- */
- function tableName( $name ) {
- global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
- # Skip the entire process when we have a string quoted on both ends.
- # Note that we check the end so that we will still quote any use of
- # use of `database`.table. But won't break things if someone wants
- # to query a database table with a dot in the name.
- if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
-
- # Lets test for any bits of text that should never show up in a table
- # name. Basically anything like JOIN or ON which are actually part of
- # SQL queries, but may end up inside of the table value to combine
- # sql. Such as how the API is doing.
- # Note that we use a whitespace test rather than a \b test to avoid
- # any remote case where a word like on may be inside of a table name
- # surrounded by symbols which may be considered word breaks.
- if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
-
- # Split database and table into proper variables.
- # We reverse the explode so that database.table and table both output
- # the correct table.
- $dbDetails = array_reverse( explode( '.', $name, 2 ) );
- if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
- else @list( $table ) = $dbDetails;
- $prefix = $this->mTablePrefix; # Default prefix
-
- # A database name has been specified in input. Quote the table name
- # because we don't want any prefixes added.
- if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
-
- # Note that we use the long format because php will complain in in_array if
- # the input is not an array, and will complain in is_array if it is not set.
- if( !isset( $database ) # Don't use shared database if pre selected.
- && isset( $wgSharedDB ) # We have a shared database
- && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
- && isset( $wgSharedTables )
- && is_array( $wgSharedTables )
- && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
- $database = $wgSharedDB;
- $prefix = isset( $wgSharedprefix ) ? $wgSharedprefix : $prefix;
- }
-
- # Quote the $database and $table and apply the prefix if not quoted.
- if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
- $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
-
- # Merge our database and table into our final table name.
- $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
-
- # We're finished, return.
- return $tableName;
- }
-
- /**
- * Fetch a number of table names into an array
- * This is handy when you need to construct SQL for joins
- *
- * Example:
- * extract($dbr->tableNames('user','watchlist'));
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
- */
- public function tableNames() {
- $inArray = func_get_args();
- $retVal = array();
- foreach ( $inArray as $name ) {
- $retVal[$name] = $this->tableName( $name );
- }
- return $retVal;
- }
-
- /**
- * Fetch a number of table names into an zero-indexed numerical array
- * This is handy when you need to construct SQL for joins
- *
- * Example:
- * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
- */
- public function tableNamesN() {
- $inArray = func_get_args();
- $retVal = array();
- foreach ( $inArray as $name ) {
- $retVal[] = $this->tableName( $name );
- }
- return $retVal;
- }
-
- /**
- * @private
- */
- function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
- $ret = array();
- $retJOIN = array();
- $use_index_safe = is_array($use_index) ? $use_index : array();
- $join_conds_safe = is_array($join_conds) ? $join_conds : array();
- foreach ( $tables as $table ) {
- // Is there a JOIN and INDEX clause for this table?
- if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
- $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
- $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
- $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
- $retJOIN[] = $tableClause;
- // Is there an INDEX clause?
- } else if ( isset($use_index_safe[$table]) ) {
- $tableClause = $this->tableName( $table );
- $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
- $ret[] = $tableClause;
- // Is there a JOIN clause?
- } else if ( isset($join_conds_safe[$table]) ) {
- $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
- $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
- $retJOIN[] = $tableClause;
- } else {
- $tableClause = $this->tableName( $table );
- $ret[] = $tableClause;
- }
- }
- // We can't separate explicit JOIN clauses with ',', use ' ' for those
- $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
- $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
- // Compile our final table clause
- return implode(' ',array($straightJoins,$otherJoins) );
- }
-
- /**
- * Wrapper for addslashes()
- * @param string $s String to be slashed.
- * @return string slashed string.
- */
- function strencode( $s ) {
- return mysql_real_escape_string( $s, $this->mConn );
- }
-
- /**
- * If it's a string, adds quotes and backslashes
- * Otherwise returns as-is
- */
- function addQuotes( $s ) {
- if ( is_null( $s ) ) {
- return 'NULL';
- } else {
- # This will also quote numeric values. This should be harmless,
- # and protects against weird problems that occur when they really
- # _are_ strings such as article titles and string->number->string
- # conversion is not 1:1.
- return "'" . $this->strencode( $s ) . "'";
- }
- }
-
- /**
- * Escape string for safe LIKE usage
- */
- function escapeLike( $s ) {
- $s=$this->strencode( $s );
- $s=str_replace(array('%','_'),array('\%','\_'),$s);
- return $s;
- }
-
- /**
- * Returns an appropriately quoted sequence value for inserting a new row.
- * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
- * subclass will return an integer, and save the value for insertId()
- */
- function nextSequenceValue( $seqName ) {
- return NULL;
- }
-
- /**
- * USE INDEX clause
- * PostgreSQL doesn't have them and returns ""
- */
- function useIndexClause( $index ) {
- return "FORCE INDEX ($index)";
- }
-
- /**
- * REPLACE query wrapper
- * PostgreSQL simulates this with a DELETE followed by INSERT
- * $row is the row to insert, an associative array
- * $uniqueIndexes is an array of indexes. Each element may be either a
- * field name or an array of field names
- *
- * It may be more efficient to leave off unique indexes which are unlikely to collide.
- * However if you do this, you run the risk of encountering errors which wouldn't have
- * occurred in MySQL
- *
- * @todo migrate comment to phodocumentor format
- */
- function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
- $table = $this->tableName( $table );
-
- # Single row case
- if ( !is_array( reset( $rows ) ) ) {
- $rows = array( $rows );
- }
-
- $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
- $first = true;
- foreach ( $rows as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- return $this->query( $sql, $fname );
- }
-
- /**
- * DELETE where the condition is a join
- * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
- *
- * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
- * join condition matches, set $conds='*'
- *
- * DO NOT put the join condition in $conds
- *
- * @param string $delTable The table to delete from.
- * @param string $joinTable The other table.
- * @param string $delVar The variable to join on, in the first table.
- * @param string $joinVar The variable to join on, in the second table.
- * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
- */
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
- if ( $conds != '*' ) {
- $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
- }
-
- return $this->query( $sql, $fname );
- }
-
- /**
- * Returns the size of a text field, or -1 for "unlimited"
- */
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
- $res = $this->query( $sql, 'Database::textFieldSize' );
- $row = $this->fetchObject( $res );
- $this->freeResult( $res );
-
- $m = array();
- if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
- $size = $m[1];
- } else {
- $size = -1;
- }
- return $size;
- }
-
- /**
- * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
- */
- function lowPriorityOption() {
- return 'LOW_PRIORITY';
- }
-
- /**
- * DELETE query wrapper
- *
- * Use $conds == "*" to delete all rows
- */
- function delete( $table, $conds, $fname = 'Database::delete' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
- }
- $table = $this->tableName( $table );
- $sql = "DELETE FROM $table";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
- }
- return $this->query( $sql, $fname );
- }
-
- /**
- * INSERT SELECT wrapper
- * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
- * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
- * $conds may be "*" to copy the whole table
- * srcTable may be an array of tables.
- */
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
- $insertOptions = array(), $selectOptions = array() )
- {
- $destTable = $this->tableName( $destTable );
- if ( is_array( $insertOptions ) ) {
- $insertOptions = implode( ' ', $insertOptions );
- }
- if( !is_array( $selectOptions ) ) {
- $selectOptions = array( $selectOptions );
- }
- list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
- if( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
- } else {
- $srcTable = $this->tableName( $srcTable );
- }
- $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
- " SELECT $startOpts " . implode( ',', $varMap ) .
- " FROM $srcTable $useIndex ";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
- }
- $sql .= " $tailOpts";
- return $this->query( $sql, $fname );
- }
-
- /**
- * Construct a LIMIT query with optional offset
- * This is used for query pages
- * $sql string SQL query we will append the limit too
- * $limit integer the SQL limit
- * $offset integer the SQL offset (default false)
- */
- function limitResult($sql, $limit, $offset=false) {
- if( !is_numeric($limit) ) {
- throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
- }
- return "$sql LIMIT "
- . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
- . "{$limit} ";
- }
- function limitResultForUpdate($sql, $num) {
- return $this->limitResult($sql, $num, 0);
- }
-
- /**
- * Returns an SQL expression for a simple conditional.
- * Uses IF on MySQL.
- *
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
- */
- function conditional( $cond, $trueVal, $falseVal ) {
- return " IF($cond, $trueVal, $falseVal) ";
- }
-
- /**
- * Returns a comand for str_replace function in SQL query.
- * Uses REPLACE() in MySQL
- *
- * @param string $orig String or column to modify
- * @param string $old String or column to seek
- * @param string $new String or column to replace with
- */
- function strreplace( $orig, $old, $new ) {
- return "REPLACE({$orig}, {$old}, {$new})";
- }
-
- /**
- * Determines if the last failure was due to a deadlock
- */
- function wasDeadlock() {
- return $this->lastErrno() == 1213;
- }
-
- /**
- * Perform a deadlock-prone transaction.
- *
- * This function invokes a callback function to perform a set of write
- * queries. If a deadlock occurs during the processing, the transaction
- * will be rolled back and the callback function will be called again.
- *
- * Usage:
- * $dbw->deadlockLoop( callback, ... );
- *
- * Extra arguments are passed through to the specified callback function.
- *
- * Returns whatever the callback function returned on its successful,
- * iteration, or false on error, for example if the retry limit was
- * reached.
- */
- function deadlockLoop() {
- $myFname = 'Database::deadlockLoop';
-
- $this->begin();
- $args = func_get_args();
- $function = array_shift( $args );
- $oldIgnore = $this->ignoreErrors( true );
- $tries = DEADLOCK_TRIES;
- if ( is_array( $function ) ) {
- $fname = $function[0];
- } else {
- $fname = $function;
- }
- do {
- $retVal = call_user_func_array( $function, $args );
- $error = $this->lastError();
- $errno = $this->lastErrno();
- $sql = $this->lastQuery();
-
- if ( $errno ) {
- if ( $this->wasDeadlock() ) {
- # Retry
- usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
- } else {
- $this->reportQueryError( $error, $errno, $sql, $fname );
- }
- }
- } while( $this->wasDeadlock() && --$tries > 0 );
- $this->ignoreErrors( $oldIgnore );
- if ( $tries <= 0 ) {
- $this->query( 'ROLLBACK', $myFname );
- $this->reportQueryError( $error, $errno, $sql, $fname );
- return false;
- } else {
- $this->query( 'COMMIT', $myFname );
- return $retVal;
- }
- }
-
- /**
- * Do a SELECT MASTER_POS_WAIT()
- *
- * @param string $file the binlog file
- * @param string $pos the binlog position
- * @param integer $timeout the maximum number of seconds to wait for synchronisation
- */
- function masterPosWait( MySQLMasterPos $pos, $timeout ) {
- $fname = 'Database::masterPosWait';
- wfProfileIn( $fname );
-
- # Commit any open transactions
- if ( $this->mTrxLevel ) {
- $this->immediateCommit();
- }
-
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
- if ( $wait > $timeout * 1e6 ) {
- wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
- wfProfileOut( $fname );
- return -1;
- } elseif ( $wait > 0 ) {
- wfDebug( "Fake slave waiting $wait us\n" );
- usleep( $wait );
- wfProfileOut( $fname );
- return 1;
- } else {
- wfDebug( "Fake slave up to date ($wait us)\n" );
- wfProfileOut( $fname );
- return 0;
- }
- }
-
- # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
- $encFile = $this->addQuotes( $pos->file );
- $encPos = intval( $pos->pos );
- $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
- $res = $this->doQuery( $sql );
- if ( $res && $row = $this->fetchRow( $res ) ) {
- $this->freeResult( $res );
- wfProfileOut( $fname );
- return $row[0];
- } else {
- wfProfileOut( $fname );
- return false;
- }
- }
-
- /**
- * Get the position of the master from SHOW SLAVE STATUS
- */
- function getSlavePos() {
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
- wfDebug( __METHOD__.": fake slave pos = $pos\n" );
- return $pos;
- }
- $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
- $row = $this->fetchObject( $res );
- if ( $row ) {
- return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
- } else {
- return false;
- }
- }
-
- /**
- * Get the position of the master from SHOW MASTER STATUS
- */
- function getMasterPos() {
- if ( $this->mFakeMaster ) {
- return new MySQLMasterPos( 'fake', microtime( true ) );
- }
- $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
- $row = $this->fetchObject( $res );
- if ( $row ) {
- return new MySQLMasterPos( $row->File, $row->Position );
- } else {
- return false;
- }
- }
-
- /**
- * Begin a transaction, committing any previously open transaction
- */
- function begin( $fname = 'Database::begin' ) {
- $this->query( 'BEGIN', $fname );
- $this->mTrxLevel = 1;
- }
-
- /**
- * End a transaction
- */
- function commit( $fname = 'Database::commit' ) {
- $this->query( 'COMMIT', $fname );
- $this->mTrxLevel = 0;
- }
-
- /**
- * Rollback a transaction.
- * No-op on non-transactional databases.
- */
- function rollback( $fname = 'Database::rollback' ) {
- $this->query( 'ROLLBACK', $fname, true );
- $this->mTrxLevel = 0;
- }
-
- /**
- * Begin a transaction, committing any previously open transaction
- * @deprecated use begin()
- */
- function immediateBegin( $fname = 'Database::immediateBegin' ) {
- $this->begin();
- }
-
- /**
- * Commit transaction, if one is open
- * @deprecated use commit()
- */
- function immediateCommit( $fname = 'Database::immediateCommit' ) {
- $this->commit();
- }
-
- /**
- * Return MW-style timestamp used for MySQL schema
- */
- function timestamp( $ts=0 ) {
- return wfTimestamp(TS_MW,$ts);
- }
-
- /**
- * Local database timestamp format or null
- */
- function timestampOrNull( $ts = null ) {
- if( is_null( $ts ) ) {
- return null;
- } else {
- return $this->timestamp( $ts );
- }
- }
-
- /**
- * @todo document
- */
- function resultObject( $result ) {
- if( empty( $result ) ) {
- return false;
- } elseif ( $result instanceof ResultWrapper ) {
- return $result;
- } elseif ( $result === true ) {
- // Successful write query
- return $result;
- } else {
- return new ResultWrapper( $this, $result );
- }
- }
-
- /**
- * Return aggregated value alias
- */
- function aggregateValue ($valuedata,$valuename='value') {
- return $valuename;
- }
-
- /**
- * @return string wikitext of a link to the server software's web site
- */
- function getSoftwareLink() {
- return "[http://www.mysql.com/ MySQL]";
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- return mysql_get_server_info( $this->mConn );
- }
-
- /**
- * Ping the server and try to reconnect if it there is no connection
- */
- function ping() {
- if( !function_exists( 'mysql_ping' ) ) {
- wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
- return true;
- }
- $ping = mysql_ping( $this->mConn );
- if ( $ping ) {
- return true;
- }
-
- // Need to reconnect manually in MySQL client 5.0.13+
- if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
- mysql_close( $this->mConn );
- $this->mOpened = false;
- $this->mConn = false;
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
- return true;
- }
- return false;
- }
-
- /**
- * Get slave lag.
- * At the moment, this will only work if the DB user has the PROCESS privilege
- */
- function getLag() {
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
- return $this->mFakeSlaveLag;
- }
- $res = $this->query( 'SHOW PROCESSLIST' );
- # Find slave SQL thread
- while ( $row = $this->fetchObject( $res ) ) {
- /* This should work for most situations - when default db
- * for thread is not specified, it had no events executed,
- * and therefore it doesn't know yet how lagged it is.
- *
- * Relay log I/O thread does not select databases.
- */
- if ( $row->User == 'system user' &&
- $row->State != 'Waiting for master to send event' &&
- $row->State != 'Connecting to master' &&
- $row->State != 'Queueing master event to the relay log' &&
- $row->State != 'Waiting for master update' &&
- $row->State != 'Requesting binlog dump'
- ) {
- # This is it, return the time (except -ve)
- if ( $row->Time > 0x7fffffff ) {
- return false;
- } else {
- return $row->Time;
- }
- }
- }
- return false;
- }
-
- /**
- * Get status information from SHOW STATUS in an associative array
- */
- function getStatus($which="%") {
- $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
- $status = array();
- while ( $row = $this->fetchObject( $res ) ) {
- $status[$row->Variable_name] = $row->Value;
- }
- return $status;
- }
-
- /**
- * Return the maximum number of items allowed in a list, or 0 for unlimited.
- */
- function maxListLen() {
- return 0;
- }
-
- function encodeBlob($b) {
- return $b;
- }
-
- function decodeBlob($b) {
- return $b;
- }
-
- /**
- * Override database's default connection timeout.
- * May be useful for very long batch queries such as
- * full-wiki dumps, where a single query reads out
- * over hours or days.
- * @param int $timeout in seconds
- */
- public function setTimeout( $timeout ) {
- $this->query( "SET net_read_timeout=$timeout" );
- $this->query( "SET net_write_timeout=$timeout" );
- }
-
- /**
- * Read and execute SQL commands from a file.
- * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
- * @param string $filename File name to open
- * @param callback $lineCallback Optional function called before reading each line
- * @param callback $resultCallback Optional function called for each MySQL result
- */
- function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
- $fp = fopen( $filename, 'r' );
- if ( false === $fp ) {
- throw new MWException( "Could not open \"{$filename}\".\n" );
- }
- $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
- fclose( $fp );
- return $error;
- }
-
- /**
- * Read and execute commands from an open file handle
- * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
- * @param string $fp File handle
- * @param callback $lineCallback Optional function called before reading each line
- * @param callback $resultCallback Optional function called for each MySQL result
- */
- function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
- $cmd = "";
- $done = false;
- $dollarquote = false;
-
- while ( ! feof( $fp ) ) {
- if ( $lineCallback ) {
- call_user_func( $lineCallback );
- }
- $line = trim( fgets( $fp, 1024 ) );
- $sl = strlen( $line ) - 1;
-
- if ( $sl < 0 ) { continue; }
- if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
-
- ## Allow dollar quoting for function declarations
- if (substr($line,0,4) == '$mw$') {
- if ($dollarquote) {
- $dollarquote = false;
- $done = true;
- }
- else {
- $dollarquote = true;
- }
- }
- else if (!$dollarquote) {
- if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
- $done = true;
- $line = substr( $line, 0, $sl );
- }
- }
-
- if ( '' != $cmd ) { $cmd .= ' '; }
- $cmd .= "$line\n";
-
- if ( $done ) {
- $cmd = str_replace(';;', ";", $cmd);
- $cmd = $this->replaceVars( $cmd );
- $res = $this->query( $cmd, __METHOD__ );
- if ( $resultCallback ) {
- call_user_func( $resultCallback, $res );
- }
-
- if ( false === $res ) {
- $err = $this->lastError();
- return "Query \"{$cmd}\" failed with error code \"$err\".\n";
- }
-
- $cmd = '';
- $done = false;
- }
- }
- return true;
- }
-
-
- /**
- * Replace variables in sourced SQL
- */
- protected function replaceVars( $ins ) {
- $varnames = array(
- 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
- 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
- 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
- );
-
- // Ordinary variables
- foreach ( $varnames as $var ) {
- if( isset( $GLOBALS[$var] ) ) {
- $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
- $ins = str_replace( '{$' . $var . '}', $val, $ins );
- $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
- $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
- }
- }
-
- // Table prefixes
- $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
- array( &$this, 'tableNameCallback' ), $ins );
- return $ins;
- }
-
- /**
- * Table name callback
- * @private
- */
- protected function tableNameCallback( $matches ) {
- return $this->tableName( $matches[1] );
- }
-
- /*
- * Build a concatenation list to feed into a SQL query
- */
- function buildConcat( $stringList ) {
- return 'CONCAT(' . implode( ',', $stringList ) . ')';
- }
-
- /**
- * Acquire a lock
- *
- * Abstracted from Filestore::lock() so child classes can implement for
- * their own needs.
- *
- * @param string $lockName Name of lock to aquire
- * @param string $method Name of method calling us
- * @return bool
- */
- public function lock( $lockName, $method ) {
- $lockName = $this->addQuotes( $lockName );
- $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
- $this->freeResult( $result );
-
- if( $row->lockstatus == 1 ) {
- return true;
- } else {
- wfDebug( __METHOD__." failed to acquire lock\n" );
- return false;
- }
- }
- /**
- * Release a lock.
- *
- * @todo fixme - Figure out a way to return a bool
- * based on successful lock release.
- *
- * @param string $lockName Name of lock to release
- * @param string $method Name of method calling us
- */
- public function unlock( $lockName, $method ) {
- $lockName = $this->addQuotes( $lockName );
- $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
- $this->freeResult( $result );
- }
-}
-
-/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
- *
- * @ingroup Database
- * @see Database
- */
-class DatabaseMysql extends Database {
- # Inherit all
-}
-
-/******************************************************************************
- * Utility classes
- *****************************************************************************/
-
-/**
- * Utility class.
- * @ingroup Database
- */
-class DBObject {
- public $mData;
-
- function DBObject($data) {
- $this->mData = $data;
- }
-
- function isLOB() {
- return false;
- }
-
- function data() {
- return $this->mData;
- }
-}
-
-/**
- * Utility class
- * @ingroup Database
- *
- * This allows us to distinguish a blob from a normal string and an array of strings
- */
-class Blob {
- private $mData;
- function __construct($data) {
- $this->mData = $data;
- }
- function fetch() {
- return $this->mData;
- }
-}
-
-/**
- * Utility class.
- * @ingroup Database
- */
-class MySQLField {
- private $name, $tablename, $default, $max_length, $nullable,
- $is_pk, $is_unique, $is_multiple, $is_key, $type;
- function __construct ($info) {
- $this->name = $info->name;
- $this->tablename = $info->table;
- $this->default = $info->def;
- $this->max_length = $info->max_length;
- $this->nullable = !$info->not_null;
- $this->is_pk = $info->primary_key;
- $this->is_unique = $info->unique_key;
- $this->is_multiple = $info->multiple_key;
- $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
- $this->type = $info->type;
- }
-
- function name() {
- return $this->name;
- }
-
- function tableName() {
- return $this->tableName;
- }
-
- function defaultValue() {
- return $this->default;
- }
-
- function maxLength() {
- return $this->max_length;
- }
-
- function nullable() {
- return $this->nullable;
- }
-
- function isKey() {
- return $this->is_key;
- }
-
- function isMultipleKey() {
- return $this->is_multiple;
- }
-
- function type() {
- return $this->type;
- }
-}
-
-/******************************************************************************
- * Error classes
- *****************************************************************************/
-
-/**
- * Database error base class
- * @ingroup Database
- */
-class DBError extends MWException {
- public $db;
-
- /**
- * Construct a database error
- * @param Database $db The database object which threw the error
- * @param string $error A simple error message to be used for debugging
- */
- function __construct( Database &$db, $error ) {
- $this->db =& $db;
- parent::__construct( $error );
- }
-}
-
-/**
- * @ingroup Database
- */
-class DBConnectionError extends DBError {
- public $error;
-
- function __construct( Database &$db, $error = 'unknown error' ) {
- $msg = 'DB connection error';
- if ( trim( $error ) != '' ) {
- $msg .= ": $error";
- }
- $this->error = $error;
- parent::__construct( $db, $msg );
- }
-
- function useOutputPage() {
- // Not likely to work
- return false;
- }
-
- function useMessageCache() {
- // Not likely to work
- return false;
- }
-
- function getText() {
- return $this->getMessage() . "\n";
- }
-
- function getLogMessage() {
- # Don't send to the exception log
- return false;
- }
-
- function getPageTitle() {
- global $wgSitename;
- return "$wgSitename has a problem";
- }
-
- function getHTML() {
- global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
- global $wgSitename, $wgServer, $wgMessageCache;
-
- # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
- # Hard coding strings instead.
-
- $noconnect = "
Sorry! This site is experiencing technical difficulties.
Try waiting a few minutes and reloading.
(Can't contact the database server: $1)
";
- $mainpage = 'Main Page';
- $searchdisabled = <<$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
-Note that their indexes of $wgSitename content may be out of date.
',
-EOT;
-
- $googlesearch = "
-
-
-";
- $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
-
- # No database access
- if ( is_object( $wgMessageCache ) ) {
- $wgMessageCache->disable();
- }
-
- if ( trim( $this->error ) == '' ) {
- $this->error = $this->db->getProperty('mServer');
- }
-
- $text = str_replace( '$1', $this->error, $noconnect );
- $text .= wfGetSiteNotice();
-
- if($wgUseFileCache) {
- if($wgTitle) {
- $t =& $wgTitle;
- } else {
- if($title) {
- $t = Title::newFromURL( $title );
- } elseif (@/**/$_REQUEST['search']) {
- $search = $_REQUEST['search'];
- return $searchdisabled .
- str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
- $wgInputEncoding ), $googlesearch );
- } else {
- $t = Title::newFromText( $mainpage );
- }
- }
-
- $cache = new HTMLFileCache( $t );
- if( $cache->isFileCached() ) {
- // @todo, FIXME: $msg is not defined on the next line.
- $msg = '
'.$msg." \n" .
- $cachederror . "
\n";
-
- $tag = '
';
- $text = str_replace(
- $tag,
- $tag . $msg,
- $cache->fetchPageText() );
- }
- }
-
- return $text;
- }
-}
-
-/**
- * @ingroup Database
- */
-class DBQueryError extends DBError {
- public $error, $errno, $sql, $fname;
-
- function __construct( Database &$db, $error, $errno, $sql, $fname ) {
- $message = "A database error has occurred\n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
-
- parent::__construct( $db, $message );
- $this->error = $error;
- $this->errno = $errno;
- $this->sql = $sql;
- $this->fname = $fname;
- }
-
- function getText() {
- if ( $this->useMessageCache() ) {
- return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
- htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
- } else {
- return $this->getMessage();
- }
- }
-
- function getSQL() {
- global $wgShowSQLErrors;
- if( !$wgShowSQLErrors ) {
- return $this->msg( 'sqlhidden', 'SQL hidden' );
- } else {
- return $this->sql;
- }
- }
-
- function getLogMessage() {
- # Don't send to the exception log
- return false;
- }
-
- function getPageTitle() {
- return $this->msg( 'databaseerror', 'Database error' );
- }
-
- function getHTML() {
- if ( $this->useMessageCache() ) {
- return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
- htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
- } else {
- return nl2br( htmlspecialchars( $this->getMessage() ) );
- }
- }
-}
-
-/**
- * @ingroup Database
- */
-class DBUnexpectedError extends DBError {}
-
-
-/**
- * Result wrapper for grabbing data queried by someone else
- * @ingroup Database
- */
-class ResultWrapper implements Iterator {
- var $db, $result, $pos = 0, $currentRow = null;
-
- /**
- * Create a new result object from a result resource and a Database object
- */
- function ResultWrapper( $database, $result ) {
- $this->db = $database;
- if ( $result instanceof ResultWrapper ) {
- $this->result = $result->result;
- } else {
- $this->result = $result;
- }
- }
-
- /**
- * Get the number of rows in a result object
- */
- function numRows() {
- return $this->db->numRows( $this->result );
- }
-
- /**
- * Fetch the next row from the given result object, in object form.
- * Fields can be retrieved with $row->fieldname, with fields acting like
- * member variables.
- *
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchObject() {
- return $this->db->fetchObject( $this->result );
- }
-
- /**
- * Fetch the next row from the given result object, in associative array
- * form. Fields are retrieved with $row['fieldname'].
- *
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchRow() {
- return $this->db->fetchRow( $this->result );
- }
-
- /**
- * Free a result object
- */
- function free() {
- $this->db->freeResult( $this->result );
- unset( $this->result );
- unset( $this->db );
- }
-
- /**
- * Change the position of the cursor in a result object
- * See mysql_data_seek()
- */
- function seek( $row ) {
- $this->db->dataSeek( $this->result, $row );
- }
-
- /*********************
- * Iterator functions
- * Note that using these in combination with the non-iterator functions
- * above may cause rows to be skipped or repeated.
- */
-
- function rewind() {
- if ($this->numRows()) {
- $this->db->dataSeek($this->result, 0);
- }
- $this->pos = 0;
- $this->currentRow = null;
- }
-
- function current() {
- if ( is_null( $this->currentRow ) ) {
- $this->next();
- }
- return $this->currentRow;
- }
-
- function key() {
- return $this->pos;
- }
-
- function next() {
- $this->pos++;
- $this->currentRow = $this->fetchObject();
- return $this->currentRow;
- }
-
- function valid() {
- return $this->current() !== false;
- }
-}
-
-class MySQLMasterPos {
- var $file, $pos;
-
- function __construct( $file, $pos ) {
- $this->file = $file;
- $this->pos = $pos;
- }
-
- function __toString() {
- return "{$this->file}/{$this->pos}";
- }
-}
diff --git a/includes/DatabaseMssql.php b/includes/DatabaseMssql.php
deleted file mode 100755
index a6dda2573f..0000000000
--- a/includes/DatabaseMssql.php
+++ /dev/null
@@ -1,1019 +0,0 @@
-mOut =& $wgOut;
- $this->mFailFunction = $failFunction;
- $this->mFlags = $flags;
-
- if ( $this->mFlags & DBO_DEFAULT ) {
- if ( $wgCommandLineMode ) {
- $this->mFlags &= ~DBO_TRX;
- } else {
- $this->mFlags |= DBO_TRX;
- }
- }
-
- /** Get the default table prefix*/
- $this->mTablePrefix = $tablePrefix == 'get from global' ? $wgDBprefix : $tablePrefix;
-
- if ($server) $this->open($server, $user, $password, $dbName);
-
- }
-
- /**
- * todo: check if these should be true like parent class
- */
- function implicitGroupby() { return false; }
- function implicitOrderby() { return false; }
-
- static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
- return new DatabaseMssql($server, $user, $password, $dbName, $failFunction, $flags);
- }
-
- /** Open an MSSQL database and return a resource handle to it
- * NOTE: only $dbName is used, the other parameters are irrelevant for MSSQL databases
- */
- function open($server,$user,$password,$dbName) {
- wfProfileIn(__METHOD__);
-
- # Test for missing mysql.so
- # First try to load it
- if (!@extension_loaded('mssql')) {
- @dl('mssql.so');
- }
-
- # Fail now
- # Otherwise we get a suppressed fatal error, which is very hard to track down
- if (!function_exists( 'mssql_connect')) {
- throw new DBConnectionError( $this, "MSSQL functions missing, have you compiled PHP with the --with-mssql option?\n" );
- }
-
- $this->close();
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- wfProfileIn("dbconnect-$server");
-
- # Try to connect up to three times
- # The kernel's default SYN retransmission period is far too slow for us,
- # so we use a short timeout plus a manual retry.
- $this->mConn = false;
- $max = 3;
- for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
- if ( $i > 1 ) {
- usleep( 1000 );
- }
- if ($this->mFlags & DBO_PERSISTENT) {
- @/**/$this->mConn = mssql_pconnect($server, $user, $password);
- } else {
- # Create a new connection...
- @/**/$this->mConn = mssql_connect($server, $user, $password, true);
- }
- }
-
- wfProfileOut("dbconnect-$server");
-
- if ($dbName != '') {
- if ($this->mConn !== false) {
- $success = @/**/mssql_select_db($dbName, $this->mConn);
- if (!$success) {
- $error = "Error selecting database $dbName on server {$this->mServer} " .
- "from client host {$wguname['nodename']}\n";
- wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
- wfDebug( $error );
- }
- } else {
- wfDebug("DB connection error\n");
- wfDebug("Server: $server, User: $user, Password: ".substr($password, 0, 3)."...\n");
- $success = false;
- }
- } else {
- # Delay USE query
- $success = (bool)$this->mConn;
- }
-
- if (!$success) $this->reportConnectionError();
- $this->mOpened = $success;
- wfProfileOut(__METHOD__);
- return $success;
- }
-
- /**
- * Close an MSSQL database
- */
- function close() {
- $this->mOpened = false;
- if ($this->mConn) {
- if ($this->trxLevel()) $this->immediateCommit();
- return mssql_close($this->mConn);
- } else return true;
- }
-
- /**
- * - MSSQL doesn't seem to do buffered results
- * - the trasnaction syntax is modified here to avoid having to replicate
- * Database::query which uses BEGIN, COMMIT, ROLLBACK
- */
- function doQuery($sql) {
- if ($sql == 'BEGIN' || $sql == 'COMMIT' || $sql == 'ROLLBACK') return true; # $sql .= ' TRANSACTION';
- $sql = preg_replace('|[^\x07-\x7e]|','?',$sql); # TODO: need to fix unicode - just removing it here while testing
- $ret = mssql_query($sql, $this->mConn);
- if ($ret === false) {
- $err = mssql_get_last_message();
- if ($err) $this->mlastError = $err;
- $row = mssql_fetch_row(mssql_query('select @@ERROR'));
- if ($row[0]) $this->mlastErrorNo = $row[0];
- } else $this->mlastErrorNo = false;
- return $ret;
- }
-
- /**#@+
- * @param mixed $res A SQL result
- */
- /**
- * Free a result object
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- if ( !@/**/mssql_free_result( $res ) ) {
- throw new DBUnexpectedError( $this, "Unable to free MSSQL result" );
- }
- }
-
- /**
- * Fetch the next row from the given result object, in object form.
- * Fields can be retrieved with $row->fieldname, with fields acting like
- * member variables.
- *
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$row = mssql_fetch_object( $res );
- if ( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * Fetch the next row from the given result object, in associative array
- * form. Fields are retrieved with $row['fieldname'].
- *
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$row = mssql_fetch_array( $res );
- if ( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * Get the number of rows in a result object
- */
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$n = mssql_num_rows( $res );
- if ( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $n;
- }
-
- /**
- * Get the number of fields in a result object
- * See documentation for mysql_num_fields()
- */
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mssql_num_fields( $res );
- }
-
- /**
- * Get a field name in a result object
- * See documentation for mysql_field_name():
- * http://www.php.net/mysql_field_name
- */
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mssql_field_name( $res, $n );
- }
-
- /**
- * Get the inserted value of an auto-increment row
- *
- * The value inserted should be fetched from nextSequenceValue()
- *
- * Example:
- * $id = $dbw->nextSequenceValue('page_page_id_seq');
- * $dbw->insert('page',array('page_id' => $id));
- * $id = $dbw->insertId();
- */
- function insertId() {
- $row = mssql_fetch_row(mssql_query('select @@IDENTITY'));
- return $row[0];
- }
-
- /**
- * Change the position of the cursor in a result object
- * See mysql_data_seek()
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mssql_data_seek( $res, $row );
- }
-
- /**
- * Get the last error number
- */
- function lastErrno() {
- return $this->mlastErrorNo;
- }
-
- /**
- * Get a description of the last error
- */
- function lastError() {
- return $this->mlastError;
- }
-
- /**
- * Get the number of rows affected by the last write query
- */
- function affectedRows() {
- return mssql_rows_affected( $this->mConn );
- }
-
- /**
- * Simple UPDATE wrapper
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- *
- * This function exists for historical reasons, Database::update() has a more standard
- * calling convention and feature set
- */
- function set( $table, $var, $value, $cond, $fname = 'Database::set' )
- {
- if ($value == "NULL") $value = "''"; # see comments in makeListWithoutNulls()
- $table = $this->tableName( $table );
- $sql = "UPDATE $table SET $var = '" .
- $this->strencode( $value ) . "' WHERE ($cond)";
- return (bool)$this->query( $sql, $fname );
- }
-
- /**
- * Simple SELECT wrapper, returns a single field, input must be encoded
- * Usually aborts on failure
- * If errors are explicitly ignored, returns FALSE on failure
- */
- function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
- if ( !is_array( $options ) ) {
- $options = array( $options );
- }
- $options['LIMIT'] = 1;
-
- $res = $this->select( $table, $var, $cond, $fname, $options );
- if ( $res === false || !$this->numRows( $res ) ) {
- return false;
- }
- $row = $this->fetchRow( $res );
- if ( $row !== false ) {
- $this->freeResult( $res );
- return $row[0];
- } else {
- return false;
- }
- }
-
- /**
- * Returns an optional USE INDEX clause to go after the table, and a
- * string to go at the end of the query
- *
- * @private
- *
- * @param array $options an associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = '';
-
- $noKeyOptions = array();
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
- if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
- if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
-
- //if (isset($options['LIMIT'])) {
- // $tailOpts .= $this->limitResult('', $options['LIMIT'],
- // isset($options['OFFSET']) ? $options['OFFSET']
- // : false);
- //}
-
- if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
- if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
- # Various MySQL extensions
- if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
- if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
- if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
- if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
- if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
- if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
- if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
- if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
-
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
- $useIndex = $this->useIndexClause( $options['USE INDEX'] );
- } else {
- $useIndex = '';
- }
-
- return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
- }
-
- /**
- * SELECT wrapper
- *
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
- * see Database::makeSelectOptions code for list of supported stuff
- * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
- */
- function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
- {
- if( is_array( $vars ) ) {
- $vars = implode( ',', $vars );
- }
- if( !is_array( $options ) ) {
- $options = array( $options );
- }
- if( is_array( $table ) ) {
- if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
- $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
- else
- $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
- } elseif ($table!='') {
- if ($table{0}==' ') {
- $from = ' FROM ' . $table;
- } else {
- $from = ' FROM ' . $this->tableName( $table );
- }
- } else {
- $from = '';
- }
-
- list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
-
- if( !empty( $conds ) ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
- }
- $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
- } else {
- $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
- }
-
- if (isset($options['LIMIT']))
- $sql = $this->limitResult($sql, $options['LIMIT'],
- isset($options['OFFSET']) ? $options['OFFSET'] : false);
- $sql = "$sql $postLimitTail";
-
- if (isset($options['EXPLAIN'])) {
- $sql = 'EXPLAIN ' . $sql;
- }
- return $this->query( $sql, $fname );
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * Takes same arguments as Database::select()
- */
- function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
- $rows = 0;
- $res = $this->select ($table, 'COUNT(*)', $conds, $fname, $options );
- if ($res) {
- $row = $this->fetchObject($res);
- $rows = $row[0];
- }
- $this->freeResult($res);
- return $rows;
- }
-
- /**
- * Determines whether a field exists in a table
- * Usually aborts on failure
- * If errors are explicitly ignored, returns NULL on failure
- */
- function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
- $table = $this->tableName( $table );
- $sql = "SELECT TOP 1 * FROM $table";
- $res = $this->query( $sql, 'Database::fieldExists' );
-
- $found = false;
- while ( $row = $this->fetchArray( $res ) ) {
- if ( isset($row[$field]) ) {
- $found = true;
- break;
- }
- }
-
- $this->freeResult( $res );
- return $found;
- }
-
- /**
- * Get information about an index into an object
- * Returns false if the index does not exist
- */
- function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
-
- throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' );
- return NULL;
-
- $table = $this->tableName( $table );
- $sql = 'SHOW INDEX FROM '.$table;
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return NULL;
- }
-
- $result = array();
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->Key_name == $index ) {
- $result[] = $row;
- }
- }
- $this->freeResult($res);
-
- return empty($result) ? false : $result;
- }
-
- /**
- * Query whether a given table exists
- */
- function tableExists( $table ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '$table'" );
- $exist = ($res->numRows() > 0);
- $this->freeResult($res);
- return $exist;
- }
-
- /**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
- *
- * @param $table
- * @param $field
- */
- function fieldInfo( $table, $field ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT TOP 1 * FROM $table" );
- $n = mssql_num_fields( $res->result );
- for( $i = 0; $i < $n; $i++ ) {
- $meta = mssql_fetch_field( $res->result, $i );
- if( $field == $meta->name ) {
- return new MSSQLField($meta);
- }
- }
- return false;
- }
-
- /**
- * mysql_field_type() wrapper
- */
- function fieldType( $res, $index ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mssql_field_type( $res, $index );
- }
-
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $a may be a single associative array, or an array of these with numeric keys, for
- * multi-row insert.
- *
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- *
- * Same as parent class implementation except that it removes primary key from column lists
- * because MSSQL doesn't support writing nulls to IDENTITY (AUTO_INCREMENT) columns
- */
- function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
- # No rows to insert, easy just return now
- if ( !count( $a ) ) {
- return true;
- }
- $table = $this->tableName( $table );
- if ( !is_array( $options ) ) {
- $options = array( $options );
- }
-
- # todo: need to record primary keys at table create time, and remove NULL assignments to them
- if ( isset( $a[0] ) && is_array( $a[0] ) ) {
- $multi = true;
- $keys = array_keys( $a[0] );
-# if (ereg('_id$',$keys[0])) {
- foreach ($a as $i) {
- if (is_null($i[$keys[0]])) unset($i[$keys[0]]); # remove primary-key column from multiple insert lists if empty value
- }
-# }
- $keys = array_keys( $a[0] );
- } else {
- $multi = false;
- $keys = array_keys( $a );
-# if (ereg('_id$',$keys[0]) && empty($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
- if (is_null($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
- $keys = array_keys( $a );
- }
-
- # handle IGNORE option
- # example:
- # MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
- # MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
- $ignore = in_array('IGNORE',$options);
-
- # remove IGNORE from options list
- if ($ignore) {
- $oldoptions = $options;
- $options = array();
- foreach ($oldoptions as $o) if ($o != 'IGNORE') $options[] = $o;
- }
-
- $keylist = implode(',', $keys);
- $sql = 'INSERT '.implode(' ', $options)." INTO $table (".implode(',', $keys).') VALUES ';
- if ($multi) {
- if ($ignore) {
- # If multiple and ignore, then do each row as a separate conditional insert
- foreach ($a as $row) {
- $prival = $row[$keys[0]];
- $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
- if (!$this->query("$sql (".$this->makeListWithoutNulls($row).')', $fname)) return false;
- }
- return true;
- } else {
- $first = true;
- foreach ($a as $row) {
- if ($first) $first = false; else $sql .= ',';
- $sql .= '('.$this->makeListWithoutNulls($row).')';
- }
- }
- } else {
- if ($ignore) {
- $prival = $a[$keys[0]];
- $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
- }
- $sql .= '('.$this->makeListWithoutNulls($a).')';
- }
- return (bool)$this->query( $sql, $fname );
- }
-
- /**
- * MSSQL doesn't allow implicit casting of NULL's into non-null values for NOT NULL columns
- * for now I've just converted the NULL's in the lists for updates and inserts into empty strings
- * which get implicitly casted to 0 for numeric columns
- * NOTE: the set() method above converts NULL to empty string as well but not via this method
- */
- function makeListWithoutNulls($a, $mode = LIST_COMMA) {
- return str_replace("NULL","''",$this->makeList($a,$mode));
- }
-
- /**
- * UPDATE wrapper, takes a condition array and a SET array
- *
- * @param string $table The table to UPDATE
- * @param array $values An array of values to SET
- * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
- * @param string $fname The Class::Function calling this function
- * (for the log)
- * @param array $options An array of UPDATE options, can be one or
- * more of IGNORE, LOW_PRIORITY
- * @return bool
- */
- function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
- $table = $this->tableName( $table );
- $opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeListWithoutNulls( $values, LIST_SET );
- if ( $conds != '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
- }
- return $this->query( $sql, $fname );
- }
-
- /**
- * Make UPDATE options for the Database::update function
- *
- * @private
- * @param array $options The options passed to Database::update
- * @return string
- */
- function makeUpdateOptions( $options ) {
- if( !is_array( $options ) ) {
- $options = array( $options );
- }
- $opts = array();
- if ( in_array( 'LOW_PRIORITY', $options ) )
- $opts[] = $this->lowPriorityOption();
- if ( in_array( 'IGNORE', $options ) )
- $opts[] = 'IGNORE';
- return implode(' ', $opts);
- }
-
- /**
- * Change the current database
- */
- function selectDB( $db ) {
- $this->mDBname = $db;
- return mssql_select_db( $db, $this->mConn );
- }
-
- /**
- * MSSQL has a problem with the backtick quoting, so all this does is ensure the prefix is added exactly once
- */
- function tableName($name) {
- return strpos($name, $this->mTablePrefix) === 0 ? $name : "{$this->mTablePrefix}$name";
- }
-
- /**
- * MSSQL doubles quotes instead of escaping them
- * @param string $s String to be slashed.
- * @return string slashed string.
- */
- function strencode($s) {
- return str_replace("'","''",$s);
- }
-
- /**
- * USE INDEX clause
- */
- function useIndexClause( $index ) {
- return "";
- }
-
- /**
- * REPLACE query wrapper
- * PostgreSQL simulates this with a DELETE followed by INSERT
- * $row is the row to insert, an associative array
- * $uniqueIndexes is an array of indexes. Each element may be either a
- * field name or an array of field names
- *
- * It may be more efficient to leave off unique indexes which are unlikely to collide.
- * However if you do this, you run the risk of encountering errors which wouldn't have
- * occurred in MySQL
- *
- * @todo migrate comment to phodocumentor format
- */
- function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
- $table = $this->tableName( $table );
-
- # Single row case
- if ( !is_array( reset( $rows ) ) ) {
- $rows = array( $rows );
- }
-
- $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
- $first = true;
- foreach ( $rows as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- return $this->query( $sql, $fname );
- }
-
- /**
- * DELETE where the condition is a join
- * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
- *
- * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
- * join condition matches, set $conds='*'
- *
- * DO NOT put the join condition in $conds
- *
- * @param string $delTable The table to delete from.
- * @param string $joinTable The other table.
- * @param string $delVar The variable to join on, in the first table.
- * @param string $joinVar The variable to join on, in the second table.
- * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
- */
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
- if ( $conds != '*' ) {
- $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
- }
-
- return $this->query( $sql, $fname );
- }
-
- /**
- * Returns the size of a text field, or -1 for "unlimited"
- */
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SELECT TOP 1 * FROM $table;";
- $res = $this->query( $sql, 'Database::textFieldSize' );
- $row = $this->fetchObject( $res );
- $this->freeResult( $res );
-
- $m = array();
- if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
- $size = $m[1];
- } else {
- $size = -1;
- }
- return $size;
- }
-
- /**
- * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
- */
- function lowPriorityOption() {
- return 'LOW_PRIORITY';
- }
-
- /**
- * INSERT SELECT wrapper
- * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
- * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
- * $conds may be "*" to copy the whole table
- * srcTable may be an array of tables.
- */
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
- $insertOptions = array(), $selectOptions = array() )
- {
- $destTable = $this->tableName( $destTable );
- if ( is_array( $insertOptions ) ) {
- $insertOptions = implode( ' ', $insertOptions );
- }
- if( !is_array( $selectOptions ) ) {
- $selectOptions = array( $selectOptions );
- }
- list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
- if( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
- } else {
- $srcTable = $this->tableName( $srcTable );
- }
- $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
- " SELECT $startOpts " . implode( ',', $varMap ) .
- " FROM $srcTable $useIndex ";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
- }
- $sql .= " $tailOpts";
- return $this->query( $sql, $fname );
- }
-
- /**
- * Construct a LIMIT query with optional offset
- * This is used for query pages
- * $sql string SQL query we will append the limit to
- * $limit integer the SQL limit
- * $offset integer the SQL offset (default false)
- */
- function limitResult($sql, $limit, $offset=false) {
- if( !is_numeric($limit) ) {
- throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
- }
- if ($offset) {
- throw new DBUnexpectedError( $this, 'Database::limitResult called with non-zero offset which is not supported yet' );
- } else {
- $sql = ereg_replace("^SELECT", "SELECT TOP $limit", $sql);
- }
- return $sql;
- }
-
- /**
- * Returns an SQL expression for a simple conditional.
- *
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
- */
- function conditional( $cond, $trueVal, $falseVal ) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
- /**
- * Should determine if the last failure was due to a deadlock
- * - don't know how to do this in MSSQL
- */
- function wasDeadlock() {
- return false;
- }
-
- /**
- * Begin a transaction, committing any previously open transaction
- * @deprecated use begin()
- */
- function immediateBegin( $fname = 'Database::immediateBegin' ) {
- $this->begin();
- }
-
- /**
- * Commit transaction, if one is open
- * @deprecated use commit()
- */
- function immediateCommit( $fname = 'Database::immediateCommit' ) {
- $this->commit();
- }
-
- /**
- * Return MW-style timestamp used for MySQL schema
- */
- function timestamp( $ts=0 ) {
- return wfTimestamp(TS_MW,$ts);
- }
-
- /**
- * Local database timestamp format or null
- */
- function timestampOrNull( $ts = null ) {
- if( is_null( $ts ) ) {
- return null;
- } else {
- return $this->timestamp( $ts );
- }
- }
-
- /**
- * @return string wikitext of a link to the server software's web site
- */
- function getSoftwareLink() {
- return "[http://www.microsoft.com/sql/default.mspx Microsoft SQL Server 2005 Home]";
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- $row = mssql_fetch_row(mssql_query('select @@VERSION'));
- return ereg("^(.+[0-9]+\\.[0-9]+\\.[0-9]+) ",$row[0],$m) ? $m[1] : $row[0];
- }
-
- function limitResultForUpdate($sql, $num) {
- return $sql;
- }
-
- /**
- * not done
- */
- public function setTimeout($timeout) { return; }
-
- function ping() {
- wfDebug("Function ping() not written for MSSQL yet");
- return true;
- }
-
- /**
- * How lagged is this slave?
- */
- public function getLag() {
- return 0;
- }
-
- /**
- * Called by the installer script
- * - this is the same way as DatabasePostgresql.php, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
- */
- public function setup_database() {
- global $IP,$wgDBTableOptions;
- $wgDBTableOptions = '';
- $mysql_tmpl = "$IP/maintenance/tables.sql";
- $mysql_iw = "$IP/maintenance/interwiki.sql";
- $mssql_tmpl = "$IP/maintenance/mssql/tables.sql";
-
- # Make an MSSQL template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
- if (!file_exists($mssql_tmpl)) { # todo: make this conditional again
- $sql = file_get_contents($mysql_tmpl);
- $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
- $sql = preg_replace('/^\s*(UNIQUE )?(INDEX|KEY|FULLTEXT).+?$/m', '', $sql); # These indexes should be created with a CREATE INDEX query
- $sql = preg_replace('/(\sKEY) [^\(]+\(/is', '$1 (', $sql); # "KEY foo (foo)" should just be "KEY (foo)"
- $sql = preg_replace('/(varchar\([0-9]+\))\s+binary/i', '$1', $sql); # "varchar(n) binary" cannot be followed by "binary"
- $sql = preg_replace('/(var)?binary\(([0-9]+)\)/ie', '"varchar(".strlen(pow(2,$2)).")"', $sql); # use varchar(chars) not binary(bits)
- $sql = preg_replace('/ (var)?binary/i', ' varchar', $sql); # use varchar not binary
- $sql = preg_replace('/(varchar\([0-9]+\)(?! N))/', '$1 NULL', $sql); # MSSQL complains if NULL is put into a varchar
- #$sql = preg_replace('/ binary/i',' varchar',$sql); # MSSQL binary's can't be assigned with strings, so use varchar's instead
- #$sql = preg_replace('/(binary\([0-9]+\) (NOT NULL )?default) [\'"].*?[\'"]/i','$1 0',$sql); # binary default cannot be string
- $sql = preg_replace('/[a-z]*(blob|text)([ ,])/i', 'text$2', $sql); # no BLOB types in MSSQL
- $sql = preg_replace('/\).+?;/',');', $sql); # remove all table options
- $sql = preg_replace('/ (un)?signed/i', '', $sql);
- $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
- $sql = str_replace(' bool ', ' bit ', $sql);
- $sql = str_replace('auto_increment', 'IDENTITY(1,1)', $sql);
- #$sql = preg_replace('/NOT NULL(?! IDENTITY)/', 'NULL', $sql); # Allow NULL's for non IDENTITY columns
-
- # Tidy up and write file
- $sql = preg_replace('/,\s*\)/s', "\n)", $sql); # Remove spurious commas left after INDEX removals
- $sql = preg_replace('/^\s*^/m', '', $sql); # Remove empty lines
- $sql = preg_replace('/;$/m', ";\n", $sql); # Separate each statement with an empty line
- file_put_contents($mssql_tmpl, $sql);
- }
-
- # Parse the MSSQL template replacing inline variables such as /*$wgDBprefix*/
- $err = $this->sourceFile($mssql_tmpl);
- if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
-
- # Use DatabasePostgres's code to populate interwiki from MySQL template
- $f = fopen($mysql_iw,'r');
- if ($f == false) dieout("
Could not find the interwiki.sql file");
- $sql = "INSERT INTO {$this->mTablePrefix}interwiki(iw_prefix,iw_url,iw_local) VALUES ";
- while (!feof($f)) {
- $line = fgets($f,1024);
- $matches = array();
- if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
- $this->query("$sql $matches[1],$matches[2])");
- }
- }
-
-}
-
-/**
- * @ingroup Database
- */
-class MSSQLField extends MySQLField {
-
- function __construct() {
- }
-
- static function fromText($db, $table, $field) {
- $n = new MSSQLField;
- $n->name = $field;
- $n->tablename = $table;
- return $n;
- }
-
-} // end DatabaseMssql class
-
diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php
deleted file mode 100644
index 8d2a6750dc..0000000000
--- a/includes/DatabaseOracle.php
+++ /dev/null
@@ -1,710 +0,0 @@
-mData = $data;
- }
-
- function getData() {
- return $this->mData;
- }
-}
-
-/**
- * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
- * other things. We use a wrapper class to handle that and other
- * Oracle-specific bits, like converting column names back to lowercase.
- * @ingroup Database
- */
-class ORAResult {
- private $rows;
- private $cursor;
- private $stmt;
- private $nrows;
- private $db;
-
- function __construct(&$db, $stmt) {
- $this->db =& $db;
- if (($this->nrows = oci_fetch_all($stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM)) === false) {
- $e = oci_error($stmt);
- $db->reportQueryError($e['message'], $e['code'], '', __FUNCTION__);
- return;
- }
-
- $this->cursor = 0;
- $this->stmt = $stmt;
- }
-
- function free() {
- oci_free_statement($this->stmt);
- }
-
- function seek($row) {
- $this->cursor = min($row, $this->nrows);
- }
-
- function numRows() {
- return $this->nrows;
- }
-
- function numFields() {
- return oci_num_fields($this->stmt);
- }
-
- function fetchObject() {
- if ($this->cursor >= $this->nrows)
- return false;
-
- $row = $this->rows[$this->cursor++];
- $ret = new stdClass();
- foreach ($row as $k => $v) {
- $lc = strtolower(oci_field_name($this->stmt, $k + 1));
- $ret->$lc = $v;
- }
-
- return $ret;
- }
-
- function fetchAssoc() {
- if ($this->cursor >= $this->nrows)
- return false;
-
- $row = $this->rows[$this->cursor++];
- $ret = array();
- foreach ($row as $k => $v) {
- $lc = strtolower(oci_field_name($this->stmt, $k + 1));
- $ret[$lc] = $v;
- $ret[$k] = $v;
- }
- return $ret;
- }
-}
-
-/**
- * @ingroup Database
- */
-class DatabaseOracle extends Database {
- var $mInsertId = NULL;
- var $mLastResult = NULL;
- var $numeric_version = NULL;
- var $lastResult = null;
- var $cursor = 0;
- var $mAffectedRows;
-
- function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false,
- $failFunction = false, $flags = 0 )
- {
-
- global $wgOut;
- # Can't get a reference if it hasn't been set yet
- if ( !isset( $wgOut ) ) {
- $wgOut = NULL;
- }
- $this->mOut =& $wgOut;
- $this->mFailFunction = $failFunction;
- $this->mFlags = $flags;
- $this->open( $server, $user, $password, $dbName);
-
- }
-
- function cascadingDeletes() {
- return true;
- }
- function cleanupTriggers() {
- return true;
- }
- function strictIPs() {
- return true;
- }
- function realTimestamps() {
- return true;
- }
- function implicitGroupby() {
- return false;
- }
- function implicitOrderby() {
- return false;
- }
- function searchableIPs() {
- return true;
- }
-
- static function newFromParams( $server = false, $user = false, $password = false, $dbName = false,
- $failFunction = false, $flags = 0)
- {
- return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags );
- }
-
- /**
- * Usually aborts on failure
- * If the failFunction is set to a non-zero integer, returns success
- */
- function open( $server, $user, $password, $dbName ) {
- if ( !function_exists( 'oci_connect' ) ) {
- throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
- }
-
- # Needed for proper UTF-8 functionality
- putenv("NLS_LANG=AMERICAN_AMERICA.AL32UTF8");
-
- $this->close();
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- if (!strlen($user)) { ## e.g. the class is being loaded
- return;
- }
-
- error_reporting( E_ALL );
- $this->mConn = oci_connect($user, $password, $dbName);
-
- if ($this->mConn == false) {
- wfDebug("DB connection error\n");
- wfDebug("Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n");
- wfDebug($this->lastError()."\n");
- return false;
- }
-
- $this->mOpened = true;
- return $this->mConn;
- }
-
- /**
- * Closes a database connection, if it is open
- * Returns success, true if already closed
- */
- function close() {
- $this->mOpened = false;
- if ( $this->mConn ) {
- return oci_close( $this->mConn );
- } else {
- return true;
- }
- }
-
- function execFlags() {
- return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS;
- }
-
- function doQuery($sql) {
- wfDebug("SQL: [$sql]\n");
- if (!mb_check_encoding($sql)) {
- throw new MWException("SQL encoding is invalid");
- }
-
- if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) {
- $e = oci_error($this->mConn);
- $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
- }
-
- if (oci_execute($stmt, $this->execFlags()) == false) {
- $e = oci_error($stmt);
- $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
- }
- if (oci_statement_type($stmt) == "SELECT")
- return new ORAResult($this, $stmt);
- else {
- $this->mAffectedRows = oci_num_rows($stmt);
- return true;
- }
- }
-
- function queryIgnore($sql, $fname = '') {
- return $this->query($sql, $fname, true);
- }
-
- function freeResult($res) {
- $res->free();
- }
-
- function fetchObject($res) {
- return $res->fetchObject();
- }
-
- function fetchRow($res) {
- return $res->fetchAssoc();
- }
-
- function numRows($res) {
- return $res->numRows();
- }
-
- function numFields($res) {
- return $res->numFields();
- }
-
- function fieldName($stmt, $n) {
- return pg_field_name($stmt, $n);
- }
-
- /**
- * This must be called after nextSequenceVal
- */
- function insertId() {
- return $this->mInsertId;
- }
-
- function dataSeek($res, $row) {
- $res->seek($row);
- }
-
- function lastError() {
- if ($this->mConn === false)
- $e = oci_error();
- else
- $e = oci_error($this->mConn);
- return $e['message'];
- }
-
- function lastErrno() {
- if ($this->mConn === false)
- $e = oci_error();
- else
- $e = oci_error($this->mConn);
- return $e['code'];
- }
-
- function affectedRows() {
- return $this->mAffectedRows;
- }
-
- /**
- * Returns information about an index
- * If errors are explicitly ignored, returns NULL on failure
- */
- function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
- return false;
- }
-
- function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
- return false;
- }
-
- function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
- if (!is_array($options))
- $options = array($options);
-
- #if (in_array('IGNORE', $options))
- # $oldIgnore = $this->ignoreErrors(true);
-
- # IGNORE is performed using single-row inserts, ignoring errors in each
- # FIXME: need some way to distiguish between key collision and other types of error
- //$oldIgnore = $this->ignoreErrors(true);
- if (!is_array(reset($a))) {
- $a = array($a);
- }
- foreach ($a as $row) {
- $this->insertOneRow($table, $row, $fname);
- }
- //$this->ignoreErrors($oldIgnore);
- $retVal = true;
-
- //if (in_array('IGNORE', $options))
- // $this->ignoreErrors($oldIgnore);
-
- return $retVal;
- }
-
- function insertOneRow($table, $row, $fname) {
- // "INSERT INTO tables (a, b, c)"
- $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')';
- $sql .= " VALUES (";
-
- // for each value, append ":key"
- $first = true;
- $returning = '';
- foreach ($row as $col => $val) {
- if (is_object($val)) {
- $what = "EMPTY_BLOB()";
- assert($returning === '');
- $returning = " RETURNING $col INTO :bval";
- $blobcol = $col;
- } else
- $what = ":$col";
-
- if ($first)
- $sql .= "$what";
- else
- $sql.= ", $what";
- $first = false;
- }
- $sql .= ") $returning";
-
- $stmt = oci_parse($this->mConn, $sql);
- foreach ($row as $col => $val) {
- if (!is_object($val)) {
- if (oci_bind_by_name($stmt, ":$col", $row[$col]) === false)
- $this->reportQueryError($this->lastErrno(), $this->lastError(), $sql, __METHOD__);
- }
- }
-
- if (($bval = oci_new_descriptor($this->mConn, OCI_D_LOB)) === false) {
- $e = oci_error($stmt);
- throw new DBUnexpectedError($this, "Cannot create LOB descriptor: " . $e['message']);
- }
-
- if (strlen($returning))
- oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB);
-
- if (oci_execute($stmt, OCI_DEFAULT) === false) {
- $e = oci_error($stmt);
- $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__);
- }
- if (strlen($returning)) {
- $bval->save($row[$blobcol]->getData());
- $bval->free();
- }
- if (!$this->mTrxLevel)
- oci_commit($this->mConn);
-
- oci_free_statement($stmt);
- }
-
- function tableName( $name ) {
- # Replace reserved words with better ones
- switch( $name ) {
- case 'user':
- return 'mwuser';
- case 'text':
- return 'pagecontent';
- default:
- return $name;
- }
- }
-
- /**
- * Return the next in a sequence, save the value for retrieval via insertId()
- */
- function nextSequenceValue($seqName) {
- $res = $this->query("SELECT $seqName.nextval FROM dual");
- $row = $this->fetchRow($res);
- $this->mInsertId = $row[0];
- $this->freeResult($res);
- return $this->mInsertId;
- }
-
- /**
- * Oracle does not have a "USE INDEX" clause, so return an empty string
- */
- function useIndexClause($index) {
- return '';
- }
-
- # REPLACE query wrapper
- # Oracle simulates this with a DELETE followed by INSERT
- # $row is the row to insert, an associative array
- # $uniqueIndexes is an array of indexes. Each element may be either a
- # field name or an array of field names
- #
- # It may be more efficient to leave off unique indexes which are unlikely to collide.
- # However if you do this, you run the risk of encountering errors which wouldn't have
- # occurred in MySQL
- function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
- $table = $this->tableName($table);
-
- if (count($rows)==0) {
- return;
- }
-
- # Single row case
- if (!is_array(reset($rows))) {
- $rows = array($rows);
- }
-
- foreach( $rows as $row ) {
- # Delete rows which collide
- if ( $uniqueIndexes ) {
- $sql = "DELETE FROM $table WHERE ";
- $first = true;
- foreach ( $uniqueIndexes as $index ) {
- if ( $first ) {
- $first = false;
- $sql .= "(";
- } else {
- $sql .= ') OR (';
- }
- if ( is_array( $index ) ) {
- $first2 = true;
- foreach ( $index as $col ) {
- if ( $first2 ) {
- $first2 = false;
- } else {
- $sql .= ' AND ';
- }
- $sql .= $col.'=' . $this->addQuotes( $row[$col] );
- }
- } else {
- $sql .= $index.'=' . $this->addQuotes( $row[$index] );
- }
- }
- $sql .= ')';
- $this->query( $sql, $fname );
- }
-
- # Now insert the row
- $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
- $this->makeList( $row, LIST_COMMA ) . ')';
- $this->query($sql, $fname);
- }
- }
-
- # DELETE where the condition is a join
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
- if ( !$conds ) {
- throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
- if ( $conds != '*' ) {
- $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
- }
- $sql .= ')';
-
- $this->query( $sql, $fname );
- }
-
- # Returns the size of a text field, or -1 for "unlimited"
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SELECT t.typname as ftype,a.atttypmod as size
- FROM pg_class c, pg_attribute a, pg_type t
- WHERE relname='$table' AND a.attrelid=c.oid AND
- a.atttypid=t.oid and a.attname='$field'";
- $res =$this->query($sql);
- $row=$this->fetchObject($res);
- if ($row->ftype=="varchar") {
- $size=$row->size-4;
- } else {
- $size=$row->size;
- }
- $this->freeResult( $res );
- return $size;
- }
-
- function lowPriorityOption() {
- return '';
- }
-
- function limitResult($sql, $limit, $offset) {
- if ($offset === false)
- $offset = 0;
- return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset";
- }
-
- /**
- * Returns an SQL expression for a simple conditional.
- * Uses CASE on Oracle
- *
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
- */
- function conditional( $cond, $trueVal, $falseVal ) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
- function wasDeadlock() {
- return $this->lastErrno() == 'OCI-00060';
- }
-
- function timestamp($ts = 0) {
- return wfTimestamp(TS_ORACLE, $ts);
- }
-
- /**
- * Return aggregated value function call
- */
- function aggregateValue ($valuedata,$valuename='value') {
- return $valuedata;
- }
-
- function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) {
- # Ignore errors during error handling to avoid infinite
- # recursion
- $ignore = $this->ignoreErrors(true);
- ++$this->mErrorCount;
-
- if ($ignore || $tempIgnore) {
-echo "error ignored! query = [$sql]\n";
- wfDebug("SQL ERROR (ignored): $error\n");
- $this->ignoreErrors( $ignore );
- }
- else {
-echo "error!\n";
- $message = "A database error has occurred\n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
- throw new DBUnexpectedError($this, $message);
- }
- }
-
- /**
- * @return string wikitext of a link to the server software's web site
- */
- function getSoftwareLink() {
- return "[http://www.oracle.com/ Oracle]";
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- return oci_server_version($this->mConn);
- }
-
- /**
- * Query whether a given table exists (in the given schema, or the default mw one if not given)
- */
- function tableExists($table) {
- $etable= $this->addQuotes($table);
- $SQL = "SELECT 1 FROM user_tables WHERE table_name='$etable'";
- $res = $this->query($SQL);
- $count = $res ? oci_num_rows($res) : 0;
- if ($res)
- $this->freeResult($res);
- return $count;
- }
-
- /**
- * Query whether a given column exists in the mediawiki schema
- */
- function fieldExists( $table, $field ) {
- return true; // XXX
- }
-
- function fieldInfo( $table, $field ) {
- return false; // XXX
- }
-
- function begin( $fname = '' ) {
- $this->mTrxLevel = 1;
- }
- function immediateCommit( $fname = '' ) {
- return true;
- }
- function commit( $fname = '' ) {
- oci_commit($this->mConn);
- $this->mTrxLevel = 0;
- }
-
- /* Not even sure why this is used in the main codebase... */
- function limitResultForUpdate($sql, $num) {
- return $sql;
- }
-
- function strencode($s) {
- return str_replace("'", "''", $s);
- }
-
- function encodeBlob($b) {
- return new ORABlob($b);
- }
- function decodeBlob($b) {
- return $b; //return $b->load();
- }
-
- function addQuotes( $s ) {
- global $wgLang;
- $s = $wgLang->checkTitleEncoding($s);
- return "'" . $this->strencode($s) . "'";
- }
-
- function quote_ident( $s ) {
- return $s;
- }
-
- /* For now, does nothing */
- function selectDB( $db ) {
- return true;
- }
-
- /**
- * Returns an optional USE INDEX clause to go after the table, and a
- * string to go at the end of the query
- *
- * @private
- *
- * @param array $options an associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = '';
-
- $noKeyOptions = array();
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
- if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
-
- if (isset($options['LIMIT'])) {
- // $tailOpts .= $this->limitResult('', $options['LIMIT'],
- // isset($options['OFFSET']) ? $options['OFFSET']
- // : false);
- }
-
- #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
- #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
- $useIndex = $this->useIndexClause( $options['USE INDEX'] );
- } else {
- $useIndex = '';
- }
-
- return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
- }
-
- public function setTimeout( $timeout ) {
- // @todo fixme no-op
- }
-
- function ping() {
- wfDebug( "Function ping() not written for DatabaseOracle.php yet");
- return true;
- }
-
- /**
- * How lagged is this slave?
- *
- * @return int
- */
- public function getLag() {
- # Not implemented for Oracle
- return 0;
- }
-
- function setFakeSlaveLag() {}
- function setFakeMaster() {}
-
- function getDBname() {
- return $this->mDBname;
- }
-
- function getServer() {
- return $this->mServer;
- }
-
-} // end DatabaseOracle class
diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php
deleted file mode 100644
index 8f8488b9ae..0000000000
--- a/includes/DatabasePostgres.php
+++ /dev/null
@@ -1,1330 +0,0 @@
-query(sprintf($q,
- $db->addQuotes($wgDBmwschema),
- $db->addQuotes($table),
- $db->addQuotes($field)));
- $row = $db->fetchObject($res);
- if (!$row)
- return null;
- $n = new PostgresField;
- $n->type = $row->typname;
- $n->nullable = ($row->attnotnull == 'f');
- $n->name = $field;
- $n->tablename = $table;
- $n->max_length = $row->attlen;
- return $n;
- }
-
- function name() {
- return $this->name;
- }
-
- function tableName() {
- return $this->tablename;
- }
-
- function type() {
- return $this->type;
- }
-
- function nullable() {
- return $this->nullable;
- }
-
- function maxLength() {
- return $this->max_length;
- }
-}
-
-/**
- * @ingroup Database
- */
-class DatabasePostgres extends Database {
- var $mInsertId = NULL;
- var $mLastResult = NULL;
- var $numeric_version = NULL;
-
- function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
- $failFunction = false, $flags = 0 )
- {
-
- global $wgOut;
- # Can't get a reference if it hasn't been set yet
- if ( !isset( $wgOut ) ) {
- $wgOut = NULL;
- }
- $this->mOut =& $wgOut;
- $this->mFailFunction = $failFunction;
- $this->mFlags = $flags;
- $this->open( $server, $user, $password, $dbName);
-
- }
-
- function cascadingDeletes() {
- return true;
- }
- function cleanupTriggers() {
- return true;
- }
- function strictIPs() {
- return true;
- }
- function realTimestamps() {
- return true;
- }
- function implicitGroupby() {
- return false;
- }
- function implicitOrderby() {
- return false;
- }
- function searchableIPs() {
- return true;
- }
- function functionalIndexes() {
- return true;
- }
-
- function hasConstraint( $name ) {
- global $wgDBmwschema;
- $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . pg_escape_string( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'";
- return $this->numRows($res = $this->doQuery($SQL));
- }
-
- static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
- {
- return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags );
- }
-
- /**
- * Usually aborts on failure
- * If the failFunction is set to a non-zero integer, returns success
- */
- function open( $server, $user, $password, $dbName ) {
- # Test for Postgres support, to avoid suppressed fatal error
- if ( !function_exists( 'pg_connect' ) ) {
- throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
- }
-
- global $wgDBport;
-
- if (!strlen($user)) { ## e.g. the class is being loaded
- return;
- }
-
- $this->close();
- $this->mServer = $server;
- $this->mPort = $port = $wgDBport;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $hstring="";
- if ($server!=false && $server!="") {
- $hstring="host=$server ";
- }
- if ($port!=false && $port!="") {
- $hstring .= "port=$port ";
- }
-
- error_reporting( E_ALL );
- @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
-
- if ( $this->mConn == false ) {
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
- wfDebug( $this->lastError()."\n" );
- return false;
- }
-
- $this->mOpened = true;
-
- global $wgCommandLineMode;
- ## If called from the command-line (e.g. importDump), only show errors
- if ($wgCommandLineMode) {
- $this->doQuery("SET client_min_messages = 'ERROR'");
- }
-
- global $wgDBmwschema, $wgDBts2schema;
- if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
- && $wgDBmwschema !== 'mediawiki'
- && preg_match( '/^\w+$/', $wgDBmwschema )
- && preg_match( '/^\w+$/', $wgDBts2schema )
- ) {
- $safeschema = $this->quote_ident($wgDBmwschema);
- $safeschema2 = $this->quote_ident($wgDBts2schema);
- $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public");
- }
-
- return $this->mConn;
- }
-
-
- function initial_setup($password, $dbName) {
- // If this is the initial connection, setup the schema stuff and possibly create the user
- global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema;
-
- print "
Checking the version of Postgres...";
- $version = $this->getServerVersion();
- $PGMINVER = '8.1';
- if ($this->numeric_version < $PGMINVER) {
- print "FAILED. Required version is $PGMINVER. You have $this->numeric_version ($version)
\n";
- dieout("");
- }
- print "version $this->numeric_version is OK.\n";
-
- $safeuser = $this->quote_ident($wgDBuser);
- // Are we connecting as a superuser for the first time?
- if ($wgDBsuperuser) {
- // Are we really a superuser? Check out our rights
- $SQL = "SELECT
- CASE WHEN usesuper IS TRUE THEN
- CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
- ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
- END AS rights
- FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser);
- $rows = $this->numRows($res = $this->doQuery($SQL));
- if (!$rows) {
- print "
ERROR: Could not read permissions for user \"$wgDBsuperuser\"
Checking that tsearch2 is installed in the database \"$wgDBname\"...";
- if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) {
- print "FAILED. tsearch2 must be installed in the database \"$wgDBname\".";
- print "Please see this article";
- print " for instructions or ask on #postgresql on irc.freenode.net
\n";
- dieout("");
- }
- print "OK\n";
- print "
Ensuring that user \"$wgDBuser\" has select rights on the tsearch2 tables...";
- foreach (array('cfg','cfgmap','dict','parser') as $table) {
- $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser";
- $this->doQuery($SQL);
- }
- print "OK
\n";
- }
-
- // Setup the schema for this user if needed
- $result = $this->schemaExists($wgDBmwschema);
- $safeschema = $this->quote_ident($wgDBmwschema);
- if (!$result) {
- print "
Schema already exists, explicitly granting rights...\n";
- $safeschema2 = $this->addQuotes($wgDBmwschema);
- $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
- "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n".
- "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n".
- "AND p.relkind IN ('r','S','v')\n";
- $SQL .= "UNION\n";
- $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
- "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n".
- "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n".
- "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
- $res = $this->doQuery($SQL);
- if (!$res) {
- print "FAILED. Could not set rights for the user.
\n";
- dieout("");
- }
- $this->doQuery("SET search_path = $safeschema");
- $rows = $this->numRows($res);
- while ($rows) {
- $rows--;
- $this->doQuery(pg_fetch_result($res, $rows, 0));
- }
- print "OK";
- }
-
- // Install plpgsql if needed
- $this->setup_plpgsql();
-
- $wgDBsuperuser = '';
- return true; // Reconnect as regular user
-
- } // end superuser
-
- if (!defined('POSTGRES_SEARCHPATH')) {
-
- if ($this->numeric_version < 8.3) {
- // Do we have the basic tsearch2 table?
- print "
Checking for tsearch2 in the schema \"$wgDBts2schema\"...";
- if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
- print "FAILED. Make sure tsearch2 is installed. See this article";
- print " for instructions.
\n";
- dieout("");
- }
- print "OK\n";
-
- // Does this user have the rights to the tsearch2 tables?
- $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
- print "
Checking tsearch2 permissions...";
- // Let's check all four, just to be safe
- error_reporting( 0 );
- $ts2tables = array('cfg','cfgmap','dict','parser');
- $safetsschema = $this->quote_ident($wgDBts2schema);
- foreach ( $ts2tables AS $tname ) {
- $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname";
- $res = $this->doQuery($SQL);
- if (!$res) {
- print "FAILED to access pg_ts_$tname. Make sure that the user ".
- "\"$wgDBuser\" has SELECT access to all four tsearch2 tables
\n";
- dieout("");
- }
- }
- $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = '$ctype'";
- $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END";
- $res = $this->doQuery($SQL);
- error_reporting( E_ALL );
- if (!$res) {
- print "FAILED. Could not determine the tsearch2 locale information\n";
- dieout("");
- }
- print "OK";
-
- // Will the current locale work? Can we force it to?
- print "
Verifying tsearch2 locale with $ctype...";
- $rows = $this->numRows($res);
- $resetlocale = 0;
- if (!$rows) {
- print "not found
\n";
- print "
Attempting to set default tsearch2 locale to \"$ctype\"...";
- $resetlocale = 1;
- }
- else {
- $tsname = pg_fetch_result($res, 0, 0);
- if ($tsname != 'default') {
- print "not set to default ($tsname)";
- print "
Attempting to change tsearch2 default locale to \"$ctype\"...";
- $resetlocale = 1;
- }
- }
- if ($resetlocale) {
- $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'";
- $res = $this->doQuery($SQL);
- if (!$res) {
- print "FAILED. ";
- print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"
\n";
- dieout("");
- }
- print "OK";
- }
-
- // Final test: try out a simple tsearch2 query
- $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')";
- $res = $this->doQuery($SQL);
- if (!$res) {
- print "FAILED. Specifically, \"$SQL\" did not work.";
- dieout("");
- }
- print "OK";
- }
-
- // Install plpgsql if needed
- $this->setup_plpgsql();
-
- // Does the schema already exist? Who owns it?
- $result = $this->schemaExists($wgDBmwschema);
- if (!$result) {
- print "
Creating schema $wgDBmwschema ...";
- error_reporting( 0 );
- $safeschema = $this->quote_ident($wgDBmwschema);
- $result = $this->doQuery("CREATE SCHEMA $safeschema");
- error_reporting( E_ALL );
- if (!$result) {
- print "FAILED. The user \"$wgDBuser\" must be able to access the schema. ".
- "You can try making them the owner of the database, or try creating the schema with a ".
- "different user, and then grant access to the \"$wgDBuser\" user.
Schema \"$wgDBmwschema\" exists but is not owned by \"$wgDBuser\". Not ideal.
\n";
- }
- else {
- print "
Schema \"$wgDBmwschema\" exists and is owned by \"$wgDBuser\". Excellent.
\n";
- }
-
- // Always return GMT time to accomodate the existing integer-based timestamp assumption
- print "
Setting the timezone to GMT for user \"$wgDBuser\" ...";
- $SQL = "ALTER USER $safeuser SET timezone = 'GMT'";
- $result = pg_query($this->mConn, $SQL);
- if (!$result) {
- print "FAILED.
\n";
- dieout("");
- }
- print "OK\n";
- // Set for the rest of this session
- $SQL = "SET timezone = 'GMT'";
- $result = pg_query($this->mConn, $SQL);
- if (!$result) {
- print "
Failed to set timezone
\n";
- dieout("");
- }
-
- print "
Setting the datestyle to ISO, YMD for user \"$wgDBuser\" ...";
- $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'";
- $result = pg_query($this->mConn, $SQL);
- if (!$result) {
- print "FAILED.
\n";
- dieout("");
- }
- print "OK\n";
- // Set for the rest of this session
- $SQL = "SET datestyle = 'ISO, YMD'";
- $result = pg_query($this->mConn, $SQL);
- if (!$result) {
- print "
Failed to set datestyle
\n";
- dieout("");
- }
-
- // Fix up the search paths if needed
- print "
Setting the search path for user \"$wgDBuser\" ...";
- $path = $this->quote_ident($wgDBmwschema);
- if ($wgDBts2schema !== $wgDBmwschema)
- $path .= ", ". $this->quote_ident($wgDBts2schema);
- if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public')
- $path .= ", public";
- $SQL = "ALTER USER $safeuser SET search_path = $path";
- $result = pg_query($this->mConn, $SQL);
- if (!$result) {
- print "FAILED.
\n";
- dieout("");
- }
- print "OK\n";
- // Set for the rest of this session
- $SQL = "SET search_path = $path";
- $result = pg_query($this->mConn, $SQL);
- if (!$result) {
- print "
Checking for Pl/Pgsql ...";
- $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'";
- $rows = $this->numRows($this->doQuery($SQL));
- if ($rows < 1) {
- // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
- print "not installed. Attempting to install Pl/Pgsql ...";
- $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
- "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
- $rows = $this->numRows($this->doQuery($SQL));
- if ($rows >= 1) {
- $olde = error_reporting(0);
- error_reporting($olde - E_WARNING);
- $result = $this->doQuery("CREATE LANGUAGE plpgsql");
- error_reporting($olde);
- if (!$result) {
- print "FAILED. You need to install the language plpgsql in the database $wgDBname
";
- dieout("");
- }
- }
- else {
- print "FAILED. You need to install the language plpgsql in the database $wgDBname";
- dieout("");
- }
- }
- print "OK\n";
- }
-
-
- /**
- * Closes a database connection, if it is open
- * Returns success, true if already closed
- */
- function close() {
- $this->mOpened = false;
- if ( $this->mConn ) {
- return pg_close( $this->mConn );
- } else {
- return true;
- }
- }
-
- function doQuery( $sql ) {
- if (function_exists('mb_convert_encoding')) {
- return $this->mLastResult=pg_query( $this->mConn , mb_convert_encoding($sql,'UTF-8') );
- }
- return $this->mLastResult=pg_query( $this->mConn , $sql);
- }
-
- function queryIgnore( $sql, $fname = '' ) {
- return $this->query( $sql, $fname, true );
- }
-
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- if ( !@pg_free_result( $res ) ) {
- throw new DBUnexpectedError($this, "Unable to free Postgres result\n" );
- }
- }
-
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @$row = pg_fetch_object( $res );
- # FIXME: HACK HACK HACK HACK debug
-
- # TODO:
- # hashar : not sure if the following test really trigger if the object
- # fetching failed.
- if( pg_last_error($this->mConn) ) {
- throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
- }
- return $row;
- }
-
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @$row = pg_fetch_array( $res );
- if( pg_last_error($this->mConn) ) {
- throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
- }
- return $row;
- }
-
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @$n = pg_num_rows( $res );
- if( pg_last_error($this->mConn) ) {
- throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
- }
- return $n;
- }
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return pg_num_fields( $res );
- }
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return pg_field_name( $res, $n );
- }
-
- /**
- * This must be called after nextSequenceVal
- */
- function insertId() {
- return $this->mInsertId;
- }
-
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return pg_result_seek( $res, $row );
- }
-
- function lastError() {
- if ( $this->mConn ) {
- return pg_last_error();
- }
- else {
- return "No database connection";
- }
- }
- function lastErrno() {
- return pg_last_error() ? 1 : 0;
- }
-
- function affectedRows() {
- if( !isset( $this->mLastResult ) or ! $this->mLastResult )
- return 0;
-
- return pg_affected_rows( $this->mLastResult );
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * This is not necessarily an accurate estimate, so use sparingly
- * Returns -1 if count cannot be found
- * Takes same arguments as Database::select()
- */
-
- function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
- $options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- $rows = -1;
- if ( $res ) {
- $row = $this->fetchRow( $res );
- $count = array();
- if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
- $rows = $count[1];
- }
- $this->freeResult($res);
- }
- return $rows;
- }
-
-
- /**
- * Returns information about an index
- * If errors are explicitly ignored, returns NULL on failure
- */
- function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return NULL;
- }
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->indexname == $index ) {
- return $row;
- }
- }
- return false;
- }
-
- function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
- " AND indexdef LIKE 'CREATE UNIQUE%({$index})'";
- $res = $this->query( $sql, $fname );
- if ( !$res )
- return NULL;
- while ($row = $this->fetchObject( $res ))
- return true;
- return false;
-
- }
-
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $args may be a single associative array, or an array of these with numeric keys,
- * for multi-row insert (Postgres version 8.2 and above only).
- *
- * @param array $table String: Name of the table to insert to.
- * @param array $args Array: Items to insert into the table.
- * @param array $fname String: Name of the function, for profiling
- * @param mixed $options String or Array. Valid options: IGNORE
- *
- * @return bool Success of insert operation. IGNORE always returns true.
- */
- function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
- global $wgDBversion;
-
- if ( !count( $args ) ) {
- return true;
- }
-
- $table = $this->tableName( $table );
- if (! isset( $wgDBversion ) ) {
- $this->getServerVersion();
- $wgDBversion = $this->numeric_version;
- }
-
- if ( !is_array( $options ) )
- $options = array( $options );
-
- if ( isset( $args[0] ) && is_array( $args[0] ) ) {
- $multi = true;
- $keys = array_keys( $args[0] );
- }
- else {
- $multi = false;
- $keys = array_keys( $args );
- }
-
- $ignore = in_array( 'IGNORE', $options ) ? 1 : 0;
- if ( $ignore )
- $olde = error_reporting( 0 );
-
- $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
- if ( $multi ) {
- if ( $wgDBversion >= 8.2 ) {
- $first = true;
- foreach ( $args as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- $res = (bool)$this->query( $sql, $fname, $ignore );
- }
- else {
- $res = true;
- $origsql = $sql;
- foreach ( $args as $row ) {
- $tempsql = $origsql;
- $tempsql .= '(' . $this->makeList( $row ) . ')';
- $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
- if (! $tempres)
- $res = false;
- }
- }
- }
- else {
- $sql .= '(' . $this->makeList( $args ) . ')';
- $res = (bool)$this->query( $sql, $fname, $ignore );
- }
-
- if ( $ignore ) {
- $olde = error_reporting( $olde );
- return true;
- }
-
- return $res;
-
- }
-
- function tableName( $name ) {
- # Replace reserved words with better ones
- switch( $name ) {
- case 'user':
- return 'mwuser';
- case 'text':
- return 'pagecontent';
- default:
- return $name;
- }
- }
-
- /**
- * Return the next in a sequence, save the value for retrieval via insertId()
- */
- function nextSequenceValue( $seqName ) {
- $safeseq = preg_replace( "/'/", "''", $seqName );
- $res = $this->query( "SELECT nextval('$safeseq')" );
- $row = $this->fetchRow( $res );
- $this->mInsertId = $row[0];
- $this->freeResult( $res );
- return $this->mInsertId;
- }
-
- /**
- * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
- */
- function currentSequenceValue( $seqName ) {
- $safeseq = preg_replace( "/'/", "''", $seqName );
- $res = $this->query( "SELECT currval('$safeseq')" );
- $row = $this->fetchRow( $res );
- $currval = $row[0];
- $this->freeResult( $res );
- return $currval;
- }
-
- /**
- * Postgres does not have a "USE INDEX" clause, so return an empty string
- */
- function useIndexClause( $index ) {
- return '';
- }
-
- # REPLACE query wrapper
- # Postgres simulates this with a DELETE followed by INSERT
- # $row is the row to insert, an associative array
- # $uniqueIndexes is an array of indexes. Each element may be either a
- # field name or an array of field names
- #
- # It may be more efficient to leave off unique indexes which are unlikely to collide.
- # However if you do this, you run the risk of encountering errors which wouldn't have
- # occurred in MySQL
- function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
- $table = $this->tableName( $table );
-
- if (count($rows)==0) {
- return;
- }
-
- # Single row case
- if ( !is_array( reset( $rows ) ) ) {
- $rows = array( $rows );
- }
-
- foreach( $rows as $row ) {
- # Delete rows which collide
- if ( $uniqueIndexes ) {
- $sql = "DELETE FROM $table WHERE ";
- $first = true;
- foreach ( $uniqueIndexes as $index ) {
- if ( $first ) {
- $first = false;
- $sql .= "(";
- } else {
- $sql .= ') OR (';
- }
- if ( is_array( $index ) ) {
- $first2 = true;
- foreach ( $index as $col ) {
- if ( $first2 ) {
- $first2 = false;
- } else {
- $sql .= ' AND ';
- }
- $sql .= $col.'=' . $this->addQuotes( $row[$col] );
- }
- } else {
- $sql .= $index.'=' . $this->addQuotes( $row[$index] );
- }
- }
- $sql .= ')';
- $this->query( $sql, $fname );
- }
-
- # Now insert the row
- $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
- $this->makeList( $row, LIST_COMMA ) . ')';
- $this->query( $sql, $fname );
- }
- }
-
- # DELETE where the condition is a join
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
- if ( !$conds ) {
- throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
- if ( $conds != '*' ) {
- $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
- }
- $sql .= ')';
-
- $this->query( $sql, $fname );
- }
-
- # Returns the size of a text field, or -1 for "unlimited"
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SELECT t.typname as ftype,a.atttypmod as size
- FROM pg_class c, pg_attribute a, pg_type t
- WHERE relname='$table' AND a.attrelid=c.oid AND
- a.atttypid=t.oid and a.attname='$field'";
- $res =$this->query($sql);
- $row=$this->fetchObject($res);
- if ($row->ftype=="varchar") {
- $size=$row->size-4;
- } else {
- $size=$row->size;
- }
- $this->freeResult( $res );
- return $size;
- }
-
- function lowPriorityOption() {
- return '';
- }
-
- function limitResult($sql, $limit, $offset=false) {
- return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
- }
-
- /**
- * Returns an SQL expression for a simple conditional.
- * Uses CASE on Postgres
- *
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
- */
- function conditional( $cond, $trueVal, $falseVal ) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
- function wasDeadlock() {
- return $this->lastErrno() == '40P01';
- }
-
- function timestamp( $ts=0 ) {
- return wfTimestamp(TS_POSTGRES,$ts);
- }
-
- /**
- * Return aggregated value function call
- */
- function aggregateValue ($valuedata,$valuename='value') {
- return $valuedata;
- }
-
-
- function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- // Ignore errors during error handling to avoid infinite recursion
- $ignore = $this->ignoreErrors( true );
- $this->mErrorCount++;
-
- if ($ignore || $tempIgnore) {
- wfDebug("SQL ERROR (ignored): $error\n");
- $this->ignoreErrors( $ignore );
- }
- else {
- $message = "A database error has occurred\n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
- throw new DBUnexpectedError($this, $message);
- }
- }
-
- /**
- * @return string wikitext of a link to the server software's web site
- */
- function getSoftwareLink() {
- return "[http://www.postgresql.org/ PostgreSQL]";
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0);
- $thisver = array();
- if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) {
- die("Could not determine the numeric version from $version!");
- }
- $this->numeric_version = $thisver[1];
- return $version;
- }
-
-
- /**
- * Query whether a given relation exists (in the given schema, or the
- * default mw one if not given)
- */
- function relationExists( $table, $types, $schema = false ) {
- global $wgDBmwschema;
- if (!is_array($types))
- $types = array($types);
- if (! $schema )
- $schema = $wgDBmwschema;
- $etable = $this->addQuotes($table);
- $eschema = $this->addQuotes($schema);
- $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
- . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
- . "AND c.relkind IN ('" . implode("','", $types) . "')";
- $res = $this->query( $SQL );
- $count = $res ? $res->numRows() : 0;
- if ($res)
- $this->freeResult( $res );
- return $count ? true : false;
- }
-
- /*
- * For backward compatibility, this function checks both tables and
- * views.
- */
- function tableExists ($table, $schema = false) {
- return $this->relationExists($table, array('r', 'v'), $schema);
- }
-
- function sequenceExists ($sequence, $schema = false) {
- return $this->relationExists($sequence, 'S', $schema);
- }
-
- function triggerExists($table, $trigger) {
- global $wgDBmwschema;
-
- $q = <<query(sprintf($q,
- $this->addQuotes($wgDBmwschema),
- $this->addQuotes($table),
- $this->addQuotes($trigger)));
- if (!$res)
- return NULL;
- $rows = $res->numRows();
- $this->freeResult($res);
- return $rows;
- }
-
- function ruleExists($table, $rule) {
- global $wgDBmwschema;
- $exists = $this->selectField("pg_rules", "rulename",
- array( "rulename" => $rule,
- "tablename" => $table,
- "schemaname" => $wgDBmwschema));
- return $exists === $rule;
- }
-
- function constraintExists($table, $constraint) {
- global $wgDBmwschema;
- $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
- "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
- $this->addQuotes($wgDBmwschema),
- $this->addQuotes($table),
- $this->addQuotes($constraint));
- $res = $this->query($SQL);
- if (!$res)
- return NULL;
- $rows = $res->numRows();
- $this->freeResult($res);
- return $rows;
- }
-
- /**
- * Query whether a given schema exists. Returns the name of the owner
- */
- function schemaExists( $schema ) {
- $eschema = preg_replace("/'/", "''", $schema);
- $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
- ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
- $res = $this->query( $SQL );
- if ( $res && $res->numRows() ) {
- $row = $res->fetchObject();
- $owner = $row->rolname;
- } else {
- $owner = false;
- }
- if ($res)
- $this->freeResult($res);
- return $owner;
- }
-
- /**
- * Query whether a given column exists in the mediawiki schema
- */
- function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) {
- global $wgDBmwschema;
- $etable = preg_replace("/'/", "''", $table);
- $eschema = preg_replace("/'/", "''", $wgDBmwschema);
- $ecol = preg_replace("/'/", "''", $field);
- $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a "
- . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
- . "AND a.attrelid = c.oid AND a.attname = '$ecol'";
- $res = $this->query( $SQL, $fname );
- $count = $res ? $res->numRows() : 0;
- if ($res)
- $this->freeResult( $res );
- return $count;
- }
-
- function fieldInfo( $table, $field ) {
- return PostgresField::fromText($this, $table, $field);
- }
-
- function begin( $fname = 'DatabasePostgres::begin' ) {
- $this->query( 'BEGIN', $fname );
- $this->mTrxLevel = 1;
- }
- function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) {
- return true;
- }
- function commit( $fname = 'DatabasePostgres::commit' ) {
- $this->query( 'COMMIT', $fname );
- $this->mTrxLevel = 0;
- }
-
- /* Not even sure why this is used in the main codebase... */
- function limitResultForUpdate($sql, $num) {
- return $sql;
- }
-
- function setup_database() {
- global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
-
- // Make sure that we can write to the correct schema
- // If not, Postgres will happily and silently go to the next search_path item
- $ctest = "mediawiki_test_table";
- $safeschema = $this->quote_ident($wgDBmwschema);
- if ($this->tableExists($ctest, $wgDBmwschema)) {
- $this->doQuery("DROP TABLE $safeschema.$ctest");
- }
- $SQL = "CREATE TABLE $safeschema.$ctest(a int)";
- $olde = error_reporting( 0 );
- $res = $this->doQuery($SQL);
- error_reporting( $olde );
- if (!$res) {
- print "FAILED. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"\n";
- dieout("");
- }
- $this->doQuery("DROP TABLE $safeschema.$ctest");
-
- $res = dbsource( "../maintenance/postgres/tables.sql", $this);
-
- ## Update version information
- $mwv = $this->addQuotes($wgVersion);
- $pgv = $this->addQuotes($this->getServerVersion());
- $pgu = $this->addQuotes($this->mUser);
- $mws = $this->addQuotes($wgDBmwschema);
- $tss = $this->addQuotes($wgDBts2schema);
- $pgp = $this->addQuotes($wgDBport);
- $dbn = $this->addQuotes($this->mDBname);
- $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
-
- $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
- "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
- "ctype = '$ctype' ".
- "WHERE type = 'Creation'";
- $this->query($SQL);
-
- ## Avoid the non-standard "REPLACE INTO" syntax
- $f = fopen( "../maintenance/interwiki.sql", 'r' );
- if ($f == false ) {
- dieout( "
Could not find the interwiki.sql file");
- }
- ## We simply assume it is already empty as we have just created it
- $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
- while ( ! feof( $f ) ) {
- $line = fgets($f,1024);
- $matches = array();
- if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
- continue;
- }
- $this->query("$SQL $matches[1],$matches[2])");
- }
- print " (table interwiki successfully populated)...\n";
-
- $this->doQuery("COMMIT");
- }
-
- function encodeBlob( $b ) {
- return new Blob ( pg_escape_bytea( $b ) ) ;
- }
-
- function decodeBlob( $b ) {
- if ($b instanceof Blob) {
- $b = $b->fetch();
- }
- return pg_unescape_bytea( $b );
- }
-
- function strencode( $s ) { ## Should not be called by us
- return pg_escape_string( $s );
- }
-
- function addQuotes( $s ) {
- if ( is_null( $s ) ) {
- return 'NULL';
- } else if ($s instanceof Blob) {
- return "'".$s->fetch($s)."'";
- }
- return "'" . pg_escape_string($s) . "'";
- }
-
- function quote_ident( $s ) {
- return '"' . preg_replace( '/"/', '""', $s) . '"';
- }
-
- /* For now, does nothing */
- function selectDB( $db ) {
- return true;
- }
-
- /**
- * Postgres specific version of replaceVars.
- * Calls the parent version in Database.php
- *
- * @private
- *
- * @param string $com SQL string, read from a stream (usually tables.sql)
- *
- * @return string SQL string
- */
- protected function replaceVars( $ins ) {
-
- $ins = parent::replaceVars( $ins );
-
- if ($this->numeric_version >= 8.3) {
- // Thanks for not providing backwards-compatibility, 8.3
- $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
- }
-
- if ($this->numeric_version <= 8.1) { // Our minimum version
- $ins = str_replace( 'USING gin', 'USING gist', $ins );
- }
-
- return $ins;
- }
-
- /**
- * Various select options
- *
- * @private
- *
- * @param array $options an associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = $useIndex = '';
-
- $noKeyOptions = array();
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
- if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
- if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
-
- //if (isset($options['LIMIT'])) {
- // $tailOpts .= $this->limitResult('', $options['LIMIT'],
- // isset($options['OFFSET']) ? $options['OFFSET']
- // : false);
- //}
-
- if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
- if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
- return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
- }
-
- public function setTimeout( $timeout ) {
- // @todo fixme no-op
- }
-
- function ping() {
- wfDebug( "Function ping() not written for DatabasePostgres.php yet");
- return true;
- }
-
- /**
- * How lagged is this slave?
- *
- */
- public function getLag() {
- # Not implemented for PostgreSQL
- return false;
- }
-
- function setFakeSlaveLag() {}
- function setFakeMaster() {}
-
- function getDBname() {
- return $this->mDBname;
- }
-
- function getServer() {
- return $this->mServer;
- }
-
- function buildConcat( $stringList ) {
- return implode( ' || ', $stringList );
- }
-
-} // end DatabasePostgres class
diff --git a/includes/DatabaseSqlite.php b/includes/DatabaseSqlite.php
deleted file mode 100644
index 8b4466ecaf..0000000000
--- a/includes/DatabaseSqlite.php
+++ /dev/null
@@ -1,395 +0,0 @@
-mOut =& $wgOut;
- $this->mFailFunction = $failFunction;
- $this->mFlags = $flags;
- $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
- $this->open($server, $user, $password, $dbName);
- }
-
- /**
- * todo: check if these should be true like parent class
- */
- function implicitGroupby() { return false; }
- function implicitOrderby() { return false; }
-
- static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
- return new DatabaseSqlite($server, $user, $password, $dbName, $failFunction, $flags);
- }
-
- /** Open an SQLite database and return a resource handle to it
- * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
- */
- function open($server,$user,$pass,$dbName) {
- $this->mConn = false;
- if ($dbName) {
- $file = $this->mDatabaseFile;
- if ($this->mFlags & DBO_PERSISTENT) $this->mConn = new PDO("sqlite:$file",$user,$pass,array(PDO::ATTR_PERSISTENT => true));
- else $this->mConn = new PDO("sqlite:$file",$user,$pass);
- if ($this->mConn === false) wfDebug("DB connection error: $err\n");;
- $this->mOpened = $this->mConn;
- $this->mConn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT); # set error codes only, dont raise exceptions
- }
- return $this->mConn;
- }
-
- /**
- * Close an SQLite database
- */
- function close() {
- $this->mOpened = false;
- if (is_object($this->mConn)) {
- if ($this->trxLevel()) $this->immediateCommit();
- $this->mConn = null;
- }
- return true;
- }
-
- /**
- * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
- */
- function doQuery($sql) {
- $res = $this->mConn->query($sql);
- if ($res === false) $this->reportQueryError($this->lastError(),$this->lastErrno(),$sql,__FUNCTION__);
- else {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- $this->mAffectedRows = $r->rowCount();
- $res = new ResultWrapper($this,$r->fetchAll());
- }
- return $res;
- }
-
- function freeResult(&$res) {
- if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL;
- }
-
- function fetchObject(&$res) {
- if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
- $cur = current($r);
- if (is_array($cur)) {
- next($r);
- $obj = new stdClass;
- foreach ($cur as $k => $v) if (!is_numeric($k)) $obj->$k = $v;
- return $obj;
- }
- return false;
- }
-
- function fetchRow(&$res) {
- if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
- $cur = current($r);
- if (is_array($cur)) {
- next($r);
- return $cur;
- }
- return false;
- }
-
- /**
- * The PDO::Statement class implements the array interface so count() will work
- */
- function numRows(&$res) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- return count($r);
- }
-
- function numFields(&$res) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- return is_array($r) ? count($r[0]) : 0;
- }
-
- function fieldName(&$res,$n) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- if (is_array($r)) {
- $keys = array_keys($r[0]);
- return $keys[$n];
- }
- return false;
- }
-
- /**
- * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
- */
- function tableName($name) {
- return str_replace('`','',parent::tableName($name));
- }
-
- /**
- * This must be called after nextSequenceVal
- */
- function insertId() {
- return $this->mConn->lastInsertId();
- }
-
- function dataSeek(&$res,$row) {
- if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
- reset($r);
- if ($row > 0) for ($i = 0; $i < $row; $i++) next($r);
- }
-
- function lastError() {
- if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
- $e = $this->mConn->errorInfo();
- return isset($e[2]) ? $e[2] : '';
- }
-
- function lastErrno() {
- if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
- return $this->mConn->errorCode();
- }
-
- function affectedRows() {
- return $this->mAffectedRows;
- }
-
- /**
- * Returns information about an index
- * - if errors are explicitly ignored, returns NULL on failure
- */
- function indexInfo($table, $index, $fname = 'Database::indexExists') {
- return false;
- }
-
- function indexUnique($table, $index, $fname = 'Database::indexUnique') {
- return false;
- }
-
- /**
- * Filter the options used in SELECT statements
- */
- function makeSelectOptions($options) {
- foreach ($options as $k => $v) if (is_numeric($k) && $v == 'FOR UPDATE') $options[$k] = '';
- return parent::makeSelectOptions($options);
- }
-
- /**
- * Based on MySQL method (parent) with some prior SQLite-sepcific adjustments
- */
- function insert($table, $a, $fname = 'DatabaseSqlite::insert', $options = array()) {
- if (!count($a)) return true;
- if (!is_array($options)) $options = array($options);
-
- # SQLite uses OR IGNORE not just IGNORE
- foreach ($options as $k => $v) if ($v == 'IGNORE') $options[$k] = 'OR IGNORE';
-
- # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
- if (isset($a[0]) && is_array($a[0])) {
- $ret = true;
- foreach ($a as $k => $v) if (!parent::insert($table,$v,"$fname/multi-row",$options)) $ret = false;
- }
- else $ret = parent::insert($table,$a,"$fname/single-row",$options);
-
- return $ret;
- }
-
- /**
- * SQLite does not have a "USE INDEX" clause, so return an empty string
- */
- function useIndexClause($index) {
- return '';
- }
-
- # Returns the size of a text field, or -1 for "unlimited"
- function textFieldSize($table, $field) {
- return -1;
- }
-
- /**
- * No low priority option in SQLite
- */
- function lowPriorityOption() {
- return '';
- }
-
- /**
- * Returns an SQL expression for a simple conditional.
- * - uses CASE on SQLite
- */
- function conditional($cond, $trueVal, $falseVal) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
- function wasDeadlock() {
- return $this->lastErrno() == SQLITE_BUSY;
- }
-
- /**
- * @return string wikitext of a link to the server software's web site
- */
- function getSoftwareLink() {
- return "[http://sqlite.org/ SQLite]";
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- global $wgContLang;
- $ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION);
- $size = $wgContLang->formatSize(filesize($this->mDatabaseFile));
- $file = basename($this->mDatabaseFile);
- return $ver." ($file: $size)";
- }
-
- /**
- * Query whether a given column exists in the mediawiki schema
- */
- function fieldExists($table, $field) { return true; }
-
- function fieldInfo($table, $field) { return SQLiteField::fromText($this, $table, $field); }
-
- function begin() {
- if ($this->mTrxLevel == 1) $this->commit();
- $this->mConn->beginTransaction();
- $this->mTrxLevel = 1;
- }
-
- function commit() {
- if ($this->mTrxLevel == 0) return;
- $this->mConn->commit();
- $this->mTrxLevel = 0;
- }
-
- function rollback() {
- if ($this->mTrxLevel == 0) return;
- $this->mConn->rollBack();
- $this->mTrxLevel = 0;
- }
-
- function limitResultForUpdate($sql, $num) {
- return $sql;
- }
-
- function strencode($s) {
- return substr($this->addQuotes($s),1,-1);
- }
-
- function encodeBlob($b) {
- return $this->strencode($b);
- }
-
- function decodeBlob($b) {
- return $b;
- }
-
- function addQuotes($s) {
- return $this->mConn->quote($s);
- }
-
- function quote_ident($s) { return $s; }
-
- /**
- * For now, does nothing
- */
- function selectDB($db) { return true; }
-
- /**
- * not done
- */
- public function setTimeout($timeout) { return; }
-
- function ping() {
- wfDebug("Function ping() not written for SQLite yet");
- return true;
- }
-
- /**
- * How lagged is this slave?
- */
- public function getLag() {
- return 0;
- }
-
- /**
- * Called by the installer script (when modified according to the MediaWikiLite installation instructions)
- * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
- */
- public function setup_database() {
- global $IP,$wgSQLiteDataDir,$wgDBTableOptions;
- $wgDBTableOptions = '';
- $mysql_tmpl = "$IP/maintenance/tables.sql";
- $mysql_iw = "$IP/maintenance/interwiki.sql";
- $sqlite_tmpl = "$IP/maintenance/sqlite/tables.sql";
-
- # Make an SQLite template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
- if (!file_exists($sqlite_tmpl)) {
- $sql = file_get_contents($mysql_tmpl);
- $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
- $sql = preg_replace('/^\s*(UNIQUE)?\s*(PRIMARY)?\s*KEY.+?$/m','',$sql);
- $sql = preg_replace('/^\s*(UNIQUE )?INDEX.+?$/m','',$sql); # These indexes should be created with a CREATE INDEX query
- $sql = preg_replace('/^\s*FULLTEXT.+?$/m','',$sql); # Full text indexes
- $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
- $sql = preg_replace('/binary\(\d+\)/','BLOB',$sql);
- $sql = preg_replace('/(TYPE|MAX_ROWS|AVG_ROW_LENGTH)=\w+/','',$sql);
- $sql = preg_replace('/,\s*\)/s',')',$sql); # removing previous items may leave a trailing comma
- $sql = str_replace('binary','',$sql);
- $sql = str_replace('auto_increment','PRIMARY KEY AUTOINCREMENT',$sql);
- $sql = str_replace(' unsigned','',$sql);
- $sql = str_replace(' int ',' INTEGER ',$sql);
- $sql = str_replace('NOT NULL','',$sql);
-
- # Tidy up and write file
- $sql = preg_replace('/^\s*^/m','',$sql); # Remove empty lines
- $sql = preg_replace('/;$/m',";\n",$sql); # Separate each statement with an empty line
- file_put_contents($sqlite_tmpl,$sql);
- }
-
- # Parse the SQLite template replacing inline variables such as /*$wgDBprefix*/
- $err = $this->sourceFile($sqlite_tmpl);
- if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
-
- # Use DatabasePostgres's code to populate interwiki from MySQL template
- $f = fopen($mysql_iw,'r');
- if ($f == false) dieout("
Could not find the interwiki.sql file");
- $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
- while (!feof($f)) {
- $line = fgets($f,1024);
- $matches = array();
- if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
- $this->query("$sql $matches[1],$matches[2])");
- }
- }
-
-}
-
-/**
- * @ingroup Database
- */
-class SQLiteField extends MySQLField {
-
- function __construct() {
- }
-
- static function fromText($db, $table, $field) {
- $n = new SQLiteField;
- $n->name = $field;
- $n->tablename = $table;
- return $n;
- }
-
-} // end DatabaseSqlite class
-
diff --git a/includes/DateFormatter.php b/includes/DateFormatter.php
deleted file mode 100644
index 9ef11d5e35..0000000000
--- a/includes/DateFormatter.php
+++ /dev/null
@@ -1,283 +0,0 @@
-monthNames = $this->getMonthRegex();
- for ( $i=1; $i<=12; $i++ ) {
- $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i;
- $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i;
- }
-
- $this->regexTrail = '(?![a-z])/iu';
-
- # Partial regular expressions
- $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')]]';
- $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})]]';
- $this->prxY = '\[\[(\d{1,4}([ _]BC|))]]';
- $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})]]';
- $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]';
-
- # Real regular expressions
- $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}";
- $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}";
- $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}";
- $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}";
- $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}";
- $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}";
- $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}";
- $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}";
-
- # Extraction keys
- # See the comments in replace() for the meaning of the letters
- $this->keys[self::DMY] = 'jFY';
- $this->keys[self::YDM] = 'Y jF';
- $this->keys[self::MDY] = 'FjY';
- $this->keys[self::YMD] = 'Y Fj';
- $this->keys[self::DM] = 'jF';
- $this->keys[self::MD] = 'Fj';
- $this->keys[self::ISO1] = 'ymd'; # y means ISO year
- $this->keys[self::ISO2] = 'ymd';
-
- # Target date formats
- $this->targets[self::DMY] = '[[F j|j F]] [[Y]]';
- $this->targets[self::YDM] = '[[Y]], [[F j|j F]]';
- $this->targets[self::MDY] = '[[F j]], [[Y]]';
- $this->targets[self::YMD] = '[[Y]] [[F j]]';
- $this->targets[self::DM] = '[[F j|j F]]';
- $this->targets[self::MD] = '[[F j]]';
- $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]';
- $this->targets[self::ISO2] = '[[y-m-d]]';
-
- # Rules
- # pref source target
- $this->rules[self::DMY][self::MD] = self::DM;
- $this->rules[self::ALL][self::MD] = self::MD;
- $this->rules[self::MDY][self::DM] = self::MD;
- $this->rules[self::ALL][self::DM] = self::DM;
- $this->rules[self::NONE][self::ISO2] = self::ISO1;
-
- $this->preferences = array(
- 'default' => self::NONE,
- 'dmy' => self::DMY,
- 'mdy' => self::MDY,
- 'ymd' => self::YMD,
- 'ISO 8601' => self::ISO1,
- );
- }
-
- /**
- * @static
- */
- function &getInstance() {
- global $wgMemc;
- static $dateFormatter = false;
- if ( !$dateFormatter ) {
- $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) );
- if ( !$dateFormatter ) {
- $dateFormatter = new DateFormatter;
- $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 );
- }
- }
- return $dateFormatter;
- }
-
- /**
- * @param string $preference User preference
- * @param string $text Text to reformat
- */
- function reformat( $preference, $text ) {
- if ( isset( $this->preferences[$preference] ) ) {
- $preference = $this->preferences[$preference];
- } else {
- $preference = self::NONE;
- }
- for ( $i=1; $i<=self::LAST; $i++ ) {
- $this->mSource = $i;
- if ( isset ( $this->rules[$preference][$i] ) ) {
- # Specific rules
- $this->mTarget = $this->rules[$preference][$i];
- } elseif ( isset ( $this->rules[self::ALL][$i] ) ) {
- # General rules
- $this->mTarget = $this->rules[self::ALL][$i];
- } elseif ( $preference ) {
- # User preference
- $this->mTarget = $preference;
- } else {
- # Default
- $this->mTarget = $i;
- }
- $text = preg_replace_callback( $this->regexes[$i], array( &$this, 'replace' ), $text );
- }
- return $text;
- }
-
- /**
- * @param $matches
- */
- function replace( $matches ) {
- # Extract information from $matches
- $bits = array();
- $key = $this->keys[$this->mSource];
- for ( $p=0; $p < strlen($key); $p++ ) {
- if ( $key{$p} != ' ' ) {
- $bits[$key{$p}] = $matches[$p+1];
- }
- }
-
- $format = $this->targets[$this->mTarget];
-
- # Construct new date
- $text = '';
- $fail = false;
-
- for ( $p=0; $p < strlen( $format ); $p++ ) {
- $char = $format{$p};
- switch ( $char ) {
- case 'd': # ISO day of month
- if ( !isset($bits['d']) ) {
- $text .= sprintf( '%02d', $bits['j'] );
- } else {
- $text .= $bits['d'];
- }
- break;
- case 'm': # ISO month
- if ( !isset($bits['m']) ) {
- $m = $this->makeIsoMonth( $bits['F'] );
- if ( !$m || $m == '00' ) {
- $fail = true;
- } else {
- $text .= $m;
- }
- } else {
- $text .= $bits['m'];
- }
- break;
- case 'y': # ISO year
- if ( !isset( $bits['y'] ) ) {
- $text .= $this->makeIsoYear( $bits['Y'] );
- } else {
- $text .= $bits['y'];
- }
- break;
- case 'j': # ordinary day of month
- if ( !isset($bits['j']) ) {
- $text .= intval( $bits['d'] );
- } else {
- $text .= $bits['j'];
- }
- break;
- case 'F': # long month
- if ( !isset( $bits['F'] ) ) {
- $m = intval($bits['m']);
- if ( $m > 12 || $m < 1 ) {
- $fail = true;
- } else {
- global $wgContLang;
- $text .= $wgContLang->getMonthName( $m );
- }
- } else {
- $text .= ucfirst( $bits['F'] );
- }
- break;
- case 'Y': # ordinary (optional BC) year
- if ( !isset( $bits['Y'] ) ) {
- $text .= $this->makeNormalYear( $bits['y'] );
- } else {
- $text .= $bits['Y'];
- }
- break;
- default:
- $text .= $char;
- }
- }
- if ( $fail ) {
- $text = $matches[0];
- }
- return $text;
- }
-
- /**
- * @todo document
- */
- function getMonthRegex() {
- global $wgContLang;
- $names = array();
- for( $i = 1; $i <= 12; $i++ ) {
- $names[] = $wgContLang->getMonthName( $i );
- $names[] = $wgContLang->getMonthAbbreviation( $i );
- }
- return implode( '|', $names );
- }
-
- /**
- * Makes an ISO month, e.g. 02, from a month name
- * @param $monthName String: month name
- * @return string ISO month name
- */
- function makeIsoMonth( $monthName ) {
- global $wgContLang;
-
- $n = $this->xMonths[$wgContLang->lc( $monthName )];
- return sprintf( '%02d', $n );
- }
-
- /**
- * @todo document
- * @param $year String: Year name
- * @return string ISO year name
- */
- function makeIsoYear( $year ) {
- # Assumes the year is in a nice format, as enforced by the regex
- if ( substr( $year, -2 ) == 'BC' ) {
- $num = intval(substr( $year, 0, -3 )) - 1;
- # PHP bug note: sprintf( "%04d", -1 ) fails poorly
- $text = sprintf( '-%04d', $num );
-
- } else {
- $text = sprintf( '%04d', $year );
- }
- return $text;
- }
-
- /**
- * @todo document
- */
- function makeNormalYear( $iso ) {
- if ( $iso{0} == '-' ) {
- $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC';
- } else {
- $text = intval( $iso );
- }
- return $text;
- }
-}
diff --git a/includes/LBFactory.php b/includes/LBFactory.php
deleted file mode 100644
index e7b2778390..0000000000
--- a/includes/LBFactory.php
+++ /dev/null
@@ -1,224 +0,0 @@
-forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) );
- }
-
- /**
- * Private helper for forEachLBCallMethod
- */
- function callMethod( $loadBalancer, $methodName, $args ) {
- call_user_func_array( array( $loadBalancer, $methodName ), $args );
- }
-
- /**
- * Commit changes on all master connections
- */
- function commitMasterChanges() {
- $this->forEachLBCallMethod( 'commitMasterChanges' );
- }
-}
-
-/**
- * A simple single-master LBFactory that gets its configuration from the b/c globals
- */
-class LBFactory_Simple extends LBFactory {
- var $mainLB;
- var $extLBs = array();
-
- # Chronology protector
- var $chronProt;
-
- function __construct( $conf ) {
- $this->chronProt = new ChronologyProtector;
- }
-
- function getMainLB( $wiki = false ) {
- if ( !isset( $this->mainLB ) ) {
- global $wgDBservers, $wgMasterWaitTimeout;
- if ( !$wgDBservers ) {
- global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
- $wgDBservers = array(array(
- 'host' => $wgDBserver,
- 'user' => $wgDBuser,
- 'password' => $wgDBpassword,
- 'dbname' => $wgDBname,
- 'type' => $wgDBtype,
- 'load' => 1,
- 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
- ));
- }
-
- $this->mainLB = new LoadBalancer( $wgDBservers, false, $wgMasterWaitTimeout, true );
- $this->mainLB->parentInfo( array( 'id' => 'main' ) );
- $this->chronProt->initLB( $this->mainLB );
- }
- return $this->mainLB;
- }
-
- function &getExternalLB( $cluster, $wiki = false ) {
- global $wgExternalServers;
- if ( !isset( $this->extLBs[$cluster] ) ) {
- if ( !isset( $wgExternalServers[$cluster] ) ) {
- throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
- }
- $this->extLBs[$cluster] = new LoadBalancer( $wgExternalServers[$cluster] );
- $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
- }
- return $this->extLBs[$cluster];
- }
-
- /**
- * Execute a function for each tracked load balancer
- * The callback is called with the load balancer as the first parameter,
- * and $params passed as the subsequent parameters.
- */
- function forEachLB( $callback, $params = array() ) {
- if ( isset( $this->mainLB ) ) {
- call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) );
- }
- foreach ( $this->extLBs as $lb ) {
- call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
- }
- }
-
- function shutdown() {
- if ( $this->mainLB ) {
- $this->chronProt->shutdownLB( $this->mainLB );
- }
- $this->chronProt->shutdown();
- $this->commitMasterChanges();
- }
-}
-
-/**
- * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
- * Kind of like Hawking's [[Chronology Protection Agency]].
- */
-class ChronologyProtector {
- var $startupPos;
- var $shutdownPos = array();
-
- /**
- * Initialise a LoadBalancer to give it appropriate chronology protection.
- *
- * @param LoadBalancer $lb
- */
- function initLB( $lb ) {
- if ( $this->startupPos === null ) {
- if ( !empty( $_SESSION[__CLASS__] ) ) {
- $this->startupPos = $_SESSION[__CLASS__];
- }
- }
- if ( !$this->startupPos ) {
- return;
- }
- $masterName = $lb->getServerName( 0 );
-
- if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
- $info = $lb->parentInfo();
- $pos = $this->startupPos[$masterName];
- wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
- $lb->waitFor( $this->startupPos[$masterName] );
- }
- }
-
- /**
- * Notify the ChronologyProtector that the LoadBalancer is about to shut
- * down. Saves replication positions.
- *
- * @param LoadBalancer $lb
- */
- function shutdownLB( $lb ) {
- if ( session_id() != '' && $lb->getServerCount() > 1 ) {
- $masterName = $lb->getServerName( 0 );
- if ( !isset( $this->shutdownPos[$masterName] ) ) {
- $pos = $lb->getMasterPos();
- $info = $lb->parentInfo();
- wfDebug( __METHOD__.": LB " . $info['id'] . " has master pos $pos\n" );
- $this->shutdownPos[$masterName] = $pos;
- }
- }
- }
-
- /**
- * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
- * May commit chronology data to persistent storage.
- */
- function shutdown() {
- if ( session_id() != '' && count( $this->shutdownPos ) ) {
- wfDebug( __METHOD__.": saving master pos for " .
- count( $this->shutdownPos ) . " master(s)\n" );
- $_SESSION[__CLASS__] = $this->shutdownPos;
- }
- }
-}
diff --git a/includes/LBFactory_Multi.php b/includes/LBFactory_Multi.php
deleted file mode 100644
index b5fc1f6dd7..0000000000
--- a/includes/LBFactory_Multi.php
+++ /dev/null
@@ -1,224 +0,0 @@
- array( 'db1' => 100, 'db2' => 100 ) )
- *
- * serverTemplate A server info associative array as documented for $wgDBservers. The host,
- * hostName and load entries will be overridden.
- *
- * groupLoadsBySection A 3-d map giving server load ratios for each section and group. For example:
- * array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) )
- *
- * groupLoadsByDB A 3-d map giving server load ratios by DB name.
- *
- * hostsByName A map of hostname to IP address.
- *
- * externalLoads A map of external storage cluster name to server load map
- *
- * externalTemplateOverrides A set of server info keys overriding serverTemplate for external storage
- *
- * templateOverridesByServer A 2-d map overriding serverTemplate and externalTemplateOverrides on a
- * server-by-server basis. Applies to both core and external storage.
- *
- * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster
- *
- * masterTemplateOverrides An override array for all master servers.
- *
- * @ingroup Database
- */
-class LBFactory_Multi extends LBFactory {
- // Required settings
- var $sectionsByDB, $sectionLoads, $serverTemplate;
- // Optional settings
- var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
- var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer;
- var $templateOverridesByCluster, $masterTemplateOverrides;
- // Other stuff
- var $conf, $mainLBs = array(), $extLBs = array();
- var $localSection = null;
-
- function __construct( $conf ) {
- $this->chronProt = new ChronologyProtector;
- $this->conf = $conf;
- $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' );
- $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
- 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
- 'templateOverridesByCluster', 'masterTemplateOverrides' );
-
- foreach ( $required as $key ) {
- if ( !isset( $conf[$key] ) ) {
- throw new MWException( __CLASS__.": $key is required in configuration" );
- }
- $this->$key = $conf[$key];
- }
-
- foreach ( $optional as $key ) {
- if ( isset( $conf[$key] ) ) {
- $this->$key = $conf[$key];
- }
- }
- }
-
- function getSectionForWiki( $wiki ) {
- list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
- if ( isset( $this->sectionsByDB[$dbName] ) ) {
- return $this->sectionsByDB[$dbName];
- } else {
- return 'DEFAULT';
- }
- }
-
- function getMainLB( $wiki = false ) {
- // Determine section
- if ( $wiki === false ) {
- if ( $this->localSection === null ) {
- $this->localSection = $this->getSectionForWiki( $wiki );
- }
- $section = $this->localSection;
- } else {
- $section = $this->getSectionForWiki( $wiki );
- }
-
- if ( !isset( $this->mainLBs[$section] ) ) {
- list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
- $groupLoads = array();
- if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
- $groupLoads = $this->groupLoadsByDB[$dbName];
- }
- if ( isset( $this->groupLoadsBySection[$section] ) ) {
- $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
- }
- $this->mainLBs[$section] = $this->newLoadBalancer( $this->serverTemplate,
- $this->sectionLoads[$section], $groupLoads, "main-$section" );
- $this->chronProt->initLB( $this->mainLBs[$section] );
- }
- return $this->mainLBs[$section];
- }
-
- function &getExternalLB( $cluster, $wiki = false ) {
- if ( !isset( $this->extLBs[$cluster] ) ) {
- if ( !isset( $this->externalLoads[$cluster] ) ) {
- throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
- }
- $template = $this->serverTemplate;
- if ( isset( $this->externalTemplateOverrides ) ) {
- $template = $this->externalTemplateOverrides + $template;
- }
- if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
- $template = $this->templateOverridesByCluster[$cluster] + $template;
- }
- $this->extLBs[$cluster] = $this->newLoadBalancer( $template,
- $this->externalLoads[$cluster], array(), "ext-$cluster" );
- }
- return $this->extLBs[$cluster];
- }
-
- /**
- * Make a new load balancer object based on template and load array
- */
- function newLoadBalancer( $template, $loads, $groupLoads, $id ) {
- global $wgMasterWaitTimeout;
- $servers = $this->makeServerArray( $template, $loads, $groupLoads );
- $lb = new LoadBalancer( $servers, false, $wgMasterWaitTimeout );
- $lb->parentInfo( array( 'id' => $id ) );
- return $lb;
- }
-
- /**
- * Make a server array as expected by LoadBalancer::__construct, using a template and load array
- */
- function makeServerArray( $template, $loads, $groupLoads ) {
- $servers = array();
- $master = true;
- $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
- foreach ( $groupLoadsByServer as $server => $stuff ) {
- if ( !isset( $loads[$server] ) ) {
- $loads[$server] = 0;
- }
- }
- foreach ( $loads as $serverName => $load ) {
- $serverInfo = $template;
- if ( $master ) {
- $serverInfo['master'] = true;
- if ( isset( $this->masterTemplateOverrides ) ) {
- $serverInfo = $this->masterTemplateOverrides + $serverInfo;
- }
- $master = false;
- }
- if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
- $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
- }
- if ( isset( $groupLoadsByServer[$serverName] ) ) {
- $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
- }
- if ( isset( $this->hostsByName[$serverName] ) ) {
- $serverInfo['host'] = $this->hostsByName[$serverName];
- } else {
- $serverInfo['host'] = $serverName;
- }
- $serverInfo['hostName'] = $serverName;
- $serverInfo['load'] = $load;
- $servers[] = $serverInfo;
- }
- return $servers;
- }
-
- /**
- * Take a group load array indexed by group then server, and reindex it by server then group
- */
- function reindexGroupLoads( $groupLoads ) {
- $reindexed = array();
- foreach ( $groupLoads as $group => $loads ) {
- foreach ( $loads as $server => $load ) {
- $reindexed[$server][$group] = $load;
- }
- }
- return $reindexed;
- }
-
- /**
- * Get the database name and prefix based on the wiki ID
- */
- function getDBNameAndPrefix( $wiki = false ) {
- if ( $wiki === false ) {
- global $wgDBname, $wgDBprefix;
- return array( $wgDBname, $wgDBprefix );
- } else {
- return wfSplitWikiID( $wiki );
- }
- }
-
- /**
- * Execute a function for each tracked load balancer
- * The callback is called with the load balancer as the first parameter,
- * and $params passed as the subsequent parameters.
- */
- function forEachLB( $callback, $params = array() ) {
- foreach ( $this->mainLBs as $lb ) {
- call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
- }
- foreach ( $this->extLBs as $lb ) {
- call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
- }
- }
-
- function shutdown() {
- foreach ( $this->mainLBs as $lb ) {
- $this->chronProt->shutdownLB( $lb );
- }
- $this->chronProt->shutdown();
- $this->commitMasterChanges();
- }
-}
diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php
deleted file mode 100644
index 4280713b2a..0000000000
--- a/includes/LoadBalancer.php
+++ /dev/null
@@ -1,894 +0,0 @@
-mServers = $servers;
- $this->mFailFunction = $failFunction;
- $this->mReadIndex = -1;
- $this->mWriteIndex = -1;
- $this->mConns = array(
- 'local' => array(),
- 'foreignUsed' => array(),
- 'foreignFree' => array() );
- $this->mLastIndex = -1;
- $this->mLoads = array();
- $this->mWaitForPos = false;
- $this->mWaitTimeout = $waitTimeout;
- $this->mLaggedSlaveMode = false;
- $this->mErrorConnection = false;
- $this->mAllowLag = false;
-
- foreach( $servers as $i => $server ) {
- $this->mLoads[$i] = $server['load'];
- if ( isset( $server['groupLoads'] ) ) {
- foreach ( $server['groupLoads'] as $group => $ratio ) {
- if ( !isset( $this->mGroupLoads[$group] ) ) {
- $this->mGroupLoads[$group] = array();
- }
- $this->mGroupLoads[$group][$i] = $ratio;
- }
- }
- }
- }
-
- static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
- {
- return new LoadBalancer( $servers, $failFunction, $waitTimeout );
- }
-
- /**
- * Get or set arbitrary data used by the parent object, usually an LBFactory
- */
- function parentInfo( $x = null ) {
- return wfSetVar( $this->mParentInfo, $x );
- }
-
- /**
- * Given an array of non-normalised probabilities, this function will select
- * an element and return the appropriate key
- */
- function pickRandom( $weights )
- {
- if ( !is_array( $weights ) || count( $weights ) == 0 ) {
- return false;
- }
-
- $sum = array_sum( $weights );
- if ( $sum == 0 ) {
- # No loads on any of them
- # In previous versions, this triggered an unweighted random selection,
- # but this feature has been removed as of April 2006 to allow for strict
- # separation of query groups.
- return false;
- }
- $max = mt_getrandmax();
- $rand = mt_rand(0, $max) / $max * $sum;
-
- $sum = 0;
- foreach ( $weights as $i => $w ) {
- $sum += $w;
- if ( $sum >= $rand ) {
- break;
- }
- }
- return $i;
- }
-
- function getRandomNonLagged( $loads, $wiki = false ) {
- # Unset excessively lagged servers
- $lags = $this->getLagTimes( $wiki );
- foreach ( $lags as $i => $lag ) {
- if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) {
- if ( $lag === false ) {
- wfDebug( "Server #$i is not replicating\n" );
- unset( $loads[$i] );
- } elseif ( $lag > $this->mServers[$i]['max lag'] ) {
- wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" );
- unset( $loads[$i] );
- }
- }
- }
-
- # Find out if all the slaves with non-zero load are lagged
- $sum = 0;
- foreach ( $loads as $load ) {
- $sum += $load;
- }
- if ( $sum == 0 ) {
- # No appropriate DB servers except maybe the master and some slaves with zero load
- # Do NOT use the master
- # Instead, this function will return false, triggering read-only mode,
- # and a lagged slave will be used instead.
- return false;
- }
-
- if ( count( $loads ) == 0 ) {
- return false;
- }
-
- #wfDebugLog( 'connect', var_export( $loads, true ) );
-
- # Return a random representative of the remainder
- return $this->pickRandom( $loads );
- }
-
- /**
- * Get the index of the reader connection, which may be a slave
- * This takes into account load ratios and lag times. It should
- * always return a consistent index during a given invocation
- *
- * Side effect: opens connections to databases
- */
- function getReaderIndex( $group = false, $wiki = false ) {
- global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
-
- # FIXME: For now, only go through all this for mysql databases
- if ($wgDBtype != 'mysql') {
- return $this->getWriterIndex();
- }
-
- if ( count( $this->mServers ) == 1 ) {
- # Skip the load balancing if there's only one server
- return 0;
- } elseif ( $group === false and $this->mReadIndex >= 0 ) {
- # Shortcut if generic reader exists already
- return $this->mReadIndex;
- }
-
- wfProfileIn( __METHOD__ );
-
- $totalElapsed = 0;
-
- # convert from seconds to microseconds
- $timeout = $wgDBClusterTimeout * 1e6;
-
- # Find the relevant load array
- if ( $group !== false ) {
- if ( isset( $this->mGroupLoads[$group] ) ) {
- $nonErrorLoads = $this->mGroupLoads[$group];
- } else {
- # No loads for this group, return false and the caller can use some other group
- wfDebug( __METHOD__.": no loads for group $group\n" );
- wfProfileOut( __METHOD__ );
- return false;
- }
- } else {
- $nonErrorLoads = $this->mLoads;
- }
-
- if ( !$nonErrorLoads ) {
- throw new MWException( "Empty server array given to LoadBalancer" );
- }
-
- $i = false;
- $found = false;
- $laggedSlaveMode = false;
-
- # First try quickly looking through the available servers for a server that
- # meets our criteria
- do {
- $totalThreadsConnected = 0;
- $overloadedServers = 0;
- $currentLoads = $nonErrorLoads;
- while ( count( $currentLoads ) ) {
- if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
- $i = $this->pickRandom( $currentLoads );
- } else {
- $i = $this->getRandomNonLagged( $currentLoads, $wiki );
- if ( $i === false && count( $currentLoads ) != 0 ) {
- # All slaves lagged. Switch to read-only mode
- $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
- $i = $this->pickRandom( $currentLoads );
- $laggedSlaveMode = true;
- }
- }
-
- if ( $i === false ) {
- # pickRandom() returned false
- # This is permanent and means the configuration wants us to return false
- wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
- $conn = $this->openConnection( $i, $wiki );
-
- if ( !$conn ) {
- wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
- unset( $nonErrorLoads[$i] );
- unset( $currentLoads[$i] );
- continue;
- }
-
- if ( isset( $this->mServers[$i]['max threads'] ) ) {
- $status = $conn->getStatus("Thread%");
- if ( $wiki !== false ) {
- $this->reuseConnection( $conn );
- }
- if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) {
- $totalThreadsConnected += $status['Threads_connected'];
- $overloadedServers++;
- unset( $currentLoads[$i] );
- } else {
- # Max threads satisfied, return this server
- break 2;
- }
- } else {
- # No maximum, return this server
- if ( $wiki !== false ) {
- $this->reuseConnection( $conn );
- }
- $found = true;
- break 2;
- }
- }
-
- # No server found yet
- $i = false;
-
- # If all servers were down, quit now
- if ( !count( $nonErrorLoads ) ) {
- wfDebugLog( 'connect', "All servers down\n" );
- break;
- }
-
- # Some servers must have been overloaded
- if ( $overloadedServers == 0 ) {
- throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
- }
- # Back off for a while
- # Scale the sleep time by the number of connected threads, to produce a
- # roughly constant global poll rate
- $avgThreads = $totalThreadsConnected / $overloadedServers;
- $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads );
- } while ( $totalElapsed < $timeout );
-
- if ( $totalElapsed >= $timeout ) {
- wfDebugLog( 'connect', "All servers busy\n" );
- $this->mErrorConnection = false;
- $this->mLastError = 'All servers busy';
- }
-
- if ( $i !== false ) {
- # Wait for the session master pos for a short time
- if ( $this->mWaitForPos && $i > 0 ) {
- if ( !$this->doWait( $i ) ) {
- $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
- }
- }
- if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) {
- $this->mReadIndex = $i;
- }
- }
- wfProfileOut( __METHOD__ );
- return $i;
- }
-
- /**
- * Wait for a specified number of microseconds, and return the period waited
- */
- function sleep( $t ) {
- wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__.": waiting $t us\n" );
- usleep( $t );
- wfProfileOut( __METHOD__ );
- return $t;
- }
-
- /**
- * Get a random server to use in a query group
- * @deprecated use getReaderIndex
- */
- function getGroupIndex( $group ) {
- return $this->getReaderIndex( $group );
- }
-
- /**
- * Set the master wait position
- * If a DB_SLAVE connection has been opened already, waits
- * Otherwise sets a variable telling it to wait if such a connection is opened
- */
- public function waitFor( $pos ) {
- wfProfileIn( __METHOD__ );
- $this->mWaitForPos = $pos;
- $i = $this->mReadIndex;
-
- if ( $i > 0 ) {
- if ( !$this->doWait( $i ) ) {
- $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
- $this->mLaggedSlaveMode = true;
- }
- }
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Get any open connection to a given server index, local or foreign
- * Returns false if there is no connection open
- */
- function getAnyOpenConnection( $i ) {
- foreach ( $this->mConns as $type => $conns ) {
- if ( !empty( $conns[$i] ) ) {
- return reset( $conns[$i] );
- }
- }
- return false;
- }
-
- /**
- * Wait for a given slave to catch up to the master pos stored in $this
- */
- function doWait( $index ) {
- # Find a connection to wait on
- $conn = $this->getAnyOpenConnection( $index );
- if ( !$conn ) {
- wfDebug( __METHOD__ . ": no connection open\n" );
- return false;
- }
-
- wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
- $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
-
- if ( $result == -1 || is_null( $result ) ) {
- # Timed out waiting for slave, use master instead
- wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
- return false;
- } else {
- wfDebug( __METHOD__.": Done\n" );
- return true;
- }
- }
-
- /**
- * Get a connection by index
- * This is the main entry point for this class.
- */
- public function &getConnection( $i, $groups = array(), $wiki = false ) {
- global $wgDBtype;
- wfProfileIn( __METHOD__ );
-
- if ( $wiki === wfWikiID() ) {
- $wiki = false;
- }
-
- # Query groups
- if ( $i == DB_MASTER ) {
- $i = $this->getWriterIndex();
- } elseif ( !is_array( $groups ) ) {
- $groupIndex = $this->getReaderIndex( $groups, $wiki );
- if ( $groupIndex !== false ) {
- $serverName = $this->getServerName( $groupIndex );
- wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
- $i = $groupIndex;
- }
- } else {
- foreach ( $groups as $group ) {
- $groupIndex = $this->getReaderIndex( $group, $wiki );
- if ( $groupIndex !== false ) {
- $serverName = $this->getServerName( $groupIndex );
- wfDebug( __METHOD__.": using server $serverName for group $group\n" );
- $i = $groupIndex;
- break;
- }
- }
- }
-
- # Operation-based index
- if ( $i == DB_SLAVE ) {
- $i = $this->getReaderIndex( false, $wiki );
- } elseif ( $i == DB_LAST ) {
- # Just use $this->mLastIndex, which should already be set
- $i = $this->mLastIndex;
- if ( $i === -1 ) {
- # Oh dear, not set, best to use the writer for safety
- wfDebug( "Warning: DB_LAST used when there was no previous index\n" );
- $i = $this->getWriterIndex();
- }
- }
- # Couldn't find a working server in getReaderIndex()?
- if ( $i === false ) {
- $this->reportConnectionError( $this->mErrorConnection );
- }
-
- # Now we have an explicit index into the servers array
- $conn = $this->openConnection( $i, $wiki );
- if ( !$conn ) {
- $this->reportConnectionError( $this->mErrorConnection );
- }
-
- wfProfileOut( __METHOD__ );
- return $conn;
- }
-
- /**
- * Mark a foreign connection as being available for reuse under a different
- * DB name or prefix. This mechanism is reference-counted, and must be called
- * the same number of times as getConnection() to work.
- */
- public function reuseConnection( $conn ) {
- $serverIndex = $conn->getLBInfo('serverIndex');
- $refCount = $conn->getLBInfo('foreignPoolRefCount');
- $dbName = $conn->getDBname();
- $prefix = $conn->tablePrefix();
- if ( strval( $prefix ) !== '' ) {
- $wiki = "$dbName-$prefix";
- } else {
- $wiki = $dbName;
- }
- if ( $serverIndex === null || $refCount === null ) {
- wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
- /**
- * This can happen in code like:
- * foreach ( $dbs as $db ) {
- * $conn = $lb->getConnection( DB_SLAVE, array(), $db );
- * ...
- * $lb->reuseConnection( $conn );
- * }
- * When a connection to the local DB is opened in this way, reuseConnection()
- * should be ignored
- */
- return;
- }
- if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
- throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
- }
- $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
- if ( $refCount <= 0 ) {
- $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
- unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
- wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
- } else {
- wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
- }
- }
-
- /**
- * Open a connection to the server given by the specified index
- * Index must be an actual index into the array.
- * If the server is already open, returns it.
- *
- * On error, returns false, and the connection which caused the
- * error will be available via $this->mErrorConnection.
- *
- * @param integer $i Server index
- * @param string $wiki Wiki ID to open
- * @return Database
- *
- * @access private
- */
- function openConnection( $i, $wiki = false ) {
- wfProfileIn( __METHOD__ );
- if ( $wiki !== false ) {
- $conn = $this->openForeignConnection( $i, $wiki );
- wfProfileOut( __METHOD__);
- return $conn;
- }
- if ( isset( $this->mConns['local'][$i][0] ) ) {
- $conn = $this->mConns['local'][$i][0];
- } else {
- $server = $this->mServers[$i];
- $server['serverIndex'] = $i;
- $conn = $this->reallyOpenConnection( $server );
- if ( $conn->isOpen() ) {
- $this->mConns['local'][$i][0] = $conn;
- } else {
- wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
- $this->mErrorConnection = $conn;
- $conn = false;
- }
- }
- $this->mLastIndex = $i;
- wfProfileOut( __METHOD__ );
- return $conn;
- }
-
- /**
- * Open a connection to a foreign DB, or return one if it is already open.
- *
- * Increments a reference count on the returned connection which locks the
- * connection to the requested wiki. This reference count can be
- * decremented by calling reuseConnection().
- *
- * If a connection is open to the appropriate server already, but with the wrong
- * database, it will be switched to the right database and returned, as long as
- * it has been freed first with reuseConnection().
- *
- * On error, returns false, and the connection which caused the
- * error will be available via $this->mErrorConnection.
- *
- * @param integer $i Server index
- * @param string $wiki Wiki ID to open
- * @return Database
- */
- function openForeignConnection( $i, $wiki ) {
- wfProfileIn(__METHOD__);
- list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
- if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
- // Reuse an already-used connection
- $conn = $this->mConns['foreignUsed'][$i][$wiki];
- wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
- } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
- // Reuse a free connection for the same wiki
- $conn = $this->mConns['foreignFree'][$i][$wiki];
- unset( $this->mConns['foreignFree'][$i][$wiki] );
- $this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
- } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
- // Reuse a connection from another wiki
- $conn = reset( $this->mConns['foreignFree'][$i] );
- $oldWiki = key( $this->mConns['foreignFree'][$i] );
-
- if ( !$conn->selectDB( $dbName ) ) {
- global $wguname;
- $this->mLastError = "Error selecting database $dbName on server " .
- $conn->getServer() . " from client host {$wguname['nodename']}\n";
- $this->mErrorConnection = $conn;
- $conn = false;
- } else {
- $conn->tablePrefix( $prefix );
- unset( $this->mConns['foreignFree'][$i][$oldWiki] );
- $this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
- }
- } else {
- // Open a new connection
- $server = $this->mServers[$i];
- $server['serverIndex'] = $i;
- $server['foreignPoolRefCount'] = 0;
- $conn = $this->reallyOpenConnection( $server, $dbName );
- if ( !$conn->isOpen() ) {
- wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
- $this->mErrorConnection = $conn;
- $conn = false;
- } else {
- $this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
- }
- }
-
- // Increment reference count
- if ( $conn ) {
- $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
- $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
- }
- wfProfileOut(__METHOD__);
- return $conn;
- }
-
- /**
- * Test if the specified index represents an open connection
- * @access private
- */
- function isOpen( $index ) {
- if( !is_integer( $index ) ) {
- return false;
- }
- return (bool)$this->getAnyOpenConnection( $index );
- }
-
- /**
- * Really opens a connection. Uncached.
- * Returns a Database object whether or not the connection was successful.
- * @access private
- */
- function reallyOpenConnection( $server, $dbNameOverride = false ) {
- if( !is_array( $server ) ) {
- throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
- }
-
- extract( $server );
- if ( $dbNameOverride !== false ) {
- $dbname = $dbNameOverride;
- }
-
- # Get class for this database type
- $class = 'Database' . ucfirst( $type );
-
- # Create object
- wfDebug( "Connecting to $host $dbname...\n" );
- $db = new $class( $host, $user, $password, $dbname, 1, $flags );
- if ( $db->isOpen() ) {
- wfDebug( "Connected\n" );
- } else {
- wfDebug( "Failed\n" );
- }
- $db->setLBInfo( $server );
- if ( isset( $server['fakeSlaveLag'] ) ) {
- $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
- }
- if ( isset( $server['fakeMaster'] ) ) {
- $db->setFakeMaster( true );
- }
- return $db;
- }
-
- function reportConnectionError( &$conn ) {
- wfProfileIn( __METHOD__ );
- # Prevent infinite recursion
-
- static $reporting = false;
- if ( !$reporting ) {
- $reporting = true;
- if ( !is_object( $conn ) ) {
- // No last connection, probably due to all servers being too busy
- $conn = new Database;
- if ( $this->mFailFunction ) {
- $conn->failFunction( $this->mFailFunction );
- $conn->reportConnectionError( $this->mLastError );
- } else {
- // If all servers were busy, mLastError will contain something sensible
- throw new DBConnectionError( $conn, $this->mLastError );
- }
- } else {
- if ( $this->mFailFunction ) {
- $conn->failFunction( $this->mFailFunction );
- } else {
- $conn->failFunction( false );
- }
- $server = $conn->getProperty( 'mServer' );
- $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
- }
- $reporting = false;
- }
- wfProfileOut( __METHOD__ );
- }
-
- function getWriterIndex() {
- return 0;
- }
-
- /**
- * Returns true if the specified index is a valid server index
- */
- function haveIndex( $i ) {
- return array_key_exists( $i, $this->mServers );
- }
-
- /**
- * Returns true if the specified index is valid and has non-zero load
- */
- function isNonZeroLoad( $i ) {
- return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
- }
-
- /**
- * Get the number of defined servers (not the number of open connections)
- */
- function getServerCount() {
- return count( $this->mServers );
- }
-
- /**
- * Get the host name or IP address of the server with the specified index
- */
- function getServerName( $i ) {
- if ( isset( $this->mServers[$i]['hostName'] ) ) {
- return $this->mServers[$i]['hostName'];
- } elseif ( isset( $this->mServers[$i]['host'] ) ) {
- return $this->mServers[$i]['host'];
- } else {
- return '';
- }
- }
-
- /**
- * Get the current master position for chronology control purposes
- * @return mixed
- */
- function getMasterPos() {
- # If this entire request was served from a slave without opening a connection to the
- # master (however unlikely that may be), then we can fetch the position from the slave.
- $masterConn = $this->getAnyOpenConnection( 0 );
- if ( !$masterConn ) {
- for ( $i = 1; $i < count( $this->mServers ); $i++ ) {
- $conn = $this->getAnyOpenConnection( $i );
- if ( $conn ) {
- wfDebug( "Master pos fetched from slave\n" );
- return $conn->getSlavePos();
- }
- }
- } else {
- wfDebug( "Master pos fetched from master\n" );
- return $masterConn->getMasterPos();
- }
- return false;
- }
-
- /**
- * Close all open connections
- */
- function closeAll() {
- foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
- foreach ( $conns3 as $conn ) {
- $conn->close();
- }
- }
- }
- $this->mConns = array(
- 'local' => array(),
- 'foreignFree' => array(),
- 'foreignUsed' => array(),
- );
- }
-
- /**
- * Close a connection
- * Using this function makes sure the LoadBalancer knows the connection is closed.
- * If you use $conn->close() directly, the load balancer won't update its state.
- */
- function closeConnecton( $conn ) {
- $done = false;
- foreach ( $this->mConns as $i1 => $conns2 ) {
- foreach ( $conns2 as $i2 => $conns3 ) {
- foreach ( $conns3 as $i3 => $candidateConn ) {
- if ( $conn === $candidateConn ) {
- $conn->close();
- unset( $this->mConns[$i1][$i2][$i3] );
- $done = true;
- break;
- }
- }
- }
- }
- if ( !$done ) {
- $conn->close();
- }
- }
-
- /**
- * Commit transactions on all open connections
- */
- function commitAll() {
- foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
- foreach ( $conns3 as $conn ) {
- $conn->immediateCommit();
- }
- }
- }
- }
-
- /* Issue COMMIT only on master, only if queries were done on connection */
- function commitMasterChanges() {
- // Always 0, but who knows.. :)
- $masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $type => $conns2 ) {
- if ( empty( $conns2[$masterIndex] ) ) {
- continue;
- }
- foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->lastQuery() != '' ) {
- $conn->commit();
- }
- }
- }
- }
-
- function waitTimeout( $value = NULL ) {
- return wfSetVar( $this->mWaitTimeout, $value );
- }
-
- function getLaggedSlaveMode() {
- return $this->mLaggedSlaveMode;
- }
-
- /* Disables/enables lag checks */
- function allowLagged($mode=null) {
- if ($mode===null)
- return $this->mAllowLagged;
- $this->mAllowLagged=$mode;
- }
-
- function pingAll() {
- $success = true;
- foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
- foreach ( $conns3 as $conn ) {
- if ( !$conn->ping() ) {
- $success = false;
- }
- }
- }
- }
- return $success;
- }
-
- /**
- * Get the hostname and lag time of the most-lagged slave.
- * This is useful for maintenance scripts that need to throttle their updates.
- * May attempt to open connections to slaves on the default DB.
- */
- function getMaxLag() {
- $maxLag = -1;
- $host = '';
- foreach ( $this->mServers as $i => $conn ) {
- $conn = $this->getAnyOpenConnection( $i );
- if ( !$conn ) {
- $conn = $this->openConnection( $i );
- }
- if ( !$conn ) {
- continue;
- }
- $lag = $conn->getLag();
- if ( $lag > $maxLag ) {
- $maxLag = $lag;
- $host = $this->mServers[$i]['host'];
- }
- }
- return array( $host, $maxLag );
- }
-
- /**
- * Get lag time for each server
- * Results are cached for a short time in memcached, and indefinitely in the process cache
- */
- function getLagTimes( $wiki = false ) {
- wfProfileIn( __METHOD__ );
-
- if ( !isset( $this->mLagTimes ) ) {
- $expiry = 5;
- $requestRate = 10;
-
- global $wgMemc;
- $masterName = $this->getServerName( 0 );
- $memcKey = wfMemcKey( 'lag_times', $masterName );
- $times = $wgMemc->get( $memcKey );
- if ( $times ) {
- # Randomly recache with probability rising over $expiry
- $elapsed = time() - $times['timestamp'];
- $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
- if ( mt_rand( 0, $chance ) != 0 ) {
- unset( $times['timestamp'] );
- wfProfileOut( __METHOD__ );
- return $times;
- }
- wfIncrStats( 'lag_cache_miss_expired' );
- } else {
- wfIncrStats( 'lag_cache_miss_absent' );
- }
-
- # Cache key missing or expired
-
- $times = array();
- foreach ( $this->mServers as $i => $conn ) {
- if ($i == 0) { # Master
- $times[$i] = 0;
- } elseif ( false !== ( $conn = $this->getAnyOpenConnection( $i ) ) ) {
- $times[$i] = $conn->getLag();
- } elseif ( false !== ( $conn = $this->openConnection( $i, $wiki ) ) ) {
- $times[$i] = $conn->getLag();
- }
- }
-
- # Add a timestamp key so we know when it was cached
- $times['timestamp'] = time();
- $wgMemc->set( $memcKey, $times, $expiry );
-
- # But don't give the timestamp to the caller
- unset($times['timestamp']);
- $this->mLagTimes = $times;
- }
- wfProfileOut( __METHOD__ );
- return $this->mLagTimes;
- }
-}
diff --git a/includes/Parser.php b/includes/Parser.php
deleted file mode 100644
index 4daa8f2956..0000000000
--- a/includes/Parser.php
+++ /dev/null
@@ -1,4974 +0,0 @@
-
- * There are five main entry points into the Parser class:
- * parse()
- * produces HTML output
- * preSaveTransform().
- * produces altered wiki markup.
- * preprocess()
- * removes HTML comments and expands templates
- * cleanSig()
- * Cleans a signature before saving it to preferences
- * extractSections()
- * Extracts sections from an article for section editing
- *
- * Globals used:
- * objects: $wgLang, $wgContLang
- *
- * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
- *
- * settings:
- * $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
- * $wgNamespacesWithSubpages, $wgAllowExternalImages*,
- * $wgLocaltimezone, $wgAllowSpecialInclusion*,
- * $wgMaxArticleSize*
- *
- * * only within ParserOptions
- *
- *
- * @ingroup Parser
- */
-class Parser
-{
- /**
- * Update this version number when the ParserOutput format
- * changes in an incompatible way, so the parser cache
- * can automatically discard old data.
- */
- const VERSION = '1.6.4';
-
- # Flags for Parser::setFunctionHook
- # Also available as global constants from Defines.php
- const SFH_NO_HASH = 1;
- const SFH_OBJECT_ARGS = 2;
-
- # Constants needed for external link processing
- # Everything except bracket, space, or control characters
- const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
- const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
- \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
-
- // State constants for the definition list colon extraction
- const COLON_STATE_TEXT = 0;
- const COLON_STATE_TAG = 1;
- const COLON_STATE_TAGSTART = 2;
- const COLON_STATE_CLOSETAG = 3;
- const COLON_STATE_TAGSLASH = 4;
- const COLON_STATE_COMMENT = 5;
- const COLON_STATE_COMMENTDASH = 6;
- const COLON_STATE_COMMENTDASHDASH = 7;
-
- // Flags for preprocessToDom
- const PTD_FOR_INCLUSION = 1;
-
- // Allowed values for $this->mOutputType
- // Parameter to startExternalParse().
- const OT_HTML = 1;
- const OT_WIKI = 2;
- const OT_PREPROCESS = 3;
- const OT_MSG = 3;
-
- // Marker Suffix needs to be accessible staticly.
- const MARKER_SUFFIX = "-QINU\x7f";
-
- /**#@+
- * @private
- */
- # Persistent:
- var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
- $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
- $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
-
-
- # Cleared with clearState():
- var $mOutput, $mAutonumber, $mDTopen, $mStripState;
- var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
- var $mInterwikiLinkHolders, $mLinkHolders;
- var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
- var $mTplExpandCache; // empty-frame expansion cache
- var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
- var $mExpensiveFunctionCount; // number of expensive parser function calls
-
- # Temporary
- # These are variables reset at least once per parse regardless of $clearState
- var $mOptions, // ParserOptions object
- $mTitle, // Title context, used for self-link rendering and similar things
- $mOutputType, // Output type, one of the OT_xxx constants
- $ot, // Shortcut alias, see setOutputType()
- $mRevisionId, // ID to display in {{REVISIONID}} tags
- $mRevisionTimestamp, // The timestamp of the specified revision ID
- $mRevIdForTs; // The revision ID which was used to fetch the timestamp
-
- /**#@-*/
-
- /**
- * Constructor
- *
- * @public
- */
- function __construct( $conf = array() ) {
- $this->mConf = $conf;
- $this->mTagHooks = array();
- $this->mTransparentTagHooks = array();
- $this->mFunctionHooks = array();
- $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
- $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
- $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
- '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
- $this->mVarCache = array();
- if ( isset( $conf['preprocessorClass'] ) ) {
- $this->mPreprocessorClass = $conf['preprocessorClass'];
- } else {
- $this->mPreprocessorClass = 'Preprocessor_Hash';
- }
- $this->mMarkerIndex = 0;
- $this->mFirstCall = true;
- }
-
- /**
- * Do various kinds of initialisation on the first call of the parser
- */
- function firstCallInit() {
- if ( !$this->mFirstCall ) {
- return;
- }
- $this->mFirstCall = false;
-
- wfProfileIn( __METHOD__ );
-
- $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
- CoreParserFunctions::register( $this );
- $this->initialiseVariables();
-
- wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Clear Parser state
- *
- * @private
- */
- function clearState() {
- wfProfileIn( __METHOD__ );
- if ( $this->mFirstCall ) {
- $this->firstCallInit();
- }
- $this->mOutput = new ParserOutput;
- $this->mAutonumber = 0;
- $this->mLastSection = '';
- $this->mDTopen = false;
- $this->mIncludeCount = array();
- $this->mStripState = new StripState;
- $this->mArgStack = false;
- $this->mInPre = false;
- $this->mInterwikiLinkHolders = array(
- 'texts' => array(),
- 'titles' => array()
- );
- $this->mLinkHolders = array(
- 'namespaces' => array(),
- 'dbkeys' => array(),
- 'queries' => array(),
- 'texts' => array(),
- 'titles' => array()
- );
- $this->mRevisionTimestamp = $this->mRevisionId = null;
-
- /**
- * Prefix for temporary replacement strings for the multipass parser.
- * \x07 should never appear in input as it's disallowed in XML.
- * Using it at the front also gives us a little extra robustness
- * since it shouldn't match when butted up against identifier-like
- * string constructs.
- *
- * Must not consist of all title characters, or else it will change
- * the behaviour of in a link.
- */
- #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
- # Changed to \x7f to allow XML double-parsing -- TS
- $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
-
-
- # Clear these on every parse, bug 4549
- $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
-
- $this->mShowToc = true;
- $this->mForceTocPosition = false;
- $this->mIncludeSizes = array(
- 'post-expand' => 0,
- 'arg' => 0,
- );
- $this->mPPNodeCount = 0;
- $this->mDefaultSort = false;
- $this->mHeadings = array();
- $this->mDoubleUnderscores = array();
- $this->mExpensiveFunctionCount = 0;
-
- # Fix cloning
- if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
- $this->mPreprocessor = null;
- }
-
- wfRunHooks( 'ParserClearState', array( &$this ) );
- wfProfileOut( __METHOD__ );
- }
-
- function setOutputType( $ot ) {
- $this->mOutputType = $ot;
- // Shortcut alias
- $this->ot = array(
- 'html' => $ot == self::OT_HTML,
- 'wiki' => $ot == self::OT_WIKI,
- 'pre' => $ot == self::OT_PREPROCESS,
- );
- }
-
- /**
- * Set the context title
- */
- function setTitle( $t ) {
- if ( !$t || $t instanceof FakeTitle ) {
- $t = Title::newFromText( 'NO TITLE' );
- }
- if ( strval( $t->getFragment() ) !== '' ) {
- # Strip the fragment to avoid various odd effects
- $this->mTitle = clone $t;
- $this->mTitle->setFragment( '' );
- } else {
- $this->mTitle = $t;
- }
- }
-
- /**
- * Accessor for mUniqPrefix.
- *
- * @public
- */
- function uniqPrefix() {
- if( !isset( $this->mUniqPrefix ) ) {
- // @fixme this is probably *horribly wrong*
- // LanguageConverter seems to want $wgParser's uniqPrefix, however
- // if this is called for a parser cache hit, the parser may not
- // have ever been initialized in the first place.
- // Not really sure what the heck is supposed to be going on here.
- return '';
- //throw new MWException( "Accessing uninitialized mUniqPrefix" );
- }
- return $this->mUniqPrefix;
- }
-
- /**
- * Convert wikitext to HTML
- * Do not call this function recursively.
- *
- * @param string $text Text we want to parse
- * @param Title &$title A title object
- * @param array $options
- * @param boolean $linestart
- * @param boolean $clearState
- * @param int $revid number to pass in {{REVISIONID}}
- * @return ParserOutput a ParserOutput
- */
- public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
- /**
- * First pass--just handle sections, pass the rest off
- * to internalParse() which does all the real work.
- */
-
- global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
- $fname = 'Parser::parse-' . wfGetCaller();
- wfProfileIn( __METHOD__ );
- wfProfileIn( $fname );
-
- if ( $clearState ) {
- $this->clearState();
- }
-
- $this->mOptions = $options;
- $this->setTitle( $title );
- $oldRevisionId = $this->mRevisionId;
- $oldRevisionTimestamp = $this->mRevisionTimestamp;
- if( $revid !== null ) {
- $this->mRevisionId = $revid;
- $this->mRevisionTimestamp = null;
- }
- $this->setOutputType( self::OT_HTML );
- wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
- # No more strip!
- wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->internalParse( $text );
- $text = $this->mStripState->unstripGeneral( $text );
-
- # Clean up special characters, only run once, next-to-last before doBlockLevels
- $fixtags = array(
- # french spaces, last one Guillemet-left
- # only if there is something before the space
- '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2',
- # french spaces, Guillemet-right
- '/(\\302\\253) /' => '\\1 ',
- '/ (!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
- );
- $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
-
- # only once and last
- $text = $this->doBlockLevels( $text, $linestart );
-
- $this->replaceLinkHolders( $text );
-
- # the position of the parserConvert() call should not be changed. it
- # assumes that the links are all replaced and the only thing left
- # is the mark.
- # Side-effects: this calls $this->mOutput->setTitleText()
- $text = $wgContLang->parserConvert( $text, $this );
-
- $text = $this->mStripState->unstripNoWiki( $text );
-
- wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
-
-//!JF Move to its own function
-
- $uniq_prefix = $this->mUniqPrefix;
- $matches = array();
- $elements = array_keys( $this->mTransparentTagHooks );
- $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
-
- foreach( $matches as $marker => $data ) {
- list( $element, $content, $params, $tag ) = $data;
- $tagName = strtolower( $element );
- if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
- $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
- array( $content, $params, $this ) );
- } else {
- $output = $tag;
- }
- $this->mStripState->general->setPair( $marker, $output );
- }
- $text = $this->mStripState->unstripGeneral( $text );
-
- $text = Sanitizer::normalizeCharReferences( $text );
-
- if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
- $text = Parser::tidy($text);
- } else {
- # attempt to sanitize at least some nesting problems
- # (bug #2702 and quite a few others)
- $tidyregs = array(
- # ''Something [http://www.cool.com cool''] -->
- # Somethingcool>
- '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
- '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
- # fix up an anchor inside another anchor, only
- # at least for a single single nested link (bug 3695)
- '/(]+>)([^<]*)(]+>[^<]*)<\/a>(.*)<\/a>/' =>
- '\\1\\2\\3\\1\\4',
- # fix div inside inline elements- doBlockLevels won't wrap a line which
- # contains a div, so fix it up here; replace
- # div with escaped text
- '/(<([aib]) [^>]+>)([^<]*)(
]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
- '\\1\\3<div\\5>\\6</div>\\8\\9',
- # remove empty italic or bold tag pairs, some
- # introduced by rules above
- '/<([bi])><\/\\1>/' => '',
- );
-
- $text = preg_replace(
- array_keys( $tidyregs ),
- array_values( $tidyregs ),
- $text );
- }
- global $wgExpensiveParserFunctionLimit;
- if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
- $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
- }
-
- wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
-
- # Information on include size limits, for the benefit of users who try to skirt them
- if ( $this->mOptions->getEnableLimitReport() ) {
- global $wgExpensiveParserFunctionLimit;
- $max = $this->mOptions->getMaxIncludeSize();
- $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
- $limitReport =
- "NewPP limit report\n" .
- "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
- "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
- "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
- $PFreport;
- wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
- $text .= "\n\n";
- }
- $this->mOutput->setText( $text );
- $this->mRevisionId = $oldRevisionId;
- $this->mRevisionTimestamp = $oldRevisionTimestamp;
- wfProfileOut( $fname );
- wfProfileOut( __METHOD__ );
-
- return $this->mOutput;
- }
-
- /**
- * Recursive parser entry point that can be called from an extension tag
- * hook.
- */
- function recursiveTagParse( $text ) {
- wfProfileIn( __METHOD__ );
- wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
- wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->internalParse( $text );
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- /**
- * Expand templates and variables in the text, producing valid, static wikitext.
- * Also removes comments.
- */
- function preprocess( $text, $title, $options, $revid = null ) {
- wfProfileIn( __METHOD__ );
- $this->clearState();
- $this->setOutputType( self::OT_PREPROCESS );
- $this->mOptions = $options;
- $this->setTitle( $title );
- if( $revid !== null ) {
- $this->mRevisionId = $revid;
- }
- wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
- wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->replaceVariables( $text );
- $text = $this->mStripState->unstripBoth( $text );
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- /**
- * Get a random string
- *
- * @private
- * @static
- */
- function getRandomString() {
- return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
- }
-
- function &getTitle() { return $this->mTitle; }
- function getOptions() { return $this->mOptions; }
- function getRevisionId() { return $this->mRevisionId; }
-
- function getFunctionLang() {
- global $wgLang, $wgContLang;
-
- $target = $this->mOptions->getTargetLanguage();
- if ( $target !== null ) {
- return $target;
- } else {
- return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
- }
- }
-
- /**
- * Get a preprocessor object
- */
- function getPreprocessor() {
- if ( !isset( $this->mPreprocessor ) ) {
- $class = $this->mPreprocessorClass;
- $this->mPreprocessor = new $class( $this );
- }
- return $this->mPreprocessor;
- }
-
- /**
- * Replaces all occurrences of HTML-style comments and the given tags
- * in the text with a random marker and returns the next text. The output
- * parameter $matches will be an associative array filled with data in
- * the form:
- * 'UNIQ-xxxxx' => array(
- * 'element',
- * 'tag content',
- * array( 'param' => 'x' ),
- * 'tag content' ) )
- *
- * @param $elements list of element names. Comments are always extracted.
- * @param $text Source text string.
- * @param $uniq_prefix
- *
- * @public
- * @static
- */
- function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
- static $n = 1;
- $stripped = '';
- $matches = array();
-
- $taglist = implode( '|', $elements );
- $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
-
- while ( '' != $text ) {
- $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
- $stripped .= $p[0];
- if( count( $p ) < 5 ) {
- break;
- }
- if( count( $p ) > 5 ) {
- // comment
- $element = $p[4];
- $attributes = '';
- $close = '';
- $inside = $p[5];
- } else {
- // tag
- $element = $p[1];
- $attributes = $p[2];
- $close = $p[3];
- $inside = $p[4];
- }
-
- $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . self::MARKER_SUFFIX;
- $stripped .= $marker;
-
- if ( $close === '/>' ) {
- // Empty element tag,
- $content = null;
- $text = $inside;
- $tail = null;
- } else {
- if( $element == '!--' ) {
- $end = '/(-->)/';
- } else {
- $end = "/(<\\/$element\\s*>)/i";
- }
- $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
- $content = $q[0];
- if( count( $q ) < 3 ) {
- # No end tag -- let it run out to the end of the text.
- $tail = '';
- $text = '';
- } else {
- $tail = $q[1];
- $text = $q[2];
- }
- }
-
- $matches[$marker] = array( $element,
- $content,
- Sanitizer::decodeTagAttributes( $attributes ),
- "<$element$attributes$close$content$tail" );
- }
- return $stripped;
- }
-
- /**
- * Get a list of strippable XML-like elements
- */
- function getStripList() {
- global $wgRawHtml;
- $elements = $this->mStripList;
- if( $wgRawHtml ) {
- $elements[] = 'html';
- }
- if( $this->mOptions->getUseTeX() ) {
- $elements[] = 'math';
- }
- return $elements;
- }
-
- /**
- * @deprecated use replaceVariables
- */
- function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
- return $text;
- }
-
- /**
- * Restores pre, math, and other extensions removed by strip()
- *
- * always call unstripNoWiki() after this one
- * @private
- * @deprecated use $this->mStripState->unstrip()
- */
- function unstrip( $text, $state ) {
- return $state->unstripGeneral( $text );
- }
-
- /**
- * Always call this after unstrip() to preserve the order
- *
- * @private
- * @deprecated use $this->mStripState->unstrip()
- */
- function unstripNoWiki( $text, $state ) {
- return $state->unstripNoWiki( $text );
- }
-
- /**
- * @deprecated use $this->mStripState->unstripBoth()
- */
- function unstripForHTML( $text ) {
- return $this->mStripState->unstripBoth( $text );
- }
-
- /**
- * Add an item to the strip state
- * Returns the unique tag which must be inserted into the stripped text
- * The tag will be replaced with the original text in unstrip()
- *
- * @private
- */
- function insertStripItem( $text ) {
- $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
- $this->mMarkerIndex++;
- $this->mStripState->general->setPair( $rnd, $text );
- return $rnd;
- }
-
- /**
- * Interface with html tidy, used if $wgUseTidy = true.
- * If tidy isn't able to correct the markup, the original will be
- * returned in all its glory with a warning comment appended.
- *
- * Either the external tidy program or the in-process tidy extension
- * will be used depending on availability. Override the default
- * $wgTidyInternal setting to disable the internal if it's not working.
- *
- * @param string $text Hideous HTML input
- * @return string Corrected HTML output
- * @public
- * @static
- */
- function tidy( $text ) {
- global $wgTidyInternal;
- $wrappedtext = ''.
-'test'.$text.'';
- if( $wgTidyInternal ) {
- $correctedtext = Parser::internalTidy( $wrappedtext );
- } else {
- $correctedtext = Parser::externalTidy( $wrappedtext );
- }
- if( is_null( $correctedtext ) ) {
- wfDebug( "Tidy error detected!\n" );
- return $text . "\n\n";
- }
- return $correctedtext;
- }
-
- /**
- * Spawn an external HTML tidy process and get corrected markup back from it.
- *
- * @private
- * @static
- */
- function externalTidy( $text ) {
- global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
- $fname = 'Parser::externalTidy';
- wfProfileIn( $fname );
-
- $cleansource = '';
- $opts = ' -utf8';
-
- $descriptorspec = array(
- 0 => array('pipe', 'r'),
- 1 => array('pipe', 'w'),
- 2 => array('file', wfGetNull(), 'a')
- );
- $pipes = array();
- $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
- if (is_resource($process)) {
- // Theoretically, this style of communication could cause a deadlock
- // here. If the stdout buffer fills up, then writes to stdin could
- // block. This doesn't appear to happen with tidy, because tidy only
- // writes to stdout after it's finished reading from stdin. Search
- // for tidyParseStdin and tidySaveStdout in console/tidy.c
- fwrite($pipes[0], $text);
- fclose($pipes[0]);
- while (!feof($pipes[1])) {
- $cleansource .= fgets($pipes[1], 1024);
- }
- fclose($pipes[1]);
- proc_close($process);
- }
-
- wfProfileOut( $fname );
-
- if( $cleansource == '' && $text != '') {
- // Some kind of error happened, so we couldn't get the corrected text.
- // Just give up; we'll use the source text and append a warning.
- return null;
- } else {
- return $cleansource;
- }
- }
-
- /**
- * Use the HTML tidy PECL extension to use the tidy library in-process,
- * saving the overhead of spawning a new process.
- *
- * 'pear install tidy' should be able to compile the extension module.
- *
- * @private
- * @static
- */
- function internalTidy( $text ) {
- global $wgTidyConf, $IP, $wgDebugTidy;
- $fname = 'Parser::internalTidy';
- wfProfileIn( $fname );
-
- $tidy = new tidy;
- $tidy->parseString( $text, $wgTidyConf, 'utf8' );
- $tidy->cleanRepair();
- if( $tidy->getStatus() == 2 ) {
- // 2 is magic number for fatal error
- // http://www.php.net/manual/en/function.tidy-get-status.php
- $cleansource = null;
- } else {
- $cleansource = tidy_get_output( $tidy );
- }
- if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
- $cleansource .= "', '-->', $tidy->errorBuffer ) .
- "\n-->";
- }
-
- wfProfileOut( $fname );
- return $cleansource;
- }
-
- /**
- * parse the wiki syntax used to render tables
- *
- * @private
- */
- function doTableStuff ( $text ) {
- $fname = 'Parser::doTableStuff';
- wfProfileIn( $fname );
-
- $lines = explode ( "\n" , $text );
- $td_history = array (); // Is currently a td tag open?
- $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
- $tr_history = array (); // Is currently a tr tag open?
- $tr_attributes = array (); // history of tr attributes
- $has_opened_tr = array(); // Did this table open a
element?
- $indent_level = 0; // indent level of the table
- foreach ( $lines as $key => $line )
- {
- $line = trim ( $line );
-
- if( $line == '' ) { // empty line, go to next line
- continue;
- }
- $first_character = $line{0};
- $matches = array();
-
- if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
- // First check if we are starting a new table
- $indent_level = strlen( $matches[1] );
-
- $attributes = $this->mStripState->unstripBoth( $matches[2] );
- $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
-
- $lines[$key] = str_repeat( '
' , $indent_level ) . "
";
- array_push ( $td_history , false );
- array_push ( $last_tag_history , '' );
- array_push ( $tr_history , false );
- array_push ( $tr_attributes , '' );
- array_push ( $has_opened_tr , false );
- } else if ( count ( $td_history ) == 0 ) {
- // Don't do any of the following
- continue;
- } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
- // We are ending a table
- $line = '
-
-
-END
-);
-
- }
-
- function doSubmit() {
- global $wgOut, $wgUser, $wgLang, $wgRequest;
- global $wgReadOnlyFile;
-
- if ( ! $wgRequest->getCheck( 'wpLockConfirm' ) ) {
- $this->showForm( wfMsg( 'locknoconfirm' ) );
- return;
- }
- $fp = @fopen( $wgReadOnlyFile, 'w' );
-
- if ( false === $fp ) {
- # This used to show a file not found error, but the likeliest reason for fopen()
- # to fail at this point is insufficient permission to write to the file...good old
- # is_writable() is plain wrong in some cases, it seems...
- self::notWritable();
- return;
- }
- fwrite( $fp, $this->reason );
- fwrite( $fp, "\n
(by " . $wgUser->getName() . " at " .
- $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
- fclose( $fp );
-
- $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
- $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) );
- }
-
- function showSuccess() {
- global $wgOut;
-
- $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
- $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
- $wgOut->addWikiMsg( 'lockdbsuccesstext' );
- }
-
- public static function notWritable() {
- global $wgOut;
- $wgOut->showErrorPage( 'lockdb', 'lockfilenotwritable' );
- }
-}
diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php
deleted file mode 100644
index 3154ed1335..0000000000
--- a/includes/SpecialLog.php
+++ /dev/null
@@ -1,65 +0,0 @@
-getVal( 'type', $par );
- $user = $wgRequest->getText( 'user' );
- $title = $wgRequest->getText( 'page' );
- $pattern = $wgRequest->getBool( 'pattern' );
- $y = $wgRequest->getIntOrNull( 'year' );
- $m = $wgRequest->getIntOrNull( 'month' );
- # Don't let the user get stuck with a certain date
- $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
- if( $skip ) {
- $y = '';
- $m = '';
- }
- # Create a LogPager item to get the results and a LogEventsList
- # item to format them...
- $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
- $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m );
- # Set title and add header
- $loglist->showHeader( $pager->getType() );
- # Show form options
- $loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(),
- $pager->getYear(), $pager->getMonth() );
- # Insert list
- $logBody = $pager->getBody();
- if( $logBody ) {
- $wgOut->addHTML(
- $pager->getNavigationBar() .
- $loglist->beginLogEventsList() .
- $logBody .
- $loglist->endLogEventsList() .
- $pager->getNavigationBar()
- );
- } else {
- $wgOut->addWikiMsg( 'logempty' );
- }
-}
diff --git a/includes/SpecialLonelypages.php b/includes/SpecialLonelypages.php
deleted file mode 100644
index 5aafac7dd5..0000000000
--- a/includes/SpecialLonelypages.php
+++ /dev/null
@@ -1,58 +0,0 @@
-tableNamesN( 'page', 'pagelinks' );
-
- return
- "SELECT 'Lonelypages' AS type,
- page_namespace AS namespace,
- page_title AS title,
- page_title AS value
- FROM $page
- LEFT JOIN $pagelinks
- ON page_namespace=pl_namespace AND page_title=pl_title
- WHERE pl_namespace IS NULL
- AND page_namespace=".NS_MAIN."
- AND page_is_redirect=0";
-
- }
-}
-
-/**
- * Constructor
- */
-function wfSpecialLonelypages() {
- list( $limit, $offset ) = wfCheckLimits();
-
- $lpp = new LonelyPagesPage();
-
- return $lpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialLongpages.php b/includes/SpecialLongpages.php
deleted file mode 100644
index be16a02942..0000000000
--- a/includes/SpecialLongpages.php
+++ /dev/null
@@ -1,31 +0,0 @@
-doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php
deleted file mode 100644
index 82ee4be6e1..0000000000
--- a/includes/SpecialMIMEsearch.php
+++ /dev/null
@@ -1,138 +0,0 @@
-
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * Searches the database for files of the requested MIME type, comparing this with the
- * 'img_major_mime' and 'img_minor_mime' fields in the image table.
- * @ingroup SpecialPage
- */
-class MIMEsearchPage extends QueryPage {
- var $major, $minor;
-
- function MIMEsearchPage( $major, $minor ) {
- $this->major = $major;
- $this->minor = $minor;
- }
-
- function getName() { return 'MIMEsearch'; }
-
- /**
- * Due to this page relying upon extra fields being passed in the SELECT it
- * will fail if it's set as expensive and misermode is on
- */
- function isExpensive() { return true; }
- function isSyndicated() { return false; }
-
- function linkParameters() {
- $arr = array( $this->major, $this->minor );
- $mime = implode( '/', $arr );
- return array( 'mime' => $mime );
- }
-
- function getSQL() {
- $dbr = wfGetDB( DB_SLAVE );
- $image = $dbr->tableName( 'image' );
- $major = $dbr->addQuotes( $this->major );
- $minor = $dbr->addQuotes( $this->minor );
-
- return
- "SELECT 'MIMEsearch' AS type,
- " . NS_IMAGE . " AS namespace,
- img_name AS title,
- img_major_mime AS value,
-
- img_size,
- img_width,
- img_height,
- img_user_text,
- img_timestamp
- FROM $image
- WHERE img_major_mime = $major AND img_minor_mime = $minor
- ";
- }
-
- function formatResult( $skin, $result ) {
- global $wgContLang, $wgLang;
-
- $nt = Title::makeTitle( $result->namespace, $result->title );
- $text = $wgContLang->convert( $nt->getText() );
- $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
-
- $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) );
- $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
- $wgLang->formatNum( $result->img_size ) );
- $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ),
- $wgLang->formatNum( $result->img_height ) );
- $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
- $time = $wgLang->timeanddate( $result->img_timestamp );
-
- return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
- }
-}
-
-/**
- * Output the HTML search form, and constructs the MIMEsearchPage object.
- */
-function wfSpecialMIMEsearch( $par = null ) {
- global $wgRequest, $wgTitle, $wgOut;
-
- $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
-
- $wgOut->addHTML(
- Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
- Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
- Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' )
- );
-
- list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
- if ( $major == '' or $minor == '' or !wfSpecialMIMEsearchValidType( $major ) )
- return;
- $wpp = new MIMEsearchPage( $major, $minor );
-
- list( $limit, $offset ) = wfCheckLimits();
- $wpp->doQuery( $offset, $limit );
-}
-
-function wfSpecialMIMEsearchParse( $str ) {
- // searched for an invalid MIME type.
- if( strpos( $str, '/' ) === false) {
- return array ('', '');
- }
-
- list( $major, $minor ) = explode( '/', $str, 2 );
-
- return array(
- ltrim( $major, ' ' ),
- rtrim( $minor, ' ' )
- );
-}
-
-function wfSpecialMIMEsearchValidType( $type ) {
- // From maintenance/tables.sql => img_major_mime
- $types = array(
- 'unknown',
- 'application',
- 'audio',
- 'image',
- 'text',
- 'video',
- 'message',
- 'model',
- 'multipart'
- );
-
- return in_array( $type, $types );
-}
diff --git a/includes/SpecialMergeHistory.php b/includes/SpecialMergeHistory.php
deleted file mode 100644
index 6183374ee4..0000000000
--- a/includes/SpecialMergeHistory.php
+++ /dev/null
@@ -1,451 +0,0 @@
-execute();
-}
-
-/**
- * The HTML form for Special:MergeHistory, which allows users with the appropriate
- * permissions to view and restore deleted content.
- * @ingroup SpecialPage
- */
-class MergehistoryForm {
- var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
- var $mTargetObj, $mDestObj;
-
- function MergehistoryForm( $request, $par = "" ) {
- global $wgUser;
-
- $this->mAction = $request->getVal( 'action' );
- $this->mTarget = $request->getVal( 'target' );
- $this->mDest = $request->getVal( 'dest' );
- $this->mSubmitted = $request->getBool( 'submitted' );
-
- $this->mTargetID = intval( $request->getVal( 'targetID' ) );
- $this->mDestID = intval( $request->getVal( 'destID' ) );
- $this->mTimestamp = $request->getVal( 'mergepoint' );
- if( !preg_match("/[0-9]{14}/",$this->mTimestamp) ) {
- $this->mTimestamp = '';
- }
- $this->mComment = $request->getText( 'wpComment' );
-
- $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
- // target page
- if( $this->mSubmitted ) {
- $this->mTargetObj = Title::newFromURL( $this->mTarget );
- $this->mDestObj = Title::newFromURL( $this->mDest );
- } else {
- $this->mTargetObj = null;
- $this->mDestObj = null;
- }
-
- $this->preCacheMessages();
- }
-
- /**
- * As we use the same small set of messages in various methods and that
- * they are called often, we call them once and save them in $this->message
- */
- function preCacheMessages() {
- // Precache various messages
- if( !isset( $this->message ) ) {
- $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
- }
- }
-
- function execute() {
- global $wgOut, $wgUser;
-
- $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
-
- if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
- return $this->merge();
- }
-
- if ( !$this->mSubmitted ) {
- $this->showMergeForm();
- return;
- }
-
- $errors = array();
- if ( !$this->mTargetObj instanceof Title ) {
- $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
- } elseif( !$this->mTargetObj->exists() ) {
- $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
- wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
- );
- }
-
- if ( !$this->mDestObj instanceof Title) {
- $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
- } elseif( !$this->mDestObj->exists() ) {
- $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
- wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
- );
- }
-
- // TODO: warn about target = dest?
-
- if ( count( $errors ) ) {
- $this->showMergeForm();
- $wgOut->addHTML( implode( "\n", $errors ) );
- } else {
- $this->showHistory();
- }
-
- }
-
- function showMergeForm() {
- global $wgOut, $wgScript;
-
- $wgOut->addWikiMsg( 'mergehistory-header' );
-
- $wgOut->addHtml(
- Xml::openElement( 'form', array(
- 'method' => 'get',
- 'action' => $wgScript ) ) .
- '
\n";
-
- // work out custom project captions
- $customCaptions = array();
- $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line :
- foreach($customLines as $line){
- $parts = explode(":",$line,2);
- if(count($parts) == 2) // validate line
- $customCaptions[$parts[0]] = $parts[1];
- }
-
-
- $prev = null;
- while( $result = $matches->next() ) {
- $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
- $prev = $result->getInterwikiPrefix();
- }
- // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
- $out .= "
\n";
-
- // convert the whole thing to desired language variant
- global $wgContLang;
- $out = $wgContLang->convert( $out );
- wfProfileOut( $fname );
- return $out;
- }
-
- /**
- * Show single interwiki link
- *
- * @param SearchResult $result
- * @param string $lastInterwiki
- * @param array $terms
- * @param string $query
- * @param array $customCaptions iw prefix -> caption
- */
- function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
- $fname = 'SpecialSearch::showInterwikiHit';
- wfProfileIn( $fname );
- global $wgUser, $wgContLang, $wgLang;
-
- if( $result->isBrokenTitle() ) {
- wfProfileOut( $fname );
- return "\n";
- }
-
- $t = $result->getTitle();
- $sk = $wgUser->getSkin();
-
- $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
-
- // format redirect if any
- $redirectTitle = $result->getRedirectTitle();
- $redirectText = $result->getRedirectSnippet($terms);
- $redirect = '';
- if( !is_null($redirectTitle) )
- $redirect = ""
- .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
- ."";
-
- $out = "";
- // display project name
- if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){
- if( key_exists($t->getInterwiki(),$customCaptions) )
- // captions from 'search-interwiki-custom'
- $caption = $customCaptions[$t->getInterwiki()];
- else{
- // default is to show the hostname of the other wiki which might suck
- // if there are many wikis on one hostname
- $parsed = parse_url($t->getFullURL());
- $caption = wfMsg('search-interwiki-default', $parsed['host']);
- }
- // "more results" link (special page stuff could be localized, but we might not know target lang)
- $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
- $searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
- wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search')));
- $out .= "
';
- break;
- }
- }
-
- if ( $file->wasDeleted() && !$file->exists() ) {
- # If the file existed before and was deleted, warn the user of this
- # Don't bother doing so if the file exists now, however
- $ltitle = SpecialPage::getTitleFor( 'Log' );
- $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
- 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
- $warning .= '
' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '
';
- }
- return $warning;
- }
-
- /**
- * Get a list of warnings
- *
- * @param string local filename, e.g. 'file exists', 'non-descriptive filename'
- * @return array list of warning messages
- */
- static function ajaxGetExistsWarning( $filename ) {
- $file = wfFindFile( $filename );
- if( !$file ) {
- // Force local file so we have an object to do further checks against
- // if there isn't an exact match...
- $file = wfLocalFile( $filename );
- }
- $s = ' ';
- if ( $file ) {
- $warning = self::getExistsWarning( $file );
- if ( $warning !== '' ) {
- $s = "
$warning
";
- }
- }
- return $s;
- }
-
- /**
- * Render a preview of a given license for the AJAX preview on upload
- *
- * @param string $license
- * @return string
- */
- public static function ajaxGetLicensePreview( $license ) {
- global $wgParser, $wgUser;
- $text = '{{' . $license . '}}';
- $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
- $options = ParserOptions::newFromUser( $wgUser );
-
- // Expand subst: first, then live templates...
- $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
- $output = $wgParser->parse( $text, $title, $options );
-
- return $output->getText();
- }
-
- /**
- * Check for duplicate files and throw up a warning before the upload
- * completes.
- */
- function getDupeWarning( $tempfile ) {
- $hash = File::sha1Base36( $tempfile );
- $dupes = RepoGroup::singleton()->findBySha1( $hash );
- if( $dupes ) {
- global $wgOut;
- $msg = "";
- foreach( $dupes as $file ) {
- $title = $file->getTitle();
- $msg .= $title->getPrefixedText() .
- "|" . $title->getText() . "\n";
- }
- $msg .= "";
- return "
\n";
- } else {
- return '';
- }
- }
-
- /**
- * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
- *
- * @return array list of prefixes
- */
- public static function getFilenamePrefixBlacklist() {
- $blacklist = array();
- $message = wfMsgForContent( 'filename-prefix-blacklist' );
- if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
- $lines = explode( "\n", $message );
- foreach( $lines as $line ) {
- // Remove comment lines
- $comment = substr( trim( $line ), 0, 1 );
- if ( $comment == '#' || $comment == '' ) {
- continue;
- }
- // Remove additional comments after a prefix
- $comment = strpos( $line, '#' );
- if ( $comment > 0 ) {
- $line = substr( $line, 0, $comment-1 );
- }
- $blacklist[] = trim( $line );
- }
- }
- return $blacklist;
- }
-
- /**
- * Stash a file in a temporary directory for later processing
- * after the user has confirmed it.
- *
- * If the user doesn't explicitly cancel or accept, these files
- * can accumulate in the temp directory.
- *
- * @param string $saveName - the destination filename
- * @param string $tempName - the source temporary file to save
- * @return string - full path the stashed file, or false on failure
- * @access private
- */
- function saveTempUploadedFile( $saveName, $tempName ) {
- global $wgOut;
- $repo = RepoGroup::singleton()->getLocalRepo();
- $status = $repo->storeTemp( $saveName, $tempName );
- if ( !$status->isGood() ) {
- $this->showError( $status->getWikiText() );
- return false;
- } else {
- return $status->value;
- }
- }
-
- /**
- * Stash a file in a temporary directory for later processing,
- * and save the necessary descriptive info into the session.
- * Returns a key value which will be passed through a form
- * to pick up the path info on a later invocation.
- *
- * @return int
- * @access private
- */
- function stashSession() {
- $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
-
- if( !$stash ) {
- # Couldn't save the file.
- return false;
- }
-
- $key = mt_rand( 0, 0x7fffffff );
- $_SESSION['wsUploadData'][$key] = array(
- 'mTempPath' => $stash,
- 'mFileSize' => $this->mFileSize,
- 'mSrcName' => $this->mSrcName,
- 'mFileProps' => $this->mFileProps,
- 'version' => self::SESSION_VERSION,
- );
- return $key;
- }
-
- /**
- * Remove a temporarily kept file stashed by saveTempUploadedFile().
- * @access private
- * @return success
- */
- function unsaveUploadedFile() {
- global $wgOut;
- $repo = RepoGroup::singleton()->getLocalRepo();
- $success = $repo->freeTemp( $this->mTempPath );
- if ( ! $success ) {
- $wgOut->showFileDeleteError( $this->mTempPath );
- return false;
- } else {
- return true;
- }
- }
-
- /* -------------------------------------------------------------- */
-
- /**
- * @param string $error as HTML
- * @access private
- */
- function uploadError( $error ) {
- global $wgOut;
- $wgOut->addHTML( '
' . wfMsgHtml( 'uploadwarning' ) . "
\n" );
- $wgOut->addHTML( '' . $error . '' );
- }
-
- /**
- * There's something wrong with this file, not enough to reject it
- * totally but we require manual intervention to save it for real.
- * Stash it away, then present a form asking to confirm or cancel.
- *
- * @param string $warning as HTML
- * @access private
- */
- function uploadWarning( $warning ) {
- global $wgOut;
- global $wgUseCopyrightUpload;
-
- $this->mSessionKey = $this->stashSession();
- if( !$this->mSessionKey ) {
- # Couldn't save file; an error has been displayed so let's go.
- return;
- }
-
- $wgOut->addHTML( '
\n" );
-
- # Print a list of allowed file extensions, if so configured. We ignore
- # MIME type here, it's incomprehensible to most people and too long.
- global $wgCheckFileExtensions, $wgStrictFileExtensions,
- $wgFileExtensions, $wgFileBlacklist;
-
- $allowedExtensions = '';
- if( $wgCheckFileExtensions ) {
- $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
- if( $wgStrictFileExtensions ) {
- # Everything not permitted is banned
- $extensionsList =
- '
" .
- Xml::closeElement( 'table' ) .
- Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' )
- );
- $uploadfooter = wfMsgNoTrans( 'uploadfooter' );
- if( $uploadfooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadfooter ) ){
- $wgOut->addWikiText( '' );
- }
- }
-
- /* -------------------------------------------------------------- */
-
- /**
- * See if we should check the 'watch this page' checkbox on the form
- * based on the user's preferences and whether we're being asked
- * to create a new file or update an existing one.
- *
- * In the case where 'watch edits' is off but 'watch creations' is on,
- * we'll leave the box unchecked.
- *
- * Note that the page target can be changed *on the form*, so our check
- * state can get out of sync.
- */
- function watchCheck() {
- global $wgUser;
- if( $wgUser->getOption( 'watchdefault' ) ) {
- // Watch all edits!
- return true;
- }
-
- $local = wfLocalFile( $this->mDesiredDestName );
- if( $local && $local->exists() ) {
- // We're uploading a new version of an existing file.
- // No creation, so don't watch it if we're not already.
- return $local->getTitle()->userIsWatching();
- } else {
- // New page should get watched if that's our option.
- return $wgUser->getOption( 'watchcreations' );
- }
- }
-
- /**
- * Split a file into a base name and all dot-delimited 'extensions'
- * on the end. Some web server configurations will fall back to
- * earlier pseudo-'extensions' to determine type and execute
- * scripts, so the blacklist needs to check them all.
- *
- * @return array
- */
- function splitExtensions( $filename ) {
- $bits = explode( '.', $filename );
- $basename = array_shift( $bits );
- return array( $basename, $bits );
- }
-
- /**
- * Perform case-insensitive match against a list of file extensions.
- * Returns true if the extension is in the list.
- *
- * @param string $ext
- * @param array $list
- * @return bool
- */
- function checkFileExtension( $ext, $list ) {
- return in_array( strtolower( $ext ), $list );
- }
-
- /**
- * Perform case-insensitive match against a list of file extensions.
- * Returns true if any of the extensions are in the list.
- *
- * @param array $ext
- * @param array $list
- * @return bool
- */
- function checkFileExtensionList( $ext, $list ) {
- foreach( $ext as $e ) {
- if( in_array( strtolower( $e ), $list ) ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Verifies that it's ok to include the uploaded file
- *
- * @param string $tmpfile the full path of the temporary file to verify
- * @param string $extension The filename extension that the file is to be served with
- * @return mixed true of the file is verified, a WikiError object otherwise.
- */
- function verify( $tmpfile, $extension ) {
- #magically determine mime type
- $magic = MimeMagic::singleton();
- $mime = $magic->guessMimeType($tmpfile,false);
-
- #check mime type, if desired
- global $wgVerifyMimeType;
- if ($wgVerifyMimeType) {
-
- wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n");
- #check mime type against file extension
- if( !$this->verifyExtension( $mime, $extension ) ) {
- return new WikiErrorMsg( 'uploadcorrupt' );
- }
-
- #check mime type blacklist
- global $wgMimeTypeBlacklist;
- if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
- && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
- return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
- }
- }
-
- #check for htmlish code and javascript
- if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
- return new WikiErrorMsg( 'uploadscripted' );
- }
-
- /**
- * Scan the uploaded file for viruses
- */
- $virus= $this->detectVirus($tmpfile);
- if ( $virus ) {
- return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
- }
-
- wfDebug( __METHOD__.": all clear; passing.\n" );
- return true;
- }
-
- /**
- * Checks if the mime type of the uploaded file matches the file extension.
- *
- * @param string $mime the mime type of the uploaded file
- * @param string $extension The filename extension that the file is to be served with
- * @return bool
- */
- function verifyExtension( $mime, $extension ) {
- $magic = MimeMagic::singleton();
-
- if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
- if ( ! $magic->isRecognizableExtension( $extension ) ) {
- wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
- "unrecognized extension '$extension', can't verify\n" );
- return true;
- } else {
- wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
- "recognized extension '$extension', so probably invalid file\n" );
- return false;
- }
-
- $match= $magic->isMatchingExtension($extension,$mime);
-
- if ($match===NULL) {
- wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
- return true;
- } elseif ($match===true) {
- wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
-
- #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
- return true;
-
- } else {
- wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
- return false;
- }
- }
-
- /**
- * Heuristic for detecting files that *could* contain JavaScript instructions or
- * things that may look like HTML to a browser and are thus
- * potentially harmful. The present implementation will produce false positives in some situations.
- *
- * @param string $file Pathname to the temporary upload file
- * @param string $mime The mime type of the file
- * @param string $extension The extension of the file
- * @return bool true if the file contains something looking like embedded scripts
- */
- function detectScript($file, $mime, $extension) {
- global $wgAllowTitlesInSVG;
-
- #ugly hack: for text files, always look at the entire file.
- #For binarie field, just check the first K.
-
- if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
- else {
- $fp = fopen( $file, 'rb' );
- $chunk = fread( $fp, 1024 );
- fclose( $fp );
- }
-
- $chunk= strtolower( $chunk );
-
- if (!$chunk) return false;
-
- #decode from UTF-16 if needed (could be used for obfuscation).
- if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
- elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
- else $enc= NULL;
-
- if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
-
- $chunk= trim($chunk);
-
- #FIXME: convert from UTF-16 if necessarry!
-
- wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
-
- #check for HTML doctype
- if (eregi("addHTML( "
Bad configuration: unknown virus scanner: $wgAntivirus
\n" );
- return "unknown antivirus: $wgAntivirus";
- }
-
- # look up scanner configuration
- $command = $wgAntivirusSetup[$wgAntivirus]["command"];
- $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
- $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
- $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
-
- if ( strpos( $command,"%f" ) === false ) {
- # simple pattern: append file to scan
- $command .= " " . wfEscapeShellArg( $file );
- } else {
- # complex pattern: replace "%f" with file to scan
- $command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
- }
-
- wfDebug( __METHOD__.": running virus scan: $command \n" );
-
- # execute virus scanner
- $exitCode = false;
-
- #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
- # that does not seem to be worth the pain.
- # Ask me (Duesentrieb) about it if it's ever needed.
- $output = array();
- if ( wfIsWindows() ) {
- exec( "$command", $output, $exitCode );
- } else {
- exec( "$command 2>&1", $output, $exitCode );
- }
-
- # map exit code to AV_xxx constants.
- $mappedCode = $exitCode;
- if ( $exitCodeMap ) {
- if ( isset( $exitCodeMap[$exitCode] ) ) {
- $mappedCode = $exitCodeMap[$exitCode];
- } elseif ( isset( $exitCodeMap["*"] ) ) {
- $mappedCode = $exitCodeMap["*"];
- }
- }
-
- if ( $mappedCode === AV_SCAN_FAILED ) {
- # scan failed (code was mapped to false by $exitCodeMap)
- wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
-
- if ( $wgAntivirusRequired ) {
- return "scan failed (code $exitCode)";
- } else {
- return NULL;
- }
- } else if ( $mappedCode === AV_SCAN_ABORTED ) {
- # scan failed because filetype is unknown (probably imune)
- wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
- return NULL;
- } else if ( $mappedCode === AV_NO_VIRUS ) {
- # no virus found
- wfDebug( __METHOD__.": file passed virus scan.\n" );
- return false;
- } else {
- $output = join( "\n", $output );
- $output = trim( $output );
-
- if ( !$output ) {
- $output = true; #if there's no output, return true
- } elseif ( $msgPattern ) {
- $groups = array();
- if ( preg_match( $msgPattern, $output, $groups ) ) {
- if ( $groups[1] ) {
- $output = $groups[1];
- }
- }
- }
-
- wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
- return $output;
- }
- }
-
- /**
- * Check if the temporary file is MacBinary-encoded, as some uploads
- * from Internet Explorer on Mac OS Classic and Mac OS X will be.
- * If so, the data fork will be extracted to a second temporary file,
- * which will then be checked for validity and either kept or discarded.
- *
- * @access private
- */
- function checkMacBinary() {
- $macbin = new MacBinary( $this->mTempPath );
- if( $macbin->isValid() ) {
- $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
- $dataHandle = fopen( $dataFile, 'wb' );
-
- wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
- $macbin->extractData( $dataHandle );
-
- $this->mTempPath = $dataFile;
- $this->mFileSize = $macbin->dataForkLength();
-
- // We'll have to manually remove the new file if it's not kept.
- $this->mRemoveTempFile = true;
- }
- $macbin->close();
- }
-
- /**
- * If we've modified the upload file we need to manually remove it
- * on exit to clean up.
- * @access private
- */
- function cleanupTempFile() {
- if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
- wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
- unlink( $this->mTempPath );
- }
- }
-
- /**
- * Check if there's an overwrite conflict and, if so, if restrictions
- * forbid this user from performing the upload.
- *
- * @return mixed true on success, WikiError on failure
- * @access private
- */
- function checkOverwrite( $name ) {
- $img = wfFindFile( $name );
-
- $error = '';
- if( $img ) {
- global $wgUser, $wgOut;
- if( $img->isLocal() ) {
- if( !self::userCanReUpload( $wgUser, $img->name ) ) {
- $error = 'fileexists-forbidden';
- }
- } else {
- if( !$wgUser->isAllowed( 'reupload' ) ||
- !$wgUser->isAllowed( 'reupload-shared' ) ) {
- $error = "fileexists-shared-forbidden";
- }
- }
- }
-
- if( $error ) {
- $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
- return $errorText;
- }
-
- // Rockin', go ahead and upload
- return true;
- }
-
- /**
- * Check if a user is the last uploader
- *
- * @param User $user
- * @param string $img, image name
- * @return bool
- */
- public static function userCanReUpload( User $user, $img ) {
- if( $user->isAllowed( 'reupload' ) )
- return true; // non-conditional
- if( !$user->isAllowed( 'reupload-own' ) )
- return false;
-
- $dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow('image',
- /* SELECT */ 'img_user',
- /* WHERE */ array( 'img_name' => $img )
- );
- if ( !$row )
- return false;
-
- return $user->getId() == $row->img_user;
- }
-
- /**
- * Display an error with a wikitext description
- */
- function showError( $description ) {
- global $wgOut;
- $wgOut->setPageTitle( wfMsg( "internalerror" ) );
- $wgOut->setRobotpolicy( "noindex,nofollow" );
- $wgOut->setArticleRelated( false );
- $wgOut->enableClientCache( false );
- $wgOut->addWikiText( $description );
- }
-
- /**
- * Get the initial image page text based on a comment and optional file status information
- */
- static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
- global $wgUseCopyrightUpload;
- if ( $wgUseCopyrightUpload ) {
- if ( $license != '' ) {
- $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
- }
- $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
- '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
- "$licensetxt" .
- '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
- } else {
- if ( $license != '' ) {
- $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
- $pageText = $filedesc .
- '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
- } else {
- $pageText = $comment;
- }
- }
- return $pageText;
- }
-
- /**
- * If there are rows in the deletion log for this file, show them,
- * along with a nice little note for the user
- *
- * @param OutputPage $out
- * @param string filename
- */
- private function showDeletionLog( $out, $filename ) {
- global $wgUser;
- $loglist = new LogEventsList( $wgUser->getSkin(), $out );
- $pager = new LogPager( $loglist, 'delete', false, $filename );
- if( $pager->getNumRows() > 0 ) {
- $out->addHtml( '
' );
- }
-
- /**#@+
- * @private
- */
-
- /**
- * @return wiki text showing the license information
- */
- static function MediaWikiCredits() {
- $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
- "__NOTOC__
- This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
- copyright (C) 2001-2008 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
- Tim Starling, Erik Möller, Gabriel Wicke, Ãvar Arnfjörð Bjarmason,
- Niklas Laxström, Domas Mituzas, Rob Church, Yuri Astrakhan and others.
-
- MediaWiki is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- MediaWiki is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License]
- along with this program; if not, write to the Free Software
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
- or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].
- ";
-
- return str_replace( "\t\t", '', $ret ) . "\n";
- }
-
- /**
- * @return wiki text showing the third party software versions (apache, php, mysql).
- */
- static function softwareInformation() {
- $dbr = wfGetDB( DB_SLAVE );
-
- return Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
- Xml::openElement( 'table', array( 'id' => 'sv-software' ) ) .
- "
';
- $form .= Xml::hidden( 'days', $days );
- if( $hideOwn )
- $form .= Xml::hidden( 'hideOwn', 1 );
- if( $hideBots )
- $form .= Xml::hidden( 'hideBots', 1 );
- if( $hideMinor )
- $form .= Xml::hidden( 'hideMinor', 1 );
- $form .= Xml::closeElement( 'form' );
- $wgOut->addHtml( $form );
-
- # If there's nothing to show, stop here
- if( $numRows == 0 ) {
- $wgOut->addWikiMsg( 'watchnochange' );
- return;
- }
-
- /* End bottom header */
-
- /* Do link batch query */
- $linkBatch = new LinkBatch;
- while ( $row = $dbr->fetchObject( $res ) ) {
- $userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
- if ( $row->rc_user != 0 ) {
- $linkBatch->add( NS_USER, $userNameUnderscored );
- }
- $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
- }
- $linkBatch->execute();
- $dbr->dataSeek( $res, 0 );
-
- $list = ChangesList::newFromUser( $wgUser );
-
- $s = $list->beginRecentChangesList();
- $counter = 1;
- while ( $obj = $dbr->fetchObject( $res ) ) {
- # Make RC entry
- $rc = RecentChange::newFromRow( $obj );
- $rc->counter = $counter++;
-
- if ( $wgShowUpdatedMarker ) {
- $updated = $obj->wl_notificationtimestamp;
- } else {
- $updated = false;
- }
-
- if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
- $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
- 'COUNT(*)',
- array(
- 'wl_namespace' => $obj->rc_namespace,
- 'wl_title' => $obj->rc_title,
- ),
- __METHOD__ );
- } else {
- $rc->numberofWatchingusers = 0;
- }
-
- $s .= $list->recentChangesLine( $rc, $updated );
- }
- $s .= $list->endRecentChangesList();
-
- $dbr->freeResult( $res );
- $wgOut->addHTML( $s );
-
-}
-
-function wlHoursLink( $h, $page, $options = array() ) {
- global $wgUser, $wgLang, $wgContLang;
- $sk = $wgUser->getSkin();
- $s = $sk->makeKnownLink(
- $wgContLang->specialPage( $page ),
- $wgLang->formatNum( $h ),
- wfArrayToCGI( array('days' => ($h / 24.0)), $options ) );
- return $s;
-}
-
-function wlDaysLink( $d, $page, $options = array() ) {
- global $wgUser, $wgLang, $wgContLang;
- $sk = $wgUser->getSkin();
- $s = $sk->makeKnownLink(
- $wgContLang->specialPage( $page ),
- ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) ),
- wfArrayToCGI( array('days' => $d), $options ) );
- return $s;
-}
-
-/**
- * Returns html
- */
-function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) {
- $hours = array( 1, 2, 6, 12 );
- $days = array( 1, 3, 7 );
- $i = 0;
- foreach( $hours as $h ) {
- $hours[$i++] = wlHoursLink( $h, $page, $options );
- }
- $i = 0;
- foreach( $days as $d ) {
- $days[$i++] = wlDaysLink( $d, $page, $options );
- }
- return wfMsgExt('wlshowlast',
- array('parseinline', 'replaceafter'),
- implode(' | ', $hours),
- implode(' | ', $days),
- wlDaysLink( 0, $page, $options ) );
-}
-
-/**
- * Count the number of items on a user's watchlist
- *
- * @param $talk Include talk pages
- * @return integer
- */
-function wlCountItems( &$user, $talk = true ) {
- $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
-
- # Fetch the raw count
- $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' );
- $row = $dbr->fetchObject( $res );
- $count = $row->count;
- $dbr->freeResult( $res );
-
- # Halve to remove talk pages if needed
- if( !$talk )
- $count = floor( $count / 2 );
-
- return( $count );
-}
diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php
deleted file mode 100644
index a57df5e03f..0000000000
--- a/includes/SpecialWhatlinkshere.php
+++ /dev/null
@@ -1,408 +0,0 @@
-execute();
-}
-
-/**
- * implements Special:Whatlinkshere
- * @ingroup SpecialPage
- */
-class WhatLinksHerePage {
- // Stored data
- protected $par;
-
- // Stored objects
- protected $opts, $target, $selfTitle;
-
- // Stored globals
- protected $skin, $request;
-
- protected $limits = array( 20, 50, 100, 250, 500 );
-
- function WhatLinksHerePage( $request, $par = null ) {
- global $wgUser;
- $this->request = $request;
- $this->skin = $wgUser->getSkin();
- $this->par = $par;
- }
-
- function execute() {
- global $wgOut;
-
- $opts = new FormOptions();
-
- $opts->add( 'target', '' );
- $opts->add( 'namespace', '', FormOptions::INTNULL );
- $opts->add( 'limit', 50 );
- $opts->add( 'from', 0 );
- $opts->add( 'back', 0 );
- $opts->add( 'hideredirs', false );
- $opts->add( 'hidetrans', false );
- $opts->add( 'hidelinks', false );
- $opts->add( 'hideimages', false );
-
- $opts->fetchValuesFromRequest( $this->request );
- $opts->validateIntBounds( 'limit', 0, 5000 );
-
- // Give precedence to subpage syntax
- if ( isset($this->par) ) {
- $opts->setValue( 'target', $this->par );
- }
-
- // Bind to member variable
- $this->opts = $opts;
-
- $this->target = Title::newFromURL( $opts->getValue( 'target' ) );
- if( !$this->target ) {
- $wgOut->addHTML( $this->whatlinkshereForm() );
- return;
- }
-
- $this->selfTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->target->getPrefixedDBkey() );
-
- $wgOut->setPageTitle( wfMsgExt( 'whatlinkshere-title', 'escape', $this->target->getPrefixedText() ) );
- $wgOut->setSubtitle( wfMsgHtml( 'linklistsub' ) );
-
- $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )." \n");
-
- $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ),
- $opts->getValue( 'from' ), $opts->getValue( 'back' ) );
- }
-
- /**
- * @param $level int Recursion level
- * @param $target Title Target title
- * @param $limit int Number of entries to display
- * @param $from Title Display from this article ID
- * @param $back Title Display from this article ID at backwards scrolling
- * @private
- */
- function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
- global $wgOut, $wgMaxRedirectLinksRetrieved;
- $dbr = wfGetDB( DB_SLAVE );
- $options = array();
-
- $hidelinks = $this->opts->getValue( 'hidelinks' );
- $hideredirs = $this->opts->getValue( 'hideredirs' );
- $hidetrans = $this->opts->getValue( 'hidetrans' );
- $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' );
-
- $fetchlinks = (!$hidelinks || !$hideredirs);
-
- // Make the query
- $plConds = array(
- 'page_id=pl_from',
- 'pl_namespace' => $target->getNamespace(),
- 'pl_title' => $target->getDBkey(),
- );
- if( $hideredirs ) {
- $plConds['page_is_redirect'] = 0;
- } elseif( $hidelinks ) {
- $plConds['page_is_redirect'] = 1;
- }
-
- $tlConds = array(
- 'page_id=tl_from',
- 'tl_namespace' => $target->getNamespace(),
- 'tl_title' => $target->getDBkey(),
- );
-
- $ilConds = array(
- 'page_id=il_from',
- 'il_to' => $target->getDBkey(),
- );
-
- $namespace = $this->opts->getValue( 'namespace' );
- if ( is_int($namespace) ) {
- $plConds['page_namespace'] = $namespace;
- $tlConds['page_namespace'] = $namespace;
- $ilConds['page_namespace'] = $namespace;
- }
-
- if ( $from ) {
- $tlConds[] = "tl_from >= $from";
- $plConds[] = "pl_from >= $from";
- $ilConds[] = "il_from >= $from";
- }
-
- // Read an extra row as an at-end check
- $queryLimit = $limit + 1;
-
- // Enforce join order, sometimes namespace selector may
- // trigger filesorts which are far less efficient than scanning many entries
- $options[] = 'STRAIGHT_JOIN';
-
- $options['LIMIT'] = $queryLimit;
- $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
-
- if( $fetchlinks ) {
- $options['ORDER BY'] = 'pl_from';
- $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
- $plConds, __METHOD__, $options );
- }
-
- if( !$hidetrans ) {
- $options['ORDER BY'] = 'tl_from';
- $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
- $tlConds, __METHOD__, $options );
- }
-
- if( !$hideimages ) {
- $options['ORDER BY'] = 'il_from';
- $ilRes = $dbr->select( array( 'imagelinks', 'page' ), $fields,
- $ilConds, __METHOD__, $options );
- }
-
- if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) {
- if ( 0 == $level ) {
- $wgOut->addHTML( $this->whatlinkshereForm() );
- $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
- $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
- // Show filters only if there are links
- if( $hidelinks || $hidetrans || $hideredirs || $hideimages )
- $wgOut->addHTML( $this->getFilterPanel() );
- }
- return;
- }
-
- // Read the rows into an array and remove duplicates
- // templatelinks comes second so that the templatelinks row overwrites the
- // pagelinks row, so we get (inclusion) rather than nothing
- if( $fetchlinks ) {
- while ( $row = $dbr->fetchObject( $plRes ) ) {
- $row->is_template = 0;
- $row->is_image = 0;
- $rows[$row->page_id] = $row;
- }
- $dbr->freeResult( $plRes );
-
- }
- if( !$hidetrans ) {
- while ( $row = $dbr->fetchObject( $tlRes ) ) {
- $row->is_template = 1;
- $row->is_image = 0;
- $rows[$row->page_id] = $row;
- }
- $dbr->freeResult( $tlRes );
- }
- if( !$hideimages ) {
- while ( $row = $dbr->fetchObject( $ilRes ) ) {
- $row->is_template = 0;
- $row->is_image = 1;
- $rows[$row->page_id] = $row;
- }
- $dbr->freeResult( $ilRes );
- }
-
- // Sort by key and then change the keys to 0-based indices
- ksort( $rows );
- $rows = array_values( $rows );
-
- $numRows = count( $rows );
-
- // Work out the start and end IDs, for prev/next links
- if ( $numRows > $limit ) {
- // More rows available after these ones
- // Get the ID from the last row in the result set
- $nextId = $rows[$limit]->page_id;
- // Remove undisplayed rows
- $rows = array_slice( $rows, 0, $limit );
- } else {
- // No more rows after
- $nextId = false;
- }
- $prevId = $from;
-
- if ( $level == 0 ) {
- $wgOut->addHTML( $this->whatlinkshereForm() );
- $wgOut->addHTML( $this->getFilterPanel() );
- $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
-
- $prevnext = $this->getPrevNext( $prevId, $nextId );
- $wgOut->addHTML( $prevnext );
- }
-
- $wgOut->addHTML( $this->listStart() );
- foreach ( $rows as $row ) {
- $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
-
- if ( $row->page_is_redirect && $level < 2 ) {
- $wgOut->addHTML( $this->listItem( $row, $nt, true ) );
- $this->showIndirectLinks( $level + 1, $nt, $wgMaxRedirectLinksRetrieved );
- $wgOut->addHTML( Xml::closeElement( 'li' ) );
- } else {
- $wgOut->addHTML( $this->listItem( $row, $nt ) );
- }
- }
-
- $wgOut->addHTML( $this->listEnd() );
-
- if( $level == 0 ) {
- $wgOut->addHTML( $prevnext );
- }
- }
-
- protected function listStart() {
- return Xml::openElement( 'ul' );
- }
-
- protected function listItem( $row, $nt, $notClose = false ) {
- # local message cache
- static $msgcache = null;
- if ( $msgcache === null ) {
- static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator',
- 'whatlinkshere-links', 'isimage' );
- $msgcache = array();
- foreach ( $msgs as $msg ) {
- $msgcache[$msg] = wfMsgHtml( $msg );
- }
- }
-
- $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : '';
- $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect );
-
- // Display properties (redirect or template)
- $propsText = '';
- $props = array();
- if ( $row->page_is_redirect )
- $props[] = $msgcache['isredirect'];
- if ( $row->is_template )
- $props[] = $msgcache['istemplate'];
- if( $row->is_image )
- $props[] = $msgcache['isimage'];
-
- if ( count( $props ) ) {
- $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')';
- }
-
- # Space for utilities links, with a what-links-here link provided
- $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
- $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' );
-
- return $notClose ?
- Xml::openElement( 'li' ) . "$link $propsText $wlh\n" :
- Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n";
- }
-
- protected function listEnd() {
- return Xml::closeElement( 'ul' );
- }
-
- protected function wlhLink( Title $target, $text ) {
- static $title = null;
- if ( $title === null )
- $title = SpecialPage::getTitleFor( 'Whatlinkshere' );
-
- $targetText = $target->getPrefixedUrl();
- return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText );
- }
-
- function makeSelfLink( $text, $query ) {
- return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
- }
-
- function getPrevNext( $prevId, $nextId ) {
- global $wgLang;
- $currentLimit = $this->opts->getValue( 'limit' );
- $fmtLimit = $wgLang->formatNum( $currentLimit );
- $prev = wfMsgExt( 'whatlinkshere-prev', array( 'parsemag', 'escape' ), $fmtLimit );
- $next = wfMsgExt( 'whatlinkshere-next', array( 'parsemag', 'escape' ), $fmtLimit );
-
- $changed = $this->opts->getChangedValues();
- unset($changed['target']); // Already in the request title
-
- if ( 0 != $prevId ) {
- $overrides = array( 'from' => $this->opts->getValue( 'back' ) );
- $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) );
- }
- if ( 0 != $nextId ) {
- $overrides = array( 'from' => $nextId, 'back' => $prevId );
- $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) );
- }
-
- $limitLinks = array();
- foreach ( $this->limits as $limit ) {
- $prettyLimit = $wgLang->formatNum( $limit );
- $overrides = array( 'limit' => $limit );
- $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) );
- }
-
- $nums = implode ( ' | ', $limitLinks );
-
- return wfMsgHtml( 'viewprevnext', $prev, $next, $nums );
- }
-
- function whatlinkshereForm() {
- global $wgScript, $wgTitle;
-
- // We get nicer value from the title object
- $this->opts->consumeValue( 'target' );
- // Reset these for new requests
- $this->opts->consumeValues( array( 'back', 'from' ) );
-
- $target = $this->target ? $this->target->getPrefixedText() : '';
- $namespace = $this->opts->consumeValue( 'namespace' );
-
- # Build up the form
- $f = Xml::openElement( 'form', array( 'action' => $wgScript ) );
-
- # Values that should not be forgotten
- $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() );
- foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
- $f .= Xml::hidden( $name, $value );
- }
-
- $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) );
-
- # Target input
- $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target',
- 'mw-whatlinkshere-target', 40, $target );
-
- $f .= ' ';
-
- # Namespace selector
- $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
- Xml::namespaceSelector( $namespace, '' );
-
- # Submit
- $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
-
- # Close
- $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
-
- return $f;
- }
-
- function getFilterPanel() {
- $show = wfMsgHtml( 'show' );
- $hide = wfMsgHtml( 'hide' );
-
- $changed = $this->opts->getChangedValues();
- unset($changed['target']); // Already in the request title
-
- $links = array();
- $types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
- if( $this->target->getNamespace() == NS_IMAGE )
- $types[] = 'hideimages';
- foreach( $types as $type ) {
- $chosen = $this->opts->getValue( $type );
- $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide );
- $overrides = array( $type => !$chosen );
- $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) );
- }
- return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), implode( ' | ', $links ) );
- }
-}
diff --git a/includes/SpecialWithoutinterwiki.php b/includes/SpecialWithoutinterwiki.php
deleted file mode 100644
index 2092e43b52..0000000000
--- a/includes/SpecialWithoutinterwiki.php
+++ /dev/null
@@ -1,88 +0,0 @@
-
- */
-class WithoutInterwikiPage extends PageQueryPage {
- private $prefix = '';
-
- function getName() {
- return 'Withoutinterwiki';
- }
-
- function getPageHeader() {
- global $wgScript, $wgMiserMode;
-
- # Do not show useless input form if wiki is running in misermode
- if( $wgMiserMode ) {
- return '';
- }
-
- $prefix = $this->prefix;
- $t = SpecialPage::getTitleFor( $this->getName() );
-
- return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'withoutinterwiki-legend' ) ) .
- Xml::hidden( 'title', $t->getPrefixedText() ) .
- Xml::inputLabel( wfMsg( 'allpagesprefix' ), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
- Xml::submitButton( wfMsg( 'withoutinterwiki-submit' ) ) .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' );
- }
-
- function sortDescending() {
- return false;
- }
-
- function isExpensive() {
- return true;
- }
-
- function isSyndicated() {
- return false;
- }
-
- function getSQL() {
- $dbr = wfGetDB( DB_SLAVE );
- list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
- $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
- return
- "SELECT 'Withoutinterwiki' AS type,
- page_namespace AS namespace,
- page_title AS title,
- page_title AS value
- FROM $page
- LEFT JOIN $langlinks
- ON ll_from = page_id
- WHERE ll_title IS NULL
- AND page_namespace=" . NS_MAIN . "
- AND page_is_redirect = 0
- {$prefix}";
- }
-
- function setPrefix( $prefix = '' ) {
- $this->prefix = $prefix;
- }
-
-}
-
-function wfSpecialWithoutinterwiki() {
- global $wgRequest, $wgContLang, $wgCapitalLinks;
- list( $limit, $offset ) = wfCheckLimits();
- if( $wgCapitalLinks ) {
- $prefix = $wgContLang->ucfirst( $wgRequest->getVal( 'prefix' ) );
- } else {
- $prefix = $wgRequest->getVal( 'prefix' );
- }
- $wip = new WithoutInterwikiPage();
- $wip->setPrefix( $prefix );
- $wip->doQuery( $offset, $limit );
-}
diff --git a/includes/WebStart.php b/includes/WebStart.php
index 2154eba07c..411c211c68 100644
--- a/includes/WebStart.php
+++ b/includes/WebStart.php
@@ -69,29 +69,32 @@ define( 'MEDIAWIKI', true );
# 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.
-$preIP = realpath( '.' );
+$IP = getenv( 'MW_INSTALL_PATH' );
+if ( $IP === false ) {
+ $IP = realpath( '.' );
+}
# Start profiler
-require_once( "$preIP/StartProfiler.php" );
+require_once( "$IP/StartProfiler.php" );
wfProfileIn( 'WebStart.php-conf' );
# Load up some global defines.
-require_once( "$preIP/includes/Defines.php" );
+require_once( "$IP/includes/Defines.php" );
# LocalSettings.php is the per site customization file. If it does not exit
# the wiki installer need to be launched or the generated file moved from
# ./config/ to ./
-if( !file_exists( "$preIP/LocalSettings.php" ) ) {
- # DefaultSettings assumes $IP is defined, like it usually is when included
- # in LocalSettings. But in this case we need to provide the default one.
- $IP = $preIP;
- require_once( "$preIP/includes/DefaultSettings.php" ); # used for printing the version
- require_once( "$preIP/includes/templates/NoLocalSettings.php" );
+if( !file_exists( "$IP/LocalSettings.php" ) ) {
+ require_once( "$IP/includes/DefaultSettings.php" ); # used for printing the version
+ require_once( "$IP/includes/templates/NoLocalSettings.php" );
die();
}
-# Include site settings. Most importantly, $IP should be available after this.
-require_once( "$preIP/LocalSettings.php" );
+# Start the autoloader, so that extensions can derive classes from core files
+require_once( "$IP/includes/AutoLoader.php" );
+
+# Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked)
+require_once( "$IP/LocalSettings.php" );
wfProfileOut( 'WebStart.php-conf' );
wfProfileIn( 'WebStart.php-ob_start' );
diff --git a/includes/db/Database.php b/includes/db/Database.php
new file mode 100644
index 0000000000..b6a8c8bde6
--- /dev/null
+++ b/includes/db/Database.php
@@ -0,0 +1,2700 @@
+mFailFunction, $function );
+ }
+
+ /**
+ * Output page, used for reporting errors
+ * FALSE means discard output
+ */
+ function setOutputPage( $out ) {
+ $this->mOut = $out;
+ }
+
+ /**
+ * Boolean, controls output of large amounts of debug information
+ */
+ function debug( $debug = NULL ) {
+ return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
+ }
+
+ /**
+ * Turns buffering of SQL result sets on (true) or off (false).
+ * Default is "on" and it should not be changed without good reasons.
+ */
+ function bufferResults( $buffer = NULL ) {
+ if ( is_null( $buffer ) ) {
+ return !(bool)( $this->mFlags & DBO_NOBUFFER );
+ } else {
+ return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+ }
+ }
+
+ /**
+ * Turns on (false) or off (true) the automatic generation and sending
+ * of a "we're sorry, but there has been a database error" page on
+ * database errors. Default is on (false). When turned off, the
+ * code should use lastErrno() and lastError() to handle the
+ * situation as appropriate.
+ */
+ function ignoreErrors( $ignoreErrors = NULL ) {
+ return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
+ }
+
+ /**
+ * The current depth of nested transactions
+ * @param $level Integer: , default NULL.
+ */
+ function trxLevel( $level = NULL ) {
+ return wfSetVar( $this->mTrxLevel, $level );
+ }
+
+ /**
+ * Number of errors logged, only useful when errors are ignored
+ */
+ function errorCount( $count = NULL ) {
+ return wfSetVar( $this->mErrorCount, $count );
+ }
+
+ function tablePrefix( $prefix = null ) {
+ return wfSetVar( $this->mTablePrefix, $prefix );
+ }
+
+ /**
+ * Properties passed down from the server info array of the load balancer
+ */
+ function getLBInfo( $name = NULL ) {
+ if ( is_null( $name ) ) {
+ return $this->mLBInfo;
+ } else {
+ if ( array_key_exists( $name, $this->mLBInfo ) ) {
+ return $this->mLBInfo[$name];
+ } else {
+ return NULL;
+ }
+ }
+ }
+
+ function setLBInfo( $name, $value = NULL ) {
+ if ( is_null( $value ) ) {
+ $this->mLBInfo = $name;
+ } else {
+ $this->mLBInfo[$name] = $value;
+ }
+ }
+
+ /**
+ * Set lag time in seconds for a fake slave
+ */
+ function setFakeSlaveLag( $lag ) {
+ $this->mFakeSlaveLag = $lag;
+ }
+
+ /**
+ * Make this connection a fake master
+ */
+ function setFakeMaster( $enabled = true ) {
+ $this->mFakeMaster = $enabled;
+ }
+
+ /**
+ * Returns true if this database supports (and uses) cascading deletes
+ */
+ function cascadingDeletes() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database supports (and uses) triggers (e.g. on the page table)
+ */
+ function cleanupTriggers() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database is strict about what can be put into an IP field.
+ * Specifically, it uses a NULL value instead of an empty string.
+ */
+ function strictIPs() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database uses timestamps rather than integers
+ */
+ function realTimestamps() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database does an implicit sort when doing GROUP BY
+ */
+ function implicitGroupby() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database does an implicit order by when the column has an index
+ * For example: SELECT page_title FROM page LIMIT 1
+ */
+ function implicitOrderby() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database can do a native search on IP columns
+ * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
+ */
+ function searchableIPs() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database can use functional indexes
+ */
+ function functionalIndexes() {
+ return false;
+ }
+
+ /**#@+
+ * Get function
+ */
+ function lastQuery() { return $this->mLastQuery; }
+ function isOpen() { return $this->mOpened; }
+ /**#@-*/
+
+ function setFlag( $flag ) {
+ $this->mFlags |= $flag;
+ }
+
+ function clearFlag( $flag ) {
+ $this->mFlags &= ~$flag;
+ }
+
+ function getFlag( $flag ) {
+ return !!($this->mFlags & $flag);
+ }
+
+ /**
+ * General read-only accessor
+ */
+ function getProperty( $name ) {
+ return $this->$name;
+ }
+
+ function getWikiID() {
+ if( $this->mTablePrefix ) {
+ return "{$this->mDBname}-{$this->mTablePrefix}";
+ } else {
+ return $this->mDBname;
+ }
+ }
+
+#------------------------------------------------------------------------------
+# Other functions
+#------------------------------------------------------------------------------
+
+ /**@{{
+ * Constructor.
+ * @param string $server database server host
+ * @param string $user database user name
+ * @param string $password database user password
+ * @param string $dbname database name
+ * @param failFunction
+ * @param $flags
+ * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
+ */
+ function __construct( $server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
+
+ global $wgOut, $wgDBprefix, $wgCommandLineMode;
+ # Can't get a reference if it hasn't been set yet
+ if ( !isset( $wgOut ) ) {
+ $wgOut = NULL;
+ }
+ $this->mOut =& $wgOut;
+
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+
+ if ( $this->mFlags & DBO_DEFAULT ) {
+ if ( $wgCommandLineMode ) {
+ $this->mFlags &= ~DBO_TRX;
+ } else {
+ $this->mFlags |= DBO_TRX;
+ }
+ }
+
+ /*
+ // Faster read-only access
+ if ( wfReadOnly() ) {
+ $this->mFlags |= DBO_PERSISTENT;
+ $this->mFlags &= ~DBO_TRX;
+ }*/
+
+ /** Get the default table prefix*/
+ if ( $tablePrefix == 'get from global' ) {
+ $this->mTablePrefix = $wgDBprefix;
+ } else {
+ $this->mTablePrefix = $tablePrefix;
+ }
+
+ if ( $server ) {
+ $this->open( $server, $user, $password, $dbName );
+ }
+ }
+
+ /**
+ * @static
+ * @param failFunction
+ * @param $flags
+ */
+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
+ {
+ return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
+ }
+
+ /**
+ * Usually aborts on failure
+ * If the failFunction is set to a non-zero integer, returns success
+ */
+ function open( $server, $user, $password, $dbName ) {
+ global $wguname, $wgAllDBsAreLocalhost;
+ wfProfileIn( __METHOD__ );
+
+ # Test for missing mysql.so
+ # First try to load it
+ if (!@extension_loaded('mysql')) {
+ @dl('mysql.so');
+ }
+
+ # Fail now
+ # Otherwise we get a suppressed fatal error, which is very hard to track down
+ if ( !function_exists( 'mysql_connect' ) ) {
+ throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
+ }
+
+ # Debugging hack -- fake cluster
+ if ( $wgAllDBsAreLocalhost ) {
+ $realServer = 'localhost';
+ } else {
+ $realServer = $server;
+ }
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ $success = false;
+
+ wfProfileIn("dbconnect-$server");
+
+ # Try to connect up to three times
+ # The kernel's default SYN retransmission period is far too slow for us,
+ # so we use a short timeout plus a manual retry.
+ $this->mConn = false;
+ $max = 3;
+ $this->installErrorHandler();
+ for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $this->mConn = mysql_pconnect( $realServer, $user, $password );
+ } else {
+ # Create a new connection...
+ $this->mConn = mysql_connect( $realServer, $user, $password, true );
+ }
+ if ($this->mConn === false) {
+ #$iplus = $i + 1;
+ #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
+ }
+ }
+ $phpError = $this->restoreErrorHandler();
+
+ wfProfileOut("dbconnect-$server");
+
+ if ( $dbName != '' ) {
+ if ( $this->mConn !== false ) {
+ $success = @/**/mysql_select_db( $dbName, $this->mConn );
+ if ( !$success ) {
+ $error = "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host {$wguname['nodename']}\n";
+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
+ wfDebug( $error );
+ }
+ } else {
+ wfDebug( "DB connection error\n" );
+ wfDebug( "Server: $server, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
+ $success = false;
+ }
+ } else {
+ # Delay USE query
+ $success = (bool)$this->mConn;
+ }
+
+ if ( $success ) {
+ $version = $this->getServerVersion();
+ if ( version_compare( $version, '4.1' ) >= 0 ) {
+ // Tell the server we're communicating with it in UTF-8.
+ // This may engage various charset conversions.
+ global $wgDBmysql5;
+ if( $wgDBmysql5 ) {
+ $this->query( 'SET NAMES utf8', __METHOD__ );
+ }
+ // Turn off strict mode
+ $this->query( "SET sql_mode = ''", __METHOD__ );
+ }
+
+ // Turn off strict mode if it is on
+ } else {
+ $this->reportConnectionError( $phpError );
+ }
+
+ $this->mOpened = $success;
+ wfProfileOut( __METHOD__ );
+ return $success;
+ }
+ /**@}}*/
+
+ protected function installErrorHandler() {
+ $this->mPHPError = false;
+ set_error_handler( array( $this, 'connectionErrorHandler' ) );
+ }
+
+ protected function restoreErrorHandler() {
+ restore_error_handler();
+ return $this->mPHPError;
+ }
+
+ protected function connectionErrorHandler( $errno, $errstr ) {
+ $this->mPHPError = $errstr;
+ }
+
+ /**
+ * Closes a database connection.
+ * if it is open : commits any open transactions
+ *
+ * @return bool operation success. true if already closed.
+ */
+ function close()
+ {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ if ( $this->trxLevel() ) {
+ $this->immediateCommit();
+ }
+ return mysql_close( $this->mConn );
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @param string $error fallback error message, used if none is given by MySQL
+ */
+ function reportConnectionError( $error = 'Unknown error' ) {
+ $myError = $this->lastError();
+ if ( $myError ) {
+ $error = $myError;
+ }
+
+ if ( $this->mFailFunction ) {
+ # Legacy error handling method
+ if ( !is_int( $this->mFailFunction ) ) {
+ $ff = $this->mFailFunction;
+ $ff( $this, $error );
+ }
+ } else {
+ # New method
+ wfLogDBError( "Connection error: $error\n" );
+ throw new DBConnectionError( $this, $error );
+ }
+ }
+
+ /**
+ * Usually aborts on failure. If errors are explicitly ignored, returns success.
+ *
+ * @param $sql String: SQL query
+ * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
+ * comment (you can use __METHOD__ or add some extra info)
+ * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
+ * maybe best to catch the exception instead?
+ * @return true for a successful write query, ResultWrapper object for a successful read query,
+ * or false on failure if $tempIgnore set
+ * @throws DBQueryError Thrown when the database returns an error of any kind
+ */
+ public function query( $sql, $fname = '', $tempIgnore = false ) {
+ global $wgProfiler;
+
+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+ if ( isset( $wgProfiler ) ) {
+ # generalizeSQL will probably cut down the query to reasonable
+ # logging size most of the time. The substr is really just a sanity check.
+
+ # Who's been wasting my precious column space? -- TS
+ #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+
+ if ( $isMaster ) {
+ $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'Database::query-master';
+ } else {
+ $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'Database::query';
+ }
+ wfProfileIn( $totalProf );
+ wfProfileIn( $queryProf );
+ }
+
+ $this->mLastQuery = $sql;
+
+ # Add a comment for easy SHOW PROCESSLIST interpretation
+ #if ( $fname ) {
+ global $wgUser;
+ if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
+ $userName = $wgUser->getName();
+ if ( mb_strlen( $userName ) > 15 ) {
+ $userName = mb_substr( $userName, 0, 15 ) . '...';
+ }
+ $userName = str_replace( '/', '', $userName );
+ } else {
+ $userName = '';
+ }
+ $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
+ #} else {
+ # $commentedSql = $sql;
+ #}
+
+ # If DBO_TRX is set, start a transaction
+ if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
+ $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
+ // avoid establishing transactions for SHOW and SET statements too -
+ // that would delay transaction initializations to once connection
+ // is really used by application
+ $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
+ if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
+ $this->begin();
+ }
+
+ if ( $this->debug() ) {
+ $sqlx = substr( $commentedSql, 0, 500 );
+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
+ if ( $isMaster ) {
+ wfDebug( "SQL-master: $sqlx\n" );
+ } else {
+ wfDebug( "SQL: $sqlx\n" );
+ }
+ }
+
+ # Do the query and handle errors
+ $ret = $this->doQuery( $commentedSql );
+
+ # Try reconnecting if the connection was lost
+ if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
+ # Transaction is gone, like it or not
+ $this->mTrxLevel = 0;
+ wfDebug( "Connection lost, reconnecting...\n" );
+ if ( $this->ping() ) {
+ wfDebug( "Reconnected\n" );
+ $sqlx = substr( $commentedSql, 0, 500 );
+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
+ global $wgRequestTime;
+ $elapsed = round( microtime(true) - $wgRequestTime, 3 );
+ wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
+ $ret = $this->doQuery( $commentedSql );
+ } else {
+ wfDebug( "Failed\n" );
+ }
+ }
+
+ if ( false === $ret ) {
+ $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+ }
+
+ if ( isset( $wgProfiler ) ) {
+ wfProfileOut( $queryProf );
+ wfProfileOut( $totalProf );
+ }
+ return $this->resultObject( $ret );
+ }
+
+ /**
+ * The DBMS-dependent part of query()
+ * @param $sql String: SQL query.
+ * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
+ * @access private
+ */
+ /*private*/ function doQuery( $sql ) {
+ if( $this->bufferResults() ) {
+ $ret = mysql_query( $sql, $this->mConn );
+ } else {
+ $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ }
+ return $ret;
+ }
+
+ /**
+ * @param $error
+ * @param $errno
+ * @param $sql
+ * @param string $fname
+ * @param bool $tempIgnore
+ */
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ global $wgCommandLineMode;
+ # Ignore errors during error handling to avoid infinite recursion
+ $ignore = $this->ignoreErrors( true );
+ ++$this->mErrorCount;
+
+ if( $ignore || $tempIgnore ) {
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ } else {
+ $sql1line = str_replace( "\n", "\\n", $sql );
+ wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
+ wfDebug("SQL ERROR: " . $error . "\n");
+ throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+ }
+ }
+
+
+ /**
+ * Intended to be compatible with the PEAR::DB wrapper functions.
+ * http://pear.php.net/manual/en/package.database.db.intro-execute.php
+ *
+ * ? = scalar value, quoted as necessary
+ * ! = raw SQL bit (a function for instance)
+ * & = filename; reads the file and inserts as a blob
+ * (we don't use this though...)
+ */
+ function prepare( $sql, $func = 'Database::prepare' ) {
+ /* MySQL doesn't support prepared statements (yet), so just
+ pack up the query for reference. We'll manually replace
+ the bits later. */
+ return array( 'query' => $sql, 'func' => $func );
+ }
+
+ function freePrepared( $prepared ) {
+ /* No-op for MySQL */
+ }
+
+ /**
+ * Execute a prepared query with the various arguments
+ * @param string $prepared the prepared sql
+ * @param mixed $args Either an array here, or put scalars as varargs
+ */
+ function execute( $prepared, $args = null ) {
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $sql = $this->fillPrepared( $prepared['query'], $args );
+ return $this->query( $sql, $prepared['func'] );
+ }
+
+ /**
+ * Prepare & execute an SQL statement, quoting and inserting arguments
+ * in the appropriate places.
+ * @param string $query
+ * @param string $args ...
+ */
+ function safeQuery( $query, $args = null ) {
+ $prepared = $this->prepare( $query, 'Database::safeQuery' );
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $retval = $this->execute( $prepared, $args );
+ $this->freePrepared( $prepared );
+ return $retval;
+ }
+
+ /**
+ * For faking prepared SQL statements on DBs that don't support
+ * it directly.
+ * @param string $preparedSql - a 'preparable' SQL statement
+ * @param array $args - array of arguments to fill it with
+ * @return string executable SQL
+ */
+ function fillPrepared( $preparedQuery, $args ) {
+ reset( $args );
+ $this->preparedArgs =& $args;
+ return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
+ array( &$this, 'fillPreparedArg' ), $preparedQuery );
+ }
+
+ /**
+ * preg_callback func for fillPrepared()
+ * The arguments should be in $this->preparedArgs and must not be touched
+ * while we're doing this.
+ *
+ * @param array $matches
+ * @return string
+ * @private
+ */
+ function fillPreparedArg( $matches ) {
+ switch( $matches[1] ) {
+ case '\\?': return '?';
+ case '\\!': return '!';
+ case '\\&': return '&';
+ }
+ list( /* $n */ , $arg ) = each( $this->preparedArgs );
+ switch( $matches[1] ) {
+ case '?': return $this->addQuotes( $arg );
+ case '!': return $arg;
+ case '&':
+ # return $this->addQuotes( file_get_contents( $arg ) );
+ throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
+ default:
+ throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
+ }
+ }
+
+ /**#@+
+ * @param mixed $res A SQL result
+ */
+ /**
+ * Free a result object
+ */
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@/**/mysql_free_result( $res ) ) {
+ throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+ }
+ }
+
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mysql_fetch_object( $res );
+ if( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mysql_fetch_array( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Get the number of rows in a result object
+ */
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$n = mysql_num_rows( $res );
+ if( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $n;
+ }
+
+ /**
+ * Get the number of fields in a result object
+ * See documentation for mysql_num_fields()
+ */
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_num_fields( $res );
+ }
+
+ /**
+ * Get a field name in a result object
+ * See documentation for mysql_field_name():
+ * http://www.php.net/mysql_field_name
+ */
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_name( $res, $n );
+ }
+
+ /**
+ * Get the inserted value of an auto-increment row
+ *
+ * The value inserted should be fetched from nextSequenceValue()
+ *
+ * Example:
+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
+ * $dbw->insert('page',array('page_id' => $id));
+ * $id = $dbw->insertId();
+ */
+ function insertId() { return mysql_insert_id( $this->mConn ); }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_data_seek( $res, $row );
+ }
+
+ /**
+ * Get the last error number
+ * See mysql_errno()
+ */
+ function lastErrno() {
+ if ( $this->mConn ) {
+ return mysql_errno( $this->mConn );
+ } else {
+ return mysql_errno();
+ }
+ }
+
+ /**
+ * Get a description of the last error
+ * See mysql_error() for more details
+ */
+ function lastError() {
+ if ( $this->mConn ) {
+ # Even if it's non-zero, it can still be invalid
+ wfSuppressWarnings();
+ $error = mysql_error( $this->mConn );
+ if ( !$error ) {
+ $error = mysql_error();
+ }
+ wfRestoreWarnings();
+ } else {
+ $error = mysql_error();
+ }
+ if( $error ) {
+ $error .= ' (' . $this->mServer . ')';
+ }
+ return $error;
+ }
+ /**
+ * Get the number of rows affected by the last write query
+ * See mysql_affected_rows() for more details
+ */
+ function affectedRows() { return mysql_affected_rows( $this->mConn ); }
+ /**#@-*/ // end of template : @param $result
+
+ /**
+ * Simple UPDATE wrapper
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ *
+ * This function exists for historical reasons, Database::update() has a more standard
+ * calling convention and feature set
+ */
+ function set( $table, $var, $value, $cond, $fname = 'Database::set' )
+ {
+ $table = $this->tableName( $table );
+ $sql = "UPDATE $table SET $var = '" .
+ $this->strencode( $value ) . "' WHERE ($cond)";
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Simple SELECT wrapper, returns a single field, input must be encoded
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns FALSE on failure
+ */
+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $options['LIMIT'] = 1;
+
+ $res = $this->select( $table, $var, $cond, $fname, $options );
+ if ( $res === false || !$this->numRows( $res ) ) {
+ return false;
+ }
+ $row = $this->fetchRow( $res );
+ if ( $row !== false ) {
+ $this->freeResult( $res );
+ return $row[0];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ # Various MySQL extensions
+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ /**
+ * SELECT wrapper
+ *
+ * @param mixed $table Array or string, table name(s) (prefix auto-added)
+ * @param mixed $vars Array or string, field name(s) to be retrieved
+ * @param mixed $conds Array or string, condition(s) for WHERE
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @param array $join_conds Associative array of table join conditions (optional)
+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
+ */
+ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
+ {
+ $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * SELECT wrapper
+ *
+ * @param mixed $table Array or string, table name(s) (prefix auto-added)
+ * @param mixed $vars Array or string, field name(s) to be retrieved
+ * @param mixed $conds Array or string, condition(s) for WHERE
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @param array $join_conds Associative array of table join conditions (optional)
+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * @return string, the SQL text
+ */
+ function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
+ if( is_array( $vars ) ) {
+ $vars = implode( ',', $vars );
+ }
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if( is_array( $table ) ) {
+ if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
+ $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
+ else
+ $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
+ } elseif ($table!='') {
+ if ($table{0}==' ') {
+ $from = ' FROM ' . $table;
+ } else {
+ $from = ' FROM ' . $this->tableName( $table );
+ }
+ } else {
+ $from = '';
+ }
+
+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+
+ if( !empty( $conds ) ) {
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, LIST_AND );
+ }
+ $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+ } else {
+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+ }
+
+ if (isset($options['LIMIT']))
+ $sql = $this->limitResult($sql, $options['LIMIT'],
+ isset($options['OFFSET']) ? $options['OFFSET'] : false);
+ $sql = "$sql $postLimitTail";
+
+ if (isset($options['EXPLAIN'])) {
+ $sql = 'EXPLAIN ' . $sql;
+ }
+ return $sql;
+ }
+
+ /**
+ * Single row SELECT wrapper
+ * Aborts or returns FALSE on error
+ *
+ * $vars: the selected variables
+ * $conds: a condition map, terms are ANDed together.
+ * Items with numeric keys are taken to be literal conditions
+ * Takes an array of selected variables, and a condition map, which is ANDed
+ * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
+ * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
+ * $obj- >page_id is the ID of the Astronomy article
+ *
+ * @todo migrate documentation to phpdocumentor format
+ */
+ function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
+ $options['LIMIT'] = 1;
+ $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
+ if ( $res === false )
+ return false;
+ if ( !$this->numRows($res) ) {
+ $this->freeResult($res);
+ return false;
+ }
+ $obj = $this->fetchObject( $res );
+ $this->freeResult( $res );
+ return $obj;
+
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
+ */
+
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $options['EXPLAIN']=true;
+ $res = $this->select ($table, $vars, $conds, $fname, $options );
+ if ( $res === false )
+ return false;
+ if (!$this->numRows($res)) {
+ $this->freeResult($res);
+ return 0;
+ }
+
+ $rows=1;
+
+ while( $plan = $this->fetchObject( $res ) ) {
+ $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
+ }
+
+ $this->freeResult($res);
+ return $rows;
+ }
+
+
+ /**
+ * Removes most variables from an SQL query and replaces them with X or N for numbers.
+ * It's only slightly flawed. Don't use for anything important.
+ *
+ * @param string $sql A SQL Query
+ * @static
+ */
+ static function generalizeSQL( $sql ) {
+ # This does the same as the regexp below would do, but in such a way
+ # as to avoid crashing php on some large strings.
+ # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
+
+ $sql = str_replace ( "\\\\", '', $sql);
+ $sql = str_replace ( "\\'", '', $sql);
+ $sql = str_replace ( "\\\"", '', $sql);
+ $sql = preg_replace ("/'.*'/s", "'X'", $sql);
+ $sql = preg_replace ('/".*"/s', "'X'", $sql);
+
+ # All newlines, tabs, etc replaced by single space
+ $sql = preg_replace ( '/\s+/', ' ', $sql);
+
+ # All numbers => N
+ $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
+
+ return $sql;
+ }
+
+ /**
+ * Determines whether a field exists in a table
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( 'DESCRIBE '.$table, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+
+ $found = false;
+
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Field == $field ) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
+ }
+
+ /**
+ * Determines whether an index exists
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
+ $info = $this->indexInfo( $table, $index, $fname );
+ if ( is_null( $info ) ) {
+ return NULL;
+ } else {
+ return $info !== false;
+ }
+ }
+
+
+ /**
+ * Get information about an index into an object
+ * Returns false if the index does not exist
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+ # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
+ # SHOW INDEX should work for 3.x and up:
+ # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+ $table = $this->tableName( $table );
+ $sql = 'SHOW INDEX FROM '.$table;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+
+ $result = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
+ }
+ $this->freeResult($res);
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * Query whether a given table exists
+ */
+ function tableExists( $table ) {
+ $table = $this->tableName( $table );
+ $old = $this->ignoreErrors( true );
+ $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
+ $this->ignoreErrors( $old );
+ if( $res ) {
+ $this->freeResult( $res );
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
+ *
+ * @param $table
+ * @param $field
+ */
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM $table LIMIT 1" );
+ $n = mysql_num_fields( $res->result );
+ for( $i = 0; $i < $n; $i++ ) {
+ $meta = mysql_fetch_field( $res->result, $i );
+ if( $field == $meta->name ) {
+ return new MySQLField($meta);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * mysql_field_type() wrapper
+ */
+ function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_type( $res, $index );
+ }
+
+ /**
+ * Determines if a given index is unique
+ */
+ function indexUnique( $table, $index ) {
+ $indexInfo = $this->indexInfo( $table, $index );
+ if ( !$indexInfo ) {
+ return NULL;
+ }
+ return !$indexInfo[0]->Non_unique;
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $a may be a single associative array, or an array of these with numeric keys, for
+ * multi-row insert.
+ *
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ */
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ # No rows to insert, easy just return now
+ if ( !count( $a ) ) {
+ return true;
+ }
+
+ $table = $this->tableName( $table );
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $a[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $a );
+ }
+
+ $sql = 'INSERT ' . implode( ' ', $options ) .
+ " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+ if ( $multi ) {
+ $first = true;
+ foreach ( $a as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ } else {
+ $sql .= '(' . $this->makeList( $a ) . ')';
+ }
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Make UPDATE options for the Database::update function
+ *
+ * @private
+ * @param array $options The options passed to Database::update
+ * @return string
+ */
+ function makeUpdateOptions( $options ) {
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $opts = array();
+ if ( in_array( 'LOW_PRIORITY', $options ) )
+ $opts[] = $this->lowPriorityOption();
+ if ( in_array( 'IGNORE', $options ) )
+ $opts[] = 'IGNORE';
+ return implode(' ', $opts);
+ }
+
+ /**
+ * UPDATE wrapper, takes a condition array and a SET array
+ *
+ * @param string $table The table to UPDATE
+ * @param array $values An array of values to SET
+ * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
+ * @param string $fname The Class::Function calling this function
+ * (for the log)
+ * @param array $options An array of UPDATE options, can be one or
+ * more of IGNORE, LOW_PRIORITY
+ * @return bool
+ */
+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+ $table = $this->tableName( $table );
+ $opts = $this->makeUpdateOptions( $options );
+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Makes an encoded list of strings from an array
+ * $mode:
+ * LIST_COMMA - comma separated, no field names
+ * LIST_AND - ANDed WHERE clause (without the WHERE)
+ * LIST_OR - ORed WHERE clause (without the WHERE)
+ * LIST_SET - comma separated with field names, like a SET clause
+ * LIST_NAMES - comma separated field names
+ */
+ function makeList( $a, $mode = LIST_COMMA ) {
+ if ( !is_array( $a ) ) {
+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+ }
+
+ $first = true;
+ $list = '';
+ foreach ( $a as $field => $value ) {
+ if ( !$first ) {
+ if ( $mode == LIST_AND ) {
+ $list .= ' AND ';
+ } elseif($mode == LIST_OR) {
+ $list .= ' OR ';
+ } else {
+ $list .= ',';
+ }
+ } else {
+ $first = false;
+ }
+ if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
+ $list .= "($value)";
+ } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
+ $list .= "$value";
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+ if( count( $value ) == 0 ) {
+ throw new MWException( __METHOD__.': empty input' );
+ } elseif( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ // Don't necessarily assume the single key is 0; we don't
+ // enforce linear numeric ordering on other arrays here.
+ $value = array_values( $value );
+ $list .= $field." = ".$this->addQuotes( $value[0] );
+ } else {
+ $list .= $field." IN (".$this->makeList($value).") ";
+ }
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
+ } else {
+ if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Change the current database
+ */
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return mysql_select_db( $db, $this->mConn );
+ }
+
+ /**
+ * Get the current DB name
+ */
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ /**
+ * Get the server hostname or IP address
+ */
+ function getServer() {
+ return $this->mServer;
+ }
+
+ /**
+ * Format a table name ready for use in constructing an SQL query
+ *
+ * This does two important things: it quotes the table names to clean them up,
+ * and it adds a table prefix if only given a table name with no quotes.
+ *
+ * All functions of this object which require a table name call this function
+ * themselves. Pass the canonical name to such functions. This is only needed
+ * when calling query() directly.
+ *
+ * @param string $name database table name
+ * @return string full database name
+ */
+ function tableName( $name ) {
+ global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
+ # Skip the entire process when we have a string quoted on both ends.
+ # Note that we check the end so that we will still quote any use of
+ # use of `database`.table. But won't break things if someone wants
+ # to query a database table with a dot in the name.
+ if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
+
+ # Lets test for any bits of text that should never show up in a table
+ # name. Basically anything like JOIN or ON which are actually part of
+ # SQL queries, but may end up inside of the table value to combine
+ # sql. Such as how the API is doing.
+ # Note that we use a whitespace test rather than a \b test to avoid
+ # any remote case where a word like on may be inside of a table name
+ # surrounded by symbols which may be considered word breaks.
+ if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
+
+ # Split database and table into proper variables.
+ # We reverse the explode so that database.table and table both output
+ # the correct table.
+ $dbDetails = array_reverse( explode( '.', $name, 2 ) );
+ if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
+ else @list( $table ) = $dbDetails;
+ $prefix = $this->mTablePrefix; # Default prefix
+
+ # A database name has been specified in input. Quote the table name
+ # because we don't want any prefixes added.
+ if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
+
+ # Note that we use the long format because php will complain in in_array if
+ # the input is not an array, and will complain in is_array if it is not set.
+ if( !isset( $database ) # Don't use shared database if pre selected.
+ && isset( $wgSharedDB ) # We have a shared database
+ && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
+ && isset( $wgSharedTables )
+ && is_array( $wgSharedTables )
+ && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
+ $database = $wgSharedDB;
+ $prefix = isset( $wgSharedprefix ) ? $wgSharedprefix : $prefix;
+ }
+
+ # Quote the $database and $table and apply the prefix if not quoted.
+ if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
+ $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
+
+ # Merge our database and table into our final table name.
+ $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
+
+ # We're finished, return.
+ return $tableName;
+ }
+
+ /**
+ * Fetch a number of table names into an array
+ * This is handy when you need to construct SQL for joins
+ *
+ * Example:
+ * extract($dbr->tableNames('user','watchlist'));
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ */
+ public function tableNames() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[$name] = $this->tableName( $name );
+ }
+ return $retVal;
+ }
+
+ /**
+ * Fetch a number of table names into an zero-indexed numerical array
+ * This is handy when you need to construct SQL for joins
+ *
+ * Example:
+ * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ */
+ public function tableNamesN() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[] = $this->tableName( $name );
+ }
+ return $retVal;
+ }
+
+ /**
+ * @private
+ */
+ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
+ $ret = array();
+ $retJOIN = array();
+ $use_index_safe = is_array($use_index) ? $use_index : array();
+ $join_conds_safe = is_array($join_conds) ? $join_conds : array();
+ foreach ( $tables as $table ) {
+ // Is there a JOIN and INDEX clause for this table?
+ if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+ $retJOIN[] = $tableClause;
+ // Is there an INDEX clause?
+ } else if ( isset($use_index_safe[$table]) ) {
+ $tableClause = $this->tableName( $table );
+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+ $ret[] = $tableClause;
+ // Is there a JOIN clause?
+ } else if ( isset($join_conds_safe[$table]) ) {
+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+ $retJOIN[] = $tableClause;
+ } else {
+ $tableClause = $this->tableName( $table );
+ $ret[] = $tableClause;
+ }
+ }
+ // We can't separate explicit JOIN clauses with ',', use ' ' for those
+ $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
+ $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
+ // Compile our final table clause
+ return implode(' ',array($straightJoins,$otherJoins) );
+ }
+
+ /**
+ * Wrapper for addslashes()
+ * @param string $s String to be slashed.
+ * @return string slashed string.
+ */
+ function strencode( $s ) {
+ return mysql_real_escape_string( $s, $this->mConn );
+ }
+
+ /**
+ * If it's a string, adds quotes and backslashes
+ * Otherwise returns as-is
+ */
+ function addQuotes( $s ) {
+ if ( is_null( $s ) ) {
+ return 'NULL';
+ } else {
+ # This will also quote numeric values. This should be harmless,
+ # and protects against weird problems that occur when they really
+ # _are_ strings such as article titles and string->number->string
+ # conversion is not 1:1.
+ return "'" . $this->strencode( $s ) . "'";
+ }
+ }
+
+ /**
+ * Escape string for safe LIKE usage
+ */
+ function escapeLike( $s ) {
+ $s=$this->strencode( $s );
+ $s=str_replace(array('%','_'),array('\%','\_'),$s);
+ return $s;
+ }
+
+ /**
+ * Returns an appropriately quoted sequence value for inserting a new row.
+ * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
+ * subclass will return an integer, and save the value for insertId()
+ */
+ function nextSequenceValue( $seqName ) {
+ return NULL;
+ }
+
+ /**
+ * USE INDEX clause
+ * PostgreSQL doesn't have them and returns ""
+ */
+ function useIndexClause( $index ) {
+ return "FORCE INDEX ($index)";
+ }
+
+ /**
+ * REPLACE query wrapper
+ * PostgreSQL simulates this with a DELETE followed by INSERT
+ * $row is the row to insert, an associative array
+ * $uniqueIndexes is an array of indexes. Each element may be either a
+ * field name or an array of field names
+ *
+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
+ * However if you do this, you run the risk of encountering errors which wouldn't have
+ * occurred in MySQL
+ *
+ * @todo migrate comment to phodocumentor format
+ */
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName( $table );
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * DELETE where the condition is a join
+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
+ *
+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+ * join condition matches, set $conds='*'
+ *
+ * DO NOT put the join condition in $conds
+ *
+ * @param string $delTable The table to delete from.
+ * @param string $joinTable The other table.
+ * @param string $delVar The variable to join on, in the first table.
+ * @param string $joinVar The variable to join on, in the second table.
+ * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ */
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+ if ( $conds != '*' ) {
+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Returns the size of a text field, or -1 for "unlimited"
+ */
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+ $res = $this->query( $sql, 'Database::textFieldSize' );
+ $row = $this->fetchObject( $res );
+ $this->freeResult( $res );
+
+ $m = array();
+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+ $size = $m[1];
+ } else {
+ $size = -1;
+ }
+ return $size;
+ }
+
+ /**
+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+ */
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ /**
+ * DELETE query wrapper
+ *
+ * Use $conds == "*" to delete all rows
+ */
+ function delete( $table, $conds, $fname = 'Database::delete' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
+ }
+ $table = $this->tableName( $table );
+ $sql = "DELETE FROM $table";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+ * $conds may be "*" to copy the whole table
+ * srcTable may be an array of tables.
+ */
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+ if ( is_array( $insertOptions ) ) {
+ $insertOptions = implode( ' ', $insertOptions );
+ }
+ if( !is_array( $selectOptions ) ) {
+ $selectOptions = array( $selectOptions );
+ }
+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+ if( is_array( $srcTable ) ) {
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+ } else {
+ $srcTable = $this->tableName( $srcTable );
+ }
+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex ";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= " $tailOpts";
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Construct a LIMIT query with optional offset
+ * This is used for query pages
+ * $sql string SQL query we will append the limit too
+ * $limit integer the SQL limit
+ * $offset integer the SQL offset (default false)
+ */
+ function limitResult($sql, $limit, $offset=false) {
+ if( !is_numeric($limit) ) {
+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+ }
+ return "$sql LIMIT "
+ . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
+ . "{$limit} ";
+ }
+ function limitResultForUpdate($sql, $num) {
+ return $this->limitResult($sql, $num, 0);
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * Uses IF on MySQL.
+ *
+ * @param string $cond SQL expression which will result in a boolean value
+ * @param string $trueVal SQL expression to return if true
+ * @param string $falseVal SQL expression to return if false
+ * @return string SQL fragment
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " IF($cond, $trueVal, $falseVal) ";
+ }
+
+ /**
+ * Returns a comand for str_replace function in SQL query.
+ * Uses REPLACE() in MySQL
+ *
+ * @param string $orig String or column to modify
+ * @param string $old String or column to seek
+ * @param string $new String or column to replace with
+ */
+ function strreplace( $orig, $old, $new ) {
+ return "REPLACE({$orig}, {$old}, {$new})";
+ }
+
+ /**
+ * Determines if the last failure was due to a deadlock
+ */
+ function wasDeadlock() {
+ return $this->lastErrno() == 1213;
+ }
+
+ /**
+ * Perform a deadlock-prone transaction.
+ *
+ * This function invokes a callback function to perform a set of write
+ * queries. If a deadlock occurs during the processing, the transaction
+ * will be rolled back and the callback function will be called again.
+ *
+ * Usage:
+ * $dbw->deadlockLoop( callback, ... );
+ *
+ * Extra arguments are passed through to the specified callback function.
+ *
+ * Returns whatever the callback function returned on its successful,
+ * iteration, or false on error, for example if the retry limit was
+ * reached.
+ */
+ function deadlockLoop() {
+ $myFname = 'Database::deadlockLoop';
+
+ $this->begin();
+ $args = func_get_args();
+ $function = array_shift( $args );
+ $oldIgnore = $this->ignoreErrors( true );
+ $tries = DEADLOCK_TRIES;
+ if ( is_array( $function ) ) {
+ $fname = $function[0];
+ } else {
+ $fname = $function;
+ }
+ do {
+ $retVal = call_user_func_array( $function, $args );
+ $error = $this->lastError();
+ $errno = $this->lastErrno();
+ $sql = $this->lastQuery();
+
+ if ( $errno ) {
+ if ( $this->wasDeadlock() ) {
+ # Retry
+ usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
+ } else {
+ $this->reportQueryError( $error, $errno, $sql, $fname );
+ }
+ }
+ } while( $this->wasDeadlock() && --$tries > 0 );
+ $this->ignoreErrors( $oldIgnore );
+ if ( $tries <= 0 ) {
+ $this->query( 'ROLLBACK', $myFname );
+ $this->reportQueryError( $error, $errno, $sql, $fname );
+ return false;
+ } else {
+ $this->query( 'COMMIT', $myFname );
+ return $retVal;
+ }
+ }
+
+ /**
+ * Do a SELECT MASTER_POS_WAIT()
+ *
+ * @param string $file the binlog file
+ * @param string $pos the binlog position
+ * @param integer $timeout the maximum number of seconds to wait for synchronisation
+ */
+ function masterPosWait( MySQLMasterPos $pos, $timeout ) {
+ $fname = 'Database::masterPosWait';
+ wfProfileIn( $fname );
+
+ # Commit any open transactions
+ if ( $this->mTrxLevel ) {
+ $this->immediateCommit();
+ }
+
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
+ if ( $wait > $timeout * 1e6 ) {
+ wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
+ wfProfileOut( $fname );
+ return -1;
+ } elseif ( $wait > 0 ) {
+ wfDebug( "Fake slave waiting $wait us\n" );
+ usleep( $wait );
+ wfProfileOut( $fname );
+ return 1;
+ } else {
+ wfDebug( "Fake slave up to date ($wait us)\n" );
+ wfProfileOut( $fname );
+ return 0;
+ }
+ }
+
+ # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+ $encFile = $this->addQuotes( $pos->file );
+ $encPos = intval( $pos->pos );
+ $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
+ $res = $this->doQuery( $sql );
+ if ( $res && $row = $this->fetchRow( $res ) ) {
+ $this->freeResult( $res );
+ wfProfileOut( $fname );
+ return $row[0];
+ } else {
+ wfProfileOut( $fname );
+ return false;
+ }
+ }
+
+ /**
+ * Get the position of the master from SHOW SLAVE STATUS
+ */
+ function getSlavePos() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
+ wfDebug( __METHOD__.": fake slave pos = $pos\n" );
+ return $pos;
+ }
+ $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the position of the master from SHOW MASTER STATUS
+ */
+ function getMasterPos() {
+ if ( $this->mFakeMaster ) {
+ return new MySQLMasterPos( 'fake', microtime( true ) );
+ }
+ $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ return new MySQLMasterPos( $row->File, $row->Position );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Begin a transaction, committing any previously open transaction
+ */
+ function begin( $fname = 'Database::begin' ) {
+ $this->query( 'BEGIN', $fname );
+ $this->mTrxLevel = 1;
+ }
+
+ /**
+ * End a transaction
+ */
+ function commit( $fname = 'Database::commit' ) {
+ $this->query( 'COMMIT', $fname );
+ $this->mTrxLevel = 0;
+ }
+
+ /**
+ * Rollback a transaction.
+ * No-op on non-transactional databases.
+ */
+ function rollback( $fname = 'Database::rollback' ) {
+ $this->query( 'ROLLBACK', $fname, true );
+ $this->mTrxLevel = 0;
+ }
+
+ /**
+ * Begin a transaction, committing any previously open transaction
+ * @deprecated use begin()
+ */
+ function immediateBegin( $fname = 'Database::immediateBegin' ) {
+ $this->begin();
+ }
+
+ /**
+ * Commit transaction, if one is open
+ * @deprecated use commit()
+ */
+ function immediateCommit( $fname = 'Database::immediateCommit' ) {
+ $this->commit();
+ }
+
+ /**
+ * Return MW-style timestamp used for MySQL schema
+ */
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_MW,$ts);
+ }
+
+ /**
+ * Local database timestamp format or null
+ */
+ function timestampOrNull( $ts = null ) {
+ if( is_null( $ts ) ) {
+ return null;
+ } else {
+ return $this->timestamp( $ts );
+ }
+ }
+
+ /**
+ * @todo document
+ */
+ function resultObject( $result ) {
+ if( empty( $result ) ) {
+ return false;
+ } elseif ( $result instanceof ResultWrapper ) {
+ return $result;
+ } elseif ( $result === true ) {
+ // Successful write query
+ return $result;
+ } else {
+ return new ResultWrapper( $this, $result );
+ }
+ }
+
+ /**
+ * Return aggregated value alias
+ */
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuename;
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.mysql.com/ MySQL]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ return mysql_get_server_info( $this->mConn );
+ }
+
+ /**
+ * Ping the server and try to reconnect if it there is no connection
+ */
+ function ping() {
+ if( !function_exists( 'mysql_ping' ) ) {
+ wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
+ return true;
+ }
+ $ping = mysql_ping( $this->mConn );
+ if ( $ping ) {
+ return true;
+ }
+
+ // Need to reconnect manually in MySQL client 5.0.13+
+ if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
+ mysql_close( $this->mConn );
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get slave lag.
+ * At the moment, this will only work if the DB user has the PROCESS privilege
+ */
+ function getLag() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+ return $this->mFakeSlaveLag;
+ }
+ $res = $this->query( 'SHOW PROCESSLIST' );
+ # Find slave SQL thread
+ while ( $row = $this->fetchObject( $res ) ) {
+ /* This should work for most situations - when default db
+ * for thread is not specified, it had no events executed,
+ * and therefore it doesn't know yet how lagged it is.
+ *
+ * Relay log I/O thread does not select databases.
+ */
+ if ( $row->User == 'system user' &&
+ $row->State != 'Waiting for master to send event' &&
+ $row->State != 'Connecting to master' &&
+ $row->State != 'Queueing master event to the relay log' &&
+ $row->State != 'Waiting for master update' &&
+ $row->State != 'Requesting binlog dump'
+ ) {
+ # This is it, return the time (except -ve)
+ if ( $row->Time > 0x7fffffff ) {
+ return false;
+ } else {
+ return $row->Time;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get status information from SHOW STATUS in an associative array
+ */
+ function getStatus($which="%") {
+ $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+ $status = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ $status[$row->Variable_name] = $row->Value;
+ }
+ return $status;
+ }
+
+ /**
+ * Return the maximum number of items allowed in a list, or 0 for unlimited.
+ */
+ function maxListLen() {
+ return 0;
+ }
+
+ function encodeBlob($b) {
+ return $b;
+ }
+
+ function decodeBlob($b) {
+ return $b;
+ }
+
+ /**
+ * Override database's default connection timeout.
+ * May be useful for very long batch queries such as
+ * full-wiki dumps, where a single query reads out
+ * over hours or days.
+ * @param int $timeout in seconds
+ */
+ public function setTimeout( $timeout ) {
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
+ }
+
+ /**
+ * Read and execute SQL commands from a file.
+ * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
+ * @param string $filename File name to open
+ * @param callback $lineCallback Optional function called before reading each line
+ * @param callback $resultCallback Optional function called for each MySQL result
+ */
+ function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
+ $fp = fopen( $filename, 'r' );
+ if ( false === $fp ) {
+ throw new MWException( "Could not open \"{$filename}\".\n" );
+ }
+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
+ fclose( $fp );
+ return $error;
+ }
+
+ /**
+ * Read and execute commands from an open file handle
+ * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
+ * @param string $fp File handle
+ * @param callback $lineCallback Optional function called before reading each line
+ * @param callback $resultCallback Optional function called for each MySQL result
+ */
+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
+ $cmd = "";
+ $done = false;
+ $dollarquote = false;
+
+ while ( ! feof( $fp ) ) {
+ if ( $lineCallback ) {
+ call_user_func( $lineCallback );
+ }
+ $line = trim( fgets( $fp, 1024 ) );
+ $sl = strlen( $line ) - 1;
+
+ if ( $sl < 0 ) { continue; }
+ if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
+
+ ## Allow dollar quoting for function declarations
+ if (substr($line,0,4) == '$mw$') {
+ if ($dollarquote) {
+ $dollarquote = false;
+ $done = true;
+ }
+ else {
+ $dollarquote = true;
+ }
+ }
+ else if (!$dollarquote) {
+ if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
+ $done = true;
+ $line = substr( $line, 0, $sl );
+ }
+ }
+
+ if ( '' != $cmd ) { $cmd .= ' '; }
+ $cmd .= "$line\n";
+
+ if ( $done ) {
+ $cmd = str_replace(';;', ";", $cmd);
+ $cmd = $this->replaceVars( $cmd );
+ $res = $this->query( $cmd, __METHOD__ );
+ if ( $resultCallback ) {
+ call_user_func( $resultCallback, $res );
+ }
+
+ if ( false === $res ) {
+ $err = $this->lastError();
+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ }
+
+ $cmd = '';
+ $done = false;
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Replace variables in sourced SQL
+ */
+ protected function replaceVars( $ins ) {
+ $varnames = array(
+ 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
+ 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
+ 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
+ );
+
+ // Ordinary variables
+ foreach ( $varnames as $var ) {
+ if( isset( $GLOBALS[$var] ) ) {
+ $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
+ $ins = str_replace( '{$' . $var . '}', $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
+ }
+ }
+
+ // Table prefixes
+ $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
+ array( &$this, 'tableNameCallback' ), $ins );
+ return $ins;
+ }
+
+ /**
+ * Table name callback
+ * @private
+ */
+ protected function tableNameCallback( $matches ) {
+ return $this->tableName( $matches[1] );
+ }
+
+ /*
+ * Build a concatenation list to feed into a SQL query
+ */
+ function buildConcat( $stringList ) {
+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ }
+
+ /**
+ * Acquire a lock
+ *
+ * Abstracted from Filestore::lock() so child classes can implement for
+ * their own needs.
+ *
+ * @param string $lockName Name of lock to aquire
+ * @param string $method Name of method calling us
+ * @return bool
+ */
+ public function lock( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ $this->freeResult( $result );
+
+ if( $row->lockstatus == 1 ) {
+ return true;
+ } else {
+ wfDebug( __METHOD__." failed to acquire lock\n" );
+ return false;
+ }
+ }
+ /**
+ * Release a lock.
+ *
+ * @todo fixme - Figure out a way to return a bool
+ * based on successful lock release.
+ *
+ * @param string $lockName Name of lock to release
+ * @param string $method Name of method calling us
+ */
+ public function unlock( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
+ $this->freeResult( $result );
+ }
+}
+
+/**
+ * Database abstraction object for mySQL
+ * Inherit all methods and properties of Database::Database()
+ *
+ * @ingroup Database
+ * @see Database
+ */
+class DatabaseMysql extends Database {
+ # Inherit all
+}
+
+/******************************************************************************
+ * Utility classes
+ *****************************************************************************/
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class DBObject {
+ public $mData;
+
+ function DBObject($data) {
+ $this->mData = $data;
+ }
+
+ function isLOB() {
+ return false;
+ }
+
+ function data() {
+ return $this->mData;
+ }
+}
+
+/**
+ * Utility class
+ * @ingroup Database
+ *
+ * This allows us to distinguish a blob from a normal string and an array of strings
+ */
+class Blob {
+ private $mData;
+ function __construct($data) {
+ $this->mData = $data;
+ }
+ function fetch() {
+ return $this->mData;
+ }
+}
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class MySQLField {
+ private $name, $tablename, $default, $max_length, $nullable,
+ $is_pk, $is_unique, $is_multiple, $is_key, $type;
+ function __construct ($info) {
+ $this->name = $info->name;
+ $this->tablename = $info->table;
+ $this->default = $info->def;
+ $this->max_length = $info->max_length;
+ $this->nullable = !$info->not_null;
+ $this->is_pk = $info->primary_key;
+ $this->is_unique = $info->unique_key;
+ $this->is_multiple = $info->multiple_key;
+ $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
+ $this->type = $info->type;
+ }
+
+ function name() {
+ return $this->name;
+ }
+
+ function tableName() {
+ return $this->tableName;
+ }
+
+ function defaultValue() {
+ return $this->default;
+ }
+
+ function maxLength() {
+ return $this->max_length;
+ }
+
+ function nullable() {
+ return $this->nullable;
+ }
+
+ function isKey() {
+ return $this->is_key;
+ }
+
+ function isMultipleKey() {
+ return $this->is_multiple;
+ }
+
+ function type() {
+ return $this->type;
+ }
+}
+
+/******************************************************************************
+ * Error classes
+ *****************************************************************************/
+
+/**
+ * Database error base class
+ * @ingroup Database
+ */
+class DBError extends MWException {
+ public $db;
+
+ /**
+ * Construct a database error
+ * @param Database $db The database object which threw the error
+ * @param string $error A simple error message to be used for debugging
+ */
+ function __construct( Database &$db, $error ) {
+ $this->db =& $db;
+ parent::__construct( $error );
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBConnectionError extends DBError {
+ public $error;
+
+ function __construct( Database &$db, $error = 'unknown error' ) {
+ $msg = 'DB connection error';
+ if ( trim( $error ) != '' ) {
+ $msg .= ": $error";
+ }
+ $this->error = $error;
+ parent::__construct( $db, $msg );
+ }
+
+ function useOutputPage() {
+ // Not likely to work
+ return false;
+ }
+
+ function useMessageCache() {
+ // Not likely to work
+ return false;
+ }
+
+ function getText() {
+ return $this->getMessage() . "\n";
+ }
+
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
+ }
+
+ function getPageTitle() {
+ global $wgSitename;
+ return "$wgSitename has a problem";
+ }
+
+ function getHTML() {
+ global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
+ global $wgSitename, $wgServer, $wgMessageCache;
+
+ # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
+ # Hard coding strings instead.
+
+ $noconnect = "
Sorry! This site is experiencing technical difficulties.
Try waiting a few minutes and reloading.
(Can't contact the database server: $1)
";
+ $mainpage = 'Main Page';
+ $searchdisabled = <<$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
+Note that their indexes of $wgSitename content may be out of date.
',
+EOT;
+
+ $googlesearch = "
+
+
+";
+ $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
+
+ # No database access
+ if ( is_object( $wgMessageCache ) ) {
+ $wgMessageCache->disable();
+ }
+
+ if ( trim( $this->error ) == '' ) {
+ $this->error = $this->db->getProperty('mServer');
+ }
+
+ $text = str_replace( '$1', $this->error, $noconnect );
+ $text .= wfGetSiteNotice();
+
+ if($wgUseFileCache) {
+ if($wgTitle) {
+ $t =& $wgTitle;
+ } else {
+ if($title) {
+ $t = Title::newFromURL( $title );
+ } elseif (@/**/$_REQUEST['search']) {
+ $search = $_REQUEST['search'];
+ return $searchdisabled .
+ str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
+ $wgInputEncoding ), $googlesearch );
+ } else {
+ $t = Title::newFromText( $mainpage );
+ }
+ }
+
+ $cache = new HTMLFileCache( $t );
+ if( $cache->isFileCached() ) {
+ // @todo, FIXME: $msg is not defined on the next line.
+ $msg = '
'.$msg." \n" .
+ $cachederror . "
\n";
+
+ $tag = '
';
+ $text = str_replace(
+ $tag,
+ $tag . $msg,
+ $cache->fetchPageText() );
+ }
+ }
+
+ return $text;
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBQueryError extends DBError {
+ public $error, $errno, $sql, $fname;
+
+ function __construct( Database &$db, $error, $errno, $sql, $fname ) {
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+
+ parent::__construct( $db, $message );
+ $this->error = $error;
+ $this->errno = $errno;
+ $this->sql = $sql;
+ $this->fname = $fname;
+ }
+
+ function getText() {
+ if ( $this->useMessageCache() ) {
+ return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ } else {
+ return $this->getMessage();
+ }
+ }
+
+ function getSQL() {
+ global $wgShowSQLErrors;
+ if( !$wgShowSQLErrors ) {
+ return $this->msg( 'sqlhidden', 'SQL hidden' );
+ } else {
+ return $this->sql;
+ }
+ }
+
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
+ }
+
+ function getPageTitle() {
+ return $this->msg( 'databaseerror', 'Database error' );
+ }
+
+ function getHTML() {
+ if ( $this->useMessageCache() ) {
+ return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
+ } else {
+ return nl2br( htmlspecialchars( $this->getMessage() ) );
+ }
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBUnexpectedError extends DBError {}
+
+
+/**
+ * Result wrapper for grabbing data queried by someone else
+ * @ingroup Database
+ */
+class ResultWrapper implements Iterator {
+ var $db, $result, $pos = 0, $currentRow = null;
+
+ /**
+ * Create a new result object from a result resource and a Database object
+ */
+ function ResultWrapper( $database, $result ) {
+ $this->db = $database;
+ if ( $result instanceof ResultWrapper ) {
+ $this->result = $result->result;
+ } else {
+ $this->result = $result;
+ }
+ }
+
+ /**
+ * Get the number of rows in a result object
+ */
+ function numRows() {
+ return $this->db->numRows( $this->result );
+ }
+
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchObject() {
+ return $this->db->fetchObject( $this->result );
+ }
+
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchRow() {
+ return $this->db->fetchRow( $this->result );
+ }
+
+ /**
+ * Free a result object
+ */
+ function free() {
+ $this->db->freeResult( $this->result );
+ unset( $this->result );
+ unset( $this->db );
+ }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function seek( $row ) {
+ $this->db->dataSeek( $this->result, $row );
+ }
+
+ /*********************
+ * Iterator functions
+ * Note that using these in combination with the non-iterator functions
+ * above may cause rows to be skipped or repeated.
+ */
+
+ function rewind() {
+ if ($this->numRows()) {
+ $this->db->dataSeek($this->result, 0);
+ }
+ $this->pos = 0;
+ $this->currentRow = null;
+ }
+
+ function current() {
+ if ( is_null( $this->currentRow ) ) {
+ $this->next();
+ }
+ return $this->currentRow;
+ }
+
+ function key() {
+ return $this->pos;
+ }
+
+ function next() {
+ $this->pos++;
+ $this->currentRow = $this->fetchObject();
+ return $this->currentRow;
+ }
+
+ function valid() {
+ return $this->current() !== false;
+ }
+}
+
+class MySQLMasterPos {
+ var $file, $pos;
+
+ function __construct( $file, $pos ) {
+ $this->file = $file;
+ $this->pos = $pos;
+ }
+
+ function __toString() {
+ return "{$this->file}/{$this->pos}";
+ }
+}
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
new file mode 100755
index 0000000000..a6dda2573f
--- /dev/null
+++ b/includes/db/DatabaseMssql.php
@@ -0,0 +1,1019 @@
+mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+
+ if ( $this->mFlags & DBO_DEFAULT ) {
+ if ( $wgCommandLineMode ) {
+ $this->mFlags &= ~DBO_TRX;
+ } else {
+ $this->mFlags |= DBO_TRX;
+ }
+ }
+
+ /** Get the default table prefix*/
+ $this->mTablePrefix = $tablePrefix == 'get from global' ? $wgDBprefix : $tablePrefix;
+
+ if ($server) $this->open($server, $user, $password, $dbName);
+
+ }
+
+ /**
+ * todo: check if these should be true like parent class
+ */
+ function implicitGroupby() { return false; }
+ function implicitOrderby() { return false; }
+
+ static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
+ return new DatabaseMssql($server, $user, $password, $dbName, $failFunction, $flags);
+ }
+
+ /** Open an MSSQL database and return a resource handle to it
+ * NOTE: only $dbName is used, the other parameters are irrelevant for MSSQL databases
+ */
+ function open($server,$user,$password,$dbName) {
+ wfProfileIn(__METHOD__);
+
+ # Test for missing mysql.so
+ # First try to load it
+ if (!@extension_loaded('mssql')) {
+ @dl('mssql.so');
+ }
+
+ # Fail now
+ # Otherwise we get a suppressed fatal error, which is very hard to track down
+ if (!function_exists( 'mssql_connect')) {
+ throw new DBConnectionError( $this, "MSSQL functions missing, have you compiled PHP with the --with-mssql option?\n" );
+ }
+
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ wfProfileIn("dbconnect-$server");
+
+ # Try to connect up to three times
+ # The kernel's default SYN retransmission period is far too slow for us,
+ # so we use a short timeout plus a manual retry.
+ $this->mConn = false;
+ $max = 3;
+ for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ($this->mFlags & DBO_PERSISTENT) {
+ @/**/$this->mConn = mssql_pconnect($server, $user, $password);
+ } else {
+ # Create a new connection...
+ @/**/$this->mConn = mssql_connect($server, $user, $password, true);
+ }
+ }
+
+ wfProfileOut("dbconnect-$server");
+
+ if ($dbName != '') {
+ if ($this->mConn !== false) {
+ $success = @/**/mssql_select_db($dbName, $this->mConn);
+ if (!$success) {
+ $error = "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host {$wguname['nodename']}\n";
+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
+ wfDebug( $error );
+ }
+ } else {
+ wfDebug("DB connection error\n");
+ wfDebug("Server: $server, User: $user, Password: ".substr($password, 0, 3)."...\n");
+ $success = false;
+ }
+ } else {
+ # Delay USE query
+ $success = (bool)$this->mConn;
+ }
+
+ if (!$success) $this->reportConnectionError();
+ $this->mOpened = $success;
+ wfProfileOut(__METHOD__);
+ return $success;
+ }
+
+ /**
+ * Close an MSSQL database
+ */
+ function close() {
+ $this->mOpened = false;
+ if ($this->mConn) {
+ if ($this->trxLevel()) $this->immediateCommit();
+ return mssql_close($this->mConn);
+ } else return true;
+ }
+
+ /**
+ * - MSSQL doesn't seem to do buffered results
+ * - the trasnaction syntax is modified here to avoid having to replicate
+ * Database::query which uses BEGIN, COMMIT, ROLLBACK
+ */
+ function doQuery($sql) {
+ if ($sql == 'BEGIN' || $sql == 'COMMIT' || $sql == 'ROLLBACK') return true; # $sql .= ' TRANSACTION';
+ $sql = preg_replace('|[^\x07-\x7e]|','?',$sql); # TODO: need to fix unicode - just removing it here while testing
+ $ret = mssql_query($sql, $this->mConn);
+ if ($ret === false) {
+ $err = mssql_get_last_message();
+ if ($err) $this->mlastError = $err;
+ $row = mssql_fetch_row(mssql_query('select @@ERROR'));
+ if ($row[0]) $this->mlastErrorNo = $row[0];
+ } else $this->mlastErrorNo = false;
+ return $ret;
+ }
+
+ /**#@+
+ * @param mixed $res A SQL result
+ */
+ /**
+ * Free a result object
+ */
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@/**/mssql_free_result( $res ) ) {
+ throw new DBUnexpectedError( $this, "Unable to free MSSQL result" );
+ }
+ }
+
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mssql_fetch_object( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mssql_fetch_array( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Get the number of rows in a result object
+ */
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$n = mssql_num_rows( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $n;
+ }
+
+ /**
+ * Get the number of fields in a result object
+ * See documentation for mysql_num_fields()
+ */
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_num_fields( $res );
+ }
+
+ /**
+ * Get a field name in a result object
+ * See documentation for mysql_field_name():
+ * http://www.php.net/mysql_field_name
+ */
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_field_name( $res, $n );
+ }
+
+ /**
+ * Get the inserted value of an auto-increment row
+ *
+ * The value inserted should be fetched from nextSequenceValue()
+ *
+ * Example:
+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
+ * $dbw->insert('page',array('page_id' => $id));
+ * $id = $dbw->insertId();
+ */
+ function insertId() {
+ $row = mssql_fetch_row(mssql_query('select @@IDENTITY'));
+ return $row[0];
+ }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_data_seek( $res, $row );
+ }
+
+ /**
+ * Get the last error number
+ */
+ function lastErrno() {
+ return $this->mlastErrorNo;
+ }
+
+ /**
+ * Get a description of the last error
+ */
+ function lastError() {
+ return $this->mlastError;
+ }
+
+ /**
+ * Get the number of rows affected by the last write query
+ */
+ function affectedRows() {
+ return mssql_rows_affected( $this->mConn );
+ }
+
+ /**
+ * Simple UPDATE wrapper
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ *
+ * This function exists for historical reasons, Database::update() has a more standard
+ * calling convention and feature set
+ */
+ function set( $table, $var, $value, $cond, $fname = 'Database::set' )
+ {
+ if ($value == "NULL") $value = "''"; # see comments in makeListWithoutNulls()
+ $table = $this->tableName( $table );
+ $sql = "UPDATE $table SET $var = '" .
+ $this->strencode( $value ) . "' WHERE ($cond)";
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Simple SELECT wrapper, returns a single field, input must be encoded
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns FALSE on failure
+ */
+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $options['LIMIT'] = 1;
+
+ $res = $this->select( $table, $var, $cond, $fname, $options );
+ if ( $res === false || !$this->numRows( $res ) ) {
+ return false;
+ }
+ $row = $this->fetchRow( $res );
+ if ( $row !== false ) {
+ $this->freeResult( $res );
+ return $row[0];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ # Various MySQL extensions
+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ /**
+ * SELECT wrapper
+ *
+ * @param mixed $table Array or string, table name(s) (prefix auto-added)
+ * @param mixed $vars Array or string, field name(s) to be retrieved
+ * @param mixed $conds Array or string, condition(s) for WHERE
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
+ */
+ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
+ {
+ if( is_array( $vars ) ) {
+ $vars = implode( ',', $vars );
+ }
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if( is_array( $table ) ) {
+ if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
+ $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
+ else
+ $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
+ } elseif ($table!='') {
+ if ($table{0}==' ') {
+ $from = ' FROM ' . $table;
+ } else {
+ $from = ' FROM ' . $this->tableName( $table );
+ }
+ } else {
+ $from = '';
+ }
+
+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+
+ if( !empty( $conds ) ) {
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, LIST_AND );
+ }
+ $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+ } else {
+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+ }
+
+ if (isset($options['LIMIT']))
+ $sql = $this->limitResult($sql, $options['LIMIT'],
+ isset($options['OFFSET']) ? $options['OFFSET'] : false);
+ $sql = "$sql $postLimitTail";
+
+ if (isset($options['EXPLAIN'])) {
+ $sql = 'EXPLAIN ' . $sql;
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
+ */
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $rows = 0;
+ $res = $this->select ($table, 'COUNT(*)', $conds, $fname, $options );
+ if ($res) {
+ $row = $this->fetchObject($res);
+ $rows = $row[0];
+ }
+ $this->freeResult($res);
+ return $rows;
+ }
+
+ /**
+ * Determines whether a field exists in a table
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT TOP 1 * FROM $table";
+ $res = $this->query( $sql, 'Database::fieldExists' );
+
+ $found = false;
+ while ( $row = $this->fetchArray( $res ) ) {
+ if ( isset($row[$field]) ) {
+ $found = true;
+ break;
+ }
+ }
+
+ $this->freeResult( $res );
+ return $found;
+ }
+
+ /**
+ * Get information about an index into an object
+ * Returns false if the index does not exist
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+
+ throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' );
+ return NULL;
+
+ $table = $this->tableName( $table );
+ $sql = 'SHOW INDEX FROM '.$table;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+
+ $result = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
+ }
+ $this->freeResult($res);
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * Query whether a given table exists
+ */
+ function tableExists( $table ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '$table'" );
+ $exist = ($res->numRows() > 0);
+ $this->freeResult($res);
+ return $exist;
+ }
+
+ /**
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
+ *
+ * @param $table
+ * @param $field
+ */
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT TOP 1 * FROM $table" );
+ $n = mssql_num_fields( $res->result );
+ for( $i = 0; $i < $n; $i++ ) {
+ $meta = mssql_fetch_field( $res->result, $i );
+ if( $field == $meta->name ) {
+ return new MSSQLField($meta);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * mysql_field_type() wrapper
+ */
+ function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_field_type( $res, $index );
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $a may be a single associative array, or an array of these with numeric keys, for
+ * multi-row insert.
+ *
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ *
+ * Same as parent class implementation except that it removes primary key from column lists
+ * because MSSQL doesn't support writing nulls to IDENTITY (AUTO_INCREMENT) columns
+ */
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ # No rows to insert, easy just return now
+ if ( !count( $a ) ) {
+ return true;
+ }
+ $table = $this->tableName( $table );
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+
+ # todo: need to record primary keys at table create time, and remove NULL assignments to them
+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $a[0] );
+# if (ereg('_id$',$keys[0])) {
+ foreach ($a as $i) {
+ if (is_null($i[$keys[0]])) unset($i[$keys[0]]); # remove primary-key column from multiple insert lists if empty value
+ }
+# }
+ $keys = array_keys( $a[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $a );
+# if (ereg('_id$',$keys[0]) && empty($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
+ if (is_null($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
+ $keys = array_keys( $a );
+ }
+
+ # handle IGNORE option
+ # example:
+ # MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
+ # MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
+ $ignore = in_array('IGNORE',$options);
+
+ # remove IGNORE from options list
+ if ($ignore) {
+ $oldoptions = $options;
+ $options = array();
+ foreach ($oldoptions as $o) if ($o != 'IGNORE') $options[] = $o;
+ }
+
+ $keylist = implode(',', $keys);
+ $sql = 'INSERT '.implode(' ', $options)." INTO $table (".implode(',', $keys).') VALUES ';
+ if ($multi) {
+ if ($ignore) {
+ # If multiple and ignore, then do each row as a separate conditional insert
+ foreach ($a as $row) {
+ $prival = $row[$keys[0]];
+ $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
+ if (!$this->query("$sql (".$this->makeListWithoutNulls($row).')', $fname)) return false;
+ }
+ return true;
+ } else {
+ $first = true;
+ foreach ($a as $row) {
+ if ($first) $first = false; else $sql .= ',';
+ $sql .= '('.$this->makeListWithoutNulls($row).')';
+ }
+ }
+ } else {
+ if ($ignore) {
+ $prival = $a[$keys[0]];
+ $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
+ }
+ $sql .= '('.$this->makeListWithoutNulls($a).')';
+ }
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * MSSQL doesn't allow implicit casting of NULL's into non-null values for NOT NULL columns
+ * for now I've just converted the NULL's in the lists for updates and inserts into empty strings
+ * which get implicitly casted to 0 for numeric columns
+ * NOTE: the set() method above converts NULL to empty string as well but not via this method
+ */
+ function makeListWithoutNulls($a, $mode = LIST_COMMA) {
+ return str_replace("NULL","''",$this->makeList($a,$mode));
+ }
+
+ /**
+ * UPDATE wrapper, takes a condition array and a SET array
+ *
+ * @param string $table The table to UPDATE
+ * @param array $values An array of values to SET
+ * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
+ * @param string $fname The Class::Function calling this function
+ * (for the log)
+ * @param array $options An array of UPDATE options, can be one or
+ * more of IGNORE, LOW_PRIORITY
+ * @return bool
+ */
+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+ $table = $this->tableName( $table );
+ $opts = $this->makeUpdateOptions( $options );
+ $sql = "UPDATE $opts $table SET " . $this->makeListWithoutNulls( $values, LIST_SET );
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Make UPDATE options for the Database::update function
+ *
+ * @private
+ * @param array $options The options passed to Database::update
+ * @return string
+ */
+ function makeUpdateOptions( $options ) {
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $opts = array();
+ if ( in_array( 'LOW_PRIORITY', $options ) )
+ $opts[] = $this->lowPriorityOption();
+ if ( in_array( 'IGNORE', $options ) )
+ $opts[] = 'IGNORE';
+ return implode(' ', $opts);
+ }
+
+ /**
+ * Change the current database
+ */
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return mssql_select_db( $db, $this->mConn );
+ }
+
+ /**
+ * MSSQL has a problem with the backtick quoting, so all this does is ensure the prefix is added exactly once
+ */
+ function tableName($name) {
+ return strpos($name, $this->mTablePrefix) === 0 ? $name : "{$this->mTablePrefix}$name";
+ }
+
+ /**
+ * MSSQL doubles quotes instead of escaping them
+ * @param string $s String to be slashed.
+ * @return string slashed string.
+ */
+ function strencode($s) {
+ return str_replace("'","''",$s);
+ }
+
+ /**
+ * USE INDEX clause
+ */
+ function useIndexClause( $index ) {
+ return "";
+ }
+
+ /**
+ * REPLACE query wrapper
+ * PostgreSQL simulates this with a DELETE followed by INSERT
+ * $row is the row to insert, an associative array
+ * $uniqueIndexes is an array of indexes. Each element may be either a
+ * field name or an array of field names
+ *
+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
+ * However if you do this, you run the risk of encountering errors which wouldn't have
+ * occurred in MySQL
+ *
+ * @todo migrate comment to phodocumentor format
+ */
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName( $table );
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * DELETE where the condition is a join
+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
+ *
+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+ * join condition matches, set $conds='*'
+ *
+ * DO NOT put the join condition in $conds
+ *
+ * @param string $delTable The table to delete from.
+ * @param string $joinTable The other table.
+ * @param string $delVar The variable to join on, in the first table.
+ * @param string $joinVar The variable to join on, in the second table.
+ * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ */
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+ if ( $conds != '*' ) {
+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Returns the size of a text field, or -1 for "unlimited"
+ */
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT TOP 1 * FROM $table;";
+ $res = $this->query( $sql, 'Database::textFieldSize' );
+ $row = $this->fetchObject( $res );
+ $this->freeResult( $res );
+
+ $m = array();
+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+ $size = $m[1];
+ } else {
+ $size = -1;
+ }
+ return $size;
+ }
+
+ /**
+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+ */
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ /**
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+ * $conds may be "*" to copy the whole table
+ * srcTable may be an array of tables.
+ */
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+ if ( is_array( $insertOptions ) ) {
+ $insertOptions = implode( ' ', $insertOptions );
+ }
+ if( !is_array( $selectOptions ) ) {
+ $selectOptions = array( $selectOptions );
+ }
+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+ if( is_array( $srcTable ) ) {
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+ } else {
+ $srcTable = $this->tableName( $srcTable );
+ }
+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex ";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= " $tailOpts";
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Construct a LIMIT query with optional offset
+ * This is used for query pages
+ * $sql string SQL query we will append the limit to
+ * $limit integer the SQL limit
+ * $offset integer the SQL offset (default false)
+ */
+ function limitResult($sql, $limit, $offset=false) {
+ if( !is_numeric($limit) ) {
+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+ }
+ if ($offset) {
+ throw new DBUnexpectedError( $this, 'Database::limitResult called with non-zero offset which is not supported yet' );
+ } else {
+ $sql = ereg_replace("^SELECT", "SELECT TOP $limit", $sql);
+ }
+ return $sql;
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ *
+ * @param string $cond SQL expression which will result in a boolean value
+ * @param string $trueVal SQL expression to return if true
+ * @param string $falseVal SQL expression to return if false
+ * @return string SQL fragment
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ /**
+ * Should determine if the last failure was due to a deadlock
+ * - don't know how to do this in MSSQL
+ */
+ function wasDeadlock() {
+ return false;
+ }
+
+ /**
+ * Begin a transaction, committing any previously open transaction
+ * @deprecated use begin()
+ */
+ function immediateBegin( $fname = 'Database::immediateBegin' ) {
+ $this->begin();
+ }
+
+ /**
+ * Commit transaction, if one is open
+ * @deprecated use commit()
+ */
+ function immediateCommit( $fname = 'Database::immediateCommit' ) {
+ $this->commit();
+ }
+
+ /**
+ * Return MW-style timestamp used for MySQL schema
+ */
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_MW,$ts);
+ }
+
+ /**
+ * Local database timestamp format or null
+ */
+ function timestampOrNull( $ts = null ) {
+ if( is_null( $ts ) ) {
+ return null;
+ } else {
+ return $this->timestamp( $ts );
+ }
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.microsoft.com/sql/default.mspx Microsoft SQL Server 2005 Home]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ $row = mssql_fetch_row(mssql_query('select @@VERSION'));
+ return ereg("^(.+[0-9]+\\.[0-9]+\\.[0-9]+) ",$row[0],$m) ? $m[1] : $row[0];
+ }
+
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ /**
+ * not done
+ */
+ public function setTimeout($timeout) { return; }
+
+ function ping() {
+ wfDebug("Function ping() not written for MSSQL yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ */
+ public function getLag() {
+ return 0;
+ }
+
+ /**
+ * Called by the installer script
+ * - this is the same way as DatabasePostgresql.php, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
+ */
+ public function setup_database() {
+ global $IP,$wgDBTableOptions;
+ $wgDBTableOptions = '';
+ $mysql_tmpl = "$IP/maintenance/tables.sql";
+ $mysql_iw = "$IP/maintenance/interwiki.sql";
+ $mssql_tmpl = "$IP/maintenance/mssql/tables.sql";
+
+ # Make an MSSQL template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
+ if (!file_exists($mssql_tmpl)) { # todo: make this conditional again
+ $sql = file_get_contents($mysql_tmpl);
+ $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
+ $sql = preg_replace('/^\s*(UNIQUE )?(INDEX|KEY|FULLTEXT).+?$/m', '', $sql); # These indexes should be created with a CREATE INDEX query
+ $sql = preg_replace('/(\sKEY) [^\(]+\(/is', '$1 (', $sql); # "KEY foo (foo)" should just be "KEY (foo)"
+ $sql = preg_replace('/(varchar\([0-9]+\))\s+binary/i', '$1', $sql); # "varchar(n) binary" cannot be followed by "binary"
+ $sql = preg_replace('/(var)?binary\(([0-9]+)\)/ie', '"varchar(".strlen(pow(2,$2)).")"', $sql); # use varchar(chars) not binary(bits)
+ $sql = preg_replace('/ (var)?binary/i', ' varchar', $sql); # use varchar not binary
+ $sql = preg_replace('/(varchar\([0-9]+\)(?! N))/', '$1 NULL', $sql); # MSSQL complains if NULL is put into a varchar
+ #$sql = preg_replace('/ binary/i',' varchar',$sql); # MSSQL binary's can't be assigned with strings, so use varchar's instead
+ #$sql = preg_replace('/(binary\([0-9]+\) (NOT NULL )?default) [\'"].*?[\'"]/i','$1 0',$sql); # binary default cannot be string
+ $sql = preg_replace('/[a-z]*(blob|text)([ ,])/i', 'text$2', $sql); # no BLOB types in MSSQL
+ $sql = preg_replace('/\).+?;/',');', $sql); # remove all table options
+ $sql = preg_replace('/ (un)?signed/i', '', $sql);
+ $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
+ $sql = str_replace(' bool ', ' bit ', $sql);
+ $sql = str_replace('auto_increment', 'IDENTITY(1,1)', $sql);
+ #$sql = preg_replace('/NOT NULL(?! IDENTITY)/', 'NULL', $sql); # Allow NULL's for non IDENTITY columns
+
+ # Tidy up and write file
+ $sql = preg_replace('/,\s*\)/s', "\n)", $sql); # Remove spurious commas left after INDEX removals
+ $sql = preg_replace('/^\s*^/m', '', $sql); # Remove empty lines
+ $sql = preg_replace('/;$/m', ";\n", $sql); # Separate each statement with an empty line
+ file_put_contents($mssql_tmpl, $sql);
+ }
+
+ # Parse the MSSQL template replacing inline variables such as /*$wgDBprefix*/
+ $err = $this->sourceFile($mssql_tmpl);
+ if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
+
+ # Use DatabasePostgres's code to populate interwiki from MySQL template
+ $f = fopen($mysql_iw,'r');
+ if ($f == false) dieout("
Could not find the interwiki.sql file");
+ $sql = "INSERT INTO {$this->mTablePrefix}interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+ while (!feof($f)) {
+ $line = fgets($f,1024);
+ $matches = array();
+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
+ $this->query("$sql $matches[1],$matches[2])");
+ }
+ }
+
+}
+
+/**
+ * @ingroup Database
+ */
+class MSSQLField extends MySQLField {
+
+ function __construct() {
+ }
+
+ static function fromText($db, $table, $field) {
+ $n = new MSSQLField;
+ $n->name = $field;
+ $n->tablename = $table;
+ return $n;
+ }
+
+} // end DatabaseMssql class
+
diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php
new file mode 100644
index 0000000000..8d2a6750dc
--- /dev/null
+++ b/includes/db/DatabaseOracle.php
@@ -0,0 +1,710 @@
+mData = $data;
+ }
+
+ function getData() {
+ return $this->mData;
+ }
+}
+
+/**
+ * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
+ * other things. We use a wrapper class to handle that and other
+ * Oracle-specific bits, like converting column names back to lowercase.
+ * @ingroup Database
+ */
+class ORAResult {
+ private $rows;
+ private $cursor;
+ private $stmt;
+ private $nrows;
+ private $db;
+
+ function __construct(&$db, $stmt) {
+ $this->db =& $db;
+ if (($this->nrows = oci_fetch_all($stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM)) === false) {
+ $e = oci_error($stmt);
+ $db->reportQueryError($e['message'], $e['code'], '', __FUNCTION__);
+ return;
+ }
+
+ $this->cursor = 0;
+ $this->stmt = $stmt;
+ }
+
+ function free() {
+ oci_free_statement($this->stmt);
+ }
+
+ function seek($row) {
+ $this->cursor = min($row, $this->nrows);
+ }
+
+ function numRows() {
+ return $this->nrows;
+ }
+
+ function numFields() {
+ return oci_num_fields($this->stmt);
+ }
+
+ function fetchObject() {
+ if ($this->cursor >= $this->nrows)
+ return false;
+
+ $row = $this->rows[$this->cursor++];
+ $ret = new stdClass();
+ foreach ($row as $k => $v) {
+ $lc = strtolower(oci_field_name($this->stmt, $k + 1));
+ $ret->$lc = $v;
+ }
+
+ return $ret;
+ }
+
+ function fetchAssoc() {
+ if ($this->cursor >= $this->nrows)
+ return false;
+
+ $row = $this->rows[$this->cursor++];
+ $ret = array();
+ foreach ($row as $k => $v) {
+ $lc = strtolower(oci_field_name($this->stmt, $k + 1));
+ $ret[$lc] = $v;
+ $ret[$k] = $v;
+ }
+ return $ret;
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DatabaseOracle extends Database {
+ var $mInsertId = NULL;
+ var $mLastResult = NULL;
+ var $numeric_version = NULL;
+ var $lastResult = null;
+ var $cursor = 0;
+ var $mAffectedRows;
+
+ function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0 )
+ {
+
+ global $wgOut;
+ # Can't get a reference if it hasn't been set yet
+ if ( !isset( $wgOut ) ) {
+ $wgOut = NULL;
+ }
+ $this->mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+ $this->open( $server, $user, $password, $dbName);
+
+ }
+
+ function cascadingDeletes() {
+ return true;
+ }
+ function cleanupTriggers() {
+ return true;
+ }
+ function strictIPs() {
+ return true;
+ }
+ function realTimestamps() {
+ return true;
+ }
+ function implicitGroupby() {
+ return false;
+ }
+ function implicitOrderby() {
+ return false;
+ }
+ function searchableIPs() {
+ return true;
+ }
+
+ static function newFromParams( $server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0)
+ {
+ return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags );
+ }
+
+ /**
+ * Usually aborts on failure
+ * If the failFunction is set to a non-zero integer, returns success
+ */
+ function open( $server, $user, $password, $dbName ) {
+ if ( !function_exists( 'oci_connect' ) ) {
+ throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
+ }
+
+ # Needed for proper UTF-8 functionality
+ putenv("NLS_LANG=AMERICAN_AMERICA.AL32UTF8");
+
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ if (!strlen($user)) { ## e.g. the class is being loaded
+ return;
+ }
+
+ error_reporting( E_ALL );
+ $this->mConn = oci_connect($user, $password, $dbName);
+
+ if ($this->mConn == false) {
+ wfDebug("DB connection error\n");
+ wfDebug("Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n");
+ wfDebug($this->lastError()."\n");
+ return false;
+ }
+
+ $this->mOpened = true;
+ return $this->mConn;
+ }
+
+ /**
+ * Closes a database connection, if it is open
+ * Returns success, true if already closed
+ */
+ function close() {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ return oci_close( $this->mConn );
+ } else {
+ return true;
+ }
+ }
+
+ function execFlags() {
+ return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS;
+ }
+
+ function doQuery($sql) {
+ wfDebug("SQL: [$sql]\n");
+ if (!mb_check_encoding($sql)) {
+ throw new MWException("SQL encoding is invalid");
+ }
+
+ if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) {
+ $e = oci_error($this->mConn);
+ $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+ }
+
+ if (oci_execute($stmt, $this->execFlags()) == false) {
+ $e = oci_error($stmt);
+ $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+ }
+ if (oci_statement_type($stmt) == "SELECT")
+ return new ORAResult($this, $stmt);
+ else {
+ $this->mAffectedRows = oci_num_rows($stmt);
+ return true;
+ }
+ }
+
+ function queryIgnore($sql, $fname = '') {
+ return $this->query($sql, $fname, true);
+ }
+
+ function freeResult($res) {
+ $res->free();
+ }
+
+ function fetchObject($res) {
+ return $res->fetchObject();
+ }
+
+ function fetchRow($res) {
+ return $res->fetchAssoc();
+ }
+
+ function numRows($res) {
+ return $res->numRows();
+ }
+
+ function numFields($res) {
+ return $res->numFields();
+ }
+
+ function fieldName($stmt, $n) {
+ return pg_field_name($stmt, $n);
+ }
+
+ /**
+ * This must be called after nextSequenceVal
+ */
+ function insertId() {
+ return $this->mInsertId;
+ }
+
+ function dataSeek($res, $row) {
+ $res->seek($row);
+ }
+
+ function lastError() {
+ if ($this->mConn === false)
+ $e = oci_error();
+ else
+ $e = oci_error($this->mConn);
+ return $e['message'];
+ }
+
+ function lastErrno() {
+ if ($this->mConn === false)
+ $e = oci_error();
+ else
+ $e = oci_error($this->mConn);
+ return $e['code'];
+ }
+
+ function affectedRows() {
+ return $this->mAffectedRows;
+ }
+
+ /**
+ * Returns information about an index
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+ return false;
+ }
+
+ function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+ return false;
+ }
+
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ if (!is_array($options))
+ $options = array($options);
+
+ #if (in_array('IGNORE', $options))
+ # $oldIgnore = $this->ignoreErrors(true);
+
+ # IGNORE is performed using single-row inserts, ignoring errors in each
+ # FIXME: need some way to distiguish between key collision and other types of error
+ //$oldIgnore = $this->ignoreErrors(true);
+ if (!is_array(reset($a))) {
+ $a = array($a);
+ }
+ foreach ($a as $row) {
+ $this->insertOneRow($table, $row, $fname);
+ }
+ //$this->ignoreErrors($oldIgnore);
+ $retVal = true;
+
+ //if (in_array('IGNORE', $options))
+ // $this->ignoreErrors($oldIgnore);
+
+ return $retVal;
+ }
+
+ function insertOneRow($table, $row, $fname) {
+ // "INSERT INTO tables (a, b, c)"
+ $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')';
+ $sql .= " VALUES (";
+
+ // for each value, append ":key"
+ $first = true;
+ $returning = '';
+ foreach ($row as $col => $val) {
+ if (is_object($val)) {
+ $what = "EMPTY_BLOB()";
+ assert($returning === '');
+ $returning = " RETURNING $col INTO :bval";
+ $blobcol = $col;
+ } else
+ $what = ":$col";
+
+ if ($first)
+ $sql .= "$what";
+ else
+ $sql.= ", $what";
+ $first = false;
+ }
+ $sql .= ") $returning";
+
+ $stmt = oci_parse($this->mConn, $sql);
+ foreach ($row as $col => $val) {
+ if (!is_object($val)) {
+ if (oci_bind_by_name($stmt, ":$col", $row[$col]) === false)
+ $this->reportQueryError($this->lastErrno(), $this->lastError(), $sql, __METHOD__);
+ }
+ }
+
+ if (($bval = oci_new_descriptor($this->mConn, OCI_D_LOB)) === false) {
+ $e = oci_error($stmt);
+ throw new DBUnexpectedError($this, "Cannot create LOB descriptor: " . $e['message']);
+ }
+
+ if (strlen($returning))
+ oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB);
+
+ if (oci_execute($stmt, OCI_DEFAULT) === false) {
+ $e = oci_error($stmt);
+ $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__);
+ }
+ if (strlen($returning)) {
+ $bval->save($row[$blobcol]->getData());
+ $bval->free();
+ }
+ if (!$this->mTrxLevel)
+ oci_commit($this->mConn);
+
+ oci_free_statement($stmt);
+ }
+
+ function tableName( $name ) {
+ # Replace reserved words with better ones
+ switch( $name ) {
+ case 'user':
+ return 'mwuser';
+ case 'text':
+ return 'pagecontent';
+ default:
+ return $name;
+ }
+ }
+
+ /**
+ * Return the next in a sequence, save the value for retrieval via insertId()
+ */
+ function nextSequenceValue($seqName) {
+ $res = $this->query("SELECT $seqName.nextval FROM dual");
+ $row = $this->fetchRow($res);
+ $this->mInsertId = $row[0];
+ $this->freeResult($res);
+ return $this->mInsertId;
+ }
+
+ /**
+ * Oracle does not have a "USE INDEX" clause, so return an empty string
+ */
+ function useIndexClause($index) {
+ return '';
+ }
+
+ # REPLACE query wrapper
+ # Oracle simulates this with a DELETE followed by INSERT
+ # $row is the row to insert, an associative array
+ # $uniqueIndexes is an array of indexes. Each element may be either a
+ # field name or an array of field names
+ #
+ # It may be more efficient to leave off unique indexes which are unlikely to collide.
+ # However if you do this, you run the risk of encountering errors which wouldn't have
+ # occurred in MySQL
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName($table);
+
+ if (count($rows)==0) {
+ return;
+ }
+
+ # Single row case
+ if (!is_array(reset($rows))) {
+ $rows = array($rows);
+ }
+
+ foreach( $rows as $row ) {
+ # Delete rows which collide
+ if ( $uniqueIndexes ) {
+ $sql = "DELETE FROM $table WHERE ";
+ $first = true;
+ foreach ( $uniqueIndexes as $index ) {
+ if ( $first ) {
+ $first = false;
+ $sql .= "(";
+ } else {
+ $sql .= ') OR (';
+ }
+ if ( is_array( $index ) ) {
+ $first2 = true;
+ foreach ( $index as $col ) {
+ if ( $first2 ) {
+ $first2 = false;
+ } else {
+ $sql .= ' AND ';
+ }
+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
+ }
+ } else {
+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
+ }
+ }
+ $sql .= ')';
+ $this->query( $sql, $fname );
+ }
+
+ # Now insert the row
+ $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
+ $this->makeList( $row, LIST_COMMA ) . ')';
+ $this->query($sql, $fname);
+ }
+ }
+
+ # DELETE where the condition is a join
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+ if ( $conds != '*' ) {
+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= ')';
+
+ $this->query( $sql, $fname );
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT t.typname as ftype,a.atttypmod as size
+ FROM pg_class c, pg_attribute a, pg_type t
+ WHERE relname='$table' AND a.attrelid=c.oid AND
+ a.atttypid=t.oid and a.attname='$field'";
+ $res =$this->query($sql);
+ $row=$this->fetchObject($res);
+ if ($row->ftype=="varchar") {
+ $size=$row->size-4;
+ } else {
+ $size=$row->size;
+ }
+ $this->freeResult( $res );
+ return $size;
+ }
+
+ function lowPriorityOption() {
+ return '';
+ }
+
+ function limitResult($sql, $limit, $offset) {
+ if ($offset === false)
+ $offset = 0;
+ return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset";
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * Uses CASE on Oracle
+ *
+ * @param string $cond SQL expression which will result in a boolean value
+ * @param string $trueVal SQL expression to return if true
+ * @param string $falseVal SQL expression to return if false
+ * @return string SQL fragment
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ function wasDeadlock() {
+ return $this->lastErrno() == 'OCI-00060';
+ }
+
+ function timestamp($ts = 0) {
+ return wfTimestamp(TS_ORACLE, $ts);
+ }
+
+ /**
+ * Return aggregated value function call
+ */
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuedata;
+ }
+
+ function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) {
+ # Ignore errors during error handling to avoid infinite
+ # recursion
+ $ignore = $this->ignoreErrors(true);
+ ++$this->mErrorCount;
+
+ if ($ignore || $tempIgnore) {
+echo "error ignored! query = [$sql]\n";
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ }
+ else {
+echo "error!\n";
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+ throw new DBUnexpectedError($this, $message);
+ }
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.oracle.com/ Oracle]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ return oci_server_version($this->mConn);
+ }
+
+ /**
+ * Query whether a given table exists (in the given schema, or the default mw one if not given)
+ */
+ function tableExists($table) {
+ $etable= $this->addQuotes($table);
+ $SQL = "SELECT 1 FROM user_tables WHERE table_name='$etable'";
+ $res = $this->query($SQL);
+ $count = $res ? oci_num_rows($res) : 0;
+ if ($res)
+ $this->freeResult($res);
+ return $count;
+ }
+
+ /**
+ * Query whether a given column exists in the mediawiki schema
+ */
+ function fieldExists( $table, $field ) {
+ return true; // XXX
+ }
+
+ function fieldInfo( $table, $field ) {
+ return false; // XXX
+ }
+
+ function begin( $fname = '' ) {
+ $this->mTrxLevel = 1;
+ }
+ function immediateCommit( $fname = '' ) {
+ return true;
+ }
+ function commit( $fname = '' ) {
+ oci_commit($this->mConn);
+ $this->mTrxLevel = 0;
+ }
+
+ /* Not even sure why this is used in the main codebase... */
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ function strencode($s) {
+ return str_replace("'", "''", $s);
+ }
+
+ function encodeBlob($b) {
+ return new ORABlob($b);
+ }
+ function decodeBlob($b) {
+ return $b; //return $b->load();
+ }
+
+ function addQuotes( $s ) {
+ global $wgLang;
+ $s = $wgLang->checkTitleEncoding($s);
+ return "'" . $this->strencode($s) . "'";
+ }
+
+ function quote_ident( $s ) {
+ return $s;
+ }
+
+ /* For now, does nothing */
+ function selectDB( $db ) {
+ return true;
+ }
+
+ /**
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ }
+
+ #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
+ #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ public function setTimeout( $timeout ) {
+ // @todo fixme no-op
+ }
+
+ function ping() {
+ wfDebug( "Function ping() not written for DatabaseOracle.php yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ *
+ * @return int
+ */
+ public function getLag() {
+ # Not implemented for Oracle
+ return 0;
+ }
+
+ function setFakeSlaveLag() {}
+ function setFakeMaster() {}
+
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ function getServer() {
+ return $this->mServer;
+ }
+
+} // end DatabaseOracle class
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
new file mode 100644
index 0000000000..8f8488b9ae
--- /dev/null
+++ b/includes/db/DatabasePostgres.php
@@ -0,0 +1,1330 @@
+query(sprintf($q,
+ $db->addQuotes($wgDBmwschema),
+ $db->addQuotes($table),
+ $db->addQuotes($field)));
+ $row = $db->fetchObject($res);
+ if (!$row)
+ return null;
+ $n = new PostgresField;
+ $n->type = $row->typname;
+ $n->nullable = ($row->attnotnull == 'f');
+ $n->name = $field;
+ $n->tablename = $table;
+ $n->max_length = $row->attlen;
+ return $n;
+ }
+
+ function name() {
+ return $this->name;
+ }
+
+ function tableName() {
+ return $this->tablename;
+ }
+
+ function type() {
+ return $this->type;
+ }
+
+ function nullable() {
+ return $this->nullable;
+ }
+
+ function maxLength() {
+ return $this->max_length;
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DatabasePostgres extends Database {
+ var $mInsertId = NULL;
+ var $mLastResult = NULL;
+ var $numeric_version = NULL;
+
+ function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0 )
+ {
+
+ global $wgOut;
+ # Can't get a reference if it hasn't been set yet
+ if ( !isset( $wgOut ) ) {
+ $wgOut = NULL;
+ }
+ $this->mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+ $this->open( $server, $user, $password, $dbName);
+
+ }
+
+ function cascadingDeletes() {
+ return true;
+ }
+ function cleanupTriggers() {
+ return true;
+ }
+ function strictIPs() {
+ return true;
+ }
+ function realTimestamps() {
+ return true;
+ }
+ function implicitGroupby() {
+ return false;
+ }
+ function implicitOrderby() {
+ return false;
+ }
+ function searchableIPs() {
+ return true;
+ }
+ function functionalIndexes() {
+ return true;
+ }
+
+ function hasConstraint( $name ) {
+ global $wgDBmwschema;
+ $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . pg_escape_string( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'";
+ return $this->numRows($res = $this->doQuery($SQL));
+ }
+
+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
+ {
+ return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags );
+ }
+
+ /**
+ * Usually aborts on failure
+ * If the failFunction is set to a non-zero integer, returns success
+ */
+ function open( $server, $user, $password, $dbName ) {
+ # Test for Postgres support, to avoid suppressed fatal error
+ if ( !function_exists( 'pg_connect' ) ) {
+ throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
+ }
+
+ global $wgDBport;
+
+ if (!strlen($user)) { ## e.g. the class is being loaded
+ return;
+ }
+
+ $this->close();
+ $this->mServer = $server;
+ $this->mPort = $port = $wgDBport;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ $hstring="";
+ if ($server!=false && $server!="") {
+ $hstring="host=$server ";
+ }
+ if ($port!=false && $port!="") {
+ $hstring .= "port=$port ";
+ }
+
+ error_reporting( E_ALL );
+ @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
+
+ if ( $this->mConn == false ) {
+ wfDebug( "DB connection error\n" );
+ wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
+ wfDebug( $this->lastError()."\n" );
+ return false;
+ }
+
+ $this->mOpened = true;
+
+ global $wgCommandLineMode;
+ ## If called from the command-line (e.g. importDump), only show errors
+ if ($wgCommandLineMode) {
+ $this->doQuery("SET client_min_messages = 'ERROR'");
+ }
+
+ global $wgDBmwschema, $wgDBts2schema;
+ if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
+ && $wgDBmwschema !== 'mediawiki'
+ && preg_match( '/^\w+$/', $wgDBmwschema )
+ && preg_match( '/^\w+$/', $wgDBts2schema )
+ ) {
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ $safeschema2 = $this->quote_ident($wgDBts2schema);
+ $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public");
+ }
+
+ return $this->mConn;
+ }
+
+
+ function initial_setup($password, $dbName) {
+ // If this is the initial connection, setup the schema stuff and possibly create the user
+ global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema;
+
+ print "
Checking the version of Postgres...";
+ $version = $this->getServerVersion();
+ $PGMINVER = '8.1';
+ if ($this->numeric_version < $PGMINVER) {
+ print "FAILED. Required version is $PGMINVER. You have $this->numeric_version ($version)
\n";
+ dieout("");
+ }
+ print "version $this->numeric_version is OK.\n";
+
+ $safeuser = $this->quote_ident($wgDBuser);
+ // Are we connecting as a superuser for the first time?
+ if ($wgDBsuperuser) {
+ // Are we really a superuser? Check out our rights
+ $SQL = "SELECT
+ CASE WHEN usesuper IS TRUE THEN
+ CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
+ ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
+ END AS rights
+ FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser);
+ $rows = $this->numRows($res = $this->doQuery($SQL));
+ if (!$rows) {
+ print "
ERROR: Could not read permissions for user \"$wgDBsuperuser\"
Checking that tsearch2 is installed in the database \"$wgDBname\"...";
+ if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) {
+ print "FAILED. tsearch2 must be installed in the database \"$wgDBname\".";
+ print "Please see this article";
+ print " for instructions or ask on #postgresql on irc.freenode.net
\n";
+ dieout("");
+ }
+ print "OK\n";
+ print "
Ensuring that user \"$wgDBuser\" has select rights on the tsearch2 tables...";
+ foreach (array('cfg','cfgmap','dict','parser') as $table) {
+ $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser";
+ $this->doQuery($SQL);
+ }
+ print "OK
\n";
+ }
+
+ // Setup the schema for this user if needed
+ $result = $this->schemaExists($wgDBmwschema);
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ if (!$result) {
+ print "
Schema already exists, explicitly granting rights...\n";
+ $safeschema2 = $this->addQuotes($wgDBmwschema);
+ $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
+ "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n".
+ "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n".
+ "AND p.relkind IN ('r','S','v')\n";
+ $SQL .= "UNION\n";
+ $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
+ "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n".
+ "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n".
+ "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "FAILED. Could not set rights for the user.
\n";
+ dieout("");
+ }
+ $this->doQuery("SET search_path = $safeschema");
+ $rows = $this->numRows($res);
+ while ($rows) {
+ $rows--;
+ $this->doQuery(pg_fetch_result($res, $rows, 0));
+ }
+ print "OK";
+ }
+
+ // Install plpgsql if needed
+ $this->setup_plpgsql();
+
+ $wgDBsuperuser = '';
+ return true; // Reconnect as regular user
+
+ } // end superuser
+
+ if (!defined('POSTGRES_SEARCHPATH')) {
+
+ if ($this->numeric_version < 8.3) {
+ // Do we have the basic tsearch2 table?
+ print "
Checking for tsearch2 in the schema \"$wgDBts2schema\"...";
+ if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
+ print "FAILED. Make sure tsearch2 is installed. See this article";
+ print " for instructions.
\n";
+ dieout("");
+ }
+ print "OK\n";
+
+ // Does this user have the rights to the tsearch2 tables?
+ $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
+ print "
Checking tsearch2 permissions...";
+ // Let's check all four, just to be safe
+ error_reporting( 0 );
+ $ts2tables = array('cfg','cfgmap','dict','parser');
+ $safetsschema = $this->quote_ident($wgDBts2schema);
+ foreach ( $ts2tables AS $tname ) {
+ $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "FAILED to access pg_ts_$tname. Make sure that the user ".
+ "\"$wgDBuser\" has SELECT access to all four tsearch2 tables
\n";
+ dieout("");
+ }
+ }
+ $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = '$ctype'";
+ $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END";
+ $res = $this->doQuery($SQL);
+ error_reporting( E_ALL );
+ if (!$res) {
+ print "FAILED. Could not determine the tsearch2 locale information\n";
+ dieout("");
+ }
+ print "OK";
+
+ // Will the current locale work? Can we force it to?
+ print "
Verifying tsearch2 locale with $ctype...";
+ $rows = $this->numRows($res);
+ $resetlocale = 0;
+ if (!$rows) {
+ print "not found
\n";
+ print "
Attempting to set default tsearch2 locale to \"$ctype\"...";
+ $resetlocale = 1;
+ }
+ else {
+ $tsname = pg_fetch_result($res, 0, 0);
+ if ($tsname != 'default') {
+ print "not set to default ($tsname)";
+ print "
Attempting to change tsearch2 default locale to \"$ctype\"...";
+ $resetlocale = 1;
+ }
+ }
+ if ($resetlocale) {
+ $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "FAILED. ";
+ print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"
\n";
+ dieout("");
+ }
+ print "OK";
+ }
+
+ // Final test: try out a simple tsearch2 query
+ $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "FAILED. Specifically, \"$SQL\" did not work.";
+ dieout("");
+ }
+ print "OK";
+ }
+
+ // Install plpgsql if needed
+ $this->setup_plpgsql();
+
+ // Does the schema already exist? Who owns it?
+ $result = $this->schemaExists($wgDBmwschema);
+ if (!$result) {
+ print "
Creating schema $wgDBmwschema ...";
+ error_reporting( 0 );
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ $result = $this->doQuery("CREATE SCHEMA $safeschema");
+ error_reporting( E_ALL );
+ if (!$result) {
+ print "FAILED. The user \"$wgDBuser\" must be able to access the schema. ".
+ "You can try making them the owner of the database, or try creating the schema with a ".
+ "different user, and then grant access to the \"$wgDBuser\" user.
Schema \"$wgDBmwschema\" exists but is not owned by \"$wgDBuser\". Not ideal.
\n";
+ }
+ else {
+ print "
Schema \"$wgDBmwschema\" exists and is owned by \"$wgDBuser\". Excellent.
\n";
+ }
+
+ // Always return GMT time to accomodate the existing integer-based timestamp assumption
+ print "
Setting the timezone to GMT for user \"$wgDBuser\" ...";
+ $SQL = "ALTER USER $safeuser SET timezone = 'GMT'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "FAILED.
\n";
+ dieout("");
+ }
+ print "OK\n";
+ // Set for the rest of this session
+ $SQL = "SET timezone = 'GMT'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "
Failed to set timezone
\n";
+ dieout("");
+ }
+
+ print "
Setting the datestyle to ISO, YMD for user \"$wgDBuser\" ...";
+ $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "FAILED.
\n";
+ dieout("");
+ }
+ print "OK\n";
+ // Set for the rest of this session
+ $SQL = "SET datestyle = 'ISO, YMD'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "
Failed to set datestyle
\n";
+ dieout("");
+ }
+
+ // Fix up the search paths if needed
+ print "
Setting the search path for user \"$wgDBuser\" ...";
+ $path = $this->quote_ident($wgDBmwschema);
+ if ($wgDBts2schema !== $wgDBmwschema)
+ $path .= ", ". $this->quote_ident($wgDBts2schema);
+ if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public')
+ $path .= ", public";
+ $SQL = "ALTER USER $safeuser SET search_path = $path";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "FAILED.
\n";
+ dieout("");
+ }
+ print "OK\n";
+ // Set for the rest of this session
+ $SQL = "SET search_path = $path";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "
Checking for Pl/Pgsql ...";
+ $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'";
+ $rows = $this->numRows($this->doQuery($SQL));
+ if ($rows < 1) {
+ // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
+ print "not installed. Attempting to install Pl/Pgsql ...";
+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
+ "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
+ $rows = $this->numRows($this->doQuery($SQL));
+ if ($rows >= 1) {
+ $olde = error_reporting(0);
+ error_reporting($olde - E_WARNING);
+ $result = $this->doQuery("CREATE LANGUAGE plpgsql");
+ error_reporting($olde);
+ if (!$result) {
+ print "FAILED. You need to install the language plpgsql in the database $wgDBname
";
+ dieout("");
+ }
+ }
+ else {
+ print "FAILED. You need to install the language plpgsql in the database $wgDBname";
+ dieout("");
+ }
+ }
+ print "OK\n";
+ }
+
+
+ /**
+ * Closes a database connection, if it is open
+ * Returns success, true if already closed
+ */
+ function close() {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ return pg_close( $this->mConn );
+ } else {
+ return true;
+ }
+ }
+
+ function doQuery( $sql ) {
+ if (function_exists('mb_convert_encoding')) {
+ return $this->mLastResult=pg_query( $this->mConn , mb_convert_encoding($sql,'UTF-8') );
+ }
+ return $this->mLastResult=pg_query( $this->mConn , $sql);
+ }
+
+ function queryIgnore( $sql, $fname = '' ) {
+ return $this->query( $sql, $fname, true );
+ }
+
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@pg_free_result( $res ) ) {
+ throw new DBUnexpectedError($this, "Unable to free Postgres result\n" );
+ }
+ }
+
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$row = pg_fetch_object( $res );
+ # FIXME: HACK HACK HACK HACK debug
+
+ # TODO:
+ # hashar : not sure if the following test really trigger if the object
+ # fetching failed.
+ if( pg_last_error($this->mConn) ) {
+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+ }
+ return $row;
+ }
+
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$row = pg_fetch_array( $res );
+ if( pg_last_error($this->mConn) ) {
+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+ }
+ return $row;
+ }
+
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$n = pg_num_rows( $res );
+ if( pg_last_error($this->mConn) ) {
+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+ }
+ return $n;
+ }
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_num_fields( $res );
+ }
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_field_name( $res, $n );
+ }
+
+ /**
+ * This must be called after nextSequenceVal
+ */
+ function insertId() {
+ return $this->mInsertId;
+ }
+
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_result_seek( $res, $row );
+ }
+
+ function lastError() {
+ if ( $this->mConn ) {
+ return pg_last_error();
+ }
+ else {
+ return "No database connection";
+ }
+ }
+ function lastErrno() {
+ return pg_last_error() ? 1 : 0;
+ }
+
+ function affectedRows() {
+ if( !isset( $this->mLastResult ) or ! $this->mLastResult )
+ return 0;
+
+ return pg_affected_rows( $this->mLastResult );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * This is not necessarily an accurate estimate, so use sparingly
+ * Returns -1 if count cannot be found
+ * Takes same arguments as Database::select()
+ */
+
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $options['EXPLAIN'] = true;
+ $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $rows = -1;
+ if ( $res ) {
+ $row = $this->fetchRow( $res );
+ $count = array();
+ if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
+ $rows = $count[1];
+ }
+ $this->freeResult($res);
+ }
+ return $rows;
+ }
+
+
+ /**
+ * Returns information about an index
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->indexname == $index ) {
+ return $row;
+ }
+ }
+ return false;
+ }
+
+ function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
+ " AND indexdef LIKE 'CREATE UNIQUE%({$index})'";
+ $res = $this->query( $sql, $fname );
+ if ( !$res )
+ return NULL;
+ while ($row = $this->fetchObject( $res ))
+ return true;
+ return false;
+
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $args may be a single associative array, or an array of these with numeric keys,
+ * for multi-row insert (Postgres version 8.2 and above only).
+ *
+ * @param array $table String: Name of the table to insert to.
+ * @param array $args Array: Items to insert into the table.
+ * @param array $fname String: Name of the function, for profiling
+ * @param mixed $options String or Array. Valid options: IGNORE
+ *
+ * @return bool Success of insert operation. IGNORE always returns true.
+ */
+ function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
+ global $wgDBversion;
+
+ if ( !count( $args ) ) {
+ return true;
+ }
+
+ $table = $this->tableName( $table );
+ if (! isset( $wgDBversion ) ) {
+ $this->getServerVersion();
+ $wgDBversion = $this->numeric_version;
+ }
+
+ if ( !is_array( $options ) )
+ $options = array( $options );
+
+ if ( isset( $args[0] ) && is_array( $args[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $args[0] );
+ }
+ else {
+ $multi = false;
+ $keys = array_keys( $args );
+ }
+
+ $ignore = in_array( 'IGNORE', $options ) ? 1 : 0;
+ if ( $ignore )
+ $olde = error_reporting( 0 );
+
+ $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+ if ( $multi ) {
+ if ( $wgDBversion >= 8.2 ) {
+ $first = true;
+ foreach ( $args as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ $res = (bool)$this->query( $sql, $fname, $ignore );
+ }
+ else {
+ $res = true;
+ $origsql = $sql;
+ foreach ( $args as $row ) {
+ $tempsql = $origsql;
+ $tempsql .= '(' . $this->makeList( $row ) . ')';
+ $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
+ if (! $tempres)
+ $res = false;
+ }
+ }
+ }
+ else {
+ $sql .= '(' . $this->makeList( $args ) . ')';
+ $res = (bool)$this->query( $sql, $fname, $ignore );
+ }
+
+ if ( $ignore ) {
+ $olde = error_reporting( $olde );
+ return true;
+ }
+
+ return $res;
+
+ }
+
+ function tableName( $name ) {
+ # Replace reserved words with better ones
+ switch( $name ) {
+ case 'user':
+ return 'mwuser';
+ case 'text':
+ return 'pagecontent';
+ default:
+ return $name;
+ }
+ }
+
+ /**
+ * Return the next in a sequence, save the value for retrieval via insertId()
+ */
+ function nextSequenceValue( $seqName ) {
+ $safeseq = preg_replace( "/'/", "''", $seqName );
+ $res = $this->query( "SELECT nextval('$safeseq')" );
+ $row = $this->fetchRow( $res );
+ $this->mInsertId = $row[0];
+ $this->freeResult( $res );
+ return $this->mInsertId;
+ }
+
+ /**
+ * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
+ */
+ function currentSequenceValue( $seqName ) {
+ $safeseq = preg_replace( "/'/", "''", $seqName );
+ $res = $this->query( "SELECT currval('$safeseq')" );
+ $row = $this->fetchRow( $res );
+ $currval = $row[0];
+ $this->freeResult( $res );
+ return $currval;
+ }
+
+ /**
+ * Postgres does not have a "USE INDEX" clause, so return an empty string
+ */
+ function useIndexClause( $index ) {
+ return '';
+ }
+
+ # REPLACE query wrapper
+ # Postgres simulates this with a DELETE followed by INSERT
+ # $row is the row to insert, an associative array
+ # $uniqueIndexes is an array of indexes. Each element may be either a
+ # field name or an array of field names
+ #
+ # It may be more efficient to leave off unique indexes which are unlikely to collide.
+ # However if you do this, you run the risk of encountering errors which wouldn't have
+ # occurred in MySQL
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName( $table );
+
+ if (count($rows)==0) {
+ return;
+ }
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ foreach( $rows as $row ) {
+ # Delete rows which collide
+ if ( $uniqueIndexes ) {
+ $sql = "DELETE FROM $table WHERE ";
+ $first = true;
+ foreach ( $uniqueIndexes as $index ) {
+ if ( $first ) {
+ $first = false;
+ $sql .= "(";
+ } else {
+ $sql .= ') OR (';
+ }
+ if ( is_array( $index ) ) {
+ $first2 = true;
+ foreach ( $index as $col ) {
+ if ( $first2 ) {
+ $first2 = false;
+ } else {
+ $sql .= ' AND ';
+ }
+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
+ }
+ } else {
+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
+ }
+ }
+ $sql .= ')';
+ $this->query( $sql, $fname );
+ }
+
+ # Now insert the row
+ $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
+ $this->makeList( $row, LIST_COMMA ) . ')';
+ $this->query( $sql, $fname );
+ }
+ }
+
+ # DELETE where the condition is a join
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+ if ( $conds != '*' ) {
+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= ')';
+
+ $this->query( $sql, $fname );
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT t.typname as ftype,a.atttypmod as size
+ FROM pg_class c, pg_attribute a, pg_type t
+ WHERE relname='$table' AND a.attrelid=c.oid AND
+ a.atttypid=t.oid and a.attname='$field'";
+ $res =$this->query($sql);
+ $row=$this->fetchObject($res);
+ if ($row->ftype=="varchar") {
+ $size=$row->size-4;
+ } else {
+ $size=$row->size;
+ }
+ $this->freeResult( $res );
+ return $size;
+ }
+
+ function lowPriorityOption() {
+ return '';
+ }
+
+ function limitResult($sql, $limit, $offset=false) {
+ return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * Uses CASE on Postgres
+ *
+ * @param string $cond SQL expression which will result in a boolean value
+ * @param string $trueVal SQL expression to return if true
+ * @param string $falseVal SQL expression to return if false
+ * @return string SQL fragment
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ function wasDeadlock() {
+ return $this->lastErrno() == '40P01';
+ }
+
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_POSTGRES,$ts);
+ }
+
+ /**
+ * Return aggregated value function call
+ */
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuedata;
+ }
+
+
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ // Ignore errors during error handling to avoid infinite recursion
+ $ignore = $this->ignoreErrors( true );
+ $this->mErrorCount++;
+
+ if ($ignore || $tempIgnore) {
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ }
+ else {
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+ throw new DBUnexpectedError($this, $message);
+ }
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.postgresql.org/ PostgreSQL]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0);
+ $thisver = array();
+ if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) {
+ die("Could not determine the numeric version from $version!");
+ }
+ $this->numeric_version = $thisver[1];
+ return $version;
+ }
+
+
+ /**
+ * Query whether a given relation exists (in the given schema, or the
+ * default mw one if not given)
+ */
+ function relationExists( $table, $types, $schema = false ) {
+ global $wgDBmwschema;
+ if (!is_array($types))
+ $types = array($types);
+ if (! $schema )
+ $schema = $wgDBmwschema;
+ $etable = $this->addQuotes($table);
+ $eschema = $this->addQuotes($schema);
+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
+ . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
+ . "AND c.relkind IN ('" . implode("','", $types) . "')";
+ $res = $this->query( $SQL );
+ $count = $res ? $res->numRows() : 0;
+ if ($res)
+ $this->freeResult( $res );
+ return $count ? true : false;
+ }
+
+ /*
+ * For backward compatibility, this function checks both tables and
+ * views.
+ */
+ function tableExists ($table, $schema = false) {
+ return $this->relationExists($table, array('r', 'v'), $schema);
+ }
+
+ function sequenceExists ($sequence, $schema = false) {
+ return $this->relationExists($sequence, 'S', $schema);
+ }
+
+ function triggerExists($table, $trigger) {
+ global $wgDBmwschema;
+
+ $q = <<query(sprintf($q,
+ $this->addQuotes($wgDBmwschema),
+ $this->addQuotes($table),
+ $this->addQuotes($trigger)));
+ if (!$res)
+ return NULL;
+ $rows = $res->numRows();
+ $this->freeResult($res);
+ return $rows;
+ }
+
+ function ruleExists($table, $rule) {
+ global $wgDBmwschema;
+ $exists = $this->selectField("pg_rules", "rulename",
+ array( "rulename" => $rule,
+ "tablename" => $table,
+ "schemaname" => $wgDBmwschema));
+ return $exists === $rule;
+ }
+
+ function constraintExists($table, $constraint) {
+ global $wgDBmwschema;
+ $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
+ "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+ $this->addQuotes($wgDBmwschema),
+ $this->addQuotes($table),
+ $this->addQuotes($constraint));
+ $res = $this->query($SQL);
+ if (!$res)
+ return NULL;
+ $rows = $res->numRows();
+ $this->freeResult($res);
+ return $rows;
+ }
+
+ /**
+ * Query whether a given schema exists. Returns the name of the owner
+ */
+ function schemaExists( $schema ) {
+ $eschema = preg_replace("/'/", "''", $schema);
+ $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
+ ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
+ $res = $this->query( $SQL );
+ if ( $res && $res->numRows() ) {
+ $row = $res->fetchObject();
+ $owner = $row->rolname;
+ } else {
+ $owner = false;
+ }
+ if ($res)
+ $this->freeResult($res);
+ return $owner;
+ }
+
+ /**
+ * Query whether a given column exists in the mediawiki schema
+ */
+ function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) {
+ global $wgDBmwschema;
+ $etable = preg_replace("/'/", "''", $table);
+ $eschema = preg_replace("/'/", "''", $wgDBmwschema);
+ $ecol = preg_replace("/'/", "''", $field);
+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a "
+ . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
+ . "AND a.attrelid = c.oid AND a.attname = '$ecol'";
+ $res = $this->query( $SQL, $fname );
+ $count = $res ? $res->numRows() : 0;
+ if ($res)
+ $this->freeResult( $res );
+ return $count;
+ }
+
+ function fieldInfo( $table, $field ) {
+ return PostgresField::fromText($this, $table, $field);
+ }
+
+ function begin( $fname = 'DatabasePostgres::begin' ) {
+ $this->query( 'BEGIN', $fname );
+ $this->mTrxLevel = 1;
+ }
+ function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) {
+ return true;
+ }
+ function commit( $fname = 'DatabasePostgres::commit' ) {
+ $this->query( 'COMMIT', $fname );
+ $this->mTrxLevel = 0;
+ }
+
+ /* Not even sure why this is used in the main codebase... */
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ function setup_database() {
+ global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
+
+ // Make sure that we can write to the correct schema
+ // If not, Postgres will happily and silently go to the next search_path item
+ $ctest = "mediawiki_test_table";
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ if ($this->tableExists($ctest, $wgDBmwschema)) {
+ $this->doQuery("DROP TABLE $safeschema.$ctest");
+ }
+ $SQL = "CREATE TABLE $safeschema.$ctest(a int)";
+ $olde = error_reporting( 0 );
+ $res = $this->doQuery($SQL);
+ error_reporting( $olde );
+ if (!$res) {
+ print "FAILED. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"\n";
+ dieout("");
+ }
+ $this->doQuery("DROP TABLE $safeschema.$ctest");
+
+ $res = dbsource( "../maintenance/postgres/tables.sql", $this);
+
+ ## Update version information
+ $mwv = $this->addQuotes($wgVersion);
+ $pgv = $this->addQuotes($this->getServerVersion());
+ $pgu = $this->addQuotes($this->mUser);
+ $mws = $this->addQuotes($wgDBmwschema);
+ $tss = $this->addQuotes($wgDBts2schema);
+ $pgp = $this->addQuotes($wgDBport);
+ $dbn = $this->addQuotes($this->mDBname);
+ $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
+
+ $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
+ "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
+ "ctype = '$ctype' ".
+ "WHERE type = 'Creation'";
+ $this->query($SQL);
+
+ ## Avoid the non-standard "REPLACE INTO" syntax
+ $f = fopen( "../maintenance/interwiki.sql", 'r' );
+ if ($f == false ) {
+ dieout( "
Could not find the interwiki.sql file");
+ }
+ ## We simply assume it is already empty as we have just created it
+ $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+ while ( ! feof( $f ) ) {
+ $line = fgets($f,1024);
+ $matches = array();
+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
+ continue;
+ }
+ $this->query("$SQL $matches[1],$matches[2])");
+ }
+ print " (table interwiki successfully populated)...\n";
+
+ $this->doQuery("COMMIT");
+ }
+
+ function encodeBlob( $b ) {
+ return new Blob ( pg_escape_bytea( $b ) ) ;
+ }
+
+ function decodeBlob( $b ) {
+ if ($b instanceof Blob) {
+ $b = $b->fetch();
+ }
+ return pg_unescape_bytea( $b );
+ }
+
+ function strencode( $s ) { ## Should not be called by us
+ return pg_escape_string( $s );
+ }
+
+ function addQuotes( $s ) {
+ if ( is_null( $s ) ) {
+ return 'NULL';
+ } else if ($s instanceof Blob) {
+ return "'".$s->fetch($s)."'";
+ }
+ return "'" . pg_escape_string($s) . "'";
+ }
+
+ function quote_ident( $s ) {
+ return '"' . preg_replace( '/"/', '""', $s) . '"';
+ }
+
+ /* For now, does nothing */
+ function selectDB( $db ) {
+ return true;
+ }
+
+ /**
+ * Postgres specific version of replaceVars.
+ * Calls the parent version in Database.php
+ *
+ * @private
+ *
+ * @param string $com SQL string, read from a stream (usually tables.sql)
+ *
+ * @return string SQL string
+ */
+ protected function replaceVars( $ins ) {
+
+ $ins = parent::replaceVars( $ins );
+
+ if ($this->numeric_version >= 8.3) {
+ // Thanks for not providing backwards-compatibility, 8.3
+ $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
+ }
+
+ if ($this->numeric_version <= 8.1) { // Our minimum version
+ $ins = str_replace( 'USING gin', 'USING gist', $ins );
+ }
+
+ return $ins;
+ }
+
+ /**
+ * Various select options
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = $useIndex = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
+
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ public function setTimeout( $timeout ) {
+ // @todo fixme no-op
+ }
+
+ function ping() {
+ wfDebug( "Function ping() not written for DatabasePostgres.php yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ *
+ */
+ public function getLag() {
+ # Not implemented for PostgreSQL
+ return false;
+ }
+
+ function setFakeSlaveLag() {}
+ function setFakeMaster() {}
+
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ function getServer() {
+ return $this->mServer;
+ }
+
+ function buildConcat( $stringList ) {
+ return implode( ' || ', $stringList );
+ }
+
+} // end DatabasePostgres class
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
new file mode 100644
index 0000000000..8b4466ecaf
--- /dev/null
+++ b/includes/db/DatabaseSqlite.php
@@ -0,0 +1,395 @@
+mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+ $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
+ $this->open($server, $user, $password, $dbName);
+ }
+
+ /**
+ * todo: check if these should be true like parent class
+ */
+ function implicitGroupby() { return false; }
+ function implicitOrderby() { return false; }
+
+ static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
+ return new DatabaseSqlite($server, $user, $password, $dbName, $failFunction, $flags);
+ }
+
+ /** Open an SQLite database and return a resource handle to it
+ * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
+ */
+ function open($server,$user,$pass,$dbName) {
+ $this->mConn = false;
+ if ($dbName) {
+ $file = $this->mDatabaseFile;
+ if ($this->mFlags & DBO_PERSISTENT) $this->mConn = new PDO("sqlite:$file",$user,$pass,array(PDO::ATTR_PERSISTENT => true));
+ else $this->mConn = new PDO("sqlite:$file",$user,$pass);
+ if ($this->mConn === false) wfDebug("DB connection error: $err\n");;
+ $this->mOpened = $this->mConn;
+ $this->mConn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT); # set error codes only, dont raise exceptions
+ }
+ return $this->mConn;
+ }
+
+ /**
+ * Close an SQLite database
+ */
+ function close() {
+ $this->mOpened = false;
+ if (is_object($this->mConn)) {
+ if ($this->trxLevel()) $this->immediateCommit();
+ $this->mConn = null;
+ }
+ return true;
+ }
+
+ /**
+ * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
+ */
+ function doQuery($sql) {
+ $res = $this->mConn->query($sql);
+ if ($res === false) $this->reportQueryError($this->lastError(),$this->lastErrno(),$sql,__FUNCTION__);
+ else {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ $this->mAffectedRows = $r->rowCount();
+ $res = new ResultWrapper($this,$r->fetchAll());
+ }
+ return $res;
+ }
+
+ function freeResult(&$res) {
+ if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL;
+ }
+
+ function fetchObject(&$res) {
+ if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+ $cur = current($r);
+ if (is_array($cur)) {
+ next($r);
+ $obj = new stdClass;
+ foreach ($cur as $k => $v) if (!is_numeric($k)) $obj->$k = $v;
+ return $obj;
+ }
+ return false;
+ }
+
+ function fetchRow(&$res) {
+ if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+ $cur = current($r);
+ if (is_array($cur)) {
+ next($r);
+ return $cur;
+ }
+ return false;
+ }
+
+ /**
+ * The PDO::Statement class implements the array interface so count() will work
+ */
+ function numRows(&$res) {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ return count($r);
+ }
+
+ function numFields(&$res) {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ return is_array($r) ? count($r[0]) : 0;
+ }
+
+ function fieldName(&$res,$n) {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ if (is_array($r)) {
+ $keys = array_keys($r[0]);
+ return $keys[$n];
+ }
+ return false;
+ }
+
+ /**
+ * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
+ */
+ function tableName($name) {
+ return str_replace('`','',parent::tableName($name));
+ }
+
+ /**
+ * This must be called after nextSequenceVal
+ */
+ function insertId() {
+ return $this->mConn->lastInsertId();
+ }
+
+ function dataSeek(&$res,$row) {
+ if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+ reset($r);
+ if ($row > 0) for ($i = 0; $i < $row; $i++) next($r);
+ }
+
+ function lastError() {
+ if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
+ $e = $this->mConn->errorInfo();
+ return isset($e[2]) ? $e[2] : '';
+ }
+
+ function lastErrno() {
+ if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
+ return $this->mConn->errorCode();
+ }
+
+ function affectedRows() {
+ return $this->mAffectedRows;
+ }
+
+ /**
+ * Returns information about an index
+ * - if errors are explicitly ignored, returns NULL on failure
+ */
+ function indexInfo($table, $index, $fname = 'Database::indexExists') {
+ return false;
+ }
+
+ function indexUnique($table, $index, $fname = 'Database::indexUnique') {
+ return false;
+ }
+
+ /**
+ * Filter the options used in SELECT statements
+ */
+ function makeSelectOptions($options) {
+ foreach ($options as $k => $v) if (is_numeric($k) && $v == 'FOR UPDATE') $options[$k] = '';
+ return parent::makeSelectOptions($options);
+ }
+
+ /**
+ * Based on MySQL method (parent) with some prior SQLite-sepcific adjustments
+ */
+ function insert($table, $a, $fname = 'DatabaseSqlite::insert', $options = array()) {
+ if (!count($a)) return true;
+ if (!is_array($options)) $options = array($options);
+
+ # SQLite uses OR IGNORE not just IGNORE
+ foreach ($options as $k => $v) if ($v == 'IGNORE') $options[$k] = 'OR IGNORE';
+
+ # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
+ if (isset($a[0]) && is_array($a[0])) {
+ $ret = true;
+ foreach ($a as $k => $v) if (!parent::insert($table,$v,"$fname/multi-row",$options)) $ret = false;
+ }
+ else $ret = parent::insert($table,$a,"$fname/single-row",$options);
+
+ return $ret;
+ }
+
+ /**
+ * SQLite does not have a "USE INDEX" clause, so return an empty string
+ */
+ function useIndexClause($index) {
+ return '';
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize($table, $field) {
+ return -1;
+ }
+
+ /**
+ * No low priority option in SQLite
+ */
+ function lowPriorityOption() {
+ return '';
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * - uses CASE on SQLite
+ */
+ function conditional($cond, $trueVal, $falseVal) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ function wasDeadlock() {
+ return $this->lastErrno() == SQLITE_BUSY;
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://sqlite.org/ SQLite]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ global $wgContLang;
+ $ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION);
+ $size = $wgContLang->formatSize(filesize($this->mDatabaseFile));
+ $file = basename($this->mDatabaseFile);
+ return $ver." ($file: $size)";
+ }
+
+ /**
+ * Query whether a given column exists in the mediawiki schema
+ */
+ function fieldExists($table, $field) { return true; }
+
+ function fieldInfo($table, $field) { return SQLiteField::fromText($this, $table, $field); }
+
+ function begin() {
+ if ($this->mTrxLevel == 1) $this->commit();
+ $this->mConn->beginTransaction();
+ $this->mTrxLevel = 1;
+ }
+
+ function commit() {
+ if ($this->mTrxLevel == 0) return;
+ $this->mConn->commit();
+ $this->mTrxLevel = 0;
+ }
+
+ function rollback() {
+ if ($this->mTrxLevel == 0) return;
+ $this->mConn->rollBack();
+ $this->mTrxLevel = 0;
+ }
+
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ function strencode($s) {
+ return substr($this->addQuotes($s),1,-1);
+ }
+
+ function encodeBlob($b) {
+ return $this->strencode($b);
+ }
+
+ function decodeBlob($b) {
+ return $b;
+ }
+
+ function addQuotes($s) {
+ return $this->mConn->quote($s);
+ }
+
+ function quote_ident($s) { return $s; }
+
+ /**
+ * For now, does nothing
+ */
+ function selectDB($db) { return true; }
+
+ /**
+ * not done
+ */
+ public function setTimeout($timeout) { return; }
+
+ function ping() {
+ wfDebug("Function ping() not written for SQLite yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ */
+ public function getLag() {
+ return 0;
+ }
+
+ /**
+ * Called by the installer script (when modified according to the MediaWikiLite installation instructions)
+ * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
+ */
+ public function setup_database() {
+ global $IP,$wgSQLiteDataDir,$wgDBTableOptions;
+ $wgDBTableOptions = '';
+ $mysql_tmpl = "$IP/maintenance/tables.sql";
+ $mysql_iw = "$IP/maintenance/interwiki.sql";
+ $sqlite_tmpl = "$IP/maintenance/sqlite/tables.sql";
+
+ # Make an SQLite template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
+ if (!file_exists($sqlite_tmpl)) {
+ $sql = file_get_contents($mysql_tmpl);
+ $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
+ $sql = preg_replace('/^\s*(UNIQUE)?\s*(PRIMARY)?\s*KEY.+?$/m','',$sql);
+ $sql = preg_replace('/^\s*(UNIQUE )?INDEX.+?$/m','',$sql); # These indexes should be created with a CREATE INDEX query
+ $sql = preg_replace('/^\s*FULLTEXT.+?$/m','',$sql); # Full text indexes
+ $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
+ $sql = preg_replace('/binary\(\d+\)/','BLOB',$sql);
+ $sql = preg_replace('/(TYPE|MAX_ROWS|AVG_ROW_LENGTH)=\w+/','',$sql);
+ $sql = preg_replace('/,\s*\)/s',')',$sql); # removing previous items may leave a trailing comma
+ $sql = str_replace('binary','',$sql);
+ $sql = str_replace('auto_increment','PRIMARY KEY AUTOINCREMENT',$sql);
+ $sql = str_replace(' unsigned','',$sql);
+ $sql = str_replace(' int ',' INTEGER ',$sql);
+ $sql = str_replace('NOT NULL','',$sql);
+
+ # Tidy up and write file
+ $sql = preg_replace('/^\s*^/m','',$sql); # Remove empty lines
+ $sql = preg_replace('/;$/m',";\n",$sql); # Separate each statement with an empty line
+ file_put_contents($sqlite_tmpl,$sql);
+ }
+
+ # Parse the SQLite template replacing inline variables such as /*$wgDBprefix*/
+ $err = $this->sourceFile($sqlite_tmpl);
+ if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
+
+ # Use DatabasePostgres's code to populate interwiki from MySQL template
+ $f = fopen($mysql_iw,'r');
+ if ($f == false) dieout("
Could not find the interwiki.sql file");
+ $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+ while (!feof($f)) {
+ $line = fgets($f,1024);
+ $matches = array();
+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
+ $this->query("$sql $matches[1],$matches[2])");
+ }
+ }
+
+}
+
+/**
+ * @ingroup Database
+ */
+class SQLiteField extends MySQLField {
+
+ function __construct() {
+ }
+
+ static function fromText($db, $table, $field) {
+ $n = new SQLiteField;
+ $n->name = $field;
+ $n->tablename = $table;
+ return $n;
+ }
+
+} // end DatabaseSqlite class
+
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
new file mode 100644
index 0000000000..e7b2778390
--- /dev/null
+++ b/includes/db/LBFactory.php
@@ -0,0 +1,224 @@
+forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) );
+ }
+
+ /**
+ * Private helper for forEachLBCallMethod
+ */
+ function callMethod( $loadBalancer, $methodName, $args ) {
+ call_user_func_array( array( $loadBalancer, $methodName ), $args );
+ }
+
+ /**
+ * Commit changes on all master connections
+ */
+ function commitMasterChanges() {
+ $this->forEachLBCallMethod( 'commitMasterChanges' );
+ }
+}
+
+/**
+ * A simple single-master LBFactory that gets its configuration from the b/c globals
+ */
+class LBFactory_Simple extends LBFactory {
+ var $mainLB;
+ var $extLBs = array();
+
+ # Chronology protector
+ var $chronProt;
+
+ function __construct( $conf ) {
+ $this->chronProt = new ChronologyProtector;
+ }
+
+ function getMainLB( $wiki = false ) {
+ if ( !isset( $this->mainLB ) ) {
+ global $wgDBservers, $wgMasterWaitTimeout;
+ if ( !$wgDBservers ) {
+ global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
+ $wgDBservers = array(array(
+ 'host' => $wgDBserver,
+ 'user' => $wgDBuser,
+ 'password' => $wgDBpassword,
+ 'dbname' => $wgDBname,
+ 'type' => $wgDBtype,
+ 'load' => 1,
+ 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
+ ));
+ }
+
+ $this->mainLB = new LoadBalancer( $wgDBservers, false, $wgMasterWaitTimeout, true );
+ $this->mainLB->parentInfo( array( 'id' => 'main' ) );
+ $this->chronProt->initLB( $this->mainLB );
+ }
+ return $this->mainLB;
+ }
+
+ function &getExternalLB( $cluster, $wiki = false ) {
+ global $wgExternalServers;
+ if ( !isset( $this->extLBs[$cluster] ) ) {
+ if ( !isset( $wgExternalServers[$cluster] ) ) {
+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ }
+ $this->extLBs[$cluster] = new LoadBalancer( $wgExternalServers[$cluster] );
+ $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ }
+ return $this->extLBs[$cluster];
+ }
+
+ /**
+ * Execute a function for each tracked load balancer
+ * The callback is called with the load balancer as the first parameter,
+ * and $params passed as the subsequent parameters.
+ */
+ function forEachLB( $callback, $params = array() ) {
+ if ( isset( $this->mainLB ) ) {
+ call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) );
+ }
+ foreach ( $this->extLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ }
+
+ function shutdown() {
+ if ( $this->mainLB ) {
+ $this->chronProt->shutdownLB( $this->mainLB );
+ }
+ $this->chronProt->shutdown();
+ $this->commitMasterChanges();
+ }
+}
+
+/**
+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Kind of like Hawking's [[Chronology Protection Agency]].
+ */
+class ChronologyProtector {
+ var $startupPos;
+ var $shutdownPos = array();
+
+ /**
+ * Initialise a LoadBalancer to give it appropriate chronology protection.
+ *
+ * @param LoadBalancer $lb
+ */
+ function initLB( $lb ) {
+ if ( $this->startupPos === null ) {
+ if ( !empty( $_SESSION[__CLASS__] ) ) {
+ $this->startupPos = $_SESSION[__CLASS__];
+ }
+ }
+ if ( !$this->startupPos ) {
+ return;
+ }
+ $masterName = $lb->getServerName( 0 );
+
+ if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
+ $info = $lb->parentInfo();
+ $pos = $this->startupPos[$masterName];
+ wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
+ $lb->waitFor( $this->startupPos[$masterName] );
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LoadBalancer is about to shut
+ * down. Saves replication positions.
+ *
+ * @param LoadBalancer $lb
+ */
+ function shutdownLB( $lb ) {
+ if ( session_id() != '' && $lb->getServerCount() > 1 ) {
+ $masterName = $lb->getServerName( 0 );
+ if ( !isset( $this->shutdownPos[$masterName] ) ) {
+ $pos = $lb->getMasterPos();
+ $info = $lb->parentInfo();
+ wfDebug( __METHOD__.": LB " . $info['id'] . " has master pos $pos\n" );
+ $this->shutdownPos[$masterName] = $pos;
+ }
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+ * May commit chronology data to persistent storage.
+ */
+ function shutdown() {
+ if ( session_id() != '' && count( $this->shutdownPos ) ) {
+ wfDebug( __METHOD__.": saving master pos for " .
+ count( $this->shutdownPos ) . " master(s)\n" );
+ $_SESSION[__CLASS__] = $this->shutdownPos;
+ }
+ }
+}
diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php
new file mode 100644
index 0000000000..b5fc1f6dd7
--- /dev/null
+++ b/includes/db/LBFactory_Multi.php
@@ -0,0 +1,224 @@
+ array( 'db1' => 100, 'db2' => 100 ) )
+ *
+ * serverTemplate A server info associative array as documented for $wgDBservers. The host,
+ * hostName and load entries will be overridden.
+ *
+ * groupLoadsBySection A 3-d map giving server load ratios for each section and group. For example:
+ * array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) )
+ *
+ * groupLoadsByDB A 3-d map giving server load ratios by DB name.
+ *
+ * hostsByName A map of hostname to IP address.
+ *
+ * externalLoads A map of external storage cluster name to server load map
+ *
+ * externalTemplateOverrides A set of server info keys overriding serverTemplate for external storage
+ *
+ * templateOverridesByServer A 2-d map overriding serverTemplate and externalTemplateOverrides on a
+ * server-by-server basis. Applies to both core and external storage.
+ *
+ * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster
+ *
+ * masterTemplateOverrides An override array for all master servers.
+ *
+ * @ingroup Database
+ */
+class LBFactory_Multi extends LBFactory {
+ // Required settings
+ var $sectionsByDB, $sectionLoads, $serverTemplate;
+ // Optional settings
+ var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
+ var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer;
+ var $templateOverridesByCluster, $masterTemplateOverrides;
+ // Other stuff
+ var $conf, $mainLBs = array(), $extLBs = array();
+ var $localSection = null;
+
+ function __construct( $conf ) {
+ $this->chronProt = new ChronologyProtector;
+ $this->conf = $conf;
+ $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' );
+ $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
+ 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
+ 'templateOverridesByCluster', 'masterTemplateOverrides' );
+
+ foreach ( $required as $key ) {
+ if ( !isset( $conf[$key] ) ) {
+ throw new MWException( __CLASS__.": $key is required in configuration" );
+ }
+ $this->$key = $conf[$key];
+ }
+
+ foreach ( $optional as $key ) {
+ if ( isset( $conf[$key] ) ) {
+ $this->$key = $conf[$key];
+ }
+ }
+ }
+
+ function getSectionForWiki( $wiki ) {
+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+ if ( isset( $this->sectionsByDB[$dbName] ) ) {
+ return $this->sectionsByDB[$dbName];
+ } else {
+ return 'DEFAULT';
+ }
+ }
+
+ function getMainLB( $wiki = false ) {
+ // Determine section
+ if ( $wiki === false ) {
+ if ( $this->localSection === null ) {
+ $this->localSection = $this->getSectionForWiki( $wiki );
+ }
+ $section = $this->localSection;
+ } else {
+ $section = $this->getSectionForWiki( $wiki );
+ }
+
+ if ( !isset( $this->mainLBs[$section] ) ) {
+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+ $groupLoads = array();
+ if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
+ $groupLoads = $this->groupLoadsByDB[$dbName];
+ }
+ if ( isset( $this->groupLoadsBySection[$section] ) ) {
+ $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
+ }
+ $this->mainLBs[$section] = $this->newLoadBalancer( $this->serverTemplate,
+ $this->sectionLoads[$section], $groupLoads, "main-$section" );
+ $this->chronProt->initLB( $this->mainLBs[$section] );
+ }
+ return $this->mainLBs[$section];
+ }
+
+ function &getExternalLB( $cluster, $wiki = false ) {
+ if ( !isset( $this->extLBs[$cluster] ) ) {
+ if ( !isset( $this->externalLoads[$cluster] ) ) {
+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ }
+ $template = $this->serverTemplate;
+ if ( isset( $this->externalTemplateOverrides ) ) {
+ $template = $this->externalTemplateOverrides + $template;
+ }
+ if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
+ $template = $this->templateOverridesByCluster[$cluster] + $template;
+ }
+ $this->extLBs[$cluster] = $this->newLoadBalancer( $template,
+ $this->externalLoads[$cluster], array(), "ext-$cluster" );
+ }
+ return $this->extLBs[$cluster];
+ }
+
+ /**
+ * Make a new load balancer object based on template and load array
+ */
+ function newLoadBalancer( $template, $loads, $groupLoads, $id ) {
+ global $wgMasterWaitTimeout;
+ $servers = $this->makeServerArray( $template, $loads, $groupLoads );
+ $lb = new LoadBalancer( $servers, false, $wgMasterWaitTimeout );
+ $lb->parentInfo( array( 'id' => $id ) );
+ return $lb;
+ }
+
+ /**
+ * Make a server array as expected by LoadBalancer::__construct, using a template and load array
+ */
+ function makeServerArray( $template, $loads, $groupLoads ) {
+ $servers = array();
+ $master = true;
+ $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
+ foreach ( $groupLoadsByServer as $server => $stuff ) {
+ if ( !isset( $loads[$server] ) ) {
+ $loads[$server] = 0;
+ }
+ }
+ foreach ( $loads as $serverName => $load ) {
+ $serverInfo = $template;
+ if ( $master ) {
+ $serverInfo['master'] = true;
+ if ( isset( $this->masterTemplateOverrides ) ) {
+ $serverInfo = $this->masterTemplateOverrides + $serverInfo;
+ }
+ $master = false;
+ }
+ if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
+ $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
+ }
+ if ( isset( $groupLoadsByServer[$serverName] ) ) {
+ $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
+ }
+ if ( isset( $this->hostsByName[$serverName] ) ) {
+ $serverInfo['host'] = $this->hostsByName[$serverName];
+ } else {
+ $serverInfo['host'] = $serverName;
+ }
+ $serverInfo['hostName'] = $serverName;
+ $serverInfo['load'] = $load;
+ $servers[] = $serverInfo;
+ }
+ return $servers;
+ }
+
+ /**
+ * Take a group load array indexed by group then server, and reindex it by server then group
+ */
+ function reindexGroupLoads( $groupLoads ) {
+ $reindexed = array();
+ foreach ( $groupLoads as $group => $loads ) {
+ foreach ( $loads as $server => $load ) {
+ $reindexed[$server][$group] = $load;
+ }
+ }
+ return $reindexed;
+ }
+
+ /**
+ * Get the database name and prefix based on the wiki ID
+ */
+ function getDBNameAndPrefix( $wiki = false ) {
+ if ( $wiki === false ) {
+ global $wgDBname, $wgDBprefix;
+ return array( $wgDBname, $wgDBprefix );
+ } else {
+ return wfSplitWikiID( $wiki );
+ }
+ }
+
+ /**
+ * Execute a function for each tracked load balancer
+ * The callback is called with the load balancer as the first parameter,
+ * and $params passed as the subsequent parameters.
+ */
+ function forEachLB( $callback, $params = array() ) {
+ foreach ( $this->mainLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ foreach ( $this->extLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ }
+
+ function shutdown() {
+ foreach ( $this->mainLBs as $lb ) {
+ $this->chronProt->shutdownLB( $lb );
+ }
+ $this->chronProt->shutdown();
+ $this->commitMasterChanges();
+ }
+}
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
new file mode 100644
index 0000000000..4280713b2a
--- /dev/null
+++ b/includes/db/LoadBalancer.php
@@ -0,0 +1,894 @@
+mServers = $servers;
+ $this->mFailFunction = $failFunction;
+ $this->mReadIndex = -1;
+ $this->mWriteIndex = -1;
+ $this->mConns = array(
+ 'local' => array(),
+ 'foreignUsed' => array(),
+ 'foreignFree' => array() );
+ $this->mLastIndex = -1;
+ $this->mLoads = array();
+ $this->mWaitForPos = false;
+ $this->mWaitTimeout = $waitTimeout;
+ $this->mLaggedSlaveMode = false;
+ $this->mErrorConnection = false;
+ $this->mAllowLag = false;
+
+ foreach( $servers as $i => $server ) {
+ $this->mLoads[$i] = $server['load'];
+ if ( isset( $server['groupLoads'] ) ) {
+ foreach ( $server['groupLoads'] as $group => $ratio ) {
+ if ( !isset( $this->mGroupLoads[$group] ) ) {
+ $this->mGroupLoads[$group] = array();
+ }
+ $this->mGroupLoads[$group][$i] = $ratio;
+ }
+ }
+ }
+ }
+
+ static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
+ {
+ return new LoadBalancer( $servers, $failFunction, $waitTimeout );
+ }
+
+ /**
+ * Get or set arbitrary data used by the parent object, usually an LBFactory
+ */
+ function parentInfo( $x = null ) {
+ return wfSetVar( $this->mParentInfo, $x );
+ }
+
+ /**
+ * Given an array of non-normalised probabilities, this function will select
+ * an element and return the appropriate key
+ */
+ function pickRandom( $weights )
+ {
+ if ( !is_array( $weights ) || count( $weights ) == 0 ) {
+ return false;
+ }
+
+ $sum = array_sum( $weights );
+ if ( $sum == 0 ) {
+ # No loads on any of them
+ # In previous versions, this triggered an unweighted random selection,
+ # but this feature has been removed as of April 2006 to allow for strict
+ # separation of query groups.
+ return false;
+ }
+ $max = mt_getrandmax();
+ $rand = mt_rand(0, $max) / $max * $sum;
+
+ $sum = 0;
+ foreach ( $weights as $i => $w ) {
+ $sum += $w;
+ if ( $sum >= $rand ) {
+ break;
+ }
+ }
+ return $i;
+ }
+
+ function getRandomNonLagged( $loads, $wiki = false ) {
+ # Unset excessively lagged servers
+ $lags = $this->getLagTimes( $wiki );
+ foreach ( $lags as $i => $lag ) {
+ if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) {
+ if ( $lag === false ) {
+ wfDebug( "Server #$i is not replicating\n" );
+ unset( $loads[$i] );
+ } elseif ( $lag > $this->mServers[$i]['max lag'] ) {
+ wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" );
+ unset( $loads[$i] );
+ }
+ }
+ }
+
+ # Find out if all the slaves with non-zero load are lagged
+ $sum = 0;
+ foreach ( $loads as $load ) {
+ $sum += $load;
+ }
+ if ( $sum == 0 ) {
+ # No appropriate DB servers except maybe the master and some slaves with zero load
+ # Do NOT use the master
+ # Instead, this function will return false, triggering read-only mode,
+ # and a lagged slave will be used instead.
+ return false;
+ }
+
+ if ( count( $loads ) == 0 ) {
+ return false;
+ }
+
+ #wfDebugLog( 'connect', var_export( $loads, true ) );
+
+ # Return a random representative of the remainder
+ return $this->pickRandom( $loads );
+ }
+
+ /**
+ * Get the index of the reader connection, which may be a slave
+ * This takes into account load ratios and lag times. It should
+ * always return a consistent index during a given invocation
+ *
+ * Side effect: opens connections to databases
+ */
+ function getReaderIndex( $group = false, $wiki = false ) {
+ global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
+
+ # FIXME: For now, only go through all this for mysql databases
+ if ($wgDBtype != 'mysql') {
+ return $this->getWriterIndex();
+ }
+
+ if ( count( $this->mServers ) == 1 ) {
+ # Skip the load balancing if there's only one server
+ return 0;
+ } elseif ( $group === false and $this->mReadIndex >= 0 ) {
+ # Shortcut if generic reader exists already
+ return $this->mReadIndex;
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ $totalElapsed = 0;
+
+ # convert from seconds to microseconds
+ $timeout = $wgDBClusterTimeout * 1e6;
+
+ # Find the relevant load array
+ if ( $group !== false ) {
+ if ( isset( $this->mGroupLoads[$group] ) ) {
+ $nonErrorLoads = $this->mGroupLoads[$group];
+ } else {
+ # No loads for this group, return false and the caller can use some other group
+ wfDebug( __METHOD__.": no loads for group $group\n" );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ } else {
+ $nonErrorLoads = $this->mLoads;
+ }
+
+ if ( !$nonErrorLoads ) {
+ throw new MWException( "Empty server array given to LoadBalancer" );
+ }
+
+ $i = false;
+ $found = false;
+ $laggedSlaveMode = false;
+
+ # First try quickly looking through the available servers for a server that
+ # meets our criteria
+ do {
+ $totalThreadsConnected = 0;
+ $overloadedServers = 0;
+ $currentLoads = $nonErrorLoads;
+ while ( count( $currentLoads ) ) {
+ if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
+ $i = $this->pickRandom( $currentLoads );
+ } else {
+ $i = $this->getRandomNonLagged( $currentLoads, $wiki );
+ if ( $i === false && count( $currentLoads ) != 0 ) {
+ # All slaves lagged. Switch to read-only mode
+ $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
+ $i = $this->pickRandom( $currentLoads );
+ $laggedSlaveMode = true;
+ }
+ }
+
+ if ( $i === false ) {
+ # pickRandom() returned false
+ # This is permanent and means the configuration wants us to return false
+ wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
+ $conn = $this->openConnection( $i, $wiki );
+
+ if ( !$conn ) {
+ wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
+ unset( $nonErrorLoads[$i] );
+ unset( $currentLoads[$i] );
+ continue;
+ }
+
+ if ( isset( $this->mServers[$i]['max threads'] ) ) {
+ $status = $conn->getStatus("Thread%");
+ if ( $wiki !== false ) {
+ $this->reuseConnection( $conn );
+ }
+ if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) {
+ $totalThreadsConnected += $status['Threads_connected'];
+ $overloadedServers++;
+ unset( $currentLoads[$i] );
+ } else {
+ # Max threads satisfied, return this server
+ break 2;
+ }
+ } else {
+ # No maximum, return this server
+ if ( $wiki !== false ) {
+ $this->reuseConnection( $conn );
+ }
+ $found = true;
+ break 2;
+ }
+ }
+
+ # No server found yet
+ $i = false;
+
+ # If all servers were down, quit now
+ if ( !count( $nonErrorLoads ) ) {
+ wfDebugLog( 'connect', "All servers down\n" );
+ break;
+ }
+
+ # Some servers must have been overloaded
+ if ( $overloadedServers == 0 ) {
+ throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
+ }
+ # Back off for a while
+ # Scale the sleep time by the number of connected threads, to produce a
+ # roughly constant global poll rate
+ $avgThreads = $totalThreadsConnected / $overloadedServers;
+ $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads );
+ } while ( $totalElapsed < $timeout );
+
+ if ( $totalElapsed >= $timeout ) {
+ wfDebugLog( 'connect', "All servers busy\n" );
+ $this->mErrorConnection = false;
+ $this->mLastError = 'All servers busy';
+ }
+
+ if ( $i !== false ) {
+ # Wait for the session master pos for a short time
+ if ( $this->mWaitForPos && $i > 0 ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
+ }
+ }
+ if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) {
+ $this->mReadIndex = $i;
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ return $i;
+ }
+
+ /**
+ * Wait for a specified number of microseconds, and return the period waited
+ */
+ function sleep( $t ) {
+ wfProfileIn( __METHOD__ );
+ wfDebug( __METHOD__.": waiting $t us\n" );
+ usleep( $t );
+ wfProfileOut( __METHOD__ );
+ return $t;
+ }
+
+ /**
+ * Get a random server to use in a query group
+ * @deprecated use getReaderIndex
+ */
+ function getGroupIndex( $group ) {
+ return $this->getReaderIndex( $group );
+ }
+
+ /**
+ * Set the master wait position
+ * If a DB_SLAVE connection has been opened already, waits
+ * Otherwise sets a variable telling it to wait if such a connection is opened
+ */
+ public function waitFor( $pos ) {
+ wfProfileIn( __METHOD__ );
+ $this->mWaitForPos = $pos;
+ $i = $this->mReadIndex;
+
+ if ( $i > 0 ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
+ $this->mLaggedSlaveMode = true;
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Get any open connection to a given server index, local or foreign
+ * Returns false if there is no connection open
+ */
+ function getAnyOpenConnection( $i ) {
+ foreach ( $this->mConns as $type => $conns ) {
+ if ( !empty( $conns[$i] ) ) {
+ return reset( $conns[$i] );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Wait for a given slave to catch up to the master pos stored in $this
+ */
+ function doWait( $index ) {
+ # Find a connection to wait on
+ $conn = $this->getAnyOpenConnection( $index );
+ if ( !$conn ) {
+ wfDebug( __METHOD__ . ": no connection open\n" );
+ return false;
+ }
+
+ wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
+ $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
+
+ if ( $result == -1 || is_null( $result ) ) {
+ # Timed out waiting for slave, use master instead
+ wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
+ return false;
+ } else {
+ wfDebug( __METHOD__.": Done\n" );
+ return true;
+ }
+ }
+
+ /**
+ * Get a connection by index
+ * This is the main entry point for this class.
+ */
+ public function &getConnection( $i, $groups = array(), $wiki = false ) {
+ global $wgDBtype;
+ wfProfileIn( __METHOD__ );
+
+ if ( $wiki === wfWikiID() ) {
+ $wiki = false;
+ }
+
+ # Query groups
+ if ( $i == DB_MASTER ) {
+ $i = $this->getWriterIndex();
+ } elseif ( !is_array( $groups ) ) {
+ $groupIndex = $this->getReaderIndex( $groups, $wiki );
+ if ( $groupIndex !== false ) {
+ $serverName = $this->getServerName( $groupIndex );
+ wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
+ $i = $groupIndex;
+ }
+ } else {
+ foreach ( $groups as $group ) {
+ $groupIndex = $this->getReaderIndex( $group, $wiki );
+ if ( $groupIndex !== false ) {
+ $serverName = $this->getServerName( $groupIndex );
+ wfDebug( __METHOD__.": using server $serverName for group $group\n" );
+ $i = $groupIndex;
+ break;
+ }
+ }
+ }
+
+ # Operation-based index
+ if ( $i == DB_SLAVE ) {
+ $i = $this->getReaderIndex( false, $wiki );
+ } elseif ( $i == DB_LAST ) {
+ # Just use $this->mLastIndex, which should already be set
+ $i = $this->mLastIndex;
+ if ( $i === -1 ) {
+ # Oh dear, not set, best to use the writer for safety
+ wfDebug( "Warning: DB_LAST used when there was no previous index\n" );
+ $i = $this->getWriterIndex();
+ }
+ }
+ # Couldn't find a working server in getReaderIndex()?
+ if ( $i === false ) {
+ $this->reportConnectionError( $this->mErrorConnection );
+ }
+
+ # Now we have an explicit index into the servers array
+ $conn = $this->openConnection( $i, $wiki );
+ if ( !$conn ) {
+ $this->reportConnectionError( $this->mErrorConnection );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $conn;
+ }
+
+ /**
+ * Mark a foreign connection as being available for reuse under a different
+ * DB name or prefix. This mechanism is reference-counted, and must be called
+ * the same number of times as getConnection() to work.
+ */
+ public function reuseConnection( $conn ) {
+ $serverIndex = $conn->getLBInfo('serverIndex');
+ $refCount = $conn->getLBInfo('foreignPoolRefCount');
+ $dbName = $conn->getDBname();
+ $prefix = $conn->tablePrefix();
+ if ( strval( $prefix ) !== '' ) {
+ $wiki = "$dbName-$prefix";
+ } else {
+ $wiki = $dbName;
+ }
+ if ( $serverIndex === null || $refCount === null ) {
+ wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
+ /**
+ * This can happen in code like:
+ * foreach ( $dbs as $db ) {
+ * $conn = $lb->getConnection( DB_SLAVE, array(), $db );
+ * ...
+ * $lb->reuseConnection( $conn );
+ * }
+ * When a connection to the local DB is opened in this way, reuseConnection()
+ * should be ignored
+ */
+ return;
+ }
+ if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
+ throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
+ }
+ $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
+ if ( $refCount <= 0 ) {
+ $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
+ unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
+ wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
+ } else {
+ wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
+ }
+ }
+
+ /**
+ * Open a connection to the server given by the specified index
+ * Index must be an actual index into the array.
+ * If the server is already open, returns it.
+ *
+ * On error, returns false, and the connection which caused the
+ * error will be available via $this->mErrorConnection.
+ *
+ * @param integer $i Server index
+ * @param string $wiki Wiki ID to open
+ * @return Database
+ *
+ * @access private
+ */
+ function openConnection( $i, $wiki = false ) {
+ wfProfileIn( __METHOD__ );
+ if ( $wiki !== false ) {
+ $conn = $this->openForeignConnection( $i, $wiki );
+ wfProfileOut( __METHOD__);
+ return $conn;
+ }
+ if ( isset( $this->mConns['local'][$i][0] ) ) {
+ $conn = $this->mConns['local'][$i][0];
+ } else {
+ $server = $this->mServers[$i];
+ $server['serverIndex'] = $i;
+ $conn = $this->reallyOpenConnection( $server );
+ if ( $conn->isOpen() ) {
+ $this->mConns['local'][$i][0] = $conn;
+ } else {
+ wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ }
+ }
+ $this->mLastIndex = $i;
+ wfProfileOut( __METHOD__ );
+ return $conn;
+ }
+
+ /**
+ * Open a connection to a foreign DB, or return one if it is already open.
+ *
+ * Increments a reference count on the returned connection which locks the
+ * connection to the requested wiki. This reference count can be
+ * decremented by calling reuseConnection().
+ *
+ * If a connection is open to the appropriate server already, but with the wrong
+ * database, it will be switched to the right database and returned, as long as
+ * it has been freed first with reuseConnection().
+ *
+ * On error, returns false, and the connection which caused the
+ * error will be available via $this->mErrorConnection.
+ *
+ * @param integer $i Server index
+ * @param string $wiki Wiki ID to open
+ * @return Database
+ */
+ function openForeignConnection( $i, $wiki ) {
+ wfProfileIn(__METHOD__);
+ list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
+ if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
+ // Reuse an already-used connection
+ $conn = $this->mConns['foreignUsed'][$i][$wiki];
+ wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
+ } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
+ // Reuse a free connection for the same wiki
+ $conn = $this->mConns['foreignFree'][$i][$wiki];
+ unset( $this->mConns['foreignFree'][$i][$wiki] );
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
+ } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
+ // Reuse a connection from another wiki
+ $conn = reset( $this->mConns['foreignFree'][$i] );
+ $oldWiki = key( $this->mConns['foreignFree'][$i] );
+
+ if ( !$conn->selectDB( $dbName ) ) {
+ global $wguname;
+ $this->mLastError = "Error selecting database $dbName on server " .
+ $conn->getServer() . " from client host {$wguname['nodename']}\n";
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ } else {
+ $conn->tablePrefix( $prefix );
+ unset( $this->mConns['foreignFree'][$i][$oldWiki] );
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
+ }
+ } else {
+ // Open a new connection
+ $server = $this->mServers[$i];
+ $server['serverIndex'] = $i;
+ $server['foreignPoolRefCount'] = 0;
+ $conn = $this->reallyOpenConnection( $server, $dbName );
+ if ( !$conn->isOpen() ) {
+ wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ } else {
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
+ }
+ }
+
+ // Increment reference count
+ if ( $conn ) {
+ $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
+ $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
+ }
+ wfProfileOut(__METHOD__);
+ return $conn;
+ }
+
+ /**
+ * Test if the specified index represents an open connection
+ * @access private
+ */
+ function isOpen( $index ) {
+ if( !is_integer( $index ) ) {
+ return false;
+ }
+ return (bool)$this->getAnyOpenConnection( $index );
+ }
+
+ /**
+ * Really opens a connection. Uncached.
+ * Returns a Database object whether or not the connection was successful.
+ * @access private
+ */
+ function reallyOpenConnection( $server, $dbNameOverride = false ) {
+ if( !is_array( $server ) ) {
+ throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
+ }
+
+ extract( $server );
+ if ( $dbNameOverride !== false ) {
+ $dbname = $dbNameOverride;
+ }
+
+ # Get class for this database type
+ $class = 'Database' . ucfirst( $type );
+
+ # Create object
+ wfDebug( "Connecting to $host $dbname...\n" );
+ $db = new $class( $host, $user, $password, $dbname, 1, $flags );
+ if ( $db->isOpen() ) {
+ wfDebug( "Connected\n" );
+ } else {
+ wfDebug( "Failed\n" );
+ }
+ $db->setLBInfo( $server );
+ if ( isset( $server['fakeSlaveLag'] ) ) {
+ $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
+ }
+ if ( isset( $server['fakeMaster'] ) ) {
+ $db->setFakeMaster( true );
+ }
+ return $db;
+ }
+
+ function reportConnectionError( &$conn ) {
+ wfProfileIn( __METHOD__ );
+ # Prevent infinite recursion
+
+ static $reporting = false;
+ if ( !$reporting ) {
+ $reporting = true;
+ if ( !is_object( $conn ) ) {
+ // No last connection, probably due to all servers being too busy
+ $conn = new Database;
+ if ( $this->mFailFunction ) {
+ $conn->failFunction( $this->mFailFunction );
+ $conn->reportConnectionError( $this->mLastError );
+ } else {
+ // If all servers were busy, mLastError will contain something sensible
+ throw new DBConnectionError( $conn, $this->mLastError );
+ }
+ } else {
+ if ( $this->mFailFunction ) {
+ $conn->failFunction( $this->mFailFunction );
+ } else {
+ $conn->failFunction( false );
+ }
+ $server = $conn->getProperty( 'mServer' );
+ $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
+ }
+ $reporting = false;
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ function getWriterIndex() {
+ return 0;
+ }
+
+ /**
+ * Returns true if the specified index is a valid server index
+ */
+ function haveIndex( $i ) {
+ return array_key_exists( $i, $this->mServers );
+ }
+
+ /**
+ * Returns true if the specified index is valid and has non-zero load
+ */
+ function isNonZeroLoad( $i ) {
+ return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
+ }
+
+ /**
+ * Get the number of defined servers (not the number of open connections)
+ */
+ function getServerCount() {
+ return count( $this->mServers );
+ }
+
+ /**
+ * Get the host name or IP address of the server with the specified index
+ */
+ function getServerName( $i ) {
+ if ( isset( $this->mServers[$i]['hostName'] ) ) {
+ return $this->mServers[$i]['hostName'];
+ } elseif ( isset( $this->mServers[$i]['host'] ) ) {
+ return $this->mServers[$i]['host'];
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the current master position for chronology control purposes
+ * @return mixed
+ */
+ function getMasterPos() {
+ # If this entire request was served from a slave without opening a connection to the
+ # master (however unlikely that may be), then we can fetch the position from the slave.
+ $masterConn = $this->getAnyOpenConnection( 0 );
+ if ( !$masterConn ) {
+ for ( $i = 1; $i < count( $this->mServers ); $i++ ) {
+ $conn = $this->getAnyOpenConnection( $i );
+ if ( $conn ) {
+ wfDebug( "Master pos fetched from slave\n" );
+ return $conn->getSlavePos();
+ }
+ }
+ } else {
+ wfDebug( "Master pos fetched from master\n" );
+ return $masterConn->getMasterPos();
+ }
+ return false;
+ }
+
+ /**
+ * Close all open connections
+ */
+ function closeAll() {
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ $conn->close();
+ }
+ }
+ }
+ $this->mConns = array(
+ 'local' => array(),
+ 'foreignFree' => array(),
+ 'foreignUsed' => array(),
+ );
+ }
+
+ /**
+ * Close a connection
+ * Using this function makes sure the LoadBalancer knows the connection is closed.
+ * If you use $conn->close() directly, the load balancer won't update its state.
+ */
+ function closeConnecton( $conn ) {
+ $done = false;
+ foreach ( $this->mConns as $i1 => $conns2 ) {
+ foreach ( $conns2 as $i2 => $conns3 ) {
+ foreach ( $conns3 as $i3 => $candidateConn ) {
+ if ( $conn === $candidateConn ) {
+ $conn->close();
+ unset( $this->mConns[$i1][$i2][$i3] );
+ $done = true;
+ break;
+ }
+ }
+ }
+ }
+ if ( !$done ) {
+ $conn->close();
+ }
+ }
+
+ /**
+ * Commit transactions on all open connections
+ */
+ function commitAll() {
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ $conn->immediateCommit();
+ }
+ }
+ }
+ }
+
+ /* Issue COMMIT only on master, only if queries were done on connection */
+ function commitMasterChanges() {
+ // Always 0, but who knows.. :)
+ $masterIndex = $this->getWriterIndex();
+ foreach ( $this->mConns as $type => $conns2 ) {
+ if ( empty( $conns2[$masterIndex] ) ) {
+ continue;
+ }
+ foreach ( $conns2[$masterIndex] as $conn ) {
+ if ( $conn->lastQuery() != '' ) {
+ $conn->commit();
+ }
+ }
+ }
+ }
+
+ function waitTimeout( $value = NULL ) {
+ return wfSetVar( $this->mWaitTimeout, $value );
+ }
+
+ function getLaggedSlaveMode() {
+ return $this->mLaggedSlaveMode;
+ }
+
+ /* Disables/enables lag checks */
+ function allowLagged($mode=null) {
+ if ($mode===null)
+ return $this->mAllowLagged;
+ $this->mAllowLagged=$mode;
+ }
+
+ function pingAll() {
+ $success = true;
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ if ( !$conn->ping() ) {
+ $success = false;
+ }
+ }
+ }
+ }
+ return $success;
+ }
+
+ /**
+ * Get the hostname and lag time of the most-lagged slave.
+ * This is useful for maintenance scripts that need to throttle their updates.
+ * May attempt to open connections to slaves on the default DB.
+ */
+ function getMaxLag() {
+ $maxLag = -1;
+ $host = '';
+ foreach ( $this->mServers as $i => $conn ) {
+ $conn = $this->getAnyOpenConnection( $i );
+ if ( !$conn ) {
+ $conn = $this->openConnection( $i );
+ }
+ if ( !$conn ) {
+ continue;
+ }
+ $lag = $conn->getLag();
+ if ( $lag > $maxLag ) {
+ $maxLag = $lag;
+ $host = $this->mServers[$i]['host'];
+ }
+ }
+ return array( $host, $maxLag );
+ }
+
+ /**
+ * Get lag time for each server
+ * Results are cached for a short time in memcached, and indefinitely in the process cache
+ */
+ function getLagTimes( $wiki = false ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( !isset( $this->mLagTimes ) ) {
+ $expiry = 5;
+ $requestRate = 10;
+
+ global $wgMemc;
+ $masterName = $this->getServerName( 0 );
+ $memcKey = wfMemcKey( 'lag_times', $masterName );
+ $times = $wgMemc->get( $memcKey );
+ if ( $times ) {
+ # Randomly recache with probability rising over $expiry
+ $elapsed = time() - $times['timestamp'];
+ $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
+ if ( mt_rand( 0, $chance ) != 0 ) {
+ unset( $times['timestamp'] );
+ wfProfileOut( __METHOD__ );
+ return $times;
+ }
+ wfIncrStats( 'lag_cache_miss_expired' );
+ } else {
+ wfIncrStats( 'lag_cache_miss_absent' );
+ }
+
+ # Cache key missing or expired
+
+ $times = array();
+ foreach ( $this->mServers as $i => $conn ) {
+ if ($i == 0) { # Master
+ $times[$i] = 0;
+ } elseif ( false !== ( $conn = $this->getAnyOpenConnection( $i ) ) ) {
+ $times[$i] = $conn->getLag();
+ } elseif ( false !== ( $conn = $this->openConnection( $i, $wiki ) ) ) {
+ $times[$i] = $conn->getLag();
+ }
+ }
+
+ # Add a timestamp key so we know when it was cached
+ $times['timestamp'] = time();
+ $wgMemc->set( $memcKey, $times, $expiry );
+
+ # But don't give the timestamp to the caller
+ unset($times['timestamp']);
+ $this->mLagTimes = $times;
+ }
+ wfProfileOut( __METHOD__ );
+ return $this->mLagTimes;
+ }
+}
diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php
new file mode 100644
index 0000000000..d9072e9310
--- /dev/null
+++ b/includes/parser/CoreParserFunctions.php
@@ -0,0 +1,385 @@
+setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'ns', array( __CLASS__, 'ns' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'urlencode', array( __CLASS__, 'urlencode' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'lcfirst', array( __CLASS__, 'lcfirst' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'ucfirst', array( __CLASS__, 'ucfirst' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'lc', array( __CLASS__, 'lc' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'uc', array( __CLASS__, 'uc' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'localurl', array( __CLASS__, 'localurl' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'localurle', array( __CLASS__, 'localurle' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'fullurl', array( __CLASS__, 'fullurl' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'fullurle', array( __CLASS__, 'fullurle' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'formatnum', array( __CLASS__, 'formatnum' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'grammar', array( __CLASS__, 'grammar' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'plural', array( __CLASS__, 'plural' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofpages', array( __CLASS__, 'numberofpages' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofusers', array( __CLASS__, 'numberofusers' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofarticles', array( __CLASS__, 'numberofarticles' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberoffiles', array( __CLASS__, 'numberoffiles' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofadmins', array( __CLASS__, 'numberofadmins' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofedits', array( __CLASS__, 'numberofedits' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'language', array( __CLASS__, 'language' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'padleft', array( __CLASS__, 'padleft' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'padright', array( __CLASS__, 'padright' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'anchorencode', array( __CLASS__, 'anchorencode' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) );
+ $parser->setFunctionHook( 'defaultsort', array( __CLASS__, 'defaultsort' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'filepath', array( __CLASS__, 'filepath' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'pagesincategory', array( __CLASS__, 'pagesincategory' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'pagesize', array( __CLASS__, 'pagesize' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), SFH_OBJECT_ARGS );
+
+ if ( $wgAllowDisplayTitle ) {
+ $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH );
+ }
+ if ( $wgAllowSlowParserFunctions ) {
+ $parser->setFunctionHook( 'pagesinnamespace', array( __CLASS__, 'pagesinnamespace' ), SFH_NO_HASH );
+ }
+ }
+
+ static function intFunction( $parser, $part1 = '' /*, ... */ ) {
+ if ( strval( $part1 ) !== '' ) {
+ $args = array_slice( func_get_args(), 2 );
+ return wfMsgReal( $part1, $args, true );
+ } else {
+ return array( 'found' => false );
+ }
+ }
+
+ static function ns( $parser, $part1 = '' ) {
+ global $wgContLang;
+ $found = false;
+ if ( intval( $part1 ) || $part1 == "0" ) {
+ $text = $wgContLang->getNsText( intval( $part1 ) );
+ $found = true;
+ } else {
+ $param = str_replace( ' ', '_', strtolower( $part1 ) );
+ $index = MWNamespace::getCanonicalIndex( strtolower( $param ) );
+ if ( !is_null( $index ) ) {
+ $text = $wgContLang->getNsText( $index );
+ $found = true;
+ }
+ }
+ if ( $found ) {
+ return $text;
+ } else {
+ return array( 'found' => false );
+ }
+ }
+
+ static function urlencode( $parser, $s = '' ) {
+ return urlencode( $s );
+ }
+
+ static function lcfirst( $parser, $s = '' ) {
+ global $wgContLang;
+ return $wgContLang->lcfirst( $s );
+ }
+
+ static function ucfirst( $parser, $s = '' ) {
+ global $wgContLang;
+ return $wgContLang->ucfirst( $s );
+ }
+
+ static function lc( $parser, $s = '' ) {
+ global $wgContLang;
+ if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
+ return $parser->markerSkipCallback( $s, array( $wgContLang, 'lc' ) );
+ } else {
+ return $wgContLang->lc( $s );
+ }
+ }
+
+ static function uc( $parser, $s = '' ) {
+ global $wgContLang;
+ if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
+ return $parser->markerSkipCallback( $s, array( $wgContLang, 'uc' ) );
+ } else {
+ return $wgContLang->uc( $s );
+ }
+ }
+
+ static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); }
+ static function localurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeLocalURL', $s, $arg ); }
+ static function fullurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getFullURL', $s, $arg ); }
+ static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); }
+
+ static function urlFunction( $func, $s = '', $arg = null ) {
+ $title = Title::newFromText( $s );
+ # Due to order of execution of a lot of bits, the values might be encoded
+ # before arriving here; if that's true, then the title can't be created
+ # and the variable will fail. If we can't get a decent title from the first
+ # attempt, url-decode and try for a second.
+ if( is_null( $title ) )
+ $title = Title::newFromUrl( urldecode( $s ) );
+ if ( !is_null( $title ) ) {
+ if ( !is_null( $arg ) ) {
+ $text = $title->$func( $arg );
+ } else {
+ $text = $title->$func();
+ }
+ return $text;
+ } else {
+ return array( 'found' => false );
+ }
+ }
+
+ static function formatNum( $parser, $num = '', $raw = null) {
+ if ( self::israw( $raw ) ) {
+ return $parser->getFunctionLang()->parseFormattedNumber( $num );
+ } else {
+ return $parser->getFunctionLang()->formatNum( $num );
+ }
+ }
+
+ static function grammar( $parser, $case = '', $word = '' ) {
+ return $parser->getFunctionLang()->convertGrammar( $word, $case );
+ }
+
+ static function plural( $parser, $text = '') {
+ $forms = array_slice( func_get_args(), 2);
+ $text = $parser->getFunctionLang()->parseFormattedNumber( $text );
+ return $parser->getFunctionLang()->convertPlural( $text, $forms );
+ }
+
+ /**
+ * Override the title of the page when viewed, provided we've been given a
+ * title which will normalise to the canonical title
+ *
+ * @param Parser $parser Parent parser
+ * @param string $text Desired title text
+ * @return string
+ */
+ static function displaytitle( $parser, $text = '' ) {
+ $text = trim( Sanitizer::decodeCharReferences( $text ) );
+ $title = Title::newFromText( $text );
+ if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
+ $parser->mOutput->setDisplayTitle( $text );
+ return '';
+ }
+
+ static function isRaw( $param ) {
+ static $mwRaw;
+ if ( !$mwRaw ) {
+ $mwRaw =& MagicWord::get( 'rawsuffix' );
+ }
+ if ( is_null( $param ) ) {
+ return false;
+ } else {
+ return $mwRaw->match( $param );
+ }
+ }
+
+ static function formatRaw( $num, $raw ) {
+ if( self::isRaw( $raw ) ) {
+ return $num;
+ } else {
+ global $wgContLang;
+ return $wgContLang->formatNum( $num );
+ }
+ }
+ static function numberofpages( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::pages(), $raw );
+ }
+ static function numberofusers( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::users(), $raw );
+ }
+ static function numberofarticles( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::articles(), $raw );
+ }
+ static function numberoffiles( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::images(), $raw );
+ }
+ static function numberofadmins( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::admins(), $raw );
+ }
+ static function numberofedits( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::edits(), $raw );
+ }
+ static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
+ return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
+ }
+
+ /**
+ * Return the number of pages in the given category, or 0 if it's nonexis-
+ * tent. This is an expensive parser function and can't be called too many
+ * times per page.
+ */
+ static function pagesincategory( $parser, $name = '', $raw = null ) {
+ static $cache = array();
+ $category = Category::newFromName( $name );
+
+ if( !is_object( $category ) ) {
+ $cache[$name] = 0;
+ return self::formatRaw( 0, $raw );
+ }
+
+ # Normalize name for cache
+ $name = $category->getName();
+
+ $count = 0;
+ if( isset( $cache[$name] ) ) {
+ $count = $cache[$name];
+ } elseif( $parser->incrementExpensiveFunctionCount() ) {
+ $count = $cache[$name] = (int)$category->getPageCount();
+ }
+ return self::formatRaw( $count, $raw );
+ }
+
+ /**
+ * Return the size of the given page, or 0 if it's nonexistent. This is an
+ * expensive parser function and can't be called too many times per page.
+ *
+ * @FIXME This doesn't work correctly on preview for getting the size of
+ * the current page.
+ * @FIXME Title::getLength() documentation claims that it adds things to
+ * the link cache, so the local cache here should be unnecessary, but in
+ * fact calling getLength() repeatedly for the same $page does seem to
+ * run one query for each call?
+ */
+ static function pagesize( $parser, $page = '', $raw = null ) {
+ static $cache = array();
+ $title = Title::newFromText($page);
+
+ if( !is_object( $title ) ) {
+ $cache[$page] = 0;
+ return self::formatRaw( 0, $raw );
+ }
+
+ # Normalize name for cache
+ $page = $title->getPrefixedText();
+
+ $length = 0;
+ if( isset( $cache[$page] ) ) {
+ $length = $cache[$page];
+ } elseif( $parser->incrementExpensiveFunctionCount() ) {
+ $length = $cache[$page] = $title->getLength();
+
+ // Register dependency in templatelinks
+ $id = $title->getArticleId();
+ $revid = Revision::newFromTitle($title);
+ $parser->mOutput->addTemplate($title, $id, $revid);
+ }
+ return self::formatRaw( $length, $raw );
+ }
+
+ static function language( $parser, $arg = '' ) {
+ global $wgContLang;
+ $lang = $wgContLang->getLanguageName( strtolower( $arg ) );
+ return $lang != '' ? $lang : $arg;
+ }
+
+ static function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) {
+ $length = min( max( $length, 0 ), 500 );
+ $char = substr( $char, 0, 1 );
+ return ( $string !== '' && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 )
+ ? str_pad( $string, $length, (string)$char, $direction )
+ : $string;
+ }
+
+ static function padleft( $parser, $string = '', $length = 0, $char = 0 ) {
+ return self::pad( $string, $length, $char, STR_PAD_LEFT );
+ }
+
+ static function padright( $parser, $string = '', $length = 0, $char = 0 ) {
+ return self::pad( $string, $length, $char );
+ }
+
+ static function anchorencode( $parser, $text ) {
+ $a = urlencode( $text );
+ $a = strtr( $a, array( '%' => '.', '+' => '_' ) );
+ # leave colons alone, however
+ $a = str_replace( '.3A', ':', $a );
+ return $a;
+ }
+
+ static function special( $parser, $text ) {
+ $title = SpecialPage::getTitleForAlias( $text );
+ if ( $title ) {
+ return $title->getPrefixedText();
+ } else {
+ return wfMsgForContent( 'nosuchspecialpage' );
+ }
+ }
+
+ public static function defaultsort( $parser, $text ) {
+ $text = trim( $text );
+ if( strlen( $text ) > 0 )
+ $parser->setDefaultSort( $text );
+ return '';
+ }
+
+ public static function filepath( $parser, $name='', $option='' ) {
+ $file = wfFindFile( $name );
+ if( $file ) {
+ $url = $file->getFullUrl();
+ if( $option == 'nowiki' ) {
+ return "$url";
+ }
+ return $url;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Parser function to extension tag adaptor
+ */
+ public static function tagObj( $parser, $frame, $args ) {
+ $xpath = false;
+ if ( !count( $args ) ) {
+ return '';
+ }
+ $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) );
+
+ if ( count( $args ) ) {
+ $inner = $frame->expand( array_shift( $args ) );
+ } else {
+ $inner = null;
+ }
+
+ $stripList = $parser->getStripList();
+ if ( !in_array( $tagName, $stripList ) ) {
+ return '' .
+ wfMsg( 'unknown_extension_tag', $tagName ) .
+ '';
+ }
+
+ $attributes = array();
+ foreach ( $args as $arg ) {
+ $bits = $arg->splitArg();
+ if ( strval( $bits['index'] ) === '' ) {
+ $name = $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS );
+ $value = trim( $frame->expand( $bits['value'] ) );
+ if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
+ $value = isset( $m[1] ) ? $m[1] : '';
+ }
+ $attributes[$name] = $value;
+ }
+ }
+
+ $params = array(
+ 'name' => $tagName,
+ 'inner' => $inner,
+ 'attributes' => $attributes,
+ 'close' => "$tagName>",
+ );
+ return $parser->extensionSubstitution( $params, $frame );
+ }
+}
diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php
new file mode 100644
index 0000000000..9ef11d5e35
--- /dev/null
+++ b/includes/parser/DateFormatter.php
@@ -0,0 +1,283 @@
+monthNames = $this->getMonthRegex();
+ for ( $i=1; $i<=12; $i++ ) {
+ $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i;
+ $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i;
+ }
+
+ $this->regexTrail = '(?![a-z])/iu';
+
+ # Partial regular expressions
+ $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')]]';
+ $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})]]';
+ $this->prxY = '\[\[(\d{1,4}([ _]BC|))]]';
+ $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})]]';
+ $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]';
+
+ # Real regular expressions
+ $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}";
+ $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}";
+ $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}";
+ $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}";
+ $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}";
+ $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}";
+ $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}";
+ $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}";
+
+ # Extraction keys
+ # See the comments in replace() for the meaning of the letters
+ $this->keys[self::DMY] = 'jFY';
+ $this->keys[self::YDM] = 'Y jF';
+ $this->keys[self::MDY] = 'FjY';
+ $this->keys[self::YMD] = 'Y Fj';
+ $this->keys[self::DM] = 'jF';
+ $this->keys[self::MD] = 'Fj';
+ $this->keys[self::ISO1] = 'ymd'; # y means ISO year
+ $this->keys[self::ISO2] = 'ymd';
+
+ # Target date formats
+ $this->targets[self::DMY] = '[[F j|j F]] [[Y]]';
+ $this->targets[self::YDM] = '[[Y]], [[F j|j F]]';
+ $this->targets[self::MDY] = '[[F j]], [[Y]]';
+ $this->targets[self::YMD] = '[[Y]] [[F j]]';
+ $this->targets[self::DM] = '[[F j|j F]]';
+ $this->targets[self::MD] = '[[F j]]';
+ $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]';
+ $this->targets[self::ISO2] = '[[y-m-d]]';
+
+ # Rules
+ # pref source target
+ $this->rules[self::DMY][self::MD] = self::DM;
+ $this->rules[self::ALL][self::MD] = self::MD;
+ $this->rules[self::MDY][self::DM] = self::MD;
+ $this->rules[self::ALL][self::DM] = self::DM;
+ $this->rules[self::NONE][self::ISO2] = self::ISO1;
+
+ $this->preferences = array(
+ 'default' => self::NONE,
+ 'dmy' => self::DMY,
+ 'mdy' => self::MDY,
+ 'ymd' => self::YMD,
+ 'ISO 8601' => self::ISO1,
+ );
+ }
+
+ /**
+ * @static
+ */
+ function &getInstance() {
+ global $wgMemc;
+ static $dateFormatter = false;
+ if ( !$dateFormatter ) {
+ $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) );
+ if ( !$dateFormatter ) {
+ $dateFormatter = new DateFormatter;
+ $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 );
+ }
+ }
+ return $dateFormatter;
+ }
+
+ /**
+ * @param string $preference User preference
+ * @param string $text Text to reformat
+ */
+ function reformat( $preference, $text ) {
+ if ( isset( $this->preferences[$preference] ) ) {
+ $preference = $this->preferences[$preference];
+ } else {
+ $preference = self::NONE;
+ }
+ for ( $i=1; $i<=self::LAST; $i++ ) {
+ $this->mSource = $i;
+ if ( isset ( $this->rules[$preference][$i] ) ) {
+ # Specific rules
+ $this->mTarget = $this->rules[$preference][$i];
+ } elseif ( isset ( $this->rules[self::ALL][$i] ) ) {
+ # General rules
+ $this->mTarget = $this->rules[self::ALL][$i];
+ } elseif ( $preference ) {
+ # User preference
+ $this->mTarget = $preference;
+ } else {
+ # Default
+ $this->mTarget = $i;
+ }
+ $text = preg_replace_callback( $this->regexes[$i], array( &$this, 'replace' ), $text );
+ }
+ return $text;
+ }
+
+ /**
+ * @param $matches
+ */
+ function replace( $matches ) {
+ # Extract information from $matches
+ $bits = array();
+ $key = $this->keys[$this->mSource];
+ for ( $p=0; $p < strlen($key); $p++ ) {
+ if ( $key{$p} != ' ' ) {
+ $bits[$key{$p}] = $matches[$p+1];
+ }
+ }
+
+ $format = $this->targets[$this->mTarget];
+
+ # Construct new date
+ $text = '';
+ $fail = false;
+
+ for ( $p=0; $p < strlen( $format ); $p++ ) {
+ $char = $format{$p};
+ switch ( $char ) {
+ case 'd': # ISO day of month
+ if ( !isset($bits['d']) ) {
+ $text .= sprintf( '%02d', $bits['j'] );
+ } else {
+ $text .= $bits['d'];
+ }
+ break;
+ case 'm': # ISO month
+ if ( !isset($bits['m']) ) {
+ $m = $this->makeIsoMonth( $bits['F'] );
+ if ( !$m || $m == '00' ) {
+ $fail = true;
+ } else {
+ $text .= $m;
+ }
+ } else {
+ $text .= $bits['m'];
+ }
+ break;
+ case 'y': # ISO year
+ if ( !isset( $bits['y'] ) ) {
+ $text .= $this->makeIsoYear( $bits['Y'] );
+ } else {
+ $text .= $bits['y'];
+ }
+ break;
+ case 'j': # ordinary day of month
+ if ( !isset($bits['j']) ) {
+ $text .= intval( $bits['d'] );
+ } else {
+ $text .= $bits['j'];
+ }
+ break;
+ case 'F': # long month
+ if ( !isset( $bits['F'] ) ) {
+ $m = intval($bits['m']);
+ if ( $m > 12 || $m < 1 ) {
+ $fail = true;
+ } else {
+ global $wgContLang;
+ $text .= $wgContLang->getMonthName( $m );
+ }
+ } else {
+ $text .= ucfirst( $bits['F'] );
+ }
+ break;
+ case 'Y': # ordinary (optional BC) year
+ if ( !isset( $bits['Y'] ) ) {
+ $text .= $this->makeNormalYear( $bits['y'] );
+ } else {
+ $text .= $bits['Y'];
+ }
+ break;
+ default:
+ $text .= $char;
+ }
+ }
+ if ( $fail ) {
+ $text = $matches[0];
+ }
+ return $text;
+ }
+
+ /**
+ * @todo document
+ */
+ function getMonthRegex() {
+ global $wgContLang;
+ $names = array();
+ for( $i = 1; $i <= 12; $i++ ) {
+ $names[] = $wgContLang->getMonthName( $i );
+ $names[] = $wgContLang->getMonthAbbreviation( $i );
+ }
+ return implode( '|', $names );
+ }
+
+ /**
+ * Makes an ISO month, e.g. 02, from a month name
+ * @param $monthName String: month name
+ * @return string ISO month name
+ */
+ function makeIsoMonth( $monthName ) {
+ global $wgContLang;
+
+ $n = $this->xMonths[$wgContLang->lc( $monthName )];
+ return sprintf( '%02d', $n );
+ }
+
+ /**
+ * @todo document
+ * @param $year String: Year name
+ * @return string ISO year name
+ */
+ function makeIsoYear( $year ) {
+ # Assumes the year is in a nice format, as enforced by the regex
+ if ( substr( $year, -2 ) == 'BC' ) {
+ $num = intval(substr( $year, 0, -3 )) - 1;
+ # PHP bug note: sprintf( "%04d", -1 ) fails poorly
+ $text = sprintf( '-%04d', $num );
+
+ } else {
+ $text = sprintf( '%04d', $year );
+ }
+ return $text;
+ }
+
+ /**
+ * @todo document
+ */
+ function makeNormalYear( $iso ) {
+ if ( $iso{0} == '-' ) {
+ $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC';
+ } else {
+ $text = intval( $iso );
+ }
+ return $text;
+ }
+}
diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php
new file mode 100644
index 0000000000..4daa8f2956
--- /dev/null
+++ b/includes/parser/Parser.php
@@ -0,0 +1,4974 @@
+
+ * There are five main entry points into the Parser class:
+ * parse()
+ * produces HTML output
+ * preSaveTransform().
+ * produces altered wiki markup.
+ * preprocess()
+ * removes HTML comments and expands templates
+ * cleanSig()
+ * Cleans a signature before saving it to preferences
+ * extractSections()
+ * Extracts sections from an article for section editing
+ *
+ * Globals used:
+ * objects: $wgLang, $wgContLang
+ *
+ * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
+ *
+ * settings:
+ * $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
+ * $wgNamespacesWithSubpages, $wgAllowExternalImages*,
+ * $wgLocaltimezone, $wgAllowSpecialInclusion*,
+ * $wgMaxArticleSize*
+ *
+ * * only within ParserOptions
+ *
+ *
+ * @ingroup Parser
+ */
+class Parser
+{
+ /**
+ * Update this version number when the ParserOutput format
+ * changes in an incompatible way, so the parser cache
+ * can automatically discard old data.
+ */
+ const VERSION = '1.6.4';
+
+ # Flags for Parser::setFunctionHook
+ # Also available as global constants from Defines.php
+ const SFH_NO_HASH = 1;
+ const SFH_OBJECT_ARGS = 2;
+
+ # Constants needed for external link processing
+ # Everything except bracket, space, or control characters
+ const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
+ const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
+ \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
+
+ // State constants for the definition list colon extraction
+ const COLON_STATE_TEXT = 0;
+ const COLON_STATE_TAG = 1;
+ const COLON_STATE_TAGSTART = 2;
+ const COLON_STATE_CLOSETAG = 3;
+ const COLON_STATE_TAGSLASH = 4;
+ const COLON_STATE_COMMENT = 5;
+ const COLON_STATE_COMMENTDASH = 6;
+ const COLON_STATE_COMMENTDASHDASH = 7;
+
+ // Flags for preprocessToDom
+ const PTD_FOR_INCLUSION = 1;
+
+ // Allowed values for $this->mOutputType
+ // Parameter to startExternalParse().
+ const OT_HTML = 1;
+ const OT_WIKI = 2;
+ const OT_PREPROCESS = 3;
+ const OT_MSG = 3;
+
+ // Marker Suffix needs to be accessible staticly.
+ const MARKER_SUFFIX = "-QINU\x7f";
+
+ /**#@+
+ * @private
+ */
+ # Persistent:
+ var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
+ $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
+ $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
+
+
+ # Cleared with clearState():
+ var $mOutput, $mAutonumber, $mDTopen, $mStripState;
+ var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
+ var $mInterwikiLinkHolders, $mLinkHolders;
+ var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
+ var $mTplExpandCache; // empty-frame expansion cache
+ var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
+ var $mExpensiveFunctionCount; // number of expensive parser function calls
+
+ # Temporary
+ # These are variables reset at least once per parse regardless of $clearState
+ var $mOptions, // ParserOptions object
+ $mTitle, // Title context, used for self-link rendering and similar things
+ $mOutputType, // Output type, one of the OT_xxx constants
+ $ot, // Shortcut alias, see setOutputType()
+ $mRevisionId, // ID to display in {{REVISIONID}} tags
+ $mRevisionTimestamp, // The timestamp of the specified revision ID
+ $mRevIdForTs; // The revision ID which was used to fetch the timestamp
+
+ /**#@-*/
+
+ /**
+ * Constructor
+ *
+ * @public
+ */
+ function __construct( $conf = array() ) {
+ $this->mConf = $conf;
+ $this->mTagHooks = array();
+ $this->mTransparentTagHooks = array();
+ $this->mFunctionHooks = array();
+ $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
+ $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+ $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
+ '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
+ $this->mVarCache = array();
+ if ( isset( $conf['preprocessorClass'] ) ) {
+ $this->mPreprocessorClass = $conf['preprocessorClass'];
+ } else {
+ $this->mPreprocessorClass = 'Preprocessor_Hash';
+ }
+ $this->mMarkerIndex = 0;
+ $this->mFirstCall = true;
+ }
+
+ /**
+ * Do various kinds of initialisation on the first call of the parser
+ */
+ function firstCallInit() {
+ if ( !$this->mFirstCall ) {
+ return;
+ }
+ $this->mFirstCall = false;
+
+ wfProfileIn( __METHOD__ );
+
+ $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
+ CoreParserFunctions::register( $this );
+ $this->initialiseVariables();
+
+ wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Clear Parser state
+ *
+ * @private
+ */
+ function clearState() {
+ wfProfileIn( __METHOD__ );
+ if ( $this->mFirstCall ) {
+ $this->firstCallInit();
+ }
+ $this->mOutput = new ParserOutput;
+ $this->mAutonumber = 0;
+ $this->mLastSection = '';
+ $this->mDTopen = false;
+ $this->mIncludeCount = array();
+ $this->mStripState = new StripState;
+ $this->mArgStack = false;
+ $this->mInPre = false;
+ $this->mInterwikiLinkHolders = array(
+ 'texts' => array(),
+ 'titles' => array()
+ );
+ $this->mLinkHolders = array(
+ 'namespaces' => array(),
+ 'dbkeys' => array(),
+ 'queries' => array(),
+ 'texts' => array(),
+ 'titles' => array()
+ );
+ $this->mRevisionTimestamp = $this->mRevisionId = null;
+
+ /**
+ * Prefix for temporary replacement strings for the multipass parser.
+ * \x07 should never appear in input as it's disallowed in XML.
+ * Using it at the front also gives us a little extra robustness
+ * since it shouldn't match when butted up against identifier-like
+ * string constructs.
+ *
+ * Must not consist of all title characters, or else it will change
+ * the behaviour of in a link.
+ */
+ #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
+ # Changed to \x7f to allow XML double-parsing -- TS
+ $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+
+
+ # Clear these on every parse, bug 4549
+ $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
+
+ $this->mShowToc = true;
+ $this->mForceTocPosition = false;
+ $this->mIncludeSizes = array(
+ 'post-expand' => 0,
+ 'arg' => 0,
+ );
+ $this->mPPNodeCount = 0;
+ $this->mDefaultSort = false;
+ $this->mHeadings = array();
+ $this->mDoubleUnderscores = array();
+ $this->mExpensiveFunctionCount = 0;
+
+ # Fix cloning
+ if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
+ $this->mPreprocessor = null;
+ }
+
+ wfRunHooks( 'ParserClearState', array( &$this ) );
+ wfProfileOut( __METHOD__ );
+ }
+
+ function setOutputType( $ot ) {
+ $this->mOutputType = $ot;
+ // Shortcut alias
+ $this->ot = array(
+ 'html' => $ot == self::OT_HTML,
+ 'wiki' => $ot == self::OT_WIKI,
+ 'pre' => $ot == self::OT_PREPROCESS,
+ );
+ }
+
+ /**
+ * Set the context title
+ */
+ function setTitle( $t ) {
+ if ( !$t || $t instanceof FakeTitle ) {
+ $t = Title::newFromText( 'NO TITLE' );
+ }
+ if ( strval( $t->getFragment() ) !== '' ) {
+ # Strip the fragment to avoid various odd effects
+ $this->mTitle = clone $t;
+ $this->mTitle->setFragment( '' );
+ } else {
+ $this->mTitle = $t;
+ }
+ }
+
+ /**
+ * Accessor for mUniqPrefix.
+ *
+ * @public
+ */
+ function uniqPrefix() {
+ if( !isset( $this->mUniqPrefix ) ) {
+ // @fixme this is probably *horribly wrong*
+ // LanguageConverter seems to want $wgParser's uniqPrefix, however
+ // if this is called for a parser cache hit, the parser may not
+ // have ever been initialized in the first place.
+ // Not really sure what the heck is supposed to be going on here.
+ return '';
+ //throw new MWException( "Accessing uninitialized mUniqPrefix" );
+ }
+ return $this->mUniqPrefix;
+ }
+
+ /**
+ * Convert wikitext to HTML
+ * Do not call this function recursively.
+ *
+ * @param string $text Text we want to parse
+ * @param Title &$title A title object
+ * @param array $options
+ * @param boolean $linestart
+ * @param boolean $clearState
+ * @param int $revid number to pass in {{REVISIONID}}
+ * @return ParserOutput a ParserOutput
+ */
+ public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
+ /**
+ * First pass--just handle sections, pass the rest off
+ * to internalParse() which does all the real work.
+ */
+
+ global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
+ $fname = 'Parser::parse-' . wfGetCaller();
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( $fname );
+
+ if ( $clearState ) {
+ $this->clearState();
+ }
+
+ $this->mOptions = $options;
+ $this->setTitle( $title );
+ $oldRevisionId = $this->mRevisionId;
+ $oldRevisionTimestamp = $this->mRevisionTimestamp;
+ if( $revid !== null ) {
+ $this->mRevisionId = $revid;
+ $this->mRevisionTimestamp = null;
+ }
+ $this->setOutputType( self::OT_HTML );
+ wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+ # No more strip!
+ wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+ $text = $this->internalParse( $text );
+ $text = $this->mStripState->unstripGeneral( $text );
+
+ # Clean up special characters, only run once, next-to-last before doBlockLevels
+ $fixtags = array(
+ # french spaces, last one Guillemet-left
+ # only if there is something before the space
+ '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2',
+ # french spaces, Guillemet-right
+ '/(\\302\\253) /' => '\\1 ',
+ '/ (!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
+ );
+ $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
+
+ # only once and last
+ $text = $this->doBlockLevels( $text, $linestart );
+
+ $this->replaceLinkHolders( $text );
+
+ # the position of the parserConvert() call should not be changed. it
+ # assumes that the links are all replaced and the only thing left
+ # is the mark.
+ # Side-effects: this calls $this->mOutput->setTitleText()
+ $text = $wgContLang->parserConvert( $text, $this );
+
+ $text = $this->mStripState->unstripNoWiki( $text );
+
+ wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
+
+//!JF Move to its own function
+
+ $uniq_prefix = $this->mUniqPrefix;
+ $matches = array();
+ $elements = array_keys( $this->mTransparentTagHooks );
+ $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+ foreach( $matches as $marker => $data ) {
+ list( $element, $content, $params, $tag ) = $data;
+ $tagName = strtolower( $element );
+ if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
+ $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
+ array( $content, $params, $this ) );
+ } else {
+ $output = $tag;
+ }
+ $this->mStripState->general->setPair( $marker, $output );
+ }
+ $text = $this->mStripState->unstripGeneral( $text );
+
+ $text = Sanitizer::normalizeCharReferences( $text );
+
+ if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
+ $text = Parser::tidy($text);
+ } else {
+ # attempt to sanitize at least some nesting problems
+ # (bug #2702 and quite a few others)
+ $tidyregs = array(
+ # ''Something [http://www.cool.com cool''] -->
+ # Somethingcool>
+ '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
+ '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
+ # fix up an anchor inside another anchor, only
+ # at least for a single single nested link (bug 3695)
+ '/(]+>)([^<]*)(]+>[^<]*)<\/a>(.*)<\/a>/' =>
+ '\\1\\2\\3\\1\\4',
+ # fix div inside inline elements- doBlockLevels won't wrap a line which
+ # contains a div, so fix it up here; replace
+ # div with escaped text
+ '/(<([aib]) [^>]+>)([^<]*)(
]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
+ '\\1\\3<div\\5>\\6</div>\\8\\9',
+ # remove empty italic or bold tag pairs, some
+ # introduced by rules above
+ '/<([bi])><\/\\1>/' => '',
+ );
+
+ $text = preg_replace(
+ array_keys( $tidyregs ),
+ array_values( $tidyregs ),
+ $text );
+ }
+ global $wgExpensiveParserFunctionLimit;
+ if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
+ $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
+ }
+
+ wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
+
+ # Information on include size limits, for the benefit of users who try to skirt them
+ if ( $this->mOptions->getEnableLimitReport() ) {
+ global $wgExpensiveParserFunctionLimit;
+ $max = $this->mOptions->getMaxIncludeSize();
+ $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
+ $limitReport =
+ "NewPP limit report\n" .
+ "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
+ "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
+ "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
+ $PFreport;
+ wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
+ $text .= "\n\n";
+ }
+ $this->mOutput->setText( $text );
+ $this->mRevisionId = $oldRevisionId;
+ $this->mRevisionTimestamp = $oldRevisionTimestamp;
+ wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
+
+ return $this->mOutput;
+ }
+
+ /**
+ * Recursive parser entry point that can be called from an extension tag
+ * hook.
+ */
+ function recursiveTagParse( $text ) {
+ wfProfileIn( __METHOD__ );
+ wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+ wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+ $text = $this->internalParse( $text );
+ wfProfileOut( __METHOD__ );
+ return $text;
+ }
+
+ /**
+ * Expand templates and variables in the text, producing valid, static wikitext.
+ * Also removes comments.
+ */
+ function preprocess( $text, $title, $options, $revid = null ) {
+ wfProfileIn( __METHOD__ );
+ $this->clearState();
+ $this->setOutputType( self::OT_PREPROCESS );
+ $this->mOptions = $options;
+ $this->setTitle( $title );
+ if( $revid !== null ) {
+ $this->mRevisionId = $revid;
+ }
+ wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+ wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+ $text = $this->replaceVariables( $text );
+ $text = $this->mStripState->unstripBoth( $text );
+ wfProfileOut( __METHOD__ );
+ return $text;
+ }
+
+ /**
+ * Get a random string
+ *
+ * @private
+ * @static
+ */
+ function getRandomString() {
+ return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
+ }
+
+ function &getTitle() { return $this->mTitle; }
+ function getOptions() { return $this->mOptions; }
+ function getRevisionId() { return $this->mRevisionId; }
+
+ function getFunctionLang() {
+ global $wgLang, $wgContLang;
+
+ $target = $this->mOptions->getTargetLanguage();
+ if ( $target !== null ) {
+ return $target;
+ } else {
+ return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
+ }
+ }
+
+ /**
+ * Get a preprocessor object
+ */
+ function getPreprocessor() {
+ if ( !isset( $this->mPreprocessor ) ) {
+ $class = $this->mPreprocessorClass;
+ $this->mPreprocessor = new $class( $this );
+ }
+ return $this->mPreprocessor;
+ }
+
+ /**
+ * Replaces all occurrences of HTML-style comments and the given tags
+ * in the text with a random marker and returns the next text. The output
+ * parameter $matches will be an associative array filled with data in
+ * the form:
+ * 'UNIQ-xxxxx' => array(
+ * 'element',
+ * 'tag content',
+ * array( 'param' => 'x' ),
+ * 'tag content' ) )
+ *
+ * @param $elements list of element names. Comments are always extracted.
+ * @param $text Source text string.
+ * @param $uniq_prefix
+ *
+ * @public
+ * @static
+ */
+ function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
+ static $n = 1;
+ $stripped = '';
+ $matches = array();
+
+ $taglist = implode( '|', $elements );
+ $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
+
+ while ( '' != $text ) {
+ $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
+ $stripped .= $p[0];
+ if( count( $p ) < 5 ) {
+ break;
+ }
+ if( count( $p ) > 5 ) {
+ // comment
+ $element = $p[4];
+ $attributes = '';
+ $close = '';
+ $inside = $p[5];
+ } else {
+ // tag
+ $element = $p[1];
+ $attributes = $p[2];
+ $close = $p[3];
+ $inside = $p[4];
+ }
+
+ $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . self::MARKER_SUFFIX;
+ $stripped .= $marker;
+
+ if ( $close === '/>' ) {
+ // Empty element tag,
+ $content = null;
+ $text = $inside;
+ $tail = null;
+ } else {
+ if( $element == '!--' ) {
+ $end = '/(-->)/';
+ } else {
+ $end = "/(<\\/$element\\s*>)/i";
+ }
+ $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
+ $content = $q[0];
+ if( count( $q ) < 3 ) {
+ # No end tag -- let it run out to the end of the text.
+ $tail = '';
+ $text = '';
+ } else {
+ $tail = $q[1];
+ $text = $q[2];
+ }
+ }
+
+ $matches[$marker] = array( $element,
+ $content,
+ Sanitizer::decodeTagAttributes( $attributes ),
+ "<$element$attributes$close$content$tail" );
+ }
+ return $stripped;
+ }
+
+ /**
+ * Get a list of strippable XML-like elements
+ */
+ function getStripList() {
+ global $wgRawHtml;
+ $elements = $this->mStripList;
+ if( $wgRawHtml ) {
+ $elements[] = 'html';
+ }
+ if( $this->mOptions->getUseTeX() ) {
+ $elements[] = 'math';
+ }
+ return $elements;
+ }
+
+ /**
+ * @deprecated use replaceVariables
+ */
+ function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
+ return $text;
+ }
+
+ /**
+ * Restores pre, math, and other extensions removed by strip()
+ *
+ * always call unstripNoWiki() after this one
+ * @private
+ * @deprecated use $this->mStripState->unstrip()
+ */
+ function unstrip( $text, $state ) {
+ return $state->unstripGeneral( $text );
+ }
+
+ /**
+ * Always call this after unstrip() to preserve the order
+ *
+ * @private
+ * @deprecated use $this->mStripState->unstrip()
+ */
+ function unstripNoWiki( $text, $state ) {
+ return $state->unstripNoWiki( $text );
+ }
+
+ /**
+ * @deprecated use $this->mStripState->unstripBoth()
+ */
+ function unstripForHTML( $text ) {
+ return $this->mStripState->unstripBoth( $text );
+ }
+
+ /**
+ * Add an item to the strip state
+ * Returns the unique tag which must be inserted into the stripped text
+ * The tag will be replaced with the original text in unstrip()
+ *
+ * @private
+ */
+ function insertStripItem( $text ) {
+ $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
+ $this->mMarkerIndex++;
+ $this->mStripState->general->setPair( $rnd, $text );
+ return $rnd;
+ }
+
+ /**
+ * Interface with html tidy, used if $wgUseTidy = true.
+ * If tidy isn't able to correct the markup, the original will be
+ * returned in all its glory with a warning comment appended.
+ *
+ * Either the external tidy program or the in-process tidy extension
+ * will be used depending on availability. Override the default
+ * $wgTidyInternal setting to disable the internal if it's not working.
+ *
+ * @param string $text Hideous HTML input
+ * @return string Corrected HTML output
+ * @public
+ * @static
+ */
+ function tidy( $text ) {
+ global $wgTidyInternal;
+ $wrappedtext = ''.
+'test'.$text.'';
+ if( $wgTidyInternal ) {
+ $correctedtext = Parser::internalTidy( $wrappedtext );
+ } else {
+ $correctedtext = Parser::externalTidy( $wrappedtext );
+ }
+ if( is_null( $correctedtext ) ) {
+ wfDebug( "Tidy error detected!\n" );
+ return $text . "\n\n";
+ }
+ return $correctedtext;
+ }
+
+ /**
+ * Spawn an external HTML tidy process and get corrected markup back from it.
+ *
+ * @private
+ * @static
+ */
+ function externalTidy( $text ) {
+ global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
+ $fname = 'Parser::externalTidy';
+ wfProfileIn( $fname );
+
+ $cleansource = '';
+ $opts = ' -utf8';
+
+ $descriptorspec = array(
+ 0 => array('pipe', 'r'),
+ 1 => array('pipe', 'w'),
+ 2 => array('file', wfGetNull(), 'a')
+ );
+ $pipes = array();
+ $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
+ if (is_resource($process)) {
+ // Theoretically, this style of communication could cause a deadlock
+ // here. If the stdout buffer fills up, then writes to stdin could
+ // block. This doesn't appear to happen with tidy, because tidy only
+ // writes to stdout after it's finished reading from stdin. Search
+ // for tidyParseStdin and tidySaveStdout in console/tidy.c
+ fwrite($pipes[0], $text);
+ fclose($pipes[0]);
+ while (!feof($pipes[1])) {
+ $cleansource .= fgets($pipes[1], 1024);
+ }
+ fclose($pipes[1]);
+ proc_close($process);
+ }
+
+ wfProfileOut( $fname );
+
+ if( $cleansource == '' && $text != '') {
+ // Some kind of error happened, so we couldn't get the corrected text.
+ // Just give up; we'll use the source text and append a warning.
+ return null;
+ } else {
+ return $cleansource;
+ }
+ }
+
+ /**
+ * Use the HTML tidy PECL extension to use the tidy library in-process,
+ * saving the overhead of spawning a new process.
+ *
+ * 'pear install tidy' should be able to compile the extension module.
+ *
+ * @private
+ * @static
+ */
+ function internalTidy( $text ) {
+ global $wgTidyConf, $IP, $wgDebugTidy;
+ $fname = 'Parser::internalTidy';
+ wfProfileIn( $fname );
+
+ $tidy = new tidy;
+ $tidy->parseString( $text, $wgTidyConf, 'utf8' );
+ $tidy->cleanRepair();
+ if( $tidy->getStatus() == 2 ) {
+ // 2 is magic number for fatal error
+ // http://www.php.net/manual/en/function.tidy-get-status.php
+ $cleansource = null;
+ } else {
+ $cleansource = tidy_get_output( $tidy );
+ }
+ if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
+ $cleansource .= "', '-->', $tidy->errorBuffer ) .
+ "\n-->";
+ }
+
+ wfProfileOut( $fname );
+ return $cleansource;
+ }
+
+ /**
+ * parse the wiki syntax used to render tables
+ *
+ * @private
+ */
+ function doTableStuff ( $text ) {
+ $fname = 'Parser::doTableStuff';
+ wfProfileIn( $fname );
+
+ $lines = explode ( "\n" , $text );
+ $td_history = array (); // Is currently a td tag open?
+ $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
+ $tr_history = array (); // Is currently a tr tag open?
+ $tr_attributes = array (); // history of tr attributes
+ $has_opened_tr = array(); // Did this table open a
element?
+ $indent_level = 0; // indent level of the table
+ foreach ( $lines as $key => $line )
+ {
+ $line = trim ( $line );
+
+ if( $line == '' ) { // empty line, go to next line
+ continue;
+ }
+ $first_character = $line{0};
+ $matches = array();
+
+ if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
+ // First check if we are starting a new table
+ $indent_level = strlen( $matches[1] );
+
+ $attributes = $this->mStripState->unstripBoth( $matches[2] );
+ $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
+
+ $lines[$key] = str_repeat( '
' , $indent_level ) . "
";
+ array_push ( $td_history , false );
+ array_push ( $last_tag_history , '' );
+ array_push ( $tr_history , false );
+ array_push ( $tr_attributes , '' );
+ array_push ( $has_opened_tr , false );
+ } else if ( count ( $td_history ) == 0 ) {
+ // Don't do any of the following
+ continue;
+ } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
+ // We are ending a table
+ $line = '
+
+
+END
+);
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+ global $wgReadOnlyFile;
+
+ if ( ! $wgRequest->getCheck( 'wpLockConfirm' ) ) {
+ $this->showForm( wfMsg( 'locknoconfirm' ) );
+ return;
+ }
+ $fp = @fopen( $wgReadOnlyFile, 'w' );
+
+ if ( false === $fp ) {
+ # This used to show a file not found error, but the likeliest reason for fopen()
+ # to fail at this point is insufficient permission to write to the file...good old
+ # is_writable() is plain wrong in some cases, it seems...
+ self::notWritable();
+ return;
+ }
+ fwrite( $fp, $this->reason );
+ fwrite( $fp, "\n
(by " . $wgUser->getName() . " at " .
+ $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
+ fclose( $fp );
+
+ $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
+ $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) );
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+ $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
+ $wgOut->addWikiMsg( 'lockdbsuccesstext' );
+ }
+
+ public static function notWritable() {
+ global $wgOut;
+ $wgOut->showErrorPage( 'lockdb', 'lockfilenotwritable' );
+ }
+}
diff --git a/includes/specials/Log.php b/includes/specials/Log.php
new file mode 100644
index 0000000000..3154ed1335
--- /dev/null
+++ b/includes/specials/Log.php
@@ -0,0 +1,65 @@
+getVal( 'type', $par );
+ $user = $wgRequest->getText( 'user' );
+ $title = $wgRequest->getText( 'page' );
+ $pattern = $wgRequest->getBool( 'pattern' );
+ $y = $wgRequest->getIntOrNull( 'year' );
+ $m = $wgRequest->getIntOrNull( 'month' );
+ # Don't let the user get stuck with a certain date
+ $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+ if( $skip ) {
+ $y = '';
+ $m = '';
+ }
+ # Create a LogPager item to get the results and a LogEventsList
+ # item to format them...
+ $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
+ $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m );
+ # Set title and add header
+ $loglist->showHeader( $pager->getType() );
+ # Show form options
+ $loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(),
+ $pager->getYear(), $pager->getMonth() );
+ # Insert list
+ $logBody = $pager->getBody();
+ if( $logBody ) {
+ $wgOut->addHTML(
+ $pager->getNavigationBar() .
+ $loglist->beginLogEventsList() .
+ $logBody .
+ $loglist->endLogEventsList() .
+ $pager->getNavigationBar()
+ );
+ } else {
+ $wgOut->addWikiMsg( 'logempty' );
+ }
+}
diff --git a/includes/specials/Lonelypages.php b/includes/specials/Lonelypages.php
new file mode 100644
index 0000000000..5aafac7dd5
--- /dev/null
+++ b/includes/specials/Lonelypages.php
@@ -0,0 +1,58 @@
+tableNamesN( 'page', 'pagelinks' );
+
+ return
+ "SELECT 'Lonelypages' AS type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $pagelinks
+ ON page_namespace=pl_namespace AND page_title=pl_title
+ WHERE pl_namespace IS NULL
+ AND page_namespace=".NS_MAIN."
+ AND page_is_redirect=0";
+
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialLonelypages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new LonelyPagesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Longpages.php b/includes/specials/Longpages.php
new file mode 100644
index 0000000000..be16a02942
--- /dev/null
+++ b/includes/specials/Longpages.php
@@ -0,0 +1,31 @@
+doQuery( $offset, $limit );
+}
diff --git a/includes/specials/MIMEsearch.php b/includes/specials/MIMEsearch.php
new file mode 100644
index 0000000000..82ee4be6e1
--- /dev/null
+++ b/includes/specials/MIMEsearch.php
@@ -0,0 +1,138 @@
+
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Searches the database for files of the requested MIME type, comparing this with the
+ * 'img_major_mime' and 'img_minor_mime' fields in the image table.
+ * @ingroup SpecialPage
+ */
+class MIMEsearchPage extends QueryPage {
+ var $major, $minor;
+
+ function MIMEsearchPage( $major, $minor ) {
+ $this->major = $major;
+ $this->minor = $minor;
+ }
+
+ function getName() { return 'MIMEsearch'; }
+
+ /**
+ * Due to this page relying upon extra fields being passed in the SELECT it
+ * will fail if it's set as expensive and misermode is on
+ */
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function linkParameters() {
+ $arr = array( $this->major, $this->minor );
+ $mime = implode( '/', $arr );
+ return array( 'mime' => $mime );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $major = $dbr->addQuotes( $this->major );
+ $minor = $dbr->addQuotes( $this->minor );
+
+ return
+ "SELECT 'MIMEsearch' AS type,
+ " . NS_IMAGE . " AS namespace,
+ img_name AS title,
+ img_major_mime AS value,
+
+ img_size,
+ img_width,
+ img_height,
+ img_user_text,
+ img_timestamp
+ FROM $image
+ WHERE img_major_mime = $major AND img_minor_mime = $minor
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang, $wgLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+ $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+
+ $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) );
+ $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->img_size ) );
+ $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ),
+ $wgLang->formatNum( $result->img_height ) );
+ $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+ $time = $wgLang->timeanddate( $result->img_timestamp );
+
+ return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
+ }
+}
+
+/**
+ * Output the HTML search form, and constructs the MIMEsearchPage object.
+ */
+function wfSpecialMIMEsearch( $par = null ) {
+ global $wgRequest, $wgTitle, $wgOut;
+
+ $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
+ Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
+ Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+
+ list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
+ if ( $major == '' or $minor == '' or !wfSpecialMIMEsearchValidType( $major ) )
+ return;
+ $wpp = new MIMEsearchPage( $major, $minor );
+
+ list( $limit, $offset ) = wfCheckLimits();
+ $wpp->doQuery( $offset, $limit );
+}
+
+function wfSpecialMIMEsearchParse( $str ) {
+ // searched for an invalid MIME type.
+ if( strpos( $str, '/' ) === false) {
+ return array ('', '');
+ }
+
+ list( $major, $minor ) = explode( '/', $str, 2 );
+
+ return array(
+ ltrim( $major, ' ' ),
+ rtrim( $minor, ' ' )
+ );
+}
+
+function wfSpecialMIMEsearchValidType( $type ) {
+ // From maintenance/tables.sql => img_major_mime
+ $types = array(
+ 'unknown',
+ 'application',
+ 'audio',
+ 'image',
+ 'text',
+ 'video',
+ 'message',
+ 'model',
+ 'multipart'
+ );
+
+ return in_array( $type, $types );
+}
diff --git a/includes/specials/MergeHistory.php b/includes/specials/MergeHistory.php
new file mode 100644
index 0000000000..6183374ee4
--- /dev/null
+++ b/includes/specials/MergeHistory.php
@@ -0,0 +1,451 @@
+execute();
+}
+
+/**
+ * The HTML form for Special:MergeHistory, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @ingroup SpecialPage
+ */
+class MergehistoryForm {
+ var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
+ var $mTargetObj, $mDestObj;
+
+ function MergehistoryForm( $request, $par = "" ) {
+ global $wgUser;
+
+ $this->mAction = $request->getVal( 'action' );
+ $this->mTarget = $request->getVal( 'target' );
+ $this->mDest = $request->getVal( 'dest' );
+ $this->mSubmitted = $request->getBool( 'submitted' );
+
+ $this->mTargetID = intval( $request->getVal( 'targetID' ) );
+ $this->mDestID = intval( $request->getVal( 'destID' ) );
+ $this->mTimestamp = $request->getVal( 'mergepoint' );
+ if( !preg_match("/[0-9]{14}/",$this->mTimestamp) ) {
+ $this->mTimestamp = '';
+ }
+ $this->mComment = $request->getText( 'wpComment' );
+
+ $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+ // target page
+ if( $this->mSubmitted ) {
+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
+ $this->mDestObj = Title::newFromURL( $this->mDest );
+ } else {
+ $this->mTargetObj = null;
+ $this->mDestObj = null;
+ }
+
+ $this->preCacheMessages();
+ }
+
+ /**
+ * As we use the same small set of messages in various methods and that
+ * they are called often, we call them once and save them in $this->message
+ */
+ function preCacheMessages() {
+ // Precache various messages
+ if( !isset( $this->message ) ) {
+ $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
+ }
+ }
+
+ function execute() {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
+
+ if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
+ return $this->merge();
+ }
+
+ if ( !$this->mSubmitted ) {
+ $this->showMergeForm();
+ return;
+ }
+
+ $errors = array();
+ if ( !$this->mTargetObj instanceof Title ) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
+ } elseif( !$this->mTargetObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
+ wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
+ );
+ }
+
+ if ( !$this->mDestObj instanceof Title) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
+ } elseif( !$this->mDestObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
+ wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
+ );
+ }
+
+ // TODO: warn about target = dest?
+
+ if ( count( $errors ) ) {
+ $this->showMergeForm();
+ $wgOut->addHTML( implode( "\n", $errors ) );
+ } else {
+ $this->showHistory();
+ }
+
+ }
+
+ function showMergeForm() {
+ global $wgOut, $wgScript;
+
+ $wgOut->addWikiMsg( 'mergehistory-header' );
+
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array(
+ 'method' => 'get',
+ 'action' => $wgScript ) ) .
+ '
\n";
+
+ // work out custom project captions
+ $customCaptions = array();
+ $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line :
+ foreach($customLines as $line){
+ $parts = explode(":",$line,2);
+ if(count($parts) == 2) // validate line
+ $customCaptions[$parts[0]] = $parts[1];
+ }
+
+
+ $prev = null;
+ while( $result = $matches->next() ) {
+ $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
+ $prev = $result->getInterwikiPrefix();
+ }
+ // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
+ $out .= "
\n";
+
+ // convert the whole thing to desired language variant
+ global $wgContLang;
+ $out = $wgContLang->convert( $out );
+ wfProfileOut( $fname );
+ return $out;
+ }
+
+ /**
+ * Show single interwiki link
+ *
+ * @param SearchResult $result
+ * @param string $lastInterwiki
+ * @param array $terms
+ * @param string $query
+ * @param array $customCaptions iw prefix -> caption
+ */
+ function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
+ $fname = 'SpecialSearch::showInterwikiHit';
+ wfProfileIn( $fname );
+ global $wgUser, $wgContLang, $wgLang;
+
+ if( $result->isBrokenTitle() ) {
+ wfProfileOut( $fname );
+ return "\n";
+ }
+
+ $t = $result->getTitle();
+ $sk = $wgUser->getSkin();
+
+ $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+
+ // format redirect if any
+ $redirectTitle = $result->getRedirectTitle();
+ $redirectText = $result->getRedirectSnippet($terms);
+ $redirect = '';
+ if( !is_null($redirectTitle) )
+ $redirect = ""
+ .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+ ."";
+
+ $out = "";
+ // display project name
+ if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){
+ if( key_exists($t->getInterwiki(),$customCaptions) )
+ // captions from 'search-interwiki-custom'
+ $caption = $customCaptions[$t->getInterwiki()];
+ else{
+ // default is to show the hostname of the other wiki which might suck
+ // if there are many wikis on one hostname
+ $parsed = parse_url($t->getFullURL());
+ $caption = wfMsg('search-interwiki-default', $parsed['host']);
+ }
+ // "more results" link (special page stuff could be localized, but we might not know target lang)
+ $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
+ $searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
+ wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search')));
+ $out .= "
';
+ break;
+ }
+ }
+
+ if ( $file->wasDeleted() && !$file->exists() ) {
+ # If the file existed before and was deleted, warn the user of this
+ # Don't bother doing so if the file exists now, however
+ $ltitle = SpecialPage::getTitleFor( 'Log' );
+ $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
+ 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
+ $warning .= '
' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '
';
+ }
+ return $warning;
+ }
+
+ /**
+ * Get a list of warnings
+ *
+ * @param string local filename, e.g. 'file exists', 'non-descriptive filename'
+ * @return array list of warning messages
+ */
+ static function ajaxGetExistsWarning( $filename ) {
+ $file = wfFindFile( $filename );
+ if( !$file ) {
+ // Force local file so we have an object to do further checks against
+ // if there isn't an exact match...
+ $file = wfLocalFile( $filename );
+ }
+ $s = ' ';
+ if ( $file ) {
+ $warning = self::getExistsWarning( $file );
+ if ( $warning !== '' ) {
+ $s = "
$warning
";
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Render a preview of a given license for the AJAX preview on upload
+ *
+ * @param string $license
+ * @return string
+ */
+ public static function ajaxGetLicensePreview( $license ) {
+ global $wgParser, $wgUser;
+ $text = '{{' . $license . '}}';
+ $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
+ $options = ParserOptions::newFromUser( $wgUser );
+
+ // Expand subst: first, then live templates...
+ $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
+ $output = $wgParser->parse( $text, $title, $options );
+
+ return $output->getText();
+ }
+
+ /**
+ * Check for duplicate files and throw up a warning before the upload
+ * completes.
+ */
+ function getDupeWarning( $tempfile ) {
+ $hash = File::sha1Base36( $tempfile );
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ if( $dupes ) {
+ global $wgOut;
+ $msg = "";
+ foreach( $dupes as $file ) {
+ $title = $file->getTitle();
+ $msg .= $title->getPrefixedText() .
+ "|" . $title->getText() . "\n";
+ }
+ $msg .= "";
+ return "
\n";
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
+ *
+ * @return array list of prefixes
+ */
+ public static function getFilenamePrefixBlacklist() {
+ $blacklist = array();
+ $message = wfMsgForContent( 'filename-prefix-blacklist' );
+ if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
+ $lines = explode( "\n", $message );
+ foreach( $lines as $line ) {
+ // Remove comment lines
+ $comment = substr( trim( $line ), 0, 1 );
+ if ( $comment == '#' || $comment == '' ) {
+ continue;
+ }
+ // Remove additional comments after a prefix
+ $comment = strpos( $line, '#' );
+ if ( $comment > 0 ) {
+ $line = substr( $line, 0, $comment-1 );
+ }
+ $blacklist[] = trim( $line );
+ }
+ }
+ return $blacklist;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing
+ * after the user has confirmed it.
+ *
+ * If the user doesn't explicitly cancel or accept, these files
+ * can accumulate in the temp directory.
+ *
+ * @param string $saveName - the destination filename
+ * @param string $tempName - the source temporary file to save
+ * @return string - full path the stashed file, or false on failure
+ * @access private
+ */
+ function saveTempUploadedFile( $saveName, $tempName ) {
+ global $wgOut;
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $status = $repo->storeTemp( $saveName, $tempName );
+ if ( !$status->isGood() ) {
+ $this->showError( $status->getWikiText() );
+ return false;
+ } else {
+ return $status->value;
+ }
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing,
+ * and save the necessary descriptive info into the session.
+ * Returns a key value which will be passed through a form
+ * to pick up the path info on a later invocation.
+ *
+ * @return int
+ * @access private
+ */
+ function stashSession() {
+ $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
+
+ if( !$stash ) {
+ # Couldn't save the file.
+ return false;
+ }
+
+ $key = mt_rand( 0, 0x7fffffff );
+ $_SESSION['wsUploadData'][$key] = array(
+ 'mTempPath' => $stash,
+ 'mFileSize' => $this->mFileSize,
+ 'mSrcName' => $this->mSrcName,
+ 'mFileProps' => $this->mFileProps,
+ 'version' => self::SESSION_VERSION,
+ );
+ return $key;
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @access private
+ * @return success
+ */
+ function unsaveUploadedFile() {
+ global $wgOut;
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $success = $repo->freeTemp( $this->mTempPath );
+ if ( ! $success ) {
+ $wgOut->showFileDeleteError( $this->mTempPath );
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * @param string $error as HTML
+ * @access private
+ */
+ function uploadError( $error ) {
+ global $wgOut;
+ $wgOut->addHTML( '
' . wfMsgHtml( 'uploadwarning' ) . "
\n" );
+ $wgOut->addHTML( '' . $error . '' );
+ }
+
+ /**
+ * There's something wrong with this file, not enough to reject it
+ * totally but we require manual intervention to save it for real.
+ * Stash it away, then present a form asking to confirm or cancel.
+ *
+ * @param string $warning as HTML
+ * @access private
+ */
+ function uploadWarning( $warning ) {
+ global $wgOut;
+ global $wgUseCopyrightUpload;
+
+ $this->mSessionKey = $this->stashSession();
+ if( !$this->mSessionKey ) {
+ # Couldn't save file; an error has been displayed so let's go.
+ return;
+ }
+
+ $wgOut->addHTML( '
\n" );
+
+ # Print a list of allowed file extensions, if so configured. We ignore
+ # MIME type here, it's incomprehensible to most people and too long.
+ global $wgCheckFileExtensions, $wgStrictFileExtensions,
+ $wgFileExtensions, $wgFileBlacklist;
+
+ $allowedExtensions = '';
+ if( $wgCheckFileExtensions ) {
+ $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
+ if( $wgStrictFileExtensions ) {
+ # Everything not permitted is banned
+ $extensionsList =
+ '
" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+ $uploadfooter = wfMsgNoTrans( 'uploadfooter' );
+ if( $uploadfooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadfooter ) ){
+ $wgOut->addWikiText( '' );
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * See if we should check the 'watch this page' checkbox on the form
+ * based on the user's preferences and whether we're being asked
+ * to create a new file or update an existing one.
+ *
+ * In the case where 'watch edits' is off but 'watch creations' is on,
+ * we'll leave the box unchecked.
+ *
+ * Note that the page target can be changed *on the form*, so our check
+ * state can get out of sync.
+ */
+ function watchCheck() {
+ global $wgUser;
+ if( $wgUser->getOption( 'watchdefault' ) ) {
+ // Watch all edits!
+ return true;
+ }
+
+ $local = wfLocalFile( $this->mDesiredDestName );
+ if( $local && $local->exists() ) {
+ // We're uploading a new version of an existing file.
+ // No creation, so don't watch it if we're not already.
+ return $local->getTitle()->userIsWatching();
+ } else {
+ // New page should get watched if that's our option.
+ return $wgUser->getOption( 'watchcreations' );
+ }
+ }
+
+ /**
+ * Split a file into a base name and all dot-delimited 'extensions'
+ * on the end. Some web server configurations will fall back to
+ * earlier pseudo-'extensions' to determine type and execute
+ * scripts, so the blacklist needs to check them all.
+ *
+ * @return array
+ */
+ function splitExtensions( $filename ) {
+ $bits = explode( '.', $filename );
+ $basename = array_shift( $bits );
+ return array( $basename, $bits );
+ }
+
+ /**
+ * Perform case-insensitive match against a list of file extensions.
+ * Returns true if the extension is in the list.
+ *
+ * @param string $ext
+ * @param array $list
+ * @return bool
+ */
+ function checkFileExtension( $ext, $list ) {
+ return in_array( strtolower( $ext ), $list );
+ }
+
+ /**
+ * Perform case-insensitive match against a list of file extensions.
+ * Returns true if any of the extensions are in the list.
+ *
+ * @param array $ext
+ * @param array $list
+ * @return bool
+ */
+ function checkFileExtensionList( $ext, $list ) {
+ foreach( $ext as $e ) {
+ if( in_array( strtolower( $e ), $list ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Verifies that it's ok to include the uploaded file
+ *
+ * @param string $tmpfile the full path of the temporary file to verify
+ * @param string $extension The filename extension that the file is to be served with
+ * @return mixed true of the file is verified, a WikiError object otherwise.
+ */
+ function verify( $tmpfile, $extension ) {
+ #magically determine mime type
+ $magic = MimeMagic::singleton();
+ $mime = $magic->guessMimeType($tmpfile,false);
+
+ #check mime type, if desired
+ global $wgVerifyMimeType;
+ if ($wgVerifyMimeType) {
+
+ wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n");
+ #check mime type against file extension
+ if( !$this->verifyExtension( $mime, $extension ) ) {
+ return new WikiErrorMsg( 'uploadcorrupt' );
+ }
+
+ #check mime type blacklist
+ global $wgMimeTypeBlacklist;
+ if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
+ && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+ return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
+ }
+ }
+
+ #check for htmlish code and javascript
+ if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
+ return new WikiErrorMsg( 'uploadscripted' );
+ }
+
+ /**
+ * Scan the uploaded file for viruses
+ */
+ $virus= $this->detectVirus($tmpfile);
+ if ( $virus ) {
+ return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
+ }
+
+ wfDebug( __METHOD__.": all clear; passing.\n" );
+ return true;
+ }
+
+ /**
+ * Checks if the mime type of the uploaded file matches the file extension.
+ *
+ * @param string $mime the mime type of the uploaded file
+ * @param string $extension The filename extension that the file is to be served with
+ * @return bool
+ */
+ function verifyExtension( $mime, $extension ) {
+ $magic = MimeMagic::singleton();
+
+ if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
+ if ( ! $magic->isRecognizableExtension( $extension ) ) {
+ wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
+ "unrecognized extension '$extension', can't verify\n" );
+ return true;
+ } else {
+ wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
+ "recognized extension '$extension', so probably invalid file\n" );
+ return false;
+ }
+
+ $match= $magic->isMatchingExtension($extension,$mime);
+
+ if ($match===NULL) {
+ wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
+ return true;
+ } elseif ($match===true) {
+ wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
+
+ #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
+ return true;
+
+ } else {
+ wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
+ return false;
+ }
+ }
+
+ /**
+ * Heuristic for detecting files that *could* contain JavaScript instructions or
+ * things that may look like HTML to a browser and are thus
+ * potentially harmful. The present implementation will produce false positives in some situations.
+ *
+ * @param string $file Pathname to the temporary upload file
+ * @param string $mime The mime type of the file
+ * @param string $extension The extension of the file
+ * @return bool true if the file contains something looking like embedded scripts
+ */
+ function detectScript($file, $mime, $extension) {
+ global $wgAllowTitlesInSVG;
+
+ #ugly hack: for text files, always look at the entire file.
+ #For binarie field, just check the first K.
+
+ if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
+ else {
+ $fp = fopen( $file, 'rb' );
+ $chunk = fread( $fp, 1024 );
+ fclose( $fp );
+ }
+
+ $chunk= strtolower( $chunk );
+
+ if (!$chunk) return false;
+
+ #decode from UTF-16 if needed (could be used for obfuscation).
+ if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
+ elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
+ else $enc= NULL;
+
+ if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
+
+ $chunk= trim($chunk);
+
+ #FIXME: convert from UTF-16 if necessarry!
+
+ wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
+
+ #check for HTML doctype
+ if (eregi("addHTML( "
Bad configuration: unknown virus scanner: $wgAntivirus
\n" );
+ return "unknown antivirus: $wgAntivirus";
+ }
+
+ # look up scanner configuration
+ $command = $wgAntivirusSetup[$wgAntivirus]["command"];
+ $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
+ $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
+ $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
+
+ if ( strpos( $command,"%f" ) === false ) {
+ # simple pattern: append file to scan
+ $command .= " " . wfEscapeShellArg( $file );
+ } else {
+ # complex pattern: replace "%f" with file to scan
+ $command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
+ }
+
+ wfDebug( __METHOD__.": running virus scan: $command \n" );
+
+ # execute virus scanner
+ $exitCode = false;
+
+ #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
+ # that does not seem to be worth the pain.
+ # Ask me (Duesentrieb) about it if it's ever needed.
+ $output = array();
+ if ( wfIsWindows() ) {
+ exec( "$command", $output, $exitCode );
+ } else {
+ exec( "$command 2>&1", $output, $exitCode );
+ }
+
+ # map exit code to AV_xxx constants.
+ $mappedCode = $exitCode;
+ if ( $exitCodeMap ) {
+ if ( isset( $exitCodeMap[$exitCode] ) ) {
+ $mappedCode = $exitCodeMap[$exitCode];
+ } elseif ( isset( $exitCodeMap["*"] ) ) {
+ $mappedCode = $exitCodeMap["*"];
+ }
+ }
+
+ if ( $mappedCode === AV_SCAN_FAILED ) {
+ # scan failed (code was mapped to false by $exitCodeMap)
+ wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
+
+ if ( $wgAntivirusRequired ) {
+ return "scan failed (code $exitCode)";
+ } else {
+ return NULL;
+ }
+ } else if ( $mappedCode === AV_SCAN_ABORTED ) {
+ # scan failed because filetype is unknown (probably imune)
+ wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
+ return NULL;
+ } else if ( $mappedCode === AV_NO_VIRUS ) {
+ # no virus found
+ wfDebug( __METHOD__.": file passed virus scan.\n" );
+ return false;
+ } else {
+ $output = join( "\n", $output );
+ $output = trim( $output );
+
+ if ( !$output ) {
+ $output = true; #if there's no output, return true
+ } elseif ( $msgPattern ) {
+ $groups = array();
+ if ( preg_match( $msgPattern, $output, $groups ) ) {
+ if ( $groups[1] ) {
+ $output = $groups[1];
+ }
+ }
+ }
+
+ wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
+ return $output;
+ }
+ }
+
+ /**
+ * Check if the temporary file is MacBinary-encoded, as some uploads
+ * from Internet Explorer on Mac OS Classic and Mac OS X will be.
+ * If so, the data fork will be extracted to a second temporary file,
+ * which will then be checked for validity and either kept or discarded.
+ *
+ * @access private
+ */
+ function checkMacBinary() {
+ $macbin = new MacBinary( $this->mTempPath );
+ if( $macbin->isValid() ) {
+ $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
+ $dataHandle = fopen( $dataFile, 'wb' );
+
+ wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
+ $macbin->extractData( $dataHandle );
+
+ $this->mTempPath = $dataFile;
+ $this->mFileSize = $macbin->dataForkLength();
+
+ // We'll have to manually remove the new file if it's not kept.
+ $this->mRemoveTempFile = true;
+ }
+ $macbin->close();
+ }
+
+ /**
+ * If we've modified the upload file we need to manually remove it
+ * on exit to clean up.
+ * @access private
+ */
+ function cleanupTempFile() {
+ if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
+ wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
+ unlink( $this->mTempPath );
+ }
+ }
+
+ /**
+ * Check if there's an overwrite conflict and, if so, if restrictions
+ * forbid this user from performing the upload.
+ *
+ * @return mixed true on success, WikiError on failure
+ * @access private
+ */
+ function checkOverwrite( $name ) {
+ $img = wfFindFile( $name );
+
+ $error = '';
+ if( $img ) {
+ global $wgUser, $wgOut;
+ if( $img->isLocal() ) {
+ if( !self::userCanReUpload( $wgUser, $img->name ) ) {
+ $error = 'fileexists-forbidden';
+ }
+ } else {
+ if( !$wgUser->isAllowed( 'reupload' ) ||
+ !$wgUser->isAllowed( 'reupload-shared' ) ) {
+ $error = "fileexists-shared-forbidden";
+ }
+ }
+ }
+
+ if( $error ) {
+ $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
+ return $errorText;
+ }
+
+ // Rockin', go ahead and upload
+ return true;
+ }
+
+ /**
+ * Check if a user is the last uploader
+ *
+ * @param User $user
+ * @param string $img, image name
+ * @return bool
+ */
+ public static function userCanReUpload( User $user, $img ) {
+ if( $user->isAllowed( 'reupload' ) )
+ return true; // non-conditional
+ if( !$user->isAllowed( 'reupload-own' ) )
+ return false;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow('image',
+ /* SELECT */ 'img_user',
+ /* WHERE */ array( 'img_name' => $img )
+ );
+ if ( !$row )
+ return false;
+
+ return $user->getId() == $row->img_user;
+ }
+
+ /**
+ * Display an error with a wikitext description
+ */
+ function showError( $description ) {
+ global $wgOut;
+ $wgOut->setPageTitle( wfMsg( "internalerror" ) );
+ $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setArticleRelated( false );
+ $wgOut->enableClientCache( false );
+ $wgOut->addWikiText( $description );
+ }
+
+ /**
+ * Get the initial image page text based on a comment and optional file status information
+ */
+ static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
+ global $wgUseCopyrightUpload;
+ if ( $wgUseCopyrightUpload ) {
+ if ( $license != '' ) {
+ $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ }
+ $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
+ '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
+ "$licensetxt" .
+ '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
+ } else {
+ if ( $license != '' ) {
+ $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
+ $pageText = $filedesc .
+ '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ } else {
+ $pageText = $comment;
+ }
+ }
+ return $pageText;
+ }
+
+ /**
+ * If there are rows in the deletion log for this file, show them,
+ * along with a nice little note for the user
+ *
+ * @param OutputPage $out
+ * @param string filename
+ */
+ private function showDeletionLog( $out, $filename ) {
+ global $wgUser;
+ $loglist = new LogEventsList( $wgUser->getSkin(), $out );
+ $pager = new LogPager( $loglist, 'delete', false, $filename );
+ if( $pager->getNumRows() > 0 ) {
+ $out->addHtml( '
' );
+ }
+
+ /**#@+
+ * @private
+ */
+
+ /**
+ * @return wiki text showing the license information
+ */
+ static function MediaWikiCredits() {
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
+ "__NOTOC__
+ This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
+ copyright (C) 2001-2008 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
+ Tim Starling, Erik Möller, Gabriel Wicke, Ãvar Arnfjörð Bjarmason,
+ Niklas Laxström, Domas Mituzas, Rob Church, Yuri Astrakhan and others.
+
+ MediaWiki is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ MediaWiki is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License]
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].
+ ";
+
+ return str_replace( "\t\t", '', $ret ) . "\n";
+ }
+
+ /**
+ * @return wiki text showing the third party software versions (apache, php, mysql).
+ */
+ static function softwareInformation() {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ return Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-software' ) ) .
+ "