From: Aaron Schulz Date: Fri, 12 Sep 2014 22:37:05 +0000 (+0000) Subject: Merge "Group E-mail settings stuff in Setup.php" X-Git-Tag: 1.31.0-rc.0~14051 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/comptes/journal.php?a=commitdiff_plain;h=16a9dd96bd5d7bec87c83d47fb954f2e7934dffe;hp=b87c3f83647f21309b2fec4bb5358a5cddac9edb;p=lhc%2Fweb%2Fwiklou.git Merge "Group E-mail settings stuff in Setup.php" --- diff --git a/.travis.yml b/.travis.yml index dedb4e14a7..6e07653321 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ language: php php: - hhvm-nightly + - 5.3 services: - mysql @@ -42,6 +43,7 @@ notifications: irc: channels: - "chat.freenode.net#mediawiki-core" + - "chat.freenode.net#mediawiki-feed" on_success: change on_failure: change skip_join: true diff --git a/RELEASE-NOTES-1.24 b/RELEASE-NOTES-1.24 index ec4bc0fa0c..33fe0d0091 100644 --- a/RELEASE-NOTES-1.24 +++ b/RELEASE-NOTES-1.24 @@ -42,11 +42,38 @@ production. configurations are $wgDeletedDirectory and $wgHashedUploadDirectory. * The deprecated $wgUseCommaCount variable has been removed. * $wgEnableSorbs and $wgSorbsUrl have been removed. -* The UserCryptPassword and UserComparePassword hooks are no longer called. Any extensions - using them must be updated to use the Password Hashing API. +* The UserCryptPassword and UserComparePassword hooks are no longer called. + Any extensions using them must be updated to use the Password Hashing API. * $wgCompiledFiles has been removed. +* $wgSortSpecialPages was removed, the listing on Special:SpecialPages is + now always sorted. +* $wgHTCPMulticastAddress, $wgHTCPMulticastRouting and $wgHTCPPort were removed. +* $wgRC2UDPAddress, $wgRC2UDPInterwikiPrefix, $wgRC2UDPOmitBots, $wgRC2UDPPort + and $wgRC2UDPPrefix have been removed. +* The default password type for MediaWiki has been changed from MD5 to PBKDF2. + Password hashes will automatically be updated as users log in. If necessary, the + old MD5 hashing can be restored by changing $wgPasswordDefault to 'B'. In addition, + there is a maintenance script wrapOldPassword.php that can wrap all passwords in + PBKDF2 (or the hashing algorithm of your choice) if you don't want to wait for your + users to log in. +* $wgImportSources can now either be a regular array, or an associative map + specifying subprojects on the interwiki map of the target wiki, or a mix of + the two. Existing configurations will still work. +* Users must be able to edit through a page's protection to be able to delete it. +* The default thumb size ($wgDefaultUserOptions['thumbsize']) is now 300px, up from + 180px. If you have altered the number of entries in $wgThumbLimits for your wiki, you + may need to adjust your default user settings to compensate for the index change. +* $wgDeferredUpdateList is now deprecated, you should use DeferredUpdates::addUpdate() + instead. +* $wgCanonicalLanguageLinks has been removed. Per Google recommendations, we + will not send a rel=canonical pointing to a variant-neutral page, however + we will send rel=alternate. === New features in 1.24 === +* Added new hook WatchlistEditorBeforeFormRender, allowing subscribers to + manipulate the list of pages and/or preload lots of data at once. +* Added new argument &$link in hook WatchlistEditorBuildRemoveLine, allowing the + link to the title to be changed. * Added a new hook, "WhatLinksHereProps", to allow extensions to annotate WhatLinksHere entries. * Added a new hook, "ContentGetParserOutput", to customize parser output for @@ -119,7 +146,7 @@ production. Special:PageLanguage. All pages are set to wiki language by default. The feature needs to be enabled with $wgPageLanguageUseDB=true and permission needs to be set for 'pagelang'. -* Upgrade Moment.js to v2.7.0. +* Upgrade Moment.js to v2.8.3. * (bug 67042) Added support for the HTML5 tag for East Asian typography. * Upgrade Sinon.JS to 1.10.3. * Added the es5-shim polyfill for older or non-compliant javascript engines. @@ -129,9 +156,12 @@ production. * (bug 66440) The MediaWiki web installer will now allow you to choose the skins to enable (from the ones included in download tarball) and decide which one should be the default. -* (bug 68085) Links of the form [[localInterwikiPrefix:languageCode:pageTitle]], +* (bug 68085, 68802) Links like [[localInterwikiPrefix:languageCode:pageTitle]], where localInterwikiPrefix is a member of the $wgLocalInterwikis array, will - no longer be displayed in the sidebar when $wgInterwikiMagic is true. + no longer be displayed in the sidebar when $wgInterwikiMagic is true. In a + similar way, links like [[localInterwikiPrefix:File:Image.png]] and + [[localInterwikiPrefix:Category:Hello]] will now render as regular links, and + will not include the file or add the page to the category. * New special page, MyLanguages, to redirect users to subpages with localised versions of a page. (Integrated from Extension:Translate) * MediaWiki now supports multiple password types, including bcrypt and PBKDF2. @@ -141,8 +171,21 @@ production. the $wgResourceModuleSkinStyles global. See the Vector skin for examples. * (bug 4488) There is now a preference to watch pages where the user has rollbacked an edit by default. +* (bug 15484) Users will now be redirected to the login page when they need to + log in, rather than being shown a page asking them to log in and having to click + another link to actually get to the login page. +* A JSONContent and JSONContentHandler were added for extensions to extend. +* (bug 35045) Redirects to sections will now update the URL in browser's address + bar using the HTML5 History API. When [[Dog]] redirects to [[Animals#Dog]], + the user will now see "Animals#Dog" in their browser instead of "Dog#Dog". +* API token handling has been rewritten. Any API module using tokens will need + to be updated. See the entry below under "Action API internal changes". +* Added HTMLAutoCompleteSelectField. +* Added a new hook, "SkinPreloadExistence", to allow extensions to add titles to + link existence cache before the page is rendered. === Bug fixes in 1.24 === +* (bug 50572) MediaWiki:Blockip should support gender * (bug 49116) Footer copyright notice is now always displayed in user language rather than content language (same as copyright notice for editing interface). * (bug 62258) A bug was fixed in File::getUnscaledThumb when a height @@ -170,21 +213,24 @@ production. * (bug 67870) wfShellExec() cuts off stdout at multiples of 8192 bytes. * $wgRunJobsAsync now works with private wikis (e.g. read requires login). * (bugs 57238, 65206) Blank pages can now be directly created. +* (bug 69789) Title::getContentModel() now loads from the database when + necessary instead of incorrectly returning the default content model. +* (bug 69249) wfBaseConvert() now works around PHP Bug #50175 when using GMP. -=== Web API changes in 1.24 === +=== Action API changes in 1.24 === * action=parse API now supports prop=modules, which provides the list of ResourceLoader modules that should be used to enhance the parsed content. * action=query&meta=siteinfo&siprop=interwikimap returns a new "protorel" - field which is true iff protocol-relative urls can be used to access + field which is true if protocol-relative urls can be used to access a particular interwiki map entry. -* ApiQueryLogEvents now provides logpage, which is the page ID from the +* list=logevents now provides logpage, which is the page ID from the logging table, if ids are requested and the user has the permissions. * action=edit now requires that appendtext, prependtext, or section=new be used when using the 'redirect' parameter, to prevent clients accidentally overwriting the target page with the content of the redirect. -* action=logevents will now return an error if both letitle and leprefix are +* list=logevents will now return an error if both letitle and leprefix are specified. -* action=logevents has a new parameter, lenamespace, to allow filtering by +* list=logevents has a new parameter, lenamespace, to allow filtering by namespace. * action=expandtemplates has a new parameter, prop, and a new output format. The old format is still used if prop isn't provided, but this is deprecated. @@ -196,6 +242,92 @@ production. * (bug 60734) Actions that use ApiPageSet (e.g. purge, watch, setnotificationtimestamp) will now include continuation information when using a generator. +* Removed 'props' and 'errors' from action=paraminfo, as they have extremely + limited use and are generally inaccurate, unmaintained, and impossible to + properly maintain. +* Formats dbg, dump, txt, wddx, and yaml are now deprecated. +* action=paraminfo now indicates when a parameter is specifying a submodule. +* The iwurl parameter to prop=iwlinks is deprecated in favor of iwprop=url, for + parallelism with prop=langlinks. +* All tokens should be fetched from action=query&meta=tokens; all other methods + of fetching tokens are deprecated. The value needed for meta=tokens's 'type' + parameter for each module is documented in the action=help output and is + returned from action=paraminfo. +* New action ClearHasMsg that can be used to clear HasMsg flag. +* The cmstartsortkey and cmendsortkey parameters to list=categorymembers are + deprecated in favor of cmstarthexsortkey and cmendhexsortkey. + +=== Action API internal changes in 1.24 === +* Methods for handling continuation are added to ApiResult, so actions other + than query that use generators can easily support continuation. +* $wgAPIModules (and the related $wgAPIFormatModules, $wgAPIMetaModules, + $wgAPIPropModules, and $wgAPIListModules settings) now allow API modules + to be specified using a "module spec" array instead of a plain class name. + A "module spec" is an associative array containing at least the 'class' key + for the module's class, and optionally a 'factory' key for the factory function + to use for the module. This is intended for extensions that want control over + the instantiation of their API modules, to allow for proper dependency + injection. +* A new param type 'submodule' is available. Parameters of this type will take + the list of valid values from the module's ApiModuleManager for the group + corresponding to the parameter name. +* The 'APIGetPossibleErrors' and 'APIGetResultProperties' hooks are no longer used. +* API token handling has been rewritten. Any API module using tokens will need + to be updated: + * ApiBase::needsToken now returns a token type instead of boolean true when a + token is needed. Returning true will throw an exception. See documentation + of that method for details. + * Information for the 'token' parameter is automatically set by ApiBase + getFinalParams and getFinalParamDescription. + * ApiBase::getTokenSalt has been removed. + * The hooks APIQueryInfoTokens, APIQueryRevisionsTokens, + APIQueryRecentChangesTokens, APIQueryUsersTokens, and + ApiTokensGetTokenTypes are deprecated, but are still called to support + backwards-compatible token access. +* ApiBase::validateLimit and ApiBase::validateTimestamp are now protected. +* The following methods have been deprecated and may be removed in a future + release: + * ApiBase::getResultProperties + * ApiBase::getFinalResultProperties + * ApiBase::addTokenProperties + * ApiBase::getRequireOnlyOneParameterErrorMessages + * ApiBase::getRequireMaxOneParameterErrorMessages + * ApiBase::getRequireAtLeastOneParameterErrorMessages + * ApiBase::getTitleOrPageIdErrorMessage + * ApiBase::getPossibleErrors + * ApiBase::getFinalPossibleErrors + * ApiBase::parseErrors + * ApiQuery::setGeneratorContinue + * ApiQueryBase::checkRowCount + * ApiQueryBase::titleToKey + * ApiQueryBase::keyToTitle + * ApiQueryBase::keyPartToTitle + * ApiQueryInfo::getTokenFunctions + * ApiQueryInfo::resetTokenCache + * ApiQueryInfo::getEditToken + * ApiQueryInfo::getDeleteToken + * ApiQueryInfo::getProtectToken + * ApiQueryInfo::getMoveToken + * ApiQueryInfo::getBlockToken + * ApiQueryInfo::getUnblockToken + * ApiQueryInfo::getEmailToken + * ApiQueryInfo::getImportToken + * ApiQueryInfo::getWatchToken + * ApiQueryInfo::getOptionsToken + * ApiQueryRecentChanges::getTokenFunctions + * ApiQueryRecentChanges::getPatrolToken + * ApiQueryRevisions::getTokenFunctions + * ApiQueryRevisions::getRollbackToken + * ApiQueryUsers::getTokenFunctions + * ApiQueryUsers::getUserrightsToken +* The following classes have been deprecated and may be removed in a future + release: + * ApiFormatDbg + * ApiFormatDump + * ApiFormatTxt + * ApiFormatWddx + * ApiFormatYaml + * ApiTokens === Languages updated in 1.24 === @@ -216,14 +348,23 @@ changes to languages because of Bugzilla reports. * Added pp_sortkey column to page_props table, so pages can be efficiently queried and sorted by property value (bug 58032). See $wgPagePropsHaveSortkey if you want to postpone the schema change. -* BREAKING CHANGE: The Modern and Cologne Blue skins were moved out of MediaWiki - core to their own respective repositories. See also - https://www.mediawiki.org/wiki/Skin:Modern and - https://www.mediawiki.org/wiki/Skin:CologneBlue. +* BREAKING CHANGE: All four built-in MediaWiki skins (Vector, MonoBook, Modern + and Cologne Blue) were moved out of MediaWiki core to their own respective + repositories. They will be installed with the release tarball, but you must + install them separately if installing MediaWiki from source code. A warning + message displayed until you do it should guide you through the process. See + also . * BREAKING CHANGE: Skins built for MediaWiki 1.15 and earlier that do not use the "headelement" template key are no longer supported. Setting $useHeadElement = false; is no longer supported and will not cause old keys like "headlinks", "skinnameclass", etc. to be defined. +* BREAKING CHANGE: The files commonElements.css, commonContent.css and + commonInterface.css (in skins/common/) have been removed. Skins may no longer + rely on their presence and include them in their style modules. ResourceLoader + modules introduced in MediaWiki 1.23 should be loaded instead: + - skins/common/commonElements.css → 'mediawiki.skinning.elements' module + - skins/common/commonContent.css → 'mediawiki.skinning.content' module + - skins/common/commonInterface.css → 'mediawiki.skinning.interface' module * The deprecated 'SpecialVersionExtensionTypes' hook was removed. * (bug 63891) Add 'X-Robots-Tag: noindex' header in action=render pages. * SpecialPage no longer supports the syntax for invoking wfSpecial*() functions. @@ -253,7 +394,7 @@ changes to languages because of Bugzilla reports. set of hooks has been removed and replaced by a single new hook SpecialPageBeforeFormDisplay. * (bug 65781) Removed block warning on included {{Special:Contributions}} -* Removed Skin::makeGlobalVariablesScript. (deprecated since 1.19) +* Removed Skin::makeGlobalVariablesScript(). (deprecated since 1.19) * Removed MWNamespace::isMain(). (deprecated since 1.19) * Removed Preferences::loadOldSearchNs(). (deprecated since 1.19) * Removed OutputPage::getStatusMessage(). (deprecated since 1.18) @@ -291,6 +432,36 @@ changes to languages because of Bugzilla reports. setPreloadedText() from EditPage.php. (deprecated since 1.21) * Removed global functions wfArrayLookup(), wfArrayMerge(), wfDebugDieBacktrace() and wfTime(). (deprecated since 1.22) +* Browser support for Internet Explorer 6 and 7 lowered from Grade A to Grade C, + meaning that JavaScript is no longer executed in these browser versions. +* Browser support for Opera 11 lowered from Grade A to Grade C. +* Removed IEFixes module which existed purely to provide support for MSIE versions + below 7 (conditionally loaded only for those browsers). +* Action::checkCanExecute() no longer has a return value. +* Removed cleanupForIRC(), loadFromCurRow(), newFromCurRow(), notifyRC2UDP() + and sendToUDP() from RecentChange.php. (deprecated since 1.22) +* Removed EnhancedChangesList::arrow(), sideArrow(), downArrow(), spacerArrow(). +* Removed Xml::namespaceSelector(). (deprecated since 1.19) +* Removed WikiPage::estimateRevisionCount(). (deprecated since 1.19) +* MYSQL: Enum item added to "major MIME type" columns. + Running update.php on MySQL < v5.1 may result in heavy processing. +* RSS and Atom feeds generated by MediaWiki no longer include a fallback + stylesheet. It was ignored by most browsers these days anyway. +* SpecialSearchNoResults hook has been removed. SpecialSearchResults is now + called unconditionally. +* TablePager::getBody() is now 'final' and can't be overridden in subclasses. +* TablePager::getBody() is deprecated, use getBodyOutput() or getFullOutput(). +* Added $outputPage parameter to the SkinTemplateGetLanguageLink hook. +* log_page for move log entries store the original page ID, rather than that + of the new redirect page. This is not retroactive. +* LCStoreAccel was removed. $wgLocalisationCacheConf can no longer be set to + use this store class. +* Html::infoBox() no longer accepts paths relative to skins/common/images/. +* Deprecated defunct Skin::getCommonStylePath(). +* Some extensions had their ResourceLoader modules depend on the "mediawiki" + and "jquery" modules. In the past, this behavior was undefined, now it will + throw an error. +* Removed BagOStuff::replace(). (deprecated since 1.23) ==== Renamed classes ==== * CLDRPluralRuleConverter_Expression to CLDRPluralRuleConverterExpression diff --git a/assets/file-type-icons/COPYING b/assets/file-type-icons/COPYING new file mode 100644 index 0000000000..136530a91e --- /dev/null +++ b/assets/file-type-icons/COPYING @@ -0,0 +1,43 @@ +The icons used here are derived from the crystalsvg icons in the the +pics/crystalsvg/ directory of kdelibs-3.4.0 they were modified on 2005-05-15 +by Ævar Arnfjörð Bjarmason for use in MediaWiki. + +What follows is the contents of the LICENSE.crystalsvg file found in the pics/ +subdirectory of kdelibs-3.4.0: + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +This copyright and license notice covers all CrystalSVG images. +Note the license notice contains an add-on. +******************************************************************************** +KDE Crystal theme icons. +Copyright (C) 2002 and following years KDE Artists +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation, +version 2.1 of the License. +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + **** NOTE THIS ADD-ON **** +The GNU Lesser General Public License or LGPL is written for software libraries +in the first place. We expressly want the LGPL to be valid for this artwork +library too. +KDE Crystal theme icons is a special kind of software library, it is an +artwork library, it's elements can be used in a Graphical User Interface, or +GUI. +Source code, for this library means: + - for vectors svg; + - for pixels, if applicable, the multi-layered formats xcf or psd, or +otherwise png. +The LGPL in some sections obliges you to make the files carry +notices. With images this is in some cases impossible or hardly useful. +With this library a notice is placed at a prominent place in the directory +containing the elements. You may follow this practice. +The exception in section 6 of the GNU Lesser General Public License covers +the use of elements of this art library in a GUI. +kde-artists [at] kde.org +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/assets/file-type-icons/fileicon-c.png b/assets/file-type-icons/fileicon-c.png new file mode 100644 index 0000000000..0d603b7090 Binary files /dev/null and b/assets/file-type-icons/fileicon-c.png differ diff --git a/assets/file-type-icons/fileicon-cpp.png b/assets/file-type-icons/fileicon-cpp.png new file mode 100644 index 0000000000..123688f002 Binary files /dev/null and b/assets/file-type-icons/fileicon-cpp.png differ diff --git a/assets/file-type-icons/fileicon-deb.png b/assets/file-type-icons/fileicon-deb.png new file mode 100644 index 0000000000..87ca3fabe1 Binary files /dev/null and b/assets/file-type-icons/fileicon-deb.png differ diff --git a/assets/file-type-icons/fileicon-djvu.png b/assets/file-type-icons/fileicon-djvu.png new file mode 100644 index 0000000000..1da2276118 Binary files /dev/null and b/assets/file-type-icons/fileicon-djvu.png differ diff --git a/assets/file-type-icons/fileicon-djvu.xcf b/assets/file-type-icons/fileicon-djvu.xcf new file mode 100644 index 0000000000..8043dcdb51 Binary files /dev/null and b/assets/file-type-icons/fileicon-djvu.xcf differ diff --git a/assets/file-type-icons/fileicon-dvi.png b/assets/file-type-icons/fileicon-dvi.png new file mode 100644 index 0000000000..f37878d80a Binary files /dev/null and b/assets/file-type-icons/fileicon-dvi.png differ diff --git a/assets/file-type-icons/fileicon-exe.png b/assets/file-type-icons/fileicon-exe.png new file mode 100644 index 0000000000..dc020eb813 Binary files /dev/null and b/assets/file-type-icons/fileicon-exe.png differ diff --git a/assets/file-type-icons/fileicon-h.png b/assets/file-type-icons/fileicon-h.png new file mode 100644 index 0000000000..339bf02506 Binary files /dev/null and b/assets/file-type-icons/fileicon-h.png differ diff --git a/assets/file-type-icons/fileicon-html.png b/assets/file-type-icons/fileicon-html.png new file mode 100644 index 0000000000..f28f8a26d1 Binary files /dev/null and b/assets/file-type-icons/fileicon-html.png differ diff --git a/assets/file-type-icons/fileicon-iso.png b/assets/file-type-icons/fileicon-iso.png new file mode 100644 index 0000000000..c73d2294d2 Binary files /dev/null and b/assets/file-type-icons/fileicon-iso.png differ diff --git a/assets/file-type-icons/fileicon-java.png b/assets/file-type-icons/fileicon-java.png new file mode 100644 index 0000000000..a1b4f225e2 Binary files /dev/null and b/assets/file-type-icons/fileicon-java.png differ diff --git a/assets/file-type-icons/fileicon-mid.png b/assets/file-type-icons/fileicon-mid.png new file mode 100644 index 0000000000..ce2bebb283 Binary files /dev/null and b/assets/file-type-icons/fileicon-mid.png differ diff --git a/assets/file-type-icons/fileicon-mov.png b/assets/file-type-icons/fileicon-mov.png new file mode 100644 index 0000000000..952de1f2f3 Binary files /dev/null and b/assets/file-type-icons/fileicon-mov.png differ diff --git a/assets/file-type-icons/fileicon-o.png b/assets/file-type-icons/fileicon-o.png new file mode 100644 index 0000000000..f3523d96ae Binary files /dev/null and b/assets/file-type-icons/fileicon-o.png differ diff --git a/assets/file-type-icons/fileicon-ogg.png b/assets/file-type-icons/fileicon-ogg.png new file mode 100644 index 0000000000..ef4d801608 Binary files /dev/null and b/assets/file-type-icons/fileicon-ogg.png differ diff --git a/assets/file-type-icons/fileicon-ogg.xcf b/assets/file-type-icons/fileicon-ogg.xcf new file mode 100644 index 0000000000..a91024bf21 Binary files /dev/null and b/assets/file-type-icons/fileicon-ogg.xcf differ diff --git a/assets/file-type-icons/fileicon-pdf.png b/assets/file-type-icons/fileicon-pdf.png new file mode 100644 index 0000000000..8c8da92ba4 Binary files /dev/null and b/assets/file-type-icons/fileicon-pdf.png differ diff --git a/assets/file-type-icons/fileicon-ps.png b/assets/file-type-icons/fileicon-ps.png new file mode 100644 index 0000000000..e872833158 Binary files /dev/null and b/assets/file-type-icons/fileicon-ps.png differ diff --git a/assets/file-type-icons/fileicon-psd.png b/assets/file-type-icons/fileicon-psd.png new file mode 100644 index 0000000000..598f190e18 Binary files /dev/null and b/assets/file-type-icons/fileicon-psd.png differ diff --git a/assets/file-type-icons/fileicon-rm.png b/assets/file-type-icons/fileicon-rm.png new file mode 100644 index 0000000000..81dbe0b7dd Binary files /dev/null and b/assets/file-type-icons/fileicon-rm.png differ diff --git a/assets/file-type-icons/fileicon-rpm.png b/assets/file-type-icons/fileicon-rpm.png new file mode 100644 index 0000000000..1903aacc26 Binary files /dev/null and b/assets/file-type-icons/fileicon-rpm.png differ diff --git a/assets/file-type-icons/fileicon-svg.png b/assets/file-type-icons/fileicon-svg.png new file mode 100644 index 0000000000..b782113a74 Binary files /dev/null and b/assets/file-type-icons/fileicon-svg.png differ diff --git a/assets/file-type-icons/fileicon-tar.png b/assets/file-type-icons/fileicon-tar.png new file mode 100644 index 0000000000..e5fd1b74d4 Binary files /dev/null and b/assets/file-type-icons/fileicon-tar.png differ diff --git a/assets/file-type-icons/fileicon-tex.png b/assets/file-type-icons/fileicon-tex.png new file mode 100644 index 0000000000..a4372841d1 Binary files /dev/null and b/assets/file-type-icons/fileicon-tex.png differ diff --git a/assets/file-type-icons/fileicon-ttf.png b/assets/file-type-icons/fileicon-ttf.png new file mode 100644 index 0000000000..1ed4e740cb Binary files /dev/null and b/assets/file-type-icons/fileicon-ttf.png differ diff --git a/assets/file-type-icons/fileicon-txt.png b/assets/file-type-icons/fileicon-txt.png new file mode 100644 index 0000000000..9e988e711e Binary files /dev/null and b/assets/file-type-icons/fileicon-txt.png differ diff --git a/assets/file-type-icons/fileicon-xcf.png b/assets/file-type-icons/fileicon-xcf.png new file mode 100644 index 0000000000..1037b506d5 Binary files /dev/null and b/assets/file-type-icons/fileicon-xcf.png differ diff --git a/assets/file-type-icons/fileicon.png b/assets/file-type-icons/fileicon.png new file mode 100644 index 0000000000..59696a382b Binary files /dev/null and b/assets/file-type-icons/fileicon.png differ diff --git a/assets/mediawiki.png b/assets/mediawiki.png new file mode 100644 index 0000000000..8c42118385 Binary files /dev/null and b/assets/mediawiki.png differ diff --git a/docs/hooks.txt b/docs/hooks.txt index 25f5ad88f6..9ac2271d54 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -393,16 +393,6 @@ descriptions. &$module: ApiBase Module object &$desc: Array of parameter descriptions -'APIGetResultProperties': Use this hook to modify the properties in a module's -result. -&$module: ApiBase Module object -&$properties: Array of properties - -'APIGetPossibleErrors': Use this hook to modify the module's list of possible -errors. -$module: ApiBase Module object -&$possibleErrors: Array of possible errors - 'APIQueryAfterExecute': After calling the execute() method of an action=query submodule. Use this to extend core API modules. &$module: Module object @@ -412,36 +402,39 @@ an action=query submodule. Use this to extend core API modules. &$module: Module object &$resultPageSet: ApiPageSet object -'APIQueryInfoTokens': Use this hook to add custom tokens to prop=info. Every -token has an action, which will be used in the intoken parameter and in the -output (actiontoken="..."), and a callback function which should return the -token, or false if the user isn't allowed to obtain it. The prototype of the -callback function is func($pageid, $title), where $pageid is the page ID of the -page the token is requested for and $title is the associated Title object. In -the hook, just add your callback to the $tokenFunctions array and return true -(returning false makes no sense). +'APIQueryInfoTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead. +Use this hook to add custom tokens to prop=info. Every token has an action, +which will be used in the intoken parameter and in the output +(actiontoken="..."), and a callback function which should return the token, or +false if the user isn't allowed to obtain it. The prototype of the callback +function is func($pageid, $title), where $pageid is the page ID of the page the +token is requested for and $title is the associated Title object. In the hook, +just add your callback to the $tokenFunctions array and return true (returning +false makes no sense). $tokenFunctions: array(action => callback) -'APIQueryRevisionsTokens': Use this hook to add custom tokens to prop=revisions. -Every token has an action, which will be used in the rvtoken parameter and in -the output (actiontoken="..."), and a callback function which should return the -token, or false if the user isn't allowed to obtain it. The prototype of the -callback function is func($pageid, $title, $rev), where $pageid is the page ID -of the page associated to the revision the token is requested for, $title the +'APIQueryRevisionsTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead. +Use this hook to add custom tokens to prop=revisions. Every token has an +action, which will be used in the rvtoken parameter and in the output +(actiontoken="..."), and a callback function which should return the token, or +false if the user isn't allowed to obtain it. The prototype of the callback +function is func($pageid, $title, $rev), where $pageid is the page ID of the +page associated to the revision the token is requested for, $title the associated Title object and $rev the associated Revision object. In the hook, just add your callback to the $tokenFunctions array and return true (returning false makes no sense). $tokenFunctions: array(action => callback) -'APIQueryRecentChangesTokens': Use this hook to add custom tokens to -list=recentchanges. Every token has an action, which will be used in the rctoken -parameter and in the output (actiontoken="..."), and a callback function which -should return the token, or false if the user isn't allowed to obtain it. The -prototype of the callback function is func($pageid, $title, $rc), where $pageid -is the page ID of the page associated to the revision the token is requested -for, $title the associated Title object and $rc the associated RecentChange -object. In the hook, just add your callback to the $tokenFunctions array and -return true (returning false makes no sense). +'APIQueryRecentChangesTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead. +Use this hook to add custom tokens to list=recentchanges. Every token has an +action, which will be used in the rctoken parameter and in the output +(actiontoken="..."), and a callback function which should return the token, or +false if the user isn't allowed to obtain it. The prototype of the callback +function is func($pageid, $title, $rc), where $pageid is the page ID of the +page associated to the revision the token is requested for, $title the +associated Title object and $rc the associated RecentChange object. In the +hook, just add your callback to the $tokenFunctions array and return true +(returning false makes no sense). $tokenFunctions: array(action => callback) 'APIQuerySiteInfoGeneralInfo': Use this hook to add extra information to the @@ -453,13 +446,19 @@ $module: the current ApiQuerySiteInfo module sites statistics information. &$results: array of results, add things here -'APIQueryUsersTokens': Use this hook to add custom token to list=users. Every -token has an action, which will be used in the ustoken parameter and in the -output (actiontoken="..."), and a callback function which should return the -token, or false if the user isn't allowed to obtain it. The prototype of the -callback function is func($user) where $user is the User object. In the hook, -just add your callback to the $tokenFunctions array and return true (returning -false makes no sense). +'ApiQueryTokensRegisterTypes': Use this hook to add additional token types to +action=query&meta=tokens. Note that most modules will probably be able to use +the 'csrf' token instead of creating their own token types. +&$salts: array( type => salt to pass to User::getEditToken() ) + +'APIQueryUsersTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead. +Use this hook to add custom token to list=users. Every token has an action, +which will be used in the ustoken parameter and in the output +(actiontoken="..."), and a callback function which should return the token, or +false if the user isn't allowed to obtain it. The prototype of the callback +function is func($user) where $user is the User object. In the hook, just add +your callback to the $tokenFunctions array and return true (returning false +makes no sense). $tokenFunctions: array(action => callback) 'ApiMain::onException': Called by ApiMain::executeActionWithErrorHandling() when @@ -473,8 +472,8 @@ key for the array that represents the service data. In this data array, the key-value-pair identified by the apiLink key is required. &$apis: array of services -'ApiTokensGetTokenTypes': Use this hook to extend action=tokens with new token -types. +'ApiTokensGetTokenTypes': DEPRECATED! Use ApiQueryTokensRegisterTypes instead. +Use this hook to extend action=tokens with new token types. &$tokenTypes: supported token types in format 'type' => callback function used to retrieve this type of tokens. @@ -864,6 +863,14 @@ $name: name of the special page, e.g. 'Watchlist' &$join_conds: join conditions for the tables $opts: FormOptions for this request +'LoginUserMigrated': Called during login to allow extensions the opportunity to +inform a user that their username doesn't exist for a specific reason, instead +of letting the login form give the generic error message that the account does +not exist. For example, when the account has been renamed or deleted. +$user: the User object being authenticated against. +&$msg: the message identifier for abort reason, or an array to pass a message + key and parameters. + 'Collation::factory': Called if $wgCategoryCollation is an unknown collation. $collationName: Name of the collation in question &$collationObject: Null. Replace with a subclass of the Collation class that @@ -1636,6 +1643,13 @@ $code: language code &$alldata: The localisation data from core and extensions &purgeBlobs: whether to purge/update the message blobs via MessageBlobStore::clear() +'LocalisationCacheRecacheFallback': Called for each language when merging +fallback data into the cache. +$cache: The LocalisationCache object +$code: language code +&$alldata: The localisation data from core and extensions. Note some keys may + be omitted if they won't be merged into the final result. + 'LocalisationChecksBlacklist': When fetching the blacklist of localisation checks. &$blacklist: array of checks to blacklist. See the bottom of @@ -1783,13 +1797,6 @@ $db: The database object to be queried. &$opts: Options for the query. &$join_conds: Join conditions for the query. -'MonoBookTemplateToolboxEnd': DEPRECATED. Called by Monobook skin after toolbox -links have been rendered (useful for adding more). Note: this is only run for -the Monobook skin. To add items to the toolbox you should use the -SkinTemplateToolboxEnd hook instead, which works for all "SkinTemplate"-type -skins. -$tools: array of tools - 'BaseTemplateToolbox': Called by BaseTemplate when building the $toolbox array and returning it for the skin to output. You can add items to the toolbox while still letting the skin make final decisions on skin-specific markup conventions @@ -2148,7 +2155,7 @@ $oldSessionID: old session id $newSessionID: new session id 'ResourceLoaderGetConfigVars': Called at the end of -ResourceLoaderStartUpModule::getConfig(). Use this to export static +ResourceLoaderStartUpModule::getConfigSettings(). Use this to export static configuration variables to JavaScript. Things that depend on the current page or request state must be added through MakeGlobalVariablesScript instead. &$vars: array( variable name => value ) @@ -2300,6 +2307,11 @@ $type: 'normal' or 'history' for old/diff views the MediaWiki icon but plain text instead. $skin: Skin object +'SkinPreloadExistence': Supply titles that should be added to link existence +cache before the page is rendered. +&$titles: Array of Title objects +$skin: Skin object + 'SkinSubPageSubtitle': At the beginning of Skin::subPageSubtitle(). &$subpages: Subpage links HTML $skin: Skin object @@ -2318,8 +2330,9 @@ $nav_urls: array of tabs which the actual html is constructed. &$languageLink: array containing data about the link. The following keys can be modified: href, text, title, class, lang, hreflang. Each of them is a string. -$languageLinkTitle: Title object belonging to the external language link -$title: Title object of the page the link belongs to +$languageLinkTitle: Title object belonging to the external language link. +$title: Title object of the page the link belongs to. +$outputPage: The OutputPage object the links are built from. To alter the structured navigation links in SkinTemplates, there are three hooks called in different spots: @@ -2364,11 +2377,6 @@ $dummy: Called when SkinTemplateToolboxEnd is used from a BaseTemplate skin, dummy parameter with "$dummy=false" in their code and return without echoing any HTML to avoid creating duplicate toolbox items. -'SkinVectorStyleModules': Called when defining the list of module styles to be -loaded by the Vector skin. -$skin: SkinVector object -&$styles: Array of module names whose style will be loaded for the skin - 'SoftwareInfo': Called by Special:Version for returning information about the software. $software: The array of software in format 'name' => 'version'. See @@ -2383,7 +2391,7 @@ $sp: SpecialPage object, for context &$fields: Current HTMLForm fields 'SpecialContributionsBeforeMainOutput': Before the form on Special:Contributions -$id: User id number, only provided for backwards-compatability +$id: User id number, only provided for backwards-compatibility $user: User object representing user contributions are being fetched for $sp: SpecialPage instance, providing context @@ -2526,16 +2534,11 @@ $specialSearch: SpecialSearch object ($this) $output: $wgOut $term: Search term specified by the user -'SpecialSearchResults': Called before search result display when there are -matches. +'SpecialSearchResults': Called before search result display $term: string of search term &$titleMatches: empty or SearchResultSet object &$textMatches: empty or SearchResultSet object -'SpecialSearchNoResults': Called before search result display when there are no -matches. -$term: string of search term - 'SpecialStatsAddExtra': Add extra statistic at the end of Special:Statistics. &$extraStats: Array to save the new stats ( $extraStats[''] => ; ) @@ -2734,11 +2737,11 @@ string &$error: output: message key for message to show if upload canceled by returning false. May also be an array, where the first element is the message key and the remaining elements are used as parameters to the message. -'UploadVerifyFile': extra file verification, based on mime type, etc. Preferred +'UploadVerifyFile': extra file verification, based on MIME type, etc. Preferred in most cases over UploadVerification. object $upload: an instance of UploadBase, with all info about the upload -string $mime: The uploaded file's mime type, as detected by MediaWiki. Handlers - will typically only apply for specific mime types. +string $mime: The uploaded file's MIME type, as detected by MediaWiki. Handlers + will typically only apply for specific MIME types. object &$error: output: true if the file is valid. Otherwise, an indexed array representing the problem with the file, where the first element is the message key and the remaining elements are used as parameters to the message. @@ -2960,12 +2963,18 @@ $page: WikiPage object to be watched $user: user that watched $page: WikiPage object watched +'WatchlistEditorBeforeFormRender': Before building the Special:EditWatchlist +form, used to manipulate the list of pages or preload data based on that list. +&$watchlistInfo: array of watchlisted pages in + [namespaceId => ['title1' => 1, 'title2' => 1]] format + 'WatchlistEditorBuildRemoveLine': when building remove lines in Special:Watchlist/edit. &$tools: array of extra links $title: Title object $redirect: whether the page is a redirect $skin: Skin object +&$link: HTML link to title 'WebRequestPathInfoRouter': While building the PathRouter to parse the REQUEST_URI. diff --git a/docs/kss/Makefile b/docs/kss/Makefile index 7d2ee3be97..a7b0c47122 100644 --- a/docs/kss/Makefile +++ b/docs/kss/Makefile @@ -1,12 +1,12 @@ MEDIAWIKI_LOAD_URL ?= http://localhost/w/load.php -kss: nodecheck -# FIXME: Use more up-to-date Ruby version - +kss: kssnodecheck # Generates CSS of mediawiki.ui and mediawiki.ui.button using ResourceLoader, then applies it to the # KSS style guide $(eval KSS_RL_TMP := $(shell mktemp /tmp/tmp.XXXXXXXXXX)) - @curl -sG "${MEDIAWIKI_LOAD_URL}?modules=mediawiki.ui.anchor|mediawiki.ui.checkbox|mediawiki.ui.input|mediawiki.legacy.shared|mediawiki.legacy.commonPrint|mediawiki.ui|mediawiki.ui.button&only=styles" > $(KSS_RL_TMP) +# Keep module names in strict alphabetical order, so CSS loads in the same order as ResourceLoader's addModuleStyles does; this can affect rendering. +# See OutputPage::makeResourceLoaderLink. + @curl -sG "${MEDIAWIKI_LOAD_URL}?modules=mediawiki.legacy.commonPrint|mediawiki.legacy.shared|mediawiki.ui|mediawiki.ui.anchor|mediawiki.ui.button|mediawiki.ui.checkbox|mediawiki.ui.input&only=styles" > $(KSS_RL_TMP) @node_modules/.bin/kss-node ../../resources/src/mediawiki.ui static/ --css $(KSS_RL_TMP) -t styleguide-template @rm $(KSS_RL_TMP) @@ -15,5 +15,5 @@ kssopen: kss @command -v xdg-open >/dev/null 2>&1 || { open ${PWD}/static/index.html; exit 0; } @xdg-open ${PWD}/static/index.html -nodecheck: - @scripts/nodecheck.sh +kssnodecheck: + @scripts/kss-node-check.sh diff --git a/docs/kss/scripts/kss-node-check.sh b/docs/kss/scripts/kss-node-check.sh new file mode 100755 index 0000000000..84ee1c4e5a --- /dev/null +++ b/docs/kss/scripts/kss-node-check.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +if command -v npm > /dev/null ; then + npm install +else + # If npm isn't installed, but kss-node is, exit normally. + # This allows setting it up on one machine, and running it on + # another (e.g. Tools Labs execution nodes) that doesn't have npm + # installed. However, "npm install" still needs to be run + # occasionally to keep kss updated. + + KSS_NODE="${BASH_SOURCE%/*}/../node_modules/.bin/kss-node" + if ! [ -x "$KSS_NODE" ] ; then + echo "Neither kss-node nor npm are installed." + echo "To install npm, see http://nodejs.org/" + echo "When npm is installed, the Makefile can automatically" + echo "install kss-node." + exit 1 + fi +fi diff --git a/docs/kss/scripts/nodecheck.sh b/docs/kss/scripts/nodecheck.sh deleted file mode 100755 index 3ee0f8346b..0000000000 --- a/docs/kss/scripts/nodecheck.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -if command -v npm > /dev/null ; then - npm install -else - echo "You need to install Node.JS!" - echo "See http://nodejs.org/" - exit 1 -fi diff --git a/docs/kss/styleguide-template/index.html b/docs/kss/styleguide-template/index.html index b6036b2d94..933260ec9c 100644 --- a/docs/kss/styleguide-template/index.html +++ b/docs/kss/styleguide-template/index.html @@ -19,25 +19,42 @@
-
diff --git a/docs/kss/styleguide-template/public/kss.less b/docs/kss/styleguide-template/public/kss.less index 9e850a3c34..c30322e6d5 100644 --- a/docs/kss/styleguide-template/public/kss.less +++ b/docs/kss/styleguide-template/public/kss.less @@ -1,3 +1,19 @@ +.container { + width: 100%; +} + +nav { + display: none; +} + +.content { + .example { + blockquote { + margin-top: 20px; + } + } +} + body { margin: 0; padding: 0; @@ -8,12 +24,12 @@ body { font-family: "Nimbus Sans L", "Liberation Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; } -.content.kss-no-margin { +.kss-no-margin { + // FIXME: Is this being used anywhere? Remove if not. margin: 0; } .container { - width: 960px; margin: 0 auto; display: -webkit-flex; display: flex; @@ -69,14 +85,24 @@ nav { width: 35px; } } + + ul { + li { + margin: 0; + } + + li a { + text-transform: none; + font-weight: normal; + } + } } } } -article { +.content { -webkit-flex: 1; flex: 1; - margin-left: 30px; h1, h2, h3, h4, h5, h6, p { margin-left: 20px; @@ -101,7 +127,6 @@ article { background: #f8f8f8; padding: 20px; color: #999; - width: 338px; word-wrap: break-word; // word-wrap in pre not affecting Firefox, so add white-space. white-space: pre-wrap; @@ -116,25 +141,44 @@ article { display: block; margin: 0; margin-left: 20px; - min-width: 360px; } } } -@media (max-width: 960px) { - .container { - width: 100%; +@media (min-width: 768px) { + nav { + display: block; + width: 100px; } + @columnWidth: (768px - 100px ) / 2; + .example { + pre, + blockquote { + width: @columnWidth; + } + } +} + +@media (min-width: 980px) { nav { - display: none; + width: auto; } - article { - .example { - blockquote { - margin-top: 20px; - } + .content { + margin-left: 30px; + } + + .container { + width: 980px; + } + + .example { + pre { + width: 338px; + } + blockquote { + width: auto; } } } diff --git a/docs/uidesign/confirmable.html b/docs/uidesign/confirmable.html new file mode 100644 index 0000000000..d03582143b --- /dev/null +++ b/docs/uidesign/confirmable.html @@ -0,0 +1,147 @@ + + + + + + + + + + + +

Introduction

+ +

The jquery.confirmable module provides a simple inline confirmation script for potentially destructive or uncancellable actions.

+ +

Possible uses include confirmable "rollback" links in histories, confirmable "unwatch" links on watchlists, or confirmable "thanks" links (provided by the Echo extension).

+ +

Shown below is a demo of how each of those could work on history and watchlist entries, in an LTR and RTL language. The enhanced links are highlighted in blue.

+ +

Examples

+ +

LTR (English)

+ +

Watchlist:

+ + + +

History:

+ + + + + +

RTL (Hebrew)

+ + +

Watchlist:

+ + + +

History:

+ + + + + + + + diff --git a/docs/uidesign/design.html b/docs/uidesign/design.html index a285a5b22f..51c1b55204 100644 --- a/docs/uidesign/design.html +++ b/docs/uidesign/design.html @@ -1,7 +1,7 @@ - + diff --git a/extensions/README b/extensions/README index b665001c7b..bad230e9b0 100644 --- a/extensions/README +++ b/extensions/README @@ -1,23 +1,19 @@ -Extensions (such as the hieroglyphic module WikiHiero) are distributed -separately. Drop them into this extensions directory and enable as -per the extension's directions. +Extensions are distributed separately. Drop them into this directory and enable +as per the extension's installation instructions. -You can find a list of extensions and documentation on the MediaWiki website: - https://www.mediawiki.org/wiki/Category:Extensions +You can find a list of extensions and documentation at +. -If you are a developer, you want to fetch the extension tree in another +If you are a developer, you might want to fetch the extension tree in another directory and make a symbolic link: - mediawiki/extensions$ ln -s ../../extensions-trunk/FooBarExt + mediawiki/extensions$ ln -s ../../extensions-trunk/FooBar Most extensions are available through Git: - https://gerrit.wikimedia.org/r/#/admin/projects/ + https://gerrit.wikimedia.org/r/#/admin/projects/?filter=mediawiki%252Fextensions%252F https://git.wikimedia.org/project/mediawiki -Old extensions are on Subversion: - https://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions/ - Please note that under POSIX systems (Linux...), parent of a symbolic path refers to the link source, NOT to the target! You should check the env diff --git a/images/README b/images/README index ca30bbcbae..e6d6c118d5 100644 --- a/images/README +++ b/images/README @@ -1,5 +1,2 @@ If uploads are enabled in the wiki, files will be put in subdirectories under here. - -Note to upgraders: as of MediaWiki 1.5, the images used in the user -interface have been moved to skins/common/images. diff --git a/img_auth.php b/img_auth.php index 55f17ac7f4..dcd171f94e 100644 --- a/img_auth.php +++ b/img_auth.php @@ -47,6 +47,7 @@ $wgArticlePath = false; # Don't let a "/*" article path clober our action path $wgActionPaths = array( "$wgUploadPath/" ); wfImageAuthMain(); +wfProfileOut( 'img_auth.php' ); wfLogProfilingData(); // Commit and close up! $factory = wfGetLBFactory(); diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index dde8467f2f..9bc92be94c 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -47,12 +47,19 @@ class AjaxDispatcher { */ private $args; + /** + * @var Config + */ + private $config; + /** * Load up our object with user supplied data */ - function __construct() { + function __construct( Config $config ) { wfProfileIn( __METHOD__ ); + $this->config = $config; + $this->mode = ""; if ( !empty( $_GET["rs"] ) ) { @@ -95,17 +102,17 @@ class AjaxDispatcher { * BEWARE! Data are passed as they have been supplied by the user, * they should be carefully handled in the function processing the * request. + * + * @param User $user */ - function performAction() { - global $wgAjaxExportList, $wgUser; - + function performAction( User $user ) { if ( empty( $this->mode ) ) { return; } wfProfileIn( __METHOD__ ); - if ( !in_array( $this->func_name, $wgAjaxExportList ) ) { + if ( !in_array( $this->func_name, $this->config->get( 'AjaxExportList' ) ) ) { wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" ); wfHttpError( @@ -113,7 +120,7 @@ class AjaxDispatcher { 'Bad Request', "unknown function " . $this->func_name ); - } elseif ( !User::isEveryoneAllowed( 'read' ) && !$wgUser->isAllowed( 'read' ) ) { + } elseif ( !User::isEveryoneAllowed( 'read' ) && !$user->isAllowed( 'read' ) ) { wfHttpError( 403, 'Forbidden', diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 41cbd24ca7..8e9f490fa2 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -70,12 +70,19 @@ class AjaxResponse { */ private $mText; + /** + * @var Config + */ + private $mConfig; + /** * @param string|null $text + * @param Config|null $config */ - function __construct( $text = null ) { + function __construct( $text = null, Config $config = null ) { $this->mCacheDuration = null; $this->mVary = null; + $this->mConfig = $config ?: ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); $this->mDisabled = false; $this->mText = ''; @@ -150,8 +157,6 @@ class AjaxResponse { * Construct the header and output it */ function sendHeaders() { - global $wgUseSquid, $wgUseESI; - if ( $this->mResponseCode ) { $n = preg_replace( '/^ *(\d+)/', '\1', $this->mResponseCode ); header( "Status: " . $this->mResponseCode, true, (int)$n ); @@ -170,12 +175,12 @@ class AjaxResponse { # and tell the client to always check with the squid. Otherwise, # tell the client to use a cached copy, without a way to purge it. - if ( $wgUseSquid ) { + if ( $this->mConfig->get( 'UseSquid' ) ) { # Expect explicit purge of the proxy cache, but require end user agents # to revalidate against the proxy on each visit. # Surrogate-Control controls our Squid, Cache-Control downstream caches - if ( $wgUseESI ) { + if ( $this->mConfig->get( 'UseESI' ) ) { header( 'Surrogate-Control: max-age=' . $this->mCacheDuration . ', content="ESI/1.0"' ); header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 9fa0c13784..87cd5d5dfb 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -31,15 +31,17 @@ $wgAutoloadLocalClasses = array( # Includes 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', 'AjaxResponse' => 'includes/AjaxResponse.php', - 'AlphabeticPager' => 'includes/Pager.php', 'AtomFeed' => 'includes/Feed.php', 'AuthPlugin' => 'includes/AuthPlugin.php', 'AuthPluginUser' => 'includes/AuthPlugin.php', 'Autopromote' => 'includes/Autopromote.php', - 'BaseTemplate' => 'includes/SkinTemplate.php', 'Block' => 'includes/Block.php', + 'BloomCache' => 'includes/cache/bloom/BloomCache.php', + 'BloomCacheRedis' => 'includes/cache/bloom/BloomCacheRedis.php', + 'BloomFilterTitleHasLogs' => 'includes/cache/bloom/BloomFilters.php', + 'CacheHelper' => 'includes/CacheHelper.php', 'Category' => 'includes/Category.php', - 'Categoryfinder' => 'includes/Categoryfinder.php', + 'CategoryFinder' => 'includes/CategoryFinder.php', 'CategoryViewer' => 'includes/CategoryViewer.php', 'ChangeTags' => 'includes/ChangeTags.php', 'ChannelFeed' => 'includes/Feed.php', @@ -67,6 +69,7 @@ $wgAutoloadLocalClasses = array( 'DumpPipeOutput' => 'includes/Export.php', 'EditPage' => 'includes/EditPage.php', 'EmailNotification' => 'includes/UserMailer.php', + 'EmptyBloomCache' => 'includes/cache/bloom/BloomCache.php', 'Fallback' => 'includes/Fallback.php', 'FauxRequest' => 'includes/WebRequest.php', 'FauxResponse' => 'includes/WebResponse.php', @@ -83,6 +86,7 @@ $wgAutoloadLocalClasses = array( 'Html' => 'includes/Html.php', 'HtmlFormatter' => 'includes/HtmlFormatter.php', 'HTMLApiField' => 'includes/htmlform/HTMLApiField.php', + 'HTMLAutoCompleteSelectField' => 'includes/htmlform/HTMLAutoCompleteSelectField.php', 'HTMLButtonField' => 'includes/htmlform/HTMLButtonField.php', 'HTMLCheckField' => 'includes/htmlform/HTMLCheckField.php', 'HTMLCheckMatrix' => 'includes/htmlform/HTMLCheckMatrix.php', @@ -101,6 +105,7 @@ $wgAutoloadLocalClasses = array( 'HTMLRadioField' => 'includes/htmlform/HTMLRadioField.php', 'HTMLSelectAndOtherField' => 'includes/htmlform/HTMLSelectAndOtherField.php', 'HTMLSelectField' => 'includes/htmlform/HTMLSelectField.php', + 'HTMLSelectLimitField' => 'includes/htmlform/HTMLSelectLimitField.php', 'HTMLSelectOrOtherField' => 'includes/htmlform/HTMLSelectOrOtherField.php', 'HTMLSubmitField' => 'includes/htmlform/HTMLSubmitField.php', 'HTMLTextAreaField' => 'includes/htmlform/HTMLTextAreaField.php', @@ -110,7 +115,6 @@ $wgAutoloadLocalClasses = array( 'IdentityCollation' => 'includes/Collation.php', 'ImportStreamSource' => 'includes/Import.php', 'ImportStringSource' => 'includes/Import.php', - 'IndexPager' => 'includes/Pager.php', 'Interwiki' => 'includes/interwiki/Interwiki.php', 'License' => 'includes/Licenses.php', 'Licenses' => 'includes/Licenses.php', @@ -120,7 +124,6 @@ $wgAutoloadLocalClasses = array( 'MagicWordArray' => 'includes/MagicWord.php', 'MailAddress' => 'includes/UserMailer.php', 'MediaWiki' => 'includes/MediaWiki.php', - 'MediaWikiI18N' => 'includes/SkinTemplate.php', 'MediaWikiVersionFetcher' => 'includes/MediaWikiVersionFetcher.php', 'Message' => 'includes/Message.php', 'MessageBlobStore' => 'includes/MessageBlobStore.php', @@ -129,7 +132,6 @@ $wgAutoloadLocalClasses = array( 'MWHttpRequest' => 'includes/HttpFunctions.php', 'MWNamespace' => 'includes/MWNamespace.php', 'OutputPage' => 'includes/OutputPage.php', - 'Pager' => 'includes/Pager.php', 'PathRouter' => 'includes/PathRouter.php', 'PathRouterPatternReplacer' => 'includes/PathRouter.php', 'PhpHttpRequest' => 'includes/HttpFunctions.php', @@ -137,15 +139,13 @@ $wgAutoloadLocalClasses = array( 'PoolCounter_Stub' => 'includes/poolcounter/PoolCounter.php', 'PoolCounterRedis' => 'includes/poolcounter/PoolCounterRedis.php', 'PoolCounterWork' => 'includes/poolcounter/PoolCounterWork.php', - 'PoolCounterWorkViaCallback' => 'includes/poolcounter/PoolCounterWork.php', + 'PoolCounterWorkViaCallback' => 'includes/poolcounter/PoolCounterWorkViaCallback.php', 'PoolWorkArticleView' => 'includes/poolcounter/PoolWorkArticleView.php', 'Preferences' => 'includes/Preferences.php', 'PreferencesForm' => 'includes/Preferences.php', 'PrefixSearch' => 'includes/PrefixSearch.php', 'ProtectionForm' => 'includes/ProtectionForm.php', - 'QuickTemplate' => 'includes/SkinTemplate.php', 'RawMessage' => 'includes/Message.php', - 'ReverseChronologicalPager' => 'includes/Pager.php', 'RevisionItem' => 'includes/RevisionList.php', 'RevisionItemBase' => 'includes/RevisionList.php', 'RevisionListBase' => 'includes/RevisionList.php', @@ -156,8 +156,6 @@ $wgAutoloadLocalClasses = array( 'SiteConfiguration' => 'includes/SiteConfiguration.php', 'SiteStats' => 'includes/SiteStats.php', 'SiteStatsInit' => 'includes/SiteStats.php', - 'Skin' => 'includes/Skin.php', - 'SkinTemplate' => 'includes/SkinTemplate.php', 'SquidPurgeClient' => 'includes/SquidPurgeClient.php', 'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php', 'StatCounter' => 'includes/StatCounter.php', @@ -166,7 +164,6 @@ $wgAutoloadLocalClasses = array( 'StringPrefixSearch' => 'includes/PrefixSearch.php', 'StubObject' => 'includes/StubObject.php', 'StubUserLang' => 'includes/StubObject.php', - 'TablePager' => 'includes/Pager.php', 'MWTimestamp' => 'includes/MWTimestamp.php', 'TimestampException' => 'includes/TimestampException.php', 'Title' => 'includes/Title.php', @@ -213,15 +210,16 @@ $wgAutoloadLocalClasses = array( 'RevertAction' => 'includes/actions/RevertAction.php', 'RevisiondeleteAction' => 'includes/actions/RevisiondeleteAction.php', 'RollbackAction' => 'includes/actions/RollbackAction.php', - 'SubmitAction' => 'includes/actions/EditAction.php', - 'UnprotectAction' => 'includes/actions/ProtectAction.php', - 'UnwatchAction' => 'includes/actions/WatchAction.php', + 'SubmitAction' => 'includes/actions/SubmitAction.php', + 'UnprotectAction' => 'includes/actions/UnprotectAction.php', + 'UnwatchAction' => 'includes/actions/UnwatchAction.php', 'ViewAction' => 'includes/actions/ViewAction.php', 'WatchAction' => 'includes/actions/WatchAction.php', # includes/api 'ApiBase' => 'includes/api/ApiBase.php', 'ApiBlock' => 'includes/api/ApiBlock.php', + 'ApiClearHasMsg' => 'includes/api/ApiClearHasMsg.php', 'ApiComparePages' => 'includes/api/ApiComparePages.php', 'ApiCreateAccount' => 'includes/api/ApiCreateAccount.php', 'ApiDelete' => 'includes/api/ApiDelete.php', @@ -236,7 +234,7 @@ $wgAutoloadLocalClasses = array( 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', 'ApiFormatDump' => 'includes/api/ApiFormatDump.php', - 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', + 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatFeedWrapper.php', 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', 'ApiFormatNone' => 'includes/api/ApiFormatNone.php', 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', @@ -310,6 +308,7 @@ $wgAutoloadLocalClasses = array( 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', 'ApiQueryStashImageInfo' => 'includes/api/ApiQueryStashImageInfo.php', 'ApiQueryTags' => 'includes/api/ApiQueryTags.php', + 'ApiQueryTokens' => 'includes/api/ApiQueryTokens.php', 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php', 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', @@ -340,7 +339,6 @@ $wgAutoloadLocalClasses = array( 'HTMLFileCache' => 'includes/cache/HTMLFileCache.php', 'ICacheHelper' => 'includes/cache/CacheHelper.php', 'LCStore' => 'includes/cache/LocalisationCache.php', - 'LCStoreAccel' => 'includes/cache/LocalisationCache.php', 'LCStoreCDB' => 'includes/cache/LocalisationCache.php', 'LCStoreDB' => 'includes/cache/LocalisationCache.php', 'LCStoreNull' => 'includes/cache/LocalisationCache.php', @@ -385,6 +383,8 @@ $wgAutoloadLocalClasses = array( 'CssContent' => 'includes/content/CssContent.php', 'JavaScriptContentHandler' => 'includes/content/JavaScriptContentHandler.php', 'JavaScriptContent' => 'includes/content/JavaScriptContent.php', + 'JSONContentHandler' => 'includes/content/JSONContentHandler.php', + 'JSONContent' => 'includes/content/JSONContent.php', 'MessageContent' => 'includes/content/MessageContent.php', 'MWContentSerializationException' => 'includes/content/ContentHandler.php', 'TextContentHandler' => 'includes/content/TextContentHandler.php', @@ -789,6 +789,13 @@ $wgAutoloadLocalClasses = array( 'WikiFilePage' => 'includes/page/WikiFilePage.php', 'WikiPage' => 'includes/page/WikiPage.php', + # includes/pager + 'AlphabeticPager' => 'includes/pager/AlphabeticPager.php', + 'IndexPager' => 'includes/pager/IndexPager.php', + 'Pager' => 'includes/pager/Pager.php', + 'ReverseChronologicalPager' => 'includes/pager/ReverseChronologicalPager.php', + 'TablePager' => 'includes/pager/TablePager.php', + # includes/parser 'CacheTime' => 'includes/parser/CacheTime.php', 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', @@ -888,25 +895,23 @@ $wgAutoloadLocalClasses = array( 'ResourceLoaderWikiModule' => 'includes/resourceloader/ResourceLoaderWikiModule.php', # includes/revisiondelete - 'RevDelArchivedFileItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelArchivedFileList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelArchivedRevisionItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelArchiveItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelArchiveList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelFileItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelFileList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelItem' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', - 'RevDelList' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', - 'RevDelLogItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelLogList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelRevisionItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDelRevisionList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDelArchivedFileItem' => 'includes/revisiondelete/RevDelArchivedFileItem.php', + 'RevDelArchivedFileList' => 'includes/revisiondelete/RevDelArchivedFileList.php', + 'RevDelArchivedRevisionItem' => 'includes/revisiondelete/RevDelArchivedRevisionItem.php', + 'RevDelArchiveItem' => 'includes/revisiondelete/RevDelArchiveItem.php', + 'RevDelArchiveList' => 'includes/revisiondelete/RevDelArchiveList.php', + 'RevDelFileItem' => 'includes/revisiondelete/RevDelFileItem.php', + 'RevDelFileList' => 'includes/revisiondelete/RevDelFileList.php', + 'RevDelItem' => 'includes/revisiondelete/RevDelItem.php', + 'RevDelList' => 'includes/revisiondelete/RevDelList.php', + 'RevDelLogItem' => 'includes/revisiondelete/RevDelLogItem.php', + 'RevDelLogList' => 'includes/revisiondelete/RevDelLogList.php', + 'RevDelRevisionItem' => 'includes/revisiondelete/RevDelRevisionItem.php', + 'RevDelRevisionList' => 'includes/revisiondelete/RevDelRevisionList.php', 'RevisionDeleter' => 'includes/revisiondelete/RevisionDeleter.php', 'RevisionDeleteUser' => 'includes/revisiondelete/RevisionDeleteUser.php', # includes/search - 'PostgresSearchResult' => 'includes/search/SearchPostgres.php', - 'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php', 'SearchDatabase' => 'includes/search/SearchDatabase.php', 'SearchEngine' => 'includes/search/SearchEngine.php', 'SearchEngineDummy' => 'includes/search/SearchEngine.php', @@ -931,6 +936,17 @@ $wgAutoloadLocalClasses = array( 'Sites' => 'includes/site/SiteSQLStore.php', 'SiteStore' => 'includes/site/SiteStore.php', + # includes/skins + 'BaseTemplate' => 'includes/skins/SkinTemplate.php', + 'MediaWikiI18N' => 'includes/skins/SkinTemplate.php', + 'QuickTemplate' => 'includes/skins/SkinTemplate.php', + 'Skin' => 'includes/skins/Skin.php', + 'SkinException' => 'includes/skins/SkinException.php', + 'SkinFactory' => 'includes/skins/SkinFactory.php', + 'SkinFallback' => 'includes/skins/SkinFallback.php', + 'SkinFallbackTemplate' => 'includes/skins/SkinFallbackTemplate.php', + 'SkinTemplate' => 'includes/skins/SkinTemplate.php', + # includes/specialpage 'ChangesListSpecialPage' => 'includes/specialpage/ChangesListSpecialPage.php', 'FormSpecialPage' => 'includes/specialpage/FormSpecialPage.php', @@ -964,7 +980,6 @@ $wgAutoloadLocalClasses = array( 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', - 'HTMLBlockedUsersItemSelect' => 'includes/specials/SpecialBlockList.php', 'ImageListPager' => 'includes/specials/SpecialListfiles.php', 'ImportReporter' => 'includes/specials/SpecialImport.php', 'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php', diff --git a/includes/Block.php b/includes/Block.php index 45bae28249..6a29a0567f 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -80,6 +80,20 @@ class Block { /** * @todo FIXME: Don't know what the best format to have for this constructor * is, but fourteen optional parameters certainly isn't it. + * @param string $address + * @param int $user + * @param int $by + * @param string $reason + * @param mixed $timestamp + * @param int $auto + * @param string $expiry + * @param int $anonOnly + * @param int $createAccount + * @param int $enableAutoblock + * @param int $hideName + * @param int $blockEmail + * @param int $allowUsertalk + * @param string $byText */ function __construct( $address = '', $user = 0, $by = 0, $reason = '', $timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, @@ -582,7 +596,6 @@ class Block { * * @param Block $block * @param array &$blockIds - * @return array Block IDs of retroactive autoblocks made */ protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) { global $wgPutIPinRC; @@ -1105,11 +1118,11 @@ class Block { * - If there are multiple exact or range blocks at the same level, the one chosen * is random * + * @param array $blocks Array of blocks * @param array $ipChain List of IPs (strings). This is used to determine how "close" * a block is to the server, and if a block matches exactly, or is in a range. * The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2, * local-squid, ...) - * @param array $block Array of blocks * @return Block|null The "best" block from the list */ public static function chooseBlock( array $blocks, array $ipChain ) { diff --git a/includes/Category.php b/includes/Category.php index 7bab464bf7..322b0530b5 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -291,6 +291,7 @@ class Category { /** * Generic accessor + * @param string $key * @return bool */ private function getX( $key ) { diff --git a/includes/CategoryFinder.php b/includes/CategoryFinder.php new file mode 100644 index 0000000000..cf537e15e5 --- /dev/null +++ b/includes/CategoryFinder.php @@ -0,0 +1,244 @@ + + * # Determines whether the article with the page_id 12345 is in both + * # "Category 1" and "Category 2" or their subcategories, respectively + * + * $cf = new CategoryFinder; + * $cf->seed( + * array( 12345 ), + * array( 'Category 1', 'Category 2' ), + * 'AND' + * ); + * $a = $cf->run(); + * print implode( ',' , $a ); + * + * + */ +class CategoryFinder { + /** @var int[] The original article IDs passed to the seed function */ + protected $articles = array(); + + /** @var array Array of DBKEY category names for categories that don't have a page */ + protected $deadend = array(); + + /** @var array Array of [ID => array()] */ + protected $parents = array(); + + /** @var array Array of article/category IDs */ + protected $next = array(); + + /** @var array Array of DBKEY category names */ + protected $targets = array(); + + /** @var array */ + protected $name2id = array(); + + /** @var string "AND" or "OR" */ + protected $mode; + + /** @var DatabaseBase Read-DB slave */ + protected $dbr; + + /** + * Initializes the instance. Do this prior to calling run(). + * @param array $articleIds Array of article IDs + * @param array $categories FIXME + * @param string $mode FIXME, default 'AND'. + * @todo FIXME: $categories/$mode + */ + public function seed( $articleIds, $categories, $mode = 'AND' ) { + $this->articles = $articleIds; + $this->next = $articleIds; + $this->mode = $mode; + + # Set the list of target categories; convert them to DBKEY form first + $this->targets = array(); + foreach ( $categories as $c ) { + $ct = Title::makeTitleSafe( NS_CATEGORY, $c ); + if ( $ct ) { + $c = $ct->getDBkey(); + $this->targets[$c] = $c; + } + } + } + + /** + * Iterates through the parent tree starting with the seed values, + * then checks the articles if they match the conditions + * @return array Array of page_ids (those given to seed() that match the conditions) + */ + public function run() { + $this->dbr = wfGetDB( DB_SLAVE ); + while ( count( $this->next ) > 0 ) { + $this->scanNextLayer(); + } + + # Now check if this applies to the individual articles + $ret = array(); + + foreach ( $this->articles as $article ) { + $conds = $this->targets; + if ( $this->check( $article, $conds ) ) { + # Matches the conditions + $ret[] = $article; + } + } + return $ret; + } + + /** + * Get the parents. Only really useful if run() has been called already + * @return array + */ + public function getParents() { + return $this->parents; + } + + /** + * This functions recurses through the parent representation, trying to match the conditions + * @param int $id The article/category to check + * @param array $conds The array of categories to match + * @param array $path Used to check for recursion loops + * @return bool Does this match the conditions? + */ + private function check( $id, &$conds, $path = array() ) { + // Check for loops and stop! + if ( in_array( $id, $path ) ) { + return false; + } + + $path[] = $id; + + # Shortcut (runtime paranoia): No conditions=all matched + if ( count( $conds ) == 0 ) { + return true; + } + + if ( !isset( $this->parents[$id] ) ) { + return false; + } + + # iterate through the parents + foreach ( $this->parents[$id] as $p ) { + $pname = $p->cl_to; + + # Is this a condition? + if ( isset( $conds[$pname] ) ) { + # This key is in the category list! + if ( $this->mode == 'OR' ) { + # One found, that's enough! + $conds = array(); + return true; + } else { + # Assuming "AND" as default + unset( $conds[$pname] ); + if ( count( $conds ) == 0 ) { + # All conditions met, done + return true; + } + } + } + + # Not done yet, try sub-parents + if ( !isset( $this->name2id[$pname] ) ) { + # No sub-parent + continue; + } + $done = $this->check( $this->name2id[$pname], $conds, $path ); + if ( $done || count( $conds ) == 0 ) { + # Subparents have done it! + return true; + } + } + return false; + } + + /** + * Scans a "parent layer" of the articles/categories in $this->next + */ + private function scanNextLayer() { + $profiler = new ProfileSection( __METHOD__ ); + + # Find all parents of the article currently in $this->next + $layer = array(); + $res = $this->dbr->select( + /* FROM */ 'categorylinks', + /* SELECT */ '*', + /* WHERE */ array( 'cl_from' => $this->next ), + __METHOD__ . '-1' + ); + foreach ( $res as $o ) { + $k = $o->cl_to; + + # Update parent tree + if ( !isset( $this->parents[$o->cl_from] ) ) { + $this->parents[$o->cl_from] = array(); + } + $this->parents[$o->cl_from][$k] = $o; + + # Ignore those we already have + if ( in_array( $k, $this->deadend ) ) { + continue; + } + + if ( isset( $this->name2id[$k] ) ) { + continue; + } + + # Hey, new category! + $layer[$k] = $k; + } + + $this->next = array(); + + # Find the IDs of all category pages in $layer, if they exist + if ( count( $layer ) > 0 ) { + $res = $this->dbr->select( + /* FROM */ 'page', + /* SELECT */ array( 'page_id', 'page_title' ), + /* WHERE */ array( 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ), + __METHOD__ . '-2' + ); + foreach ( $res as $o ) { + $id = $o->page_id; + $name = $o->page_title; + $this->name2id[$name] = $id; + $this->next[] = $id; + unset( $layer[$name] ); + } + } + + # Mark dead ends + foreach ( $layer as $v ) { + $this->deadend[$v] = $v; + } + } +} diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index 22eb3d1ce7..7581ae40d4 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -25,10 +25,10 @@ class CategoryViewer extends ContextSource { public $limit; /** @var array */ - protected $from; + public $from; /** @var array */ - protected $until; + public $until; /** @var string[] */ public $articles; @@ -37,37 +37,37 @@ class CategoryViewer extends ContextSource { public $articles_start_char; /** @var array */ - protected $children; + public $children; /** @var array */ - protected $children_start_char; + public $children_start_char; /** @var bool */ - protected $showGallery; + public $showGallery; /** @var array */ - protected $imgsNoGallery_start_char; + public $imgsNoGallery_start_char; /** @var array */ - protected $imgsNoGallery; + public $imgsNoGallery; /** @var array */ - protected $nextPage; + public $nextPage; /** @var array */ protected $prevPage; /** @var array */ - protected $flip; + public $flip; /** @var Title */ - protected $title; + public $title; /** @var Collation */ - protected $collation; + public $collation; /** @var ImageGallery */ - protected $gallery; + public $gallery; /** @var Category Category object for this page. */ private $cat; @@ -87,12 +87,11 @@ class CategoryViewer extends ContextSource { function __construct( $title, IContextSource $context, $from = array(), $until = array(), $query = array() ) { - global $wgCategoryPagingLimit; $this->title = $title; $this->setContext( $context ); $this->from = $from; $this->until = $until; - $this->limit = $wgCategoryPagingLimit; + $this->limit = $context->getConfig()->get( 'CategoryPagingLimit' ); $this->cat = Category::newFromTitle( $title ); $this->query = $query; $this->collation = Collation::singleton(); @@ -105,10 +104,10 @@ class CategoryViewer extends ContextSource { * @return string HTML output */ public function getHTML() { - global $wgCategoryMagicGallery; wfProfileIn( __METHOD__ ); - $this->showGallery = $wgCategoryMagicGallery && !$this->getOutput()->mNoGallery; + $this->showGallery = $this->getConfig()->get( 'CategoryMagicGallery' ) + && !$this->getOutput()->mNoGallery; $this->clearCategoryState(); $this->doCategoryQuery(); @@ -154,14 +153,13 @@ class CategoryViewer extends ContextSource { // Note that null for mode is taken to mean use default. $mode = $this->getRequest()->getVal( 'gallerymode', null ); try { - $this->gallery = ImageGalleryBase::factory( $mode ); + $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() ); } catch ( MWException $e ) { // User specified something invalid, fallback to default. - $this->gallery = ImageGalleryBase::factory(); + $this->gallery = ImageGalleryBase::factory( false, $this->getContext() ); } $this->gallery->setHideBadImages(); - $this->gallery->setContext( $this->getContext() ); } else { $this->imgsNoGallery = array(); $this->imgsNoGallery_start_char = array(); diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php deleted file mode 100644 index a5415afd3e..0000000000 --- a/includes/Categoryfinder.php +++ /dev/null @@ -1,241 +0,0 @@ - - * # Determines whether the article with the page_id 12345 is in both - * # "Category 1" and "Category 2" or their subcategories, respectively - * - * $cf = new Categoryfinder; - * $cf->seed( - * array( 12345 ), - * array( 'Category 1', 'Category 2' ), - * 'AND' - * ); - * $a = $cf->run(); - * print implode( ',' , $a ); - * - * - */ -class Categoryfinder { - /** @var int[] The original article IDs passed to the seed function */ - protected $articles = array(); - - /** @var array Array of DBKEY category names for categories that don't have a page */ - protected $deadend = array(); - - /** @var array Array of [ID => array()] */ - protected $parents = array(); - - /** @var array Array of article/category IDs */ - protected $next = array(); - - /** @var array Array of DBKEY category names */ - protected $targets = array(); - - /** @var array */ - protected $name2id = array(); - - /** @var string "AND" or "OR" */ - protected $mode; - - /** @var DatabaseBase Read-DB slave */ - protected $dbr; - - function __construct() { - } - - /** - * Initializes the instance. Do this prior to calling run(). - * @param array $article_ids Array of article IDs - * @param array $categories FIXME - * @param string $mode FIXME, default 'AND'. - * @todo FIXME: $categories/$mode - */ - function seed( $article_ids, $categories, $mode = 'AND' ) { - $this->articles = $article_ids; - $this->next = $article_ids; - $this->mode = $mode; - - # Set the list of target categories; convert them to DBKEY form first - $this->targets = array(); - foreach ( $categories as $c ) { - $ct = Title::makeTitleSafe( NS_CATEGORY, $c ); - if ( $ct ) { - $c = $ct->getDBkey(); - $this->targets[$c] = $c; - } - } - } - - /** - * Iterates through the parent tree starting with the seed values, - * then checks the articles if they match the conditions - * @return array Array of page_ids (those given to seed() that match the conditions) - */ - function run() { - $this->dbr = wfGetDB( DB_SLAVE ); - while ( count( $this->next ) > 0 ) { - $this->scan_next_layer(); - } - - # Now check if this applies to the individual articles - $ret = array(); - - foreach ( $this->articles as $article ) { - $conds = $this->targets; - if ( $this->check( $article, $conds ) ) { - # Matches the conditions - $ret[] = $article; - } - } - return $ret; - } - - /** - * This functions recurses through the parent representation, trying to match the conditions - * @param int $id The article/category to check - * @param array $conds The array of categories to match - * @param array $path Used to check for recursion loops - * @return bool Does this match the conditions? - */ - function check( $id, &$conds, $path = array() ) { - // Check for loops and stop! - if ( in_array( $id, $path ) ) { - return false; - } - - $path[] = $id; - - # Shortcut (runtime paranoia): No conditions=all matched - if ( count( $conds ) == 0 ) { - return true; - } - - if ( !isset( $this->parents[$id] ) ) { - return false; - } - - # iterate through the parents - foreach ( $this->parents[$id] as $p ) { - $pname = $p->cl_to; - - # Is this a condition? - if ( isset( $conds[$pname] ) ) { - # This key is in the category list! - if ( $this->mode == 'OR' ) { - # One found, that's enough! - $conds = array(); - return true; - } else { - # Assuming "AND" as default - unset( $conds[$pname] ); - if ( count( $conds ) == 0 ) { - # All conditions met, done - return true; - } - } - } - - # Not done yet, try sub-parents - if ( !isset( $this->name2id[$pname] ) ) { - # No sub-parent - continue; - } - $done = $this->check( $this->name2id[$pname], $conds, $path ); - if ( $done || count( $conds ) == 0 ) { - # Subparents have done it! - return true; - } - } - return false; - } - - /** - * Scans a "parent layer" of the articles/categories in $this->next - */ - function scan_next_layer() { - wfProfileIn( __METHOD__ ); - - # Find all parents of the article currently in $this->next - $layer = array(); - $res = $this->dbr->select( - /* FROM */ 'categorylinks', - /* SELECT */ '*', - /* WHERE */ array( 'cl_from' => $this->next ), - __METHOD__ . '-1' - ); - foreach ( $res as $o ) { - $k = $o->cl_to; - - # Update parent tree - if ( !isset( $this->parents[$o->cl_from] ) ) { - $this->parents[$o->cl_from] = array(); - } - $this->parents[$o->cl_from][$k] = $o; - - # Ignore those we already have - if ( in_array( $k, $this->deadend ) ) { - continue; - } - - if ( isset( $this->name2id[$k] ) ) { - continue; - } - - # Hey, new category! - $layer[$k] = $k; - } - - $this->next = array(); - - # Find the IDs of all category pages in $layer, if they exist - if ( count( $layer ) > 0 ) { - $res = $this->dbr->select( - /* FROM */ 'page', - /* SELECT */ array( 'page_id', 'page_title' ), - /* WHERE */ array( 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ), - __METHOD__ . '-2' - ); - foreach ( $res as $o ) { - $id = $o->page_id; - $name = $o->page_title; - $this->name2id[$name] = $id; - $this->next[] = $id; - unset( $layer[$name] ); - } - } - - # Mark dead ends - foreach ( $layer as $v ) { - $this->deadend[$v] = $v; - } - - wfProfileOut( __METHOD__ ); - } -} diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index dea6e716d5..6626ab6160 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -758,7 +758,7 @@ $wgFileBlacklist = array( 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl' ); /** - * Files with these mime types will never be allowed as uploads + * Files with these MIME types will never be allowed as uploads * if $wgVerifyMimeType is enabled. */ $wgMimeTypeBlacklist = array( @@ -810,7 +810,7 @@ $wgDisableUploadScriptChecks = false; $wgUploadSizeWarning = false; /** - * list of trusted media-types and mime types. + * list of trusted media-types and MIME types. * Use the MEDIATYPE_xxx constants to represent media types. * This list is used by File::isSafeFile * @@ -858,9 +858,11 @@ $wgContentHandlers = array( CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // dumb version, no syntax highlighting CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', + // simple implementation, for use by extensions, etc. + CONTENT_MODEL_JSON => 'JSONContentHandler', // dumb version, no syntax highlighting CONTENT_MODEL_CSS => 'CssContentHandler', - // plain text, for use by extensions etc + // plain text, for use by extensions, etc. CONTENT_MODEL_TEXT => 'TextContentHandler', ); @@ -1133,45 +1135,45 @@ $wgAntivirusSetup = array( $wgAntivirusRequired = true; /** - * Determines if the mime type of uploaded files should be checked + * Determines if the MIME type of uploaded files should be checked */ $wgVerifyMimeType = true; /** - * Sets the mime type definition file to use by MimeMagic.php. + * Sets the MIME type definition file to use by MimeMagic.php. * Set to null, to use built-in defaults only. * example: $wgMimeTypeFile = '/etc/mime.types'; */ $wgMimeTypeFile = 'includes/mime.types'; /** - * Sets the mime type info file to use by MimeMagic.php. + * Sets the MIME type info file to use by MimeMagic.php. * Set to null, to use built-in defaults only. */ $wgMimeInfoFile = 'includes/mime.info'; /** - * Sets an external mime detector program. The command must print only - * the mime type to standard output. + * Sets an external MIME detector program. The command must print only + * the MIME type to standard output. * The name of the file to process will be appended to the command given here. - * If not set or NULL, mime_content_type will be used if available. + * If not set or NULL, PHP's fileinfo extension will be used if available. * * @par Example: * @code - * #$wgMimeDetectorCommand = "file -bi"; # use external mime detector (Linux) + * #$wgMimeDetectorCommand = "file -bi"; # use external MIME detector (Linux) * @endcode */ $wgMimeDetectorCommand = null; /** - * Switch for trivial mime detection. Used by thumb.php to disable all fancy + * Switch for trivial MIME detection. Used by thumb.php to disable all fancy * things, because only a few types of images are needed and file extensions * can be trusted. */ $wgTrivialMimeDetection = false; /** - * Additional XML types we can allow via mime-detection. + * Additional XML types we can allow via MIME-detection. * array = ( 'rootElement' => 'associatedMimeType' ) */ $wgXMLMimeTypes = array( @@ -2073,6 +2075,28 @@ $wgObjectCaches = array( 'hash' => array( 'class' => 'HashBagOStuff' ), ); +/** + * Map of bloom filter store names to configuration arrays. + * + * Example: + * $wgBloomFilterStores['main'] = array( + * 'cacheId' => 'main-v1', + * 'class' => 'BloomCacheRedis', + * 'redisServers' => array( '127.0.0.1:6379' ), + * 'redisConfig' => array( 'connectTimeout' => 2 ) + * ); + * + * A primary bloom filter must be created manually. + * Example in eval.php: + * + * BloomCache::get( 'main' )->init( 'shared', 1000000000, .001 ); + * + * The size should be as large as practical given wiki size and resources. + * + * @since 1.24 + */ +$wgBloomFilterStores = array(); + /** * The expiry time for the parser cache, in seconds. * The default is 86400 (one day). @@ -2445,42 +2469,6 @@ $wgSquidPurgeUseHostHeader = true; */ $wgHTCPRouting = array(); -/** - * @deprecated since 1.22, please use $wgHTCPRouting instead. - * - * Whenever this is set and $wgHTCPRouting evaluates to false, $wgHTCPRouting - * will be set to this value. - * This is merely for back compatibility. - * - * @since 1.20 - */ -$wgHTCPMulticastRouting = null; - -/** - * HTCP multicast address. Set this to a multicast IP address to enable HTCP. - * - * Note that MediaWiki uses the old non-RFC compliant HTCP format, which was - * present in the earliest Squid implementations of the protocol. - * - * This setting is DEPRECATED in favor of $wgHTCPRouting , and kept for - * backwards compatibility only. If $wgHTCPRouting is set, this setting is - * ignored. If $wgHTCPRouting is not set and this setting is, it is used to - * populate $wgHTCPRouting. - * - * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting and since 1.22 in - * favor of $wgHTCPRouting. - */ -$wgHTCPMulticastAddress = false; - -/** - * HTCP multicast port. - * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting and since 1.22 in - * favor of $wgHTCPRouting. - * - * @see $wgHTCPMulticastAddress - */ -$wgHTCPPort = 4827; - /** * HTCP multicast TTL. * @see $wgHTCPRouting @@ -2733,11 +2721,6 @@ $wgDisableLangConversion = false; */ $wgDisableTitleConversion = false; -/** - * Whether to enable canonical language links in meta data. - */ -$wgCanonicalLanguageLinks = true; - /** * Default variant code, if false, the default will be the language code */ @@ -2885,6 +2868,23 @@ $wgHtml5 = true; */ $wgHtml5Version = null; +/** + * Temporary variable that allows HTMLForms to be rendered as tables. + * Table based layouts cause various issues when designing for mobile. + * This global allows skins or extensions a means to force non-table based rendering. + * Setting to false forces form components to always render as div elements. + * @since 1.24 + */ +$wgHTMLFormAllowTableFormat = true; + +/** + * Temporary variable that applies MediaWiki UI wherever it can be supported. + * Temporary variable that should be removed when mediawiki ui is more + * stable and change has been communicated. + * @since 1.24 + */ +$wgUseMediaWikiUIEverywhere = false; + /** * Enabled RDFa attributes for use in wikitext. * NOTE: Interaction with HTML5 is somewhat underspecified. @@ -2926,7 +2926,7 @@ $wgWellFormedXml = true; * Normally we wouldn't have to define this in the root "" * element, but IE needs it there in some circumstances. * - * This is ignored if $wgMimeType is set to a non-XML mimetype. + * This is ignored if $wgMimeType is set to a non-XML MIME type. */ $wgXhtmlNamespaces = array(); @@ -2968,7 +2968,7 @@ $wgDefaultSkin = 'vector'; * * @since 1.24 */ -$wgFallbackSkin = 'vector'; +$wgFallbackSkin = 'fallback'; /** * Specify the names of skins that should not be presented in the list of @@ -3117,20 +3117,6 @@ $wgFooterIcons = array( */ $wgUseCombinedLoginLink = false; -/** - * Search form look for Vector skin only. - * - true = use an icon search button - * - false = use Go & Search buttons - */ -$wgVectorUseSimpleSearch = true; - -/** - * Watch and unwatch as an icon rather than a link for Vector skin only. - * - true = use an icon watch/unwatch button - * - false = use watch/unwatch text link - */ -$wgVectorUseIconWatch = true; - /** * Display user edit counts in various prominent places. */ @@ -3305,10 +3291,7 @@ $wgResourceModuleSkinStyles = array(); * * @par Example: * @code - * $wgResourceLoaderSources['foo'] = array( - * 'loadScript' => 'http://example.org/w/load.php', - * 'apiScript' => 'http://example.org/w/api.php' - * ); + * $wgResourceLoaderSources['foo'] = 'http://example.org/w/load.php'; * @endcode */ $wgResourceLoaderSources = array(); @@ -3477,12 +3460,14 @@ $wgResourceLoaderValidateStaticJS = false; $wgResourceLoaderExperimentalAsyncLoading = false; /** - * Global LESS variables. An associative array binding variable names to CSS - * string values. + * Global LESS variables. An associative array binding variable names to + * LESS code snippets representing their values. + * + * Adding an item here is equivalent to writing `@variable: value;` + * at the beginning of all your .less files, with all the consequences. + * In particular, string values must be escaped and quoted. * - * Because the hashed contents of this array are used to construct the cache key - * that ResourceLoader uses to look up LESS compilation results, updating this - * array can be used to deliberately invalidate the set of cached results. + * Changes to LESS variables do not trigger cache invalidation. * * @par Example: * @code @@ -3500,10 +3485,7 @@ $wgResourceLoaderLESSVars = array(); * Custom LESS functions. An associative array mapping function name to PHP * callable. * - * Changes to LESS functions do not trigger cache invalidation. If you update - * the behavior of a LESS function and need to invalidate stale compilation - * results, you can touch one of values in $wgResourceLoaderLESSVars, as - * documented above. + * Changes to LESS functions do not trigger cache invalidation. * * @since 1.22 */ @@ -3892,6 +3874,12 @@ $wgMaxPPExpandDepth = 40; /** * URL schemes that should be recognized as valid by wfParseUrl(). + * + * WARNING: Do not add 'file:' to this or internal file links will be broken. + * Instead, if you want to support file links, add 'file://'. The same applies + * to any other protocols with the same name as a namespace. See bug #44011 for + * more information. + * * @see wfParseUrl */ $wgUrlProtocols = array( @@ -4146,7 +4134,7 @@ $wgInvalidPasswordReset = true; * * @since 1.24 */ -$wgPasswordDefault = 'B'; +$wgPasswordDefault = 'pbkdf2'; /** * Configuration for built-in password types. Maps the password type @@ -4279,7 +4267,7 @@ $wgDefaultUserOptions = array( 'showtoolbar' => 1, 'skin' => false, 'stubthreshold' => 0, - 'thumbsize' => 2, + 'thumbsize' => 5, 'underline' => 2, 'uselivepreview' => 0, 'usenewrc' => 0, @@ -5323,9 +5311,9 @@ $wgUDPProfilerPort = '3811'; /** * Format string for the UDP profiler. The UDP profiler invokes sprintf() with - * (profile id, count, cpu, cpu_sq, real, real_sq, entry name) as arguments. - * You can use sprintf's argument numbering/swapping capability to repeat, - * re-order or omit fields. + * (profile id, count, cpu, cpu_sq, real, real_sq, entry name, memory) as + * arguments. You can use sprintf's argument numbering/swapping capability to + * repeat, re-order or omit fields. * * @see $wgStatsFormatString * @since 1.22 @@ -5689,9 +5677,9 @@ $wgGitBin = '/usr/bin/git'; */ $wgGitRepositoryViewers = array( 'https://(?:[a-z0-9_]+@)?gerrit.wikimedia.org/r/(?:p/)?(.*)' => - 'https://git.wikimedia.org/commit/%r/%H', + 'https://git.wikimedia.org/tree/%r/%H', 'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' => - 'https://git.wikimedia.org/commit/%r/%H', + 'https://git.wikimedia.org/tree/%r/%H', ); /** @} */ # End of maintenance } @@ -5729,48 +5717,6 @@ $wgRCLinkLimits = array( 50, 100, 250, 500 ); */ $wgRCLinkDays = array( 1, 3, 7, 14, 30 ); -/** - * Send recent changes updates via UDP. The updates will be formatted for IRC. - * Set this to the IP address of the receiver. - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPAddress = false; - -/** - * Port number for RC updates - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPPort = false; - -/** - * Prefix to prepend to each UDP packet. - * This can be used to identify the wiki. A script is available called - * mxircecho.py which listens on a UDP port, and uses a prefix ending in a - * tab to identify the IRC channel to send the log line to. - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPPrefix = ''; - -/** - * If this is set to true, the first entry in the $wgLocalInterwikis array (or - * the value of $wgLocalInterwiki, if set) will be prepended to links in the IRC - * feed. If this is set to a string, that string will be used as the prefix. - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPInterwikiPrefix = false; - -/** - * Set to true to omit "bot" edits (by users with the bot permission) from the - * UDP feed. - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPOmitBots = false; - /** * Destinations to which notifications about recent changes * should be sent. @@ -5798,9 +5744,6 @@ $wgRC2UDPOmitBots = false; * The JSON-specific options are: * * 'channel' -- if set, the 'channel' parameter is also set in JSON values. * - * To ensure backwards-compatibility, whenever $wgRC2UDPAddress is set, a - * 'default' feed will be created reusing the deprecated $wgRC2UDP* variables. - * * @example $wgRCFeeds['example'] = array( * 'formatter' => 'JSONRCFeedFormatter', * 'uri' => "udp://localhost:1336", @@ -6071,6 +6014,17 @@ $wgShowCreditsIfMax = true; * Special:Import (for sysops). Since complete page history can be imported, * these should be 'trusted'. * + * This can either be a regular array, or an associative map specifying + * subprojects on the interwiki map of the target wiki, or a mix of the two, + * e.g. + * @code + * $wgImportSources = array( + * 'wikipedia' => array( 'cs', 'en', 'fr', 'zh' ), + * 'wikispecies', + * 'wikia' => array( 'animanga', 'brickipedia', 'desserts' ), + * ); + * @endcode + * * If a user has the 'import' permission but not the 'importupload' permission, * they will only be able to run imports through this transwiki interface. */ @@ -6610,9 +6564,6 @@ $wgLogActions = array( 'protect/modify' => 'modifiedarticleprotection', 'protect/unprotect' => 'unprotectedarticle', 'protect/move_prot' => 'movedarticleprotection', - 'upload/upload' => 'uploadedimage', - 'upload/overwrite' => 'overwroteimage', - 'upload/revert' => 'uploadedimage', 'import/upload' => 'import-logentry-upload', 'import/interwiki' => 'import-logentry-interwiki', 'merge/merge' => 'pagemerge-logentry', @@ -6639,6 +6590,9 @@ $wgLogActionsHandlers = array( 'patrol/patrol' => 'PatrolLogFormatter', 'rights/rights' => 'RightsLogFormatter', 'rights/autopromote' => 'RightsLogFormatter', + 'upload/upload' => 'LogFormatter', + 'upload/overwrite' => 'LogFormatter', + 'upload/revert' => 'LogFormatter', ); /** @@ -6672,11 +6626,6 @@ $wgDisableQueryPageUpdate = false; */ $wgSpecialPageGroups = array(); -/** - * Whether or not to sort special pages in Special:Specialpages - */ -$wgSortSpecialPages = true; - /** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. @@ -6841,16 +6790,45 @@ $wgDebugAPI = false; /** * API module extensions. - * Associative array mapping module name to class name. - * Extension modules may override the core modules. * + * Associative array mapping module name to modules specs; + * Each module spec is an associative array containing at least + * the 'class' key for the module's class, and optionally a + * 'factory' key for the factory function to use for the module. + * + * That factory function will be called with two parameters, + * the parent module (an instance of ApiBase, usually ApiMain) + * and the name the module was registered under. The return + * value must be an instance of the class given in the 'class' + * field. + * + * For backward compatibility, the module spec may also be a + * simple string containing the module's class name. In that + * case, the class' constructor will be called with the parent + * module and module name as parameters, as described above. + * + * Examples for registering API modules: + * + * @code + * $wgAPIModules['foo'] = 'ApiFoo'; + * $wgAPIModules['bar'] = array( + * 'class' => 'ApiBar', + * 'factory' => function( $main, $name ) { ... } + * ); + * $wgAPIModules['xyzzy'] = array( + * 'class' => 'ApiXyzzy', + * 'factory' => array( 'XyzzyFactory', 'newApiModule' ) + * ); + * @endcode + * + * Extension modules may override the core modules. * See ApiMain::$Modules for a list of the core modules. */ $wgAPIModules = array(); /** * API format module extensions. - * Associative array mapping format module name to class name. + * Associative array mapping format module name to module specs (see $wgAPIModules). * Extension modules may override the core modules. * * See ApiMain::$Formats for a list of the core format modules. @@ -6859,7 +6837,7 @@ $wgAPIFormatModules = array(); /** * API Query meta module extensions. - * Associative array mapping meta module name to class name. + * Associative array mapping meta module name to module specs (see $wgAPIModules). * Extension modules may override the core modules. * * See ApiQuery::$QueryMetaModules for a list of the core meta modules. @@ -6868,7 +6846,7 @@ $wgAPIMetaModules = array(); /** * API Query prop module extensions. - * Associative array mapping properties module name to class name. + * Associative array mapping prop module name to module specs (see $wgAPIModules). * Extension modules may override the core modules. * * See ApiQuery::$QueryPropModules for a list of the core prop modules. @@ -6877,7 +6855,7 @@ $wgAPIPropModules = array(); /** * API Query list module extensions. - * Associative array mapping list module name to class name. + * Associative array mapping list module name to module specs (see $wgAPIModules). * Extension modules may override the core modules. * * See ApiQuery::$QueryListModules for a list of the core list modules. @@ -7299,7 +7277,7 @@ $wgPageLanguageUseDB = false; * @var bool * @since 1.24 */ -$wgUseLinkNamespaceDBFields = false; +$wgUseLinkNamespaceDBFields = true; /** * For really cool vim folding this needs to be at the end: diff --git a/includes/Defines.php b/includes/Defines.php index e0579cbf06..017e9ea4da 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -281,6 +281,7 @@ define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' ); define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' ); define( 'CONTENT_MODEL_CSS', 'css' ); define( 'CONTENT_MODEL_TEXT', 'text' ); +define( 'CONTENT_MODEL_JSON', 'json' ); /**@}*/ /**@{ diff --git a/includes/EditPage.php b/includes/EditPage.php index 6454cfa0da..87bdf91e8c 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -185,7 +185,7 @@ class EditPage { private $mContextTitle = null; /** @var string */ - protected $action = 'submit'; + public $action = 'submit'; /** @var bool */ public $isConflict = false; @@ -200,13 +200,13 @@ class EditPage { public $isJsSubpage = false; /** @var bool */ - protected $isWrongCaseCssJsPage = false; + public $isWrongCaseCssJsPage = false; /** @var bool New page or new section */ - protected $isNew = false; + public $isNew = false; /** @var bool */ - protected $deletedSinceEdit; + public $deletedSinceEdit; /** @var string */ public $formtype; @@ -215,34 +215,34 @@ class EditPage { public $firsttime; /** @var bool|stdClass */ - protected $lastDelete; + public $lastDelete; /** @var bool */ - protected $mTokenOk = false; + public $mTokenOk = false; /** @var bool */ - protected $mTokenOkExceptSuffix = false; + public $mTokenOkExceptSuffix = false; /** @var bool */ - protected $mTriedSave = false; + public $mTriedSave = false; /** @var bool */ - protected $incompleteForm = false; + public $incompleteForm = false; /** @var bool */ - protected $tooBig = false; + public $tooBig = false; /** @var bool */ - protected $kblength = false; + public $kblength = false; /** @var bool */ - protected $missingComment = false; + public $missingComment = false; /** @var bool */ - protected $missingSummary = false; + public $missingSummary = false; /** @var bool */ - protected $allowBlankSummary = false; + public $allowBlankSummary = false; /** @var bool */ protected $blankArticle = false; @@ -251,19 +251,19 @@ class EditPage { protected $allowBlankArticle = false; /** @var string */ - protected $autoSumm = ''; + public $autoSumm = ''; /** @var string */ public $hookError = ''; /** @var ParserOutput */ - protected $mParserOutput; + public $mParserOutput; /** @var bool Has a summary been preset using GET parameter &summary= ? */ - protected $hasPresetSummary = false; + public $hasPresetSummary = false; /** @var bool */ - protected $mBaseRevision = false; + public $mBaseRevision = false; /** @var bool */ public $mShowSummaryField = true; @@ -277,16 +277,16 @@ class EditPage { public $preview = false; /** @var bool */ - protected $diff = false; + public $diff = false; /** @var bool */ public $minoredit = false; /** @var bool */ - protected $watchthis = false; + public $watchthis = false; /** @var bool */ - protected $recreate = false; + public $recreate = false; /** @var string */ public $textbox1 = ''; @@ -298,7 +298,7 @@ class EditPage { public $summary = ''; /** @var bool */ - protected $nosummary = false; + public $nosummary = false; /** @var string */ public $edittime = ''; @@ -310,13 +310,13 @@ class EditPage { public $sectiontitle = ''; /** @var string */ - protected $starttime = ''; + public $starttime = ''; /** @var int */ public $oldid = 0; /** @var string */ - protected $editintro = ''; + public $editintro = ''; /** @var null */ public $scrolltop = null; @@ -1468,9 +1468,8 @@ class EditPage { $cleanSummary = $wgParser->stripSectionName( $this->summary ); return wfMessage( 'newsectionsummary' ) ->rawParams( $cleanSummary )->inContentLanguage()->text(); - } else { - return $this->summary; } + return $this->summary; } /** @@ -2426,9 +2425,7 @@ class EditPage { $wgOut->addHTML( $this->editFormTextBeforeContent ); - if ( $this->contentModel === CONTENT_MODEL_WIKITEXT && - $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) - { + if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) { $wgOut->addHTML( EditPage::getEditToolbar() ); } @@ -2613,9 +2610,18 @@ class EditPage { ); } elseif ( $wgUser->isAnon() ) { if ( $this->formtype != 'preview' ) { - $wgOut->wrapWikiMsg( "
\n$1
", 'anoneditwarning' ); + $wgOut->wrapWikiMsg( + "
\n$1\n
", + array( 'anoneditwarning', + // Log-in link + '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}', + // Sign-up link + '{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}' ) + ); } else { - $wgOut->wrapWikiMsg( "
\n$1
", 'anonpreviewwarning' ); + $wgOut->wrapWikiMsg( "
\n$1
", + 'anonpreviewwarning' + ); } } else { if ( $this->isCssJsSubpage ) { @@ -2759,7 +2765,6 @@ class EditPage { * up top, or false if this is the comment summary * down below the textarea * @param string $summary The text of the summary to display - * @return string */ protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { global $wgOut, $wgContLang; @@ -3101,6 +3106,7 @@ HTML * Get the copyright warning * * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility + * @return string */ protected function getCopywarn() { return self::getCopyrightWarning( $this->mTitle ); @@ -3184,7 +3190,7 @@ HTML } protected function showStandardInputs( &$tabindex = 2 ) { - global $wgOut; + global $wgOut, $wgUseMediaWikiUIEverywhere; $wgOut->addHTML( "
\n" ); if ( $this->section != 'new' ) { @@ -3212,8 +3218,14 @@ HTML $message = wfMessage( 'edithelppage' )->inContentLanguage()->text(); $edithelpurl = Skin::makeInternalOrExternalUrl( $message ); - $edithelp = '' . - wfMessage( 'edithelp' )->escaped() . ' ' . + $attrs = array( + 'target' => 'helpwindow', + 'href' => $edithelpurl, + ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attrs['class'] = 'mw-ui-button mw-ui-quiet'; + } + $edithelp = Html::element( 'a', $attrs, wfMessage( 'edithelp' )->text() ) . wfMessage( 'newwindow' )->parse(); $wgOut->addHTML( " {$cancel}\n" ); @@ -3255,15 +3267,20 @@ HTML * @return string */ public function getCancelLink() { + global $wgUseMediaWikiUIEverywhere; $cancelParams = array(); if ( !$this->isConflict && $this->oldid > 0 ) { $cancelParams['oldid'] = $this->oldid; } + $attrs = array( 'id' => 'mw-editform-cancel' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attrs['class'] = 'mw-ui-button mw-ui-quiet'; + } return Linker::linkKnown( $this->getContextTitle(), wfMessage( 'cancel' )->parse(), - array( 'id' => 'mw-editform-cancel' ), + $attrs, $cancelParams ); } @@ -3527,7 +3544,6 @@ HTML /** * Shows a bulletin board style toolbar for common editing functions. * It can be disabled in the user preferences. - * The necessary JavaScript code can be found in skins/common/edit.js. * * @return string */ @@ -3544,11 +3560,6 @@ HTML * inserted between the two when no selection is highlighted * and. The tip text is shown when the user moves the mouse * over the button. - * - * Also here: accesskeys (key), which are not used yet until - * someone can figure out a way to make them work in - * IE. However, we should make sure these keys are not defined - * on the edit page. */ $toolarray = array( array( @@ -3558,7 +3569,6 @@ HTML 'close' => '\'\'\'', 'sample' => wfMessage( 'bold_sample' )->text(), 'tip' => wfMessage( 'bold_tip' )->text(), - 'key' => 'B' ), array( 'image' => $wgLang->getImageFile( 'button-italic' ), @@ -3567,7 +3577,6 @@ HTML 'close' => '\'\'', 'sample' => wfMessage( 'italic_sample' )->text(), 'tip' => wfMessage( 'italic_tip' )->text(), - 'key' => 'I' ), array( 'image' => $wgLang->getImageFile( 'button-link' ), @@ -3576,7 +3585,6 @@ HTML 'close' => ']]', 'sample' => wfMessage( 'link_sample' )->text(), 'tip' => wfMessage( 'link_tip' )->text(), - 'key' => 'L' ), array( 'image' => $wgLang->getImageFile( 'button-extlink' ), @@ -3585,7 +3593,6 @@ HTML 'close' => ']', 'sample' => wfMessage( 'extlink_sample' )->text(), 'tip' => wfMessage( 'extlink_tip' )->text(), - 'key' => 'X' ), array( 'image' => $wgLang->getImageFile( 'button-headline' ), @@ -3594,7 +3601,6 @@ HTML 'close' => " ==\n", 'sample' => wfMessage( 'headline_sample' )->text(), 'tip' => wfMessage( 'headline_tip' )->text(), - 'key' => 'H' ), $imagesAvailable ? array( 'image' => $wgLang->getImageFile( 'button-image' ), @@ -3603,7 +3609,6 @@ HTML 'close' => ']]', 'sample' => wfMessage( 'image_sample' )->text(), 'tip' => wfMessage( 'image_tip' )->text(), - 'key' => 'D', ) : false, $imagesAvailable ? array( 'image' => $wgLang->getImageFile( 'button-media' ), @@ -3612,7 +3617,6 @@ HTML 'close' => ']]', 'sample' => wfMessage( 'media_sample' )->text(), 'tip' => wfMessage( 'media_tip' )->text(), - 'key' => 'M' ) : false, array( 'image' => $wgLang->getImageFile( 'button-nowiki' ), @@ -3621,7 +3625,6 @@ HTML 'close' => "", 'sample' => wfMessage( 'nowiki_sample' )->text(), 'tip' => wfMessage( 'nowiki_tip' )->text(), - 'key' => 'N' ), array( 'image' => $wgLang->getImageFile( 'button-sig' ), @@ -3630,7 +3633,6 @@ HTML 'close' => '', 'sample' => '', 'tip' => wfMessage( 'sig_tip' )->text(), - 'key' => 'Y' ), array( 'image' => $wgLang->getImageFile( 'button-hr' ), @@ -3639,7 +3641,6 @@ HTML 'close' => '', 'sample' => '', 'tip' => wfMessage( 'hr_tip' )->text(), - 'key' => 'R' ) ); @@ -3692,7 +3693,7 @@ HTML * @return array */ public function getCheckboxes( &$tabindex, $checked ) { - global $wgUser; + global $wgUser, $wgUseMediaWikiUIEverywhere; $checkboxes = array(); @@ -3706,11 +3707,19 @@ HTML 'accesskey' => wfMessage( 'accesskey-minoredit' )->text(), 'id' => 'wpMinoredit', ); - $checkboxes['minor'] = + $minorEditHtml = Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . " "; + + if ( $wgUseMediaWikiUIEverywhere ) { + $checkboxes['minor'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . + $minorEditHtml . + Html::closeElement( 'div' ); + } else { + $checkboxes['minor'] = $minorEditHtml; + } } } @@ -3722,11 +3731,18 @@ HTML 'accesskey' => wfMessage( 'accesskey-watch' )->text(), 'id' => 'wpWatchthis', ); - $checkboxes['watch'] = + $watchThisHtml = Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . " "; + if ( $wgUseMediaWikiUIEverywhere ) { + $checkboxes['watch'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . + $watchThisHtml . + Html::closeElement( 'div' ); + } else { + $checkboxes['watch'] = $watchThisHtml; + } } wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) ); return $checkboxes; @@ -3741,6 +3757,8 @@ HTML * @return array */ public function getEditButtons( &$tabindex ) { + global $wgUseMediaWikiUIEverywhere; + $buttons = array(); $attribs = array( @@ -3750,6 +3768,9 @@ HTML 'tabindex' => ++$tabindex, 'value' => wfMessage( 'savearticle' )->text(), ) + Linker::tooltipAndAccesskeyAttribs( 'save' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attribs['class'] = 'mw-ui-button mw-ui-constructive'; + } $buttons['save'] = Xml::element( 'input', $attribs, '' ); ++$tabindex; // use the same for preview and live preview @@ -3760,6 +3781,9 @@ HTML 'tabindex' => $tabindex, 'value' => wfMessage( 'showpreview' )->text(), ) + Linker::tooltipAndAccesskeyAttribs( 'preview' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attribs['class'] = 'mw-ui-button mw-ui-progressive'; + } $buttons['preview'] = Xml::element( 'input', $attribs, '' ); $buttons['live'] = ''; @@ -3770,6 +3794,9 @@ HTML 'tabindex' => ++$tabindex, 'value' => wfMessage( 'showdiff' )->text(), ) + Linker::tooltipAndAccesskeyAttribs( 'diff' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attribs['class'] = 'mw-ui-button mw-ui-progressive'; + } $buttons['diff'] = Xml::element( 'input', $attribs, '' ); wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) ); diff --git a/includes/Export.php b/includes/Export.php index 43dfd17195..48a814d322 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -41,7 +41,7 @@ class WikiExporter { public $dumpUploadFileContents = false; /** @var string */ - protected $author_list = ""; + public $author_list = ""; const FULL = 1; const CURRENT = 2; @@ -56,13 +56,13 @@ class WikiExporter { const STUB = 1; /** @var int */ - protected $buffer; + public $buffer; /** @var int */ - protected $text; + public $text; /** @var DumpOutput */ - protected $sink; + public $sink; /** * Returns the export schema version. @@ -1370,10 +1370,10 @@ class DumpNotalkFilter extends DumpFilter { */ class DumpNamespaceFilter extends DumpFilter { /** @var bool */ - protected $invert = false; + public $invert = false; /** @var array */ - protected $namespaces = array(); + public $namespaces = array(); /** * @param DumpOutput $sink @@ -1437,13 +1437,13 @@ class DumpNamespaceFilter extends DumpFilter { * @ingroup Dump */ class DumpLatestFilter extends DumpFilter { - protected $page; + public $page; - protected $pageString; + public $pageString; - protected $rev; + public $rev; - protected $revString; + public $revString; /** * @param object $page diff --git a/includes/Feed.php b/includes/Feed.php index 58f3ebde72..2fdfa424c4 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -37,19 +37,19 @@ */ class FeedItem { /** @var Title */ - protected $title; + public $title; - protected $description; + public $description; - protected $url; + public $url; - protected $date; + public $date; - protected $author; + public $author; - protected $uniqueId; + public $uniqueId; - protected $comments; + public $comments; public $rssIsPermalink = false; @@ -258,20 +258,11 @@ abstract class ChannelFeed extends FeedItem { } /** - * Output the initial XML headers with a stylesheet for legibility - * if someone finds it in a browser. + * Output the initial XML headers. */ protected function outXmlHeader() { - global $wgStylePath, $wgStyleVersion; - $this->httpHeaders(); echo '' . "\n"; - echo '\n"; } } @@ -348,6 +339,7 @@ class RSSFeed extends ChannelFeed { class AtomFeed extends ChannelFeed { /** * @todo document + * @param string|int $ts * @return string */ function formatTime( $ts ) { diff --git a/includes/ForkController.php b/includes/ForkController.php index 05822302ef..c1765e24e4 100644 --- a/includes/ForkController.php +++ b/includes/ForkController.php @@ -160,6 +160,7 @@ class ForkController { /** * Fork a number of worker processes. * + * @param int $numProcs * @return string */ protected function forkWorkers( $numProcs ) { diff --git a/includes/FormOptions.php b/includes/FormOptions.php index 079267a246..c91c336762 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -375,6 +375,7 @@ class FormOptions implements ArrayAccess { /* @{ */ /** * Whether the option exists. + * @param string $name * @return bool */ public function offsetExists( $name ) { @@ -383,6 +384,7 @@ class FormOptions implements ArrayAccess { /** * Retrieve an option value. + * @param string $name * @return mixed */ public function offsetGet( $name ) { @@ -391,6 +393,8 @@ class FormOptions implements ArrayAccess { /** * Set an option to given value. + * @param string $name + * @param mixed $value */ public function offsetSet( $name, $value ) { $this->setValue( $name, $value ); @@ -398,6 +402,7 @@ class FormOptions implements ArrayAccess { /** * Delete the option. + * @param string $name */ public function offsetUnset( $name ) { $this->delete( $name ); diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 4c2b7725c6..cfe9a87dc1 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -38,6 +38,7 @@ if ( !defined( 'MEDIAWIKI' ) ) { if ( !function_exists( 'mb_substr' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_substr * @return string */ function mb_substr( $str, $start, $count = 'end' ) { @@ -46,6 +47,7 @@ if ( !function_exists( 'mb_substr' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_substr_split_unicode * @return int */ function mb_substr_split_unicode( $str, $splitPos ) { @@ -56,6 +58,7 @@ if ( !function_exists( 'mb_substr' ) ) { if ( !function_exists( 'mb_strlen' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_strlen * @return int */ function mb_strlen( $str, $enc = '' ) { @@ -66,6 +69,7 @@ if ( !function_exists( 'mb_strlen' ) ) { if ( !function_exists( 'mb_strpos' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_strpos * @return int */ function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) { @@ -76,6 +80,7 @@ if ( !function_exists( 'mb_strpos' ) ) { if ( !function_exists( 'mb_strrpos' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_strrpos * @return int */ function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) { @@ -88,6 +93,7 @@ if ( !function_exists( 'mb_strrpos' ) ) { if ( !function_exists( 'gzdecode' ) ) { /** * @codeCoverageIgnore + * @param string $data * @return string */ function gzdecode( $data ) { @@ -3360,7 +3366,10 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, ); if ( extension_loaded( 'gmp' ) && ( $engine == 'auto' || $engine == 'gmp' ) ) { - $result = gmp_strval( gmp_init( $input, $sourceBase ), $destBase ); + // Removing leading zeros works around broken base detection code in + // some PHP versions (see and + // ). + $result = gmp_strval( gmp_init( ltrim( $input, '0' ), $sourceBase ), $destBase ); } elseif ( extension_loaded( 'bcmath' ) && ( $engine == 'auto' || $engine == 'bcmath' ) ) { $decimal = '0'; foreach ( str_split( strtolower( $input ) ) as $char ) { diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index d2be9e93b0..69f1120d43 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -351,10 +351,10 @@ class HistoryBlobCurStub { */ class DiffHistoryBlob implements HistoryBlob { /** @var array Uncompressed item cache */ - protected $mItems = array(); + public $mItems = array(); /** @var int Total uncompressed size */ - protected $mSize = 0; + public $mSize = 0; /** * @var array Array of diffs. If a diff D from A to B is notated D = B - A, @@ -364,20 +364,20 @@ class DiffHistoryBlob implements HistoryBlob { * diff[i] = { * { item[map[i]] - Z where i = 0 */ - protected $mDiffs; + public $mDiffs; /** @var array The diff map, see above */ - protected $mDiffMap; + public $mDiffMap; /** @var int The key for getText() */ - protected $mDefaultKey; + public $mDefaultKey; /** @var string Compressed storage */ public $mCompressed; /** @var bool True if the object is locked against further writes */ - protected $mFrozen = false; + public $mFrozen = false; /** * @var int The maximum uncompressed size before the object becomes sad diff --git a/includes/Html.php b/includes/Html.php index ce439cb3ee..1e16e39430 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -36,7 +36,7 @@ * * There are two important configuration options this class uses: * - * $wgMimeType: If this is set to an xml mimetype then output should be + * $wgMimeType: If this is set to an xml MIME type then output should be * valid XHTML5. * $wgWellFormedXml: If this is set to true, then all output should be * well-formed XML (quotes on attributes, self-closing tags, etc.). @@ -101,6 +101,35 @@ class Html { 'itemscope', ); + /** + * Modifies a set of attributes meant for text input elements + * and apply a set of default attributes. + * Removes size attribute when $wgUseMediaWikiUIEverywhere enabled. + * @param array $attrs An attribute array. + * @return array $attrs A modified attribute array + */ + public static function getTextInputAttributes( $attrs ) { + global $wgUseMediaWikiUIEverywhere; + if ( !$attrs ) { + $attrs = array(); + } + if ( isset( $attrs['class'] ) ) { + if ( is_array( $attrs['class'] ) ) { + $attrs['class'][] = 'mw-ui-input'; + } else { + $attrs['class'] .= ' mw-ui-input'; + } + } else { + $attrs['class'] = 'mw-ui-input'; + } + if ( $wgUseMediaWikiUIEverywhere ) { + // Note that size can effect the desired width rendering of mw-ui-input elements + // so it is removed. Left intact when mediawiki ui not enabled. + unset( $attrs['size'] ); + } + return $attrs; + } + /** * Returns an HTML element in a string. The major advantage here over * manually typing out the HTML is that it will escape all attribute @@ -225,33 +254,15 @@ class Html { } /** - * Returns "", except if $wgWellFormedXml is off, in which case - * it returns the empty string when that's guaranteed to be safe. + * Returns "" * * @since 1.17 * @param string $element Name of the element, e.g., 'a' - * @return string A closing tag, if required + * @return string A closing tag */ public static function closeElement( $element ) { - global $wgWellFormedXml; - $element = strtolower( $element ); - // Reference: - // http://www.whatwg.org/html/syntax.html#optional-tags - if ( !$wgWellFormedXml && in_array( $element, array( - 'html', - 'head', - 'body', - 'li', - 'dt', - 'dd', - 'tr', - 'td', - 'th', - ) ) ) { - return ''; - } return ""; } @@ -650,7 +661,9 @@ class Html { $attribs['type'] = $type; $attribs['value'] = $value; $attribs['name'] = $name; - + if ( in_array( $type, array( 'text', 'search', 'email', 'password', 'number' ) ) ) { + $attribs = Html::getTextInputAttributes( $attribs ); + } return self::element( 'input', $attribs ); } @@ -660,6 +673,7 @@ class Html { * @param string $name Name attribute * @param bool $checked Whether the checkbox is checked or not * @param array $attribs Array of additional attributes + * @return string */ public static function check( $name, $checked = false, array $attribs = array() ) { if ( isset( $attribs['value'] ) ) { @@ -682,6 +696,7 @@ class Html { * @param string $name Name attribute * @param bool $checked Whether the checkbox is checked or not * @param array $attribs Array of additional attributes + * @return string */ public static function radio( $name, $checked = false, array $attribs = array() ) { if ( isset( $attribs['value'] ) ) { @@ -704,6 +719,7 @@ class Html { * @param string $label Contents of the label * @param string $id ID of the element being labeled * @param array $attribs Additional attributes + * @return string */ public static function label( $label, $id, array $attribs = array() ) { $attribs += array( @@ -749,7 +765,7 @@ class Html { } else { $spacedValue = $value; } - return self::element( 'textarea', $attribs, $spacedValue ); + return self::element( 'textarea', Html::getTextInputAttributes( $attribs ), $spacedValue ); } /** @@ -872,7 +888,7 @@ class Html { $isXHTML = self::isXmlMimeType( $wgMimeType ); if ( $isXHTML ) { // XHTML5 - // XML mimetyped markup should have an xml header. + // XML MIME-typed markup should have an xml header. // However a DOCTYPE is not needed. $ret .= "\n"; @@ -904,16 +920,16 @@ class Html { } /** - * Determines if the given mime type is xml. + * Determines if the given MIME type is xml. * - * @param string $mimetype MimeType + * @param string $mimetype MIME type * @return bool */ public static function isXmlMimeType( $mimetype ) { # http://www.whatwg.org/html/infrastructure.html#xml-mime-type # * text/xml # * application/xml - # * Any mimetype with a subtype ending in +xml (this implicitly includes application/xhtml+xml) + # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml) return (bool)preg_match( '!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype ); } @@ -921,20 +937,13 @@ class Html { * Get HTML for an info box with an icon. * * @param string $text Wikitext, get this with wfMessage()->plain() - * @param string $icon Icon name, file in skins/common/images + * @param string $icon Path to icon file (used as 'src' attribute) * @param string $alt Alternate text for the icon * @param string $class Additional class name to add to the wrapper div - * @param bool $useStylePath * * @return string */ - static function infoBox( $text, $icon, $alt, $class = false, $useStylePath = true ) { - global $wgStylePath; - - if ( $useStylePath ) { - $icon = $wgStylePath . '/common/images/' . $icon; - } - + static function infoBox( $text, $icon, $alt, $class = false ) { $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class" ) ); $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ) . diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index 1eb8ca5294..8302124570 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -809,7 +809,8 @@ class CurlHttpRequest extends MWHttpRequest { return false; } - if ( !defined( 'CURLOPT_REDIR_PROTOCOLS' ) ) { + $curlVersionInfo = curl_version(); + if ( $curlVersionInfo['version_number'] < 0x071304 ) { wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" ); return false; } diff --git a/includes/Import.php b/includes/Import.php index e6b5dc256f..5319076e82 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -429,53 +429,12 @@ class WikiImporter { return ''; } - # -------------- - - /** Left in for debugging */ - private function dumpElement() { - static $lookup = null; - if ( !$lookup ) { - $xmlReaderConstants = array( - "NONE", - "ELEMENT", - "ATTRIBUTE", - "TEXT", - "CDATA", - "ENTITY_REF", - "ENTITY", - "PI", - "COMMENT", - "DOC", - "DOC_TYPE", - "DOC_FRAGMENT", - "NOTATION", - "WHITESPACE", - "SIGNIFICANT_WHITESPACE", - "END_ELEMENT", - "END_ENTITY", - "XML_DECLARATION", - ); - $lookup = array(); - - foreach ( $xmlReaderConstants as $name ) { - $lookup[constant( "XmlReader::$name" )] = $name; - } - } - - print var_dump( - $lookup[$this->reader->nodeType], - $this->reader->name, - $this->reader->value - ) . "\n\n"; - } - /** * Primary entry point * @throws MWException * @return bool */ public function doImport() { - // Calls to reader->read need to be wrapped in calls to // libxml_disable_entity_loader() to avoid local file // inclusion attacks (bug 46932). @@ -932,7 +891,7 @@ class WikiImporter { /** This is a horrible hack used to keep source compatibility */ class UploadSourceAdapter { /** @var array */ - private static $sourceRegistrations = array(); + public static $sourceRegistrations = array(); /** @var string */ private $mSource; @@ -1056,13 +1015,13 @@ class UploadSourceAdapter { */ class WikiRevision { /** @todo Unused? */ - private $importer = null; + public $importer = null; /** @var Title */ public $title = null; /** @var int */ - private $id = 0; + public $id = 0; /** @var string */ public $timestamp = "20010115000000"; @@ -1076,10 +1035,10 @@ class WikiRevision { public $user_text = ""; /** @var string */ - protected $model = null; + public $model = null; /** @var string */ - protected $format = null; + public $format = null; /** @var string */ public $text = ""; @@ -1088,7 +1047,7 @@ class WikiRevision { protected $size; /** @var Content */ - protected $content = null; + public $content = null; /** @var ContentHandler */ protected $contentHandler = null; @@ -1097,31 +1056,31 @@ class WikiRevision { public $comment = ""; /** @var bool */ - protected $minor = false; + public $minor = false; /** @var string */ - protected $type = ""; + public $type = ""; /** @var string */ - protected $action = ""; + public $action = ""; /** @var string */ - protected $params = ""; + public $params = ""; /** @var string */ - protected $fileSrc = ''; + public $fileSrc = ''; /** @var bool|string */ - protected $sha1base36 = false; + public $sha1base36 = false; /** * @var bool * @todo Unused? */ - private $isTemp = false; + public $isTemp = false; /** @var string */ - protected $archiveName = ''; + public $archiveName = ''; protected $filename; @@ -1129,7 +1088,7 @@ class WikiRevision { protected $src; /** @todo Unused? */ - private $fileIsTemp; + public $fileIsTemp; /** @var bool */ private $mNoUpdates = false; @@ -1535,9 +1494,6 @@ class WikiRevision { return true; } - /** - * @return mixed - */ function importLogItem() { $dbw = wfGetDB( DB_MASTER ); # @todo FIXME: This will not record autoblocks diff --git a/includes/Linker.php b/includes/Linker.php index 9f578a925f..d9f4255e68 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -259,6 +259,7 @@ class Linker { /** * Identical to link(), except $options defaults to 'known'. + * @see Linker::link * @return string */ public static function linkKnown( @@ -702,6 +703,7 @@ class Linker { * frame parameters supplied by the Parser. * @param array $frameParams The frame parameters * @param string $query An optional query string to add to description page links + * @param Parser|null $parser * @return array */ private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) { @@ -770,7 +772,6 @@ class Linker { public static function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false, $query = "" ) { - global $wgStylePath, $wgContLang; $exists = $file && $file->exists(); # Shortcuts @@ -879,12 +880,7 @@ class Linker { 'href' => $url, 'class' => 'internal', 'title' => wfMessage( 'thumbnail-more' )->text() ), - Html::element( 'img', array( - 'src' => $wgStylePath . '/common/images/magnify-clip' - . ( $wgContLang->isRTL() ? '-rtl' : '' ) . '.png', - 'width' => 15, - 'height' => 11, - 'alt' => "" ) ) ) ); + "" ) ); } } $s .= '
' . $zoomIcon . $fp['caption'] . "
"; @@ -913,10 +909,10 @@ class Linker { $thumb15 = $file->transform( $hp15 ); $thumb20 = $file->transform( $hp20 ); - if ( $thumb15 && $thumb15->getUrl() !== $thumb->getUrl() ) { + if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) { $thumb->responsiveUrls['1.5'] = $thumb15->getUrl(); } - if ( $thumb20 && $thumb20->getUrl() !== $thumb->getUrl() ) { + if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) { $thumb->responsiveUrls['2'] = $thumb20->getUrl(); } } @@ -2155,7 +2151,7 @@ class Linker { return $tooltip; } - private static $accesskeycache; + public static $accesskeycache; /** * Given the id of an interface element, constructs the appropriate @@ -2202,7 +2198,7 @@ class Linker { * * @param User $user * @param Revision $rev - * @param Revision $title + * @param Title $title * @return string HTML fragment */ public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) { diff --git a/includes/MWTimestamp.php b/includes/MWTimestamp.php index bc59588dce..26f5e54370 100644 --- a/includes/MWTimestamp.php +++ b/includes/MWTimestamp.php @@ -238,7 +238,6 @@ class MWTimestamp { * @since 1.22 * * @param User $user User to take preferences from - * @param[out] MWTimestamp $ts Timestamp to adjust * @return DateInterval Offset that was applied to the timestamp */ public function offsetForUser( User $user ) { diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 7decbee0ab..4d17298b23 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -754,6 +754,7 @@ class MagicWordArray { /** * Get a 2-d hashtable for this array + * @return array */ function getHash() { if ( is_null( $this->hash ) ) { @@ -775,6 +776,7 @@ class MagicWordArray { /** * Get the base regex + * @return array */ function getBaseRegex() { if ( is_null( $this->baseRegex ) ) { @@ -799,6 +801,7 @@ class MagicWordArray { /** * Get an unanchored regex that does not match parameters + * @return array */ function getRegex() { if ( is_null( $this->regex ) ) { diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 1b014f6f64..9213c021ef 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -27,34 +27,14 @@ */ class MediaWiki { /** - * @todo Fold $output, etc, into this * @var IContextSource */ private $context; /** - * @param null|WebRequest $x - * @return WebRequest + * @var Config */ - public function request( WebRequest $x = null ) { - $old = $this->context->getRequest(); - if ( $x ) { - $this->context->setRequest( $x ); - } - return $old; - } - - /** - * @param null|OutputPage $x - * @return OutputPage - */ - public function output( OutputPage $x = null ) { - $old = $this->context->getOutput(); - if ( $x ) { - $this->context->setOutput( $x ); - } - return $old; - } + private $config; /** * @param IContextSource|null $context @@ -65,6 +45,7 @@ class MediaWiki { } $this->context = $context; + $this->config = $context->getConfig(); } /** @@ -174,7 +155,7 @@ class MediaWiki { * @return void */ private function performRequest() { - global $wgServer, $wgUsePathInfo, $wgTitle; + global $wgTitle; wfProfileIn( __METHOD__ ); @@ -237,7 +218,7 @@ class MediaWiki { $url = $title->getFullURL( $query ); } // Check for a redirect loop - if ( !preg_match( '/^' . preg_quote( $wgServer, '/' ) . '/', $url ) + if ( !preg_match( '/^' . preg_quote( $this->config->get( 'Server' ), '/' ) . '/', $url ) && $title->isLocal() ) { // 301 so google et al report the target as the actual url. @@ -268,7 +249,7 @@ class MediaWiki { "requested; this sometimes happens when moving a wiki " . "to a new server or changing the server configuration.\n\n"; - if ( $wgUsePathInfo ) { + if ( $this->config->get( 'UsePathInfo' ) ) { $message .= "The wiki is trying to interpret the page " . "title from the URL path portion (PATH_INFO), which " . "sometimes fails depending on the web server. Try " . @@ -323,8 +304,6 @@ class MediaWiki { * @return mixed An Article, or a string to redirect to another URL */ private function initializeArticle() { - global $wgDisableHardRedirects; - wfProfileIn( __METHOD__ ); $title = $this->context->getTitle(); @@ -372,7 +351,7 @@ class MediaWiki { // Is the target already set by an extension? $target = $target ? $target : $article->followRedirect(); if ( is_string( $target ) ) { - if ( !$wgDisableHardRedirects ) { + if ( !$this->config->get( 'DisableHardRedirects' ) ) { // we'll need to redirect wfProfileOut( __METHOD__ ); return $target; @@ -406,8 +385,6 @@ class MediaWiki { * @param Title $requestTitle The original title, before any redirects were applied */ private function performAction( Page $page, Title $requestTitle ) { - global $wgUseSquid, $wgSquidMaxage; - wfProfileIn( __METHOD__ ); $request = $this->context->getRequest(); @@ -428,10 +405,10 @@ class MediaWiki { if ( $action instanceof Action ) { # Let Squid cache things if we can purge them. - if ( $wgUseSquid && + if ( $this->config->get( 'UseSquid' ) && in_array( $request->getFullRequestURL(), $requestTitle->getSquidURLs() ) ) { - $output->setSquidMaxage( $wgSquidMaxage ); + $output->setSquidMaxage( $this->config->get( 'SquidMaxage' ) ); } $action->show(); @@ -479,8 +456,6 @@ class MediaWiki { * @return bool */ private function checkMaxLag() { - global $wgShowHostnames; - wfProfileIn( __METHOD__ ); $maxLag = $this->context->getRequest()->getVal( 'maxlag' ); if ( !is_null( $maxLag ) ) { @@ -491,7 +466,7 @@ class MediaWiki { $resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); $resp->header( 'X-Database-Lag: ' . intval( $lag ) ); $resp->header( 'Content-Type: text/plain' ); - if ( $wgShowHostnames ) { + if ( $this->config->get( 'ShowHostnames' ) ) { echo "Waiting for $host: $lag seconds lagged\n"; } else { echo "Waiting for a database server: $lag seconds lagged\n"; @@ -507,22 +482,22 @@ class MediaWiki { } private function main() { - global $wgUseFileCache, $wgTitle, $wgUseAjax; + global $wgTitle; wfProfileIn( __METHOD__ ); $request = $this->context->getRequest(); // Send Ajax requests to the Ajax dispatcher. - if ( $wgUseAjax && $request->getVal( 'action', 'view' ) == 'ajax' ) { + if ( $this->config->get( 'UseAjax' ) && $request->getVal( 'action', 'view' ) == 'ajax' ) { // Set a dummy title, because $wgTitle == null might break things $title = Title::makeTitle( NS_MAIN, 'AJAX' ); $this->context->setTitle( $title ); $wgTitle = $title; - $dispatcher = new AjaxDispatcher(); - $dispatcher->performAction(); + $dispatcher = new AjaxDispatcher( $this->config ); + $dispatcher->performAction( $this->context->getUser() ); wfProfileOut( __METHOD__ ); return; } @@ -581,7 +556,7 @@ class MediaWiki { } } - if ( $wgUseFileCache && $title->getNamespace() >= 0 ) { + if ( $this->config->get( 'UseFileCache' ) && $title->getNamespace() >= 0 ) { wfProfileIn( 'main-try-filecache' ); if ( HTMLFileCache::useFileCache( $this->context ) ) { // Try low-level file cache hit @@ -645,9 +620,8 @@ class MediaWiki { * the socket once it's done. */ protected function triggerJobs() { - global $wgJobRunRate, $wgServer, $wgRunJobsAsync; - - if ( $wgJobRunRate <= 0 || wfReadOnly() ) { + $jobRunRate = $this->config->get( 'JobRunRate' ); + if ( $jobRunRate <= 0 || wfReadOnly() ) { return; } elseif ( $this->getTitle()->isSpecial( 'RunJobs' ) ) { return; // recursion guard @@ -655,17 +629,17 @@ class MediaWiki { $section = new ProfileSection( __METHOD__ ); - if ( $wgJobRunRate < 1 ) { + if ( $jobRunRate < 1 ) { $max = mt_getrandmax(); - if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) { - return; // the higher $wgJobRunRate, the less likely we return here + if ( mt_rand( 0, $max ) > $max * $jobRunRate ) { + return; // the higher the job run rate, the less likely we return here } $n = 1; } else { - $n = intval( $wgJobRunRate ); + $n = intval( $jobRunRate ); } - if ( !$wgRunJobsAsync ) { + if ( !$this->config->get( 'RunJobsAsync' ) ) { // Fall back to running the job here while the user waits $runner = new JobRunner(); $runner->run( array( 'maxJobs' => $n ) ); @@ -683,10 +657,11 @@ class MediaWiki { $query = array( 'title' => 'Special:RunJobs', 'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ); - $query['signature'] = SpecialRunJobs::getQuerySignature( $query ); + $query['signature'] = SpecialRunJobs::getQuerySignature( + $query, $this->config->get( 'SecretKey' ) ); $errno = $errstr = null; - $info = wfParseUrl( $wgServer ); + $info = wfParseUrl( $this->config->get( 'Server' ) ); wfSuppressWarnings(); $sock = fsockopen( $info['host'], diff --git a/includes/Message.php b/includes/Message.php index 931a3f9dcc..4df0d809d6 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -175,10 +175,16 @@ class Message { protected $language = null; /** - * @var string|string[] The message key or array of keys. + * @var string The message key. If $keysToTry has more than one element, + * this may change to one of the keys to try when fetching the message text. */ protected $key; + /** + * @var string[] List of keys to try when fetching the message. + */ + protected $keysToTry; + /** * @var array List of parameters which will be substituted into the message. */ @@ -224,30 +230,61 @@ class Message { * non-empty message for. * @param array $params Message parameters. * @param Language $language Optional language of the message, defaults to $wgLang. + * + * @throws InvalidArgumentException */ public function __construct( $key, $params = array(), Language $language = null ) { global $wgLang; - $this->key = $key; + if ( !is_string( $key ) && !is_array( $key ) ) { + throw new InvalidArgumentException( '$key must be a string or an array' ); + } + + $this->keysToTry = (array)$key; + + if ( empty( $this->keysToTry ) ) { + throw new InvalidArgumentException( '$key must not be an empty list' ); + } + + $this->key = reset( $this->keysToTry ); + $this->parameters = array_values( $params ); $this->language = $language ? $language : $wgLang; } /** - * Returns the message key or the first from an array of message keys. + * @since 1.24 + * + * @return bool True if this is a multi-key message, that is, if the key provided to the + * constructor was a fallback list of keys to try. + */ + public function isMultiKey() { + return count( $this->keysToTry ) > 1; + } + + /** + * @since 1.24 + * + * @return string[] The list of keys to try when fetching the message text, + * in order of preference. + */ + public function getKeysToTry() { + return $this->keysToTry; + } + + /** + * Returns the message key. + * + * If a list of multiple possible keys was supplied to the constructor, this method may + * return any of these keys. After the message ahs been fetched, this method will return + * the key that was actually used to fetch the message. * * @since 1.21 * * @return string */ public function getKey() { - if ( is_array( $this->key ) ) { - // May happen if some kind of fallback is applied. - // For now, just use the first key. We really need a better solution. - return $this->key[0]; - } else { - return $this->key; - } + return $this->key; } /** @@ -637,7 +674,7 @@ class Message { $string = $this->fetchMessage(); if ( $string === false ) { - $key = htmlspecialchars( is_array( $this->key ) ? $this->key[0] : $this->key ); + $key = htmlspecialchars( $this->key ); if ( $this->format === 'plain' ) { return '<' . $key . '>'; } @@ -997,20 +1034,18 @@ class Message { protected function fetchMessage() { if ( $this->message === null ) { $cache = MessageCache::singleton(); - if ( is_array( $this->key ) ) { - if ( !count( $this->key ) ) { - throw new MWException( "Given empty message key array." ); - } - foreach ( $this->key as $key ) { - $message = $cache->get( $key, $this->useDatabase, $this->language ); - if ( $message !== false && $message !== '' ) { - break; - } + + foreach ( $this->keysToTry as $key ) { + $message = $cache->get( $key, $this->useDatabase, $this->language ); + if ( $message !== false && $message !== '' ) { + break; } - $this->message = $message; - } else { - $this->message = $cache->get( $this->key, $this->useDatabase, $this->language ); } + + // NOTE: The constructor makes sure keysToTry isn't empty, + // so we know that $key and $message are initialized. + $this->key = $key; + $this->message = $message; } return $this->message; } @@ -1038,13 +1073,20 @@ class RawMessage extends Message { * * @see Message::__construct * - * @param string|string[] $key Message to use. + * @param string $text Message to use. * @param array $params Parameters for the message. + * + * @throws InvalidArgumentException */ - public function __construct( $key, $params = array() ) { - parent::__construct( $key, $params ); + public function __construct( $text, $params = array() ) { + if ( !is_string( $text ) ) { + throw new InvalidArgumentException( '$text must be a string' ); + } + + parent::__construct( $text, $params ); + // The key is the message. - $this->message = $key; + $this->message = $text; } /** @@ -1057,6 +1099,7 @@ class RawMessage extends Message { if ( $this->message === null ) { $this->message = $this->key; } + return $this->message; } diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php index 5725898420..e3b4dbe8fe 100644 --- a/includes/MessageBlobStore.php +++ b/includes/MessageBlobStore.php @@ -32,6 +32,21 @@ * constituent messages or the resource itself is changed. */ class MessageBlobStore { + /** + * Get the singleton instance + * + * @since 1.24 + * @return MessageBlobStore + */ + public static function getInstance() { + static $instance = null; + if ( $instance === null ) { + $instance = new self; + } + + return $instance; + } + /** * Get the message blobs for a set of modules * @@ -40,19 +55,19 @@ class MessageBlobStore { * @param string $lang Language code * @return array An array mapping module names to message blobs */ - public static function get( ResourceLoader $resourceLoader, $modules, $lang ) { + public function get( ResourceLoader $resourceLoader, $modules, $lang ) { wfProfileIn( __METHOD__ ); if ( !count( $modules ) ) { wfProfileOut( __METHOD__ ); return array(); } // Try getting from the DB first - $blobs = self::getFromDB( $resourceLoader, array_keys( $modules ), $lang ); + $blobs = $this->getFromDB( $resourceLoader, array_keys( $modules ), $lang ); // Generate blobs for any missing modules and store them in the DB $missing = array_diff( array_keys( $modules ), array_keys( $blobs ) ); foreach ( $missing as $name ) { - $blob = self::insertMessageBlob( $name, $modules[$name], $lang ); + $blob = $this->insertMessageBlob( $name, $modules[$name], $lang ); if ( $blob ) { $blobs[$name] = $blob; } @@ -72,8 +87,8 @@ class MessageBlobStore { * @param string $lang Language code * @return mixed Message blob or false if the module has no messages */ - public static function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) { - $blob = self::generateMessageBlob( $module, $lang ); + public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) { + $blob = $this->generateMessageBlob( $module, $lang ); if ( !$blob ) { return false; @@ -130,7 +145,7 @@ class MessageBlobStore { * @return string Regenerated message blob, or null if there was no blob for * the given module/language pair. */ - public static function updateModule( $name, ResourceLoaderModule $module, $lang ) { + public function updateModule( $name, ResourceLoaderModule $module, $lang ) { $dbw = wfGetDB( DB_MASTER ); $row = $dbw->selectRow( 'msg_resource', 'mr_blob', array( 'mr_resource' => $name, 'mr_lang' => $lang ), @@ -142,7 +157,7 @@ class MessageBlobStore { // Save the old and new blobs for later $oldBlob = $row->mr_blob; - $newBlob = self::generateMessageBlob( $module, $lang ); + $newBlob = $this->generateMessageBlob( $module, $lang ); try { $newRow = array( @@ -197,7 +212,7 @@ class MessageBlobStore { * * @param string $key Message key */ - public static function updateMessage( $key ) { + public function updateMessage( $key ) { try { $dbw = wfGetDB( DB_MASTER ); @@ -206,7 +221,7 @@ class MessageBlobStore { // in one iteration. $updates = null; do { - $updates = self::getUpdatesForMessage( $key, $updates ); + $updates = $this->getUpdatesForMessage( $key, $updates ); foreach ( $updates as $k => $update ) { // Update the row on the condition that it @@ -240,7 +255,7 @@ class MessageBlobStore { } } - public static function clear() { + public function clear() { // TODO: Give this some more thought try { // Not using TRUNCATE, because that needs extra permissions, @@ -260,7 +275,7 @@ class MessageBlobStore { * @param array $prevUpdates Updates queue to refresh or null to build a fresh update queue * @return array Updates queue */ - private static function getUpdatesForMessage( $key, $prevUpdates = null ) { + private function getUpdatesForMessage( $key, $prevUpdates = null ) { $dbw = wfGetDB( DB_MASTER ); if ( is_null( $prevUpdates ) ) { @@ -297,7 +312,7 @@ class MessageBlobStore { 'resource' => $row->mr_resource, 'lang' => $row->mr_lang, 'timestamp' => $row->mr_timestamp, - 'newBlob' => self::reencodeBlob( $row->mr_blob, $key, $row->mr_lang ) + 'newBlob' => $this->reencodeBlob( $row->mr_blob, $key, $row->mr_lang ) ); } @@ -312,7 +327,7 @@ class MessageBlobStore { * @param string $lang Language code * @return string Message blob with $key replaced with its new value */ - private static function reencodeBlob( $blob, $key, $lang ) { + private function reencodeBlob( $blob, $key, $lang ) { $decoded = FormatJson::decode( $blob, true ); $decoded[$key] = wfMessage( $key )->inLanguage( $lang )->plain(); @@ -329,9 +344,8 @@ class MessageBlobStore { * @throws MWException * @return array Array mapping module names to blobs */ - private static function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) { - global $wgCacheEpoch; - + private function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) { + $config = $resourceLoader->getConfig(); $retval = array(); $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'msg_resource', @@ -348,13 +362,13 @@ class MessageBlobStore { } // Update the module's blobs if the set of messages changed or if the blob is - // older than $wgCacheEpoch + // older than the CacheEpoch setting $keys = array_keys( FormatJson::decode( $row->mr_blob, true ) ); $values = array_values( array_unique( $module->getMessages() ) ); if ( $keys !== $values - || wfTimestamp( TS_MW, $row->mr_timestamp ) <= $wgCacheEpoch + || wfTimestamp( TS_MW, $row->mr_timestamp ) <= $config->get( 'CacheEpoch' ) ) { - $retval[$row->mr_resource] = self::updateModule( $row->mr_resource, $module, $lang ); + $retval[$row->mr_resource] = $this->updateModule( $row->mr_resource, $module, $lang ); } else { $retval[$row->mr_resource] = $row->mr_blob; } @@ -370,7 +384,7 @@ class MessageBlobStore { * @param string $lang Language code * @return string JSON object */ - private static function generateMessageBlob( ResourceLoaderModule $module, $lang ) { + private function generateMessageBlob( ResourceLoaderModule $module, $lang ) { $messages = array(); foreach ( $module->getMessages() as $key ) { diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index b4d3ab1c1e..bfd60111a5 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -1,6 +1,6 @@ ext - * map. Each line contains a mime type followed by a space separated list of - * extensions. If multiple extensions for a single mime type exist or if - * multiple mime types exist for a single extension then in most cases - * MediaWiki assumes that the first extension following the mime type is the - * canonical extension, and the first time a mime type appears for a certain - * extension is considered the canonical mime type. + * This list concatenated with mime.types is used to create a MIME <-> ext + * map. Each line contains a MIME type followed by a space separated list of + * extensions. If multiple extensions for a single MIME type exist or if + * multiple MIME types exist for a single extension then in most cases + * MediaWiki assumes that the first extension following the MIME type is the + * canonical extension, and the first time a MIME type appears for a certain + * extension is considered the canonical MIME type. * * (Note that appending $wgMimeTypeFile to the end of MM_WELL_KNOWN_MIME_TYPES * sucks because you can't redefine canonical types. This could be fixed by @@ -86,9 +86,9 @@ END_STRING ); /** - * Defines a set of well known mime info entries + * Defines a set of well known MIME info entries * This is used as a fallback to mime.info files. - * An extensive list of well known mime types is provided by + * An extensive list of well known MIME types is provided by * the file mime.info in the includes directory. */ define( 'MM_WELL_KNOWN_MIME_INFO', <<makeConfig( 'main' ); + } + $this->mConfig = $config; + /** * --- load mime.types --- */ - global $wgMimeTypeFile, $IP; + global $IP; # Allow media handling extensions adding MIME-types and MIME-info wfRunHooks( 'MimeMagicInit', array( $this ) ); $types = MM_WELL_KNOWN_MIME_TYPES; - if ( $wgMimeTypeFile == 'includes/mime.types' ) { - $wgMimeTypeFile = "$IP/$wgMimeTypeFile"; + $mimeTypeFile = $this->mConfig->get( 'MimeTypeFile' ); + if ( $mimeTypeFile == 'includes/mime.types' ) { + $mimeTypeFile = "$IP/$mimeTypeFile"; } - if ( $wgMimeTypeFile ) { - if ( is_file( $wgMimeTypeFile ) and is_readable( $wgMimeTypeFile ) ) { - wfDebug( __METHOD__ . ": loading mime types from $wgMimeTypeFile\n" ); + if ( $mimeTypeFile ) { + if ( is_file( $mimeTypeFile ) and is_readable( $mimeTypeFile ) ) { + wfDebug( __METHOD__ . ": loading mime types from $mimeTypeFile\n" ); $types .= "\n"; - $types .= file_get_contents( $wgMimeTypeFile ); + $types .= file_get_contents( $mimeTypeFile ); } else { - wfDebug( __METHOD__ . ": can't load mime types from $wgMimeTypeFile\n" ); + wfDebug( __METHOD__ . ": can't load mime types from $mimeTypeFile\n" ); } } else { wfDebug( __METHOD__ . ": no mime types file defined, using build-ins only.\n" ); @@ -266,20 +279,20 @@ class MimeMagic { * --- load mime.info --- */ - global $wgMimeInfoFile; - if ( $wgMimeInfoFile == 'includes/mime.info' ) { - $wgMimeInfoFile = "$IP/$wgMimeInfoFile"; + $mimeInfoFile = $this->mConfig->get( 'MimeInfoFile' ); + if ( $mimeInfoFile == 'includes/mime.info' ) { + $mimeInfoFile = "$IP/$mimeInfoFile"; } $info = MM_WELL_KNOWN_MIME_INFO; - if ( $wgMimeInfoFile ) { - if ( is_file( $wgMimeInfoFile ) and is_readable( $wgMimeInfoFile ) ) { - wfDebug( __METHOD__ . ": loading mime info from $wgMimeInfoFile\n" ); + if ( $mimeInfoFile ) { + if ( is_file( $mimeInfoFile ) and is_readable( $mimeInfoFile ) ) { + wfDebug( __METHOD__ . ": loading mime info from $mimeInfoFile\n" ); $info .= "\n"; - $info .= file_get_contents( $wgMimeInfoFile ); + $info .= file_get_contents( $mimeInfoFile ); } else { - wfDebug( __METHOD__ . ": can't load mime info from $wgMimeInfoFile\n" ); + wfDebug( __METHOD__ . ": can't load mime info from $mimeInfoFile\n" ); } } else { wfDebug( __METHOD__ . ": no mime info file defined, using build-ins only.\n" ); @@ -352,7 +365,9 @@ class MimeMagic { */ public static function singleton() { if ( self::$instance === null ) { - self::$instance = new MimeMagic; + self::$instance = new MimeMagic( + ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) + ); } return self::$instance; } @@ -378,9 +393,9 @@ class MimeMagic { } /** - * Returns a list of file extensions for a given mime type as a space - * separated string or null if the mime type was unrecognized. Resolves - * mime type aliases. + * Returns a list of file extensions for a given MIME type as a space + * separated string or null if the MIME type was unrecognized. Resolves + * MIME type aliases. * * @param string $mime * @return string|null @@ -393,7 +408,7 @@ class MimeMagic { return $this->mMimeToExt[$mime]; } - // Resolve the mime type to the canonical type + // Resolve the MIME type to the canonical type if ( isset( $this->mMimeTypeAliases[$mime] ) ) { $mime = $this->mMimeTypeAliases[$mime]; if ( isset( $this->mMimeToExt[$mime] ) ) { @@ -405,7 +420,7 @@ class MimeMagic { } /** - * Returns a list of mime types for a given file extension as a space + * Returns a list of MIME types for a given file extension as a space * separated string or null if the extension was unrecognized. * * @param string $ext @@ -419,7 +434,7 @@ class MimeMagic { } /** - * Returns a single mime type for a given file extension or null if unknown. + * Returns a single MIME type for a given file extension or null if unknown. * This is always the first type from the list returned by getTypesForExtension($ext). * * @param string $ext @@ -439,9 +454,9 @@ class MimeMagic { } /** - * Tests if the extension matches the given mime type. Returns true if a - * match was found, null if the mime type is unknown, and false if the - * mime type is known but no matches where found. + * Tests if the extension matches the given MIME type. Returns true if a + * match was found, null if the MIME type is unknown, and false if the + * MIME type is known but no matches where found. * * @param string $extension * @param string $mime @@ -451,7 +466,7 @@ class MimeMagic { $ext = $this->getExtensionsForType( $mime ); if ( !$ext ) { - return null; // Unknown mime type + return null; // Unknown MIME type } $ext = explode( ' ', $ext ); @@ -461,7 +476,7 @@ class MimeMagic { } /** - * Returns true if the mime type is known to represent an image format + * Returns true if the MIME type is known to represent an image format * supported by the PHP GD library. * * @param string $mime @@ -490,7 +505,7 @@ class MimeMagic { * invalid uploads; if we can't identify the type we won't * be able to say if it's invalid. * - * @todo Be more accurate when using fancy mime detector plugins; + * @todo Be more accurate when using fancy MIME detector plugins; * right now this is the bare minimum getimagesize() list. * @param string $extension * @return bool @@ -515,15 +530,15 @@ class MimeMagic { } /** - * Improves a mime type using the file extension. Some file formats are very generic, - * so their mime type is not very meaningful. A more useful mime type can be derived + * Improves a MIME type using the file extension. Some file formats are very generic, + * so their MIME type is not very meaningful. A more useful MIME type can be derived * by looking at the file extension. Typically, this method would be called on the * result of guessMimeType(). * - * @param string $mime The mime type, typically guessed from a file's content. + * @param string $mime The MIME type, typically guessed from a file's content. * @param string $ext The file extension, as taken from the file name * - * @return string The mime type + * @return string The MIME type */ public function improveTypeFromExtension( $mime, $ext ) { if ( $mime === 'unknown/unknown' ) { @@ -538,7 +553,7 @@ class MimeMagic { } elseif ( $mime === 'application/x-opc+zip' ) { if ( $this->isMatchingExtension( $ext, $mime ) ) { // A known file extension for an OPC file, - // find the proper mime type for that file extension + // find the proper MIME type for that file extension $mime = $this->guessTypesForExtension( $ext ); } else { wfDebug( __METHOD__ . ": refusing to guess better type for $mime file, " . @@ -565,18 +580,18 @@ class MimeMagic { } /** - * Mime type detection. This uses detectMimeType to detect the mime type + * MIME type detection. This uses detectMimeType to detect the MIME type * of the file, but applies additional checks to determine some well known - * file formats that may be missed or misinterpreted by the default mime + * file formats that may be missed or misinterpreted by the default MIME * detection (namely XML based formats like XHTML or SVG, as well as ZIP * based formats like OPC/ODF files). * * @param string $file The file to check * @param string|bool $ext The file extension, or true (default) to extract it from the filename. * Set it to false to ignore the extension. DEPRECATED! Set to false, use - * improveTypeFromExtension($mime, $ext) later to improve mime type. + * improveTypeFromExtension($mime, $ext) later to improve MIME type. * - * @return string The mime type of $file + * @return string The MIME type of $file */ public function guessMimeType( $file, $ext = true ) { if ( $ext ) { // TODO: make $ext default to false. Or better, remove it. @@ -600,7 +615,7 @@ class MimeMagic { } /** - * Guess the mime type from the file contents. + * Guess the MIME type from the file contents. * * @param string $file * @param mixed $ext @@ -711,9 +726,9 @@ class MimeMagic { */ $xml = new XmlTypeCheck( $file ); if ( $xml->wellFormed ) { - global $wgXMLMimeTypes; - if ( isset( $wgXMLMimeTypes[$xml->getRootElement()] ) ) { - return $wgXMLMimeTypes[$xml->getRootElement()]; + $xmlMimeTypes = $this->mConfig->get( 'XMLMimeTypes' ); + if ( isset( $xmlMimeTypes[$xml->getRootElement()] ) ) { + return $xmlMimeTypes[$xml->getRootElement()]; } else { return 'application/xml'; } @@ -804,7 +819,7 @@ class MimeMagic { * @param string|null $tail The tail of the file * @param string|bool $ext The file extension, or true to extract it from the filename. * Set it to false (default) to ignore the extension. DEPRECATED! Set to false, - * use improveTypeFromExtension($mime, $ext) later to improve mime type. + * use improveTypeFromExtension($mime, $ext) later to improve MIME type. * * @return string */ @@ -847,7 +862,7 @@ class MimeMagic { # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere if ( $ext !== true && $ext !== false ) { /** This is the mode used by getPropsFromPath - * These mime's are stored in the database, where we don't really want + * These MIME's are stored in the database, where we don't really want * x-opc+zip, because we use it only for internal purposes */ if ( $this->isMatchingExtension( $ext, $mime ) ) { @@ -896,49 +911,37 @@ class MimeMagic { } /** - * Internal mime type detection. Detection is done using an external + * Internal MIME type detection. Detection is done using an external * program, if $wgMimeDetectorCommand is set. Otherwise, the fileinfo - * extension and mime_content_type are tried (in this order), if they - * are available. If the detections fails and $ext is not false, the mime - * type is guessed from the file extension, using guessTypesForExtension. + * extension is tried if it is available. If detection fails and $ext + * is not false, the MIME type is guessed from the file extension, + * using guessTypesForExtension. * - * If the mime type is still unknown, getimagesize is used to detect the - * mime type if the file is an image. If no mime type can be determined, + * If the MIME type is still unknown, getimagesize is used to detect the + * MIME type if the file is an image. If no MIME type can be determined, * this function returns 'unknown/unknown'. * * @param string $file The file to check * @param string|bool $ext The file extension, or true (default) to extract it from the filename. * Set it to false to ignore the extension. DEPRECATED! Set to false, use - * improveTypeFromExtension($mime, $ext) later to improve mime type. + * improveTypeFromExtension($mime, $ext) later to improve MIME type. * - * @return string The mime type of $file + * @return string The MIME type of $file */ private function detectMimeType( $file, $ext = true ) { - global $wgMimeDetectorCommand; - /** @todo Make $ext default to false. Or better, remove it. */ if ( $ext ) { wfDebug( __METHOD__ . ": WARNING: use of the \$ext parameter is deprecated. " . "Use improveTypeFromExtension(\$mime, \$ext) instead.\n" ); } + $mimeDetectorCommand = $this->mConfig->get( 'MimeDetectorCommand' ); $m = null; - if ( $wgMimeDetectorCommand ) { + if ( $mimeDetectorCommand ) { $args = wfEscapeShellArg( $file ); - $m = wfShellExec( "$wgMimeDetectorCommand $args" ); + $m = wfShellExec( "$mimeDetectorCommand $args" ); } elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) { - - # This required the fileinfo extension by PECL, - # see http://pecl.php.net/package/fileinfo - # This must be compiled into PHP - # - # finfo is the official replacement for the deprecated - # mime_content_type function, see below. - # - # If you may need to load the fileinfo extension at runtime, set - # $wgLoadFileinfoExtension in LocalSettings.php - - $mime_magic_resource = finfo_open( FILEINFO_MIME ); /* return mime type ala mimetype extension */ + $mime_magic_resource = finfo_open( FILEINFO_MIME ); if ( $mime_magic_resource ) { $m = finfo_file( $mime_magic_resource, $file ); @@ -946,21 +949,6 @@ class MimeMagic { } else { wfDebug( __METHOD__ . ": finfo_open failed on " . FILEINFO_MIME . "!\n" ); } - } elseif ( function_exists( "mime_content_type" ) ) { - - # NOTE: this function is available since PHP 4.3.0, but only if - # PHP was compiled with --with-mime-magic or, before 4.3.2, with - # --enable-mime-magic. - # - # On Windows, you must set mime_magic.magicfile in php.ini to point - # to the mime.magic file bundled with PHP; sometimes, this may even - # be needed under *nix. - # - # Also note that this has been DEPRECATED in favor of the fileinfo - # extension by PECL, see above. - # See http://www.php.net/manual/en/ref.mime-magic.php for details. - - $m = mime_content_type( $file ); } else { wfDebug( __METHOD__ . ": no magic mime detector found!\n" ); } @@ -1003,18 +991,18 @@ class MimeMagic { } /** - * Determine the media type code for a file, using its mime type, name and + * Determine the media type code for a file, using its MIME type, name and * possibly its contents. * - * This function relies on the findMediaType(), mapping extensions and mime + * This function relies on the findMediaType(), mapping extensions and MIME * types to media types. * * @todo analyse file if need be * @todo look at multiple extension, separately and together. * * @param string $path Full path to the image file, in case we have to look at the contents - * (if null, only the mime type is used to determine the media type code). - * @param string $mime Mime type. If null it will be guessed using guessMimeType. + * (if null, only the MIME type is used to determine the media type code). + * @param string $mime MIME type. If null it will be guessed using guessMimeType. * * @return string A value to be used with the MEDIATYPE_xxx constants. */ @@ -1023,7 +1011,7 @@ class MimeMagic { return MEDIATYPE_UNKNOWN; } - // If mime type is unknown, guess it + // If MIME type is unknown, guess it if ( !$mime ) { $mime = $this->guessMimeType( $path, false ); } @@ -1056,7 +1044,7 @@ class MimeMagic { } } - // Check for entry for full mime type + // Check for entry for full MIME type if ( $mime ) { $type = $this->findMediaType( $mime ); if ( $type !== MEDIATYPE_UNKNOWN ) { @@ -1076,7 +1064,7 @@ class MimeMagic { } } - // Check major mime type + // Check major MIME type if ( $mime ) { $i = strpos( $mime, '/' ); if ( $i !== false ) { @@ -1096,9 +1084,9 @@ class MimeMagic { } /** - * Returns a media code matching the given mime type or file extension. + * Returns a media code matching the given MIME type or file extension. * File extensions are represented by a string starting with a dot (.) to - * distinguish them from mime types. + * distinguish them from MIME types. * * This function relies on the mapping defined by $this->mMediaTypes * @access private @@ -1107,7 +1095,7 @@ class MimeMagic { */ function findMediaType( $extMime ) { if ( strpos( $extMime, '.' ) === 0 ) { - // If it's an extension, look up the mime types + // If it's an extension, look up the MIME types $m = $this->getTypesForExtension( substr( $extMime, 1 ) ); if ( !$m ) { return MEDIATYPE_UNKNOWN; @@ -1115,7 +1103,7 @@ class MimeMagic { $m = explode( ' ', $m ); } else { - // Normalize mime type + // Normalize MIME type if ( isset( $this->mMimeTypeAliases[$extMime] ) ) { $extMime = $this->mMimeTypeAliases[$extMime]; } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 87a0809257..af90ca6da4 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -311,6 +311,7 @@ class OutputPage extends ContextSource { * Constructor for OutputPage. This should not be called directly. * Instead a new RequestContext should be created and it will implicitly create * a OutputPage tied to that context. + * @param IContextSource|null $context */ function __construct( IContextSource $context = null ) { if ( $context === null ) { @@ -388,6 +389,7 @@ class OutputPage extends ContextSource { /** * Set the URL to be used for the . This should be used * in preference to addLink(), to avoid duplicate link tags. + * @param string $url */ function setCanonicalUrl( $url ) { $this->mCanonicalUrl = $url; @@ -447,15 +449,14 @@ class OutputPage extends ContextSource { * @param string $version Style version of the file. Defaults to $wgStyleVersion */ public function addScriptFile( $file, $version = null ) { - global $wgStylePath, $wgStyleVersion; // See if $file parameter is an absolute URL or begins with a slash if ( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) { $path = $file; } else { - $path = "{$wgStylePath}/common/{$file}"; + $path = $this->getConfig()->get( 'StylePath' ) . "/common/{$file}"; } if ( is_null( $version ) ) { - $version = $wgStyleVersion; + $version = $this->getConfig()->get( 'StyleVersion' ); } $this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) ); } @@ -731,13 +732,12 @@ class OutputPage extends ContextSource { * @return bool True if cache-ok headers was sent. */ public function checkLastModified( $timestamp ) { - global $wgCachePages, $wgCacheEpoch, $wgUseSquid, $wgSquidMaxage; - if ( !$timestamp || $timestamp == '19700101000000' ) { wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" ); return false; } - if ( !$wgCachePages ) { + $config = $this->getConfig(); + if ( !$config->get( 'CachePages' ) ) { wfDebug( __METHOD__ . ": CACHE DISABLED\n" ); return false; } @@ -746,11 +746,11 @@ class OutputPage extends ContextSource { $modifiedTimes = array( 'page' => $timestamp, 'user' => $this->getUser()->getTouched(), - 'epoch' => $wgCacheEpoch + 'epoch' => $config->get( 'CacheEpoch' ) ); - if ( $wgUseSquid ) { + if ( $config->get( 'UseSquid' ) ) { // bug 44570: the core page itself may not change, but resources might - $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $wgSquidMaxage ); + $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $config->get( 'SquidMaxage' ) ); } wfRunHooks( 'OutputPageCheckLastModified', array( &$modifiedTimes ) ); @@ -1002,9 +1002,9 @@ class OutputPage extends ContextSource { * Add a subtitle containing a backlink to a page * * @param Title $title Title to link to + * @param array $query Array of additional parameters to include in the link */ - public function addBacklinkSubtitle( Title $title ) { - $query = array(); + public function addBacklinkSubtitle( Title $title, $query = array() ) { if ( $title->isRedirect() ) { $query['redirect'] = 'no'; } @@ -1105,11 +1105,9 @@ class OutputPage extends ContextSource { * default links */ public function setFeedAppendQuery( $val ) { - global $wgAdvertisedFeedTypes; - $this->mFeedLinks = array(); - foreach ( $wgAdvertisedFeedTypes as $type ) { + foreach ( $this->getConfig()->get( 'AdvertisedFeedTypes' ) as $type ) { $query = "feed=$type"; if ( is_string( $val ) ) { $query .= '&' . $val; @@ -1125,9 +1123,7 @@ class OutputPage extends ContextSource { * @param string $href URL */ public function addFeedLink( $format, $href ) { - global $wgAdvertisedFeedTypes; - - if ( in_array( $format, $wgAdvertisedFeedTypes ) ) { + if ( in_array( $format, $this->getConfig()->get( 'AdvertisedFeedTypes' ) ) ) { $this->mFeedLinks[$format] = $href; } } @@ -1251,9 +1247,15 @@ class OutputPage extends ContextSource { # Fetch existence plus the hiddencat property $dbr = wfGetDB( DB_SLAVE ); + $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_len', + 'page_is_redirect', 'page_latest', 'pp_value' ); + + if ( $this->getConfig()->get( 'ContentHandlerUseDB' ) ) { + $fields[] = 'page_content_model'; + } + $res = $dbr->select( array( 'page', 'page_props' ), - array( 'page_id', 'page_namespace', 'page_title', 'page_len', - 'page_is_redirect', 'page_latest', 'pp_value' ), + $fields, $lb->constructSet( 'page', $dbr ), __METHOD__, array(), @@ -1609,7 +1611,7 @@ class OutputPage extends ContextSource { * @deprecated since 1.24, use addParserOutputMetadata() instead. * @param ParserOutput $parserOutput */ - public function addParserOutputNoText( &$parserOutput ) { + public function addParserOutputNoText( $parserOutput ) { $this->addParserOutputMetadata( $parserOutput ); } @@ -1621,7 +1623,7 @@ class OutputPage extends ContextSource { * @since 1.24 * @param ParserOutput $parserOutput */ - public function addParserOutputMetadata( &$parserOutput ) { + public function addParserOutputMetadata( $parserOutput ) { $this->mLanguageLinks += $parserOutput->getLanguageLinks(); $this->addCategoryLinks( $parserOutput->getCategories() ); $this->mNewSectionLink = $parserOutput->getNewSection(); @@ -1655,11 +1657,11 @@ class OutputPage extends ContextSource { } // Hooks registered in the object - global $wgParserOutputHooks; + $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' ); foreach ( $parserOutput->getOutputHooks() as $hookInfo ) { list( $hookName, $data ) = $hookInfo; - if ( isset( $wgParserOutputHooks[$hookName] ) ) { - call_user_func( $wgParserOutputHooks[$hookName], $this, $parserOutput, $data ); + if ( isset( $parserOutputHooks[$hookName] ) ) { + call_user_func( $parserOutputHooks[$hookName], $this, $parserOutput, $data ); } } @@ -1677,7 +1679,7 @@ class OutputPage extends ContextSource { * @since 1.24 * @param ParserOutput $parserOutput */ - public function addParserOutputContent( &$parserOutput ) { + public function addParserOutputContent( $parserOutput ) { $this->addParserOutputText( $parserOutput ); $this->addModules( $parserOutput->getModules() ); @@ -1694,7 +1696,7 @@ class OutputPage extends ContextSource { * @since 1.24 * @param ParserOutput $parserOutput */ - public function addParserOutputText( &$parserOutput ) { + public function addParserOutputText( $parserOutput ) { $text = $parserOutput->getText(); wfRunHooks( 'OutputPageBeforeHTML', array( &$this, &$text ) ); $this->addHTML( $text ); @@ -1705,7 +1707,7 @@ class OutputPage extends ContextSource { * * @param ParserOutput $parserOutput */ - function addParserOutput( &$parserOutput ) { + function addParserOutput( $parserOutput ) { $this->addParserOutputMetadata( $parserOutput ); $parserOutput->setTOCEnabled( $this->mEnableTOC ); @@ -1809,17 +1811,17 @@ class OutputPage extends ContextSource { * @return array */ function getCacheVaryCookies() { - global $wgCookiePrefix, $wgCacheVaryCookies; static $cookies; if ( $cookies === null ) { + $config = $this->getConfig(); $cookies = array_merge( array( - "{$wgCookiePrefix}Token", - "{$wgCookiePrefix}LoggedOut", + $config->get( 'CookiePrefix' ) . 'Token', + $config->get( 'CookiePrefix' ) . 'LoggedOut', "forceHTTPS", session_name() ), - $wgCacheVaryCookies + $config->get( 'CacheVaryCookies' ) ); wfRunHooks( 'GetCacheVaryCookies', array( $this, &$cookies ) ); } @@ -1985,11 +1987,11 @@ class OutputPage extends ContextSource { * @return string */ public function getFrameOptions() { - global $wgBreakFrames, $wgEditPageFrameOptions; - if ( $wgBreakFrames ) { + $config = $this->getConfig(); + if ( $config->get( 'BreakFrames' ) ) { return 'DENY'; - } elseif ( $this->mPreventClickjacking && $wgEditPageFrameOptions ) { - return $wgEditPageFrameOptions; + } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) { + return $config->get( 'EditPageFrameOptions' ); } return false; } @@ -1998,10 +2000,9 @@ class OutputPage extends ContextSource { * Send cache control HTTP headers */ public function sendCacheControl() { - global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgUseXVO; - $response = $this->getRequest()->response(); - if ( $wgUseETag && $this->mETag ) { + $config = $this->getConfig(); + if ( $config->get( 'UseETag' ) && $this->mETag ) { $response->header( "ETag: $this->mETag" ); } @@ -2012,24 +2013,24 @@ class OutputPage extends ContextSource { # maintain different caches for logged-in users and non-logged in ones $response->header( $this->getVaryHeader() ); - if ( $wgUseXVO ) { + if ( $config->get( 'UseXVO' ) ) { # Add an X-Vary-Options header for Squid with Wikimedia patches $response->header( $this->getXVO() ); } if ( $this->mEnableClientCache ) { if ( - $wgUseSquid && session_id() == '' && !$this->isPrintable() && + $config->get( 'UseSquid' ) && session_id() == '' && !$this->isPrintable() && $this->mSquidMaxage != 0 && !$this->haveCacheVaryCookies() ) { - if ( $wgUseESI ) { + if ( $config->get( 'UseESI' ) ) { # We'll purge the proxy cache explicitly, but require end user agents # to revalidate against the proxy on each visit. # Surrogate-Control controls our Squid, Cache-Control downstream caches wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **\n", 'log' ); # start with a shorter timeout for initial testing # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"'); - $response->header( 'Surrogate-Control: max-age=' . $wgSquidMaxage + $response->header( 'Surrogate-Control: max-age=' . $config->get( 'SquidMaxage' ) . '+' . $this->mSquidMaxage . ', content="ESI/1.0"' ); $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { @@ -2069,8 +2070,7 @@ class OutputPage extends ContextSource { * the object, let's actually output it: */ public function output() { - global $wgLanguageCode, $wgDebugRedirects, $wgMimeType, $wgVaryOnXFP, - $wgResponsiveImages; + global $wgLanguageCode; if ( $this->mDoNothing ) { return; @@ -2079,6 +2079,7 @@ class OutputPage extends ContextSource { wfProfileIn( __METHOD__ ); $response = $this->getRequest()->response(); + $config = $this->getConfig(); if ( $this->mRedirect != '' ) { # Standards require redirect URLs to be absolute @@ -2089,19 +2090,19 @@ class OutputPage extends ContextSource { if ( wfRunHooks( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) { if ( $code == '301' || $code == '303' ) { - if ( !$wgDebugRedirects ) { + if ( !$config->get( 'DebugRedirects' ) ) { $message = HttpStatus::getMessage( $code ); $response->header( "HTTP/1.1 $code $message" ); } $this->mLastModified = wfTimestamp( TS_RFC2822 ); } - if ( $wgVaryOnXFP ) { + if ( $config->get( 'VaryOnXFP' ) ) { $this->addVaryHeader( 'X-Forwarded-Proto' ); } $this->sendCacheControl(); $response->header( "Content-Type: text/html; charset=utf-8" ); - if ( $wgDebugRedirects ) { + if ( $config->get( 'DebugRedirects' ) ) { $url = htmlspecialchars( $redirect ); print "\n\nRedirect\n\n\n"; print "

Location: $url

\n"; @@ -2123,7 +2124,7 @@ class OutputPage extends ContextSource { # Buffer output; final headers may depend on later processing ob_start(); - $response->header( "Content-type: $wgMimeType; charset=UTF-8" ); + $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' ); $response->header( 'Content-language: ' . $wgLanguageCode ); // Avoid Internet Explorer "compatibility view" in IE 8-10, so that @@ -2152,7 +2153,7 @@ class OutputPage extends ContextSource { ); // Support for high-density display images if enabled - if ( $wgResponsiveImages ) { + if ( $config->get( 'ResponsiveImages' ) ) { $coreModules[] = 'mediawiki.hidpi'; } @@ -2495,9 +2496,9 @@ $templates * @param int $lag Slave lag */ public function showLagWarning( $lag ) { - global $wgSlaveLagWarning, $wgSlaveLagCritical; - if ( $lag >= $wgSlaveLagWarning ) { - $message = $lag < $wgSlaveLagCritical + $config = $this->getConfig(); + if ( $lag >= $config->get( 'SlaveLagWarning' ) ) { + $message = $lag < $config->get( 'SlaveLagCritical' ) ? 'lag-warn-normal' : 'lag-warn-high'; $wrap = Html::rawElement( 'div', array( 'class' => "mw-{$message}" ), "\n$1\n" ); @@ -2584,7 +2585,7 @@ $templates * @return string The doctype, opening "", and head element. */ public function headElement( Skin $sk, $includeStyle = true ) { - global $wgContLang, $wgMimeType; + global $wgContLang; $userdir = $this->getLanguage()->getDir(); $sitedir = $wgContLang->getDir(); @@ -2601,7 +2602,7 @@ $templates $ret .= "$openHead\n"; } - if ( !Html::isXmlMimeType( $wgMimeType ) ) { + if ( !Html::isXmlMimeType( $this->getConfig()->get( 'MimeType' ) ) ) { // Add // This should be before since it defines the charset used by // text including the text inside <title>. @@ -2671,7 +2672,7 @@ $templates */ public function getResourceLoader() { if ( is_null( $this->mResourceLoader ) ) { - $this->mResourceLoader = new ResourceLoader(); + $this->mResourceLoader = new ResourceLoader( $this->getConfig() ); } return $this->mResourceLoader; } @@ -2690,8 +2691,6 @@ $templates protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) { - global $wgResourceLoaderUseESI; - $modules = (array)$modules; $links = array( @@ -2727,6 +2726,7 @@ $templates // Create keyed-by-source and then keyed-by-group list of module objects from modules list $sortedModules = array(); $resourceLoader = $this->getResourceLoader(); + $resourceLoaderUseESI = $this->getConfig()->get( 'ResourceLoaderUseESI' ); foreach ( $modules as $name ) { $module = $resourceLoader->getModule( $name ); # Check that we're allowed to include this module on this page @@ -2826,7 +2826,7 @@ $templates $moduleContext = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) ); $url = $resourceLoader->createLoaderURL( $source, $moduleContext, $extraQuery ); - if ( $useESI && $wgResourceLoaderUseESI ) { + if ( $useESI && $resourceLoaderUseESI ) { $esi = Xml::element( 'esi:include', array( 'src' => $url ) ); if ( $only == ResourceLoaderModule::TYPE_STYLES ) { $link = Html::inlineStyle( $esi ); @@ -2903,8 +2903,6 @@ $templates * @return string HTML fragment */ function getHeadScripts() { - global $wgResourceLoaderExperimentalAsyncLoading; - // Startup - this will immediately load jquery and mediawiki modules $links = array(); $links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true ); @@ -2944,7 +2942,7 @@ $templates ); } - if ( $wgResourceLoaderExperimentalAsyncLoading ) { + if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { $links[] = $this->getScriptsForBottomQueue( true ); } @@ -2963,8 +2961,6 @@ $templates * @return string */ function getScriptsForBottomQueue( $inHead ) { - global $wgAllowUserJs; - // Scripts and messages "only" requests marked for bottom inclusion // If we're in the <head>, use load() calls rather than <script src="..."> tags // Messages should go first @@ -2998,7 +2994,7 @@ $templates ); // Add user JS if enabled - if ( $wgAllowUserJs + if ( $this->getConfig()->get( 'AllowUserJs' ) && $this->getUser()->isLoggedIn() && $this->getTitle() && $this->getTitle()->isJsSubpage() @@ -3037,15 +3033,13 @@ $templates * @return string */ function getBottomScripts() { - global $wgResourceLoaderExperimentalAsyncLoading; - // Optimise jQuery ready event cross-browser. // This also enforces $.isReady to be true at </body> which fixes the // mw.loader bug in Firefox with using document.write between </body> // and the DOMContentReady event (bug 47457). $html = Html::inlineScript( 'window.jQuery && jQuery.ready();' ); - if ( !$wgResourceLoaderExperimentalAsyncLoading ) { + if ( !$this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { $html .= $this->getScriptsForBottomQueue( false ); } @@ -3232,13 +3226,10 @@ $templates * @return array Array in format "link name or number => 'link html'". */ public function getHeadLinksArray() { - global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI, - $wgSitename, $wgVersion, - $wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes, - $wgDisableLangConversion, $wgCanonicalLanguageLinks, - $wgRightsPage, $wgRightsUrl; + global $wgVersion; $tags = array(); + $config = $this->getConfig(); $canonicalUrl = $this->mCanonicalUrl; @@ -3281,7 +3272,7 @@ $templates } # Universal edit button - if ( $wgUniversalEditButton && $this->isArticleRelated() ) { + if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) { $user = $this->getUser(); if ( $this->getTitle()->quickUserCan( 'edit', $user ) && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) { @@ -3306,17 +3297,17 @@ $templates # should not matter, but Konqueror (3.5.9 at least) incorrectly # uses whichever one appears later in the HTML source. Make sure # apple-touch-icon is specified first to avoid this. - if ( $wgAppleTouchIcon !== false ) { + if ( $config->get( 'AppleTouchIcon' ) !== false ) { $tags['apple-touch-icon'] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', - 'href' => $wgAppleTouchIcon + 'href' => $config->get( 'AppleTouchIcon' ) ) ); } - if ( $wgFavicon !== false ) { + if ( $config->get( 'Favicon' ) !== false ) { $tags['favicon'] = Html::element( 'link', array( 'rel' => 'shortcut icon', - 'href' => $wgFavicon + 'href' => $config->get( 'Favicon' ) ) ); } @@ -3328,7 +3319,7 @@ $templates 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(), ) ); - if ( $wgEnableAPI ) { + if ( $config->get( 'EnableAPI' ) ) { # Real Simple Discovery link, provides auto-discovery information # for the MediaWiki API (and potentially additional custom API # support such as WordPress or Twitter-compatible APIs for a @@ -3347,39 +3338,37 @@ $templates } # Language variants - if ( !$wgDisableLangConversion && $wgCanonicalLanguageLinks ) { + if ( !$config->get( 'DisableLangConversion' ) ) { $lang = $this->getTitle()->getPageLanguage(); if ( $lang->hasVariants() ) { - - $urlvar = $lang->getURLVariant(); - - if ( !$urlvar ) { - $variants = $lang->getVariants(); - foreach ( $variants as $_v ) { - $tags["variant-$_v"] = Html::element( 'link', array( - 'rel' => 'alternate', - 'hreflang' => wfBCP47( $_v ), - 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) ) - ); - } - } else { - $canonicalUrl = $this->getTitle()->getLocalURL(); + $variants = $lang->getVariants(); + foreach ( $variants as $_v ) { + $tags["variant-$_v"] = Html::element( 'link', array( + 'rel' => 'alternate', + 'hreflang' => wfBCP47( $_v ), + 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) ) + ); } } + # x-default link per https://support.google.com/webmasters/answer/189077?hl=en + $tags["variant-x-default"] = Html::element( 'link', array( + 'rel' => 'alternate', + 'hreflang' => 'x-default', + 'href' => $this->getTitle()->getLocalURL() ) ); } # Copyright $copyright = ''; - if ( $wgRightsPage ) { - $copy = Title::newFromText( $wgRightsPage ); + if ( $config->get( 'RightsPage' ) ) { + $copy = Title::newFromText( $config->get( 'RightsPage' ) ); if ( $copy ) { $copyright = $copy->getLocalURL(); } } - if ( !$copyright && $wgRightsUrl ) { - $copyright = $wgRightsUrl; + if ( !$copyright && $config->get( 'RightsUrl' ) ) { + $copyright = $config->get( 'RightsUrl' ); } if ( $copyright ) { @@ -3390,7 +3379,7 @@ $templates } # Feeds - if ( $wgFeed ) { + if ( $config->get( 'Feed' ) ) { foreach ( $this->getSyndicationLinks() as $format => $link ) { # Use the page name for the title. In principle, this could # lead to issues with having the same name for different feeds @@ -3412,31 +3401,31 @@ $templates # like to promote instead of the RC feed (maybe like a "Recent New Articles" # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined. # If so, use it instead. - if ( $wgOverrideSiteFeed ) { - foreach ( $wgOverrideSiteFeed as $type => $feedUrl ) { + $sitename = $config->get( 'Sitename' ); + if ( $config->get( 'OverrideSiteFeed' ) ) { + foreach ( $config->get( 'OverrideSiteFeed' ) as $type => $feedUrl ) { // Note, this->feedLink escapes the url. $tags[] = $this->feedLink( $type, $feedUrl, - $this->msg( "site-{$type}-feed", $wgSitename )->text() + $this->msg( "site-{$type}-feed", $sitename )->text() ); } } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) { $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); - foreach ( $wgAdvertisedFeedTypes as $format ) { + foreach ( $config->get( 'AdvertisedFeedTypes' ) as $format ) { $tags[] = $this->feedLink( $format, $rctitle->getLocalURL( array( 'feed' => $format ) ), # For grep: 'site-rss-feed', 'site-atom-feed' - $this->msg( "site-{$format}-feed", $wgSitename )->text() + $this->msg( "site-{$format}-feed", $sitename )->text() ); } } } # Canonical URL - global $wgEnableCanonicalServerLink; - if ( $wgEnableCanonicalServerLink ) { + if ( $config->get( 'EnableCanonicalServerLink' ) ) { if ( $canonicalUrl !== false ) { $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL ); } else { @@ -3515,6 +3504,8 @@ $templates if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) { # If wanted, and the interface is right-to-left, flip the CSS $style_css = CSSJanus::transform( $style_css, true, false ); + } else { + $style_css = CSSJanus::nullTransform( $style_css ); } $this->mInlineStyles .= Html::inlineStyle( $style_css ) . "\n"; } @@ -3526,7 +3517,7 @@ $templates * @return string */ public function buildCssLinks() { - global $wgAllowUserCss, $wgContLang; + global $wgContLang; $this->getSkin()->setupSkinUserCss( $this ); @@ -3551,7 +3542,7 @@ $templates $moduleStyles[] = 'user.groups'; // Per-user custom styles - if ( $wgAllowUserCss && $this->getTitle()->isCssSubpage() && $this->userCanPreview() ) { + if ( $this->getConfig()->get( 'AllowUserCss' ) && $this->getTitle()->isCssSubpage() && $this->userCanPreview() ) { // We're on a preview of a CSS subpage // Exclude this page from the user module in case it's in there (bug 26283) $link = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false, @@ -3565,6 +3556,8 @@ $templates $previewedCSS = $this->getRequest()->getText( 'wpTextbox1' ); if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) { $previewedCSS = CSSJanus::transform( $previewedCSS, true, false ); + } else { + $previewedCSS = CSSJanus::nullTransform( $previewedCSS ); } $otherTags .= Html::inlineStyle( $previewedCSS ) . "\n"; } else { @@ -3663,8 +3656,8 @@ $templates substr( $style, 0, 6 ) == 'https:' ) { $url = $style; } else { - global $wgStylePath, $wgStyleVersion; - $url = $wgStylePath . '/' . $style . '?' . $wgStyleVersion; + $config = $this->getConfig(); + $url = $config->get( 'StylePath' ) . '/' . $style . '?' . $config->get( 'StyleVersion' ); } $link = Html::linkedStyle( $url, $media ); diff --git a/includes/PHPVersionError.php b/includes/PHPVersionError.php index 0f5a6fc087..44038a5895 100644 --- a/includes/PHPVersionError.php +++ b/includes/PHPVersionError.php @@ -60,7 +60,7 @@ function wfPHPVersionError( $type ) { } $encLogo = htmlspecialchars( str_replace( '//', '/', $dirname . '/' ) . - 'skins/common/images/mediawiki.png' + 'assets/mediawiki.png' ); header( "$protocol 500 MediaWiki configuration Error" ); diff --git a/includes/Pager.php b/includes/Pager.php deleted file mode 100644 index c7de8c1ea0..0000000000 --- a/includes/Pager.php +++ /dev/null @@ -1,1331 +0,0 @@ -<?php -/** - * Efficient paging for SQL queries. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Pager - */ - -/** - * @defgroup Pager Pager - */ - -/** - * Basic pager interface. - * @ingroup Pager - */ -interface Pager { - function getNavigationBar(); - function getBody(); -} - -/** - * IndexPager is an efficient pager which uses a (roughly unique) index in the - * data set to implement paging, rather than a "LIMIT offset,limit" clause. - * In MySQL, such a limit/offset clause requires counting through the - * specified number of offset rows to find the desired data, which can be - * expensive for large offsets. - * - * ReverseChronologicalPager is a child class of the abstract IndexPager, and - * contains some formatting and display code which is specific to the use of - * timestamps as indexes. Here is a synopsis of its operation: - * - * * The query is specified by the offset, limit and direction (dir) - * parameters, in addition to any subclass-specific parameters. - * * The offset is the non-inclusive start of the DB query. A row with an - * index value equal to the offset will never be shown. - * * The query may either be done backwards, where the rows are returned by - * the database in the opposite order to which they are displayed to the - * user, or forwards. This is specified by the "dir" parameter, dir=prev - * means backwards, anything else means forwards. The offset value - * specifies the start of the database result set, which may be either - * the start or end of the displayed data set. This allows "previous" - * links to be implemented without knowledge of the index value at the - * start of the previous page. - * * An additional row beyond the user-specified limit is always requested. - * This allows us to tell whether we should display a "next" link in the - * case of forwards mode, or a "previous" link in the case of backwards - * mode. Determining whether to display the other link (the one for the - * page before the start of the database result set) can be done - * heuristically by examining the offset. - * - * * An empty offset indicates that the offset condition should be omitted - * from the query. This naturally produces either the first page or the - * last page depending on the dir parameter. - * - * Subclassing the pager to implement concrete functionality should be fairly - * simple, please see the examples in HistoryAction.php and - * SpecialBlockList.php. You just need to override formatRow(), - * getQueryInfo() and getIndexField(). Don't forget to call the parent - * constructor if you override it. - * - * @ingroup Pager - */ -abstract class IndexPager extends ContextSource implements Pager { - public $mRequest; - public $mLimitsShown = array( 20, 50, 100, 250, 500 ); - public $mDefaultLimit = 50; - public $mOffset, $mLimit; - public $mQueryDone = false; - public $mDb; - public $mPastTheEndRow; - - /** - * The index to actually be used for ordering. This is a single column, - * for one ordering, even if multiple orderings are supported. - */ - protected $mIndexField; - /** - * An array of secondary columns to order by. These fields are not part of the offset. - * This is a column list for one ordering, even if multiple orderings are supported. - */ - protected $mExtraSortFields; - /** For pages that support multiple types of ordering, which one to use. - */ - protected $mOrderType; - /** - * $mDefaultDirection gives the direction to use when sorting results: - * false for ascending, true for descending. If $mIsBackwards is set, we - * start from the opposite end, but we still sort the page itself according - * to $mDefaultDirection. E.g., if $mDefaultDirection is false but we're - * going backwards, we'll display the last page of results, but the last - * result will be at the bottom, not the top. - * - * Like $mIndexField, $mDefaultDirection will be a single value even if the - * class supports multiple default directions for different order types. - */ - public $mDefaultDirection; - public $mIsBackwards; - - /** True if the current result set is the first one */ - public $mIsFirst; - public $mIsLast; - - protected $mLastShown, $mFirstShown, $mPastTheEndIndex, $mDefaultQuery, $mNavigationBar; - - /** - * Whether to include the offset in the query - */ - protected $mIncludeOffset = false; - - /** - * Result object for the query. Warning: seek before use. - * - * @var ResultWrapper - */ - public $mResult; - - public function __construct( IContextSource $context = null ) { - if ( $context ) { - $this->setContext( $context ); - } - - $this->mRequest = $this->getRequest(); - - # NB: the offset is quoted, not validated. It is treated as an - # arbitrary string to support the widest variety of index types. Be - # careful outputting it into HTML! - $this->mOffset = $this->mRequest->getText( 'offset' ); - - # Use consistent behavior for the limit options - $this->mDefaultLimit = $this->getUser()->getIntOption( 'rclimit' ); - if ( !$this->mLimit ) { - // Don't override if a subclass calls $this->setLimit() in its constructor. - list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset(); - } - - $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' ); - # Let the subclass set the DB here; otherwise use a slave DB for the current wiki - $this->mDb = $this->mDb ?: wfGetDB( DB_SLAVE ); - - $index = $this->getIndexField(); // column to sort on - $extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning - $order = $this->mRequest->getVal( 'order' ); - if ( is_array( $index ) && isset( $index[$order] ) ) { - $this->mOrderType = $order; - $this->mIndexField = $index[$order]; - $this->mExtraSortFields = isset( $extraSort[$order] ) - ? (array)$extraSort[$order] - : array(); - } elseif ( is_array( $index ) ) { - # First element is the default - reset( $index ); - list( $this->mOrderType, $this->mIndexField ) = each( $index ); - $this->mExtraSortFields = isset( $extraSort[$this->mOrderType] ) - ? (array)$extraSort[$this->mOrderType] - : array(); - } else { - # $index is not an array - $this->mOrderType = null; - $this->mIndexField = $index; - $this->mExtraSortFields = (array)$extraSort; - } - - if ( !isset( $this->mDefaultDirection ) ) { - $dir = $this->getDefaultDirections(); - $this->mDefaultDirection = is_array( $dir ) - ? $dir[$this->mOrderType] - : $dir; - } - } - - /** - * Get the Database object in use - * - * @return DatabaseBase - */ - public function getDatabase() { - return $this->mDb; - } - - /** - * Do the query, using information from the object context. This function - * has been kept minimal to make it overridable if necessary, to allow for - * result sets formed from multiple DB queries. - */ - public function doQuery() { - # Use the child class name for profiling - $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; - wfProfileIn( $fname ); - - $descending = ( $this->mIsBackwards == $this->mDefaultDirection ); - # Plus an extra row so that we can tell the "next" link should be shown - $queryLimit = $this->mLimit + 1; - - if ( $this->mOffset == '' ) { - $isFirst = true; - } else { - // If there's an offset, we may or may not be at the first entry. - // The only way to tell is to run the query in the opposite - // direction see if we get a row. - $oldIncludeOffset = $this->mIncludeOffset; - $this->mIncludeOffset = !$this->mIncludeOffset; - $isFirst = !$this->reallyDoQuery( $this->mOffset, 1, !$descending )->numRows(); - $this->mIncludeOffset = $oldIncludeOffset; - } - - $this->mResult = $this->reallyDoQuery( - $this->mOffset, - $queryLimit, - $descending - ); - - $this->extractResultInfo( $isFirst, $queryLimit, $this->mResult ); - $this->mQueryDone = true; - - $this->preprocessResults( $this->mResult ); - $this->mResult->rewind(); // Paranoia - - wfProfileOut( $fname ); - } - - /** - * @return ResultWrapper The result wrapper. - */ - function getResult() { - return $this->mResult; - } - - /** - * Set the offset from an other source than the request - * - * @param int|string $offset - */ - function setOffset( $offset ) { - $this->mOffset = $offset; - } - - /** - * Set the limit from an other source than the request - * - * Verifies limit is between 1 and 5000 - * - * @param int|string $limit - */ - function setLimit( $limit ) { - $limit = (int)$limit; - // WebRequest::getLimitOffset() puts a cap of 5000, so do same here. - if ( $limit > 5000 ) { - $limit = 5000; - } - if ( $limit > 0 ) { - $this->mLimit = $limit; - } - } - - /** - * Get the current limit - * - * @return int - */ - function getLimit() { - return $this->mLimit; - } - - /** - * Set whether a row matching exactly the offset should be also included - * in the result or not. By default this is not the case, but when the - * offset is user-supplied this might be wanted. - * - * @param bool $include - */ - public function setIncludeOffset( $include ) { - $this->mIncludeOffset = $include; - } - - /** - * Extract some useful data from the result object for use by - * the navigation bar, put it into $this - * - * @param bool $isFirst False if there are rows before those fetched (i.e. - * if a "previous" link would make sense) - * @param int $limit Exact query limit - * @param ResultWrapper $res - */ - function extractResultInfo( $isFirst, $limit, ResultWrapper $res ) { - $numRows = $res->numRows(); - if ( $numRows ) { - # Remove any table prefix from index field - $parts = explode( '.', $this->mIndexField ); - $indexColumn = end( $parts ); - - $row = $res->fetchRow(); - $firstIndex = $row[$indexColumn]; - - # Discard the extra result row if there is one - if ( $numRows > $this->mLimit && $numRows > 1 ) { - $res->seek( $numRows - 1 ); - $this->mPastTheEndRow = $res->fetchObject(); - $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexColumn; - $res->seek( $numRows - 2 ); - $row = $res->fetchRow(); - $lastIndex = $row[$indexColumn]; - } else { - $this->mPastTheEndRow = null; - # Setting indexes to an empty string means that they will be - # omitted if they would otherwise appear in URLs. It just so - # happens that this is the right thing to do in the standard - # UI, in all the relevant cases. - $this->mPastTheEndIndex = ''; - $res->seek( $numRows - 1 ); - $row = $res->fetchRow(); - $lastIndex = $row[$indexColumn]; - } - } else { - $firstIndex = ''; - $lastIndex = ''; - $this->mPastTheEndRow = null; - $this->mPastTheEndIndex = ''; - } - - if ( $this->mIsBackwards ) { - $this->mIsFirst = ( $numRows < $limit ); - $this->mIsLast = $isFirst; - $this->mLastShown = $firstIndex; - $this->mFirstShown = $lastIndex; - } else { - $this->mIsFirst = $isFirst; - $this->mIsLast = ( $numRows < $limit ); - $this->mLastShown = $lastIndex; - $this->mFirstShown = $firstIndex; - } - } - - /** - * Get some text to go in brackets in the "function name" part of the SQL comment - * - * @return string - */ - function getSqlComment() { - return get_class( $this ); - } - - /** - * Do a query with specified parameters, rather than using the object - * context - * - * @param string $offset Index offset, inclusive - * @param int $limit Exact query limit - * @param bool $descending Query direction, false for ascending, true for descending - * @return ResultWrapper - */ - public function reallyDoQuery( $offset, $limit, $descending ) { - list( $tables, $fields, $conds, $fname, $options, $join_conds ) = - $this->buildQueryInfo( $offset, $limit, $descending ); - - return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); - } - - /** - * Build variables to use by the database wrapper. - * - * @param string $offset Index offset, inclusive - * @param int $limit Exact query limit - * @param bool $descending Query direction, false for ascending, true for descending - * @return array - */ - protected function buildQueryInfo( $offset, $limit, $descending ) { - $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')'; - $info = $this->getQueryInfo(); - $tables = $info['tables']; - $fields = $info['fields']; - $conds = isset( $info['conds'] ) ? $info['conds'] : array(); - $options = isset( $info['options'] ) ? $info['options'] : array(); - $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array(); - $sortColumns = array_merge( array( $this->mIndexField ), $this->mExtraSortFields ); - if ( $descending ) { - $options['ORDER BY'] = $sortColumns; - $operator = $this->mIncludeOffset ? '>=' : '>'; - } else { - $orderBy = array(); - foreach ( $sortColumns as $col ) { - $orderBy[] = $col . ' DESC'; - } - $options['ORDER BY'] = $orderBy; - $operator = $this->mIncludeOffset ? '<=' : '<'; - } - if ( $offset != '' ) { - $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ); - } - $options['LIMIT'] = intval( $limit ); - return array( $tables, $fields, $conds, $fname, $options, $join_conds ); - } - - /** - * Pre-process results; useful for performing batch existence checks, etc. - * - * @param ResultWrapper $result - */ - protected function preprocessResults( $result ) { - } - - /** - * Get the formatted result list. Calls getStartBody(), formatRow() and - * getEndBody(), concatenates the results and returns them. - * - * @return string - */ - public function getBody() { - if ( !$this->mQueryDone ) { - $this->doQuery(); - } - - if ( $this->mResult->numRows() ) { - # Do any special query batches before display - $this->doBatchLookups(); - } - - # Don't use any extra rows returned by the query - $numRows = min( $this->mResult->numRows(), $this->mLimit ); - - $s = $this->getStartBody(); - if ( $numRows ) { - if ( $this->mIsBackwards ) { - for ( $i = $numRows - 1; $i >= 0; $i-- ) { - $this->mResult->seek( $i ); - $row = $this->mResult->fetchObject(); - $s .= $this->formatRow( $row ); - } - } else { - $this->mResult->seek( 0 ); - for ( $i = 0; $i < $numRows; $i++ ) { - $row = $this->mResult->fetchObject(); - $s .= $this->formatRow( $row ); - } - } - } else { - $s .= $this->getEmptyBody(); - } - $s .= $this->getEndBody(); - return $s; - } - - /** - * Make a self-link - * - * @param string $text Text displayed on the link - * @param array $query Associative array of parameter to be in the query string - * @param string $type Value of the "rel" attribute - * - * @return string HTML fragment - */ - function makeLink( $text, array $query = null, $type = null ) { - if ( $query === null ) { - return $text; - } - - $attrs = array(); - if ( in_array( $type, array( 'first', 'prev', 'next', 'last' ) ) ) { - # HTML5 rel attributes - $attrs['rel'] = $type; - } - - if ( $type ) { - $attrs['class'] = "mw-{$type}link"; - } - - return Linker::linkKnown( - $this->getTitle(), - $text, - $attrs, - $query + $this->getDefaultQuery() - ); - } - - /** - * Called from getBody(), before getStartBody() is called and - * after doQuery() was called. This will be called only if there - * are rows in the result set. - * - * @return void - */ - protected function doBatchLookups() { - } - - /** - * Hook into getBody(), allows text to be inserted at the start. This - * will be called even if there are no rows in the result set. - * - * @return string - */ - protected function getStartBody() { - return ''; - } - - /** - * Hook into getBody() for the end of the list - * - * @return string - */ - protected function getEndBody() { - return ''; - } - - /** - * Hook into getBody(), for the bit between the start and the - * end when there are no rows - * - * @return string - */ - protected function getEmptyBody() { - return ''; - } - - /** - * Get an array of query parameters that should be put into self-links. - * By default, all parameters passed in the URL are used, except for a - * short blacklist. - * - * @return array Associative array - */ - function getDefaultQuery() { - if ( !isset( $this->mDefaultQuery ) ) { - $this->mDefaultQuery = $this->getRequest()->getQueryValues(); - unset( $this->mDefaultQuery['title'] ); - unset( $this->mDefaultQuery['dir'] ); - unset( $this->mDefaultQuery['offset'] ); - unset( $this->mDefaultQuery['limit'] ); - unset( $this->mDefaultQuery['order'] ); - unset( $this->mDefaultQuery['month'] ); - unset( $this->mDefaultQuery['year'] ); - } - return $this->mDefaultQuery; - } - - /** - * Get the number of rows in the result set - * - * @return int - */ - function getNumRows() { - if ( !$this->mQueryDone ) { - $this->doQuery(); - } - return $this->mResult->numRows(); - } - - /** - * Get a URL query array for the prev, next, first and last links. - * - * @return array - */ - function getPagingQueries() { - if ( !$this->mQueryDone ) { - $this->doQuery(); - } - - # Don't announce the limit everywhere if it's the default - $urlLimit = $this->mLimit == $this->mDefaultLimit ? null : $this->mLimit; - - if ( $this->mIsFirst ) { - $prev = false; - $first = false; - } else { - $prev = array( - 'dir' => 'prev', - 'offset' => $this->mFirstShown, - 'limit' => $urlLimit - ); - $first = array( 'limit' => $urlLimit ); - } - if ( $this->mIsLast ) { - $next = false; - $last = false; - } else { - $next = array( 'offset' => $this->mLastShown, 'limit' => $urlLimit ); - $last = array( 'dir' => 'prev', 'limit' => $urlLimit ); - } - return array( - 'prev' => $prev, - 'next' => $next, - 'first' => $first, - 'last' => $last - ); - } - - /** - * Returns whether to show the "navigation bar" - * - * @return bool - */ - function isNavigationBarShown() { - if ( !$this->mQueryDone ) { - $this->doQuery(); - } - // Hide navigation by default if there is nothing to page - return !( $this->mIsFirst && $this->mIsLast ); - } - - /** - * Get paging links. If a link is disabled, the item from $disabledTexts - * will be used. If there is no such item, the unlinked text from - * $linkTexts will be used. Both $linkTexts and $disabledTexts are arrays - * of HTML. - * - * @param array $linkTexts - * @param array $disabledTexts - * @return array - */ - function getPagingLinks( $linkTexts, $disabledTexts = array() ) { - $queries = $this->getPagingQueries(); - $links = array(); - - foreach ( $queries as $type => $query ) { - if ( $query !== false ) { - $links[$type] = $this->makeLink( - $linkTexts[$type], - $queries[$type], - $type - ); - } elseif ( isset( $disabledTexts[$type] ) ) { - $links[$type] = $disabledTexts[$type]; - } else { - $links[$type] = $linkTexts[$type]; - } - } - - return $links; - } - - function getLimitLinks() { - $links = array(); - if ( $this->mIsBackwards ) { - $offset = $this->mPastTheEndIndex; - } else { - $offset = $this->mOffset; - } - foreach ( $this->mLimitsShown as $limit ) { - $links[] = $this->makeLink( - $this->getLanguage()->formatNum( $limit ), - array( 'offset' => $offset, 'limit' => $limit ), - 'num' - ); - } - return $links; - } - - /** - * Abstract formatting function. This should return an HTML string - * representing the result row $row. Rows will be concatenated and - * returned by getBody() - * - * @param array|stdClass $row Database row - * @return string - */ - abstract function formatRow( $row ); - - /** - * This function should be overridden to provide all parameters - * needed for the main paged query. It returns an associative - * array with the following elements: - * tables => Table(s) for passing to Database::select() - * fields => Field(s) for passing to Database::select(), may be * - * conds => WHERE conditions - * options => option array - * join_conds => JOIN conditions - * - * @return array - */ - abstract function getQueryInfo(); - - /** - * This function should be overridden to return the name of the index fi- - * eld. If the pager supports multiple orders, it may return an array of - * 'querykey' => 'indexfield' pairs, so that a request with &count=querykey - * will use indexfield to sort. In this case, the first returned key is - * the default. - * - * Needless to say, it's really not a good idea to use a non-unique index - * for this! That won't page right. - * - * @return string|array - */ - abstract function getIndexField(); - - /** - * This function should be overridden to return the names of secondary columns - * to order by in addition to the column in getIndexField(). These fields will - * not be used in the pager offset or in any links for users. - * - * If getIndexField() returns an array of 'querykey' => 'indexfield' pairs then - * this must return a corresponding array of 'querykey' => array( fields...) pairs - * in order for a request with &count=querykey to use array( fields...) to sort. - * - * This is useful for pagers that GROUP BY a unique column (say page_id) - * and ORDER BY another (say page_len). Using GROUP BY and ORDER BY both on - * page_len,page_id avoids temp tables (given a page_len index). This would - * also work if page_id was non-unique but we had a page_len,page_id index. - * - * @return array - */ - protected function getExtraSortFields() { - return array(); - } - - /** - * Return the default sorting direction: false for ascending, true for - * descending. You can also have an associative array of ordertype => dir, - * if multiple order types are supported. In this case getIndexField() - * must return an array, and the keys of that must exactly match the keys - * of this. - * - * For backward compatibility, this method's return value will be ignored - * if $this->mDefaultDirection is already set when the constructor is - * called, for instance if it's statically initialized. In that case the - * value of that variable (which must be a boolean) will be used. - * - * Note that despite its name, this does not return the value of the - * $this->mDefaultDirection member variable. That's the default for this - * particular instantiation, which is a single value. This is the set of - * all defaults for the class. - * - * @return bool - */ - protected function getDefaultDirections() { - return false; - } -} - -/** - * IndexPager with an alphabetic list and a formatted navigation bar - * @ingroup Pager - */ -abstract class AlphabeticPager extends IndexPager { - - /** - * Shamelessly stolen bits from ReverseChronologicalPager, - * didn't want to do class magic as may be still revamped - * - * @return string HTML - */ - function getNavigationBar() { - if ( !$this->isNavigationBarShown() ) { - return ''; - } - - if ( isset( $this->mNavigationBar ) ) { - return $this->mNavigationBar; - } - - $linkTexts = array( - 'prev' => $this->msg( 'prevn' )->numParams( $this->mLimit )->escaped(), - 'next' => $this->msg( 'nextn' )->numParams( $this->mLimit )->escaped(), - 'first' => $this->msg( 'page_first' )->escaped(), - 'last' => $this->msg( 'page_last' )->escaped() - ); - - $lang = $this->getLanguage(); - - $pagingLinks = $this->getPagingLinks( $linkTexts ); - $limitLinks = $this->getLimitLinks(); - $limits = $lang->pipeList( $limitLinks ); - - $this->mNavigationBar = $this->msg( 'parentheses' )->rawParams( - $lang->pipeList( array( $pagingLinks['first'], - $pagingLinks['last'] ) ) )->escaped() . " " . - $this->msg( 'viewprevnext' )->rawParams( $pagingLinks['prev'], - $pagingLinks['next'], $limits )->escaped(); - - if ( !is_array( $this->getIndexField() ) ) { - # Early return to avoid undue nesting - return $this->mNavigationBar; - } - - $extra = ''; - $first = true; - $msgs = $this->getOrderTypeMessages(); - foreach ( array_keys( $msgs ) as $order ) { - if ( $first ) { - $first = false; - } else { - $extra .= $this->msg( 'pipe-separator' )->escaped(); - } - - if ( $order == $this->mOrderType ) { - $extra .= $this->msg( $msgs[$order] )->escaped(); - } else { - $extra .= $this->makeLink( - $this->msg( $msgs[$order] )->escaped(), - array( 'order' => $order ) - ); - } - } - - if ( $extra !== '' ) { - $extra = ' ' . $this->msg( 'parentheses' )->rawParams( $extra )->escaped(); - $this->mNavigationBar .= $extra; - } - - return $this->mNavigationBar; - } - - /** - * If this supports multiple order type messages, give the message key for - * enabling each one in getNavigationBar. The return type is an associative - * array whose keys must exactly match the keys of the array returned - * by getIndexField(), and whose values are message keys. - * - * @return array - */ - protected function getOrderTypeMessages() { - return null; - } -} - -/** - * IndexPager with a formatted navigation bar - * @ingroup Pager - */ -abstract class ReverseChronologicalPager extends IndexPager { - public $mDefaultDirection = true; - public $mYear; - public $mMonth; - - function getNavigationBar() { - if ( !$this->isNavigationBarShown() ) { - return ''; - } - - if ( isset( $this->mNavigationBar ) ) { - return $this->mNavigationBar; - } - - $linkTexts = array( - 'prev' => $this->msg( 'pager-newer-n' )->numParams( $this->mLimit )->escaped(), - 'next' => $this->msg( 'pager-older-n' )->numParams( $this->mLimit )->escaped(), - 'first' => $this->msg( 'histlast' )->escaped(), - 'last' => $this->msg( 'histfirst' )->escaped() - ); - - $pagingLinks = $this->getPagingLinks( $linkTexts ); - $limitLinks = $this->getLimitLinks(); - $limits = $this->getLanguage()->pipeList( $limitLinks ); - $firstLastLinks = $this->msg( 'parentheses' )->rawParams( "{$pagingLinks['first']}" . - $this->msg( 'pipe-separator' )->escaped() . - "{$pagingLinks['last']}" )->escaped(); - - $this->mNavigationBar = $firstLastLinks . ' ' . - $this->msg( 'viewprevnext' )->rawParams( - $pagingLinks['prev'], $pagingLinks['next'], $limits )->escaped(); - - return $this->mNavigationBar; - } - - function getDateCond( $year, $month ) { - $year = intval( $year ); - $month = intval( $month ); - - // Basic validity checks - $this->mYear = $year > 0 ? $year : false; - $this->mMonth = ( $month > 0 && $month < 13 ) ? $month : false; - - // Given an optional year and month, we need to generate a timestamp - // to use as "WHERE rev_timestamp <= result" - // Examples: year = 2006 equals < 20070101 (+000000) - // year=2005, month=1 equals < 20050201 - // year=2005, month=12 equals < 20060101 - if ( !$this->mYear && !$this->mMonth ) { - return; - } - - if ( $this->mYear ) { - $year = $this->mYear; - } else { - // If no year given, assume the current one - $timestamp = MWTimestamp::getInstance(); - $year = $timestamp->format( 'Y' ); - // If this month hasn't happened yet this year, go back to last year's month - if ( $this->mMonth > $timestamp->format( 'n' ) ) { - $year--; - } - } - - if ( $this->mMonth ) { - $month = $this->mMonth + 1; - // For December, we want January 1 of the next year - if ( $month > 12 ) { - $month = 1; - $year++; - } - } else { - // No month implies we want up to the end of the year in question - $month = 1; - $year++; - } - - // Y2K38 bug - if ( $year > 2032 ) { - $year = 2032; - } - - $ymd = (int)sprintf( "%04d%02d01", $year, $month ); - - if ( $ymd > 20320101 ) { - $ymd = 20320101; - } - - $this->mOffset = $this->mDb->timestamp( "${ymd}000000" ); - } -} - -/** - * Table-based display with a user-selectable sort order - * @ingroup Pager - */ -abstract class TablePager extends IndexPager { - protected $mSort; - - protected $mCurrentRow; - - public function __construct( IContextSource $context = null ) { - if ( $context ) { - $this->setContext( $context ); - } - - $this->mSort = $this->getRequest()->getText( 'sort' ); - if ( !array_key_exists( $this->mSort, $this->getFieldNames() ) - || !$this->isFieldSortable( $this->mSort ) - ) { - $this->mSort = $this->getDefaultSort(); - } - if ( $this->getRequest()->getBool( 'asc' ) ) { - $this->mDefaultDirection = false; - } elseif ( $this->getRequest()->getBool( 'desc' ) ) { - $this->mDefaultDirection = true; - } /* Else leave it at whatever the class default is */ - - parent::__construct(); - } - - /** - * @protected - * @return string - */ - function getStartBody() { - global $wgStylePath; - $sortClass = $this->getSortHeaderClass(); - - $s = ''; - $fields = $this->getFieldNames(); - - # Make table header - foreach ( $fields as $field => $name ) { - if ( strval( $name ) == '' ) { - $s .= Html::rawElement( 'th', array(), ' ' ) . "\n"; - } elseif ( $this->isFieldSortable( $field ) ) { - $query = array( 'sort' => $field, 'limit' => $this->mLimit ); - if ( $field == $this->mSort ) { - # This is the sorted column - # Prepare a link that goes in the other sort order - if ( $this->mDefaultDirection ) { - # Descending - $image = 'Arr_d.png'; - $query['asc'] = '1'; - $query['desc'] = ''; - $alt = $this->msg( 'descending_abbrev' )->escaped(); - } else { - # Ascending - $image = 'Arr_u.png'; - $query['asc'] = ''; - $query['desc'] = '1'; - $alt = $this->msg( 'ascending_abbrev' )->escaped(); - } - $image = "$wgStylePath/common/images/$image"; - $link = $this->makeLink( - Html::element( 'img', array( 'width' => 12, 'height' => 12, - 'alt' => $alt, 'src' => $image ) ) . htmlspecialchars( $name ), $query ); - $s .= Html::rawElement( 'th', array( 'class' => $sortClass ), $link ) . "\n"; - } else { - $s .= Html::rawElement( 'th', array(), - $this->makeLink( htmlspecialchars( $name ), $query ) ) . "\n"; - } - } else { - $s .= Html::element( 'th', array(), $name ) . "\n"; - } - } - - $tableClass = $this->getTableClass(); - $ret = Html::openElement( 'table', array( - 'style' => 'border:1px;', - 'class' => "mw-datatable $tableClass" ) - ); - $ret .= Html::rawElement( 'thead', array(), Html::rawElement( 'tr', array(), "\n" . $s . "\n" ) ); - $ret .= Html::openElement( 'tbody' ) . "\n"; - - return $ret; - } - - /** - * @protected - * @return string - */ - function getEndBody() { - return "</tbody></table>\n"; - } - - /** - * @protected - * @return string - */ - function getEmptyBody() { - $colspan = count( $this->getFieldNames() ); - $msgEmpty = $this->msg( 'table_pager_empty' )->text(); - return Html::rawElement( 'tr', array(), - Html::element( 'td', array( 'colspan' => $colspan ), $msgEmpty ) ); - } - - /** - * @protected - * @param stdClass $row - * @return string HTML - */ - function formatRow( $row ) { - $this->mCurrentRow = $row; // In case formatValue etc need to know - $s = Html::openElement( 'tr', $this->getRowAttrs( $row ) ) . "\n"; - $fieldNames = $this->getFieldNames(); - - foreach ( $fieldNames as $field => $name ) { - $value = isset( $row->$field ) ? $row->$field : null; - $formatted = strval( $this->formatValue( $field, $value ) ); - - if ( $formatted == '' ) { - $formatted = ' '; - } - - $s .= Html::rawElement( 'td', $this->getCellAttrs( $field, $value ), $formatted ) . "\n"; - } - - $s .= Html::closeElement( 'tr' ) . "\n"; - - return $s; - } - - /** - * Get a class name to be applied to the given row. - * - * @protected - * - * @param object $row The database result row - * @return string - */ - function getRowClass( $row ) { - return ''; - } - - /** - * Get attributes to be applied to the given row. - * - * @protected - * - * @param object $row The database result row - * @return array Array of attribute => value - */ - function getRowAttrs( $row ) { - $class = $this->getRowClass( $row ); - if ( $class === '' ) { - // Return an empty array to avoid clutter in HTML like class="" - return array(); - } else { - return array( 'class' => $this->getRowClass( $row ) ); - } - } - - /** - * Get any extra attributes to be applied to the given cell. Don't - * take this as an excuse to hardcode styles; use classes and - * CSS instead. Row context is available in $this->mCurrentRow - * - * @protected - * - * @param string $field The column - * @param string $value The cell contents - * @return array Array of attr => value - */ - function getCellAttrs( $field, $value ) { - return array( 'class' => 'TablePager_col_' . $field ); - } - - /** - * @protected - * @return string - */ - function getIndexField() { - return $this->mSort; - } - - /** - * @protected - * @return string - */ - function getTableClass() { - return 'TablePager'; - } - - /** - * @protected - * @return string - */ - function getNavClass() { - return 'TablePager_nav'; - } - - /** - * @protected - * @return string - */ - function getSortHeaderClass() { - return 'TablePager_sort'; - } - - /** - * A navigation bar with images - * @return string HTML - */ - public function getNavigationBar() { - global $wgStylePath; - - if ( !$this->isNavigationBarShown() ) { - return ''; - } - - $path = "$wgStylePath/common/images"; - $labels = array( - 'first' => 'table_pager_first', - 'prev' => 'table_pager_prev', - 'next' => 'table_pager_next', - 'last' => 'table_pager_last', - ); - $images = array( - 'first' => 'arrow_first_25.png', - 'prev' => 'arrow_left_25.png', - 'next' => 'arrow_right_25.png', - 'last' => 'arrow_last_25.png', - ); - $disabledImages = array( - 'first' => 'arrow_disabled_first_25.png', - 'prev' => 'arrow_disabled_left_25.png', - 'next' => 'arrow_disabled_right_25.png', - 'last' => 'arrow_disabled_last_25.png', - ); - if ( $this->getLanguage()->isRTL() ) { - $keys = array_keys( $labels ); - $images = array_combine( $keys, array_reverse( $images ) ); - $disabledImages = array_combine( $keys, array_reverse( $disabledImages ) ); - } - - $linkTexts = array(); - $disabledTexts = array(); - foreach ( $labels as $type => $label ) { - $msgLabel = $this->msg( $label )->escaped(); - $linkTexts[$type] = Html::element( 'img', array( 'src' => "$path/{$images[$type]}", - 'alt' => $msgLabel ) ) . "<br />$msgLabel"; - $disabledTexts[$type] = Html::element( 'img', array( 'src' => "$path/{$disabledImages[$type]}", - 'alt' => $msgLabel ) ) . "<br />$msgLabel"; - } - $links = $this->getPagingLinks( $linkTexts, $disabledTexts ); - - $s = Html::openElement( 'table', array( 'class' => $this->getNavClass() ) ); - $s .= Html::openElement( 'tr' ) . "\n"; - $width = 100 / count( $links ) . '%'; - foreach ( $labels as $type => $label ) { - $s .= Html::rawElement( 'td', array( 'style' => "width:$width;" ), $links[$type] ) . "\n"; - } - $s .= Html::closeElement( 'tr' ) . Html::closeElement( 'table' ) . "\n"; - return $s; - } - - /** - * Get a "<select>" element which has options for each of the allowed limits - * - * @param string $attribs Extra attributes to set - * @return string HTML fragment - */ - public function getLimitSelect( $attribs = array() ) { - $select = new XmlSelect( 'limit', false, $this->mLimit ); - $select->addOptions( $this->getLimitSelectList() ); - foreach ( $attribs as $name => $value ) { - $select->setAttribute( $name, $value ); - } - return $select->getHTML(); - } - - /** - * Get a list of items to show in a "<select>" element of limits. - * This can be passed directly to XmlSelect::addOptions(). - * - * @since 1.22 - * @return array - */ - public function getLimitSelectList() { - # Add the current limit from the query string - # to avoid that the limit is lost after clicking Go next time - if ( !in_array( $this->mLimit, $this->mLimitsShown ) ) { - $this->mLimitsShown[] = $this->mLimit; - sort( $this->mLimitsShown ); - } - $ret = array(); - foreach ( $this->mLimitsShown as $key => $value ) { - # The pair is either $index => $limit, in which case the $value - # will be numeric, or $limit => $text, in which case the $value - # will be a string. - if ( is_int( $value ) ) { - $limit = $value; - $text = $this->getLanguage()->formatNum( $limit ); - } else { - $limit = $key; - $text = $value; - } - $ret[$text] = $limit; - } - return $ret; - } - - /** - * Get \<input type="hidden"\> elements for use in a method="get" form. - * Resubmits all defined elements of the query string, except for a - * blacklist, passed in the $blacklist parameter. - * - * @param array $blacklist Parameters from the request query which should not be resubmitted - * @return string HTML fragment - */ - function getHiddenFields( $blacklist = array() ) { - $blacklist = (array)$blacklist; - $query = $this->getRequest()->getQueryValues(); - foreach ( $blacklist as $name ) { - unset( $query[$name] ); - } - $s = ''; - foreach ( $query as $name => $value ) { - $s .= Html::hidden( $name, $value ) . "\n"; - } - return $s; - } - - /** - * Get a form containing a limit selection dropdown - * - * @return string HTML fragment - */ - function getLimitForm() { - global $wgScript; - - return Html::rawElement( - 'form', - array( - 'method' => 'get', - 'action' => $wgScript - ), - "\n" . $this->getLimitDropdown() - ) . "\n"; - } - - /** - * Gets a limit selection dropdown - * - * @return string - */ - function getLimitDropdown() { - # Make the select with some explanatory text - $msgSubmit = $this->msg( 'table_pager_limit_submit' )->escaped(); - - return $this->msg( 'table_pager_limit' ) - ->rawParams( $this->getLimitSelect() )->escaped() . - "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" . - $this->getHiddenFields( array( 'limit' ) ); - } - - /** - * Return true if the named field should be sortable by the UI, false - * otherwise - * - * @param string $field - */ - abstract function isFieldSortable( $field ); - - /** - * Format a table cell. The return value should be HTML, but use an empty - * string not   for empty cells. Do not include the <td> and </td>. - * - * The current result row is available as $this->mCurrentRow, in case you - * need more context. - * - * @protected - * - * @param string $name The database field name - * @param string $value The value retrieved from the database - */ - abstract function formatValue( $name, $value ); - - /** - * The database field name used as a default sort order. - * - * @protected - * - * @return string - */ - abstract function getDefaultSort(); - - /** - * An array mapping database field names to a textual description of the - * field name, for use in the table header. The description should be plain - * text, it will be HTML-escaped later. - * - * @return array - */ - abstract function getFieldNames(); -} diff --git a/includes/Preferences.php b/includes/Preferences.php index 084d6ab2e5..eb29e41508 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -579,12 +579,16 @@ class Preferences { ## Skin ##################################### global $wgAllowUserCss, $wgAllowUserJs; - $defaultPreferences['skin'] = array( - 'type' => 'radio', - 'options' => self::generateSkinOptions( $user, $context ), - 'label' => ' ', - 'section' => 'rendering/skin', - ); + // Skin selector, if there is at least one valid skin + $skinOptions = self::generateSkinOptions( $user, $context ); + if ( $skinOptions ) { + $defaultPreferences['skin'] = array( + 'type' => 'radio', + 'options' => $skinOptions, + 'label' => ' ', + 'section' => 'rendering/skin', + ); + } # Create links to user CSS/JS pages for all skins # This code is basically copied from generateSkinOptions(). It'd @@ -1064,12 +1068,14 @@ class Preferences { } asort( $validSkinNames ); + $foundDefault = false; foreach ( $validSkinNames as $skinkey => $sn ) { $linkTools = array(); # Mark the default skin if ( $skinkey == $wgDefaultSkin ) { $linkTools[] = $context->msg( 'default' )->escaped(); + $foundDefault = true; } # Create preview link @@ -1094,6 +1100,12 @@ class Preferences { $ret[$display] = $skinkey; } + if ( !$foundDefault ) { + // If the default skin is not available, things are going to break horribly because the + // default value for skin selector will not be a valid value. Let's just not show it then. + return array(); + } + return $ret; } diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index 35be2a9d87..295183c62a 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -174,6 +174,9 @@ abstract class PrefixSearch { if ( $subpageSearch !== null ) { // Try matching the full search string as a page name $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey ); + if ( !$specialTitle ) { + return array(); + } $special = SpecialPageFactory::getPage( $specialTitle->getText() ); if ( $special ) { $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit ); diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index 853e2cc412..4aa65d9403 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -57,16 +57,21 @@ class ProtectionForm { /** @var array Map of action to the expiry time of the existing protection */ protected $mExistingExpiry = array(); - function __construct( Page $article ) { - global $wgUser; + /** @var IContextSource */ + private $mContext; + + function __construct( Article $article ) { // Set instance variables. $this->mArticle = $article; $this->mTitle = $article->getTitle(); $this->mApplicableTypes = $this->mTitle->getRestrictionTypes(); + $this->mContext = $article->getContext(); // Check if the form should be disabled. // If it is, the form will be available in read-only to show levels. - $this->mPermErrors = $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser ); + $this->mPermErrors = $this->mTitle->getUserPermissionsErrors( + 'protect', $this->mContext->getUser() + ); if ( wfReadOnly() ) { $this->mPermErrors[] = array( 'readonlytext', wfReadOnlyReason() ); } @@ -82,14 +87,15 @@ class ProtectionForm { * Loads the current state of protection into the object. */ function loadData() { - global $wgRequest, $wgUser; - - $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(), $wgUser ); + $levels = MWNamespace::getRestrictionLevels( + $this->mTitle->getNamespace(), $this->mContext->getUser() + ); $this->mCascade = $this->mTitle->areRestrictionsCascading(); - $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); - $this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' ); - $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade', $this->mCascade ); + $request = $this->mContext->getRequest(); + $this->mReason = $request->getText( 'mwProtect-reason' ); + $this->mReasonSelection = $request->getText( 'wpProtectReasonSelection' ); + $this->mCascade = $request->getBool( 'mwProtect-cascade', $this->mCascade ); foreach ( $this->mApplicableTypes as $action ) { // @todo FIXME: This form currently requires individual selections, @@ -106,8 +112,8 @@ class ProtectionForm { } $this->mExistingExpiry[$action] = $existingExpiry; - $requestExpiry = $wgRequest->getText( "mwProtect-expiry-$action" ); - $requestExpirySelection = $wgRequest->getVal( "wpProtectExpirySelection-$action" ); + $requestExpiry = $request->getText( "mwProtect-expiry-$action" ); + $requestExpirySelection = $request->getVal( "wpProtectExpirySelection-$action" ); if ( $requestExpiry ) { // Custom expiry takes precedence @@ -128,7 +134,7 @@ class ProtectionForm { $this->mExpirySelection[$action] = 'infinite'; } - $val = $wgRequest->getVal( "mwProtect-level-$action" ); + $val = $request->getVal( "mwProtect-level-$action" ); if ( isset( $val ) && in_array( $val, $levels ) ) { $this->mRestrictions[$action] = $val; } @@ -170,16 +176,14 @@ class ProtectionForm { * Main entry point for action=protect and action=unprotect */ function execute() { - global $wgRequest, $wgOut; - if ( MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) === array( '' ) ) { throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' ); } - if ( $wgRequest->wasPosted() ) { + if ( $this->mContext->getRequest()->wasPosted() ) { if ( $this->save() ) { $q = $this->mArticle->isRedirect() ? 'redirect=no' : ''; - $wgOut->redirect( $this->mTitle->getFullURL( $q ) ); + $this->mContext->getOutput()->redirect( $this->mTitle->getFullURL( $q ) ); } } else { $this->show(); @@ -192,28 +196,27 @@ class ProtectionForm { * @param string $err Error message or null if there's no error */ function show( $err = null ) { - global $wgOut; - - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->addBacklinkSubtitle( $this->mTitle ); + $out = $this->mContext->getOutput(); + $out->setRobotPolicy( 'noindex,nofollow' ); + $out->addBacklinkSubtitle( $this->mTitle ); if ( is_array( $err ) ) { - $wgOut->wrapWikiMsg( "<p class='error'>\n$1\n</p>\n", $err ); + $out->wrapWikiMsg( "<p class='error'>\n$1\n</p>\n", $err ); } elseif ( is_string( $err ) ) { - $wgOut->addHTML( "<p class='error'>{$err}</p>\n" ); + $out->addHTML( "<p class='error'>{$err}</p>\n" ); } if ( $this->mTitle->getRestrictionTypes() === array() ) { // No restriction types available for the current title // this might happen if an extension alters the available types - $wgOut->setPageTitle( wfMessage( + $out->setPageTitle( wfMessage( 'protect-norestrictiontypes-title', $this->mTitle->getPrefixedText() ) ); - $wgOut->addWikiText( wfMessage( 'protect-norestrictiontypes-text' )->text() ); + $out->addWikiText( wfMessage( 'protect-norestrictiontypes-text' )->text() ); // Show the log in case protection was possible once - $this->showLogExtract( $wgOut ); + $this->showLogExtract( $out ); // return as there isn't anything else we can do return; } @@ -227,7 +230,7 @@ class ProtectionForm { } /** @todo FIXME: i18n issue, should use formatted number. */ - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>", array( 'protect-cascadeon', count( $cascadeSources ) ) ); @@ -236,19 +239,19 @@ class ProtectionForm { # Show an appropriate message if the user isn't allowed or able to change # the protection settings at this time if ( $this->disabled ) { - $wgOut->setPageTitle( + $out->setPageTitle( wfMessage( 'protect-title-notallowed', $this->mTitle->getPrefixedText() ) ); - $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $this->mPermErrors, 'protect' ) ); + $out->addWikiText( $out->formatPermissionsErrorMessage( $this->mPermErrors, 'protect' ) ); } else { - $wgOut->setPageTitle( wfMessage( 'protect-title', $this->mTitle->getPrefixedText() ) ); - $wgOut->addWikiMsg( 'protect-text', + $out->setPageTitle( wfMessage( 'protect-title', $this->mTitle->getPrefixedText() ) ); + $out->addWikiMsg( 'protect-text', wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ); } - $wgOut->addHTML( $this->buildForm() ); - $this->showLogExtract( $wgOut ); + $out->addHTML( $this->buildForm() ); + $this->showLogExtract( $out ); } /** @@ -257,16 +260,17 @@ class ProtectionForm { * @return bool Success */ function save() { - global $wgRequest, $wgUser, $wgOut; - # Permission check! if ( $this->disabled ) { $this->show(); return false; } - $token = $wgRequest->getVal( 'wpEditToken' ); - if ( !$wgUser->matchEditToken( $token, array( 'protect', $this->mTitle->getPrefixedDBkey() ) ) ) { + $request = $this->mContext->getRequest(); + $user = $this->mContext->getUser(); + $out = $this->mContext->getOutput(); + $token = $request->getVal( 'wpEditToken' ); + if ( !$user->matchEditToken( $token, array( 'protect', $this->mTitle->getPrefixedDBkey() ) ) ) { $this->show( array( 'sessionfailure' ) ); return false; } @@ -295,18 +299,18 @@ class ProtectionForm { } } - $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); + $this->mCascade = $request->getBool( 'mwProtect-cascade' ); $status = $this->mArticle->doUpdateRestrictions( $this->mRestrictions, $expiry, $this->mCascade, $reasonstr, - $wgUser + $user ); if ( !$status->isOK() ) { - $this->show( $wgOut->parseInline( $status->getWikiText() ) ); + $this->show( $out->parseInline( $status->getWikiText() ) ); return false; } @@ -327,7 +331,7 @@ class ProtectionForm { return false; } - WatchAction::doWatchOrUnwatch( $wgRequest->getCheck( 'mwProtectWatch' ), $this->mTitle, $wgUser ); + WatchAction::doWatchOrUnwatch( $request->getCheck( 'mwProtectWatch' ), $this->mTitle, $user ); return true; } @@ -338,12 +342,14 @@ class ProtectionForm { * @return string HTML form */ function buildForm() { - global $wgUser, $wgLang, $wgOut, $wgCascadingRestrictionLevels; - + $user = $this->mContext->getUser(); + $output = $this->mContext->getOutput(); + $lang = $this->mContext->getLanguage(); + $cascadingRestrictionLevels = $this->mContext->getConfig()->get( 'CascadingRestrictionLevels' ); $out = ''; if ( !$this->disabled ) { - $wgOut->addModules( 'mediawiki.legacy.protect' ); - $wgOut->addJsConfigVars( 'wgCascadeableLevels', $wgCascadingRestrictionLevels ); + $output->addModules( 'mediawiki.legacy.protect' ); + $output->addJsConfigVars( 'wgCascadeableLevels', $cascadingRestrictionLevels ); $out .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=protect' ), 'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) ); @@ -379,9 +385,9 @@ class ProtectionForm { $expiryFormOptions = ''; if ( $this->mExistingExpiry[$action] && $this->mExistingExpiry[$action] != 'infinity' ) { - $timestamp = $wgLang->timeanddate( $this->mExistingExpiry[$action], true ); - $d = $wgLang->date( $this->mExistingExpiry[$action], true ); - $t = $wgLang->time( $this->mExistingExpiry[$action], true ); + $timestamp = $lang->timeanddate( $this->mExistingExpiry[$action], true ); + $d = $lang->date( $this->mExistingExpiry[$action], true ); + $t = $lang->time( $this->mExistingExpiry[$action], true ); $expiryFormOptions .= Xml::option( wfMessage( 'protect-existing-expiry', $timestamp, $d, $t )->text(), @@ -508,14 +514,14 @@ class ProtectionForm { "</td> </tr>"; # Disallow watching is user is not logged in - if ( $wgUser->isLoggedIn() ) { + if ( $user->isLoggedIn() ) { $out .= " <tr> <td></td> <td class='mw-input'>" . Xml::checkLabel( wfMessage( 'watchthis' )->text(), 'mwProtectWatch', 'mwProtectWatch', - $wgUser->isWatched( $this->mTitle ) || $wgUser->getOption( 'watchdefault' ) ) . + $user->isWatched( $this->mTitle ) || $user->getOption( 'watchdefault' ) ) . "</td> </tr>"; } @@ -533,7 +539,7 @@ class ProtectionForm { } $out .= Xml::closeElement( 'fieldset' ); - if ( $wgUser->isAllowed( 'editinterface' ) ) { + if ( $user->isAllowed( 'editinterface' ) ) { $title = Title::makeTitle( NS_MEDIAWIKI, 'Protect-dropdown' ); $link = Linker::link( $title, @@ -547,10 +553,10 @@ class ProtectionForm { if ( !$this->disabled ) { $out .= Html::hidden( 'wpEditToken', - $wgUser->getEditToken( array( 'protect', $this->mTitle->getPrefixedDBkey() ) ) + $user->getEditToken( array( 'protect', $this->mTitle->getPrefixedDBkey() ) ) ); $out .= Xml::closeElement( 'form' ); - $wgOut->addScript( $this->buildCleanupScript() ); + $output->addScript( $this->buildCleanupScript() ); } return $out; @@ -564,12 +570,10 @@ class ProtectionForm { * @return string HTML fragment */ function buildSelector( $action, $selected ) { - global $wgUser; - // If the form is disabled, display all relevant levels. Otherwise, // just show the ones this user can use. $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(), - $this->disabled ? null : $wgUser + $this->disabled ? null : $this->mContext->getUser() ); $id = 'mwProtect-level-' . $action; diff --git a/includes/Revision.php b/includes/Revision.php index a6148c7abf..28a825d051 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -1654,9 +1654,13 @@ class Revision implements IDBAccessObject { * self::DELETED_COMMENT = File::DELETED_COMMENT, * self::DELETED_USER = File::DELETED_USER * @param User|null $user User object to check, or null to use $wgUser + * @param Title|null $title A Title object to check for per-page restrictions on, + * instead of just plain userrights * @return bool */ - public static function userCanBitfield( $bitfield, $field, User $user = null ) { + public static function userCanBitfield( $bitfield, $field, User $user = null, + Title $title = null + ) { if ( $bitfield & $field ) { // aspect is deleted if ( $user === null ) { global $wgUser; @@ -1670,8 +1674,19 @@ class Revision implements IDBAccessObject { $permissions = array( 'deletedhistory' ); } $permissionlist = implode( ', ', $permissions ); - wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" ); - return call_user_func_array( array( $user, 'isAllowedAny' ), $permissions ); + if ( $title === null ) { + wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" ); + return call_user_func_array( array( $user, 'isAllowedAny' ), $permissions ); + } else { + $text = $title->getPrefixedText(); + wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" ); + foreach ( $permissions as $perm ) { + if ( $title->userCan( $perm, $user ) ) { + return true; + } + } + return false; + } } else { return true; } diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index b173ae9738..ce70047ee5 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -328,6 +328,7 @@ class Sanitizer { * Regular expression to match HTML/XML attribute pairs within a tag. * Allows some... latitude. * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes + * @return string */ static function getAttribsRegex() { if ( self::$attribsRegex === null ) { @@ -1096,8 +1097,9 @@ class Sanitizer { global $wgExperimentalHtmlIds; $options = (array)$options; + $id = Sanitizer::decodeCharReferences( $id ); + if ( $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) { - $id = Sanitizer::decodeCharReferences( $id ); $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id ); $id = trim( $id, '_' ); if ( $id === '' ) { @@ -1114,7 +1116,7 @@ class Sanitizer { '%' => '.' ); - $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) ); + $id = urlencode( strtr( $id, ' ', '_' ) ); $id = str_replace( array_keys( $replace ), array_values( $replace ), $id ); if ( !preg_match( '/^[a-zA-Z]/', $id ) diff --git a/includes/Setup.php b/includes/Setup.php index 15fe94a75b..8b2138bd36 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -78,14 +78,6 @@ if ( $wgExtensionAssetsPath === false ) { $wgExtensionAssetsPath = "$wgScriptPath/extensions"; } -// Enable default skins. Temporary, to be removed before 1.24 release. -// This is hacky and bad, the require_once calls should eventually be generated by the installer -// and placed in LocalSettings.php. -// While this is in Setup.php, it needs to be done as soon as possible, as some of the setup code -// depends on all extensions and skins being already required (bug 67318). -require_once "$wgStyleDirectory/MonoBook/MonoBook.php"; -require_once "$wgStyleDirectory/Vector/Vector.php"; - if ( $wgLogo === false ) { $wgLogo = "$wgStylePath/common/images/wiki.png"; } @@ -271,6 +263,23 @@ if ( $wgSkipSkin ) { $wgSkipSkins[] = $wgSkipSkin; } +// Register skins +// Use a closure to avoid leaking into global state +call_user_func( function () use ( $wgValidSkinNames ) { + $factory = SkinFactory::getDefaultInstance(); + foreach ( $wgValidSkinNames as $name => $skin ) { + $factory->register( $name, $skin, function () use ( $name, $skin ) { + $class = "Skin$skin"; + return new $class( $name ); + } ); + } + // Register a hidden "fallback" skin + $factory->register( 'fallback', 'Fallback', function () { + return new SkinFallback; + } ); +} ); +$wgSkipSkins[] = 'fallback'; + if ( $wgLocalInterwiki ) { array_unshift( $wgLocalInterwikis, $wgLocalInterwiki ); } @@ -417,13 +426,14 @@ if ( $wgCookieSecure === 'detect' ) { $wgCookieSecure = ( WebRequest::detectProtocol() === 'https' ); } -if ( $wgRC2UDPAddress ) { - $wgRCFeeds['default'] = array( - 'formatter' => 'IRCColourfulRCFeedFormatter', - 'uri' => "udp://$wgRC2UDPAddress:$wgRC2UDPPort/$wgRC2UDPPrefix", - 'add_interwiki_prefix' => &$wgRC2UDPInterwikiPrefix, - 'omit_bots' => &$wgRC2UDPOmitBots, - ); +// Back compatibility for $wgRateLimitLog deprecated with 1.23 +if ( $wgRateLimitLog && !array_key_exists( 'ratelimit', $wgDebugLogGroups ) ) { + $wgDebugLogGroups['ratelimit'] = $wgRateLimitLog; +} + +if ( $wgProfileOnly ) { + $wgDebugLogGroups['profileoutput'] = $wgDebugLogFile; + $wgDebugLogFile = ''; } wfProfileOut( $fname . '-defaults' ); @@ -490,33 +500,6 @@ if ( $wgTmpDirectory === false ) { wfProfileOut( $fname . '-tempDir' ); } -// $wgHTCPMulticastRouting got renamed to $wgHTCPRouting in MediaWiki 1.22 -// ensure back compatibility. -if ( !$wgHTCPRouting && $wgHTCPMulticastRouting ) { - $wgHTCPRouting = $wgHTCPMulticastRouting; -} - -// Initialize $wgHTCPRouting from backwards-compatible settings that -// comes from pre 1.20 version. -if ( !$wgHTCPRouting && $wgHTCPMulticastAddress ) { - $wgHTCPRouting = array( - '' => array( - 'host' => $wgHTCPMulticastAddress, - 'port' => $wgHTCPPort, - ) - ); -} - -// Back compatibility for $wgRateLimitLog deprecated with 1.23 -if ( $wgRateLimitLog && !array_key_exists( 'ratelimit', $wgDebugLogGroups ) ) { - $wgDebugLogGroups['ratelimit'] = $wgRateLimitLog; -} - -if ( $wgProfileOnly ) { - $wgDebugLogGroups['profileoutput'] = $wgDebugLogFile; - $wgDebugLogFile = ''; -} - wfProfileOut( $fname . '-defaults2' ); wfProfileIn( $fname . '-misc1' ); @@ -579,15 +562,15 @@ wfRunHooks( 'SetupAfterCache' ); wfProfileIn( $fname . '-session' ); -// If session.auto_start is there, we can't touch session name -if ( !wfIniGetBool( 'session.auto_start' ) ) { - session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' ); -} +if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) { + // If session.auto_start is there, we can't touch session name + if ( !wfIniGetBool( 'session.auto_start' ) ) { + session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' ); + } -if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode && - ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix . 'Token'] ) ) -) { - wfSetupSession(); + if ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix . 'Token'] ) ) { + wfSetupSession(); + } } wfProfileOut( $fname . '-session' ); @@ -633,6 +616,10 @@ if ( !is_object( $wgAuth ) ) { */ $wgTitle = null; +/** + * @deprecated since 1.24 Use DeferredUpdates::addUpdate instead + * @var array + */ $wgDeferredUpdateList = array(); wfProfileOut( $fname . '-globals' ); diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index b877544099..8c1f26b82a 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -532,7 +532,7 @@ class SiteConfiguration { if ( isset( $this->cfgCache[$wiki] ) ) { $res = array_intersect_key( $this->cfgCache[$wiki], array_flip( $settings ) ); if ( count( $res ) == count( $settings ) ) { - return $res; // cache hit + return $multi ? $res : current( $res ); // cache hit } } elseif ( !in_array( $wiki, $this->wikis ) ) { throw new MWException( "No such wiki '$wiki'." ); diff --git a/includes/Skin.php b/includes/Skin.php deleted file mode 100644 index a59d5678a3..0000000000 --- a/includes/Skin.php +++ /dev/null @@ -1,1686 +0,0 @@ -<?php -/** - * Base class for all skins. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * @defgroup Skins Skins - */ - -/** - * The main skin class which provides methods and properties for all other skins. - * - * See docs/skin.txt for more information. - * - * @ingroup Skins - */ -abstract class Skin extends ContextSource { - protected $skinname = null; - protected $mRelevantTitle = null; - protected $mRelevantUser = null; - - /** - * @var string Stylesheets set to use. Subdirectory in skins/ where various stylesheets are - * located. Only needs to be set if you intend to use the getSkinStylePath() method. - */ - public $stylename = null; - - /** - * Fetch the set of available skins. - * @return array Associative array of strings - */ - static function getSkinNames() { - global $wgValidSkinNames; - static $skinsInitialised = false; - - if ( !$skinsInitialised || !count( $wgValidSkinNames ) ) { - # Get a list of available skins - # Build using the regular expression '^(.*).php$' - # Array keys are all lower case, array value keep the case used by filename - # - wfProfileIn( __METHOD__ . '-init' ); - - global $wgStyleDirectory; - - $skinDir = dir( $wgStyleDirectory ); - - if ( $skinDir !== false && $skinDir !== null ) { - # while code from www.php.net - while ( false !== ( $file = $skinDir->read() ) ) { - // Skip non-PHP files, hidden files, and '.dep' includes - $matches = array(); - - if ( preg_match( '/^([^.]*)\.php$/', $file, $matches ) ) { - $aSkin = $matches[1]; - - // Explicitly disallow loading core skins via the autodiscovery mechanism. - // - // They should be loaded already (in a non-autodicovery way), but old files might still - // exist on the server because our MW version upgrade process is widely documented as - // requiring just copying over all files, without removing old ones. - // - // This is one of the reasons we should have never used autodiscovery in the first - // place. This hack can be safely removed when autodiscovery is gone. - if ( in_array( $aSkin, array( 'CologneBlue', 'Modern', 'MonoBook', 'Vector' ) ) ) { - wfLogWarning( - "An old copy of the $aSkin skin was found in your skins/ directory. " . - "You should remove it to avoid problems in the future." . - "See https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery for details." - ); - continue; - } - - wfLogWarning( - "A skin using autodiscovery mechanism, $aSkin, was found in your skins/ directory. " . - "The mechanism will be removed in MediaWiki 1.25 and the skin will no longer be recognized. " . - "See https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery for information how to fix this." - ); - $wgValidSkinNames[strtolower( $aSkin )] = $aSkin; - } - } - $skinDir->close(); - } - $skinsInitialised = true; - wfProfileOut( __METHOD__ . '-init' ); - } - return $wgValidSkinNames; - } - - /** - * Fetch the skinname messages for available skins. - * @return string[] - */ - static function getSkinNameMessages() { - $messages = array(); - foreach ( self::getSkinNames() as $skinKey => $skinName ) { - $messages[] = "skinname-$skinKey"; - } - return $messages; - } - - /** - * Fetch the list of user-selectable skins in regards to $wgSkipSkins. - * Useful for Special:Preferences and other places where you - * only want to show skins users _can_ use. - * @return string[] - * @since 1.23 - */ - public static function getAllowedSkins() { - global $wgSkipSkins; - - $allowedSkins = self::getSkinNames(); - - foreach ( $wgSkipSkins as $skip ) { - unset( $allowedSkins[$skip] ); - } - - return $allowedSkins; - } - - /** - * @deprecated since 1.23, use getAllowedSkins - * @return string[] - */ - public static function getUsableSkins() { - wfDeprecated( __METHOD__, '1.23' ); - return self::getAllowedSkins(); - } - - /** - * Normalize a skin preference value to a form that can be loaded. - * - * If a skin can't be found, it will fall back to the configured default ($wgDefaultSkin), or the - * hardcoded default ($wgFallbackSkin) if the default skin is unavailable too. - * - * @param string $key 'monobook', 'vector', etc. - * @return string - */ - static function normalizeKey( $key ) { - global $wgDefaultSkin, $wgFallbackSkin; - - $skinNames = Skin::getSkinNames(); - - // Make keys lowercase for case-insensitive matching. - $skinNames = array_change_key_case( $skinNames, CASE_LOWER ); - $key = strtolower( $key ); - $defaultSkin = strtolower( $wgDefaultSkin ); - $fallbackSkin = strtolower( $wgFallbackSkin ); - - if ( $key == '' || $key == 'default' ) { - // Don't return the default immediately; - // in a misconfiguration we need to fall back. - $key = $defaultSkin; - } - - if ( isset( $skinNames[$key] ) ) { - return $key; - } - - // Older versions of the software used a numeric setting - // in the user preferences. - $fallback = array( - 0 => $defaultSkin, - 2 => 'cologneblue' - ); - - if ( isset( $fallback[$key] ) ) { - $key = $fallback[$key]; - } - - if ( isset( $skinNames[$key] ) ) { - return $key; - } elseif ( isset( $skinNames[$defaultSkin] ) ) { - return $defaultSkin; - } else { - return $fallbackSkin; - } - } - - /** - * Factory method for loading a skin of a given type - * @param string $key 'monobook', 'vector', etc. - * @return Skin - */ - static function &newFromKey( $key ) { - global $wgStyleDirectory, $wgFallbackSkin; - - $key = Skin::normalizeKey( $key ); - - $skinNames = Skin::getSkinNames(); - $skinName = $skinNames[$key]; - $className = "Skin{$skinName}"; - - # Grab the skin class and initialise it. - if ( !class_exists( $className ) ) { - - require_once "{$wgStyleDirectory}/{$skinName}.php"; - - # Check if we got if not fallback to default skin - if ( !class_exists( $className ) ) { - # DO NOT die if the class isn't found. This breaks maintenance - # scripts and can cause a user account to be unrecoverable - # except by SQL manipulation if a previously valid skin name - # is no longer valid. - wfDebug( "Skin class does not exist: $className\n" ); - - $fallback = $skinNames[Skin::normalizeKey( $wgFallbackSkin )]; - $className = "Skin{$fallback}"; - } - } - $skin = new $className( $key ); - return $skin; - } - - /** - * @return string Skin name - */ - public function getSkinName() { - return $this->skinname; - } - - /** - * @param OutputPage $out - */ - function initPage( OutputPage $out ) { - wfProfileIn( __METHOD__ ); - - $this->preloadExistence(); - - wfProfileOut( __METHOD__ ); - } - - /** - * Defines the ResourceLoader modules that should be added to the skin - * It is recommended that skins wishing to override call parent::getDefaultModules() - * and substitute out any modules they wish to change by using a key to look them up - * @return array Array of modules with helper keys for easy overriding - */ - public function getDefaultModules() { - global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax, - $wgAjaxWatch, $wgEnableAPI, $wgEnableWriteAPI; - - $out = $this->getOutput(); - $user = $out->getUser(); - $modules = array( - // modules that enhance the page content in some way - 'content' => array( - 'mediawiki.page.ready', - ), - // modules that exist for legacy reasons - 'legacy' => array(), - // modules relating to search functionality - 'search' => array(), - // modules relating to functionality relating to watching an article - 'watch' => array(), - // modules which relate to the current users preferences - 'user' => array(), - ); - if ( $wgIncludeLegacyJavaScript ) { - $modules['legacy'][] = 'mediawiki.legacy.wikibits'; - } - - if ( $wgPreloadJavaScriptMwUtil ) { - $modules['legacy'][] = 'mediawiki.util'; - } - - // Add various resources if required - if ( $wgUseAjax ) { - $modules['legacy'][] = 'mediawiki.legacy.ajax'; - - if ( $wgEnableAPI ) { - if ( $wgEnableWriteAPI && $wgAjaxWatch && $user->isLoggedIn() - && $user->isAllowed( 'writeapi' ) - ) { - $modules['watch'][] = 'mediawiki.page.watch.ajax'; - } - - $modules['search'][] = 'mediawiki.searchSuggest'; - } - } - - if ( $user->getBoolOption( 'editsectiononrightclick' ) ) { - $modules['user'][] = 'mediawiki.action.view.rightClickEdit'; - } - - // Crazy edit-on-double-click stuff - if ( $out->isArticle() && $user->getOption( 'editondblclick' ) ) { - $modules['user'][] = 'mediawiki.action.view.dblClickEdit'; - } - return $modules; - } - - /** - * Preload the existence of three commonly-requested pages in a single query - */ - function preloadExistence() { - $user = $this->getUser(); - - // User/talk link - $titles = array( $user->getUserPage(), $user->getTalkPage() ); - - // Other tab link - if ( $this->getTitle()->isSpecialPage() ) { - // nothing - } elseif ( $this->getTitle()->isTalkPage() ) { - $titles[] = $this->getTitle()->getSubjectPage(); - } else { - $titles[] = $this->getTitle()->getTalkPage(); - } - - $lb = new LinkBatch( $titles ); - $lb->setCaller( __METHOD__ ); - $lb->execute(); - } - - /** - * Get the current revision ID - * - * @return int - */ - public function getRevisionId() { - return $this->getOutput()->getRevisionId(); - } - - /** - * Whether the revision displayed is the latest revision of the page - * - * @return bool - */ - public function isRevisionCurrent() { - $revID = $this->getRevisionId(); - return $revID == 0 || $revID == $this->getTitle()->getLatestRevID(); - } - - /** - * Set the "relevant" title - * @see self::getRelevantTitle() - * @param Title $t - */ - public function setRelevantTitle( $t ) { - $this->mRelevantTitle = $t; - } - - /** - * Return the "relevant" title. - * A "relevant" title is not necessarily the actual title of the page. - * Special pages like Special:MovePage use set the page they are acting on - * as their "relevant" title, this allows the skin system to display things - * such as content tabs which belong to to that page instead of displaying - * a basic special page tab which has almost no meaning. - * - * @return Title - */ - public function getRelevantTitle() { - if ( isset( $this->mRelevantTitle ) ) { - return $this->mRelevantTitle; - } - return $this->getTitle(); - } - - /** - * Set the "relevant" user - * @see self::getRelevantUser() - * @param User $u - */ - public function setRelevantUser( $u ) { - $this->mRelevantUser = $u; - } - - /** - * Return the "relevant" user. - * A "relevant" user is similar to a relevant title. Special pages like - * Special:Contributions mark the user which they are relevant to so that - * things like the toolbox can display the information they usually are only - * able to display on a user's userpage and talkpage. - * @return User - */ - public function getRelevantUser() { - if ( isset( $this->mRelevantUser ) ) { - return $this->mRelevantUser; - } - $title = $this->getRelevantTitle(); - if ( $title->hasSubjectNamespace( NS_USER ) ) { - $rootUser = $title->getRootText(); - if ( User::isIP( $rootUser ) ) { - $this->mRelevantUser = User::newFromName( $rootUser, false ); - } else { - $user = User::newFromName( $rootUser, false ); - if ( $user && $user->isLoggedIn() ) { - $this->mRelevantUser = $user; - } - } - return $this->mRelevantUser; - } - return null; - } - - /** - * Outputs the HTML generated by other functions. - * @param OutputPage $out - */ - abstract function outputPage( OutputPage $out = null ); - - /** - * @param array $data - * @return string - */ - static function makeVariablesScript( $data ) { - if ( $data ) { - return Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( ResourceLoader::makeConfigSetScript( $data ) ) - ); - } else { - return ''; - } - } - - /** - * Get the query to generate a dynamic stylesheet - * - * @return array - */ - public static function getDynamicStylesheetQuery() { - global $wgSquidMaxage; - - return array( - 'action' => 'raw', - 'maxage' => $wgSquidMaxage, - 'usemsgcache' => 'yes', - 'ctype' => 'text/css', - 'smaxage' => $wgSquidMaxage, - ); - } - - /** - * Add skin specific stylesheets - * Calling this method with an $out of anything but the same OutputPage - * inside ->getOutput() is deprecated. The $out arg is kept - * for compatibility purposes with skins. - * @param OutputPage $out - * @todo delete - */ - abstract function setupSkinUserCss( OutputPage $out ); - - /** - * TODO: document - * @param Title $title - * @return string - */ - function getPageClasses( $title ) { - $numeric = 'ns-' . $title->getNamespace(); - - if ( $title->isSpecialPage() ) { - $type = 'ns-special'; - // bug 23315: provide a class based on the canonical special page name without subpages - list( $canonicalName ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); - if ( $canonicalName ) { - $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" ); - } else { - $type .= ' mw-invalidspecialpage'; - } - } elseif ( $title->isTalkPage() ) { - $type = 'ns-talk'; - } else { - $type = 'ns-subject'; - } - - $name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() ); - - return "$numeric $type $name"; - } - - /* - * Return values for <html> element - * @return array of associative name-to-value elements for <html> element - */ - public function getHtmlElementAttributes() { - $lang = $this->getLanguage(); - return array( - 'lang' => $lang->getHtmlCode(), - 'dir' => $lang->getDir(), - 'class' => 'client-nojs', - ); - } - - /** - * This will be called by OutputPage::headElement when it is creating the - * "<body>" tag, skins can override it if they have a need to add in any - * body attributes or classes of their own. - * @param OutputPage $out - * @param array $bodyAttrs - */ - function addToBodyAttributes( $out, &$bodyAttrs ) { - // does nothing by default - } - - /** - * URL to the logo - * @return string - */ - function getLogo() { - global $wgLogo; - return $wgLogo; - } - - /** - * @return string - */ - function getCategoryLinks() { - global $wgUseCategoryBrowser; - - $out = $this->getOutput(); - $allCats = $out->getCategoryLinks(); - - if ( !count( $allCats ) ) { - return ''; - } - - $embed = "<li>"; - $pop = "</li>"; - - $s = ''; - $colon = $this->msg( 'colon-separator' )->escaped(); - - if ( !empty( $allCats['normal'] ) ) { - $t = $embed . implode( "{$pop}{$embed}", $allCats['normal'] ) . $pop; - - $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped(); - $linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text(); - $s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' . - Linker::link( Title::newFromText( $linkPage ), $msg ) - . $colon . '<ul>' . $t . '</ul>' . '</div>'; - } - - # Hidden categories - if ( isset( $allCats['hidden'] ) ) { - if ( $this->getUser()->getBoolOption( 'showhiddencats' ) ) { - $class = ' mw-hidden-cats-user-shown'; - } elseif ( $this->getTitle()->getNamespace() == NS_CATEGORY ) { - $class = ' mw-hidden-cats-ns-shown'; - } else { - $class = ' mw-hidden-cats-hidden'; - } - - $s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" . - $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() . - $colon . '<ul>' . $embed . implode( "{$pop}{$embed}", $allCats['hidden'] ) . $pop . '</ul>' . - '</div>'; - } - - # optional 'dmoz-like' category browser. Will be shown under the list - # of categories an article belong to - if ( $wgUseCategoryBrowser ) { - $s .= '<br /><hr />'; - - # get a big array of the parents tree - $parenttree = $this->getTitle()->getParentCategoryTree(); - # Skin object passed by reference cause it can not be - # accessed under the method subfunction drawCategoryBrowser - $tempout = explode( "\n", $this->drawCategoryBrowser( $parenttree ) ); - # Clean out bogus first entry and sort them - unset( $tempout[0] ); - asort( $tempout ); - # Output one per line - $s .= implode( "<br />\n", $tempout ); - } - - return $s; - } - - /** - * Render the array as a series of links. - * @param array $tree Categories tree returned by Title::getParentCategoryTree - * @return string Separated by >, terminate with "\n" - */ - function drawCategoryBrowser( $tree ) { - $return = ''; - - foreach ( $tree as $element => $parent ) { - if ( empty( $parent ) ) { - # element start a new list - $return .= "\n"; - } else { - # grab the others elements - $return .= $this->drawCategoryBrowser( $parent ) . ' > '; - } - - # add our current element to the list - $eltitle = Title::newFromText( $element ); - $return .= Linker::link( $eltitle, htmlspecialchars( $eltitle->getText() ) ); - } - - return $return; - } - - /** - * @return string - */ - function getCategories() { - $out = $this->getOutput(); - - $catlinks = $this->getCategoryLinks(); - - $classes = 'catlinks'; - - // Check what we're showing - $allCats = $out->getCategoryLinks(); - $showHidden = $this->getUser()->getBoolOption( 'showhiddencats' ) || - $this->getTitle()->getNamespace() == NS_CATEGORY; - - if ( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) { - $classes .= ' catlinks-allhidden'; - } - - return "<div id='catlinks' class='$classes'>{$catlinks}</div>"; - } - - /** - * This runs a hook to allow extensions placing their stuff after content - * and article metadata (e.g. categories). - * Note: This function has nothing to do with afterContent(). - * - * This hook is placed here in order to allow using the same hook for all - * skins, both the SkinTemplate based ones and the older ones, which directly - * use this class to get their data. - * - * The output of this function gets processed in SkinTemplate::outputPage() for - * the SkinTemplate based skins, all other skins should directly echo it. - * - * @return string Empty by default, if not changed by any hook function. - */ - protected function afterContentHook() { - $data = ''; - - if ( wfRunHooks( 'SkinAfterContent', array( &$data, $this ) ) ) { - // adding just some spaces shouldn't toggle the output - // of the whole <div/>, so we use trim() here - if ( trim( $data ) != '' ) { - // Doing this here instead of in the skins to - // ensure that the div has the same ID in all - // skins - $data = "<div id='mw-data-after-content'>\n" . - "\t$data\n" . - "</div>\n"; - } - } else { - wfDebug( "Hook SkinAfterContent changed output processing.\n" ); - } - - return $data; - } - - /** - * Generate debug data HTML for displaying at the bottom of the main content - * area. - * @return string HTML containing debug data, if enabled (otherwise empty). - */ - protected function generateDebugHTML() { - return MWDebug::getHTMLDebugLog(); - } - - /** - * This gets called shortly before the "</body>" tag. - * - * @return string HTML-wrapped JS code to be put before "</body>" - */ - function bottomScripts() { - // TODO and the suckage continues. This function is really just a wrapper around - // OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned - // up at some point - $bottomScriptText = $this->getOutput()->getBottomScripts(); - wfRunHooks( 'SkinAfterBottomScripts', array( $this, &$bottomScriptText ) ); - - return $bottomScriptText; - } - - /** - * Text with the permalink to the source page, - * usually shown on the footer of a printed page - * - * @return string HTML text with an URL - */ - function printSource() { - $oldid = $this->getRevisionId(); - if ( $oldid ) { - $canonicalUrl = $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid ); - $url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) ); - } else { - // oldid not available for non existing pages - $url = htmlspecialchars( wfExpandIRI( $this->getTitle()->getCanonicalURL() ) ); - } - - return $this->msg( 'retrievedfrom', '<a dir="ltr" href="' . $url - . '">' . $url . '</a>' )->text(); - } - - /** - * @return string - */ - function getUndeleteLink() { - $action = $this->getRequest()->getVal( 'action', 'view' ); - - if ( $this->getUser()->isAllowed( 'deletedhistory' ) && - ( $this->getTitle()->getArticleID() == 0 || $action == 'history' ) ) { - $n = $this->getTitle()->isDeleted(); - - if ( $n ) { - if ( $this->getUser()->isAllowed( 'undelete' ) ) { - $msg = 'thisisdeleted'; - } else { - $msg = 'viewdeleted'; - } - - return $this->msg( $msg )->rawParams( - Linker::linkKnown( - SpecialPage::getTitleFor( 'Undelete', $this->getTitle()->getPrefixedDBkey() ), - $this->msg( 'restorelink' )->numParams( $n )->escaped() ) - )->text(); - } - } - - return ''; - } - - /** - * @return string - */ - function subPageSubtitle() { - $out = $this->getOutput(); - $subpages = ''; - - if ( !wfRunHooks( 'SkinSubPageSubtitle', array( &$subpages, $this, $out ) ) ) { - return $subpages; - } - - if ( $out->isArticle() && MWNamespace::hasSubpages( $out->getTitle()->getNamespace() ) ) { - $ptext = $this->getTitle()->getPrefixedText(); - if ( preg_match( '/\//', $ptext ) ) { - $links = explode( '/', $ptext ); - array_pop( $links ); - $c = 0; - $growinglink = ''; - $display = ''; - $lang = $this->getLanguage(); - - foreach ( $links as $link ) { - $growinglink .= $link; - $display .= $link; - $linkObj = Title::newFromText( $growinglink ); - - if ( is_object( $linkObj ) && $linkObj->isKnown() ) { - $getlink = Linker::linkKnown( - $linkObj, - htmlspecialchars( $display ) - ); - - $c++; - - if ( $c > 1 ) { - $subpages .= $lang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped(); - } else { - $subpages .= '< '; - } - - $subpages .= $getlink; - $display = ''; - } else { - $display .= '/'; - } - $growinglink .= '/'; - } - } - } - - return $subpages; - } - - /** - * Returns true if the IP should be shown in the header - * @return bool - */ - function showIPinHeader() { - global $wgShowIPinHeader; - return $wgShowIPinHeader && session_id() != ''; - } - - /** - * @return string - */ - function getSearchLink() { - $searchPage = SpecialPage::getTitleFor( 'Search' ); - return $searchPage->getLocalURL(); - } - - /** - * @return string - */ - function escapeSearchLink() { - return htmlspecialchars( $this->getSearchLink() ); - } - - /** - * @param string $type - * @return string - */ - function getCopyright( $type = 'detect' ) { - global $wgRightsPage, $wgRightsUrl, $wgRightsText; - - if ( $type == 'detect' ) { - if ( !$this->isRevisionCurrent() - && !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled() - ) { - $type = 'history'; - } else { - $type = 'normal'; - } - } - - if ( $type == 'history' ) { - $msg = 'history_copyright'; - } else { - $msg = 'copyright'; - } - - if ( $wgRightsPage ) { - $title = Title::newFromText( $wgRightsPage ); - $link = Linker::linkKnown( $title, $wgRightsText ); - } elseif ( $wgRightsUrl ) { - $link = Linker::makeExternalLink( $wgRightsUrl, $wgRightsText ); - } elseif ( $wgRightsText ) { - $link = $wgRightsText; - } else { - # Give up now - return ''; - } - - // Allow for site and per-namespace customization of copyright notice. - // @todo Remove deprecated $forContent param from hook handlers and then remove here. - $forContent = true; - - wfRunHooks( - 'SkinCopyrightFooter', - array( $this->getTitle(), $type, &$msg, &$link, &$forContent ) - ); - - return $this->msg( $msg )->rawParams( $link )->text(); - } - - /** - * @return null|string - */ - function getCopyrightIcon() { - global $wgRightsUrl, $wgRightsText, $wgRightsIcon, $wgCopyrightIcon; - - $out = ''; - - if ( $wgCopyrightIcon ) { - $out = $wgCopyrightIcon; - } elseif ( $wgRightsIcon ) { - $icon = htmlspecialchars( $wgRightsIcon ); - - if ( $wgRightsUrl ) { - $url = htmlspecialchars( $wgRightsUrl ); - $out .= '<a href="' . $url . '">'; - } - - $text = htmlspecialchars( $wgRightsText ); - $out .= "<img src=\"$icon\" alt=\"$text\" width=\"88\" height=\"31\" />"; - - if ( $wgRightsUrl ) { - $out .= '</a>'; - } - } - - return $out; - } - - /** - * Gets the powered by MediaWiki icon. - * @return string - */ - function getPoweredBy() { - global $wgStylePath; - - $url = htmlspecialchars( "$wgStylePath/common/images/poweredby_mediawiki_88x31.png" ); - $text = '<a href="//www.mediawiki.org/"><img src="' . $url - . '" height="31" width="88" alt="Powered by MediaWiki" /></a>'; - wfRunHooks( 'SkinGetPoweredBy', array( &$text, $this ) ); - return $text; - } - - /** - * Get the timestamp of the latest revision, formatted in user language - * - * @return string - */ - protected function lastModified() { - $timestamp = $this->getOutput()->getRevisionTimestamp(); - - # No cached timestamp, load it from the database - if ( $timestamp === null ) { - $timestamp = Revision::getTimestampFromId( $this->getTitle(), $this->getRevisionId() ); - } - - if ( $timestamp ) { - $d = $this->getLanguage()->userDate( $timestamp, $this->getUser() ); - $t = $this->getLanguage()->userTime( $timestamp, $this->getUser() ); - $s = ' ' . $this->msg( 'lastmodifiedat', $d, $t )->text(); - } else { - $s = ''; - } - - if ( wfGetLB()->getLaggedSlaveMode() ) { - $s .= ' <strong>' . $this->msg( 'laggedslavemode' )->text() . '</strong>'; - } - - return $s; - } - - /** - * @param string $align - * @return string - */ - function logoText( $align = '' ) { - if ( $align != '' ) { - $a = " style='float: {$align};'"; - } else { - $a = ''; - } - - $mp = $this->msg( 'mainpage' )->escaped(); - $mptitle = Title::newMainPage(); - $url = ( is_object( $mptitle ) ? htmlspecialchars( $mptitle->getLocalURL() ) : '' ); - - $logourl = $this->getLogo(); - $s = "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>"; - - return $s; - } - - /** - * Renders a $wgFooterIcons icon according to the method's arguments - * @param array $icon The icon to build the html for, see $wgFooterIcons - * for the format of this array. - * @param bool|string $withImage Whether to use the icon's image or output - * a text-only footericon. - * @return string HTML - */ - function makeFooterIcon( $icon, $withImage = 'withImage' ) { - if ( is_string( $icon ) ) { - $html = $icon; - } else { // Assuming array - $url = isset( $icon["url"] ) ? $icon["url"] : null; - unset( $icon["url"] ); - if ( isset( $icon["src"] ) && $withImage === 'withImage' ) { - // do this the lazy way, just pass icon data as an attribute array - $html = Html::element( 'img', $icon ); - } else { - $html = htmlspecialchars( $icon["alt"] ); - } - if ( $url ) { - $html = Html::rawElement( 'a', array( "href" => $url ), $html ); - } - } - return $html; - } - - /** - * Gets the link to the wiki's main page. - * @return string - */ - function mainPageLink() { - $s = Linker::linkKnown( - Title::newMainPage(), - $this->msg( 'mainpage' )->escaped() - ); - - return $s; - } - - /** - * Returns an HTML link for use in the footer - * @param string $desc The i18n message key for the link text - * @param string $page The i18n message key for the page to link to - * @return string HTML anchor - */ - public function footerLink( $desc, $page ) { - // if the link description has been set to "-" in the default language, - if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) { - // then it is disabled, for all languages. - return ''; - } else { - // Otherwise, we display the link for the user, described in their - // language (which may or may not be the same as the default language), - // but we make the link target be the one site-wide page. - $title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() ); - - return Linker::linkKnown( - $title, - $this->msg( $desc )->escaped() - ); - } - } - - /** - * Gets the link to the wiki's privacy policy page. - * @return string HTML - */ - function privacyLink() { - return $this->footerLink( 'privacy', 'privacypage' ); - } - - /** - * Gets the link to the wiki's about page. - * @return string HTML - */ - function aboutLink() { - return $this->footerLink( 'aboutsite', 'aboutpage' ); - } - - /** - * Gets the link to the wiki's general disclaimers page. - * @return string HTML - */ - function disclaimerLink() { - return $this->footerLink( 'disclaimers', 'disclaimerpage' ); - } - - /** - * Return URL options for the 'edit page' link. - * This may include an 'oldid' specifier, if the current page view is such. - * - * @return array - * @private - */ - function editUrlOptions() { - $options = array( 'action' => 'edit' ); - - if ( !$this->isRevisionCurrent() ) { - $options['oldid'] = intval( $this->getRevisionId() ); - } - - return $options; - } - - /** - * @param User|int $id - * @return bool - */ - function showEmailUser( $id ) { - if ( $id instanceof User ) { - $targetUser = $id; - } else { - $targetUser = User::newFromId( $id ); - } - - # The sending user must have a confirmed email address and the target - # user must have a confirmed email address and allow emails from users. - return $this->getUser()->canSendEmail() && - $targetUser->canReceiveEmail(); - } - - /** - * Return a fully resolved style path url to images or styles stored in the common folder. - * This method returns a url resolved using the configured skin style path - * and includes the style version inside of the url. - * @param string $name The name or path of a skin resource file - * @return string The fully resolved style path url including styleversion - */ - function getCommonStylePath( $name ) { - global $wgStylePath, $wgStyleVersion; - return "$wgStylePath/common/$name?$wgStyleVersion"; - } - - /** - * Return a fully resolved style path url to images or styles stored in the current skins's folder. - * This method returns a url resolved using the configured skin style path - * and includes the style version inside of the url. - * - * Requires $stylename to be set, otherwise throws MWException. - * - * @param string $name The name or path of a skin resource file - * @return string The fully resolved style path url including styleversion - */ - function getSkinStylePath( $name ) { - global $wgStylePath, $wgStyleVersion; - - if ( $this->stylename === null ) { - $class = get_class( $this ); - throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" ); - } - - return "$wgStylePath/{$this->stylename}/$name?$wgStyleVersion"; - } - - /* these are used extensively in SkinTemplate, but also some other places */ - - /** - * @param string $urlaction - * @return string - */ - static function makeMainPageUrl( $urlaction = '' ) { - $title = Title::newMainPage(); - self::checkTitle( $title, '' ); - - return $title->getLocalURL( $urlaction ); - } - - /** - * Make a URL for a Special Page using the given query and protocol. - * - * If $proto is set to null, make a local URL. Otherwise, make a full - * URL with the protocol specified. - * - * @param string $name Name of the Special page - * @param string $urlaction Query to append - * @param string|null $proto Protocol to use or null for a local URL - * @return string - */ - static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) { - $title = SpecialPage::getSafeTitleFor( $name ); - if ( is_null( $proto ) ) { - return $title->getLocalURL( $urlaction ); - } else { - return $title->getFullURL( $urlaction, false, $proto ); - } - } - - /** - * @param string $name - * @param string $subpage - * @param string $urlaction - * @return string - */ - static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) { - $title = SpecialPage::getSafeTitleFor( $name, $subpage ); - return $title->getLocalURL( $urlaction ); - } - - /** - * @param string $name - * @param string $urlaction - * @return string - */ - static function makeI18nUrl( $name, $urlaction = '' ) { - $title = Title::newFromText( wfMessage( $name )->inContentLanguage()->text() ); - self::checkTitle( $title, $name ); - return $title->getLocalURL( $urlaction ); - } - - /** - * @param string $name - * @param string $urlaction - * @return string - */ - static function makeUrl( $name, $urlaction = '' ) { - $title = Title::newFromText( $name ); - self::checkTitle( $title, $name ); - - return $title->getLocalURL( $urlaction ); - } - - /** - * If url string starts with http, consider as external URL, else - * internal - * @param string $name - * @return string URL - */ - static function makeInternalOrExternalUrl( $name ) { - if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) { - return $name; - } else { - return self::makeUrl( $name ); - } - } - - /** - * this can be passed the NS number as defined in Language.php - * @param string $name - * @param string $urlaction - * @param int $namespace - * @return string - */ - static function makeNSUrl( $name, $urlaction = '', $namespace = NS_MAIN ) { - $title = Title::makeTitleSafe( $namespace, $name ); - self::checkTitle( $title, $name ); - - return $title->getLocalURL( $urlaction ); - } - - /** - * these return an array with the 'href' and boolean 'exists' - * @param string $name - * @param string $urlaction - * @return array - */ - static function makeUrlDetails( $name, $urlaction = '' ) { - $title = Title::newFromText( $name ); - self::checkTitle( $title, $name ); - - return array( - 'href' => $title->getLocalURL( $urlaction ), - 'exists' => $title->getArticleID() != 0, - ); - } - - /** - * Make URL details where the article exists (or at least it's convenient to think so) - * @param string $name Article name - * @param string $urlaction - * @return array - */ - static function makeKnownUrlDetails( $name, $urlaction = '' ) { - $title = Title::newFromText( $name ); - self::checkTitle( $title, $name ); - - return array( - 'href' => $title->getLocalURL( $urlaction ), - 'exists' => true - ); - } - - /** - * make sure we have some title to operate on - * - * @param Title $title - * @param string $name - */ - static function checkTitle( &$title, $name ) { - if ( !is_object( $title ) ) { - $title = Title::newFromText( $name ); - if ( !is_object( $title ) ) { - $title = Title::newFromText( '--error: link target missing--' ); - } - } - } - - /** - * Build an array that represents the sidebar(s), the navigation bar among them. - * - * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins. - * - * The format of the returned array is array( heading => content, ... ), where: - * - heading is the heading of a navigation portlet. It is either: - * - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...) - * - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin - * - plain text, which should be HTML-escaped by the skin - * - content is the contents of the portlet. It is either: - * - HTML text (<ul><li>...</li>...</ul>) - * - array of link data in a format accepted by BaseTemplate::makeListItem() - * - (for a magic string as a key, any value) - * - * Note that extensions can control the sidebar contents using the SkinBuildSidebar hook - * and can technically insert anything in here; skin creators are expected to handle - * values described above. - * - * @return array - */ - function buildSidebar() { - global $wgMemc, $wgEnableSidebarCache, $wgSidebarCacheExpiry; - wfProfileIn( __METHOD__ ); - - $key = wfMemcKey( 'sidebar', $this->getLanguage()->getCode() ); - - if ( $wgEnableSidebarCache ) { - $cachedsidebar = $wgMemc->get( $key ); - if ( $cachedsidebar ) { - wfRunHooks( 'SidebarBeforeOutput', array( $this, &$cachedsidebar ) ); - - wfProfileOut( __METHOD__ ); - return $cachedsidebar; - } - } - - $bar = array(); - $this->addToSidebar( $bar, 'sidebar' ); - - wfRunHooks( 'SkinBuildSidebar', array( $this, &$bar ) ); - if ( $wgEnableSidebarCache ) { - $wgMemc->set( $key, $bar, $wgSidebarCacheExpiry ); - } - - wfRunHooks( 'SidebarBeforeOutput', array( $this, &$bar ) ); - - wfProfileOut( __METHOD__ ); - return $bar; - } - - /** - * Add content from a sidebar system message - * Currently only used for MediaWiki:Sidebar (but may be used by Extensions) - * - * This is just a wrapper around addToSidebarPlain() for backwards compatibility - * - * @param array $bar - * @param string $message - */ - function addToSidebar( &$bar, $message ) { - $this->addToSidebarPlain( $bar, wfMessage( $message )->inContentLanguage()->plain() ); - } - - /** - * Add content from plain text - * @since 1.17 - * @param array $bar - * @param string $text - * @return array - */ - function addToSidebarPlain( &$bar, $text ) { - $lines = explode( "\n", $text ); - - $heading = ''; - - foreach ( $lines as $line ) { - if ( strpos( $line, '*' ) !== 0 ) { - continue; - } - $line = rtrim( $line, "\r" ); // for Windows compat - - if ( strpos( $line, '**' ) !== 0 ) { - $heading = trim( $line, '* ' ); - if ( !array_key_exists( $heading, $bar ) ) { - $bar[$heading] = array(); - } - } else { - $line = trim( $line, '* ' ); - - if ( strpos( $line, '|' ) !== false ) { // sanity check - $line = MessageCache::singleton()->transform( $line, false, null, $this->getTitle() ); - $line = array_map( 'trim', explode( '|', $line, 2 ) ); - if ( count( $line ) !== 2 ) { - // Second sanity check, could be hit by people doing - // funky stuff with parserfuncs... (bug 33321) - continue; - } - - $extraAttribs = array(); - - $msgLink = $this->msg( $line[0] )->inContentLanguage(); - if ( $msgLink->exists() ) { - $link = $msgLink->text(); - if ( $link == '-' ) { - continue; - } - } else { - $link = $line[0]; - } - $msgText = $this->msg( $line[1] ); - if ( $msgText->exists() ) { - $text = $msgText->text(); - } else { - $text = $line[1]; - } - - if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) { - $href = $link; - - // Parser::getExternalLinkAttribs won't work here because of the Namespace things - global $wgNoFollowLinks, $wgNoFollowDomainExceptions; - if ( $wgNoFollowLinks && !wfMatchesDomainList( $href, $wgNoFollowDomainExceptions ) ) { - $extraAttribs['rel'] = 'nofollow'; - } - - global $wgExternalLinkTarget; - if ( $wgExternalLinkTarget ) { - $extraAttribs['target'] = $wgExternalLinkTarget; - } - } else { - $title = Title::newFromText( $link ); - - if ( $title ) { - $title = $title->fixSpecialName(); - $href = $title->getLinkURL(); - } else { - $href = 'INVALID-TITLE'; - } - } - - $bar[$heading][] = array_merge( array( - 'text' => $text, - 'href' => $href, - 'id' => 'n-' . Sanitizer::escapeId( strtr( $line[1], ' ', '-' ), 'noninitial' ), - 'active' => false - ), $extraAttribs ); - } else { - continue; - } - } - } - - return $bar; - } - - /** - * This function previously controlled whether the 'mediawiki.legacy.wikiprintable' module - * should be loaded by OutputPage. That module no longer exists and the return value of this - * method is ignored. - * - * If your skin doesn't provide its own print styles, the 'mediawiki.legacy.commonPrint' module - * can be used instead (SkinTemplate-based skins do it automatically). - * - * @deprecated since 1.22 - * @return bool - */ - public function commonPrintStylesheet() { - wfDeprecated( __METHOD__, '1.22' ); - return false; - } - - /** - * Gets new talk page messages for the current user and returns an - * appropriate alert message (or an empty string if there are no messages) - * @return string - */ - function getNewtalks() { - - $newMessagesAlert = ''; - $user = $this->getUser(); - $newtalks = $user->getNewMessageLinks(); - $out = $this->getOutput(); - - // Allow extensions to disable or modify the new messages alert - if ( !wfRunHooks( 'GetNewMessagesAlert', array( &$newMessagesAlert, $newtalks, $user, $out ) ) ) { - return ''; - } - if ( $newMessagesAlert ) { - return $newMessagesAlert; - } - - if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) { - $uTalkTitle = $user->getTalkPage(); - $lastSeenRev = isset( $newtalks[0]['rev'] ) ? $newtalks[0]['rev'] : null; - $nofAuthors = 0; - if ( $lastSeenRev !== null ) { - $plural = true; // Default if we have a last seen revision: if unknown, use plural - $latestRev = Revision::newFromTitle( $uTalkTitle, false, Revision::READ_NORMAL ); - if ( $latestRev !== null ) { - // Singular if only 1 unseen revision, plural if several unseen revisions. - $plural = $latestRev->getParentId() !== $lastSeenRev->getId(); - $nofAuthors = $uTalkTitle->countAuthorsBetween( - $lastSeenRev, $latestRev, 10, 'include_new' ); - } - } else { - // Singular if no revision -> diff link will show latest change only in any case - $plural = false; - } - $plural = $plural ? 999 : 1; - // 999 signifies "more than one revision". We don't know how many, and even if we did, - // the number of revisions or authors is not necessarily the same as the number of - // "messages". - $newMessagesLink = Linker::linkKnown( - $uTalkTitle, - $this->msg( 'newmessageslinkplural' )->params( $plural )->escaped(), - array(), - array( 'redirect' => 'no' ) - ); - - $newMessagesDiffLink = Linker::linkKnown( - $uTalkTitle, - $this->msg( 'newmessagesdifflinkplural' )->params( $plural )->escaped(), - array(), - $lastSeenRev !== null - ? array( 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ) - : array( 'diff' => 'cur' ) - ); - - if ( $nofAuthors >= 1 && $nofAuthors <= 10 ) { - $newMessagesAlert = $this->msg( - 'youhavenewmessagesfromusers', - $newMessagesLink, - $newMessagesDiffLink - )->numParams( $nofAuthors, $plural ); - } else { - // $nofAuthors === 11 signifies "11 or more" ("more than 10") - $newMessagesAlert = $this->msg( - $nofAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages', - $newMessagesLink, - $newMessagesDiffLink - )->numParams( $plural ); - } - $newMessagesAlert = $newMessagesAlert->text(); - # Disable Squid cache - $out->setSquidMaxage( 0 ); - } elseif ( count( $newtalks ) ) { - $sep = $this->msg( 'newtalkseparator' )->escaped(); - $msgs = array(); - - foreach ( $newtalks as $newtalk ) { - $msgs[] = Xml::element( - 'a', - array( 'href' => $newtalk['link'] ), $newtalk['wiki'] - ); - } - $parts = implode( $sep, $msgs ); - $newMessagesAlert = $this->msg( 'youhavenewmessagesmulti' )->rawParams( $parts )->escaped(); - $out->setSquidMaxage( 0 ); - } - - return $newMessagesAlert; - } - - /** - * Get a cached notice - * - * @param string $name Message name, or 'default' for $wgSiteNotice - * @return string HTML fragment - */ - private function getCachedNotice( $name ) { - global $wgRenderHashAppend, $parserMemc, $wgContLang; - - wfProfileIn( __METHOD__ ); - - $needParse = false; - - if ( $name === 'default' ) { - // special case - global $wgSiteNotice; - $notice = $wgSiteNotice; - if ( empty( $notice ) ) { - wfProfileOut( __METHOD__ ); - return false; - } - } else { - $msg = $this->msg( $name )->inContentLanguage(); - if ( $msg->isDisabled() ) { - wfProfileOut( __METHOD__ ); - return false; - } - $notice = $msg->plain(); - } - - // Use the extra hash appender to let eg SSL variants separately cache. - $key = wfMemcKey( $name . $wgRenderHashAppend ); - $cachedNotice = $parserMemc->get( $key ); - if ( is_array( $cachedNotice ) ) { - if ( md5( $notice ) == $cachedNotice['hash'] ) { - $notice = $cachedNotice['html']; - } else { - $needParse = true; - } - } else { - $needParse = true; - } - - if ( $needParse ) { - $parsed = $this->getOutput()->parse( $notice ); - $parserMemc->set( $key, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); - $notice = $parsed; - } - - $notice = Html::rawElement( 'div', array( 'id' => 'localNotice', - 'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ), $notice ); - wfProfileOut( __METHOD__ ); - return $notice; - } - - /** - * Get a notice based on page's namespace - * - * @return string HTML fragment - */ - function getNamespaceNotice() { - wfProfileIn( __METHOD__ ); - - $key = 'namespacenotice-' . $this->getTitle()->getNsText(); - $namespaceNotice = $this->getCachedNotice( $key ); - if ( $namespaceNotice && substr( $namespaceNotice, 0, 7 ) != '<p><' ) { - $namespaceNotice = '<div id="namespacebanner">' . $namespaceNotice . '</div>'; - } else { - $namespaceNotice = ''; - } - - wfProfileOut( __METHOD__ ); - return $namespaceNotice; - } - - /** - * Get the site notice - * - * @return string HTML fragment - */ - function getSiteNotice() { - wfProfileIn( __METHOD__ ); - $siteNotice = ''; - - if ( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice, $this ) ) ) { - if ( is_object( $this->getUser() ) && $this->getUser()->isLoggedIn() ) { - $siteNotice = $this->getCachedNotice( 'sitenotice' ); - } else { - $anonNotice = $this->getCachedNotice( 'anonnotice' ); - if ( !$anonNotice ) { - $siteNotice = $this->getCachedNotice( 'sitenotice' ); - } else { - $siteNotice = $anonNotice; - } - } - if ( !$siteNotice ) { - $siteNotice = $this->getCachedNotice( 'default' ); - } - } - - wfRunHooks( 'SiteNoticeAfter', array( &$siteNotice, $this ) ); - wfProfileOut( __METHOD__ ); - return $siteNotice; - } - - /** - * Create a section edit link. This supersedes editSectionLink() and - * editSectionLinkForOther(). - * - * @param Title $nt The title being linked to (may not be the same as - * the current page, if the section is included from a template) - * @param string $section The designation of the section being pointed to, - * to be included in the link, like "§ion=$section" - * @param string $tooltip The tooltip to use for the link: will be escaped - * and wrapped in the 'editsectionhint' message - * @param string $lang Language code - * @return string HTML to use for edit link - */ - public function doEditSectionLink( Title $nt, $section, $tooltip = null, $lang = false ) { - // HTML generated here should probably have userlangattributes - // added to it for LTR text on RTL pages - - $lang = wfGetLangObj( $lang ); - - $attribs = array(); - if ( !is_null( $tooltip ) ) { - # Bug 25462: undo double-escaping. - $tooltip = Sanitizer::decodeCharReferences( $tooltip ); - $attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip ) - ->inLanguage( $lang )->text(); - } - $link = Linker::link( $nt, wfMessage( 'editsection' )->inLanguage( $lang )->text(), - $attribs, - array( 'action' => 'edit', 'section' => $section ), - array( 'noclasses', 'known' ) - ); - - # Add the brackets and the span and run the hook. - $result = '<span class="mw-editsection">' - . '<span class="mw-editsection-bracket">[</span>' - . $link - . '<span class="mw-editsection-bracket">]</span>' - . '</span>'; - - wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result, $lang ) ); - return $result; - } - - /** - * Use PHP's magic __call handler to intercept legacy calls to the linker - * for backwards compatibility. - * - * @param string $fname Name of called method - * @param array $args Arguments to the method - * @throws MWException - * @return mixed - */ - function __call( $fname, $args ) { - $realFunction = array( 'Linker', $fname ); - if ( is_callable( $realFunction ) ) { - wfDeprecated( get_class( $this ) . '::' . $fname, '1.21' ); - return call_user_func_array( $realFunction, $args ); - } else { - $className = get_class( $this ); - throw new MWException( "Call to undefined method $className::$fname" ); - } - } - -} diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php deleted file mode 100644 index 52e72e87de..0000000000 --- a/includes/SkinTemplate.php +++ /dev/null @@ -1,2113 +0,0 @@ -<?php -/** - * Base class for template-based skins. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Wrapper object for MediaWiki's localization functions, - * to be passed to the template engine. - * - * @private - * @ingroup Skins - */ -class MediaWikiI18N { - private $context = array(); - - function set( $varName, $value ) { - $this->context[$varName] = $value; - } - - function translate( $value ) { - wfProfileIn( __METHOD__ ); - - // Hack for i18n:attributes in PHPTAL 1.0.0 dev version as of 2004-10-23 - $value = preg_replace( '/^string:/', '', $value ); - - $value = wfMessage( $value )->text(); - // interpolate variables - $m = array(); - while ( preg_match( '/\$([0-9]*?)/sm', $value, $m ) ) { - list( $src, $var ) = $m; - wfSuppressWarnings(); - $varValue = $this->context[$var]; - wfRestoreWarnings(); - $value = str_replace( $src, $varValue, $value ); - } - wfProfileOut( __METHOD__ ); - return $value; - } -} - -/** - * Template-filler skin base class - * Formerly generic PHPTal (http://phptal.sourceforge.net/) skin - * Based on Brion's smarty skin - * @copyright Copyright © Gabriel Wicke -- http://www.aulinx.de/ - * - * @todo Needs some serious refactoring into functions that correspond - * to the computations individual esi snippets need. Most importantly no body - * parsing for most of those of course. - * - * @ingroup Skins - */ -class SkinTemplate extends Skin { - /** - * @var string Name of our skin, it probably needs to be all lower case. - * Child classes should override the default. - */ - public $skinname = 'monobook'; - - /** - * @var string For QuickTemplate, the name of the subclass which will - * actually fill the template. Child classes should override the default. - */ - public $template = 'QuickTemplate'; - - /** - * Add specific styles for this skin - * - * @param OutputPage $out - */ - function setupSkinUserCss( OutputPage $out ) { - $out->addModuleStyles( array( - 'mediawiki.legacy.shared', - 'mediawiki.legacy.commonPrint', - 'mediawiki.ui.button' - ) ); - } - - /** - * Create the template engine object; we feed it a bunch of data - * and eventually it spits out some HTML. Should have interface - * roughly equivalent to PHPTAL 0.7. - * - * @param string $classname - * @param bool|string $repository Subdirectory where we keep template files - * @param bool|string $cache_dir - * @return QuickTemplate - * @private - */ - function setupTemplate( $classname, $repository = false, $cache_dir = false ) { - return new $classname(); - } - - /** - * Generates array of language links for the current page - * - * @return array - */ - public function getLanguages() { - global $wgHideInterlanguageLinks; - if ( $wgHideInterlanguageLinks ) { - return array(); - } - - $userLang = $this->getLanguage(); - $languageLinks = array(); - - foreach ( $this->getOutput()->getLanguageLinks() as $languageLinkText ) { - $languageLinkParts = explode( ':', $languageLinkText, 2 ); - $class = 'interlanguage-link interwiki-' . $languageLinkParts[0]; - unset( $languageLinkParts ); - - $languageLinkTitle = Title::newFromText( $languageLinkText ); - if ( $languageLinkTitle ) { - $ilInterwikiCode = $languageLinkTitle->getInterwiki(); - $ilLangName = Language::fetchLanguageName( $ilInterwikiCode ); - - if ( strval( $ilLangName ) === '' ) { - $ilDisplayTextMsg = wfMessage( "interlanguage-link-$ilInterwikiCode" ); - if ( !$ilDisplayTextMsg->isDisabled() ) { - // Use custom MW message for the display text - $ilLangName = $ilDisplayTextMsg->text(); - } else { - // Last resort: fallback to the language link target - $ilLangName = $languageLinkText; - } - } else { - // Use the language autonym as display text - $ilLangName = $this->formatLanguageName( $ilLangName ); - } - - // CLDR extension or similar is required to localize the language name; - // otherwise we'll end up with the autonym again. - $ilLangLocalName = Language::fetchLanguageName( - $ilInterwikiCode, - $userLang->getCode() - ); - - $languageLinkTitleText = $languageLinkTitle->getText(); - if ( $ilLangLocalName === '' ) { - $ilFriendlySiteName = wfMessage( "interlanguage-link-sitename-$ilInterwikiCode" ); - if ( !$ilFriendlySiteName->isDisabled() ) { - if ( $languageLinkTitleText === '' ) { - $ilTitle = wfMessage( - 'interlanguage-link-title-nonlangonly', - $ilFriendlySiteName->text() - )->text(); - } else { - $ilTitle = wfMessage( - 'interlanguage-link-title-nonlang', - $languageLinkTitleText, - $ilFriendlySiteName->text() - )->text(); - } - } else { - // we have nothing friendly to put in the title, so fall back to - // displaying the interlanguage link itself in the title text - // (similar to what is done in page content) - $ilTitle = $languageLinkTitle->getInterwiki() . - ":$languageLinkTitleText"; - } - } elseif ( $languageLinkTitleText === '' ) { - $ilTitle = wfMessage( - 'interlanguage-link-title-langonly', - $ilLangLocalName - )->text(); - } else { - $ilTitle = wfMessage( - 'interlanguage-link-title', - $languageLinkTitleText, - $ilLangLocalName - )->text(); - } - - $ilInterwikiCodeBCP47 = wfBCP47( $ilInterwikiCode ); - $languageLink = array( - 'href' => $languageLinkTitle->getFullURL(), - 'text' => $ilLangName, - 'title' => $ilTitle, - 'class' => $class, - 'lang' => $ilInterwikiCodeBCP47, - 'hreflang' => $ilInterwikiCodeBCP47, - ); - wfRunHooks( - 'SkinTemplateGetLanguageLink', - array( &$languageLink, $languageLinkTitle, $this->getTitle() ) - ); - $languageLinks[] = $languageLink; - } - } - - return $languageLinks; - } - - protected function setupTemplateForOutput() { - wfProfileIn( __METHOD__ ); - - $request = $this->getRequest(); - $user = $this->getUser(); - $title = $this->getTitle(); - - wfProfileIn( __METHOD__ . '-init' ); - $tpl = $this->setupTemplate( $this->template, 'skins' ); - wfProfileOut( __METHOD__ . '-init' ); - - wfProfileIn( __METHOD__ . '-stuff' ); - $this->thispage = $title->getPrefixedDBkey(); - $this->titletxt = $title->getPrefixedText(); - $this->userpage = $user->getUserPage()->getPrefixedText(); - $query = array(); - if ( !$request->wasPosted() ) { - $query = $request->getValues(); - unset( $query['title'] ); - unset( $query['returnto'] ); - unset( $query['returntoquery'] ); - } - $this->thisquery = wfArrayToCgi( $query ); - $this->loggedin = $user->isLoggedIn(); - $this->username = $user->getName(); - - if ( $this->loggedin || $this->showIPinHeader() ) { - $this->userpageUrlDetails = self::makeUrlDetails( $this->userpage ); - } else { - # This won't be used in the standard skins, but we define it to preserve the interface - # To save time, we check for existence - $this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage ); - } - - wfProfileOut( __METHOD__ . '-stuff' ); - - wfProfileOut( __METHOD__ ); - - return $tpl; - } - - /** - * initialize various variables and generate the template - * - * @param OutputPage $out - */ - function outputPage( OutputPage $out = null ) { - wfProfileIn( __METHOD__ ); - Profiler::instance()->setTemplated( true ); - - $oldContext = null; - if ( $out !== null ) { - // @todo Add wfDeprecated in 1.20 - $oldContext = $this->getContext(); - $this->setContext( $out->getContext() ); - } - - $out = $this->getOutput(); - - wfProfileIn( __METHOD__ . '-init' ); - $this->initPage( $out ); - wfProfileOut( __METHOD__ . '-init' ); - $tpl = $this->prepareQuickTemplate( $out ); - // execute template - wfProfileIn( __METHOD__ . '-execute' ); - $res = $tpl->execute(); - wfProfileOut( __METHOD__ . '-execute' ); - - // result may be an error - $this->printOrError( $res ); - - if ( $oldContext ) { - $this->setContext( $oldContext ); - } - - wfProfileOut( __METHOD__ ); - } - - /** - * initialize various variables and generate the template - * - * @since 1.23 - * @return QuickTemplate The template to be executed by outputPage - */ - protected function prepareQuickTemplate() { - global $wgContLang, $wgScript, $wgStylePath, $wgMimeType, $wgJsMimeType, - $wgDisableCounters, $wgSitename, $wgLogo, $wgMaxCredits, - $wgShowCreditsIfMax, $wgPageShowWatchingUsers, $wgArticlePath, - $wgScriptPath, $wgServer; - - wfProfileIn( __METHOD__ ); - - $title = $this->getTitle(); - $request = $this->getRequest(); - $out = $this->getOutput(); - $tpl = $this->setupTemplateForOutput(); - - wfProfileIn( __METHOD__ . '-stuff2' ); - $tpl->set( 'title', $out->getPageTitle() ); - $tpl->set( 'pagetitle', $out->getHTMLTitle() ); - $tpl->set( 'displaytitle', $out->mPageLinkTitle ); - - $tpl->setRef( 'thispage', $this->thispage ); - $tpl->setRef( 'titleprefixeddbkey', $this->thispage ); - $tpl->set( 'titletext', $title->getText() ); - $tpl->set( 'articleid', $title->getArticleID() ); - - $tpl->set( 'isarticle', $out->isArticle() ); - - $subpagestr = $this->subPageSubtitle(); - if ( $subpagestr !== '' ) { - $subpagestr = '<span class="subpages">' . $subpagestr . '</span>'; - } - $tpl->set( 'subtitle', $subpagestr . $out->getSubtitle() ); - - $undelete = $this->getUndeleteLink(); - if ( $undelete === '' ) { - $tpl->set( 'undelete', '' ); - } else { - $tpl->set( 'undelete', '<span class="subpages">' . $undelete . '</span>' ); - } - - $tpl->set( 'catlinks', $this->getCategories() ); - if ( $out->isSyndicated() ) { - $feeds = array(); - foreach ( $out->getSyndicationLinks() as $format => $link ) { - $feeds[$format] = array( - // Messages: feed-atom, feed-rss - 'text' => $this->msg( "feed-$format" )->text(), - 'href' => $link - ); - } - $tpl->setRef( 'feeds', $feeds ); - } else { - $tpl->set( 'feeds', false ); - } - - $tpl->setRef( 'mimetype', $wgMimeType ); - $tpl->setRef( 'jsmimetype', $wgJsMimeType ); - $tpl->set( 'charset', 'UTF-8' ); - $tpl->setRef( 'wgScript', $wgScript ); - $tpl->setRef( 'skinname', $this->skinname ); - $tpl->set( 'skinclass', get_class( $this ) ); - $tpl->setRef( 'skin', $this ); - $tpl->setRef( 'stylename', $this->stylename ); - $tpl->set( 'printable', $out->isPrintable() ); - $tpl->set( 'handheld', $request->getBool( 'handheld' ) ); - $tpl->setRef( 'loggedin', $this->loggedin ); - $tpl->set( 'notspecialpage', !$title->isSpecialPage() ); - $tpl->set( 'searchaction', $this->escapeSearchLink() ); - $tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey() ); - $tpl->set( 'search', trim( $request->getVal( 'search' ) ) ); - $tpl->setRef( 'stylepath', $wgStylePath ); - $tpl->setRef( 'articlepath', $wgArticlePath ); - $tpl->setRef( 'scriptpath', $wgScriptPath ); - $tpl->setRef( 'serverurl', $wgServer ); - $tpl->setRef( 'logopath', $wgLogo ); - $tpl->setRef( 'sitename', $wgSitename ); - - $userLang = $this->getLanguage(); - $userLangCode = $userLang->getHtmlCode(); - $userLangDir = $userLang->getDir(); - - $tpl->set( 'lang', $userLangCode ); - $tpl->set( 'dir', $userLangDir ); - $tpl->set( 'rtl', $userLang->isRTL() ); - - $tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' ); - $tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed - $tpl->set( 'username', $this->loggedin ? $this->username : null ); - $tpl->setRef( 'userpage', $this->userpage ); - $tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] ); - $tpl->set( 'userlang', $userLangCode ); - - // Users can have their language set differently than the - // content of the wiki. For these users, tell the web browser - // that interface elements are in a different language. - $tpl->set( 'userlangattributes', '' ); - $tpl->set( 'specialpageattributes', '' ); # obsolete - // Used by VectorBeta to insert HTML before content but after the - // heading for the page title. Defaults to empty string. - $tpl->set( 'prebodyhtml', '' ); - - if ( $userLangCode !== $wgContLang->getHtmlCode() || $userLangDir !== $wgContLang->getDir() ) { - $escUserlang = htmlspecialchars( $userLangCode ); - $escUserdir = htmlspecialchars( $userLangDir ); - // Attributes must be in double quotes because htmlspecialchars() doesn't - // escape single quotes - $attrs = " lang=\"$escUserlang\" dir=\"$escUserdir\""; - $tpl->set( 'userlangattributes', $attrs ); - } - - wfProfileOut( __METHOD__ . '-stuff2' ); - - wfProfileIn( __METHOD__ . '-stuff3' ); - $tpl->set( 'newtalk', $this->getNewtalks() ); - $tpl->set( 'logo', $this->logoText() ); - - $tpl->set( 'copyright', false ); - $tpl->set( 'viewcount', false ); - $tpl->set( 'lastmod', false ); - $tpl->set( 'credits', false ); - $tpl->set( 'numberofwatchingusers', false ); - if ( $out->isArticle() && $title->exists() ) { - if ( $this->isRevisionCurrent() ) { - if ( !$wgDisableCounters ) { - $viewcount = $this->getWikiPage()->getCount(); - if ( $viewcount ) { - $tpl->set( 'viewcount', $this->msg( 'viewcount' )->numParams( $viewcount )->parse() ); - } - } - - if ( $wgPageShowWatchingUsers ) { - $dbr = wfGetDB( DB_SLAVE ); - $num = $dbr->selectField( 'watchlist', 'COUNT(*)', - array( 'wl_title' => $title->getDBkey(), 'wl_namespace' => $title->getNamespace() ), - __METHOD__ - ); - if ( $num > 0 ) { - $tpl->set( 'numberofwatchingusers', - $this->msg( 'number_of_watching_users_pageview' )->numParams( $num )->parse() - ); - } - } - - if ( $wgMaxCredits != 0 ) { - $tpl->set( 'credits', Action::factory( 'credits', $this->getWikiPage(), - $this->getContext() )->getCredits( $wgMaxCredits, $wgShowCreditsIfMax ) ); - } else { - $tpl->set( 'lastmod', $this->lastModified() ); - } - } - $tpl->set( 'copyright', $this->getCopyright() ); - } - wfProfileOut( __METHOD__ . '-stuff3' ); - - wfProfileIn( __METHOD__ . '-stuff4' ); - $tpl->set( 'copyrightico', $this->getCopyrightIcon() ); - $tpl->set( 'poweredbyico', $this->getPoweredBy() ); - $tpl->set( 'disclaimer', $this->disclaimerLink() ); - $tpl->set( 'privacy', $this->privacyLink() ); - $tpl->set( 'about', $this->aboutLink() ); - - $tpl->set( 'footerlinks', array( - 'info' => array( - 'lastmod', - 'viewcount', - 'numberofwatchingusers', - 'credits', - 'copyright', - ), - 'places' => array( - 'privacy', - 'about', - 'disclaimer', - ), - ) ); - - global $wgFooterIcons; - $tpl->set( 'footericons', $wgFooterIcons ); - foreach ( $tpl->data['footericons'] as $footerIconsKey => &$footerIconsBlock ) { - if ( count( $footerIconsBlock ) > 0 ) { - foreach ( $footerIconsBlock as &$footerIcon ) { - if ( isset( $footerIcon['src'] ) ) { - if ( !isset( $footerIcon['width'] ) ) { - $footerIcon['width'] = 88; - } - if ( !isset( $footerIcon['height'] ) ) { - $footerIcon['height'] = 31; - } - } - } - } else { - unset( $tpl->data['footericons'][$footerIconsKey] ); - } - } - - $tpl->set( 'sitenotice', $this->getSiteNotice() ); - $tpl->set( 'bottomscripts', $this->bottomScripts() ); - $tpl->set( 'printfooter', $this->printSource() ); - - # An ID that includes the actual body text; without categories, contentSub, ... - $realBodyAttribs = array( 'id' => 'mw-content-text' ); - - # Add a mw-content-ltr/rtl class to be able to style based on text direction - # when the content is different from the UI language, i.e.: - # not for special pages or file pages AND only when viewing AND if the page exists - # (or is in MW namespace, because that has default content) - if ( !in_array( $title->getNamespace(), array( NS_SPECIAL, NS_FILE ) ) && - Action::getActionName( $this ) === 'view' && - ( $title->exists() || $title->getNamespace() == NS_MEDIAWIKI ) ) { - $pageLang = $title->getPageViewLanguage(); - $realBodyAttribs['lang'] = $pageLang->getHtmlCode(); - $realBodyAttribs['dir'] = $pageLang->getDir(); - $realBodyAttribs['class'] = 'mw-content-' . $pageLang->getDir(); - } - - $out->mBodytext = Html::rawElement( 'div', $realBodyAttribs, $out->mBodytext ); - $tpl->setRef( 'bodytext', $out->mBodytext ); - - $language_urls = $this->getLanguages(); - if ( count( $language_urls ) ) { - $tpl->setRef( 'language_urls', $language_urls ); - } else { - $tpl->set( 'language_urls', false ); - } - wfProfileOut( __METHOD__ . '-stuff4' ); - - wfProfileIn( __METHOD__ . '-stuff5' ); - # Personal toolbar - $tpl->set( 'personal_urls', $this->buildPersonalUrls() ); - $content_navigation = $this->buildContentNavigationUrls(); - $content_actions = $this->buildContentActionUrls( $content_navigation ); - $tpl->setRef( 'content_navigation', $content_navigation ); - $tpl->setRef( 'content_actions', $content_actions ); - - $tpl->set( 'sidebar', $this->buildSidebar() ); - $tpl->set( 'nav_urls', $this->buildNavUrls() ); - - // Set the head scripts near the end, in case the above actions resulted in added scripts - $tpl->set( 'headelement', $out->headElement( $this ) ); - - $tpl->set( 'debug', '' ); - $tpl->set( 'debughtml', $this->generateDebugHTML() ); - $tpl->set( 'reporttime', wfReportTime() ); - - // original version by hansm - if ( !wfRunHooks( 'SkinTemplateOutputPageBeforeExec', array( &$this, &$tpl ) ) ) { - wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" ); - } - - // Set the bodytext to another key so that skins can just output it on it's own - // and output printfooter and debughtml separately - $tpl->set( 'bodycontent', $tpl->data['bodytext'] ); - - // Append printfooter and debughtml onto bodytext so that skins that - // were already using bodytext before they were split out don't suddenly - // start not outputting information. - $tpl->data['bodytext'] .= Html::rawElement( - 'div', - array( 'class' => 'printfooter' ), - "\n{$tpl->data['printfooter']}" - ) . "\n"; - $tpl->data['bodytext'] .= $tpl->data['debughtml']; - - // allow extensions adding stuff after the page content. - // See Skin::afterContentHook() for further documentation. - $tpl->set( 'dataAfterContent', $this->afterContentHook() ); - wfProfileOut( __METHOD__ . '-stuff5' ); - - wfProfileOut( __METHOD__ ); - return $tpl; - } - - /** - * Get the HTML for the p-personal list - * @return string - */ - public function getPersonalToolsList() { - $tpl = $this->setupTemplateForOutput(); - $tpl->set( 'personal_urls', $this->buildPersonalUrls() ); - $html = ''; - foreach ( $tpl->getPersonalTools() as $key => $item ) { - $html .= $tpl->makeListItem( $key, $item ); - } - return $html; - } - - /** - * Format language name for use in sidebar interlanguage links list. - * By default it is capitalized. - * - * @param string $name Language name, e.g. "English" or "español" - * @return string - * @private - */ - function formatLanguageName( $name ) { - return $this->getLanguage()->ucfirst( $name ); - } - - /** - * Output the string, or print error message if it's - * an error object of the appropriate type. - * For the base class, assume strings all around. - * - * @param string $str - * @private - */ - function printOrError( $str ) { - echo $str; - } - - /** - * Output a boolean indicating if buildPersonalUrls should output separate - * login and create account links or output a combined link - * By default we simply return a global config setting that affects most skins - * This is setup as a method so that like with $wgLogo and getLogo() a skin - * can override this setting and always output one or the other if it has - * a reason it can't output one of the two modes. - * @return bool - */ - function useCombinedLoginLink() { - global $wgUseCombinedLoginLink; - return $wgUseCombinedLoginLink; - } - - /** - * build array of urls for personal toolbar - * @return array - */ - protected function buildPersonalUrls() { - $title = $this->getTitle(); - $request = $this->getRequest(); - $pageurl = $title->getLocalURL(); - wfProfileIn( __METHOD__ ); - - /* set up the default links for the personal toolbar */ - $personal_urls = array(); - - # Due to bug 32276, if a user does not have read permissions, - # $this->getTitle() will just give Special:Badtitle, which is - # not especially useful as a returnto parameter. Use the title - # from the request instead, if there was one. - if ( $this->getUser()->isAllowed( 'read' ) ) { - $page = $this->getTitle(); - } else { - $page = Title::newFromText( $request->getVal( 'title', '' ) ); - } - $page = $request->getVal( 'returnto', $page ); - $a = array(); - if ( strval( $page ) !== '' ) { - $a['returnto'] = $page; - $query = $request->getVal( 'returntoquery', $this->thisquery ); - if ( $query != '' ) { - $a['returntoquery'] = $query; - } - } - - $returnto = wfArrayToCgi( $a ); - if ( $this->loggedin ) { - $personal_urls['userpage'] = array( - 'text' => $this->username, - 'href' => &$this->userpageUrlDetails['href'], - 'class' => $this->userpageUrlDetails['exists'] ? false : 'new', - 'active' => ( $this->userpageUrlDetails['href'] == $pageurl ), - 'dir' => 'auto' - ); - $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage ); - $personal_urls['mytalk'] = array( - 'text' => $this->msg( 'mytalk' )->text(), - 'href' => &$usertalkUrlDetails['href'], - 'class' => $usertalkUrlDetails['exists'] ? false : 'new', - 'active' => ( $usertalkUrlDetails['href'] == $pageurl ) - ); - $href = self::makeSpecialUrl( 'Preferences' ); - $personal_urls['preferences'] = array( - 'text' => $this->msg( 'mypreferences' )->text(), - 'href' => $href, - 'active' => ( $href == $pageurl ) - ); - - if ( $this->getUser()->isAllowed( 'viewmywatchlist' ) ) { - $href = self::makeSpecialUrl( 'Watchlist' ); - $personal_urls['watchlist'] = array( - 'text' => $this->msg( 'mywatchlist' )->text(), - 'href' => $href, - 'active' => ( $href == $pageurl ) - ); - } - - # We need to do an explicit check for Special:Contributions, as we - # have to match both the title, and the target, which could come - # from request values (Special:Contributions?target=Jimbo_Wales) - # or be specified in "sub page" form - # (Special:Contributions/Jimbo_Wales). The plot - # thickens, because the Title object is altered for special pages, - # so it doesn't contain the original alias-with-subpage. - $origTitle = Title::newFromText( $request->getText( 'title' ) ); - if ( $origTitle instanceof Title && $origTitle->isSpecialPage() ) { - list( $spName, $spPar ) = SpecialPageFactory::resolveAlias( $origTitle->getText() ); - $active = $spName == 'Contributions' - && ( ( $spPar && $spPar == $this->username ) - || $request->getText( 'target' ) == $this->username ); - } else { - $active = false; - } - - $href = self::makeSpecialUrlSubpage( 'Contributions', $this->username ); - $personal_urls['mycontris'] = array( - 'text' => $this->msg( 'mycontris' )->text(), - 'href' => $href, - 'active' => $active - ); - $personal_urls['logout'] = array( - 'text' => $this->msg( 'pt-userlogout' )->text(), - 'href' => self::makeSpecialUrl( 'Userlogout', - // userlogout link must always contain an & character, otherwise we might not be able - // to detect a buggy precaching proxy (bug 17790) - $title->isSpecial( 'Preferences' ) ? 'noreturnto' : $returnto - ), - 'active' => false - ); - } else { - $useCombinedLoginLink = $this->useCombinedLoginLink(); - $loginlink = $this->getUser()->isAllowed( 'createaccount' ) && $useCombinedLoginLink - ? 'nav-login-createaccount' - : 'pt-login'; - $is_signup = $request->getText( 'type' ) == 'signup'; - - $login_url = array( - 'text' => $this->msg( $loginlink )->text(), - 'href' => self::makeSpecialUrl( 'Userlogin', $returnto ), - 'active' => $title->isSpecial( 'Userlogin' ) - && ( $loginlink == 'nav-login-createaccount' || !$is_signup ), - ); - $createaccount_url = array( - 'text' => $this->msg( 'pt-createaccount' )->text(), - 'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ), - 'active' => $title->isSpecial( 'Userlogin' ) && $is_signup, - ); - - if ( $this->showIPinHeader() ) { - $href = &$this->userpageUrlDetails['href']; - $personal_urls['anonuserpage'] = array( - 'text' => $this->username, - 'href' => $href, - 'class' => $this->userpageUrlDetails['exists'] ? false : 'new', - 'active' => ( $pageurl == $href ) - ); - $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage ); - $href = &$usertalkUrlDetails['href']; - $personal_urls['anontalk'] = array( - 'text' => $this->msg( 'anontalk' )->text(), - 'href' => $href, - 'class' => $usertalkUrlDetails['exists'] ? false : 'new', - 'active' => ( $pageurl == $href ) - ); - } - - if ( $this->getUser()->isAllowed( 'createaccount' ) && !$useCombinedLoginLink ) { - $personal_urls['createaccount'] = $createaccount_url; - } - - $personal_urls['login'] = $login_url; - } - - wfRunHooks( 'PersonalUrls', array( &$personal_urls, &$title, $this ) ); - wfProfileOut( __METHOD__ ); - return $personal_urls; - } - - /** - * Builds an array with tab definition - * - * @param Title $title Page Where the tab links to - * @param string|array $message Message key or an array of message keys (will fall back) - * @param bool $selected Display the tab as selected - * @param string $query Query string attached to tab URL - * @param bool $checkEdit Check if $title exists and mark with .new if one doesn't - * - * @return array - */ - function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) { - $classes = array(); - if ( $selected ) { - $classes[] = 'selected'; - } - if ( $checkEdit && !$title->isKnown() ) { - $classes[] = 'new'; - if ( $query !== '' ) { - $query = 'action=edit&redlink=1&' . $query; - } else { - $query = 'action=edit&redlink=1'; - } - } - - // wfMessageFallback will nicely accept $message as an array of fallbacks - // or just a single key - $msg = wfMessageFallback( $message )->setContext( $this->getContext() ); - if ( is_array( $message ) ) { - // for hook compatibility just keep the last message name - $message = end( $message ); - } - if ( $msg->exists() ) { - $text = $msg->text(); - } else { - global $wgContLang; - $text = $wgContLang->getFormattedNsText( - MWNamespace::getSubject( $title->getNamespace() ) ); - } - - $result = array(); - if ( !wfRunHooks( 'SkinTemplateTabAction', array( &$this, - $title, $message, $selected, $checkEdit, - &$classes, &$query, &$text, &$result ) ) ) { - return $result; - } - - return array( - 'class' => implode( ' ', $classes ), - 'text' => $text, - 'href' => $title->getLocalURL( $query ), - 'primary' => true ); - } - - function makeTalkUrlDetails( $name, $urlaction = '' ) { - $title = Title::newFromText( $name ); - if ( !is_object( $title ) ) { - throw new MWException( __METHOD__ . " given invalid pagename $name" ); - } - $title = $title->getTalkPage(); - self::checkTitle( $title, $name ); - return array( - 'href' => $title->getLocalURL( $urlaction ), - 'exists' => $title->getArticleID() != 0, - ); - } - - function makeArticleUrlDetails( $name, $urlaction = '' ) { - $title = Title::newFromText( $name ); - $title = $title->getSubjectPage(); - self::checkTitle( $title, $name ); - return array( - 'href' => $title->getLocalURL( $urlaction ), - 'exists' => $title->getArticleID() != 0, - ); - } - - /** - * a structured array of links usually used for the tabs in a skin - * - * There are 4 standard sections - * namespaces: Used for namespace tabs like special, page, and talk namespaces - * views: Used for primary page views like read, edit, history - * actions: Used for most extra page actions like deletion, protection, etc... - * variants: Used to list the language variants for the page - * - * Each section's value is a key/value array of links for that section. - * The links themselves have these common keys: - * - class: The css classes to apply to the tab - * - text: The text to display on the tab - * - href: The href for the tab to point to - * - rel: An optional rel= for the tab's link - * - redundant: If true the tab will be dropped in skins using content_actions - * this is useful for tabs like "Read" which only have meaning in skins that - * take special meaning from the grouped structure of content_navigation - * - * Views also have an extra key which can be used: - * - primary: If this is not true skins like vector may try to hide the tab - * when the user has limited space in their browser window - * - * content_navigation using code also expects these ids to be present on the - * links, however these are usually automatically generated by SkinTemplate - * itself and are not necessary when using a hook. The only things these may - * matter to are people modifying content_navigation after it's initial creation: - * - id: A "preferred" id, most skins are best off outputting this preferred - * id for best compatibility. - * - tooltiponly: This is set to true for some tabs in cases where the system - * believes that the accesskey should not be added to the tab. - * - * @return array - */ - protected function buildContentNavigationUrls() { - global $wgDisableLangConversion; - - wfProfileIn( __METHOD__ ); - - // Display tabs for the relevant title rather than always the title itself - $title = $this->getRelevantTitle(); - $onPage = $title->equals( $this->getTitle() ); - - $out = $this->getOutput(); - $request = $this->getRequest(); - $user = $this->getUser(); - - $content_navigation = array( - 'namespaces' => array(), - 'views' => array(), - 'actions' => array(), - 'variants' => array() - ); - - // parameters - $action = $request->getVal( 'action', 'view' ); - - $userCanRead = $title->quickUserCan( 'read', $user ); - - $preventActiveTabs = false; - wfRunHooks( 'SkinTemplatePreventOtherActiveTabs', array( &$this, &$preventActiveTabs ) ); - - // Checks if page is some kind of content - if ( $title->canExist() ) { - // Gets page objects for the related namespaces - $subjectPage = $title->getSubjectPage(); - $talkPage = $title->getTalkPage(); - - // Determines if this is a talk page - $isTalk = $title->isTalkPage(); - - // Generates XML IDs from namespace names - $subjectId = $title->getNamespaceKey( '' ); - - if ( $subjectId == 'main' ) { - $talkId = 'talk'; - } else { - $talkId = "{$subjectId}_talk"; - } - - $skname = $this->skinname; - - // Adds namespace links - $subjectMsg = array( "nstab-$subjectId" ); - if ( $subjectPage->isMainPage() ) { - array_unshift( $subjectMsg, 'mainpage-nstab' ); - } - $content_navigation['namespaces'][$subjectId] = $this->tabAction( - $subjectPage, $subjectMsg, !$isTalk && !$preventActiveTabs, '', $userCanRead - ); - $content_navigation['namespaces'][$subjectId]['context'] = 'subject'; - $content_navigation['namespaces'][$talkId] = $this->tabAction( - $talkPage, array( "nstab-$talkId", 'talk' ), $isTalk && !$preventActiveTabs, '', $userCanRead - ); - $content_navigation['namespaces'][$talkId]['context'] = 'talk'; - - if ( $userCanRead ) { - $isForeignFile = $title->inNamespace( NS_FILE ) && $this->canUseWikiPage() && - $this->getWikiPage() instanceof WikiFilePage && !$this->getWikiPage()->isLocal(); - - // Adds view view link - if ( $title->exists() || $isForeignFile ) { - $content_navigation['views']['view'] = $this->tabAction( - $isTalk ? $talkPage : $subjectPage, - array( "$skname-view-view", 'view' ), - ( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true - ); - // signal to hide this from simple content_actions - $content_navigation['views']['view']['redundant'] = true; - } - - // If it is a non-local file, show a link to the file in its own repository - if ( $isForeignFile ) { - $file = $this->getWikiPage()->getFile(); - $content_navigation['views']['view-foreign'] = array( - 'class' => '', - 'text' => wfMessageFallback( "$skname-view-foreign", 'view-foreign' )-> - setContext( $this->getContext() )-> - params( $file->getRepo()->getDisplayName() )->text(), - 'href' => $file->getDescriptionUrl(), - 'primary' => false, - ); - } - - wfProfileIn( __METHOD__ . '-edit' ); - - // Checks if user can edit the current page if it exists or create it otherwise - if ( $title->quickUserCan( 'edit', $user ) - && ( $title->exists() || $title->quickUserCan( 'create', $user ) ) - ) { - // Builds CSS class for talk page links - $isTalkClass = $isTalk ? ' istalk' : ''; - // Whether the user is editing the page - $isEditing = $onPage && ( $action == 'edit' || $action == 'submit' ); - // Whether to show the "Add a new section" tab - // Checks if this is a current rev of talk page and is not forced to be hidden - $showNewSection = !$out->forceHideNewSectionLink() - && ( ( $isTalk && $this->isRevisionCurrent() ) || $out->showNewSectionLink() ); - $section = $request->getVal( 'section' ); - - if ( $title->exists() - || ( $title->getNamespace() == NS_MEDIAWIKI - && $title->getDefaultMessageText() !== false - ) - ) { - $msgKey = $isForeignFile ? 'edit-local' : 'edit'; - } else { - $msgKey = $isForeignFile ? 'create-local' : 'create'; - } - $content_navigation['views']['edit'] = array( - 'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection ) - ? 'selected' - : '' - ) . $isTalkClass, - 'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey ) - ->setContext( $this->getContext() )->text(), - 'href' => $title->getLocalURL( $this->editUrlOptions() ), - 'primary' => !$isForeignFile, // don't collapse this in vector - ); - - // section link - if ( $showNewSection ) { - // Adds new section link - //$content_navigation['actions']['addsection'] - $content_navigation['views']['addsection'] = array( - 'class' => ( $isEditing && $section == 'new' ) ? 'selected' : false, - 'text' => wfMessageFallback( "$skname-action-addsection", 'addsection' ) - ->setContext( $this->getContext() )->text(), - 'href' => $title->getLocalURL( 'action=edit§ion=new' ) - ); - } - // Checks if the page has some kind of viewable content - } elseif ( $title->hasSourceText() ) { - // Adds view source view link - $content_navigation['views']['viewsource'] = array( - 'class' => ( $onPage && $action == 'edit' ) ? 'selected' : false, - 'text' => wfMessageFallback( "$skname-action-viewsource", 'viewsource' ) - ->setContext( $this->getContext() )->text(), - 'href' => $title->getLocalURL( $this->editUrlOptions() ), - 'primary' => true, // don't collapse this in vector - ); - } - wfProfileOut( __METHOD__ . '-edit' ); - - wfProfileIn( __METHOD__ . '-live' ); - // Checks if the page exists - if ( $title->exists() ) { - // Adds history view link - $content_navigation['views']['history'] = array( - 'class' => ( $onPage && $action == 'history' ) ? 'selected' : false, - 'text' => wfMessageFallback( "$skname-view-history", 'history_short' ) - ->setContext( $this->getContext() )->text(), - 'href' => $title->getLocalURL( 'action=history' ), - 'rel' => 'archives', - ); - - if ( $title->quickUserCan( 'delete', $user ) ) { - $content_navigation['actions']['delete'] = array( - 'class' => ( $onPage && $action == 'delete' ) ? 'selected' : false, - 'text' => wfMessageFallback( "$skname-action-delete", 'delete' ) - ->setContext( $this->getContext() )->text(), - 'href' => $title->getLocalURL( 'action=delete' ) - ); - } - - if ( $title->quickUserCan( 'move', $user ) ) { - $moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() ); - $content_navigation['actions']['move'] = array( - 'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : false, - 'text' => wfMessageFallback( "$skname-action-move", 'move' ) - ->setContext( $this->getContext() )->text(), - 'href' => $moveTitle->getLocalURL() - ); - } - } else { - // article doesn't exist or is deleted - if ( $user->isAllowed( 'deletedhistory' ) ) { - $n = $title->isDeleted(); - if ( $n ) { - $undelTitle = SpecialPage::getTitleFor( 'Undelete' ); - // If the user can't undelete but can view deleted - // history show them a "View .. deleted" tab instead. - $msgKey = $user->isAllowed( 'undelete' ) ? 'undelete' : 'viewdeleted'; - $content_navigation['actions']['undelete'] = array( - 'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false, - 'text' => wfMessageFallback( "$skname-action-$msgKey", "{$msgKey}_short" ) - ->setContext( $this->getContext() )->numParams( $n )->text(), - 'href' => $undelTitle->getLocalURL( array( 'target' => $title->getPrefixedDBkey() ) ) - ); - } - } - } - - if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() && - MWNamespace::getRestrictionLevels( $title->getNamespace(), $user ) !== array( '' ) - ) { - $mode = $title->isProtected() ? 'unprotect' : 'protect'; - $content_navigation['actions'][$mode] = array( - 'class' => ( $onPage && $action == $mode ) ? 'selected' : false, - 'text' => wfMessageFallback( "$skname-action-$mode", $mode ) - ->setContext( $this->getContext() )->text(), - 'href' => $title->getLocalURL( "action=$mode" ) - ); - } - - wfProfileOut( __METHOD__ . '-live' ); - - // Checks if the user is logged in - if ( $this->loggedin && $user->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) ) { - /** - * The following actions use messages which, if made particular to - * the any specific skins, would break the Ajax code which makes this - * action happen entirely inline. OutputPage::getJSVars - * defines a set of messages in a javascript object - and these - * messages are assumed to be global for all skins. Without making - * a change to that procedure these messages will have to remain as - * the global versions. - */ - $mode = $user->isWatched( $title ) ? 'unwatch' : 'watch'; - $token = WatchAction::getWatchToken( $title, $user, $mode ); - $content_navigation['actions'][$mode] = array( - 'class' => $onPage && ( $action == 'watch' || $action == 'unwatch' ) ? 'selected' : false, - // uses 'watch' or 'unwatch' message - 'text' => $this->msg( $mode )->text(), - 'href' => $title->getLocalURL( array( 'action' => $mode, 'token' => $token ) ) - ); - } - } - - wfRunHooks( 'SkinTemplateNavigation', array( &$this, &$content_navigation ) ); - - if ( $userCanRead && !$wgDisableLangConversion ) { - $pageLang = $title->getPageLanguage(); - // Gets list of language variants - $variants = $pageLang->getVariants(); - // Checks that language conversion is enabled and variants exist - // And if it is not in the special namespace - if ( count( $variants ) > 1 ) { - // Gets preferred variant (note that user preference is - // only possible for wiki content language variant) - $preferred = $pageLang->getPreferredVariant(); - if ( Action::getActionName( $this ) === 'view' ) { - $params = $request->getQueryValues(); - unset( $params['title'] ); - } else { - $params = array(); - } - // Loops over each variant - foreach ( $variants as $code ) { - // Gets variant name from language code - $varname = $pageLang->getVariantname( $code ); - // Appends variant link - $content_navigation['variants'][] = array( - 'class' => ( $code == $preferred ) ? 'selected' : false, - 'text' => $varname, - 'href' => $title->getLocalURL( array( 'variant' => $code ) + $params ), - 'lang' => wfBCP47( $code ), - 'hreflang' => wfBCP47( $code ), - ); - } - } - } - } else { - // If it's not content, it's got to be a special page - $content_navigation['namespaces']['special'] = array( - 'class' => 'selected', - 'text' => $this->msg( 'nstab-special' )->text(), - 'href' => $request->getRequestURL(), // @see: bug 2457, bug 2510 - 'context' => 'subject' - ); - - wfRunHooks( 'SkinTemplateNavigation::SpecialPage', - array( &$this, &$content_navigation ) ); - } - - // Equiv to SkinTemplateContentActions - wfRunHooks( 'SkinTemplateNavigation::Universal', array( &$this, &$content_navigation ) ); - - // Setup xml ids and tooltip info - foreach ( $content_navigation as $section => &$links ) { - foreach ( $links as $key => &$link ) { - $xmlID = $key; - if ( isset( $link['context'] ) && $link['context'] == 'subject' ) { - $xmlID = 'ca-nstab-' . $xmlID; - } elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) { - $xmlID = 'ca-talk'; - } elseif ( $section == 'variants' ) { - $xmlID = 'ca-varlang-' . $xmlID; - } else { - $xmlID = 'ca-' . $xmlID; - } - $link['id'] = $xmlID; - } - } - - # We don't want to give the watch tab an accesskey if the - # page is being edited, because that conflicts with the - # accesskey on the watch checkbox. We also don't want to - # give the edit tab an accesskey, because that's fairly - # superfluous and conflicts with an accesskey (Ctrl-E) often - # used for editing in Safari. - if ( in_array( $action, array( 'edit', 'submit' ) ) ) { - if ( isset( $content_navigation['views']['edit'] ) ) { - $content_navigation['views']['edit']['tooltiponly'] = true; - } - if ( isset( $content_navigation['actions']['watch'] ) ) { - $content_navigation['actions']['watch']['tooltiponly'] = true; - } - if ( isset( $content_navigation['actions']['unwatch'] ) ) { - $content_navigation['actions']['unwatch']['tooltiponly'] = true; - } - } - - wfProfileOut( __METHOD__ ); - - return $content_navigation; - } - - /** - * an array of edit links by default used for the tabs - * @param array $content_navigation - * @return array - */ - private function buildContentActionUrls( $content_navigation ) { - - wfProfileIn( __METHOD__ ); - - // content_actions has been replaced with content_navigation for backwards - // compatibility and also for skins that just want simple tabs content_actions - // is now built by flattening the content_navigation arrays into one - - $content_actions = array(); - - foreach ( $content_navigation as $links ) { - foreach ( $links as $key => $value ) { - if ( isset( $value['redundant'] ) && $value['redundant'] ) { - // Redundant tabs are dropped from content_actions - continue; - } - - // content_actions used to have ids built using the "ca-$key" pattern - // so the xmlID based id is much closer to the actual $key that we want - // for that reason we'll just strip out the ca- if present and use - // the latter potion of the "id" as the $key - if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) { - $key = substr( $value['id'], 3 ); - } - - if ( isset( $content_actions[$key] ) ) { - wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening " . - "content_navigation into content_actions.\n" ); - continue; - } - - $content_actions[$key] = $value; - } - } - - wfProfileOut( __METHOD__ ); - - return $content_actions; - } - - /** - * build array of common navigation links - * @return array - */ - protected function buildNavUrls() { - global $wgUploadNavigationUrl; - - wfProfileIn( __METHOD__ ); - - $out = $this->getOutput(); - $request = $this->getRequest(); - - $nav_urls = array(); - $nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() ); - if ( $wgUploadNavigationUrl ) { - $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl ); - } elseif ( UploadBase::isEnabled() && UploadBase::isAllowed( $this->getUser() ) === true ) { - $nav_urls['upload'] = array( 'href' => self::makeSpecialUrl( 'Upload' ) ); - } else { - $nav_urls['upload'] = false; - } - $nav_urls['specialpages'] = array( 'href' => self::makeSpecialUrl( 'Specialpages' ) ); - - $nav_urls['print'] = false; - $nav_urls['permalink'] = false; - $nav_urls['info'] = false; - $nav_urls['whatlinkshere'] = false; - $nav_urls['recentchangeslinked'] = false; - $nav_urls['contributions'] = false; - $nav_urls['log'] = false; - $nav_urls['blockip'] = false; - $nav_urls['emailuser'] = false; - $nav_urls['userrights'] = false; - - // A print stylesheet is attached to all pages, but nobody ever - // figures that out. :) Add a link... - if ( !$out->isPrintable() && ( $out->isArticle() || $this->getTitle()->isSpecialPage() ) ) { - $nav_urls['print'] = array( - 'text' => $this->msg( 'printableversion' )->text(), - 'href' => $this->getTitle()->getLocalURL( - $request->appendQueryValue( 'printable', 'yes', true ) ) - ); - } - - if ( $out->isArticle() ) { - // Also add a "permalink" while we're at it - $revid = $this->getRevisionId(); - if ( $revid ) { - $nav_urls['permalink'] = array( - 'text' => $this->msg( 'permalink' )->text(), - 'href' => $this->getTitle()->getLocalURL( "oldid=$revid" ) - ); - } - - // Use the copy of revision ID in case this undocumented, shady hook tries to mess with internals - wfRunHooks( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink', - array( &$this, &$nav_urls, &$revid, &$revid ) ); - } - - if ( $out->isArticleRelated() ) { - $nav_urls['whatlinkshere'] = array( - 'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage )->getLocalURL() - ); - - $nav_urls['info'] = array( - 'text' => $this->msg( 'pageinfo-toolboxlink' )->text(), - 'href' => $this->getTitle()->getLocalURL( "action=info" ) - ); - - if ( $this->getTitle()->getArticleID() ) { - $nav_urls['recentchangeslinked'] = array( - 'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalURL() - ); - } - } - - $user = $this->getRelevantUser(); - if ( $user ) { - $rootUser = $user->getName(); - - $nav_urls['contributions'] = array( - 'text' => $this->msg( 'contributions', $rootUser )->text(), - 'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ) - ); - - $nav_urls['log'] = array( - 'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser ) - ); - - if ( $this->getUser()->isAllowed( 'block' ) ) { - $nav_urls['blockip'] = array( - 'href' => self::makeSpecialUrlSubpage( 'Block', $rootUser ) - ); - } - - if ( $this->showEmailUser( $user ) ) { - $nav_urls['emailuser'] = array( - 'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ) - ); - } - - if ( !$user->isAnon() ) { - $sur = new UserrightsPage; - $sur->setContext( $this->getContext() ); - if ( $sur->userCanExecute( $this->getUser() ) ) { - $nav_urls['userrights'] = array( - 'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser ) - ); - } - } - } - - wfProfileOut( __METHOD__ ); - return $nav_urls; - } - - /** - * Generate strings used for xml 'id' names - * @return string - */ - protected function getNameSpaceKey() { - return $this->getTitle()->getNamespaceKey(); - } -} - -/** - * Generic wrapper for template functions, with interface - * compatible with what we use of PHPTAL 0.7. - * @ingroup Skins - */ -abstract class QuickTemplate { - /** - * Constructor - */ - function __construct() { - $this->data = array(); - $this->translator = new MediaWikiI18N(); - } - - /** - * Sets the value $value to $name - * @param string $name - * @param mixed $value - */ - public function set( $name, $value ) { - $this->data[$name] = $value; - } - - /** - * Gets the template data requested - * @since 1.22 - * @param string $name Key for the data - * @param mixed $default Optional default (or null) - * @return mixed The value of the data requested or the deafult - */ - public function get( $name, $default = null ) { - if ( isset( $this->data[$name] ) ) { - return $this->data[$name]; - } else { - return $default; - } - } - - /** - * @param string $name - * @param mixed $value - */ - public function setRef( $name, &$value ) { - $this->data[$name] =& $value; - } - - /** - * @param MediaWikiI18N $t - */ - public function setTranslator( &$t ) { - $this->translator = &$t; - } - - /** - * Main function, used by classes that subclass QuickTemplate - * to show the actual HTML output - */ - abstract public function execute(); - - /** - * @private - * @param string $str - * @return string - */ - function text( $str ) { - echo htmlspecialchars( $this->data[$str] ); - } - - /** - * @private - * @param string $str - * @return string - */ - function html( $str ) { - echo $this->data[$str]; - } - - /** - * @private - * @param string $str - * @return string - */ - function msg( $str ) { - echo htmlspecialchars( $this->translator->translate( $str ) ); - } - - /** - * @private - * @param string $str - * @return string - */ - function msgHtml( $str ) { - echo $this->translator->translate( $str ); - } - - /** - * An ugly, ugly hack. - * @private - * @param string $str - * @return string - */ - function msgWiki( $str ) { - global $wgOut; - - $text = $this->translator->translate( $str ); - echo $wgOut->parse( $text ); - } - - /** - * @private - * @param string $str - * @return bool - */ - function haveData( $str ) { - return isset( $this->data[$str] ); - } - - /** - * @private - * - * @param string $str - * @return bool - */ - function haveMsg( $str ) { - $msg = $this->translator->translate( $str ); - return ( $msg != '-' ) && ( $msg != '' ); # ???? - } - - /** - * Get the Skin object related to this object - * - * @return Skin - */ - public function getSkin() { - return $this->data['skin']; - } - - /** - * Fetch the output of a QuickTemplate and return it - * - * @since 1.23 - * @return string - */ - public function getHTML() { - ob_start(); - $this->execute(); - $html = ob_get_contents(); - ob_end_clean(); - return $html; - } -} - -/** - * New base template for a skin's template extended from QuickTemplate - * this class features helper methods that provide common ways of interacting - * with the data stored in the QuickTemplate - */ -abstract class BaseTemplate extends QuickTemplate { - - /** - * Get a Message object with its context set - * - * @param string $name Message name - * @return Message - */ - public function getMsg( $name ) { - return $this->getSkin()->msg( $name ); - } - - function msg( $str ) { - echo $this->getMsg( $str )->escaped(); - } - - function msgHtml( $str ) { - echo $this->getMsg( $str )->text(); - } - - function msgWiki( $str ) { - echo $this->getMsg( $str )->parseAsBlock(); - } - - /** - * Create an array of common toolbox items from the data in the quicktemplate - * stored by SkinTemplate. - * The resulting array is built according to a format intended to be passed - * through makeListItem to generate the html. - * @return array - */ - function getToolbox() { - wfProfileIn( __METHOD__ ); - - $toolbox = array(); - if ( isset( $this->data['nav_urls']['whatlinkshere'] ) - && $this->data['nav_urls']['whatlinkshere'] - ) { - $toolbox['whatlinkshere'] = $this->data['nav_urls']['whatlinkshere']; - $toolbox['whatlinkshere']['id'] = 't-whatlinkshere'; - } - if ( isset( $this->data['nav_urls']['recentchangeslinked'] ) - && $this->data['nav_urls']['recentchangeslinked'] - ) { - $toolbox['recentchangeslinked'] = $this->data['nav_urls']['recentchangeslinked']; - $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox'; - $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked'; - } - if ( isset( $this->data['feeds'] ) && $this->data['feeds'] ) { - $toolbox['feeds']['id'] = 'feedlinks'; - $toolbox['feeds']['links'] = array(); - foreach ( $this->data['feeds'] as $key => $feed ) { - $toolbox['feeds']['links'][$key] = $feed; - $toolbox['feeds']['links'][$key]['id'] = "feed-$key"; - $toolbox['feeds']['links'][$key]['rel'] = 'alternate'; - $toolbox['feeds']['links'][$key]['type'] = "application/{$key}+xml"; - $toolbox['feeds']['links'][$key]['class'] = 'feedlink'; - } - } - foreach ( array( 'contributions', 'log', 'blockip', 'emailuser', - 'userrights', 'upload', 'specialpages' ) as $special - ) { - if ( isset( $this->data['nav_urls'][$special] ) && $this->data['nav_urls'][$special] ) { - $toolbox[$special] = $this->data['nav_urls'][$special]; - $toolbox[$special]['id'] = "t-$special"; - } - } - if ( isset( $this->data['nav_urls']['print'] ) && $this->data['nav_urls']['print'] ) { - $toolbox['print'] = $this->data['nav_urls']['print']; - $toolbox['print']['id'] = 't-print'; - $toolbox['print']['rel'] = 'alternate'; - $toolbox['print']['msg'] = 'printableversion'; - } - if ( isset( $this->data['nav_urls']['permalink'] ) && $this->data['nav_urls']['permalink'] ) { - $toolbox['permalink'] = $this->data['nav_urls']['permalink']; - if ( $toolbox['permalink']['href'] === '' ) { - unset( $toolbox['permalink']['href'] ); - $toolbox['ispermalink']['tooltiponly'] = true; - $toolbox['ispermalink']['id'] = 't-ispermalink'; - $toolbox['ispermalink']['msg'] = 'permalink'; - } else { - $toolbox['permalink']['id'] = 't-permalink'; - } - } - if ( isset( $this->data['nav_urls']['info'] ) && $this->data['nav_urls']['info'] ) { - $toolbox['info'] = $this->data['nav_urls']['info']; - $toolbox['info']['id'] = 't-info'; - } - - wfRunHooks( 'BaseTemplateToolbox', array( &$this, &$toolbox ) ); - wfProfileOut( __METHOD__ ); - return $toolbox; - } - - /** - * Create an array of personal tools items from the data in the quicktemplate - * stored by SkinTemplate. - * The resulting array is built according to a format intended to be passed - * through makeListItem to generate the html. - * This is in reality the same list as already stored in personal_urls - * however it is reformatted so that you can just pass the individual items - * to makeListItem instead of hardcoding the element creation boilerplate. - * @return array - */ - function getPersonalTools() { - $personal_tools = array(); - foreach ( $this->get( 'personal_urls' ) as $key => $plink ) { - # The class on a personal_urls item is meant to go on the <a> instead - # of the <li> so we have to use a single item "links" array instead - # of using most of the personal_url's keys directly. - $ptool = array( - 'links' => array( - array( 'single-id' => "pt-$key" ), - ), - 'id' => "pt-$key", - ); - if ( isset( $plink['active'] ) ) { - $ptool['active'] = $plink['active']; - } - foreach ( array( 'href', 'class', 'text', 'dir' ) as $k ) { - if ( isset( $plink[$k] ) ) { - $ptool['links'][0][$k] = $plink[$k]; - } - } - $personal_tools[$key] = $ptool; - } - return $personal_tools; - } - - function getSidebar( $options = array() ) { - // Force the rendering of the following portals - $sidebar = $this->data['sidebar']; - if ( !isset( $sidebar['SEARCH'] ) ) { - $sidebar['SEARCH'] = true; - } - if ( !isset( $sidebar['TOOLBOX'] ) ) { - $sidebar['TOOLBOX'] = true; - } - if ( !isset( $sidebar['LANGUAGES'] ) ) { - $sidebar['LANGUAGES'] = true; - } - - if ( !isset( $options['search'] ) || $options['search'] !== true ) { - unset( $sidebar['SEARCH'] ); - } - if ( isset( $options['toolbox'] ) && $options['toolbox'] === false ) { - unset( $sidebar['TOOLBOX'] ); - } - if ( isset( $options['languages'] ) && $options['languages'] === false ) { - unset( $sidebar['LANGUAGES'] ); - } - - $boxes = array(); - foreach ( $sidebar as $boxName => $content ) { - if ( $content === false ) { - continue; - } - switch ( $boxName ) { - case 'SEARCH': - // Search is a special case, skins should custom implement this - $boxes[$boxName] = array( - 'id' => 'p-search', - 'header' => $this->getMsg( 'search' )->text(), - 'generated' => false, - 'content' => true, - ); - break; - case 'TOOLBOX': - $msgObj = $this->getMsg( 'toolbox' ); - $boxes[$boxName] = array( - 'id' => 'p-tb', - 'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox', - 'generated' => false, - 'content' => $this->getToolbox(), - ); - break; - case 'LANGUAGES': - if ( $this->data['language_urls'] ) { - $msgObj = $this->getMsg( 'otherlanguages' ); - $boxes[$boxName] = array( - 'id' => 'p-lang', - 'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages', - 'generated' => false, - 'content' => $this->data['language_urls'], - ); - } - break; - default: - $msgObj = $this->getMsg( $boxName ); - $boxes[$boxName] = array( - 'id' => "p-$boxName", - 'header' => $msgObj->exists() ? $msgObj->text() : $boxName, - 'generated' => true, - 'content' => $content, - ); - break; - } - } - - // HACK: Compatibility with extensions still using SkinTemplateToolboxEnd - $hookContents = null; - if ( isset( $boxes['TOOLBOX'] ) ) { - ob_start(); - // We pass an extra 'true' at the end so extensions using BaseTemplateToolbox - // can abort and avoid outputting double toolbox links - wfRunHooks( 'SkinTemplateToolboxEnd', array( &$this, true ) ); - $hookContents = ob_get_contents(); - ob_end_clean(); - if ( !trim( $hookContents ) ) { - $hookContents = null; - } - } - // END hack - - if ( isset( $options['htmlOnly'] ) && $options['htmlOnly'] === true ) { - foreach ( $boxes as $boxName => $box ) { - if ( is_array( $box['content'] ) ) { - $content = '<ul>'; - foreach ( $box['content'] as $key => $val ) { - $content .= "\n " . $this->makeListItem( $key, $val ); - } - // HACK, shove the toolbox end onto the toolbox if we're rendering itself - if ( $hookContents ) { - $content .= "\n $hookContents"; - } - // END hack - $content .= "\n</ul>\n"; - $boxes[$boxName]['content'] = $content; - } - } - } else { - if ( $hookContents ) { - $boxes['TOOLBOXEND'] = array( - 'id' => 'p-toolboxend', - 'header' => $boxes['TOOLBOX']['header'], - 'generated' => false, - 'content' => "<ul>{$hookContents}</ul>", - ); - // HACK: Make sure that TOOLBOXEND is sorted next to TOOLBOX - $boxes2 = array(); - foreach ( $boxes as $key => $box ) { - if ( $key === 'TOOLBOXEND' ) { - continue; - } - $boxes2[$key] = $box; - if ( $key === 'TOOLBOX' ) { - $boxes2['TOOLBOXEND'] = $boxes['TOOLBOXEND']; - } - } - $boxes = $boxes2; - // END hack - } - } - - return $boxes; - } - - /** - * @param string $name - */ - protected function renderAfterPortlet( $name ) { - $content = ''; - wfRunHooks( 'BaseTemplateAfterPortlet', array( $this, $name, &$content ) ); - - if ( $content !== '' ) { - echo "<div class='after-portlet after-portlet-$name'>$content</div>"; - } - - } - - /** - * Makes a link, usually used by makeListItem to generate a link for an item - * in a list used in navigation lists, portlets, portals, sidebars, etc... - * - * @param string $key Usually a key from the list you are generating this - * link from. - * @param array $item Contains some of a specific set of keys. - * - * The text of the link will be generated either from the contents of the - * "text" key in the $item array, if a "msg" key is present a message by - * that name will be used, and if neither of those are set the $key will be - * used as a message name. - * - * If a "href" key is not present makeLink will just output htmlescaped text. - * The "href", "id", "class", "rel", and "type" keys are used as attributes - * for the link if present. - * - * If an "id" or "single-id" (if you don't want the actual id to be output - * on the link) is present it will be used to generate a tooltip and - * accesskey for the link. - * - * The keys "context" and "primary" are ignored; these keys are used - * internally by skins and are not supposed to be included in the HTML - * output. - * - * If you don't want an accesskey, set $item['tooltiponly'] = true; - * - * @param array $options Can be used to affect the output of a link. - * Possible options are: - * - 'text-wrapper' key to specify a list of elements to wrap the text of - * a link in. This should be an array of arrays containing a 'tag' and - * optionally an 'attributes' key. If you only have one element you don't - * need to wrap it in another array. eg: To use <a><span>...</span></a> - * in all links use array( 'text-wrapper' => array( 'tag' => 'span' ) ) - * for your options. - * - 'link-class' key can be used to specify additional classes to apply - * to all links. - * - 'link-fallback' can be used to specify a tag to use instead of "<a>" - * if there is no link. eg: If you specify 'link-fallback' => 'span' than - * any non-link will output a "<span>" instead of just text. - * - * @return string - */ - function makeLink( $key, $item, $options = array() ) { - if ( isset( $item['text'] ) ) { - $text = $item['text']; - } else { - $text = $this->translator->translate( isset( $item['msg'] ) ? $item['msg'] : $key ); - } - - $html = htmlspecialchars( $text ); - - if ( isset( $options['text-wrapper'] ) ) { - $wrapper = $options['text-wrapper']; - if ( isset( $wrapper['tag'] ) ) { - $wrapper = array( $wrapper ); - } - while ( count( $wrapper ) > 0 ) { - $element = array_pop( $wrapper ); - $html = Html::rawElement( $element['tag'], isset( $element['attributes'] ) - ? $element['attributes'] - : null, $html ); - } - } - - if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) { - $attrs = $item; - foreach ( array( 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary' ) as $k ) { - unset( $attrs[$k] ); - } - - if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) { - $item['single-id'] = $item['id']; - } - if ( isset( $item['single-id'] ) ) { - if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) { - $title = Linker::titleAttrib( $item['single-id'] ); - if ( $title !== false ) { - $attrs['title'] = $title; - } - } else { - $tip = Linker::tooltipAndAccesskeyAttribs( $item['single-id'] ); - if ( isset( $tip['title'] ) && $tip['title'] !== false ) { - $attrs['title'] = $tip['title']; - } - if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) { - $attrs['accesskey'] = $tip['accesskey']; - } - } - } - if ( isset( $options['link-class'] ) ) { - if ( isset( $attrs['class'] ) ) { - $attrs['class'] .= " {$options['link-class']}"; - } else { - $attrs['class'] = $options['link-class']; - } - } - $html = Html::rawElement( isset( $attrs['href'] ) - ? 'a' - : $options['link-fallback'], $attrs, $html ); - } - - return $html; - } - - /** - * Generates a list item for a navigation, portlet, portal, sidebar... list - * - * @param string $key Usually a key from the list you are generating this link from. - * @param array $item Array of list item data containing some of a specific set of keys. - * The "id", "class" and "itemtitle" keys will be used as attributes for the list item, - * if "active" contains a value of true a "active" class will also be appended to class. - * - * @param array $options - * - * If you want something other than a "<li>" you can pass a tag name such as - * "tag" => "span" in the $options array to change the tag used. - * link/content data for the list item may come in one of two forms - * A "links" key may be used, in which case it should contain an array with - * a list of links to include inside the list item, see makeLink for the - * format of individual links array items. - * - * Otherwise the relevant keys from the list item $item array will be passed - * to makeLink instead. Note however that "id" and "class" are used by the - * list item directly so they will not be passed to makeLink - * (however the link will still support a tooltip and accesskey from it) - * If you need an id or class on a single link you should include a "links" - * array with just one link item inside of it. If you want to add a title - * to the list item itself, you can set "itemtitle" to the value. - * $options is also passed on to makeLink calls - * - * @return string - */ - function makeListItem( $key, $item, $options = array() ) { - if ( isset( $item['links'] ) ) { - $links = array(); - foreach ( $item['links'] as $linkKey => $link ) { - $links[] = $this->makeLink( $linkKey, $link, $options ); - } - $html = implode( ' ', $links ); - } else { - $link = $item; - // These keys are used by makeListItem and shouldn't be passed on to the link - foreach ( array( 'id', 'class', 'active', 'tag', 'itemtitle' ) as $k ) { - unset( $link[$k] ); - } - if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) { - // The id goes on the <li> not on the <a> for single links - // but makeSidebarLink still needs to know what id to use when - // generating tooltips and accesskeys. - $link['single-id'] = $item['id']; - } - $html = $this->makeLink( $key, $link, $options ); - } - - $attrs = array(); - foreach ( array( 'id', 'class' ) as $attr ) { - if ( isset( $item[$attr] ) ) { - $attrs[$attr] = $item[$attr]; - } - } - if ( isset( $item['active'] ) && $item['active'] ) { - if ( !isset( $attrs['class'] ) ) { - $attrs['class'] = ''; - } - $attrs['class'] .= ' active'; - $attrs['class'] = trim( $attrs['class'] ); - } - if ( isset( $item['itemtitle'] ) ) { - $attrs['title'] = $item['itemtitle']; - } - return Html::rawElement( isset( $options['tag'] ) ? $options['tag'] : 'li', $attrs, $html ); - } - - function makeSearchInput( $attrs = array() ) { - $realAttrs = array( - 'type' => 'search', - 'name' => 'search', - 'placeholder' => wfMessage( 'searchsuggest-search' )->text(), - 'value' => $this->get( 'search', '' ), - ); - $realAttrs = array_merge( $realAttrs, Linker::tooltipAndAccesskeyAttribs( 'search' ), $attrs ); - return Html::element( 'input', $realAttrs ); - } - - function makeSearchButton( $mode, $attrs = array() ) { - switch ( $mode ) { - case 'go': - case 'fulltext': - $realAttrs = array( - 'type' => 'submit', - 'name' => $mode, - 'value' => $this->translator->translate( - $mode == 'go' ? 'searcharticle' : 'searchbutton' ), - ); - $realAttrs = array_merge( - $realAttrs, - Linker::tooltipAndAccesskeyAttribs( "search-$mode" ), - $attrs - ); - return Html::element( 'input', $realAttrs ); - case 'image': - $buttonAttrs = array( - 'type' => 'submit', - 'name' => 'button', - ); - $buttonAttrs = array_merge( - $buttonAttrs, - Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ), - $attrs - ); - unset( $buttonAttrs['src'] ); - unset( $buttonAttrs['alt'] ); - unset( $buttonAttrs['width'] ); - unset( $buttonAttrs['height'] ); - $imgAttrs = array( - 'src' => $attrs['src'], - 'alt' => isset( $attrs['alt'] ) - ? $attrs['alt'] - : $this->translator->translate( 'searchbutton' ), - 'width' => isset( $attrs['width'] ) ? $attrs['width'] : null, - 'height' => isset( $attrs['height'] ) ? $attrs['height'] : null, - ); - return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) ); - default: - throw new MWException( 'Unknown mode passed to BaseTemplate::makeSearchButton' ); - } - } - - /** - * Returns an array of footerlinks trimmed down to only those footer links that - * are valid. - * If you pass "flat" as an option then the returned array will be a flat array - * of footer icons instead of a key/value array of footerlinks arrays broken - * up into categories. - * @param string $option - * @return array|mixed - */ - function getFooterLinks( $option = null ) { - $footerlinks = $this->get( 'footerlinks' ); - - // Reduce footer links down to only those which are being used - $validFooterLinks = array(); - foreach ( $footerlinks as $category => $links ) { - $validFooterLinks[$category] = array(); - foreach ( $links as $link ) { - if ( isset( $this->data[$link] ) && $this->data[$link] ) { - $validFooterLinks[$category][] = $link; - } - } - if ( count( $validFooterLinks[$category] ) <= 0 ) { - unset( $validFooterLinks[$category] ); - } - } - - if ( $option == 'flat' ) { - // fold footerlinks into a single array using a bit of trickery - $validFooterLinks = call_user_func_array( - 'array_merge', - array_values( $validFooterLinks ) - ); - } - - return $validFooterLinks; - } - - /** - * Returns an array of footer icons filtered down by options relevant to how - * the skin wishes to display them. - * If you pass "icononly" as the option all footer icons which do not have an - * image icon set will be filtered out. - * If you pass "nocopyright" then MediaWiki's copyright icon will not be included - * in the list of footer icons. This is mostly useful for skins which only - * display the text from footericons instead of the images and don't want a - * duplicate copyright statement because footerlinks already rendered one. - * @param string $option - * @return string - */ - function getFooterIcons( $option = null ) { - // Generate additional footer icons - $footericons = $this->get( 'footericons' ); - - if ( $option == 'icononly' ) { - // Unset any icons which don't have an image - foreach ( $footericons as &$footerIconsBlock ) { - foreach ( $footerIconsBlock as $footerIconKey => $footerIcon ) { - if ( !is_string( $footerIcon ) && !isset( $footerIcon['src'] ) ) { - unset( $footerIconsBlock[$footerIconKey] ); - } - } - } - // Redo removal of any empty blocks - foreach ( $footericons as $footerIconsKey => &$footerIconsBlock ) { - if ( count( $footerIconsBlock ) <= 0 ) { - unset( $footericons[$footerIconsKey] ); - } - } - } elseif ( $option == 'nocopyright' ) { - unset( $footericons['copyright']['copyright'] ); - if ( count( $footericons['copyright'] ) <= 0 ) { - unset( $footericons['copyright'] ); - } - } - - return $footericons; - } - - /** - * Output the basic end-page trail including bottomscripts, reporttime, and - * debug stuff. This should be called right before outputting the closing - * body and html tags. - */ - function printTrail() { ?> -<?php echo MWDebug::getDebugHTML( $this->getSkin()->getContext() ); ?> -<?php $this->html( 'bottomscripts' ); /* JS call to runBodyOnloadHook */ ?> -<?php $this->html( 'reporttime' ) ?> -<?php - } -} diff --git a/includes/StatCounter.php b/includes/StatCounter.php index 102fffd0d9..5fc8f2f5d8 100644 --- a/includes/StatCounter.php +++ b/includes/StatCounter.php @@ -39,7 +39,11 @@ class StatCounter { /** @var array */ protected $deltas = array(); // (key => count) - protected function __construct() { + /** @var Config */ + protected $config; + + protected function __construct( Config $config ) { + $this->config = $config; } /** @@ -48,7 +52,9 @@ class StatCounter { public static function singleton() { static $instance = null; if ( !$instance ) { - $instance = new self(); + $instance = new self( + ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) + ); } return $instance; } @@ -74,12 +80,11 @@ class StatCounter { * @return void */ public function flush() { - global $wgStatsMethod; - + $statsMethod = $this->config->get( 'StatsMethod' ); $deltas = array_filter( $this->deltas ); // remove 0 valued entries - if ( $wgStatsMethod === 'udp' ) { + if ( $statsMethod === 'udp' ) { $this->sendDeltasUDP( $deltas ); - } elseif ( $wgStatsMethod === 'cache' ) { + } elseif ( $statsMethod === 'cache' ) { $this->sendDeltasMemc( $deltas ); } else { // disabled @@ -92,14 +97,12 @@ class StatCounter { * @return void */ protected function sendDeltasUDP( array $deltas ) { - global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgAggregateStatsID, - $wgStatsFormatString; - - $id = strlen( $wgAggregateStatsID ) ? $wgAggregateStatsID : wfWikiID(); + $aggregateStatsID = $this->config->get( 'AggregateStatsID' ); + $id = strlen( $aggregateStatsID ) ? $aggregateStatsID : wfWikiID(); $lines = array(); foreach ( $deltas as $key => $count ) { - $lines[] = sprintf( $wgStatsFormatString, $id, $count, $key ); + $lines[] = sprintf( $this->config->get( 'StatsFormatString' ), $id, $count, $key ); } if ( count( $lines ) ) { @@ -126,8 +129,8 @@ class StatCounter { $packet, strlen( $packet ), 0, - $wgUDPProfilerHost, - $wgUDPProfilerPort + $this->config->get( 'UDPProfilerHost' ), + $this->config->get( 'UDPProfilerPort' ) ); wfRestoreWarnings(); } diff --git a/includes/StubObject.php b/includes/StubObject.php index 73952da8c5..8878660b73 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -58,7 +58,7 @@ class StubObject { * @param string $class Name of the class of the real object. * @param array $params Parameters to pass to constructor of the real object. */ - function __construct( $global = null, $class = null, $params = array() ) { + public function __construct( $global = null, $class = null, $params = array() ) { $this->global = $global; $this->class = $class; $this->params = $params; @@ -71,7 +71,7 @@ class StubObject { * @param object $obj Object to check. * @return bool True if $obj is not an instance of StubObject class. */ - static function isRealObject( $obj ) { + public static function isRealObject( $obj ) { return is_object( $obj ) && !$obj instanceof StubObject; } @@ -83,7 +83,7 @@ class StubObject { * @param object $obj Object to check. * @return void */ - static function unstub( &$obj ) { + public static function unstub( &$obj ) { if ( $obj instanceof StubObject ) { $obj = $obj->_unstub( 'unstub', 3 ); } @@ -100,7 +100,7 @@ class StubObject { * @param array $args Arguments * @return mixed */ - function _call( $name, $args ) { + public function _call( $name, $args ) { $this->_unstub( $name, 5 ); return call_user_func_array( array( $GLOBALS[$this->global], $name ), $args ); } @@ -109,7 +109,7 @@ class StubObject { * Create a new object to replace this stub object. * @return object */ - function _newObject() { + public function _newObject() { return MWFunction::newObj( $this->class, $this->params ); } @@ -121,7 +121,7 @@ class StubObject { * @param array $args Arguments * @return mixed */ - function __call( $name, $args ) { + public function __call( $name, $args ) { return $this->_call( $name, $args ); } @@ -137,7 +137,7 @@ class StubObject { * @return object The unstubbed version of itself * @throws MWException */ - function _unstub( $name = '_unstub', $level = 2 ) { + public function _unstub( $name = '_unstub', $level = 2 ) { static $recursionLevel = 0; if ( !$GLOBALS[$this->global] instanceof StubObject ) { @@ -170,18 +170,18 @@ class StubObject { */ class StubUserLang extends StubObject { - function __construct() { + public function __construct() { parent::__construct( 'wgLang' ); } - function __call( $name, $args ) { + public function __call( $name, $args ) { return $this->_call( $name, $args ); } /** * @return Language */ - function _newObject() { + public function _newObject() { return RequestContext::getMain()->getLanguage(); } } diff --git a/includes/Title.php b/includes/Title.php index 24811788f2..ca292eefcf 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -74,6 +74,9 @@ class Title { /** @var string Interwiki prefix */ public $mInterwiki = ''; + /** @var bool Was this Title created from a string with a local interwiki prefix? */ + private $mLocalInterwiki = false; + /** @var string Title fragment (i.e. the bit after the #) */ public $mFragment = ''; @@ -155,6 +158,9 @@ class Title { /** @var TitleValue A corresponding TitleValue object */ private $mTitleValue = null; + + /** @var bool Would deleting this page be a big deletion? */ + private $mIsBigDeletion = null; // @} /** @@ -823,6 +829,15 @@ class Title { return $this->mInterwiki; } + /** + * Was this a local interwiki link? + * + * @return bool + */ + public function wasLocalInterwiki() { + return $this->mLocalInterwiki; + } + /** * Determine whether the object refers to a page within * this project and is transcludable. @@ -929,10 +944,12 @@ class Title { * Get the page's content model id, see the CONTENT_MODEL_XXX constants. * * @throws MWException + * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update * @return string Content model id */ - public function getContentModel() { - if ( !$this->mContentModel ) { + public function getContentModel( $flags = 0 ) { + # Calling getArticleID() loads the field from cache as needed + if ( !$this->mContentModel && $this->getArticleID( $flags ) ) { $linkCache = LinkCache::singleton(); $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); } @@ -2063,7 +2080,7 @@ class Title { $ns = $this->mNamespace == NS_MAIN ? wfMessage( 'nstab-main' )->text() : $this->getNsText(); $errors[] = $this->mNamespace == NS_MEDIAWIKI ? - array( 'protectedinterface' ) : array( 'namespaceprotected', $ns ); + array( 'protectedinterface', $action ) : array( 'namespaceprotected', $ns, $action ); } return $errors; @@ -2087,15 +2104,15 @@ class Title { if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) { if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { - $errors[] = array( 'mycustomcssprotected' ); + $errors[] = array( 'mycustomcssprotected', $action ); } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) { - $errors[] = array( 'mycustomjsprotected' ); + $errors[] = array( 'mycustomjsprotected', $action ); } } else { if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { - $errors[] = array( 'customcssprotected' ); + $errors[] = array( 'customcssprotected', $action ); } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { - $errors[] = array( 'customjsprotected' ); + $errors[] = array( 'customjsprotected', $action ); } } } @@ -2130,9 +2147,9 @@ class Title { continue; } if ( !$user->isAllowed( $right ) ) { - $errors[] = array( 'protectedpagetext', $right ); + $errors[] = array( 'protectedpagetext', $right, $action ); } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) { - $errors[] = array( 'protectedpagetext', 'protect' ); + $errors[] = array( 'protectedpagetext', 'protect', $action ); } } @@ -2179,7 +2196,7 @@ class Title { foreach ( $cascadingSources as $page ) { $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; } - $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); + $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages, $action ); } } } @@ -2246,6 +2263,16 @@ class Title { $errors[] = array( 'immobile-target-page' ); } } elseif ( $action == 'delete' ) { + $tempErrors = $this->checkPageRestrictions( 'edit', + $user, array(), $doExpensiveQueries, true ); + if ( !$tempErrors ) { + $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit', + $user, $tempErrors, $doExpensiveQueries, true ); + } + if ( $tempErrors ) { + // If protection keeps them from editing, they shouldn't be able to delete. + $errors[] = array( 'deleteprotected' ); + } if ( $doExpensiveQueries && $wgDeleteRevisionsLimit && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() ) { @@ -2419,6 +2446,19 @@ class Title { 'checkPermissionHooks', 'checkReadPermissions', ); + # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions + # here as it will lead to duplicate error messages. This is okay to do + # since anywhere that checks for create will also check for edit, and + # those checks are called for edit. + } elseif ( $action == 'create' ) { + $checks = array( + 'checkQuickPermissions', + 'checkPermissionHooks', + 'checkPageRestrictions', + 'checkCascadingSourcesRestrictions', + 'checkActionPermissions', + 'checkUserBlock' + ); } else { $checks = array( 'checkQuickPermissions', @@ -3240,6 +3280,7 @@ class Title { $this->mEstimateRevisions = null; $this->mPageLanguage = false; $this->mDbPageLanguage = null; + $this->mIsBigDeletion = null; } /** @@ -3282,8 +3323,8 @@ class Title { // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share // the parsing code with Title, while avoiding massive refactoring. // @todo: get rid of secureAndSplit, refactor parsing code. - $parser = self::getTitleParser(); - $parts = $parser->splitTitleString( $dbkey, $this->getDefaultNamespace() ); + $titleParser = self::getTitleParser(); + $parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() ); } catch ( MalformedTitleException $ex ) { return false; } @@ -3291,6 +3332,7 @@ class Title { # Fill fields $this->setFragment( '#' . $parts['fragment'] ); $this->mInterwiki = $parts['interwiki']; + $this->mLocalInterwiki = $parts['local_interwiki']; $this->mNamespace = $parts['namespace']; $this->mUserCaseDBKey = $parts['user_case_dbkey']; @@ -3889,9 +3931,13 @@ class Title { $redirectContent = null; } + // bug 57084: log_page should be the ID of the *moved* page + $oldid = $this->getArticleID(); + $logTitle = clone $this; + $logEntry = new ManualLogEntry( 'move', $logType ); $logEntry->setPerformer( $wgUser ); - $logEntry->setTarget( $this ); + $logEntry->setTarget( $logTitle ); $logEntry->setComment( $reason ); $logEntry->setParameters( array( '4::target' => $nt->getPrefixedText(), @@ -3907,8 +3953,6 @@ class Title { # Truncate for whole multibyte characters. $comment = $wgContLang->truncate( $comment, 255 ); - $oldid = $this->getArticleID(); - $dbw = wfGetDB( DB_MASTER ); $newpage = WikiPage::factory( $nt ); @@ -4345,12 +4389,32 @@ class Title { return false; } - $revCount = $this->estimateRevisionCount(); - return $revCount > $wgDeleteRevisionsLimit; + if ( $this->mIsBigDeletion === null ) { + $dbr = wfGetDB( DB_SLAVE ); + + $innerQuery = $dbr->selectSQLText( + 'revision', + '1', + array( 'rev_page' => $this->getArticleID() ), + __METHOD__, + array( 'LIMIT' => $wgDeleteRevisionsLimit + 1 ) + ); + + $revCount = $dbr->query( + 'SELECT COUNT(*) FROM (' . $innerQuery . ') AS innerQuery', + __METHOD__ + ); + $revCount = $revCount->fetchRow(); + $revCount = $revCount['COUNT(*)']; + + $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit; + } + + return $this->mIsBigDeletion; } /** - * Get the approximate revision count of this page. + * Get the approximate revision count of this page. * * @return int */ diff --git a/includes/User.php b/includes/User.php index fe4118782b..45570966e8 100644 --- a/includes/User.php +++ b/includes/User.php @@ -829,7 +829,7 @@ class User implements IDBAccessObject { * @param int $ts Optional timestamp to convert, default 0 for the current time */ public function expirePassword( $ts = 0 ) { - $this->load(); + $this->loadPasswords(); $timestamp = wfTimestamp( TS_MW, $ts ); $this->mPasswordExpires = $timestamp; $this->saveSettings(); @@ -1885,7 +1885,6 @@ class User implements IDBAccessObject { return $this->mLocked; } global $wgAuth; - StubObject::unstub( $wgAuth ); $authUser = $wgAuth->getUserInstance( $this ); $this->mLocked = (bool)$authUser->isLocked(); return $this->mLocked; @@ -1903,7 +1902,6 @@ class User implements IDBAccessObject { $this->getBlockedStatus(); if ( !$this->mHideName ) { global $wgAuth; - StubObject::unstub( $wgAuth ); $authUser = $wgAuth->getUserInstance( $this ); $this->mHideName = (bool)$authUser->isHidden(); } @@ -2733,7 +2731,7 @@ class User implements IDBAccessObject { foreach ( $columns as $column ) { foreach ( $rows as $row ) { - $checkmatrixOptions["$prefix-$column-$row"] = true; + $checkmatrixOptions["$prefix$column-$row"] = true; } } @@ -3086,10 +3084,8 @@ class User implements IDBAccessObject { /** * Check if user is allowed to access a feature / make an action * - * @internal param \String $varargs permissions to test + * @param string $permissions,... Permissions to test * @return bool True if user is allowed to perform *any* of the given actions - * - * @return bool */ public function isAllowedAny( /*...*/ ) { $permissions = func_get_args(); @@ -3103,7 +3099,7 @@ class User implements IDBAccessObject { /** * - * @internal param $varargs string + * @param string $permissions,... Permissions to test * @return bool True if the user is allowed to perform *all* of the given actions */ public function isAllowedAll( /*...*/ ) { @@ -3790,12 +3786,14 @@ class User implements IDBAccessObject { */ public function checkPassword( $password ) { global $wgAuth, $wgLegacyEncoding; + + $section = new ProfileSection( __METHOD__ ); + $this->loadPasswords(); // Certain authentication plugins do NOT want to save // domain passwords in a mysql database, so we should // check this (in case $wgAuth->strict() is false). - if ( $wgAuth->authenticate( $this->getName(), $password ) ) { return true; } elseif ( $wgAuth->strict() ) { diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 913b430742..0ce9b5aaa5 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -448,6 +448,8 @@ class UserMailer { * This method is doing Q encoding inside encoded-words as defined by RFC 2047 * This is for email headers. * The built in quoted_printable_encode() is for email bodies + * @param string $string + * @param string $charset * @return string */ public static function quotedPrintable( $string, $charset = '' ) { diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php index e3655ceddb..53c69d817a 100644 --- a/includes/UserRightsProxy.php +++ b/includes/UserRightsProxy.php @@ -113,12 +113,23 @@ class UserRightsProxy { * @return null|UserRightsProxy */ private static function newFromLookup( $database, $field, $value, $ignoreInvalidDB = false ) { + global $wgSharedDB, $wgSharedTables; + // If the user table is shared, perform the user query on it, but don't pass it to the UserRightsProxy, + // as user rights are normally not shared. + if ( $wgSharedDB && in_array( 'user', $wgSharedTables ) ) { + $userdb = self::getDB( $wgSharedDB, $ignoreInvalidDB ); + } else { + $userdb = self::getDB( $database, $ignoreInvalidDB ); + } + $db = self::getDB( $database, $ignoreInvalidDB ); - if ( $db ) { - $row = $db->selectRow( 'user', + + if ( $db && $userdb ) { + $row = $userdb->selectRow( 'user', array( 'user_id', 'user_name' ), array( $field => $value ), __METHOD__ ); + if ( $row !== false ) { return new UserRightsProxy( $db, $database, $row->user_name, diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php index f69fe6334f..ab136b8954 100644 --- a/includes/WatchedItem.php +++ b/includes/WatchedItem.php @@ -164,6 +164,7 @@ class WatchedItem { /** * Check permissions * @param string $what 'viewmywatchlist' or 'editmywatchlist' + * @return bool */ private function isAllowed( $what ) { return !$this->mCheckRights || $this->mUser->isAllowed( $what ); @@ -270,45 +271,61 @@ class WatchedItem { } /** - * Given a title and user (assumes the object is setup), add the watch to the database. + * @param WatchedItem[] $items * @return bool */ - public function addWatch() { - wfProfileIn( __METHOD__ ); + public static function batchAddWatch( array $items ) { + $section = new ProfileSection( __METHOD__ ); - // Only loggedin user can have a watchlist - if ( wfReadOnly() || $this->mUser->isAnon() || !$this->isAllowed( 'editmywatchlist' ) ) { - wfProfileOut( __METHOD__ ); + if ( wfReadOnly() ) { return false; } - // Use INSERT IGNORE to avoid overwriting the notification timestamp - // if there's already an entry for this page - $dbw = wfGetDB( DB_MASTER ); - $dbw->insert( 'watchlist', - array( - 'wl_user' => $this->getUserId(), - 'wl_namespace' => MWNamespace::getSubject( $this->getTitleNs() ), - 'wl_title' => $this->getTitleDBkey(), + $rows = array(); + foreach ( $items as $item ) { + // Only loggedin user can have a watchlist + if ( $item->mUser->isAnon() || !$item->isAllowed( 'editmywatchlist' ) ) { + continue; + } + $rows[] = array( + 'wl_user' => $item->getUserId(), + 'wl_namespace' => MWNamespace::getSubject( $item->getTitleNs() ), + 'wl_title' => $item->getTitleDBkey(), + 'wl_notificationtimestamp' => null, + ); + // Every single watched page needs now to be listed in watchlist; + // namespace:page and namespace_talk:page need separate entries: + $rows[] = array( + 'wl_user' => $item->getUserId(), + 'wl_namespace' => MWNamespace::getTalk( $item->getTitleNs() ), + 'wl_title' => $item->getTitleDBkey(), 'wl_notificationtimestamp' => null - ), __METHOD__, 'IGNORE' ); + ); + $item->watched = true; + } - // Every single watched page needs now to be listed in watchlist; - // namespace:page and namespace_talk:page need separate entries: - $dbw->insert( 'watchlist', - array( - 'wl_user' => $this->getUserId(), - 'wl_namespace' => MWNamespace::getTalk( $this->getTitleNs() ), - 'wl_title' => $this->getTitleDBkey(), - 'wl_notificationtimestamp' => null - ), __METHOD__, 'IGNORE' ); + if ( !$rows ) { + return false; + } - $this->watched = true; + $dbw = wfGetDB( DB_MASTER ); + foreach ( array_chunk( $rows, 100 ) as $toInsert ) { + // Use INSERT IGNORE to avoid overwriting the notification timestamp + // if there's already an entry for this page + $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' ); + } - wfProfileOut( __METHOD__ ); return true; } + /** + * Given a title and user (assumes the object is setup), add the watch to the database. + * @return bool + */ + public function addWatch() { + return self::batchAddWatch( array( $this ) ); + } + /** * Same as addWatch, only the opposite. * @return bool diff --git a/includes/WebRequest.php b/includes/WebRequest.php index a1fa0eb775..b187c4acef 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -181,7 +181,12 @@ class WebRequest { continue; } $host = $parts[0]; - if ( $parts[1] === false ) { + if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) { + // Bug 70021: Assume that upstream proxy is running on the default + // port based on the protocol. We have no reliable way to determine + // the actual port in use upstream. + $port = $stdPort; + } elseif ( $parts[1] === false ) { if ( isset( $_SERVER['SERVER_PORT'] ) ) { $port = $_SERVER['SERVER_PORT']; } // else leave it as $stdPort diff --git a/includes/WebResponse.php b/includes/WebResponse.php index adccf9c7da..ad9f4e664c 100644 --- a/includes/WebResponse.php +++ b/includes/WebResponse.php @@ -51,7 +51,7 @@ class WebResponse { * secure: bool, secure attribute ($wgCookieSecure) * httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly) * raw: bool, if true uses PHP's setrawcookie() instead of setcookie() - * For backwards compatability, if $options is not an array then it and + * For backwards compatibility, if $options is not an array then it and * the following two parameters will be interpreted as values for * 'prefix', 'domain', and 'secure' * @since 1.22 Replaced $prefix, $domain, and $forceSecure with $options @@ -61,7 +61,7 @@ class WebResponse { global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly; if ( !is_array( $options ) ) { - // Backwards compatability + // Backwards compatibility $options = array( 'prefix' => $options ); if ( func_num_args() >= 5 ) { $options['domain'] = func_get_arg( 4 ); diff --git a/includes/WebStart.php b/includes/WebStart.php index e137628c5e..2ae72dcc00 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -40,8 +40,6 @@ if ( ini_get( 'register_globals' ) ) { header( 'X-Content-Type-Options: nosniff' ); $wgRequestTime = microtime( true ); -# getrusage() does not exist on the Microsoft Windows platforms, catching this -$wgRUstart = function_exists( 'getrusage' ) ? getrusage() : array(); unset( $IP ); # Valid web server entry point, enable includes. @@ -60,11 +58,12 @@ if ( $IP === false ) { $IP = realpath( '.' ) ?: dirname( __DIR__ ); } -# Start the autoloader, so that extensions can derive classes from core files -require_once "$IP/includes/AutoLoader.php"; - # Load the profiler require_once "$IP/includes/profiler/Profiler.php"; +$wgRUstart = wfGetRusage() ?: array(); + +# Start the autoloader, so that extensions can derive classes from core files +require_once "$IP/includes/AutoLoader.php"; # Load up some global defines. require_once "$IP/includes/Defines.php"; diff --git a/includes/Xml.php b/includes/Xml.php index 7761ecc0a6..159f711464 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -134,31 +134,6 @@ class Xml { return self::openElement( $element, $attribs ) . $contents . "</$element>"; } - /** - * Build a drop-down box for selecting a namespace - * - * @param string $selected Namespace which should be pre-selected - * @param string|null $all Value of an item denoting all namespaces, or null to omit - * @param string $element_name Value of the "name" attribute of the select tag - * @param string $label Optional label to add to the field - * @return string - * @deprecated since 1.19 - */ - public static function namespaceSelector( $selected = '', $all = null, - $element_name = 'namespace', $label = null - ) { - wfDeprecated( __METHOD__, '1.19' ); - return Html::namespaceSelector( array( - 'selected' => $selected, - 'all' => $all, - 'label' => $label, - ), array( - 'name' => $element_name, - 'id' => 'namespace', - 'class' => 'namespaceselector', - ) ); - } - /** * Create a date selector * @@ -317,7 +292,8 @@ class Xml { $attributes['value'] = $value; } - return self::element( 'input', $attributes + $attribs ); + return self::element( 'input', + Html::getTextInputAttributes( $attributes + $attribs ) ); } /** @@ -453,9 +429,16 @@ class Xml { * @return string HTML */ public static function checkLabel( $label, $name, $id, $checked = false, $attribs = array() ) { - return self::check( $name, $checked, array( 'id' => $id ) + $attribs ) . + global $wgUseMediaWikiUIEverywhere; + $chkLabel = self::check( $name, $checked, array( 'id' => $id ) + $attribs ) . ' ' . self::label( $label, $id, $attribs ); + + if ( $wgUseMediaWikiUIEverywhere ) { + $chkLabel = self::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . + $chkLabel . self::closeElement( 'div' ); + } + return $chkLabel; } /** @@ -480,12 +463,26 @@ class Xml { /** * Convenience function to build an HTML submit button + * When $wgUseMediaWikiUIEverywhere is true it will default to a constructive button * @param string $value Label text for the button * @param array $attribs Optional custom attributes * @return string HTML */ public static function submitButton( $value, $attribs = array() ) { - return Html::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs ); + global $wgUseMediaWikiUIEverywhere; + $baseAttrs = array( + 'type' => 'submit', + 'value' => $value, + ); + // Done conditionally for time being as it is possible + // some submit forms + // might need to be mw-ui-destructive (e.g. delete a page) + if ( $wgUseMediaWikiUIEverywhere ) { + $baseAttrs['class'] = 'mw-ui-button mw-ui-constructive'; + } + // Any custom attributes will take precendence of anything in baseAttrs e.g. override the class + $attribs = $attribs + $baseAttrs; + return Html::element( 'input', $attribs ); } /** @@ -617,12 +614,14 @@ class Xml { */ public static function textarea( $name, $content, $cols = 40, $rows = 5, $attribs = array() ) { return self::element( 'textarea', - array( - 'name' => $name, - 'id' => $name, - 'cols' => $cols, - 'rows' => $rows - ) + $attribs, + Html::getTextInputAttributes( + array( + 'name' => $name, + 'id' => $name, + 'cols' => $cols, + 'rows' => $rows + ) + $attribs + ), $content, false ); } diff --git a/includes/actions/Action.php b/includes/actions/Action.php index 839d0edb94..ffdf5168f2 100644 --- a/includes/actions/Action.php +++ b/includes/actions/Action.php @@ -38,18 +38,21 @@ abstract class Action { /** * Page on which we're performing the action + * @since 1.17 * @var WikiPage|Article|ImagePage|CategoryPage|Page $page */ protected $page; /** * IContextSource if specified; otherwise we'll use the Context from the Page + * @since 1.17 * @var IContextSource $context */ protected $context; /** * The fields used to create the HTMLForm + * @since 1.17 * @var array $fields */ protected $fields; @@ -82,6 +85,7 @@ abstract class Action { /** * Get an appropriate Action subclass for the given action + * @since 1.17 * @param string $action * @param Page $page * @param IContextSource $context @@ -152,6 +156,7 @@ abstract class Action { /** * Check if a given action is recognised, even if it's disabled + * @since 1.17 * * @param string $name Name of an action * @return bool @@ -162,6 +167,7 @@ abstract class Action { /** * Get the IContextSource in use here + * @since 1.17 * @return IContextSource */ final public function getContext() { @@ -179,6 +185,7 @@ abstract class Action { /** * Get the WebRequest being used for this instance + * @since 1.17 * * @return WebRequest */ @@ -188,6 +195,7 @@ abstract class Action { /** * Get the OutputPage being used for this instance + * @since 1.17 * * @return OutputPage */ @@ -197,6 +205,7 @@ abstract class Action { /** * Shortcut to get the User being used for this instance + * @since 1.17 * * @return User */ @@ -206,6 +215,7 @@ abstract class Action { /** * Shortcut to get the Skin being used for this instance + * @since 1.17 * * @return Skin */ @@ -224,6 +234,8 @@ abstract class Action { /** * Shortcut to get the Title object from the page + * @since 1.17 + * * @return Title */ final public function getTitle() { @@ -262,6 +274,8 @@ abstract class Action { /** * Return the name of the action this object responds to + * @since 1.17 + * * @return string Lowercase name */ abstract public function getName(); @@ -269,6 +283,8 @@ abstract class Action { /** * Get the permission required to perform this action. Often, but not always, * the same as the action name + * @since 1.17 + * * @return string|null */ public function getRestriction() { @@ -279,10 +295,10 @@ abstract class Action { * Checks if the given user (identified by an object) can perform this action. Can be * overridden by sub-classes with more complicated permissions schemes. Failures here * must throw subclasses of ErrorPageError + * @since 1.17 * * @param User $user The user to check, or null to use the context user * @throws UserBlockedError|ReadOnlyError|PermissionsError - * @return bool True on success */ protected function checkCanExecute( User $user ) { $right = $this->getRestriction(); @@ -304,11 +320,12 @@ abstract class Action { if ( $this->requiresWrite() && wfReadOnly() ) { throw new ReadOnlyError(); } - return true; } /** * Whether this action requires the wiki not to be locked + * @since 1.17 + * * @return bool */ public function requiresWrite() { @@ -317,6 +334,8 @@ abstract class Action { /** * Whether this action can still be executed by a blocked user + * @since 1.17 + * * @return bool */ public function requiresUnblock() { @@ -326,6 +345,7 @@ abstract class Action { /** * Set output headers for noindexing etc. This function will not be called through * the execute() entry point, so only put UI-related stuff in here. + * @since 1.17 */ protected function setHeaders() { $out = $this->getOutput(); @@ -346,6 +366,7 @@ abstract class Action { /** * Returns the description that goes below the \<h1\> tag + * @since 1.17 * * @return string */ @@ -357,6 +378,8 @@ abstract class Action { * The main action entry point. Do all output for display and send it to the context * output. Do not use globals $wgOut, $wgRequest, etc, in implementations; use * $this->getOutput(), etc. + * @since 1.17 + * * @throws ErrorPageError */ abstract public function show(); diff --git a/includes/actions/CreditsAction.php b/includes/actions/CreditsAction.php index dd5195dd86..e064aab4e6 100644 --- a/includes/actions/CreditsAction.php +++ b/includes/actions/CreditsAction.php @@ -100,6 +100,17 @@ class CreditsAction extends FormlessAction { $this->userLink( $user ) )->params( $user->getName() )->escaped(); } + /** + * Whether we can display the user's real name (not a hidden pref) + * + * @since 1.24 + * @return bool + */ + protected function canShowRealUserName() { + $hiddenPrefs = $this->context->getConfig()->get( 'HiddenPrefs' ); + return !in_array( 'realname', $hiddenPrefs ); + } + /** * Get a list of contributors of $article * @param int $cnt Maximum list of contributors to show @@ -107,8 +118,6 @@ class CreditsAction extends FormlessAction { * @return string Html */ protected function getContributors( $cnt, $showIfMax ) { - global $wgHiddenPrefs; - $contributors = $this->page->getContributors(); $others_link = false; @@ -132,7 +141,7 @@ class CreditsAction extends FormlessAction { $cnt--; if ( $user->isLoggedIn() ) { $link = $this->link( $user ); - if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { + if ( $this->canShowRealUserName() && $user->getRealName() ) { $real_names[] = $link; } else { $user_names[] = $link; @@ -192,8 +201,7 @@ class CreditsAction extends FormlessAction { * @return string Html */ protected function link( User $user ) { - global $wgHiddenPrefs; - if ( !in_array( 'realname', $wgHiddenPrefs ) && !$user->isAnon() ) { + if ( $this->canShowRealUserName() && !$user->isAnon() ) { $real = $user->getRealName(); } else { $real = false; @@ -216,8 +224,7 @@ class CreditsAction extends FormlessAction { if ( $user->isAnon() ) { return $this->msg( 'anonuser' )->rawParams( $link )->parse(); } else { - global $wgHiddenPrefs; - if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { + if ( $this->canShowRealUserName() && $user->getRealName() ) { return $link; } else { return $this->msg( 'siteuser' )->rawParams( $link )->params( $user->getName() )->escaped(); diff --git a/includes/actions/DeleteAction.php b/includes/actions/DeleteAction.php index 069d570063..12f0dff044 100644 --- a/includes/actions/DeleteAction.php +++ b/includes/actions/DeleteAction.php @@ -41,7 +41,13 @@ class DeleteAction extends FormlessAction { } public function show() { - + if ( $this->getContext()->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { + $out = $this->getOutput(); + $out->addModuleStyles( array( + 'mediawiki.ui.input', + 'mediawiki.ui.checkbox', + ) ); + } $this->page->delete(); } } diff --git a/includes/actions/EditAction.php b/includes/actions/EditAction.php index 31f58b8072..8876724412 100644 --- a/includes/actions/EditAction.php +++ b/includes/actions/EditAction.php @@ -1,6 +1,6 @@ <?php /** - * action=edit / action=submit handler + * action=edit handler * * Copyright © 2012 Timo Tijhof * @@ -41,6 +41,13 @@ class EditAction extends FormlessAction { } public function show() { + if ( $this->getContext()->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { + $out = $this->getOutput(); + $out->addModuleStyles( array( + 'mediawiki.ui.input', + 'mediawiki.ui.checkbox', + ) ); + } $page = $this->page; $user = $this->getUser(); @@ -50,26 +57,3 @@ class EditAction extends FormlessAction { } } } - -/** - * Edit submission handler - * - * This is the same as EditAction; except that it sets the session cookie. - * - * @ingroup Actions - */ -class SubmitAction extends EditAction { - - public function getName() { - return 'submit'; - } - - public function show() { - if ( session_id() == '' ) { - // Send a cookie so anons get talk message notifications - wfSetupSession(); - } - - parent::show(); - } -} diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 4992313a70..523da686c4 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -92,8 +92,6 @@ class HistoryAction extends FormlessAction { * Print the history page for an article. */ function onView() { - global $wgScript, $wgUseFileCache; - $out = $this->getOutput(); $request = $this->getRequest(); @@ -107,9 +105,11 @@ class HistoryAction extends FormlessAction { wfProfileIn( __METHOD__ ); $this->preCacheMessages(); + $config = $this->context->getConfig(); # Fill in the file cache if not set already - if ( $wgUseFileCache && HTMLFileCache::useFileCache( $this->getContext() ) ) { + $useFileCache = $config->get( 'UseFileCache' ); + if ( $useFileCache && HTMLFileCache::useFileCache( $this->getContext() ) ) { $cache = HTMLFileCache::newFromTitle( $this->getTitle(), 'history' ); if ( !$cache->isCacheGood( /* Assume up to date */ ) ) { ob_start( array( &$cache, 'saveToFileCache' ) ); @@ -119,6 +119,13 @@ class HistoryAction extends FormlessAction { // Setup page variables. $out->setFeedAppendQuery( 'action=history' ); $out->addModules( 'mediawiki.action.history' ); + if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) { + $out = $this->getOutput(); + $out->addModuleStyles( array( + 'mediawiki.ui.input', + 'mediawiki.ui.checkbox', + ) ); + } // Handle atom/RSS feeds. $feedType = $request->getVal( 'feed' ); @@ -173,7 +180,7 @@ class HistoryAction extends FormlessAction { } // Add the general form - $action = htmlspecialchars( $wgScript ); + $action = htmlspecialchars( wfScript() ); $out->addHTML( "<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" . Xml::fieldset( @@ -254,14 +261,14 @@ class HistoryAction extends FormlessAction { * @param string $type Feed type */ function feed( $type ) { - global $wgFeedClasses, $wgFeedLimit; if ( !FeedUtils::checkFeedOutput( $type ) ) { return; } $request = $this->getRequest(); + $feedClasses = $this->context->getConfig()->get( 'FeedClasses' ); /** @var RSSFeed|AtomFeed $feed */ - $feed = new $wgFeedClasses[$type]( + $feed = new $feedClasses[$type]( $this->getTitle()->getPrefixedText() . ' - ' . $this->msg( 'history-feed-title' )->inContentLanguage()->text(), $this->msg( 'history-feed-description' )->inContentLanguage()->text(), @@ -271,7 +278,10 @@ class HistoryAction extends FormlessAction { // Get a limit on number of feed entries. Provide a sane default // of 10 if none is defined (but limit to $wgFeedLimit max) $limit = $request->getInt( 'limit', 10 ); - $limit = min( max( $limit, 1 ), $wgFeedLimit ); + $limit = min( + max( $limit, 1 ), + $this->context->getConfig()->get( 'FeedLimit' ) + ); $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT ); @@ -462,21 +472,24 @@ class HistoryPager extends ReverseChronologicalPager { * @return string HTML output */ function getStartBody() { - global $wgScript; $this->lastRow = false; $this->counter = 1; $this->oldIdChecked = 0; $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' ); - $s = Html::openElement( 'form', array( 'action' => $wgScript, + $s = Html::openElement( 'form', array( 'action' => wfScript(), 'id' => 'mw-history-compare' ) ) . "\n"; $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n"; $s .= Html::hidden( 'action', 'historysubmit' ) . "\n"; // Button container stored in $this->buttons for re-use in getEndBody() $this->buttons = '<div>'; + $className = 'historysubmit mw-history-compareselectedversions-button'; + if ( $this->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { + $className .= ' mw-ui-button mw-ui-constructive'; + } $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(), - array( 'class' => 'historysubmit mw-history-compareselectedversions-button' ) + array( 'class' => $className ) + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' ) ) . "\n"; @@ -873,6 +886,7 @@ class HistoryPager extends ReverseChronologicalPager { /** * This is called if a write operation is possible from the generated HTML + * @param bool $enable */ function preventClickjacking( $enable = true ) { $this->preventClickjacking = $enable; diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index c2c1ff5cd4..f932a40543 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -193,13 +193,13 @@ class InfoAction extends FormlessAction { * @return array */ protected function pageInfo() { - global $wgContLang, $wgRCMaxAge, $wgMemc, $wgMiserMode, - $wgUnwatchedPageThreshold, $wgPageInfoTransclusionLimit, $wgPageLanguageUseDB; + global $wgContLang, $wgMemc; $user = $this->getUser(); $lang = $this->getLanguage(); $title = $this->getTitle(); $id = $title->getArticleID(); + $config = $this->context->getConfig(); $memcKey = wfMemcKey( 'infoaction', sha1( $title->getPrefixedText() ), $this->page->getLatest() ); @@ -207,7 +207,7 @@ class InfoAction extends FormlessAction { $version = isset( $pageCounts['cacheversion'] ) ? $pageCounts['cacheversion'] : false; if ( $pageCounts === false || $version !== self::CACHE_VERSION ) { // Get page information that would be too "expensive" to retrieve by normal means - $pageCounts = self::pageCounts( $title ); + $pageCounts = $this->pageCounts( $title ); $pageCounts['cacheversion'] = self::CACHE_VERSION; $wgMemc->set( $memcKey, $pageCounts ); @@ -276,7 +276,7 @@ class InfoAction extends FormlessAction { // Language in which the page content is (supposed to be) written $pageLang = $title->getPageLanguage()->getCode(); - if ( $wgPageLanguageUseDB && $this->getTitle()->userCan( 'pagelang' ) ) { + if ( $config->get( 'PageLanguageUseDB' ) && $this->getTitle()->userCan( 'pagelang' ) ) { // Link to Special:PageLanguage with pre-filled page title if user has permissions $titleObj = SpecialPage::getTitleFor( 'PageLanguage', $title->getPrefixedText() ); $langDisp = Linker::link( @@ -321,19 +321,20 @@ class InfoAction extends FormlessAction { ); } + $unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' ); if ( $user->isAllowed( 'unwatchedpages' ) || - ( $wgUnwatchedPageThreshold !== false && - $pageCounts['watchers'] >= $wgUnwatchedPageThreshold ) + ( $unwatchedPageThreshold !== false && + $pageCounts['watchers'] >= $unwatchedPageThreshold ) ) { // Number of page watchers $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-watchers' ), $lang->formatNum( $pageCounts['watchers'] ) ); - } elseif ( $wgUnwatchedPageThreshold !== false ) { + } elseif ( $unwatchedPageThreshold !== false ) { $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-watchers' ), - $this->msg( 'pageinfo-few-watchers' )->numParams( $wgUnwatchedPageThreshold ) + $this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold ) ); } @@ -521,7 +522,7 @@ class InfoAction extends FormlessAction { // Recent number of edits (within past 30 days) $pageInfo['header-edits'][] = array( - $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $wgRCMaxAge ) ), + $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $config->get( 'RCMaxAge' ) ) ), $lang->formatNum( $pageCounts['recent_edits'] ) ); @@ -555,9 +556,9 @@ class InfoAction extends FormlessAction { $pageCounts['transclusion']['from'] > 0 || $pageCounts['transclusion']['to'] > 0 ) { - $options = array( 'LIMIT' => $wgPageInfoTransclusionLimit ); + $options = array( 'LIMIT' => $config->get( 'PageInfoTransclusionLimit' ) ); $transcludedTemplates = $title->getTemplateLinksFrom( $options ); - if ( $wgMiserMode ) { + if ( $config->get( 'MiserMode' ) ) { $transcludedTargets = array(); } else { $transcludedTargets = $title->getTemplateLinksTo( $options ); @@ -602,7 +603,7 @@ class InfoAction extends FormlessAction { ); } - if ( !$wgMiserMode && $pageCounts['transclusion']['to'] > 0 ) { + if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) { if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) { $more = Linker::link( $whatLinksHere, @@ -635,16 +636,15 @@ class InfoAction extends FormlessAction { * @param Title $title Title to get counts for * @return array */ - protected static function pageCounts( Title $title ) { - global $wgRCMaxAge, $wgDisableCounters, $wgMiserMode; - + protected function pageCounts( Title $title ) { wfProfileIn( __METHOD__ ); $id = $title->getArticleID(); + $config = $this->context->getConfig(); $dbr = wfGetDB( DB_SLAVE ); $result = array(); - if ( !$wgDisableCounters ) { + if ( !$config->get( 'DisableCounters' ) ) { // Number of views $views = (int)$dbr->selectField( 'page', @@ -685,8 +685,8 @@ class InfoAction extends FormlessAction { ); $result['authors'] = $authors; - // "Recent" threshold defined by $wgRCMaxAge - $threshold = $dbr->timestamp( time() - $wgRCMaxAge ); + // "Recent" threshold defined by RCMaxAge setting + $threshold = $dbr->timestamp( time() - $config->get( 'RCMaxAge' ) ); // Recent number of edits $edits = (int)$dbr->selectField( @@ -740,7 +740,7 @@ class InfoAction extends FormlessAction { } // Counts for the number of transclusion links (to/from) - if ( $wgMiserMode ) { + if ( $config->get( 'MiserMode' ) ) { $result['transclusion']['to'] = 0; } else { $result['transclusion']['to'] = (int)$dbr->selectField( @@ -780,8 +780,6 @@ class InfoAction extends FormlessAction { * @return string Html */ protected function getContributors() { - global $wgHiddenPrefs; - $contributors = $this->page->getContributors(); $real_names = array(); $user_names = array(); @@ -794,9 +792,10 @@ class InfoAction extends FormlessAction { ? SpecialPage::getTitleFor( 'Contributions', $user->getName() ) : $user->getUserPage(); + $hiddenPrefs = $this->context->getConfig()->get( 'HiddenPrefs' ); if ( $user->getID() == 0 ) { $anon_ips[] = Linker::link( $page, htmlspecialchars( $user->getName() ) ); - } elseif ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { + } elseif ( !in_array( 'realname', $hiddenPrefs ) && $user->getRealName() ) { $real_names[] = Linker::link( $page, htmlspecialchars( $user->getRealName() ) ); } else { $user_names[] = Linker::link( $page, htmlspecialchars( $user->getName() ) ); diff --git a/includes/actions/ProtectAction.php b/includes/actions/ProtectAction.php index 8b2bfaa983..a7f1ac34e4 100644 --- a/includes/actions/ProtectAction.php +++ b/includes/actions/ProtectAction.php @@ -41,26 +41,15 @@ class ProtectAction extends FormlessAction { } public function show() { + if ( $this->getContext()->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { + $out = $this->getOutput(); + $out->addModuleStyles( array( + 'mediawiki.ui.input', + 'mediawiki.ui.checkbox', + ) ); + } $this->page->protect(); } } -/** - * Handle page unprotection - * - * This is a wrapper that will call Article::unprotect(). - * - * @ingroup Actions - */ -class UnprotectAction extends ProtectAction { - - public function getName() { - return 'unprotect'; - } - - public function show() { - - $this->page->unprotect(); - } -} diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index 37e6e667e8..d0d956ec36 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -33,7 +33,13 @@ * @ingroup Actions */ class RawAction extends FormlessAction { - private $mGen; + /** + * @var bool Does the request include a gen=css|javascript parameter + * @deprecated This used to be a string for "css" or "javascript" but + * it is no longer used. Setting this parameter results in empty content + * being served + */ + private $gen = false; public function getName() { return 'raw'; @@ -48,10 +54,9 @@ class RawAction extends FormlessAction { } function onView() { - global $wgSquidMaxage, $wgForcedRawSMaxage; - $this->getOutput()->disable(); $request = $this->getRequest(); + $config = $this->context->getConfig(); if ( !$request->checkUrlExtension() ) { return; @@ -67,12 +72,10 @@ class RawAction extends FormlessAction { $smaxage = $request->getIntOrNull( 'smaxage' ); if ( $gen == 'css' || $gen == 'js' ) { - $this->mGen = $gen; + $this->gen = true; if ( $smaxage === null ) { - $smaxage = $wgSquidMaxage; + $smaxage = $config->get( 'SquidMaxage' ); } - } else { - $this->mGen = false; } $contentType = $this->getContentType(); @@ -81,13 +84,13 @@ class RawAction extends FormlessAction { # Note: If using a canonical url for userpage css/js, we send an HTCP purge. if ( $smaxage === null ) { if ( $contentType == 'text/css' || $contentType == 'text/javascript' ) { - $smaxage = intval( $wgForcedRawSMaxage ); + $smaxage = intval( $config->get( 'ForcedRawSMaxage' ) ); } else { $smaxage = 0; } } - $maxage = $request->getInt( 'maxage', $wgSquidMaxage ); + $maxage = $request->getInt( 'maxage', $config->get( 'SquidMaxage' ) ); $response = $request->response(); @@ -131,7 +134,7 @@ class RawAction extends FormlessAction { global $wgParser; # No longer used - if ( $this->mGen ) { + if ( $this->gen ) { return ''; } diff --git a/includes/actions/SubmitAction.php b/includes/actions/SubmitAction.php new file mode 100644 index 0000000000..fae49f61e3 --- /dev/null +++ b/includes/actions/SubmitAction.php @@ -0,0 +1,42 @@ +<?php +/** + * Wrapper for EditAction; sets the session cookie. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * @file + * @ingroup Actions + */ + +/** + * This is the same as EditAction; except that it sets the session cookie. + * + * @ingroup Actions + */ +class SubmitAction extends EditAction { + + public function getName() { + return 'submit'; + } + + public function show() { + if ( session_id() === '' ) { + // Send a cookie so anons get talk message notifications + wfSetupSession(); + } + + parent::show(); + } +} diff --git a/includes/actions/UnprotectAction.php b/includes/actions/UnprotectAction.php new file mode 100644 index 0000000000..bc28c8ed17 --- /dev/null +++ b/includes/actions/UnprotectAction.php @@ -0,0 +1,43 @@ +<?php +/** + * action=unprotect handler + * + * Copyright © 2012 Timo Tijhof + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * @file + * @ingroup Actions + * @author Timo Tijhof + */ + +/** + * Handle page unprotection + * + * This is a wrapper that will call Article::unprotect(). + * + * @ingroup Actions + */ +class UnprotectAction extends ProtectAction { + + public function getName() { + return 'unprotect'; + } + + public function show() { + + $this->page->unprotect(); + } +} diff --git a/includes/actions/UnwatchAction.php b/includes/actions/UnwatchAction.php new file mode 100644 index 0000000000..e2e5a1d843 --- /dev/null +++ b/includes/actions/UnwatchAction.php @@ -0,0 +1,57 @@ +<?php +/** + * Performs the unwatch actions on a page + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * @file + * @ingroup Actions + */ + +/** + * Page removal from a user's watchlist + * + * @ingroup Actions + */ +class UnwatchAction extends WatchAction { + + public function getName() { + return 'unwatch'; + } + + protected function getDescription() { + return $this->msg( 'removewatch' )->escaped(); + } + + public function onSubmit( $data ) { + wfProfileIn( __METHOD__ ); + self::doUnwatch( $this->getTitle(), $this->getUser() ); + wfProfileOut( __METHOD__ ); + + return true; + } + + protected function alterForm( HTMLForm $form ) { + $form->setSubmitTextMsg( 'confirm-unwatch-button' ); + } + + protected function preText() { + return $this->msg( 'confirm-unwatch-top' )->parse(); + } + + public function onSuccess() { + $this->getOutput()->addWikiMsg( 'removedwatchtext', $this->getTitle()->getPrefixedText() ); + } +} diff --git a/includes/actions/WatchAction.php b/includes/actions/WatchAction.php index 2c7502e574..8c9a46a5b9 100644 --- a/includes/actions/WatchAction.php +++ b/includes/actions/WatchAction.php @@ -1,6 +1,6 @@ <?php /** - * Performs the watch and unwatch actions on a page + * Performs the watch actions on a page * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -82,17 +82,10 @@ class WatchAction extends FormAction { protected function checkCanExecute( User $user ) { // Must be logged in if ( $user->isAnon() ) { - $loginreqlink = Linker::linkKnown( - SpecialPage::getTitleFor( 'Userlogin' ), - $this->msg( 'loginreqlink' )->escaped(), - array(), - array( 'returnto' => $this->getPageTitle(), 'returntoquery' => 'action=' . $this->getName() ) - ); - $reasonMsg = $this->msg( 'watchlistanontext' )->rawParams( $loginreqlink ); - throw new UserNotLoggedIn( $reasonMsg, 'watchnologin' ); + throw new UserNotLoggedIn( 'watchlistanontext', 'watchnologin' ); } - return parent::checkCanExecute( $user ); + parent::checkCanExecute( $user ); } /** @@ -185,7 +178,7 @@ class WatchAction extends FormAction { if ( $action != 'unwatch' ) { $action = 'watch'; } - $salt = array( $action, $title->getDBkey() ); + $salt = array( $action, $title->getPrefixedDBkey() ); // This token stronger salted and not compatible with ApiWatch // It's title/action specific because index.php is GET and API is POST @@ -217,39 +210,3 @@ class WatchAction extends FormAction { $this->getOutput()->addWikiMsg( 'addedwatchtext', $this->getTitle()->getPrefixedText() ); } } - -/** - * Page removal from a user's watchlist - * - * @ingroup Actions - */ -class UnwatchAction extends WatchAction { - - public function getName() { - return 'unwatch'; - } - - protected function getDescription() { - return $this->msg( 'removewatch' )->escaped(); - } - - public function onSubmit( $data ) { - wfProfileIn( __METHOD__ ); - self::doUnwatch( $this->getTitle(), $this->getUser() ); - wfProfileOut( __METHOD__ ); - - return true; - } - - protected function alterForm( HTMLForm $form ) { - $form->setSubmitTextMsg( 'confirm-unwatch-button' ); - } - - protected function preText() { - return $this->msg( 'confirm-unwatch-top' )->parse(); - } - - public function onSuccess() { - $this->getOutput()->addWikiMsg( 'removedwatchtext', $this->getTitle()->getPrefixedText() ); - } -} diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 7ebd0c38d1..eafa9ccd1e 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -109,9 +109,11 @@ abstract class ApiBase extends ContextSource { } } - /***************************************************************************** - * ABSTRACT METHODS * - *****************************************************************************/ + + /************************************************************************//** + * @name Methods to implement + * @{ + */ /** * Evaluates the parameters, performs the requested query, and sets up @@ -132,436 +134,232 @@ abstract class ApiBase extends ContextSource { abstract public function execute(); /** - * Returns a string that identifies the version of the extending class. - * Typically includes the class name, the svn revision, timestamp, and - * last author. Usually done with SVN's Id keyword - * @return string - * @deprecated since 1.21, version string is no longer supported + * Get the module manager, or null if this module has no sub-modules + * @since 1.21 + * @return ApiModuleManager */ - public function getVersion() { - wfDeprecated( __METHOD__, '1.21' ); + public function getModuleManager() { + return null; + } - return ''; + /** + * If the module may only be used with a certain format module, + * it should override this method to return an instance of that formatter. + * A value of null means the default format will be used. + * @return mixed Instance of a derived class of ApiFormatBase, or null + */ + public function getCustomPrinter() { + return null; } /** - * Get the name of the module being executed by this instance - * @return string + * Returns the description string for this module + * @return string|array */ - public function getModuleName() { - return $this->mModuleName; + protected function getDescription() { + return false; } /** - * Get the module manager, or null if this module has no sub-modules - * @since 1.21 - * @return ApiModuleManager + * Returns usage examples for this module. Return false if no examples are available. + * @return bool|string|array */ - public function getModuleManager() { - return null; + protected function getExamples() { + return false; } /** - * Get parameter prefix (usually two letters or an empty string). - * @return string + * @return bool|string|array Returns a false if the module has no help URL, + * else returns a (array of) string */ - public function getModulePrefix() { - return $this->mModulePrefix; + public function getHelpUrls() { + return false; } /** - * Get the name of the module as shown in the profiler log + * Returns an array of allowed parameters (parameter name) => (default + * value) or (parameter name) => (array with PARAM_* constants as keys) + * Don't call this function directly: use getFinalParams() to allow + * hooks to modify parameters as needed. * - * @param DatabaseBase|bool $db + * Some derived classes may choose to handle an integer $flags parameter + * in the overriding methods. Callers of this method can pass zero or + * more OR-ed flags like GET_VALUES_FOR_HELP. * - * @return string + * @return array|bool */ - public function getModuleProfileName( $db = false ) { - if ( $db ) { - return 'API:' . $this->mModuleName . '-DB'; - } - - return 'API:' . $this->mModuleName; + protected function getAllowedParams( /* $flags = 0 */ ) { + // int $flags is not declared because it causes "Strict standards" + // warning. Most derived classes do not implement it. + return false; } /** - * Get the main module - * @return ApiMain + * Returns an array of parameter descriptions. + * Don't call this function directly: use getFinalParamDescription() to + * allow hooks to modify descriptions as needed. + * @return array|bool False on no parameter descriptions */ - public function getMain() { - return $this->mMainModule; + protected function getParamDescription() { + return false; } /** - * Returns true if this module is the main module ($this === $this->mMainModule), - * false otherwise. + * Indicates if this module needs maxlag to be checked * @return bool */ - public function isMain() { - return $this === $this->mMainModule; + public function shouldCheckMaxlag() { + return true; } /** - * Get the result object - * @return ApiResult + * Indicates whether this module requires read rights + * @return bool */ - public function getResult() { - // Main module has getResult() method overridden - // Safety - avoid infinite loop: - if ( $this->isMain() ) { - ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' ); - } - - return $this->getMain()->getResult(); + public function isReadMode() { + return true; } /** - * Get the result data array (read-only) - * @return array + * Indicates whether this module requires write mode + * @return bool */ - public function getResultData() { - return $this->getResult()->getData(); + public function isWriteMode() { + return false; } /** - * Set warning section for this module. Users should monitor this - * section to notice any changes in API. Multiple calls to this - * function will result in the warning messages being separated by - * newlines - * @param string $warning Warning message + * Indicates whether this module must be called with a POST request + * @return bool */ - public function setWarning( $warning ) { - $result = $this->getResult(); - $data = $result->getData(); - $moduleName = $this->getModuleName(); - if ( isset( $data['warnings'][$moduleName] ) ) { - // Don't add duplicate warnings - $oldWarning = $data['warnings'][$moduleName]['*']; - $warnPos = strpos( $oldWarning, $warning ); - // If $warning was found in $oldWarning, check if it starts at 0 or after "\n" - if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) { - // Check if $warning is followed by "\n" or the end of the $oldWarning - $warnPos += strlen( $warning ); - if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) { - return; - } - } - // If there is a warning already, append it to the existing one - $warning = "$oldWarning\n$warning"; - } - $msg = array(); - ApiResult::setContent( $msg, $warning ); - $result->addValue( 'warnings', $moduleName, - $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + public function mustBePosted() { + return $this->needsToken() !== false; } /** - * If the module may only be used with a certain format module, - * it should override this method to return an instance of that formatter. - * A value of null means the default format will be used. - * @return mixed Instance of a derived class of ApiFormatBase, or null + * Returns the token type this module requires in order to execute. + * + * Modules are strongly encouraged to use the core 'csrf' type unless they + * have specialized security needs. If the token type is not one of the + * core types, you must use the ApiQueryTokensRegisterTypes hook to + * register it. + * + * Returning a non-falsey value here will cause self::getFinalParams() to + * return a required string 'token' parameter and + * self::getFinalParamDescription() to ensure there is standardized + * documentation for it. Also, self::mustBePosted() must return true when + * tokens are used. + * + * In previous versions of MediaWiki, true was a valid return value. + * Returning true will generate errors indicating that the API module needs + * updating. + * + * @return string|false */ - public function getCustomPrinter() { - return null; + public function needsToken() { + return false; } /** - * Generates help message for this module, or false if there is no description - * @return string|bool + * Fetch the salt used in the Web UI corresponding to this module. + * + * Only override this if the Web UI uses a token with a non-constant salt. + * + * @since 1.24 + * @param array $params All supplied parameters for the module + * @return string|array|null */ - public function makeHelpMsg() { - static $lnPrfx = "\n "; - - $msg = $this->getFinalDescription(); - - if ( $msg !== false ) { - - if ( !is_array( $msg ) ) { - $msg = array( - $msg - ); - } - $msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n"; - - $msg .= $this->makeHelpArrayToString( $lnPrfx, false, $this->getHelpUrls() ); - - if ( $this->isReadMode() ) { - $msg .= "\nThis module requires read rights"; - } - if ( $this->isWriteMode() ) { - $msg .= "\nThis module requires write rights"; - } - if ( $this->mustBePosted() ) { - $msg .= "\nThis module only accepts POST requests"; - } - if ( $this->isReadMode() || $this->isWriteMode() || - $this->mustBePosted() - ) { - $msg .= "\n"; - } - - // Parameters - $paramsMsg = $this->makeHelpMsgParameters(); - if ( $paramsMsg !== false ) { - $msg .= "Parameters:\n$paramsMsg"; - } + protected function getWebUITokenSalt( array $params ) { + return null; + } - $examples = $this->getExamples(); - if ( $examples ) { - if ( !is_array( $examples ) ) { - $examples = array( - $examples - ); - } - $msg .= "Example" . ( count( $examples ) > 1 ? 's' : '' ) . ":\n"; - foreach ( $examples as $k => $v ) { - if ( is_numeric( $k ) ) { - $msg .= " $v\n"; - } else { - if ( is_array( $v ) ) { - $msgExample = implode( "\n", array_map( array( $this, 'indentExampleText' ), $v ) ); - } else { - $msgExample = " $v"; - } - $msgExample .= ":"; - $msg .= wordwrap( $msgExample, 100, "\n" ) . "\n $k\n"; - } - } - } - } + /**@}*/ - return $msg; - } + /************************************************************************//** + * @name Data access methods + * @{ + */ /** - * @param string $item + * Get the name of the module being executed by this instance * @return string */ - private function indentExampleText( $item ) { - return " " . $item; + public function getModuleName() { + return $this->mModuleName; } /** - * @param string $prefix Text to split output items - * @param string $title What is being output - * @param string|array $input + * Get parameter prefix (usually two letters or an empty string). * @return string */ - protected function makeHelpArrayToString( $prefix, $title, $input ) { - if ( $input === false ) { - return ''; - } - if ( !is_array( $input ) ) { - $input = array( $input ); - } - - if ( count( $input ) > 0 ) { - if ( $title ) { - $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n "; - } else { - $msg = ' '; - } - $msg .= implode( $prefix, $input ) . "\n"; - - return $msg; - } - - return ''; + public function getModulePrefix() { + return $this->mModulePrefix; } /** - * Generates the parameter descriptions for this module, to be displayed in the - * module's help. - * @return string|bool + * Get the main module + * @return ApiMain */ - public function makeHelpMsgParameters() { - $params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP ); - if ( $params ) { - - $paramsDescription = $this->getFinalParamDescription(); - $msg = ''; - $paramPrefix = "\n" . str_repeat( ' ', 24 ); - $descWordwrap = "\n" . str_repeat( ' ', 28 ); - foreach ( $params as $paramName => $paramSettings ) { - $desc = isset( $paramsDescription[$paramName] ) ? $paramsDescription[$paramName] : ''; - if ( is_array( $desc ) ) { - $desc = implode( $paramPrefix, $desc ); - } - - //handle shorthand - if ( !is_array( $paramSettings ) ) { - $paramSettings = array( - self::PARAM_DFLT => $paramSettings, - ); - } - - //handle missing type - if ( !isset( $paramSettings[ApiBase::PARAM_TYPE] ) ) { - $dflt = isset( $paramSettings[ApiBase::PARAM_DFLT] ) - ? $paramSettings[ApiBase::PARAM_DFLT] - : null; - if ( is_bool( $dflt ) ) { - $paramSettings[ApiBase::PARAM_TYPE] = 'boolean'; - } elseif ( is_string( $dflt ) || is_null( $dflt ) ) { - $paramSettings[ApiBase::PARAM_TYPE] = 'string'; - } elseif ( is_int( $dflt ) ) { - $paramSettings[ApiBase::PARAM_TYPE] = 'integer'; - } - } - - if ( isset( $paramSettings[self::PARAM_DEPRECATED] ) - && $paramSettings[self::PARAM_DEPRECATED] - ) { - $desc = "DEPRECATED! $desc"; - } - - if ( isset( $paramSettings[self::PARAM_REQUIRED] ) - && $paramSettings[self::PARAM_REQUIRED] - ) { - $desc .= $paramPrefix . "This parameter is required"; - } - - $type = isset( $paramSettings[self::PARAM_TYPE] ) - ? $paramSettings[self::PARAM_TYPE] - : null; - if ( isset( $type ) ) { - $hintPipeSeparated = true; - $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) - ? $paramSettings[self::PARAM_ISMULTI] - : false; - if ( $multi ) { - $prompt = 'Values (separate with \'|\'): '; - } else { - $prompt = 'One value: '; - } - - if ( is_array( $type ) ) { - $choices = array(); - $nothingPrompt = ''; - foreach ( $type as $t ) { - if ( $t === '' ) { - $nothingPrompt = 'Can be empty, or '; - } else { - $choices[] = $t; - } - } - $desc .= $paramPrefix . $nothingPrompt . $prompt; - $choicesstring = implode( ', ', $choices ); - $desc .= wordwrap( $choicesstring, 100, $descWordwrap ); - $hintPipeSeparated = false; - } else { - switch ( $type ) { - case 'namespace': - // Special handling because namespaces are - // type-limited, yet they are not given - $desc .= $paramPrefix . $prompt; - $desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ), - 100, $descWordwrap ); - $hintPipeSeparated = false; - break; - case 'limit': - $desc .= $paramPrefix . "No more than {$paramSettings[self::PARAM_MAX]}"; - if ( isset( $paramSettings[self::PARAM_MAX2] ) ) { - $desc .= " ({$paramSettings[self::PARAM_MAX2]} for bots)"; - } - $desc .= ' allowed'; - break; - case 'integer': - $s = $multi ? 's' : ''; - $hasMin = isset( $paramSettings[self::PARAM_MIN] ); - $hasMax = isset( $paramSettings[self::PARAM_MAX] ); - if ( $hasMin || $hasMax ) { - if ( !$hasMax ) { - $intRangeStr = "The value$s must be no less than " . - "{$paramSettings[self::PARAM_MIN]}"; - } elseif ( !$hasMin ) { - $intRangeStr = "The value$s must be no more than " . - "{$paramSettings[self::PARAM_MAX]}"; - } else { - $intRangeStr = "The value$s must be between " . - "{$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; - } - - $desc .= $paramPrefix . $intRangeStr; - } - break; - case 'upload': - $desc .= $paramPrefix . "Must be posted as a file upload using multipart/form-data"; - break; - } - } - - if ( $multi ) { - if ( $hintPipeSeparated ) { - $desc .= $paramPrefix . "Separate values with '|'"; - } - - $isArray = is_array( $type ); - if ( !$isArray - || $isArray && count( $type ) > self::LIMIT_SML1 - ) { - $desc .= $paramPrefix . "Maximum number of values " . - self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)"; - } - } - } - - $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null; - if ( !is_null( $default ) && $default !== false ) { - $desc .= $paramPrefix . "Default: $default"; - } + public function getMain() { + return $this->mMainModule; + } - $msg .= sprintf( " %-19s - %s\n", $this->encodeParamName( $paramName ), $desc ); - } + /** + * Returns true if this module is the main module ($this === $this->mMainModule), + * false otherwise. + * @return bool + */ + public function isMain() { + return $this === $this->mMainModule; + } - return $msg; + /** + * Get the result object + * @return ApiResult + */ + public function getResult() { + // Main module has getResult() method overridden + // Safety - avoid infinite loop: + if ( $this->isMain() ) { + ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' ); } - return false; + return $this->getMain()->getResult(); } /** - * Returns the description string for this module - * @return string|array + * Get the result data array (read-only) + * @return array */ - protected function getDescription() { - return false; + public function getResultData() { + return $this->getResult()->getData(); } /** - * Returns usage examples for this module. Return false if no examples are available. - * @return bool|string|array + * Gets a default slave database connection object + * @return DatabaseBase */ - protected function getExamples() { - return false; + protected function getDB() { + if ( !isset( $this->mSlaveDB ) ) { + $this->profileDBIn(); + $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' ); + $this->profileDBOut(); + } + + return $this->mSlaveDB; } /** - * Returns an array of allowed parameters (parameter name) => (default - * value) or (parameter name) => (array with PARAM_* constants as keys) - * Don't call this function directly: use getFinalParams() to allow - * hooks to modify parameters as needed. - * - * Some derived classes may choose to handle an integer $flags parameter - * in the overriding methods. Callers of this method can pass zero or - * more OR-ed flags like GET_VALUES_FOR_HELP. + * Get final module description, after hooks have had a chance to tweak it as + * needed. * - * @return array|bool + * @return array|bool False on no parameters */ - protected function getAllowedParams( /* $flags = 0 */ ) { - // int $flags is not declared because it causes "Strict standards" - // warning. Most derived classes do not implement it. - return false; - } + public function getFinalDescription() { + $desc = $this->getDescription(); + wfRunHooks( 'APIGetDescription', array( &$this, &$desc ) ); - /** - * Returns an array of parameter descriptions. - * Don't call this function directly: use getFinalParamDescription() to - * allow hooks to modify descriptions as needed. - * @return array|bool False on no parameter descriptions - */ - protected function getParamDescription() { - return false; + return $desc; } /** @@ -574,6 +372,14 @@ abstract class ApiBase extends ContextSource { */ public function getFinalParams( $flags = 0 ) { $params = $this->getAllowedParams( $flags ); + + if ( $this->needsToken() ) { + $params['token'] = array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ); + } + wfRunHooks( 'APIGetAllowedParams', array( &$this, &$params, $flags ) ); return $params; @@ -587,72 +393,33 @@ abstract class ApiBase extends ContextSource { */ public function getFinalParamDescription() { $desc = $this->getParamDescription(); - wfRunHooks( 'APIGetParamDescription', array( &$this, &$desc ) ); - - return $desc; - } - - /** - * Returns possible properties in the result, grouped by the value of the prop parameter - * that shows them. - * - * Properties that are shown always are in a group with empty string as a key. - * Properties that can be shown by several values of prop are included multiple times. - * If some properties are part of a list and some are on the root object (see ApiQueryQueryPage), - * those on the root object are under the key PROP_ROOT. - * The array can also contain a boolean under the key PROP_LIST, - * indicating whether the result is a list. - * - * Don't call this function directly: use getFinalResultProperties() to - * allow hooks to modify descriptions as needed. - * - * @return array|bool False on no properties - */ - protected function getResultProperties() { - return false; - } - - /** - * Get final possible result properties, after hooks have had a chance to tweak it as - * needed. - * - * @return array - */ - public function getFinalResultProperties() { - $properties = $this->getResultProperties(); - wfRunHooks( 'APIGetResultProperties', array( $this, &$properties ) ); - return $properties; - } - - /** - * Add token properties to the array used by getResultProperties, - * based on a token functions mapping. - * @param array $props - * @param array $tokenFunctions - */ - protected static function addTokenProperties( &$props, $tokenFunctions ) { - foreach ( array_keys( $tokenFunctions ) as $token ) { - $props[''][$token . 'token'] = array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true + $tokenType = $this->needsToken(); + if ( $tokenType ) { + if ( !isset( $desc['token'] ) ) { + $desc['token'] = array(); + } elseif ( !is_array( $desc['token'] ) ) { + // We ignore a plain-string token, because it's probably an + // extension that is supplying the string for BC. + $desc['token'] = array(); + } + array_unshift( $desc['token'], + "A '$tokenType' token retrieved from action=query&meta=tokens" ); } - } - /** - * Get final module description, after hooks have had a chance to tweak it as - * needed. - * - * @return array|bool False on no parameters - */ - public function getFinalDescription() { - $desc = $this->getDescription(); - wfRunHooks( 'APIGetDescription', array( &$this, &$desc ) ); + wfRunHooks( 'APIGetParamDescription', array( &$this, &$desc ) ); return $desc; } + /**@}*/ + + /************************************************************************//** + * @name Parameter handling + * @{ + */ + /** * This method mangles parameter name based on the prefix supplied to the constructor. * Override this method to change parameter name during runtime @@ -706,8 +473,6 @@ abstract class ApiBase extends ContextSource { /** * Die if none or more than one of a certain set of parameters is set and not false. * - * Call getRequireOnlyOneParameterErrorMessages() to get a list of possible errors. - * * @param array $params User provided set of parameters, as from $this->extractRequestParams() * @param string $required,... Names of parameters of which exactly one must be set */ @@ -731,33 +496,9 @@ abstract class ApiBase extends ContextSource { } } - /** - * Generates the possible errors requireOnlyOneParameter() can die with - * - * @param array $params - * @return array - */ - public function getRequireOnlyOneParameterErrorMessages( $params ) { - $p = $this->getModulePrefix(); - $params = implode( ", {$p}", $params ); - - return array( - array( - 'code' => "{$p}missingparam", - 'info' => "One of the parameters {$p}{$params} is required" - ), - array( - 'code' => "{$p}invalidparammix", - 'info' => "The parameters {$p}{$params} can not be used together" - ) - ); - } - /** * Die if more than one of a certain set of parameters is set and not false. * - * Call getRequireMaxOneParameterErrorMessages() to get a list of possible errors. - * * @param array $params User provided set of parameters, as from $this->extractRequestParams() * @param string $required,... Names of parameters of which at most one must be set */ @@ -777,29 +518,9 @@ abstract class ApiBase extends ContextSource { } } - /** - * Generates the possible error requireMaxOneParameter() can die with - * - * @param array $params - * @return array - */ - public function getRequireMaxOneParameterErrorMessages( $params ) { - $p = $this->getModulePrefix(); - $params = implode( ", {$p}", $params ); - - return array( - array( - 'code' => "{$p}invalidparammix", - 'info' => "The parameters {$p}{$params} can not be used together" - ) - ); - } - /** * Die if none of a certain set of parameters is set and not false. * - * Call getRequireAtLeastOneParameterErrorMessages() to get a list of possible errors. - * * @since 1.23 * @param array $params User provided set of parameters, as from $this->extractRequestParams() * @param string $required,... Names of parameters of which at least one must be set @@ -821,30 +542,19 @@ abstract class ApiBase extends ContextSource { } /** - * Generates the possible errors requireAtLeastOneParameter() can die with + * Callback function used in requireOnlyOneParameter to check whether required parameters are set * - * @since 1.23 - * @param array $params Array of parameter key names - * @return array + * @param object $x Parameter to check is not null/false + * @return bool */ - public function getRequireAtLeastOneParameterErrorMessages( $params ) { - $p = $this->getModulePrefix(); - $params = implode( ", {$p}", $params ); - - return array( - array( - 'code' => "{$p}missingparam", - 'info' => "At least one of the parameters {$p}{$params} is required", - ), - ); + private function parameterNotEmpty( $x ) { + return !is_null( $x ) && $x !== false; } /** * Get a WikiPage object from a title or pageid param, if possible. * Can die, if no param is set or if the title or page id is not valid. * - * Call getTitleOrPageIdErrorMessage() to get a list of possible errors. - * * @param array $params * @param bool|string $load Whether load the object's state from the database: * - false: don't load (if the pageid is given, it will still be loaded) @@ -881,32 +591,6 @@ abstract class ApiBase extends ContextSource { return $pageObj; } - /** - * Generates the possible error getTitleOrPageId() can die with - * - * @return array - */ - public function getTitleOrPageIdErrorMessage() { - return array_merge( - $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), - array( - array( 'invalidtitle', 'title' ), - array( 'nosuchpageid', 'pageid' ), - array( 'code' => 'pagecannotexist', 'info' => "Namespace doesn't allow actual pages" ), - ) - ); - } - - /** - * Callback function used in requireOnlyOneParameter to check whether required parameters are set - * - * @param object $x Parameter to check is not null/false - * @return bool - */ - private function parameterNotEmpty( $x ) { - return !is_null( $x ) && $x !== false; - } - /** * Return true if we're to watch the page, false if not, null if no change. * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange' @@ -948,21 +632,6 @@ abstract class ApiBase extends ContextSource { } } - /** - * Set a watch (or unwatch) based the based on a watchlist parameter. - * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange' - * @param Title $titleObj The article's title to change - * @param string $userOption The user option to consider when $watch=preferences - */ - protected function setWatch( $watch, $titleObj, $userOption = null ) { - $value = $this->getWatchlistValue( $watch, $titleObj, $userOption ); - if ( $value === null ) { - return; - } - - WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() ); - } - /** * Using the settings determine the value for the given parameter * @@ -1056,6 +725,9 @@ abstract class ApiBase extends ContextSource { if ( isset( $value ) && $type == 'namespace' ) { $type = MWNamespace::getValidNamespaces(); } + if ( isset( $value ) && $type == 'submodule' ) { + $type = $this->getModuleManager()->getNames( $paramName ); + } } if ( isset( $value ) && ( $multi || is_array( $type ) ) ) { @@ -1255,7 +927,7 @@ abstract class ApiBase extends ContextSource { * @param int $botMax Maximum value for sysops/bots * @param bool $enforceLimits Whether to enforce (die) if value is outside limits */ - function validateLimit( $paramName, &$value, $min, $max, $botMax = null, $enforceLimits = false ) { + protected function validateLimit( $paramName, &$value, $min, $max, $botMax = null, $enforceLimits = false ) { if ( !is_null( $min ) && $value < $min ) { $msg = $this->encodeParamName( $paramName ) . " may not be less than $min (set to $value)"; @@ -1293,7 +965,7 @@ abstract class ApiBase extends ContextSource { * @param string $encParamName Parameter name * @return string Validated and normalized parameter */ - function validateTimestamp( $value, $encParamName ) { + protected function validateTimestamp( $value, $encParamName ) { $unixTimestamp = wfTimestamp( TS_UNIX, $value ); if ( $unixTimestamp === false ) { $this->dieUsage( @@ -1305,6 +977,44 @@ abstract class ApiBase extends ContextSource { return wfTimestamp( TS_MW, $unixTimestamp ); } + /** + * Validate the supplied token. + * + * @since 1.24 + * @param string $token Supplied token + * @param array $params All supplied parameters for the module + * @return bool + */ + public final function validateToken( $token, array $params ) { + $tokenType = $this->needsToken(); + $salts = ApiQueryTokens::getTokenTypeSalts(); + if ( !isset( $salts[$tokenType] ) ) { + throw new MWException( + "Module '{$this->getModuleName()}' tried to use token type '$tokenType' " . + 'without registering it' + ); + } + + if ( $this->getUser()->matchEditToken( + $token, + $salts[$tokenType], + $this->getRequest() + ) ) { + return true; + } + + $webUiSalt = $this->getWebUITokenSalt( $params ); + if ( $webUiSalt !== null && $this->getUser()->matchEditToken( + $token, + $webUiSalt, + $this->getRequest() + ) ) { + return true; + } + + return false; + } + /** * Validate and normalize of parameters of type 'user' * @param string $value Parameter value @@ -1323,6 +1033,115 @@ abstract class ApiBase extends ContextSource { return $title->getText(); } + /**@}*/ + + /************************************************************************//** + * @name Utility methods + * @{ + */ + + /** + * Set a watch (or unwatch) based the based on a watchlist parameter. + * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange' + * @param Title $titleObj The article's title to change + * @param string $userOption The user option to consider when $watch=preferences + */ + protected function setWatch( $watch, $titleObj, $userOption = null ) { + $value = $this->getWatchlistValue( $watch, $titleObj, $userOption ); + if ( $value === null ) { + return; + } + + WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() ); + } + + /** + * Truncate an array to a certain length. + * @param array $arr Array to truncate + * @param int $limit Maximum length + * @return bool True if the array was truncated, false otherwise + */ + public static function truncateArray( &$arr, $limit ) { + $modified = false; + while ( count( $arr ) > $limit ) { + array_pop( $arr ); + $modified = true; + } + + return $modified; + } + + /** + * Gets the user for whom to get the watchlist + * + * @param array $params + * @return User + */ + public function getWatchlistUser( $params ) { + if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) { + $user = User::newFromName( $params['owner'], false ); + if ( !( $user && $user->getId() ) ) { + $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' ); + } + $token = $user->getOption( 'watchlisttoken' ); + if ( $token == '' || $token != $params['token'] ) { + $this->dieUsage( + 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', + 'bad_wltoken' + ); + } + } else { + if ( !$this->getUser()->isLoggedIn() ) { + $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); + } + if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) { + $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' ); + } + $user = $this->getUser(); + } + + return $user; + } + + /**@}*/ + + /************************************************************************//** + * @name Warning and error reporting + * @{ + */ + + /** + * Set warning section for this module. Users should monitor this + * section to notice any changes in API. Multiple calls to this + * function will result in the warning messages being separated by + * newlines + * @param string $warning Warning message + */ + public function setWarning( $warning ) { + $result = $this->getResult(); + $data = $result->getData(); + $moduleName = $this->getModuleName(); + if ( isset( $data['warnings'][$moduleName] ) ) { + // Don't add duplicate warnings + $oldWarning = $data['warnings'][$moduleName]['*']; + $warnPos = strpos( $oldWarning, $warning ); + // If $warning was found in $oldWarning, check if it starts at 0 or after "\n" + if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) { + // Check if $warning is followed by "\n" or the end of the $oldWarning + $warnPos += strlen( $warning ); + if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) { + return; + } + } + // If there is a warning already, append it to the existing one + $warning = "$oldWarning\n$warning"; + } + $msg = array(); + ApiResult::setContent( $msg, $warning ); + $result->addValue( 'warnings', $moduleName, + $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + } + /** * Adds a warning to the output, else dies * @@ -1337,22 +1156,6 @@ abstract class ApiBase extends ContextSource { $this->setWarning( $msg ); } - /** - * Truncate an array to a certain length. - * @param array $arr Array to truncate - * @param int $limit Maximum length - * @return bool True if the array was truncated, false otherwise - */ - public static function truncateArray( &$arr, $limit ) { - $modified = false; - while ( count( $arr ) > $limit ) { - array_pop( $arr ); - $modified = true; - } - - return $modified; - } - /** * Throw a UsageException, which will (if uncaught) call the main module's * error handler and die with an error message. @@ -1407,7 +1210,7 @@ abstract class ApiBase extends ContextSource { $msg = wfMessage( $code, $errors[0] ); } if ( isset( ApiBase::$messageMap[$code] ) ) { - // Translate message to code, for backwards compatability + // Translate message to code, for backwards compatibility $code = ApiBase::$messageMap[$code]['code']; } @@ -1469,6 +1272,10 @@ abstract class ApiBase extends ContextSource { 'code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it" ), + 'deleteprotected' => array( + 'code' => 'cantedit', + 'info' => "You can't delete this page because it has been protected" + ), 'badaccess-group0' => array( 'code' => 'permissiondenied', 'info' => "Permission denied" @@ -1864,6 +1671,10 @@ abstract class ApiBase extends ContextSource { 'code' => 'undofailure', 'info' => 'Undo failed due to conflicting intermediate edits' ), + 'content-not-allowed-here' => array( + 'code' => 'contentnotallowedhere', + 'info' => 'Content model "$1" is not allowed at title "$2"' + ), // Messages from WikiPage::doEit() 'edit-hook-aborted' => array( @@ -1996,209 +1807,313 @@ abstract class ApiBase extends ContextSource { ); } - // If the key isn't present, throw an "unknown error" - return $this->parseMsg( array( 'unknownerror', $key ) ); - } + // If the key isn't present, throw an "unknown error" + return $this->parseMsg( array( 'unknownerror', $key ) ); + } + + /** + * Internal code errors should be reported with this method + * @param string $method Method or function name + * @param string $message Error message + * @throws MWException + */ + protected static function dieDebug( $method, $message ) { + throw new MWException( "Internal error in $method: $message" ); + } + + /**@}*/ + + /************************************************************************//** + * @name Help message generation + * @{ + */ + + /** + * Generates help message for this module, or false if there is no description + * @return string|bool + */ + public function makeHelpMsg() { + static $lnPrfx = "\n "; + + $msg = $this->getFinalDescription(); + + if ( $msg !== false ) { + + if ( !is_array( $msg ) ) { + $msg = array( + $msg + ); + } + $msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n"; + + $msg .= $this->makeHelpArrayToString( $lnPrfx, false, $this->getHelpUrls() ); + + if ( $this->isReadMode() ) { + $msg .= "\nThis module requires read rights"; + } + if ( $this->isWriteMode() ) { + $msg .= "\nThis module requires write rights"; + } + if ( $this->mustBePosted() ) { + $msg .= "\nThis module only accepts POST requests"; + } + if ( $this->isReadMode() || $this->isWriteMode() || + $this->mustBePosted() + ) { + $msg .= "\n"; + } + + // Parameters + $paramsMsg = $this->makeHelpMsgParameters(); + if ( $paramsMsg !== false ) { + $msg .= "Parameters:\n$paramsMsg"; + } + + $examples = $this->getExamples(); + if ( $examples ) { + if ( !is_array( $examples ) ) { + $examples = array( + $examples + ); + } + $msg .= "Example" . ( count( $examples ) > 1 ? 's' : '' ) . ":\n"; + foreach ( $examples as $k => $v ) { + if ( is_numeric( $k ) ) { + $msg .= " $v\n"; + } else { + if ( is_array( $v ) ) { + $msgExample = implode( "\n", array_map( array( $this, 'indentExampleText' ), $v ) ); + } else { + $msgExample = " $v"; + } + $msgExample .= ":"; + $msg .= wordwrap( $msgExample, 100, "\n" ) . "\n $k\n"; + } + } + } + } + + return $msg; + } + + /** + * @param string $item + * @return string + */ + private function indentExampleText( $item ) { + return " " . $item; + } + + /** + * @param string $prefix Text to split output items + * @param string $title What is being output + * @param string|array $input + * @return string + */ + protected function makeHelpArrayToString( $prefix, $title, $input ) { + if ( $input === false ) { + return ''; + } + if ( !is_array( $input ) ) { + $input = array( $input ); + } + + if ( count( $input ) > 0 ) { + if ( $title ) { + $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n "; + } else { + $msg = ' '; + } + $msg .= implode( $prefix, $input ) . "\n"; + + return $msg; + } + + return ''; + } + + /** + * Generates the parameter descriptions for this module, to be displayed in the + * module's help. + * @return string|bool + */ + public function makeHelpMsgParameters() { + $params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP ); + if ( $params ) { + + $paramsDescription = $this->getFinalParamDescription(); + $msg = ''; + $paramPrefix = "\n" . str_repeat( ' ', 24 ); + $descWordwrap = "\n" . str_repeat( ' ', 28 ); + foreach ( $params as $paramName => $paramSettings ) { + $desc = isset( $paramsDescription[$paramName] ) ? $paramsDescription[$paramName] : ''; + if ( is_array( $desc ) ) { + $desc = implode( $paramPrefix, $desc ); + } + + //handle shorthand + if ( !is_array( $paramSettings ) ) { + $paramSettings = array( + self::PARAM_DFLT => $paramSettings, + ); + } + + //handle missing type + if ( !isset( $paramSettings[ApiBase::PARAM_TYPE] ) ) { + $dflt = isset( $paramSettings[ApiBase::PARAM_DFLT] ) + ? $paramSettings[ApiBase::PARAM_DFLT] + : null; + if ( is_bool( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'boolean'; + } elseif ( is_string( $dflt ) || is_null( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'string'; + } elseif ( is_int( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'integer'; + } + } + + if ( isset( $paramSettings[self::PARAM_DEPRECATED] ) + && $paramSettings[self::PARAM_DEPRECATED] + ) { + $desc = "DEPRECATED! $desc"; + } + + if ( isset( $paramSettings[self::PARAM_REQUIRED] ) + && $paramSettings[self::PARAM_REQUIRED] + ) { + $desc .= $paramPrefix . "This parameter is required"; + } + + $type = isset( $paramSettings[self::PARAM_TYPE] ) + ? $paramSettings[self::PARAM_TYPE] + : null; + if ( isset( $type ) ) { + $hintPipeSeparated = true; + $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) + ? $paramSettings[self::PARAM_ISMULTI] + : false; + if ( $multi ) { + $prompt = 'Values (separate with \'|\'): '; + } else { + $prompt = 'One value: '; + } + + if ( $type === 'submodule' ) { + $type = $this->getModuleManager()->getNames( $paramName ); + sort( $type ); + } + if ( is_array( $type ) ) { + $choices = array(); + $nothingPrompt = ''; + foreach ( $type as $t ) { + if ( $t === '' ) { + $nothingPrompt = 'Can be empty, or '; + } else { + $choices[] = $t; + } + } + $desc .= $paramPrefix . $nothingPrompt . $prompt; + $choicesstring = implode( ', ', $choices ); + $desc .= wordwrap( $choicesstring, 100, $descWordwrap ); + $hintPipeSeparated = false; + } else { + switch ( $type ) { + case 'namespace': + // Special handling because namespaces are + // type-limited, yet they are not given + $desc .= $paramPrefix . $prompt; + $desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ), + 100, $descWordwrap ); + $hintPipeSeparated = false; + break; + case 'limit': + $desc .= $paramPrefix . "No more than {$paramSettings[self::PARAM_MAX]}"; + if ( isset( $paramSettings[self::PARAM_MAX2] ) ) { + $desc .= " ({$paramSettings[self::PARAM_MAX2]} for bots)"; + } + $desc .= ' allowed'; + break; + case 'integer': + $s = $multi ? 's' : ''; + $hasMin = isset( $paramSettings[self::PARAM_MIN] ); + $hasMax = isset( $paramSettings[self::PARAM_MAX] ); + if ( $hasMin || $hasMax ) { + if ( !$hasMax ) { + $intRangeStr = "The value$s must be no less than " . + "{$paramSettings[self::PARAM_MIN]}"; + } elseif ( !$hasMin ) { + $intRangeStr = "The value$s must be no more than " . + "{$paramSettings[self::PARAM_MAX]}"; + } else { + $intRangeStr = "The value$s must be between " . + "{$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; + } + + $desc .= $paramPrefix . $intRangeStr; + } + break; + case 'upload': + $desc .= $paramPrefix . "Must be posted as a file upload using multipart/form-data"; + break; + } + } - /** - * Internal code errors should be reported with this method - * @param string $method Method or function name - * @param string $message Error message - * @throws MWException - */ - protected static function dieDebug( $method, $message ) { - throw new MWException( "Internal error in $method: $message" ); - } + if ( $multi ) { + if ( $hintPipeSeparated ) { + $desc .= $paramPrefix . "Separate values with '|'"; + } - /** - * Indicates if this module needs maxlag to be checked - * @return bool - */ - public function shouldCheckMaxlag() { - return true; - } + $isArray = is_array( $type ); + if ( !$isArray + || $isArray && count( $type ) > self::LIMIT_SML1 + ) { + $desc .= $paramPrefix . "Maximum number of values " . + self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)"; + } + } + } - /** - * Indicates whether this module requires read rights - * @return bool - */ - public function isReadMode() { - return true; - } + $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null; + if ( !is_null( $default ) && $default !== false ) { + $desc .= $paramPrefix . "Default: $default"; + } - /** - * Indicates whether this module requires write mode - * @return bool - */ - public function isWriteMode() { - return false; - } + $msg .= sprintf( " %-19s - %s\n", $this->encodeParamName( $paramName ), $desc ); + } - /** - * Indicates whether this module must be called with a POST request - * @return bool - */ - public function mustBePosted() { - return false; - } + return $msg; + } - /** - * Returns whether this module requires a token to execute - * It is used to show possible errors in action=paraminfo - * see bug 25248 - * @return bool - */ - public function needsToken() { return false; } - /** - * Returns the token salt if there is one, - * '' if the module doesn't require a salt, - * else false if the module doesn't need a token - * You have also to override needsToken() - * Value is passed to User::getEditToken - * @return bool|string|array - */ - public function getTokenSalt() { - return false; - } + /**@}*/ - /** - * Gets the user for whom to get the watchlist - * - * @param array $params - * @return User + /************************************************************************//** + * @name Profiling + * @{ */ - public function getWatchlistUser( $params ) { - if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) { - $user = User::newFromName( $params['owner'], false ); - if ( !( $user && $user->getId() ) ) { - $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' ); - } - $token = $user->getOption( 'watchlisttoken' ); - if ( $token == '' || $token != $params['token'] ) { - $this->dieUsage( - 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', - 'bad_wltoken' - ); - } - } else { - if ( !$this->getUser()->isLoggedIn() ) { - $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); - } - if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) { - $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' ); - } - $user = $this->getUser(); - } - - return $user; - } /** - * @return bool|string|array Returns a false if the module has no help URL, - * else returns a (array of) string + * Profiling: total module execution time */ - public function getHelpUrls() { - return false; - } + private $mTimeIn = 0, $mModuleTime = 0; /** - * Returns a list of all possible errors returned by the module - * - * Don't call this function directly: use getFinalPossibleErrors() to allow - * hooks to modify parameters as needed. + * Get the name of the module as shown in the profiler log * - * @return array Array in the format of array( key, param1, param2, ... ) - * or array( 'code' => ..., 'info' => ... ) - */ - public function getPossibleErrors() { - $ret = array(); - - $params = $this->getFinalParams(); - if ( $params ) { - foreach ( $params as $paramName => $paramSettings ) { - if ( isset( $paramSettings[ApiBase::PARAM_REQUIRED] ) - && $paramSettings[ApiBase::PARAM_REQUIRED] - ) { - $ret[] = array( 'missingparam', $paramName ); - } - } - if ( array_key_exists( 'continue', $params ) ) { - $ret[] = array( - 'code' => 'badcontinue', - 'info' => 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query' - ); - } - } - - if ( $this->mustBePosted() ) { - $ret[] = array( 'mustbeposted', $this->getModuleName() ); - } - - if ( $this->isReadMode() ) { - $ret[] = array( 'readrequired' ); - } - - if ( $this->isWriteMode() ) { - $ret[] = array( 'writerequired' ); - $ret[] = array( 'writedisabled' ); - } - - if ( $this->needsToken() ) { - if ( !isset( $params['token'][ApiBase::PARAM_REQUIRED] ) - || !$params['token'][ApiBase::PARAM_REQUIRED] - ) { - // Add token as possible missing parameter, if not already done - $ret[] = array( 'missingparam', 'token' ); - } - $ret[] = array( 'sessionfailure' ); - } - - return $ret; - } - - /** - * Get final list of possible errors, after hooks have had a chance to - * tweak it as needed. + * @param DatabaseBase|bool $db * - * @return array - * @since 1.22 - */ - public function getFinalPossibleErrors() { - $possibleErrors = $this->getPossibleErrors(); - wfRunHooks( 'APIGetPossibleErrors', array( $this, &$possibleErrors ) ); - - return $possibleErrors; - } - - /** - * Parses a list of errors into a standardised format - * @param array $errors List of errors. Items can be in the for - * array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... ) - * @return array Parsed list of errors with items in the form array( 'code' => ..., 'info' => ... ) + * @return string */ - public function parseErrors( $errors ) { - $ret = array(); - - foreach ( $errors as $row ) { - if ( isset( $row['code'] ) && isset( $row['info'] ) ) { - $ret[] = $row; - } else { - $ret[] = $this->parseMsg( $row ); - } + public function getModuleProfileName( $db = false ) { + if ( $db ) { + return 'API:' . $this->mModuleName . '-DB'; } - return $ret; + return 'API:' . $this->mModuleName; } - /** - * Profiling: total module execution time - */ - private $mTimeIn = 0, $mModuleTime = 0; - /** * Start module profiling */ @@ -2309,31 +2224,152 @@ abstract class ApiBase extends ContextSource { } /** - * Gets a default slave database connection object - * @return DatabaseBase + * Write logging information for API features to a debug log, for usage + * analysis. + * @param string $feature Feature being used. */ - protected function getDB() { - if ( !isset( $this->mSlaveDB ) ) { - $this->profileDBIn(); - $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' ); - $this->profileDBOut(); - } + protected function logFeatureUsage( $feature ) { + $request = $this->getRequest(); + $s = '"' . addslashes( $feature ) . '"' . + ' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' . + ' "' . $request->getIP() . '"' . + ' "' . addslashes( $request->getHeader( 'Referer' ) ) . '"' . + ' "' . addslashes( $request->getHeader( 'User-agent' ) ) . '"'; + wfDebugLog( 'api-feature-usage', $s, 'private' ); + } - return $this->mSlaveDB; + /**@}*/ + + /************************************************************************//** + * @name Deprecated + * @{ + */ + + /** + * Formerly returned a string that identifies the version of the extending + * class. Typically included the class name, the svn revision, timestamp, + * and last author. Usually done with SVN's Id keyword + * + * @deprecated since 1.21, version string is no longer supported + * @return string + */ + public function getVersion() { + wfDeprecated( __METHOD__, '1.21' ); + return ''; } /** - * Debugging function that prints a value and an optional backtrace - * @param mixed $value Value to print - * @param string $name Description of the printed value - * @param bool $backtrace If true, print a backtrace + * Formerly used to fetch a list of possible properites in the result, + * somehow organized with respect to the prop parameter that causes them to + * be returned. The specific semantics of the return value was never + * specified. Since this was never possible to be accurately updated, it + * has been removed. + * + * @deprecated since 1.24 + * @return array|bool */ - public static function debugPrint( $value, $name = 'unknown', $backtrace = false ) { - print "\n\n<pre><b>Debugging value '$name':</b>\n\n"; - var_export( $value ); - if ( $backtrace ) { - print "\n" . wfBacktrace(); - } - print "\n</pre>\n"; + protected function getResultProperties() { + wfDeprecated( __METHOD__, '1.24' ); + return false; + } + + /** + * @see self::getResultProperties() + * @deprecated since 1.24 + * @return array|bool + */ + public function getFinalResultProperties() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getResultProperties() + * @deprecated since 1.24 + */ + protected static function addTokenProperties( &$props, $tokenFunctions ) { + wfDeprecated( __METHOD__, '1.24' ); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getRequireOnlyOneParameterErrorMessages( $params ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getRequireMaxOneParameterErrorMessages( $params ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getRequireAtLeastOneParameterErrorMessages( $params ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getTitleOrPageIdErrorMessage() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * This formerly attempted to return a list of all possible errors returned + * by the module. However, this was impossible to maintain in many cases + * since errors could come from other areas of MediaWiki and in some cases + * from arbitrary extension hooks. Since a partial list claiming to be + * comprehensive is unlikely to be useful, it was removed. + * + * @deprecated since 1.24 + * @return array + */ + public function getPossibleErrors() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getFinalPossibleErrors() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function parseErrors( $errors ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /**@}*/ } + +/** + * For really cool vim folding this needs to be at the end: + * vim: foldmarker=@{,@} foldmethod=marker + */ diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 364300e14e..07f62c668c 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -152,7 +152,6 @@ class ApiBlock extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, 'expiry' => 'never', 'reason' => '', 'anononly' => false, @@ -169,7 +168,6 @@ class ApiBlock extends ApiBase { public function getParamDescription() { return array( 'user' => 'Username, IP address or IP range you want to block', - 'token' => 'A block token previously obtained through prop=info', 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. ' . 'If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.', 'reason' => 'Reason for block', @@ -187,66 +185,18 @@ class ApiBlock extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userID' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'expiry' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'id' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'reason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'anononly' => 'boolean', - 'nocreate' => 'boolean', - 'autoblock' => 'boolean', - 'noemail' => 'boolean', - 'hidename' => 'boolean', - 'allowusertalk' => 'boolean', - 'watchuser' => 'boolean' - ) - ); - } - public function getDescription() { return 'Block a user.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'cantblock' ), - array( 'canthide' ), - array( 'cantblock-email' ), - array( 'ipbblocked' ), - array( 'ipbnounblockself' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=block&user=123.5.5.12&expiry=3%20days&reason=First%20strike', - 'api.php?action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate=&autoblock=&noemail=' + 'api.php?action=block&user=123.5.5.12&expiry=3%20days&reason=First%20strike&token=123ABC', + 'api.php?action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate=&autoblock=&noemail=&token=123ABC' ); } diff --git a/includes/api/ApiClearHasMsg.php b/includes/api/ApiClearHasMsg.php new file mode 100644 index 0000000000..32e20e80f3 --- /dev/null +++ b/includes/api/ApiClearHasMsg.php @@ -0,0 +1,58 @@ +<?php + +/** + * Created on August 26, 2014 + * + * Copyright © 2014 Petr Bena (benapetr@gmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * API module that clears the hasmsg flag for current user + * @ingroup API + */ +class ApiClearHasMsg extends ApiBase { + public function execute() { + $user = $this->getUser(); + $user->setNewtalk( false ); + $this->getResult()->addValue( null, $this->getModuleName(), 'success' ); + } + + public function isWriteMode() { + return true; + } + + public function mustBePosted() { + return false; + } + + public function getDescription() { + return array( 'Clears the hasmsg flag for current user.' ); + } + + public function getExamples() { + return array( + 'api.php?action=clearhasmsg' => 'Clears the hasmsg flag for current user', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:ClearHasMsg'; + } +} diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 6b12a7de83..4855926856 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -137,24 +137,6 @@ class ApiComparePages extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'fromtitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'fromrevid' => 'integer', - 'totitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'torevid' => 'integer', - '*' => 'string' - ) - ); - } - public function getDescription() { return array( 'Get the difference between 2 pages.', @@ -162,19 +144,6 @@ class ApiComparePages extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'inputneeded', 'info' => 'A title or a revision is needed' ), - array( 'invalidtitle', 'title' ), - array( 'nosuchpageid', 'pageid' ), - array( - 'code' => 'baddiff', - 'info' => 'The diff cannot be retrieved. Maybe one or both ' . - 'revisions do not exist or you do not have permission to view them.' - ), - ) ); - } - public function getExamples() { return array( 'api.php?action=compare&fromrev=1&torev=2' => 'Create a diff between revision 1 and 2', diff --git a/includes/api/ApiCreateAccount.php b/includes/api/ApiCreateAccount.php index 35bba174c3..2ce532b920 100644 --- a/includes/api/ApiCreateAccount.php +++ b/includes/api/ApiCreateAccount.php @@ -221,84 +221,6 @@ class ApiCreateAccount extends ApiBase { ); } - public function getResultProperties() { - return array( - 'createaccount' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Warning', - 'NeedToken' - ) - ), - 'username' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userid' => array( - ApiBase::PROP_TYPE => 'int', - ApiBase::PROP_NULLABLE => true - ), - 'token' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - ) - ); - } - - public function getPossibleErrors() { - // Note the following errors aren't possible and don't need to be listed: - // sessionfailure, nocookiesfornew, badretype - $localErrors = array( - 'wrongpassword', // Actually caused by wrong domain field. Riddle me that... - 'sorbs_create_account_reason', - 'noname', - 'userexists', - 'password-name-match', // from User::getPasswordValidity - 'password-login-forbidden', // from User::getPasswordValidity - 'noemailtitle', - 'invalidemailaddress', - 'externaldberror', - 'acct_creation_throttle_hit', - ); - - $errors = parent::getPossibleErrors(); - // All local errors are from LoginForm, which means they're actually message keys. - foreach ( $localErrors as $error ) { - $errors[] = array( - 'code' => $error, - 'info' => wfMessage( $error )->inLanguage( 'en' )->useDatabase( false )->parse() - ); - } - - $errors[] = array( - 'code' => 'permdenied-createaccount', - 'info' => 'You do not have the right to create a new account' - ); - $errors[] = array( - 'code' => 'blocked', - 'info' => 'You cannot create a new account because you are blocked' - ); - $errors[] = array( - 'code' => 'aborted', - 'info' => 'Account creation aborted by hook (info may vary)' - ); - $errors[] = array( - 'code' => 'langinvalid', - 'info' => 'Invalid language parameter' - ); - - // 'passwordtooshort' has parameters. :( - $errors[] = array( - 'code' => 'passwordtooshort', - 'info' => wfMessage( 'passwordtooshort', $this->getConfig()->get( 'MinimalPasswordLength' ) ) - ->inLanguage( 'en' )->useDatabase( false )->parse() - ); - - return $errors; - } - public function getExamples() { return array( 'api.php?action=createaccount&name=testuser&password=test123', diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 35555bc7e5..abca824545 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -72,8 +72,10 @@ class ApiDelete extends ApiBase { // Deprecated parameters if ( $params['watch'] ) { + $this->logFeatureUsage( 'action=delete&watch' ); $watch = 'watch'; } elseif ( $params['unwatch'] ) { + $this->logFeatureUsage( 'action=delete&unwatch' ); $watch = 'unwatch'; } else { $watch = $params['watchlist']; @@ -186,10 +188,6 @@ class ApiDelete extends ApiBase { 'pageid' => array( ApiBase::PARAM_TYPE => 'integer' ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => null, 'watch' => array( ApiBase::PARAM_DFLT => false, @@ -218,7 +216,6 @@ class ApiDelete extends ApiBase { return array( 'title' => "Title of the page you want to delete. Cannot be used together with {$p}pageid", 'pageid' => "Page ID of the page you want to delete. Cannot be used together with {$p}title", - 'token' => 'A delete token previously retrieved through prop=info', 'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used', 'watch' => 'Add the page to your watchlist', @@ -229,40 +226,12 @@ class ApiDelete extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'reason' => 'string', - 'logid' => 'integer' - ) - ); - } - public function getDescription() { return 'Delete a page.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'notanarticle' ), - array( 'hookaborted', 'error' ), - array( 'delete-toobig', 'limit' ), - array( 'cannotdelete', 'title' ), - array( 'invalidoldimage' ), - array( 'nodeleteablefile' ), - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 8a0c28081e..8a762714eb 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -309,8 +309,10 @@ class ApiEditPage extends ApiBase { // Deprecated parameters if ( $params['watch'] ) { + $this->logFeatureUsage( 'action=edit&watch' ); $watch = true; } elseif ( $params['unwatch'] ) { + $this->logFeatureUsage( 'action=edit&unwatch' ); $watch = false; } @@ -498,65 +500,6 @@ class ApiEditPage extends ApiBase { return 'Create and edit pages.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'missingtext' ), - array( 'createonly-exists' ), - array( 'nocreate-missing' ), - array( 'nosuchrevid', 'undo' ), - array( 'nosuchrevid', 'undoafter' ), - array( 'revwrongpage', 'id', 'text' ), - array( 'undo-failure' ), - array( 'hashcheckfailed' ), - array( 'hookaborted' ), - array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), - array( 'noimageredirect-anon' ), - array( 'noimageredirect-logged' ), - array( 'spamdetected', 'spam' ), - array( 'summaryrequired' ), - array( 'blockedtext' ), - array( 'contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) ), - array( 'noedit-anon' ), - array( 'noedit' ), - array( 'actionthrottledtext' ), - array( 'wasdeleted' ), - array( 'nocreate-loggedin' ), - array( 'blankpage' ), - array( 'editconflict' ), - array( 'emptynewsection' ), - array( 'unknownerror', 'retval' ), - array( 'code' => 'nosuchsection', 'info' => 'There is no such section.' ), - array( - 'code' => 'invalidsection', - 'info' => 'The section parameter must be a valid section id or \'new\'' - ), - array( - 'code' => 'sectionsnotsupported', - 'info' => 'Sections are not supported for this type of page.' - ), - array( - 'code' => 'editnotsupported', - 'info' => 'Editing of this type of page is not supported using the text based edit API.' - ), - array( - 'code' => 'appendnotsupported', - 'info' => 'This type of page can not be edited by appending or prepending text.' ), - array( - 'code' => 'redirect-appendonly', - 'info' => 'You have attempted to edit using the "redirect"-following mode, which must be used in conjuction with section=new, prependtext, or appendtext.', - ), - array( - 'code' => 'badformat', - 'info' => 'The requested serialization format can not be applied to the page\'s content model' - ), - array( 'customcssprotected' ), - array( 'customjsprotected' ), - ) - ); - } - public function getAllowedParams() { return array( 'title' => array( @@ -570,10 +513,6 @@ class ApiEditPage extends ApiBase { ApiBase::PARAM_TYPE => 'string', ), 'text' => null, - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'summary' => null, 'minor' => false, 'notminor' => false, @@ -632,8 +571,8 @@ class ApiEditPage extends ApiBase { 'sectiontitle' => 'The title for a new section', 'text' => 'Page content', 'token' => array( - 'Edit token. You can get one of these through prop=info.', - "The token should always be sent as the last parameter, or at " . + /* Standard description is automatically prepended */ + 'The token should always be sent as the last parameter, or at ' . "least, after the {$p}text parameter" ), 'summary' @@ -646,7 +585,8 @@ class ApiEditPage extends ApiBase { 'Used to detect edit conflicts; leave unset to ignore conflicts' ), 'starttimestamp' => array( - 'Timestamp when you obtained the edit token.', + 'Timestamp when you began the editing process, e.g. when the current page content ' . + 'was loaded for editing.', 'Used to detect edit conflicts; leave unset to ignore conflicts' ), 'recreate' => 'Override any errors about the article having been deleted in the meantime', @@ -672,47 +612,8 @@ class ApiEditPage extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'new' => 'boolean', - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Failure' - ), - ), - 'pageid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'nochange' => 'boolean', - 'oldrevid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'newrevid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'newtimestamp' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index 29f7e056e9..d35b848bc1 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -94,10 +94,6 @@ class ApiEmailUser extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'ccme' => false, ); } @@ -107,49 +103,21 @@ class ApiEmailUser extends ApiBase { 'target' => 'User to send email to', 'subject' => 'Subject header', 'text' => 'Mail body', - 'token' => 'A token previously acquired via prop=info', 'ccme' => 'Send a copy of this mail to me', ); } - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Failure' - ), - ), - 'message' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Email a user.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'usermaildisabled' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=emailuser&target=WikiSysop&text=Content' + 'api.php?action=emailuser&target=WikiSysop&text=Content&token=123ABC' => 'Send an email to the User "WikiSysop" with the text "Content"', ); } diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index b3a9d83121..8a3b534d4b 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -42,6 +42,7 @@ class ApiExpandTemplates extends ApiBase { $this->requireMaxOneParameter( $params, 'prop', 'generatexml' ); if ( $params['prop'] === null ) { + $this->logFeatureUsage( 'action=expandtemplates&!prop' ); $this->setWarning( 'Because no values have been specified for the prop parameter, a ' . 'legacy format has been used for the output. This format is deprecated, and in ' . 'the future, a default value will be set for the prop parameter, causing the new' . @@ -70,6 +71,10 @@ class ApiExpandTemplates extends ApiBase { $retval = array(); if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) { + if ( !isset( $prop['parsetree'] ) ) { + $this->logFeatureUsage( 'action=expandtemplates&generatexml' ); + } + $wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS ); $dom = $wgParser->preprocessToDom( $params['text'] ); if ( is_callable( array( $dom, 'saveXML' ) ) ) { @@ -176,45 +181,10 @@ class ApiExpandTemplates extends ApiBase { ); } - public function getResultProperties() { - return array( - 'wikitext' => array( - 'wikitext' => 'string', - ), - 'categories' => array( - 'categories' => array( - ApiBase::PROP_TYPE => 'array', - ApiBase::PROP_NULLABLE => true, - ), - ), - 'volatile' => array( - 'volatile' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => true, - ), - ), - 'ttl' => array( - 'ttl' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true, - ), - ), - 'parsetree' => array( - 'parsetree' => 'string', - ), - ); - } - public function getDescription() { return 'Expands all templates in wikitext.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'invalidtitle', 'title' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=expandtemplates&text={{Project:Sandbox}}' diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 3392a5c2ed..374203ebb2 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -210,14 +210,6 @@ class ApiFeedContributions extends ApiBase { return 'Returns a user contributions feed.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'feed-unavailable', 'info' => 'Syndication feeds are not available' ), - array( 'code' => 'feed-invalid', 'info' => 'Invalid subscription feed type' ), - array( 'code' => 'sizediffdisabled', 'info' => 'Size difference is disabled in Miser Mode' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=feedcontributions&user=Reedy', diff --git a/includes/api/ApiFeedRecentChanges.php b/includes/api/ApiFeedRecentChanges.php index bb68d5aed7..7239a2967f 100644 --- a/includes/api/ApiFeedRecentChanges.php +++ b/includes/api/ApiFeedRecentChanges.php @@ -198,13 +198,6 @@ class ApiFeedRecentChanges extends ApiBase { return 'Returns a recent changes feed'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'feed-unavailable', 'info' => 'Syndication feeds are not available' ), - array( 'code' => 'feed-invalid', 'info' => 'Invalid subscription feed type' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=feedrecentchanges', diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index 983b6a81f2..6aef8fc294 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -259,13 +259,6 @@ class ApiFeedWatchlist extends ApiBase { return 'Returns a watchlist feed.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'feed-unavailable', 'info' => 'Syndication feeds are not available' ), - array( 'code' => 'feed-invalid', 'info' => 'Invalid subscription feed type' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=feedwatchlist', diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php index fab8b5a9c4..f518e172e3 100644 --- a/includes/api/ApiFileRevert.php +++ b/includes/api/ApiFileRevert.php @@ -132,63 +132,25 @@ class ApiFileRevert extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), ); } public function getParamDescription() { return array( 'filename' => 'Target filename without the File: prefix', - 'token' => 'Edit token. You can get one of these through prop=info', 'comment' => 'Upload comment', 'archivename' => 'Archive name of the revision to revert to', ); } - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Failure' - ) - ), - 'errors' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return array( 'Revert a file to an old version.' ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - array( - array( 'mustbeloggedin', 'upload' ), - array( 'badaccess-groups' ), - array( 'invalidtitle', 'title' ), - array( 'notanarticle' ), - array( 'filerevert-badversion' ), - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 5d20005fac..9165ce8805 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -52,7 +52,7 @@ abstract class ApiFormatBase extends ApiBase { } /** - * Overriding class returns the mime type that should be sent to the client. + * Overriding class returns the MIME type that should be sent to the client. * This method is not called if getIsHtml() returns true. * @return string */ @@ -247,6 +247,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, /** * Get the contents of the buffer. + * @return string */ public function getBuffer() { return $this->mBuffer; @@ -338,80 +339,16 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, public function getDescription() { return $this->getIsHtml() ? ' (pretty-print in HTML)' : ''; } -} - -/** - * This printer is used to wrap an instance of the Feed class - * @ingroup API - */ -class ApiFormatFeedWrapper extends ApiFormatBase { - - public function __construct( ApiMain $main ) { - parent::__construct( $main, 'feed' ); - } - - /** - * Call this method to initialize output data. See execute() - * @param ApiResult $result - * @param object $feed An instance of one of the $wgFeedClasses classes - * @param array $feedItems Array of FeedItem objects - */ - public static function setResult( $result, $feed, $feedItems ) { - // Store output in the Result data. - // This way we can check during execution if any error has occurred - // Disable size checking for this because we can't continue - // cleanly; size checking would cause more problems than it'd - // solve - $result->addValue( null, '_feed', $feed, ApiResult::NO_SIZE_CHECK ); - $result->addValue( null, '_feeditems', $feedItems, ApiResult::NO_SIZE_CHECK ); - } /** - * Feed does its own headers - * - * @return null + * To avoid code duplication with the deprecation of dbg, dump, txt, wddx, + * and yaml, this method is added to do the necessary work. It should be + * removed when those deprecated formats are removed. */ - public function getMimeType() { - return null; - } - - /** - * Optimization - no need to sanitize data that will not be needed - * - * @return bool - */ - public function getNeedsRawData() { - return true; - } - - /** - * ChannelFeed doesn't give us a method to print errors in a friendly - * manner, so just punt errors to the default printer. - * @return bool - */ - public function canPrintErrors() { - return false; - } - - /** - * This class expects the result data to be in a custom format set by self::setResult() - * $result['_feed'] - an instance of one of the $wgFeedClasses classes - * $result['_feeditems'] - an array of FeedItem instances - */ - public function execute() { - $data = $this->getResultData(); - if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) { - $feed = $data['_feed']; - $items = $data['_feeditems']; - - $feed->outHeader(); - foreach ( $items as & $item ) { - $feed->outItem( $item ); - } - $feed->outFooter(); - } else { - // Error has occurred, print something useful - ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); - } + protected function markDeprecated() { + $fm = $this->getIsHtml() ? 'fm' : ''; + $name = $this->getModuleName(); + $this->logFeatureUsage( "format=$name" ); + $this->setWarning( "format=$name has been deprecated. Please use format=json$fm instead." ); } } diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php index 1b2e02c9ad..5ec518b358 100644 --- a/includes/api/ApiFormatDbg.php +++ b/includes/api/ApiFormatDbg.php @@ -26,6 +26,7 @@ /** * API PHP's var_export() output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatDbg extends ApiFormatBase { @@ -38,10 +39,11 @@ class ApiFormatDbg extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); $this->printText( var_export( $this->getResultData(), true ) ); } public function getDescription() { - return 'Output data in PHP\'s var_export() format' . parent::getDescription(); + return 'DEPRECATED! Output data in PHP\'s var_export() format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatDump.php b/includes/api/ApiFormatDump.php index 62253e1466..d4c7cab4f3 100644 --- a/includes/api/ApiFormatDump.php +++ b/includes/api/ApiFormatDump.php @@ -26,6 +26,7 @@ /** * API PHP's var_dump() output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatDump extends ApiFormatBase { @@ -38,6 +39,7 @@ class ApiFormatDump extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); ob_start(); var_dump( $this->getResultData() ); $result = ob_get_contents(); @@ -46,6 +48,6 @@ class ApiFormatDump extends ApiFormatBase { } public function getDescription() { - return 'Output data in PHP\'s var_dump() format' . parent::getDescription(); + return 'DEPRECATED! Output data in PHP\'s var_dump() format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatFeedWrapper.php b/includes/api/ApiFormatFeedWrapper.php new file mode 100644 index 0000000000..92600067f1 --- /dev/null +++ b/includes/api/ApiFormatFeedWrapper.php @@ -0,0 +1,101 @@ +<?php +/** + * + * + * Created on Sep 19, 2006 + * + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * This printer is used to wrap an instance of the Feed class + * @ingroup API + */ +class ApiFormatFeedWrapper extends ApiFormatBase { + + public function __construct( ApiMain $main ) { + parent::__construct( $main, 'feed' ); + } + + /** + * Call this method to initialize output data. See execute() + * @param ApiResult $result + * @param object $feed An instance of one of the $wgFeedClasses classes + * @param array $feedItems Array of FeedItem objects + */ + public static function setResult( $result, $feed, $feedItems ) { + // Store output in the Result data. + // This way we can check during execution if any error has occurred + // Disable size checking for this because we can't continue + // cleanly; size checking would cause more problems than it'd + // solve + $result->addValue( null, '_feed', $feed, ApiResult::NO_SIZE_CHECK ); + $result->addValue( null, '_feeditems', $feedItems, ApiResult::NO_SIZE_CHECK ); + } + + /** + * Feed does its own headers + * + * @return null + */ + public function getMimeType() { + return null; + } + + /** + * Optimization - no need to sanitize data that will not be needed + * + * @return bool + */ + public function getNeedsRawData() { + return true; + } + + /** + * ChannelFeed doesn't give us a method to print errors in a friendly + * manner, so just punt errors to the default printer. + * @return bool + */ + public function canPrintErrors() { + return false; + } + + /** + * This class expects the result data to be in a custom format set by self::setResult() + * $result['_feed'] - an instance of one of the $wgFeedClasses classes + * $result['_feeditems'] - an array of FeedItem instances + */ + public function execute() { + $data = $this->getResultData(); + if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) { + $feed = $data['_feed']; + $items = $data['_feeditems']; + + $feed->outHeader(); + foreach ( $items as & $item ) { + $feed->outItem( $item ); + } + $feed->outFooter(); + } else { + // Error has occurred, print something useful + ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); + } + } +} diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php index 4130e70cf2..c451ed7756 100644 --- a/includes/api/ApiFormatTxt.php +++ b/includes/api/ApiFormatTxt.php @@ -26,6 +26,7 @@ /** * API Text output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatTxt extends ApiFormatBase { @@ -38,10 +39,11 @@ class ApiFormatTxt extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); $this->printText( print_r( $this->getResultData(), true ) ); } public function getDescription() { - return 'Output data in PHP\'s print_r() format' . parent::getDescription(); + return 'DEPRECATED! Output data in PHP\'s print_r() format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 2e58c720fa..ba90c2603a 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -26,6 +26,7 @@ /** * API WDDX output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatWddx extends ApiFormatBase { @@ -35,6 +36,8 @@ class ApiFormatWddx extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); + // Some versions of PHP have a broken wddx_serialize_value, see // PHP bug 45314. Test encoding an affected character (U+00A0) // to avoid this. @@ -107,6 +110,6 @@ class ApiFormatWddx extends ApiFormatBase { } public function getDescription() { - return 'Output data in WDDX format' . parent::getDescription(); + return 'DEPRECATED! Output data in WDDX format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index 700d4a5e9d..3798f89409 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -26,6 +26,7 @@ /** * API YAML output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatYaml extends ApiFormatJson { @@ -34,7 +35,12 @@ class ApiFormatYaml extends ApiFormatJson { return 'application/yaml'; } + public function execute() { + $this->markDeprecated(); + parent::execute(); + } + public function getDescription() { - return 'Output data in YAML format' . ApiFormatBase::getDescription(); + return 'DEPRECATED! Output data in YAML format' . ApiFormatBase::getDescription(); } } diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index b0b832862a..bcd6c12e7c 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -51,6 +51,7 @@ class ApiHelp extends ApiBase { } if ( is_array( $params['querymodules'] ) ) { + $this->logFeatureUsage( 'action=help&querymodules' ); $queryModules = $params['querymodules']; foreach ( $queryModules as $m ) { $modules[] = 'query+' . $m; diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php index fa7524fe75..20396dd761 100644 --- a/includes/api/ApiImageRotate.php +++ b/includes/api/ApiImageRotate.php @@ -184,10 +184,6 @@ class ApiImageRotate extends ApiBase { ApiBase::PARAM_TYPE => array( '90', '180', '270' ), ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'continue' => '', ); if ( $flags ) { @@ -202,7 +198,6 @@ class ApiImageRotate extends ApiBase { return $pageSet->getFinalParamDescription() + array( 'rotation' => 'Degrees to rotate image clockwise', - 'token' => 'Edit token. You can get one of these through action=tokens', 'continue' => 'When more results are available, use this to continue', ); } @@ -212,20 +207,7 @@ class ApiImageRotate extends ApiBase { } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; - } - - public function getPossibleErrors() { - $pageSet = $this->getPageSet(); - - return array_merge( - parent::getPossibleErrors(), - $pageSet->getFinalPossibleErrors() - ); + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index 3144fc15ad..b11348e5b1 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -99,10 +99,6 @@ class ApiImport extends ApiBase { public function getAllowedParams() { return array( - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'summary' => null, 'xml' => array( ApiBase::PARAM_TYPE => 'upload', @@ -122,7 +118,6 @@ class ApiImport extends ApiBase { public function getParamDescription() { return array( - 'token' => 'Import token obtained through prop=info', 'summary' => 'Import summary', 'xml' => 'Uploaded XML file', 'interwikisource' => 'For interwiki imports: wiki to import from', @@ -134,17 +129,6 @@ class ApiImport extends ApiBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => true, - '' => array( - 'ns' => 'namespace', - 'title' => 'string', - 'revisions' => 'integer' - ) - ); - } - public function getDescription() { return array( 'Import a page from another wiki, or an XML file.', @@ -153,24 +137,8 @@ class ApiImport extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'cantimport' ), - array( 'missingparam', 'interwikipage' ), - array( 'cantimport-upload' ), - array( 'import-unknownerror', 'source' ), - array( 'import-unknownerror', 'result' ), - array( 'import-rootpage-nosubpage', 'namespace' ), - array( 'import-rootpage-invalid' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index f818c5facb..976f4c121c 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -193,66 +193,6 @@ class ApiLogin extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'NeedToken', - 'WrongToken', - 'NoName', - 'Illegal', - 'WrongPluginPass', - 'NotExists', - 'WrongPass', - 'EmptyPass', - 'CreateBlocked', - 'Throttled', - 'Blocked', - 'Aborted' - ) - ), - 'lguserid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'lgusername' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'lgtoken' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'cookieprefix' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'sessionid' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'token' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'details' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'wait' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'reason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return array( 'Log in and get the authentication tokens.', @@ -263,37 +203,6 @@ class ApiLogin extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'NeedToken', 'info' => 'You need to resubmit your ' . - 'login with the specified token. See ' . - 'https://bugzilla.wikimedia.org/show_bug.cgi?id=23076' - ), - array( 'code' => 'WrongToken', 'info' => 'You specified an invalid token' ), - array( 'code' => 'NoName', 'info' => 'You didn\'t set the lgname parameter' ), - array( 'code' => 'Illegal', 'info' => 'You provided an illegal username' ), - array( 'code' => 'NotExists', 'info' => 'The username you provided doesn\'t exist' ), - array( - 'code' => 'EmptyPass', - 'info' => 'You didn\'t set the lgpassword parameter or you left it empty' - ), - array( 'code' => 'WrongPass', 'info' => 'The password you provided is incorrect' ), - array( - 'code' => 'WrongPluginPass', - 'info' => 'Same as "WrongPass", returned when an authentication ' . - 'plugin rather than MediaWiki itself rejected the password' - ), - array( - 'code' => 'CreateBlocked', - 'info' => 'The wiki tried to automatically create a new account ' . - 'for you, but your IP address has been blocked from account creation' - ), - array( 'code' => 'Throttled', 'info' => 'You\'ve logged in too many times in a short time' ), - array( 'code' => 'Blocked', 'info' => 'User is blocked' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=login&lgname=user&lgpassword=password' diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index c8b388205a..324f4b2fdf 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -50,10 +50,6 @@ class ApiLogout extends ApiBase { return array(); } - public function getResultProperties() { - return array(); - } - public function getParamDescription() { return array(); } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 4a75f3c5e0..0d677b19ea 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -81,6 +81,7 @@ class ApiMain extends ApiBase { 'watch' => 'ApiWatch', 'patrol' => 'ApiPatrol', 'import' => 'ApiImport', + 'clearhasmsg' => 'ApiClearHasMsg', 'userrights' => 'ApiUserrights', 'options' => 'ApiOptions', 'imagerotate' => 'ApiImageRotate', @@ -736,6 +737,11 @@ class ApiMain extends ApiBase { } } + if ( $this->getParameter( 'curtimestamp' ) ) { + $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ), + ApiResult::NO_SIZE_CHECK ); + } + $params = $this->extractRequestParams(); $this->mAction = $params['action']; @@ -759,18 +765,35 @@ class ApiMain extends ApiBase { } $moduleParams = $module->extractRequestParams(); - // Die if token required, but not provided - $salt = $module->getTokenSalt(); - if ( $salt !== false ) { + // Check token, if necessary + if ( $module->needsToken() === true ) { + throw new MWException( + "Module '{$module->getModuleName()}' must be updated for the new token handling. " . + "See documentation for ApiBase::needsToken for details." + ); + } + if ( $module->needsToken() ) { + if ( !$module->mustBePosted() ) { + throw new MWException( + "Module '{$module->getModuleName()}' must require POST to use tokens." + ); + } + if ( !isset( $moduleParams['token'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'token' ) ); } - if ( !$this->getUser()->matchEditToken( - $moduleParams['token'], - $salt, - $this->getContext()->getRequest() ) - ) { + if ( array_key_exists( + $module->encodeParamName( 'token' ), + $this->getRequest()->getQueryValues() + ) ) { + $this->dieUsage( + "The '{$module->encodeParamName( 'token' )}' parameter was found in the query string, but must be in the POST body", + 'mustposttoken' + ); + } + + if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) { $this->dieUsageMsg( 'sessionfailure' ); } } @@ -1090,11 +1113,11 @@ class ApiMain extends ApiBase { return array( 'format' => array( ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'format' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'action' => array( ApiBase::PARAM_DFLT => 'help', - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'maxlag' => array( ApiBase::PARAM_TYPE => 'integer' @@ -1112,6 +1135,7 @@ class ApiMain extends ApiBase { ), 'requestid' => null, 'servedby' => false, + 'curtimestamp' => false, 'origin' => null, ); } @@ -1139,6 +1163,7 @@ class ApiMain extends ApiBase { 'requestid' => 'Request ID to distinguish requests. This will just be output back to you', 'servedby' => 'Include the hostname that served the request in the ' . 'results. Unconditionally shown on error', + 'curtimestamp' => 'Include the current timestamp in the result.', 'origin' => array( 'When accessing the API using a cross-domain AJAX request (CORS), set this to the', 'originating domain. This must be included in any pre-flight request, and', @@ -1198,24 +1223,6 @@ class ApiMain extends ApiBase { ); } - /** - * @return array - */ - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'readonlytext' ), - array( 'code' => 'unknown_format', 'info' => 'Unrecognized format: format' ), - array( 'code' => 'unknown_action', 'info' => 'The API requires a valid action parameter' ), - array( 'code' => 'maxlag', 'info' => 'Waiting for host: x seconds lagged' ), - array( 'code' => 'maxlag', 'info' => 'Waiting for a database server: x seconds lagged' ), - array( 'code' => 'assertuserfailed', 'info' => 'Assertion that the user is logged in failed' ), - array( - 'code' => 'assertbotfailed', - 'info' => 'Assertion that the user has the bot right failed' - ), - ) ); - } - /** * Returns an array of strings with credits for the API * @return array diff --git a/includes/api/ApiModuleManager.php b/includes/api/ApiModuleManager.php index 822652977e..a0300ab55d 100644 --- a/includes/api/ApiModuleManager.php +++ b/includes/api/ApiModuleManager.php @@ -59,13 +59,55 @@ class ApiModuleManager extends ContextSource { } /** - * Add a list of modules to the manager - * @param array $modules A map of ModuleName => ModuleClass + * Add a list of modules to the manager. Each module is described + * by a module spec. + * + * Each module spec is an associative array containing at least + * the 'class' key for the module's class, and optionally a + * 'factory' key for the factory function to use for the module. + * + * That factory function will be called with two parameters, + * the parent module (an instance of ApiBase, usually ApiMain) + * and the name the module was registered under. The return + * value must be an instance of the class given in the 'class' + * field. + * + * For backward compatibility, the module spec may also be a + * simple string containing the module's class name. In that + * case, the class' constructor will be called with the parent + * module and module name as parameters, as described above. + * + * Examples for defining module specs: + * + * @code + * $modules['foo'] = 'ApiFoo'; + * $modules['bar'] = array( + * 'class' => 'ApiBar', + * 'factory' => function( $main, $name ) { ... } + * ); + * $modules['xyzzy'] = array( + * 'class' => 'ApiXyzzy', + * 'factory' => array( 'XyzzyFactory', 'newApiModule' ) + * ); + * @endcode + * + * @param array $modules A map of ModuleName => ModuleSpec; The ModuleSpec + * is either a string containing the module's class name, or an associative + * array (see above for details). * @param string $group Which group modules belong to (action,format,...) */ public function addModules( array $modules, $group ) { - foreach ( $modules as $name => $class ) { - $this->addModule( $name, $group, $class ); + + foreach ( $modules as $name => $moduleSpec ) { + if ( is_array( $moduleSpec ) ) { + $class = $moduleSpec['class']; + $factory = ( isset( $moduleSpec['factory'] ) ? $moduleSpec['factory'] : null ); + } else { + $class = $moduleSpec; + $factory = null; + } + + $this->addModule( $name, $group, $class, $factory ); } } @@ -74,37 +116,61 @@ class ApiModuleManager extends ContextSource { * classes who wish to add their own modules to their lexicon or override the * behavior of inherent ones. * - * @param string $group Name of the module group * @param string $name The identifier for this module. + * @param string $group Name of the module group * @param string $class The class where this module is implemented. + * @param callable|null $factory Callback for instantiating the module. + * + * @throws InvalidArgumentException */ - public function addModule( $name, $group, $class ) { + public function addModule( $name, $group, $class, $factory = null ) { + if ( !is_string( $name ) ) { + throw new InvalidArgumentException( '$name must be a string' ); + } + + if ( !is_string( $group ) ) { + throw new InvalidArgumentException( '$group must be a string' ); + } + + if ( !is_string( $class ) ) { + throw new InvalidArgumentException( '$class must be a string' ); + } + + if ( $factory !== null && !is_callable( $factory ) ) { + throw new InvalidArgumentException( '$factory must be a callable (or null)' ); + } + $this->mGroups[$group] = null; - $this->mModules[$name] = array( $group, $class ); + $this->mModules[$name] = array( $group, $class, $factory ); } /** * Get module instance by name, or instantiate it if it does not exist + * * @param string $moduleName Module name * @param string $group Optionally validate that the module is in a specific group * @param bool $ignoreCache If true, force-creates a new instance and does not cache it - * @return mixed The new module instance, or null if failed + * + * @return ApiBase|null The new module instance, or null if failed */ public function getModule( $moduleName, $group = null, $ignoreCache = false ) { if ( !isset( $this->mModules[$moduleName] ) ) { return null; } - $grpCls = $this->mModules[$moduleName]; - if ( $group !== null && $grpCls[0] !== $group ) { + + list( $moduleGroup, $moduleClass, $moduleFactory ) = $this->mModules[$moduleName]; + + if ( $group !== null && $moduleGroup !== $group ) { return null; } + if ( !$ignoreCache && isset( $this->mInstances[$moduleName] ) ) { // already exists return $this->mInstances[$moduleName]; } else { // new instance - $class = $grpCls[1]; - $instance = new $class ( $this->mParent, $moduleName ); + $instance = $this->instantiateModule( $moduleName, $moduleClass, $moduleFactory ); + if ( !$ignoreCache ) { // cache this instance in case it is needed later $this->mInstances[$moduleName] = $instance; @@ -114,6 +180,32 @@ class ApiModuleManager extends ContextSource { } } + /** + * Instantiate the module using the given class or factory function. + * + * @param string $name The identifier for this module. + * @param string $class The class where this module is implemented. + * @param callable|null $factory Callback for instantiating the module. + * + * @throws MWException + * @return ApiBase + */ + private function instantiateModule( $name, $class, $factory = null ) { + if ( $factory !== null ) { + // create instance from factory + $instance = call_user_func( $factory, $this->mParent, $name ); + + if ( !$instance instanceof $class ) { + throw new MWException( "The factory function for module $name did not return an instance of $class!" ); + } + } else { + // create instance from class name + $instance = new $class( $this->mParent, $name ); + } + + return $instance; + } + /** * Get an array of modules in a specific group or all if no group is set. * @param string $group Optional group filter @@ -149,6 +241,21 @@ class ApiModuleManager extends ContextSource { return $result; } + /** + * Returns the class name of the given module + * + * @param string $module Module name + * @return string|bool class name or false if the module does not exist + * @since 1.24 + */ + public function getClassName( $module ) { + if ( isset( $this->mModules[$module] ) ) { + return $this->mModules[$module][1]; + } + + return false; + } + /** * Returns true if the specific module is defined at all or in a specific group. * @param string $moduleName Module name diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 2cfc25180e..04e931d2f3 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -134,8 +134,10 @@ class ApiMove extends ApiBase { $watch = $params['watchlist']; } elseif ( $params['watch'] ) { $watch = 'watch'; + $this->logFeatureUsage( 'action=move&watch' ); } elseif ( $params['unwatch'] ) { $watch = 'unwatch'; + $this->logFeatureUsage( 'action=move&unwatch' ); } // Watch pages @@ -193,10 +195,6 @@ class ApiMove extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => '', 'movetalk' => false, 'movesubpages' => false, @@ -229,7 +227,6 @@ class ApiMove extends ApiBase { 'from' => "Title of the page you want to move. Cannot be used together with {$p}fromid", 'fromid' => "Page ID of the page you want to move. Cannot be used together with {$p}from", 'to' => 'Title you want to rename the page to', - 'token' => 'A move token previously retrieved through prop=info', 'reason' => 'Reason for the move', 'movetalk' => 'Move the talk page, if it exists', 'movesubpages' => 'Move subpages, if applicable', @@ -242,58 +239,12 @@ class ApiMove extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'from' => 'string', - 'to' => 'string', - 'reason' => 'string', - 'redirectcreated' => 'boolean', - 'moveoverredirect' => 'boolean', - 'talkfrom' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'talkto' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'talkmoveoverredirect' => 'boolean', - 'talkmove-error-code' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'talkmove-error-info' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Move a page.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'from', 'fromid' ) ), - array( - array( 'invalidtitle', 'from' ), - array( 'nosuchpageid', 'fromid' ), - array( 'notanarticle' ), - array( 'invalidtitle', 'to' ), - array( 'sharedfile-exists' ), - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php index 86d051a683..b01dc3e239 100644 --- a/includes/api/ApiOptions.php +++ b/includes/api/ApiOptions.php @@ -135,10 +135,6 @@ class ApiOptions extends ApiBase { $optionKinds[] = 'all'; return array( - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reset' => false, 'resetkinds' => array( ApiBase::PARAM_TYPE => $optionKinds, @@ -157,21 +153,8 @@ class ApiOptions extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - '*' => array( - ApiBase::PROP_TYPE => array( - 'success' - ) - ) - ) - ); - } - public function getParamDescription() { return array( - 'token' => 'An options token previously obtained through the action=tokens', 'reset' => 'Resets preferences to the site defaults', 'resetkinds' => 'List of types of options to reset when the "reset" option is set', 'change' => array( 'List of changes, formatted name=value (e.g. skin=vector), ' . @@ -194,19 +177,8 @@ class ApiOptions extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot change preferences' ), - array( 'code' => 'nochanges', 'info' => 'No changes were requested' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getHelpUrls() { diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 20444d0f4a..0f26467591 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -61,6 +61,7 @@ class ApiPageSet extends ApiBase { private $mSpecialTitles = array(); private $mNormalizedTitles = array(); private $mInterwikiTitles = array(); + /** @var Title[] */ private $mPendingRedirectIDs = array(); private $mConvertedTitles = array(); private $mGoodRevIDs = array(); @@ -68,9 +69,7 @@ class ApiPageSet extends ApiBase { private $mFakePageId = -1; private $mCacheMode = 'public'; private $mRequestedPageFields = array(); - /** - * @var int - */ + /** @var int */ private $mDefaultNamespace = NS_MAIN; /** @@ -389,7 +388,7 @@ class ApiPageSet extends ApiBase { /** * Get a list of redirect resolutions - maps a title to its redirect * target, as an array of output-ready arrays - * @return array + * @return Title[] */ public function getRedirectTitles() { return $this->mRedirectTitles; @@ -598,7 +597,7 @@ class ApiPageSet extends ApiBase { /** * Get the list of titles with negative namespace - * @return array Title + * @return Title[] */ public function getSpecialTitles() { return $this->mSpecialTitles; @@ -1168,21 +1167,4 @@ class ApiPageSet extends ApiBase { ), ); } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'multisource', - 'info' => "Cannot use 'pageids' at the same time as 'dataSource'" - ), - array( - 'code' => 'multisource', - 'info' => "Cannot use 'revids' at the same time as 'dataSource'" - ), - array( - 'code' => 'badgenerator', - 'info' => 'Module $generatorName cannot be used as a generator' - ), - ) ); - } } diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 622e3a60c6..067b2f5968 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -198,6 +198,10 @@ class ApiParamInfo extends ApiBase { $a['required'] = ''; } + if ( $n === 'token' && $obj->needsToken() ) { + $a['tokentype'] = $obj->needsToken(); + } + if ( isset( $p[ApiBase::PARAM_DFLT] ) ) { $type = $p[ApiBase::PARAM_TYPE]; if ( $type === 'boolean' ) { @@ -224,7 +228,13 @@ class ApiParamInfo extends ApiBase { } if ( isset( $p[ApiBase::PARAM_TYPE] ) ) { - $a['type'] = $p[ApiBase::PARAM_TYPE]; + if ( $p[ApiBase::PARAM_TYPE] === 'submodule' ) { + $a['type'] = $obj->getModuleManager()->getNames( $n ); + sort( $a['type'] ); + $a['submodules'] = ''; + } else { + $a['type'] = $p[ApiBase::PARAM_TYPE]; + } if ( is_array( $a['type'] ) ) { // To prevent sparse arrays from being serialized to JSON as objects $a['type'] = array_values( $a['type'] ); @@ -244,66 +254,6 @@ class ApiParamInfo extends ApiBase { } $result->setIndexedTagName( $retval['parameters'], 'param' ); - $props = $obj->getFinalResultProperties(); - $listResult = null; - if ( $props !== false ) { - $retval['props'] = array(); - - foreach ( $props as $prop => $properties ) { - $propResult = array(); - if ( $prop == ApiBase::PROP_LIST ) { - $listResult = $properties; - continue; - } - if ( $prop != ApiBase::PROP_ROOT ) { - $propResult['name'] = $prop; - } - $propResult['properties'] = array(); - - foreach ( $properties as $name => $p ) { - $propertyResult = array(); - - $propertyResult['name'] = $name; - - if ( !is_array( $p ) ) { - $p = array( ApiBase::PROP_TYPE => $p ); - } - - $propertyResult['type'] = $p[ApiBase::PROP_TYPE]; - - if ( is_array( $propertyResult['type'] ) ) { - $propertyResult['type'] = array_values( $propertyResult['type'] ); - $result->setIndexedTagName( $propertyResult['type'], 't' ); - } - - $nullable = null; - if ( isset( $p[ApiBase::PROP_NULLABLE] ) ) { - $nullable = $p[ApiBase::PROP_NULLABLE]; - } - - if ( $nullable === true ) { - $propertyResult['nullable'] = ''; - } - - $propResult['properties'][] = $propertyResult; - } - - $result->setIndexedTagName( $propResult['properties'], 'property' ); - $retval['props'][] = $propResult; - } - - // default is true for query modules, false for other modules, overridden by ApiBase::PROP_LIST - if ( $listResult === true || ( $listResult !== false && $obj instanceof ApiQueryBase ) ) { - $retval['listresult'] = ''; - } - - $result->setIndexedTagName( $retval['props'], 'prop' ); - } - - // Errors - $retval['errors'] = $this->parseErrors( $obj->getFinalPossibleErrors() ); - $result->setIndexedTagName( $retval['errors'], 'error' ); - return $retval; } diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 2e993e3648..06fdf85bbb 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -473,6 +473,7 @@ class ApiParse extends ApiBase { /** * @param Content $content * @param string $what Identifies the content in error messages, e.g. page title. + * @return Content|bool */ private function getSectionContent( Content $content, $what ) { // Not cached (save or load) @@ -804,29 +805,6 @@ class ApiParse extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'params', - 'info' => 'The page parameter cannot be used together with the text and title parameters' - ), - array( 'code' => 'missingrev', 'info' => 'There is no revision ID oldid' ), - array( - 'code' => 'permissiondenied', - 'info' => 'You don\'t have permission to view deleted revisions' - ), - array( 'code' => 'missingtitle', 'info' => 'The page you specified doesn\'t exist' ), - array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ), - array( 'nosuchpageid' ), - array( 'invalidtitle', 'title' ), - array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), - array( - 'code' => 'notwikitext', - 'info' => 'The requested operation is only supported on wikitext content.' - ), - ) ); - } - public function getExamples() { return array( 'api.php?action=parse&page=Project:Sandbox' => 'Parse a page', diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index 00297ec27c..8b66781a4d 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -77,10 +77,6 @@ class ApiPatrol extends ApiBase { public function getAllowedParams() { return array( - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'rcid' => array( ApiBase::PARAM_TYPE => 'integer' ), @@ -92,53 +88,23 @@ class ApiPatrol extends ApiBase { public function getParamDescription() { return array( - 'token' => 'Patrol token obtained from list=recentchanges', 'rcid' => 'Recentchanges ID to patrol', 'revid' => 'Revision ID to patrol', ); } - public function getResultProperties() { - return array( - '' => array( - 'rcid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return 'Patrol a page or revision.'; } - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - parent::getRequireOnlyOneParameterErrorMessages( array( 'rcid', 'revid' ) ), - array( - array( 'nosuchrcid', 'rcid' ), - array( 'nosuchrevid', 'revid' ), - array( - 'code' => 'notpatrollable', - 'info' => "The revision can't be patrolled as it's too old" - ) - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { return 'patrol'; } public function getExamples() { return array( - 'api.php?action=patrol&token=123abc&rcid=230672766', - 'api.php?action=patrol&token=123abc&revid=230672766' + 'api.php?action=patrol&token=123ABC&rcid=230672766', + 'api.php?action=patrol&token=123ABC&revid=230672766' ); } diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index b9f97e3536..a3d12b7fc0 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -102,6 +102,9 @@ class ApiProtect extends ApiBase { $cascade = $params['cascade']; + if ( $params['watch'] ) { + $this->logFeatureUsage( 'action=protect&watch' ); + } $watch = $params['watch'] ? 'watch' : $params['watchlist']; $this->setWatch( $watch, $titleObj, 'watchdefault' ); @@ -145,10 +148,6 @@ class ApiProtect extends ApiBase { 'pageid' => array( ApiBase::PARAM_TYPE => 'integer', ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'protections' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_REQUIRED => true, @@ -182,7 +181,6 @@ class ApiProtect extends ApiBase { return array( 'title' => "Title of the page you want to (un)protect. Cannot be used together with {$p}pageid", 'pageid' => "ID of the page you want to (un)protect. Cannot be used together with {$p}title", - 'token' => 'A protect token previously retrieved through prop=info', 'protections' => 'List of protection levels, formatted action=group (e.g. edit=sysop)', 'expiry' => array( 'Expiry timestamps. If only one timestamp is ' . @@ -200,41 +198,12 @@ class ApiProtect extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'reason' => 'string', - 'cascade' => 'boolean' - ) - ); - } - public function getDescription() { return 'Change the protection level of a page.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'toofewexpiries', 'noofexpiries', 'noofprotections' ), - array( 'create-titleexists' ), - array( 'missingtitle-createonly' ), - array( 'protect-invalidaction', 'action' ), - array( 'protect-invalidlevel', 'level' ), - array( 'invalidexpiry', 'expiry' ), - array( 'pastexpiry', 'expiry' ), - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 33998431c4..7667b23529 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -152,52 +152,12 @@ class ApiPurge extends ApiBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => true, - '' => array( - 'ns' => array( - ApiBase::PROP_TYPE => 'namespace', - ApiBase::PROP_NULLABLE => true - ), - 'title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'pageid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'revid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'invalid' => 'boolean', - 'special' => 'boolean', - 'missing' => 'boolean', - 'purged' => 'boolean', - 'linkupdate' => 'boolean', - 'iw' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - ) - ); - } - public function getDescription() { return array( 'Purge the cache for the given titles.', 'Requires a POST request if the user is not logged in.' ); } - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - $this->getPageSet()->getFinalPossibleErrors() - ); - } - public function getExamples() { return array( 'api.php?action=purge&titles=Main_Page|API' => 'Purge the "Main Page" and the "API" page', diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 606012c485..3d6372c5e0 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -108,6 +108,7 @@ class ApiQuery extends ApiBase { 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', 'filerepoinfo' => 'ApiQueryFileRepoInfo', + 'tokens' => 'ApiQueryTokens', ); /** @@ -510,15 +511,15 @@ class ApiQuery extends ApiBase { $result = array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'prop' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'list' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'list' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'meta' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'meta' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'indexpageids' => false, 'export' => false, @@ -620,13 +621,6 @@ class ApiQuery extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - $this->getPageSet()->getFinalPossibleErrors() - ); - } - public function getExamples() { return array( 'api.php?action=query&prop=revisions&meta=siteinfo&' . diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 1b65097d4c..79fab7275b 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -207,23 +207,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - '*' => 'string' - ), - 'size' => array( - 'size' => 'integer', - 'pages' => 'integer', - 'files' => 'integer', - 'subcats' => 'integer' - ), - 'hidden' => array( - 'hidden' => 'boolean' - ) - ); - } - public function getDescription() { return 'Enumerate all categories.'; } diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php index 68d968f0b0..9dc5f69a80 100644 --- a/includes/api/ApiQueryAllImages.php +++ b/includes/api/ApiQueryAllImages.php @@ -393,76 +393,10 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { private $propertyFilter = array( 'archivename', 'thumbmime', 'uploadwarning' ); - public function getResultProperties() { - return array_merge( - array( - '' => array( - 'name' => 'string', - 'ns' => 'namespace', - 'title' => 'string' - ) - ), - ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter ) - ); - } - public function getDescription() { return 'Enumerate all images sequentially.'; } - public function getPossibleErrors() { - $p = $this->getModulePrefix(); - - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'params', - 'info' => 'Use "gaifilterredir=nonredirects" option instead ' . - 'of "redirects" when using allimages as a generator' - ), - array( - 'code' => 'badparams', - 'info' => "Parameter'{$p}start' can only be used with {$p}sort=timestamp" - ), - array( - 'code' => 'badparams', - 'info' => "Parameter'{$p}end' can only be used with {$p}sort=timestamp" - ), - array( - 'code' => 'badparams', - 'info' => "Parameter'{$p}user' can only be used with {$p}sort=timestamp" - ), - array( - 'code' => 'badparams', - 'info' => "Parameter'{$p}filterbots' can only be used with {$p}sort=timestamp" - ), - array( - 'code' => 'badparams', - 'info' => "Parameter'{$p}from' can only be used with {$p}sort=name" - ), - array( - 'code' => 'badparams', - 'info' => "Parameter'{$p}to' can only be used with {$p}sort=name" - ), - array( - 'code' => 'badparams', - 'info' => "Parameter'{$p}prefix' can only be used with {$p}sort=name" - ), - array( - 'code' => 'badparams', - 'info' => "Parameters '{$p}user' and '{$p}filterbots' cannot be used together" - ), - array( - 'code' => 'unsupportedrepo', - 'info' => 'Local file repository does not support querying all images' ), - array( 'code' => 'mimesearchdisabled', 'info' => 'MIME search disabled in Miser Mode' ), - array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ), - array( - 'code' => 'invalidsha1base36hash', - 'info' => 'The SHA1Base36 hash provided is not valid' - ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=allimages&aifrom=B' => array( diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 61bc90e5f9..903dee42d4 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -334,34 +334,10 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { return $paramDescription; } - public function getResultProperties() { - return array( - 'ids' => array( - 'fromid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return $this->description; } - public function getPossibleErrors() { - $m = $this->getModuleName(); - $what = $this->descriptionWhat; - - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'params', - 'info' => "{$m} cannot return corresponding page ids in unique {$what}s mode" - ), - ) ); - } - public function getExamples() { $p = $this->getModulePrefix(); $name = $this->getModuleName(); diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php index c6171e97c2..a75a16fcbc 100644 --- a/includes/api/ApiQueryAllMessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -256,33 +256,6 @@ class ApiQueryAllMessages extends ApiQueryBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'invalidlang', 'info' => 'Invalid language code for parameter lang' ), - ) ); - } - - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string', - 'customised' => 'boolean', - 'missing' => 'boolean', - '*' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'default' => array( - 'defaultmissing' => 'boolean', - 'default' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Return messages from this site.'; } diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php index a3ba5ab63e..b7bd65a53c 100644 --- a/includes/api/ApiQueryAllPages.php +++ b/includes/api/ApiQueryAllPages.php @@ -328,31 +328,10 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return 'Enumerate all pages sequentially in a given namespace.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'params', - 'info' => 'Use "gapfilterredir=nonredirects" option instead of ' . - '"redirects" when using allpages as a generator' - ), - array( 'code' => 'params', 'info' => 'prlevel may not be used without prtype' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=allpages&apfrom=B' => array( diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index d0980e6b14..311921a1f8 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -46,10 +46,11 @@ class ApiQueryAllUsers extends ApiQueryBase { public function execute() { $params = $this->extractRequestParams(); + $activeUserDays = $this->getConfig()->get( 'ActiveUserDays' ); if ( $params['activeusers'] ) { // Update active user cache - SpecialActiveUsers::mergeActiveUsers( 600 ); + SpecialActiveUsers::mergeActiveUsers( 600, $activeUserDays ); } $db = $this->getDB(); @@ -110,35 +111,18 @@ class ApiQueryAllUsers extends ApiQueryBase { } } - if ( !is_null( $params['group'] ) && !is_null( $params['excludegroup'] ) ) { - $this->dieUsage( 'group and excludegroup cannot be used together', 'group-excludegroup' ); - } - if ( !is_null( $params['group'] ) && count( $params['group'] ) ) { - $useIndex = false; // Filter only users that belong to a given group - $this->addTables( 'user_groups', 'ug1' ); - $this->addJoinConds( array( 'ug1' => array( 'INNER JOIN', array( 'ug1.ug_user=user_id', - 'ug1.ug_group' => $params['group'] ) ) ) ); + $this->addWhere( 'EXISTS (' . $db->selectSQLText( + 'user_groups', '1', array( 'ug_user=user_id', 'ug_group' => $params['group'] ) + ) . ')' ); } if ( !is_null( $params['excludegroup'] ) && count( $params['excludegroup'] ) ) { - $useIndex = false; // Filter only users don't belong to a given group - $this->addTables( 'user_groups', 'ug1' ); - - if ( count( $params['excludegroup'] ) == 1 ) { - $exclude = array( 'ug1.ug_group' => $params['excludegroup'][0] ); - } else { - $exclude = array( $db->makeList( - array( 'ug1.ug_group' => $params['excludegroup'] ), - LIST_OR - ) ); - } - $this->addJoinConds( array( 'ug1' => array( 'LEFT OUTER JOIN', - array_merge( array( 'ug1.ug_user=user_id' ), $exclude ) - ) ) ); - $this->addWhere( 'ug1.ug_user IS NULL' ); + $this->addWhere( 'NOT EXISTS (' . $db->selectSQLText( + 'user_groups', '1', array( 'ug_user=user_id', 'ug_group' => $params['excludegroup'] ) + ) . ')' ); } if ( $params['witheditsonly'] ) { @@ -155,13 +139,13 @@ class ApiQueryAllUsers extends ApiQueryBase { $this->addTables( 'user_groups', 'ug2' ); $this->addJoinConds( array( 'ug2' => array( 'LEFT JOIN', 'ug2.ug_user=user_id' ) ) ); - $this->addFields( 'ug2.ug_group ug_group2' ); + $this->addFields( array( 'ug_group2' => 'ug2.ug_group' ) ); } else { $sqlLimit = $limit + 1; } if ( $params['activeusers'] ) { - $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400; + $activeUserSeconds = $activeUserDays * 86400; // Filter query to only include users in the active users cache $this->addTables( 'querycachetwo' ); @@ -420,61 +404,10 @@ class ApiQueryAllUsers extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'userid' => 'integer', - 'name' => 'string', - 'recentactions' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'blockinfo' => array( - 'blockid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedby' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedbyid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedreason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedexpiry' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'hidden' => 'boolean' - ), - 'editcount' => array( - 'editcount' => 'integer' - ), - 'registration' => array( - 'registration' => 'string' - ) - ); - } - public function getDescription() { return 'Enumerate all registered users.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'group-excludegroup', - 'info' => 'group and excludegroup cannot be used together' - ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=allusers&aufrom=Y', diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 8dc2a6580a..c141246d5d 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -520,17 +520,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ) ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string', - 'redirect' => 'boolean' - ) - ); - } - public function getDescription() { switch ( $this->getModuleName() ) { case 'backlinks': @@ -544,18 +533,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( - 'code' => 'bad_image_title', - 'info' => "The title for {$this->getModuleName()} query must be an image" - ), - ) - ); - } - public function getExamples() { static $examples = array( 'backlinks' => array( diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index ebfd8b28e2..6b08fc5c87 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -47,6 +47,11 @@ abstract class ApiQueryBase extends ApiBase { $this->resetQueryParams(); } + /************************************************************************//** + * @name Methods to implement + * @{ + */ + /** * Get the cache mode for the data generated by this module. Override * this in the module subclass. For possible return values and other @@ -62,6 +67,68 @@ abstract class ApiQueryBase extends ApiBase { return 'private'; } + /** + * Override this method to request extra fields from the pageSet + * using $pageSet->requestField('fieldName') + * @param ApiPageSet $pageSet + */ + public function requestExtraData( $pageSet ) { + } + + /**@}*/ + + /************************************************************************//** + * @name Data access + * @{ + */ + + /** + * Get the main Query module + * @return ApiQuery + */ + public function getQuery() { + return $this->mQueryModule; + } + + /** + * Get the Query database connection (read-only) + * @return DatabaseBase + */ + protected function getDB() { + if ( is_null( $this->mDb ) ) { + $this->mDb = $this->getQuery()->getDB(); + } + + return $this->mDb; + } + + /** + * Selects the query database connection with the given name. + * See ApiQuery::getNamedDB() for more information + * @param string $name Name to assign to the database connection + * @param int $db One of the DB_* constants + * @param array $groups Query groups + * @return DatabaseBase + */ + public function selectNamedDB( $name, $db, $groups ) { + $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups ); + } + + /** + * Get the PageSet object to work on + * @return ApiPageSet + */ + protected function getPageSet() { + return $this->getQuery()->getPageSet(); + } + + /**@}*/ + + /************************************************************************//** + * @name Querying + * @{ + */ + /** * Blank the internal arrays with query parameters */ @@ -305,29 +372,64 @@ abstract class ApiQueryBase extends ApiBase { } /** - * Estimate the row count for the SELECT query that would be run if we - * called select() right now, and check if it's acceptable. - * @return bool True if acceptable, false otherwise + * @param string $query + * @param string $protocol + * @return null|string */ - protected function checkRowCount() { - $db = $this->getDB(); - $this->profileDBIn(); - $rowcount = $db->estimateRowCount( - $this->tables, - $this->fields, - $this->where, - __METHOD__, - $this->options - ); - $this->profileDBOut(); + public function prepareUrlQuerySearchString( $query = null, $protocol = null ) { + $db = $this->getDb(); + if ( !is_null( $query ) || $query != '' ) { + if ( is_null( $protocol ) ) { + $protocol = 'http://'; + } - if ( $rowcount > $this->getConfig()->get( 'APIMaxDBRows' ) ) { - return false; + $likeQuery = LinkFilter::makeLikeArray( $query, $protocol ); + if ( !$likeQuery ) { + $this->dieUsage( 'Invalid query', 'bad_query' ); + } + + $likeQuery = LinkFilter::keepOneWildcard( $likeQuery ); + + return 'el_index ' . $db->buildLike( $likeQuery ); + } elseif ( !is_null( $protocol ) ) { + return 'el_index ' . $db->buildLike( "$protocol", $db->anyString() ); } - return true; + return null; } + /** + * Filters hidden users (where the user doesn't have the right to view them) + * Also adds relevant block information + * + * @param bool $showBlockInfo + * @return void + */ + public function showHiddenUsersAddBlockInfo( $showBlockInfo ) { + $this->addTables( 'ipblocks' ); + $this->addJoinConds( array( + 'ipblocks' => array( 'LEFT JOIN', 'ipb_user=user_id' ), + ) ); + + $this->addFields( 'ipb_deleted' ); + + if ( $showBlockInfo ) { + $this->addFields( array( 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_reason', 'ipb_expiry' ) ); + } + + // Don't show hidden names + if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { + $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' ); + } + } + + /**@}*/ + + /************************************************************************//** + * @name Utility methods + * @{ + */ + /** * Add information (title and namespace) about a Title object to a * result array @@ -340,22 +442,6 @@ abstract class ApiQueryBase extends ApiBase { $arr[$prefix . 'title'] = $title->getPrefixedText(); } - /** - * Override this method to request extra fields from the pageSet - * using $pageSet->requestField('fieldName') - * @param ApiPageSet $pageSet - */ - public function requestExtraData( $pageSet ) { - } - - /** - * Get the main Query module - * @return ApiQuery - */ - public function getQuery() { - return $this->mQueryModule; - } - /** * Add a sub-element under the page element with the given page ID * @param int $pageId Page ID @@ -405,89 +491,21 @@ abstract class ApiQueryBase extends ApiBase { } /** - * Get the Query database connection (read-only) - * @return DatabaseBase - */ - protected function getDB() { - if ( is_null( $this->mDb ) ) { - $this->mDb = $this->getQuery()->getDB(); - } - - return $this->mDb; - } - - /** - * Selects the query database connection with the given name. - * See ApiQuery::getNamedDB() for more information - * @param string $name Name to assign to the database connection - * @param int $db One of the DB_* constants - * @param array $groups Query groups - * @return DatabaseBase - */ - public function selectNamedDB( $name, $db, $groups ) { - $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups ); - } - - /** - * Get the PageSet object to work on - * @return ApiPageSet - */ - protected function getPageSet() { - return $this->getQuery()->getPageSet(); - } - - /** - * Convert a title to a DB key - * @param string $title Page title with spaces - * @return string Page title with underscores - */ - public function titleToKey( $title ) { - // Don't throw an error if we got an empty string - if ( trim( $title ) == '' ) { - return ''; - } - $t = Title::newFromText( $title ); - if ( !$t ) { - $this->dieUsageMsg( array( 'invalidtitle', $title ) ); - } - - return $t->getPrefixedDBkey(); - } - - /** - * The inverse of titleToKey() - * @param string $key Page title with underscores - * @return string Page title with spaces - */ - public function keyToTitle( $key ) { - // Don't throw an error if we got an empty string - if ( trim( $key ) == '' ) { - return ''; - } - $t = Title::newFromDBkey( $key ); - // This really shouldn't happen but we gotta check anyway - if ( !$t ) { - $this->dieUsageMsg( array( 'invalidtitle', $key ) ); - } - - return $t->getPrefixedText(); - } - - /** - * An alternative to titleToKey() that doesn't trim trailing spaces, and - * does not mangle the input if starts with something that looks like a - * namespace. It is advisable to pass the namespace parameter in order to - * handle per-namespace capitalization settings. - * @param string $titlePart Title part with spaces - * @param int $defaultNamespace Namespace to assume - * @return string Title part with underscores + * Convert an input title or title prefix into a dbkey. + * + * $namespace should always be specified in order to handle per-namespace + * capitalization settings. + * + * @param string $titlePart Title part + * @param int $defaultNamespace Namespace of the title + * @return string DBkey (no namespace prefix) */ - public function titlePartToKey( $titlePart, $defaultNamespace = NS_MAIN ) { - $t = Title::makeTitleSafe( $defaultNamespace, $titlePart . 'x' ); + public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) { + $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' ); if ( !$t ) { $this->dieUsageMsg( array( 'invalidtitle', $titlePart ) ); } - if ( $defaultNamespace != $t->getNamespace() || $t->isExternal() ) { + if ( $namespace != $t->getNamespace() || $t->isExternal() ) { // This can happen in two cases. First, if you call titlePartToKey with a title part // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very // difficult to handle such a case. Such cases cannot exist and are therefore treated @@ -499,15 +517,6 @@ abstract class ApiQueryBase extends ApiBase { return substr( $t->getDbKey(), 0, -1 ); } - /** - * An alternative to keyToTitle() that doesn't trim trailing spaces - * @param string $keyPart Key part with spaces - * @return string Key part with underscores - */ - public function keyPartToTitle( $keyPart ) { - return substr( $this->keyToTitle( $keyPart . 'x' ), 0, -1 ); - } - /** * Gets the personalised direction parameter description * @@ -523,58 +532,6 @@ abstract class ApiQueryBase extends ApiBase { ); } - /** - * @param string $query - * @param string $protocol - * @return null|string - */ - public function prepareUrlQuerySearchString( $query = null, $protocol = null ) { - $db = $this->getDb(); - if ( !is_null( $query ) || $query != '' ) { - if ( is_null( $protocol ) ) { - $protocol = 'http://'; - } - - $likeQuery = LinkFilter::makeLikeArray( $query, $protocol ); - if ( !$likeQuery ) { - $this->dieUsage( 'Invalid query', 'bad_query' ); - } - - $likeQuery = LinkFilter::keepOneWildcard( $likeQuery ); - - return 'el_index ' . $db->buildLike( $likeQuery ); - } elseif ( !is_null( $protocol ) ) { - return 'el_index ' . $db->buildLike( "$protocol", $db->anyString() ); - } - - return null; - } - - /** - * Filters hidden users (where the user doesn't have the right to view them) - * Also adds relevant block information - * - * @param bool $showBlockInfo - * @return void - */ - public function showHiddenUsersAddBlockInfo( $showBlockInfo ) { - $this->addTables( 'ipblocks' ); - $this->addJoinConds( array( - 'ipblocks' => array( 'LEFT JOIN', 'ipb_user=user_id' ), - ) ); - - $this->addFields( 'ipb_deleted' ); - - if ( $showBlockInfo ) { - $this->addFields( array( 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_reason', 'ipb_expiry' ) ); - } - - // Don't show hidden names - if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { - $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' ); - } - } - /** * @param string $hash * @return bool @@ -591,19 +548,6 @@ abstract class ApiQueryBase extends ApiBase { return preg_match( '/^[a-z0-9]{31}$/', $hash ); } - /** - * @return array - */ - public function getPossibleErrors() { - $errors = parent::getPossibleErrors(); - $errors = array_merge( $errors, array( - array( 'invalidtitle', 'title' ), - array( 'invalidtitle', 'key' ), - ) ); - - return $errors; - } - /** * Check whether the current user has permission to view revision-deleted * fields. @@ -617,6 +561,94 @@ abstract class ApiQueryBase extends ApiBase { 'viewsuppressed' ); } + + /**@}*/ + + /************************************************************************//** + * @name Deprecated + * @{ + */ + + /** + * Estimate the row count for the SELECT query that would be run if we + * called select() right now, and check if it's acceptable. + * @deprecated since 1.24 + * @return bool True if acceptable, false otherwise + */ + protected function checkRowCount() { + wfDeprecated( __METHOD__, '1.24' ); + $db = $this->getDB(); + $this->profileDBIn(); + $rowcount = $db->estimateRowCount( + $this->tables, + $this->fields, + $this->where, + __METHOD__, + $this->options + ); + $this->profileDBOut(); + + if ( $rowcount > $this->getConfig()->get( 'APIMaxDBRows' ) ) { + return false; + } + + return true; + } + + /** + * Convert a title to a DB key + * @deprecated since 1.24, past uses of this were always incorrect and should + * have used self::titlePartToKey() instead + * @param string $title Page title with spaces + * @return string Page title with underscores + */ + public function titleToKey( $title ) { + wfDeprecated( __METHOD__, '1.24' ); + // Don't throw an error if we got an empty string + if ( trim( $title ) == '' ) { + return ''; + } + $t = Title::newFromText( $title ); + if ( !$t ) { + $this->dieUsageMsg( array( 'invalidtitle', $title ) ); + } + + return $t->getPrefixedDBkey(); + } + + /** + * The inverse of titleToKey() + * @deprecated since 1.24, unused and probably never needed + * @param string $key Page title with underscores + * @return string Page title with spaces + */ + public function keyToTitle( $key ) { + wfDeprecated( __METHOD__, '1.24' ); + // Don't throw an error if we got an empty string + if ( trim( $key ) == '' ) { + return ''; + } + $t = Title::newFromDBkey( $key ); + // This really shouldn't happen but we gotta check anyway + if ( !$t ) { + $this->dieUsageMsg( array( 'invalidtitle', $key ) ); + } + + return $t->getPrefixedText(); + } + + /** + * Inverse of titlePartToKey() + * @deprecated since 1.24, unused and probably never needed + * @param string $keyPart DBkey, with prefix + * @return string Key part with underscores + */ + public function keyPartToTitle( $keyPart ) { + wfDeprecated( __METHOD__, '1.24' ); + return substr( $this->keyToTitle( $keyPart . 'x' ), 0, -1 ); + } + + /**@}*/ } /** @@ -654,7 +686,7 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { } /** - * Overrides base class to prepend 'g' to every generator parameter + * Overrides ApiBase to prepend 'g' to every generator parameter * @param string $paramName Parameter name * @return string Prefixed parameter name */ diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index d62e87d000..33b25fd927 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -368,86 +368,10 @@ class ApiQueryBlocks extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - 'id' => array( - 'id' => 'integer' - ), - 'user' => array( - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'userid' => array( - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'by' => array( - 'by' => 'string' - ), - 'byid' => array( - 'byid' => 'integer' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'expiry' => array( - 'expiry' => 'timestamp' - ), - 'reason' => array( - 'reason' => 'string' - ), - 'range' => array( - 'rangestart' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'rangeend' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'flags' => array( - 'automatic' => 'boolean', - 'anononly' => 'boolean', - 'nocreate' => 'boolean', - 'autoblock' => 'boolean', - 'noemail' => 'boolean', - 'hidden' => 'boolean', - 'allowusertalk' => 'boolean' - ) - ); - } - public function getDescription() { return 'List all blocked users and IP addresses.'; } - public function getPossibleErrors() { - $blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' ); - - return array_merge( parent::getPossibleErrors(), - $this->getRequireMaxOneParameterErrorMessages( array( 'users', 'ip' ) ), - array( - array( - 'code' => 'cidrtoobroad', - 'info' => "IPv4 CIDR ranges broader than /{$blockCIDRLimit['IPv4']} are not accepted" - ), - array( - 'code' => 'cidrtoobroad', - 'info' => "IPv6 CIDR ranges broader than /{$blockCIDRLimit['IPv6']} are not accepted" - ), - array( 'code' => 'param_ip', 'info' => 'IP parameter is not valid' ), - array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ), - array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), - array( 'show' ), - ) - ); - } - public function getExamples() { return array( 'api.php?action=query&list=blocks', diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index cfc76e66fc..1926dd0946 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -234,35 +234,10 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'sortkey' => array( - 'sortkey' => 'string', - 'sortkeyprefix' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'hidden' => array( - 'hidden' => 'boolean' - ) - ); - } - public function getDescription() { return 'List all categories the page(s) belong to.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'show' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&prop=categories&titles=Albert%20Einstein' diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index 8097b7bd69..6e9f33c1b3 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -114,34 +114,6 @@ class ApiQueryCategoryInfo extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => false, - '' => array( - 'size' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'pages' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'files' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'subcats' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'hidden' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => false - ) - ) - ); - } - public function getDescription() { return 'Returns information about the given categories.'; } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index dc11071b0a..a88a9cb1f9 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -140,12 +140,22 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addWhereRange( 'cl_sortkey', $dir, null, null ); $this->addWhereRange( 'cl_from', $dir, null, null ); } else { - $startsortkey = $params['startsortkeyprefix'] !== null ? - Collation::singleton()->getSortkey( $params['startsortkeyprefix'] ) : - $params['startsortkey']; - $endsortkey = $params['endsortkeyprefix'] !== null ? - Collation::singleton()->getSortkey( $params['endsortkeyprefix'] ) : - $params['endsortkey']; + if ( $params['startsortkeyprefix'] !== null ) { + $startsortkey = Collation::singleton()->getSortkey( $params['startsortkeyprefix'] ); + } elseif ( $params['starthexsortkey'] !== null ) { + $startsortkey = pack( 'H*', $params['starthexsortkey'] ); + } else { + $this->logFeatureUsage( 'list=categorymembers&cmstartsortkey' ); + $startsortkey = $params['startsortkey']; + } + if ( $params['endsortkeyprefix'] !== null ) { + $endsortkey = Collation::singleton()->getSortkey( $params['endsortkeyprefix'] ); + } elseif ( $params['endhexsortkey'] !== null ) { + $endsortkey = pack( 'H*', $params['endhexsortkey'] ); + } else { + $this->logFeatureUsage( 'list=categorymembers&cmendsortkey' ); + $endsortkey = $params['endsortkey']; + } // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them $this->addWhereRange( 'cl_sortkey', @@ -330,10 +340,16 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'end' => array( ApiBase::PARAM_TYPE => 'timestamp' ), - 'startsortkey' => null, - 'endsortkey' => null, + 'starthexsortkey' => null, + 'endhexsortkey' => null, 'startsortkeyprefix' => null, 'endsortkeyprefix' => null, + 'startsortkey' => array( + ApiBase::PARAM_DEPRECATED => true, + ), + 'endsortkey' => array( + ApiBase::PARAM_DEPRECATED => true, + ), ); } @@ -359,15 +375,17 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'dir' => 'In which direction to sort', 'start' => "Timestamp to start listing from. Can only be used with {$p}sort=timestamp", 'end' => "Timestamp to end listing at. Can only be used with {$p}sort=timestamp", - 'startsortkey' => "Sortkey to start listing from. Must be given in " . - "binary format. Can only be used with {$p}sort=sortkey", - 'endsortkey' => "Sortkey to end listing at. Must be given in binary " . - "format. Can only be used with {$p}sort=sortkey", + 'starthexsortkey' => "Sortkey to start listing from, as returned by prop=sortkey. " . + "Can only be used with {$p}sort=sortkey", + 'endhexsortkey' => "Sortkey to end listing from, as returned by prop=sortkey. " . + "Can only be used with {$p}sort=sortkey", 'startsortkeyprefix' => "Sortkey prefix to start listing from. Can " . - "only be used with {$p}sort=sortkey. Overrides {$p}startsortkey", + "only be used with {$p}sort=sortkey. Overrides {$p}starthexsortkey", 'endsortkeyprefix' => "Sortkey prefix to end listing BEFORE (not at, " . "if this value occurs it will not be included!). Can only be used with " . - "{$p}sort=sortkey. Overrides {$p}endsortkey", + "{$p}sort=sortkey. Overrides {$p}endhexsortkey", + 'startsortkey' => "Use starthexsortkey instead", + 'endsortkey' => "Use endhexsortkey instead", 'continue' => 'For large categories, give the value returned from previous query', 'limit' => 'The maximum number of pages to return.', ); @@ -384,49 +402,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { return $desc; } - public function getResultProperties() { - return array( - 'ids' => array( - 'pageid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'sortkey' => array( - 'sortkey' => 'string' - ), - 'sortkeyprefix' => array( - 'sortkeyprefix' => 'string' - ), - 'type' => array( - 'type' => array( - ApiBase::PROP_TYPE => array( - 'page', - 'subcat', - 'file' - ) - ) - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ) - ); - } - public function getDescription() { return 'List all pages in a given category.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ), - ) - ); - } - public function getExamples() { return array( 'api.php?action=query&list=categorymembers&cmtitle=Category:Physics' diff --git a/includes/api/ApiQueryContributors.php b/includes/api/ApiQueryContributors.php index b90283f978..55ea47025e 100644 --- a/includes/api/ApiQueryContributors.php +++ b/includes/api/ApiQueryContributors.php @@ -265,14 +265,6 @@ class ApiQueryContributors extends ApiQueryBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getRequireMaxOneParameterErrorMessages( - array( 'group', 'excludegroup', 'rights', 'excluderights' ) - ) - ); - } - public function getDescription() { return 'Get the list of logged-in contributors and ' . 'the count of anonymous contributors to a page.'; diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 4f77078440..9042696bee 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -61,6 +61,13 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $fld_token = isset( $prop['token'] ); $fld_tags = isset( $prop['tags'] ); + if ( isset( $prop['token'] ) ) { + $p = $this->getModulePrefix(); + $this->setWarning( + "{$p}prop=token has been deprecated. Please use action=query&meta=tokens instead." + ); + } + // If we're in JSON callback mode, no tokens can be obtained if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) { $fld_token = false; @@ -493,7 +500,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ' len - Adds the length (bytes) of the revision', ' sha1 - Adds the SHA-1 (base 16) of the revision', ' content - Adds the content of the revision', - ' token - Gives the edit token', + ' token - DEPRECATED! Gives the edit token', ' tags - Tags for the revision', ), 'namespace' => 'Only list pages in this namespace (3)', @@ -505,18 +512,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'token' => array( - 'token' => 'string' - ) - ); - } - public function getDescription() { $p = $this->getModulePrefix(); @@ -532,29 +527,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'permissiondenied', - 'info' => 'You don\'t have permission to view deleted revision information' - ), - array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' - ), - array( - 'code' => 'permissiondenied', - 'info' => 'You don\'t have permission to view deleted revision content' - ), - array( 'code' => 'badparams', 'info' => "The 'from' parameter cannot be used in modes 1 or 2" ), - array( 'code' => 'badparams', 'info' => "The 'to' parameter cannot be used in modes 1 or 2" ), - array( - 'code' => 'badparams', - 'info' => "The 'prefix' parameter cannot be used in modes 1 or 2" - ), - array( 'code' => 'badparams', 'info' => "The 'start' parameter cannot be used in mode 3" ), - array( 'code' => 'badparams', 'info' => "The 'end' parameter cannot be used in mode 3" ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&' . diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php index 46454029fb..6d836cd5f7 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -188,17 +188,6 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string', - 'user' => 'string', - 'timestamp' => 'timestamp', - 'shared' => 'boolean', - ) - ); - } - public function getDescription() { return 'List all files that are duplicates of the given file(s) based on hash values.'; } diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 33e8739e68..faabb920eb 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -239,31 +239,10 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { return $desc; } - public function getResultProperties() { - return array( - 'ids' => array( - 'pageid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'url' => array( - 'url' => 'string' - ) - ); - } - public function getDescription() { return 'Enumerate pages that contain a given URL.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'bad_query', 'info' => 'Invalid query' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=exturlusage&euquery=www.mediawiki.org' diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index e3a7be3da7..95666354a2 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -141,24 +141,10 @@ class ApiQueryExternalLinks extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - '*' => 'string' - ) - ); - } - public function getDescription() { return 'Returns all external URLs (not interwikis) from the given page(s).'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'bad_query', 'info' => 'Invalid query' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&prop=extlinks&titles=Main%20Page' diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index a94b4fac6d..f047d8d414 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -315,89 +315,10 @@ class ApiQueryFilearchive extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string', - 'ns' => 'namespace', - 'title' => 'string', - 'filehidden' => 'boolean', - 'commenthidden' => 'boolean', - 'userhidden' => 'boolean', - 'suppressed' => 'boolean' - ), - 'sha1' => array( - 'sha1' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'user' => array( - 'userid' => 'integer', - 'user' => 'string' - ), - 'size' => array( - 'size' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'height' => 'integer', - 'width' => 'integer' - ), - 'dimensions' => array( - 'size' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'height' => 'integer', - 'width' => 'integer' - ), - 'description' => array( - 'description' => 'string' - ), - 'parseddescription' => array( - 'description' => 'string', - 'parseddescription' => 'string' - ), - 'metadata' => array( - 'metadata' => 'string' - ), - 'bitdepth' => array( - 'bitdepth' => 'integer' - ), - 'mime' => array( - 'mime' => 'string' - ), - 'mediatype' => array( - 'mediatype' => 'string' - ), - 'archivename' => array( - 'archivename' => 'string' - ), - ); - } - public function getDescription() { return 'Enumerate all deleted files sequentially.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( - 'code' => 'permissiondenied', - 'info' => 'You don\'t have permission to view deleted file information' - ), - array( 'code' => 'hashsearchdisabled', 'info' => 'Search by hash disabled in Miser Mode' ), - array( 'code' => 'invalidsha1hash', 'info' => 'The SHA-1 hash provided is not valid' ), - array( - 'code' => 'invalidsha1base36hash', - 'info' => 'The SHA1Base36 hash provided is not valid' - ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=filearchive' => array( diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index 35b2b4092b..b5aa45bfff 100644 --- a/includes/api/ApiQueryIWBacklinks.php +++ b/includes/api/ApiQueryIWBacklinks.php @@ -210,23 +210,6 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string', - 'redirect' => 'boolean' - ), - 'iwprefix' => array( - 'iwprefix' => 'string' - ), - 'iwtitle' => array( - 'iwtitle' => 'string' - ) - ); - } - public function getDescription() { return array( 'Find all pages that link to the given interwiki link.', 'Can be used to find all links with a prefix, or', @@ -235,12 +218,6 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'prefix' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=iwbacklinks&iwbltitle=Test&iwblprefix=wikibooks', diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index f38a7b17ed..a185ee24e6 100644 --- a/includes/api/ApiQueryIWLinks.php +++ b/includes/api/ApiQueryIWLinks.php @@ -42,11 +42,19 @@ class ApiQueryIWLinks extends ApiQueryBase { } $params = $this->extractRequestParams(); + $prop = array_flip( (array)$params['prop'] ); if ( isset( $params['title'] ) && !isset( $params['prefix'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'prefix' ) ); } + // Handle deprecated param + $this->requireMaxOneParameter( $params, 'url', 'prop' ); + if ( $params['url'] ) { + $this->logFeatureUsage( 'prop=iwlinks&iwurl' ); + $prop = array( 'url' => 1 ); + } + $this->addFields( array( 'iwl_from', 'iwl_prefix', @@ -114,7 +122,7 @@ class ApiQueryIWLinks extends ApiQueryBase { } $entry = array( 'prefix' => $row->iwl_prefix ); - if ( $params['url'] ) { + if ( isset( $prop['url'] ) ) { $title = Title::newFromText( "{$row->iwl_prefix}:{$row->iwl_title}" ); if ( $title ) { $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); @@ -139,7 +147,16 @@ class ApiQueryIWLinks extends ApiQueryBase { public function getAllowedParams() { return array( - 'url' => false, + 'url' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), + 'prop' => array( + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array( + 'url', + ) + ), 'limit' => array( ApiBase::PARAM_DFLT => 10, ApiBase::PARAM_TYPE => 'limit', @@ -162,7 +179,11 @@ class ApiQueryIWLinks extends ApiQueryBase { public function getParamDescription() { return array( - 'url' => 'Whether to get the full URL', + 'prop' => array( + 'Which additional properties to get for each interlanguage link', + ' url - Adds the full URL', + ), + 'url' => "Whether to get the full URL (Cannot be used with {$this->getModulePrefix()}prop)", 'limit' => 'How many interwiki links to return', 'continue' => 'When more results are available, use this to continue', 'prefix' => 'Prefix for the interwiki', @@ -171,29 +192,10 @@ class ApiQueryIWLinks extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'prefix' => 'string', - 'url' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - '*' => 'string' - ) - ); - } - public function getDescription() { return 'Returns all interwiki links from the given page(s).'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'prefix' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&prop=iwlinks&titles=Main%20Page' diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index a214610d4b..5cc1454046 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -739,151 +739,10 @@ class ApiQueryImageInfo extends ApiQueryBase { ); } - public static function getResultPropertiesFiltered( $filter = array() ) { - $props = array( - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'user' => array( - 'userhidden' => 'boolean', - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userhidden' => 'boolean', - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'size' => array( - 'size' => 'integer', - 'width' => 'integer', - 'height' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'dimensions' => array( - 'size' => 'integer', - 'width' => 'integer', - 'height' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'canonicaltitle' => array( - 'canonicaltitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'url' => array( - 'filehidden' => 'boolean', - 'thumburl' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'thumbwidth' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'thumbheight' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'thumberror' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'url' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'descriptionurl' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sha1' => array( - 'filehidden' => 'boolean', - 'sha1' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'mime' => array( - 'filehidden' => 'boolean', - 'mime' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'thumbmime' => array( - 'filehidden' => 'boolean', - 'thumbmime' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'mediatype' => array( - 'filehidden' => 'boolean', - 'mediatype' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'archivename' => array( - 'filehidden' => 'boolean', - 'archivename' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'bitdepth' => array( - 'filehidden' => 'boolean', - 'bitdepth' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - ); - - return array_diff_key( $props, array_flip( $filter ) ); - } - - public function getResultProperties() { - return self::getResultPropertiesFiltered(); - } - public function getDescription() { return 'Returns image information and upload history.'; } - public function getPossibleErrors() { - $p = $this->getModulePrefix(); - - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => "{$p}urlwidth", 'info' => "{$p}urlheight cannot be used without {$p}urlwidth" ), - array( 'code' => 'urlparam', 'info' => "Invalid value for {$p}urlparam" ), - array( 'code' => 'urlparam_no_width', 'info' => "{$p}urlparam requires {$p}urlwidth" ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&titles=File:Albert%20Einstein%20Head.jpg&prop=imageinfo', diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index 87b07782c0..9bc3abedd6 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -170,15 +170,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return 'Returns all images contained on the given page(s).'; } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 8b6886d4f8..d7037e3a19 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -79,6 +79,7 @@ class ApiQueryInfo extends ApiQueryBase { * Get an array mapping token names to their handler functions. * The prototype for a token function is func($pageid, $title) * it should return a token or false (permission denied) + * @deprecated since 1.24 * @return array Array(tokenname => function) */ protected function getTokenFunctions() { @@ -110,10 +111,16 @@ class ApiQueryInfo extends ApiQueryBase { static protected $cachedTokens = array(); + /** + * @deprecated since 1.24 + */ public static function resetTokenCache() { ApiQueryInfo::$cachedTokens = array(); } + /** + * @deprecated since 1.24 + */ public static function getEditToken( $pageid, $title ) { // We could check for $title->userCan('edit') here, // but that's too expensive for this purpose @@ -131,6 +138,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['edit']; } + /** + * @deprecated since 1.24 + */ public static function getDeleteToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'delete' ) ) { @@ -145,6 +155,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['delete']; } + /** + * @deprecated since 1.24 + */ public static function getProtectToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'protect' ) ) { @@ -159,6 +172,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['protect']; } + /** + * @deprecated since 1.24 + */ public static function getMoveToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'move' ) ) { @@ -173,6 +189,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['move']; } + /** + * @deprecated since 1.24 + */ public static function getBlockToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'block' ) ) { @@ -187,11 +206,17 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['block']; } + /** + * @deprecated since 1.24 + */ public static function getUnblockToken( $pageid, $title ) { // Currently, this is exactly the same as the block token return self::getBlockToken( $pageid, $title ); } + /** + * @deprecated since 1.24 + */ public static function getEmailToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailUser() ) { @@ -206,6 +231,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['email']; } + /** + * @deprecated since 1.24 + */ public static function getImportToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) { @@ -220,6 +248,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['import']; } + /** + * @deprecated since 1.24 + */ public static function getWatchToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isLoggedIn() ) { @@ -234,6 +265,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['watch']; } + /** + * @deprecated since 1.24 + */ public static function getOptionsToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isLoggedIn() ) { @@ -424,6 +458,7 @@ class ApiQueryInfo extends ApiQueryBase { if ( $this->fld_url ) { $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT ); + $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL ); } if ( $this->fld_readable && $title->userCan( 'read', $this->getUser() ) ) { $pageInfo['readable'] = ''; @@ -784,6 +819,7 @@ class ApiQueryInfo extends ApiQueryBase { // need to be added to getCacheMode() ) ), 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_DFLT => null, ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ) @@ -802,7 +838,7 @@ class ApiQueryInfo extends ApiQueryBase { ' watchers - The number of watchers, if allowed', ' notificationtimestamp - The watchlist notification timestamp of each page', ' subjectid - The page ID of the parent page for each talk page', - ' url - Gives a full URL to the page, and also an edit URL', + ' url - Gives a full URL, an edit URL, and the canonical URL for each page', ' readable - Whether the user can read this page', ' preload - Gives the text returned by EditFormPreloadText', ' displaytitle - Gives the way the page title is actually displayed', @@ -812,72 +848,6 @@ class ApiQueryInfo extends ApiQueryBase { ); } - public function getResultProperties() { - $props = array( - ApiBase::PROP_LIST => false, - '' => array( - 'touched' => 'timestamp', - 'lastrevid' => 'integer', - 'counter' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'length' => 'integer', - 'redirect' => 'boolean', - 'new' => 'boolean', - 'starttimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ), - 'contentmodel' => 'string', - ), - 'watched' => array( - 'watched' => 'boolean' - ), - 'watchers' => array( - 'watchers' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'notificationtimestamp' => array( - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'talkid' => array( - 'talkid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'subjectid' => array( - 'subjectid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'url' => array( - 'fullurl' => 'string', - 'editurl' => 'string' - ), - 'readable' => array( - 'readable' => 'boolean' - ), - 'preload' => array( - 'preload' => 'string' - ), - 'displaytitle' => array( - 'displaytitle' => 'string' - ) - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { return 'Get basic page information such as namespace, title, last touched date, ...'; } diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php index 13711e6c83..34842c6333 100644 --- a/includes/api/ApiQueryLangBacklinks.php +++ b/includes/api/ApiQueryLangBacklinks.php @@ -209,23 +209,6 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string', - 'redirect' => 'boolean' - ), - 'lllang' => array( - 'lllang' => 'string' - ), - 'lltitle' => array( - 'lltitle' => 'string' - ) - ); - } - public function getDescription() { return array( 'Find all pages that link to the given language link.', 'Can be used to find all links with a language code, or', @@ -235,12 +218,6 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'lang' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=langbacklinks&lbltitle=Test&lbllang=fr', diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index 53cfba1ec8..da05f2732b 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -50,6 +50,7 @@ class ApiQueryLangLinks extends ApiQueryBase { // Handle deprecated param $this->requireMaxOneParameter( $params, 'url', 'prop' ); if ( $params['url'] ) { + $this->logFeatureUsage( 'prop=langlinks&llurl' ); $prop = array( 'url' => 1 ); } @@ -191,42 +192,10 @@ class ApiQueryLangLinks extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'lang' => 'string', - 'url' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'langname' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'autonym' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - '*' => 'string' - ) - ); - } - public function getDescription() { return 'Returns all interlanguage links from the given page(s).'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getRequireMaxOneParameterErrorMessages( - array( 'url', 'prop' ) - ), - array( - array( 'missingparam', 'lang' ), - ) - ); - } - public function getExamples() { return array( 'api.php?action=query&prop=langlinks&titles=Main%20Page&redirects=' diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 7c17938193..71329c4d41 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -223,15 +223,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return "Returns all {$this->description}s from the given page(s)."; } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 7cd8aacd19..d3607e111c 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -566,80 +566,10 @@ class ApiQueryLogEvents extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - 'ids' => array( - 'logid' => 'integer', - 'pageid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'type' => array( - 'type' => array( - ApiBase::PROP_TYPE => $this->getConfig()->get( 'LogTypes' ) - ), - 'action' => 'string' - ), - 'details' => array( - 'actionhidden' => 'boolean' - ), - 'user' => array( - 'userhidden' => 'boolean', - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'anon' => 'boolean' - ), - 'userid' => array( - 'userhidden' => 'boolean', - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'anon' => 'boolean' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Get events from logs.'; } - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - $this->getRequireMaxOneParameterErrorMessages( - array( 'title', 'prefix', 'namespace' ) ), - array( - array( 'code' => 'param_user', 'info' => 'User name $user not found' ), - array( 'code' => 'param_title', 'info' => 'Bad title value \'title\'' ), - array( 'code' => 'param_prefix', 'info' => 'Bad title value \'prefix\'' ), - array( 'code' => 'prefixsearchdisabled', - 'info' => 'Prefix search disabled in Miser Mode' ), - ) - ); - } - public function getExamples() { return array( 'api.php?action=query&list=logevents' diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php index 2cc18c5985..4c88be7a9d 100644 --- a/includes/api/ApiQueryProtectedTitles.php +++ b/includes/api/ApiQueryProtectedTitles.php @@ -243,42 +243,6 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'user' => array( - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userid' => 'integer' - ), - 'userid' => array( - 'userid' => 'integer' - ), - 'comment' => array( - 'comment' => 'string' - ), - 'parsedcomment' => array( - 'parsedcomment' => 'string' - ), - 'expiry' => array( - 'expiry' => 'timestamp' - ), - 'level' => array( - 'level' => array( - ApiBase::PROP_TYPE => array_diff( $this->getConfig()->get( 'RestrictionLevels' ), array( '' ) ) - ) - ) - ); - } - public function getDescription() { return 'List all titles protected from creation.'; } diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 1a7f826a79..5ddd94508c 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -163,48 +163,10 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_ROOT => array( - 'name' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => false - ), - 'disabled' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => false - ), - 'cached' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => false - ), - 'cachedtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - '' => array( - 'value' => 'string', - 'timestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ), - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return 'Get a list provided by a QueryPage-based special page.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'specialpage-cantexecute' ) - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=querypage&qppage=Ancientpages' diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index 07f8a0e405..530557e6c4 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -173,16 +173,6 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'id' => 'integer', - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return array( 'Get a set of random pages.', diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index cb4e3e8389..6f0c5d3477 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -47,6 +47,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { * Get an array mapping token names to their handler functions. * The prototype for a token function is func($pageid, $title, $rc) * it should return a token or false (permission denied) + * @deprecated since 1.24 * @return array Array(tokenname => function) */ protected function getTokenFunctions() { @@ -69,6 +70,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } /** + * @deprecated since 1.24 * @param int $pageid * @param Title $title * @param RecentChange|null $rc @@ -657,6 +659,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ) ), 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ), ApiBase::PARAM_ISMULTI => true ), @@ -737,122 +740,10 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - $props = array( - '' => array( - 'type' => array( - ApiBase::PROP_TYPE => array( - 'edit', - 'new', - 'move', - 'log', - 'move over redirect' - ) - ) - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string', - 'new_ns' => array( - ApiBase::PROP_TYPE => 'namespace', - ApiBase::PROP_NULLABLE => true - ), - 'new_title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'ids' => array( - 'rcid' => 'integer', - 'pageid' => 'integer', - 'revid' => 'integer', - 'old_revid' => 'integer' - ), - 'user' => array( - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'flags' => array( - 'bot' => 'boolean', - 'new' => 'boolean', - 'minor' => 'boolean' - ), - 'sizes' => array( - 'oldlen' => 'integer', - 'newlen' => 'integer' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'comment' => array( - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'redirect' => array( - 'redirect' => 'boolean' - ), - 'patrolled' => array( - 'patrolled' => 'boolean', - 'unpatrolled' => 'boolean' - ), - 'loginfo' => array( - 'logid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'logtype' => array( - ApiBase::PROP_TYPE => $this->getConfig()->get( 'LogTypes' ), - ApiBase::PROP_NULLABLE => true - ), - 'logaction' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sha1' => array( - 'sha1' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'sha1hidden' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => true - ), - ), - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { return 'Enumerate recent changes.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'show' ), - array( - 'code' => 'permissiondenied', - 'info' => 'You need the patrol right to request the patrolled flag' - ), - array( 'code' => 'user-excludeuser', 'info' => 'user and excludeuser cannot be used together' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=recentchanges' diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 582f61e33b..da4ec19541 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -48,6 +48,7 @@ class ApiQueryRevisions extends ApiQueryBase { private $tokenFunctions; + /** @deprecated since 1.24 */ protected function getTokenFunctions() { // tokenname => function // function prototype is func($pageid, $title, $rev) @@ -72,6 +73,7 @@ class ApiQueryRevisions extends ApiQueryBase { } /** + * @deprecated since 1.24 * @param int $pageid * @param Title $title * @param Revision $rev @@ -748,6 +750,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'parse' => false, 'section' => null, 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ), ApiBase::PARAM_ISMULTI => true ), @@ -807,70 +810,6 @@ class ApiQueryRevisions extends ApiQueryBase { ); } - public function getResultProperties() { - $props = array( - '' => array(), - 'ids' => array( - 'revid' => 'integer', - 'parentid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'flags' => array( - 'minor' => 'boolean' - ), - 'user' => array( - 'userhidden' => 'boolean', - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userhidden' => 'boolean', - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'size' => array( - 'size' => 'integer' - ), - 'sha1' => array( - 'sha1' => 'string' - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'content' => array( - '*' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'texthidden' => 'boolean', - 'textmissing' => 'boolean', - ), - 'contentmodel' => array( - 'contentmodel' => 'string' - ), - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { return array( 'Get revision information.', @@ -882,33 +821,6 @@ class ApiQueryRevisions extends ApiQueryBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'nosuchrevid', 'diffto' ), - array( - 'code' => 'revids', - 'info' => 'The revids= parameter may not be used with the list options ' - . '(limit, startid, endid, dirNewer, start, end).' - ), - array( - 'code' => 'multpages', - 'info' => 'titles, pageids or a generator was used to supply multiple pages, ' - . ' but the limit, startid, endid, dirNewer, user, excludeuser, ' - . 'start and end parameters may only be used on a single page.' - ), - array( - 'code' => 'diffto', - 'info' => 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"' - ), - array( 'code' => 'badparams', 'info' => 'start and startid cannot be used together' ), - array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ), - array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ), - array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ), - array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied ' - . ' to the page\'s content model' ), - ) ); - } - public function getExamples() { return array( 'Get data with content for the last revision of titles "API" and "Main Page"', diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 1c411134d8..b7dcd0ed2b 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -67,6 +67,16 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $searchInfo = array_flip( $params['info'] ); $prop = array_flip( $params['prop'] ); + // Deprecated parameters + if ( isset( $prop['hasrelated'] ) ) { + $this->logFeatureUsage( 'action=search&srprop=hasrelated' ); + $this->setWarning( 'srprop=hasrelated has been deprecated' ); + } + if ( isset( $prop['score'] ) ) { + $this->logFeatureUsage( 'action=search&srprop=score' ); + $this->setWarning( 'srprop=score has been deprecated' ); + } + // Create search engine instance and set options $search = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ? SearchEngine::create( $params['backend'] ) : SearchEngine::create(); @@ -157,9 +167,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { if ( isset( $prop['timestamp'] ) ) { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $result->getTimestamp() ); } - if ( !is_null( $result->getScore() ) && isset( $prop['score'] ) ) { - $vals['score'] = $result->getScore(); - } if ( isset( $prop['titlesnippet'] ) ) { $vals['titlesnippet'] = $result->getTitleSnippet( $terms ); } @@ -179,9 +186,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $vals['sectionsnippet'] = $result->getSectionSnippet(); } } - if ( isset( $prop['hasrelated'] ) && $result->hasRelated() ) { - $vals['hasrelated'] = ''; - } // Add item to results and see whether it fits $fit = $apiResult->addValue( array( 'query', $this->getModuleName() ), @@ -200,7 +204,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $hasInterwikiResults = false; if ( $interwiki && $resultPageSet === null && $matches->hasInterwikiResults() ) { $matches = $matches->getInterwikiResults(); - $iwprefixes = array(); $hasInterwikiResults = true; // Include number of results if requested @@ -336,14 +339,14 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ' size - Adds the size of the page in bytes', ' wordcount - Adds the word count of the page', ' timestamp - Adds the timestamp of when the page was last edited', - ' score - Adds the score (if any) from the search engine', + ' score - DEPRECATED and IGNORED', ' snippet - Adds a parsed snippet of the page', ' titlesnippet - Adds a parsed snippet of the page title', ' redirectsnippet - Adds a parsed snippet of the redirect title', ' redirecttitle - Adds the title of the matching redirect', ' sectionsnippet - Adds a parsed snippet of the matching section title', ' sectiontitle - Adds the title of the matching section', - ' hasrelated - Indicates whether a related search is available', + ' hasrelated - DEPRECATED and IGNORED', ), 'offset' => 'Use this value to continue paging (return by query)', 'limit' => 'How many total pages to return', @@ -357,75 +360,10 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { return $descriptions; } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'snippet' => array( - 'snippet' => 'string' - ), - 'size' => array( - 'size' => 'integer' - ), - 'wordcount' => array( - 'wordcount' => 'integer' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'score' => array( - 'score' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'titlesnippet' => array( - 'titlesnippet' => 'string' - ), - 'redirecttitle' => array( - 'redirecttitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'redirectsnippet' => array( - 'redirectsnippet' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sectiontitle' => array( - 'sectiontitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sectionsnippet' => array( - 'sectionsnippet' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'hasrelated' => array( - 'hasrelated' => 'boolean' - ) - ); - } - public function getDescription() { return 'Perform a full text search.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'search-text-disabled', 'info' => 'text search is disabled' ), - array( 'code' => 'search-title-disabled', 'info' => 'title search is disabled' ), - array( 'code' => 'search-error', 'info' => 'search error has occurred' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=search&srsearch=meaning', diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 30201fc29e..522c7c0805 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -138,6 +138,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['phpversion'] = PHP_VERSION; $data['phpsapi'] = PHP_SAPI; + if ( defined( 'HHVM_VERSION' ) ) { + $data['hhvmversion'] = HHVM_VERSION; + } $data['dbtype'] = $config->get( 'DBtype' ); $data['dbversion'] = $this->getDB()->getServerVersion(); @@ -166,7 +169,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { if ( $wgContLang->linkPrefixExtension() ) { $linkPrefixCharset = $wgContLang->linkPrefixCharset(); $data['linkprefixcharset'] = $linkPrefixCharset; - // For backwards compatability + // For backwards compatibility $data['linkprefix'] = "/^((?>.*[^$linkPrefixCharset]|))(.+)$/sDu"; } else { $data['linkprefixcharset'] = ''; @@ -393,7 +396,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $prefix = $row['iw_prefix']; $val = array(); $val['prefix'] = $prefix; - if ( $row['iw_local'] == '1' ) { + if ( isset( $row['iw_local'] ) && $row['iw_local'] == '1' ) { $val['local'] = ''; } if ( $row['iw_trans'] == '1' ) { @@ -840,7 +843,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { ' fileextensions - Returns list of file extensions allowed to be uploaded', ' rightsinfo - Returns wiki rights (license) information if available', ' restrictions - Returns information on available restriction (protection) types', - ' languages - Returns a list of languages MediaWiki supports' . + ' languages - Returns a list of languages MediaWiki supports ' . "(optionally localised by using {$p}inlanguagecode)", ' skins - Returns a list of all enabled skins', ' extensiontags - Returns a list of parser extension tags', @@ -862,13 +865,6 @@ class ApiQuerySiteinfo extends ApiQueryBase { return 'Return general information about the site.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( array( - 'code' => 'includeAllDenied', - 'info' => 'Cannot view all servers info unless $wgShowHostnames is true' - ), ) ); - } - public function getExamples() { return array( 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics', diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index d9409ec46e..db9285602a 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -47,6 +47,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { // Alias sessionkey to filekey, but give an existing filekey precedence. if ( !$params['filekey'] && $params['sessionkey'] ) { + $this->logFeatureUsage( 'prop=stashimageinfo&siisessionkey' ); $params['filekey'] = $params['sessionkey']; } @@ -124,10 +125,6 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { ); } - public function getResultProperties() { - return ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter ); - } - public function getDescription() { return 'Returns image information for stashed images.'; } diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php index 77c105aeda..3184564845 100644 --- a/includes/api/ApiQueryTags.php +++ b/includes/api/ApiQueryTags.php @@ -170,23 +170,6 @@ class ApiQueryTags extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string' - ), - 'displayname' => array( - 'displayname' => 'string' - ), - 'description' => array( - 'description' => 'string' - ), - 'hitcount' => array( - 'hitcount' => 'integer' - ) - ); - } - public function getDescription() { return 'List change tags.'; } diff --git a/includes/api/ApiQueryTokens.php b/includes/api/ApiQueryTokens.php new file mode 100644 index 0000000000..ba9c937712 --- /dev/null +++ b/includes/api/ApiQueryTokens.php @@ -0,0 +1,104 @@ +<?php +/** + * Module to fetch tokens via action=query&meta=tokens + * + * Created on August 8, 2014 + * + * Copyright © 2014 Brad Jorsch bjorsch@wikimedia.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.24 + */ + +/** + * Module to fetch tokens via action=query&meta=tokens + * + * @ingroup API + * @since 1.24 + */ +class ApiQueryTokens extends ApiQueryBase { + + public function execute() { + $params = $this->extractRequestParams(); + $res = array(); + + if ( $this->getMain()->getRequest()->getVal( 'callback' ) !== null ) { + $this->setWarning( 'Tokens may not be obtained when using a callback' ); + return; + } + + $salts = self::getTokenTypeSalts(); + foreach ( $params['type'] as $type ) { + $salt = $salts[$type]; + $val = $this->getUser()->getEditToken( $salt, $this->getRequest() ); + $res[$type . 'token'] = $val; + } + + $this->getResult()->addValue( 'query', $this->getModuleName(), $res ); + } + + public static function getTokenTypeSalts() { + static $salts = null; + if ( !$salts ) { + wfProfileIn( __METHOD__ ); + $salts = array( + 'csrf' => '', + 'watch' => 'watch', + 'patrol' => 'patrol', + 'rollback' => 'rollback', + 'userrights' => 'userrights', + ); + wfRunHooks( 'ApiQueryTokensRegisterTypes', array( &$salts ) ); + ksort( $salts ); + wfProfileOut( __METHOD__ ); + } + + return $salts; + } + + public function getAllowedParams() { + return array( + 'type' => array( + ApiBase::PARAM_DFLT => 'csrf', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array_keys( self::getTokenTypeSalts() ), + ), + ); + } + + public function getParamDescription() { + return array( + 'type' => 'Type of token(s) to request' + ); + } + + public function getDescription() { + return 'Gets tokens for data-modifying actions.'; + } + + protected function getExamples() { + return array( + 'api.php?action=query&meta=tokens' => 'Retrieve a csrf token (the default)', + 'api.php?action=query&meta=tokens&type=watch|patrol' => 'Retrieve a watch token and a patrol token' + ); + } + + public function getCacheMode( $params ) { + return 'private'; + } +} diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 29d03001c7..4b167b8b7a 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -224,6 +224,7 @@ class ApiQueryContributions extends ApiQueryBase { $show = $this->params['show']; if ( $this->params['toponly'] ) { // deprecated/old param + $this->logFeatureUsage( 'list=usercontribs&uctoponly' ); $show[] = 'top'; } if ( !is_null( $show ) ) { @@ -555,81 +556,10 @@ class ApiQueryContributions extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'userid' => 'integer', - 'user' => 'string', - 'userhidden' => 'boolean' - ), - 'ids' => array( - 'pageid' => 'integer', - 'revid' => 'integer', - 'parentid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'flags' => array( - 'new' => 'boolean', - 'minor' => 'boolean', - 'top' => 'boolean' - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'patrolled' => array( - 'patrolled' => 'boolean' - ), - 'size' => array( - 'size' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sizediff' => array( - 'sizediff' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Get all edits by a user.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'param_user', 'info' => 'User parameter may not be empty.' ), - array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), - array( 'show' ), - array( - 'code' => 'permissiondenied', - 'info' => 'You need the patrol right to request the patrolled flag' - ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=usercontribs&ucuser=YurikBot', diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 140f35a832..8b7831cb8e 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -104,6 +104,12 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['options'] = $user->getOptions(); } + if ( isset( $this->prop['preferencestoken'] ) ) { + $p = $this->getModulePrefix(); + $this->setWarning( + "{$p}prop=preferencestoken has been deprecated. Please use action=query&meta=tokens instead." + ); + } if ( isset( $this->prop['preferencestoken'] ) && is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) && $user->isAllowed( 'editmyoptions' ) @@ -252,7 +258,7 @@ class ApiQueryUserInfo extends ApiQueryBase { ' rights - Lists all the rights the current user has', ' changeablegroups - Lists the groups the current user can add to and remove from', ' options - Lists all preferences the current user has set', - ' preferencestoken - Get a token to change current user\'s preferences', + ' preferencestoken - DEPRECATED! Get a token to change current user\'s preferences', ' editcount - Adds the current user\'s edit count', ' ratelimits - Lists all rate limits applying to the current user', ' realname - Adds the user\'s real name', @@ -267,63 +273,6 @@ class ApiQueryUserInfo extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => false, - '' => array( - 'id' => 'integer', - 'name' => 'string', - 'anon' => 'boolean' - ), - 'blockinfo' => array( - 'blockid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedby' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedbyid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedreason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'hasmsg' => array( - 'messages' => 'boolean' - ), - 'preferencestoken' => array( - 'preferencestoken' => 'string' - ), - 'editcount' => array( - 'editcount' => 'integer' - ), - 'realname' => array( - 'realname' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'email' => array( - 'email' => 'string', - 'emailauthenticated' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'registrationdate' => array( - 'registrationdate' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Get information about the current user.'; } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index d0d0f08223..b62d6a83e0 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -58,6 +58,7 @@ class ApiQueryUsers extends ApiQueryBase { * Get an array mapping token names to their handler functions. * The prototype for a token function is func($user) * it should return a token or false (permission denied) + * @deprecated since 1.24 * @return array Array of tokenname => function */ protected function getTokenFunctions() { @@ -80,6 +81,7 @@ class ApiQueryUsers extends ApiQueryBase { } /** + * @deprecated since 1.24 * @param User $user * @return string */ @@ -317,6 +319,7 @@ class ApiQueryUsers extends ApiQueryBase { ApiBase::PARAM_ISMULTI => true ), 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ), ApiBase::PARAM_ISMULTI => true ), @@ -342,73 +345,6 @@ class ApiQueryUsers extends ApiQueryBase { ); } - public function getResultProperties() { - $props = array( - '' => array( - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'name' => 'string', - 'invalid' => 'boolean', - 'hidden' => 'boolean', - 'interwiki' => 'boolean', - 'missing' => 'boolean' - ), - 'editcount' => array( - 'editcount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'registration' => array( - 'registration' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'blockinfo' => array( - 'blockid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedby' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedbyid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedreason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedexpiry' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'emailable' => array( - 'emailable' => 'boolean' - ), - 'gender' => array( - 'gender' => array( - ApiBase::PROP_TYPE => array( - 'male', - 'female', - 'unknown' - ), - ApiBase::PROP_NULLABLE => true - ) - ) - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { return 'Get information about a list of users.'; } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index b1b84d87b1..efbe05ee65 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -561,109 +561,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'type' => array( - ApiBase::PROP_TYPE => array( - 'edit', - 'new', - 'move', - 'log', - 'move over redirect' - ) - ) - ), - 'ids' => array( - 'pageid' => 'integer', - 'revid' => 'integer', - 'old_revid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'user' => array( - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'flags' => array( - 'new' => 'boolean', - 'minor' => 'boolean', - 'bot' => 'boolean' - ), - 'patrol' => array( - 'patrolled' => 'boolean' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'sizes' => array( - 'oldlen' => 'integer', - 'newlen' => 'integer' - ), - 'notificationtimestamp' => array( - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'comment' => array( - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'loginfo' => array( - 'logid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'logtype' => array( - ApiBase::PROP_TYPE => $this->getConfig()->get( 'LogTypes' ), - ApiBase::PROP_NULLABLE => true - ), - 'logaction' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return "Get all recent changes to pages in the logged in user's watchlist."; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'bad_wlowner', 'info' => 'Specified user does not exist' ), - array( - 'code' => 'bad_wltoken', - 'info' => 'Incorrect watchlist token provided -- ' . - 'please set a correct token in Special:Preferences' - ), - array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ), - array( 'code' => 'patrol', 'info' => 'patrol property is not available' ), - array( 'show' ), - array( - 'code' => 'permissiondenied', - 'info' => 'You need the patrol right to request the patrolled flag' - ), - array( 'code' => 'user-excludeuser', 'info' => 'user and excludeuser cannot be used together' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=watchlist', diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index 6aae6dc33f..6b2223ac87 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -189,38 +189,10 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'changed' => array( - 'changed' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return "Get all pages on the logged in user's watchlist."; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ), - array( 'show' ), - array( 'code' => 'bad_wlowner', 'info' => 'Specified user does not exist' ), - array( - 'code' => 'bad_wltoken', - 'info' => 'Incorrect watchlist token provided -- ' . - 'please set a correct token in Special:Preferences' - ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=watchlistraw', diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 97b74e5640..2e80447ec7 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -461,7 +461,7 @@ class ApiResult extends ApiBase { * * @since 1.24 * @param string|null $continue The "continue" parameter, if any - * @param array $allModules Contains ApiBase instances that will be executed + * @param ApiBase[] $allModules Contains ApiBase instances that will be executed * @param array $generatedModules Names of modules that depend on the generator * @return array Two elements: a boolean indicating if the generator is done, * and an array of modules to actually execute. diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php index 2c76f371ee..cbc3070422 100644 --- a/includes/api/ApiRevisionDelete.php +++ b/includes/api/ApiRevisionDelete.php @@ -195,10 +195,6 @@ class ApiRevisionDelete extends ApiBase { ApiBase::PARAM_TYPE => array( 'yes', 'no', 'nochange' ), ApiBase::PARAM_DFLT => 'nochange', ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => null, ); } @@ -211,7 +207,6 @@ class ApiRevisionDelete extends ApiBase { 'hide' => 'What to hide for each revision', 'show' => 'What to unhide for each revision', 'suppress' => 'Whether to suppress data from administrators as well as others', - 'token' => 'A delete token previously retrieved through action=tokens', 'reason' => 'Reason for the deletion/undeletion', ); } @@ -220,22 +215,8 @@ class ApiRevisionDelete extends ApiBase { return 'Delete/undelete revisions.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - array( - array( 'code' => 'needtarget', - 'info' => 'A target title is required for this RevDel type' ), - array( 'code' => 'badparams', 'info' => 'Bad value for some parameter' ), - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 1bba715448..f4d3c5414d 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -40,9 +40,19 @@ class ApiRollback extends ApiBase { private $mUser = null; public function execute() { + $user = $this->getUser(); $params = $this->extractRequestParams(); - // User and title already validated in call to getTokenSalt from Main + // WikiPage::doRollback needs a Web UI token, so get one of those if we + // validated based on an API rollback token. + $token = $params['token']; + if ( $user->matchEditToken( $token, 'rollback', $this->getRequest() ) ) { + $token = $this->getUser()->getEditToken( + $this->getWebUITokenSalt( $params ), + $this->getRequest() + ); + } + $titleObj = $this->getRbTitle( $params ); $pageObj = WikiPage::factory( $titleObj ); $summary = $params['summary']; @@ -50,10 +60,10 @@ class ApiRollback extends ApiBase { $retval = $pageObj->doRollback( $this->getRbUser( $params ), $summary, - $params['token'], + $token, $params['markbot'], $details, - $this->getUser() + $user ); if ( $retval ) { @@ -99,10 +109,6 @@ class ApiRollback extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'summary' => '', 'markbot' => false, 'watchlist' => array( @@ -123,10 +129,11 @@ class ApiRollback extends ApiBase { return array( 'title' => "Title of the page you want to roll back. Cannot be used together with {$p}pageid", 'pageid' => "Page ID of the page you want to roll back. Cannot be used together with {$p}title", - 'user' => 'Name of the user whose edits are to be rolled back. If ' . - 'set incorrectly, you\'ll get a badtoken error.', - 'token' => 'A rollback token previously retrieved through ' . - "{$this->getModulePrefix()}prop=revisions", + 'user' => 'Name of the user whose edits are to be rolled back.', + 'token' => array( + /* Standard description automatically prepended */ + 'For compatibility, the token used in the web UI is also accepted.' + ), 'summary' => 'Custom edit summary. If empty, default summary will be used', 'markbot' => 'Mark the reverted edits and the revert as bot edits', 'watchlist' => 'Unconditionally add or remove the page from your watchlist, ' . @@ -134,19 +141,6 @@ class ApiRollback extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'pageid' => 'integer', - 'summary' => 'string', - 'revid' => 'integer', - 'old_revid' => 'integer', - 'last_revid' => 'integer' - ) - ); - } - public function getDescription() { return array( 'Undo the last edit to the page. If the last user who edited the page made', @@ -154,26 +148,11 @@ class ApiRollback extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), - array( - array( 'invalidtitle', 'title' ), - array( 'notanarticle' ), - array( 'nosuchpageid', 'pageid' ), - array( 'invaliduser', 'user' ), - ) - ); - } - public function needsToken() { - return true; + return 'rollback'; } - public function getTokenSalt() { - $params = $this->extractRequestParams(); - + protected function getWebUITokenSalt( array $params ) { return array( $this->getRbTitle( $params )->getPrefixedText(), $this->getRbUser( $params ) diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index 04be450bb6..5d527fc79b 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -202,11 +202,7 @@ class ApiSetNotificationTimestamp extends ApiBase { } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getAllowedParams( $flags = 0 ) { @@ -214,7 +210,6 @@ class ApiSetNotificationTimestamp extends ApiBase { 'entirewatchlist' => array( ApiBase::PARAM_TYPE => 'boolean' ), - 'token' => null, 'timestamp' => array( ApiBase::PARAM_TYPE => 'timestamp' ), @@ -239,48 +234,10 @@ class ApiSetNotificationTimestamp extends ApiBase { 'timestamp' => 'Timestamp to which to set the notification timestamp', 'torevid' => 'Revision to set the notification timestamp to (one page only)', 'newerthanrevid' => 'Revision to set the notification timestamp newer than (one page only)', - 'token' => 'A token previously acquired via prop=info', 'continue' => 'When more results are available, use this to continue', ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => true, - ApiBase::PROP_ROOT => array( - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - '' => array( - 'ns' => array( - ApiBase::PROP_TYPE => 'namespace', - ApiBase::PROP_NULLABLE => true - ), - 'title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'pageid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'revid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'invalid' => 'boolean', - 'missing' => 'boolean', - 'notwatched' => 'boolean', - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return array( 'Update the notification timestamp for watched pages.', 'This affects the highlighting of changed pages in the watchlist and history,', @@ -289,25 +246,6 @@ class ApiSetNotificationTimestamp extends ApiBase { ); } - public function getPossibleErrors() { - $ps = $this->getPageSet(); - - return array_merge( - parent::getPossibleErrors(), - $ps->getFinalPossibleErrors(), - $this->getRequireMaxOneParameterErrorMessages( - array( 'timestamp', 'torevid', 'newerthanrevid' ) ), - $this->getRequireOnlyOneParameterErrorMessages( - array_merge( array( 'entirewatchlist' ), array_keys( $ps->getFinalParams() ) ) ), - array( - array( 'code' => 'notloggedin', 'info' - => 'Anonymous users cannot use watchlist change notifications' ), - array( 'code' => 'multpages', 'info' => 'torevid may only be used with a single page' ), - array( 'code' => 'multpages', 'info' => 'newerthanrevid may only be used with a single page' ), - ) - ); - } - public function getExamples() { return array( 'api.php?action=setnotificationtimestamp&entirewatchlist=&token=123ABC' diff --git a/includes/api/ApiTokens.php b/includes/api/ApiTokens.php index 5e197db7ce..9287fe6e7f 100644 --- a/includes/api/ApiTokens.php +++ b/includes/api/ApiTokens.php @@ -25,11 +25,16 @@ */ /** + * @deprecated since 1.24 * @ingroup API */ class ApiTokens extends ApiBase { public function execute() { + $this->setWarning( + "action=tokens has been deprecated. Please use action=query&meta=tokens instead." + ); + $params = $this->extractRequestParams(); $res = array(); @@ -81,16 +86,6 @@ class ApiTokens extends ApiBase { ); } - public function getResultProperties() { - $props = array( - '' => array(), - ); - - self::addTokenProperties( $props, $this->getTokenTypes() ); - - return $props; - } - public function getParamDescription() { return array( 'type' => 'Type of token(s) to request' @@ -98,7 +93,10 @@ class ApiTokens extends ApiBase { } public function getDescription() { - return 'Gets tokens for data-modifying actions.'; + return array( + 'This module is deprecated in favor of action=query&meta=tokens.', + 'Gets tokens for data-modifying actions.' + ); } protected function getExamples() { diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index f34d4dfa6c..2854a82529 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -89,7 +89,6 @@ class ApiUnblock extends ApiBase { ApiBase::PARAM_TYPE => 'integer', ), 'user' => null, - 'token' => null, 'reason' => '', ); } @@ -102,54 +101,16 @@ class ApiUnblock extends ApiBase { "Cannot be used together with {$p}user", 'user' => "Username, IP address or IP range you want to unblock. " . "Cannot be used together with {$p}id", - 'token' => "An unblock token previously obtained through prop=info", 'reason' => 'Reason for unblock', ); } - public function getResultProperties() { - return array( - '' => array( - 'id' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'reason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Unblock a user.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'unblock-notarget' ), - array( 'unblock-idanduser' ), - array( 'cantunblock' ), - array( 'ipbblocked' ), - array( 'ipbnounblockself' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index 7d6a7e42d3..07aad9f57d 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -56,7 +56,7 @@ class ApiUndelete extends ApiBase { $params['timestamps'][$i] = wfTimestamp( TS_MW, $ts ); } - $pa = new PageArchive( $titleObj ); + $pa = new PageArchive( $titleObj, $this->getConfig() ); $retval = $pa->undelete( ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), $params['reason'], @@ -96,10 +96,6 @@ class ApiUndelete extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => '', 'timestamps' => array( ApiBase::PARAM_TYPE => 'timestamp', @@ -124,10 +120,6 @@ class ApiUndelete extends ApiBase { public function getParamDescription() { return array( 'title' => 'Title of the page you want to restore', - 'token' => array( - 'An undelete token previously retrieved through list=deletedrevs, or ', - 'a delete token retrieved through action=tokens.' - ), 'reason' => 'Reason for restoring', 'timestamps' => array( 'Timestamps of the revisions to restore.', @@ -142,17 +134,6 @@ class ApiUndelete extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'revisions' => 'integer', - 'filerevisions' => 'integer', - 'reason' => 'string' - ) - ); - } - public function getDescription() { return array( 'Restore certain revisions of a deleted page. A list of deleted revisions ', @@ -161,21 +142,8 @@ class ApiUndelete extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'permdenied-undelete' ), - array( 'blockedtext' ), - array( 'invalidtitle', 'title' ), - array( 'cannotundelete' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index 5e6c962d12..657181b77a 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -28,7 +28,7 @@ * @ingroup API */ class ApiUpload extends ApiBase { - /** @var UploadBase */ + /** @var UploadBase|UploadFromChunks */ protected $mUpload = null; protected $mParams; @@ -52,6 +52,7 @@ class ApiUpload extends ApiBase { // Copy the session key to the file key, for backward compatibility. if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) { + $this->logFeatureUsage( 'action=upload&sessionkey' ); $this->mParams['filekey'] = $this->mParams['sessionkey']; } @@ -210,7 +211,6 @@ class ApiUpload extends ApiBase { } } else { $filekey = $this->mParams['filekey']; - /** @var $status Status */ $status = $this->mUpload->addChunk( $chunkPath, $chunkSize, $this->mParams['offset'] ); if ( !$status->isGood() ) { @@ -241,6 +241,7 @@ class ApiUpload extends ApiBase { ) ) ); $result['result'] = 'Poll'; + $result['stage'] = 'queued'; } else { $status = $this->mUpload->concatenateChunks(); if ( !$status->isGood() ) { @@ -467,6 +468,7 @@ class ApiUpload extends ApiBase { /** * Performs file verification, dies on error. + * @param array $verification */ protected function checkVerification( array $verification ) { // @todo Move them to ApiBase's message map @@ -551,6 +553,7 @@ class ApiUpload extends ApiBase { if ( isset( $warnings['duplicate'] ) ) { $dupes = array(); + /** @var File $dupe */ foreach ( $warnings['duplicate'] as $dupe ) { $dupes[] = $dupe->getName(); } @@ -561,6 +564,7 @@ class ApiUpload extends ApiBase { if ( isset( $warnings['exists'] ) ) { $warning = $warnings['exists']; unset( $warnings['exists'] ); + /** @var LocalFile $localFile */ $localFile = isset( $warning['normalizedFile'] ) ? $warning['normalizedFile'] : $warning['file']; @@ -602,6 +606,7 @@ class ApiUpload extends ApiBase { // Deprecated parameters if ( $this->mParams['watch'] ) { + $this->logFeatureUsage( 'action=upload&watch' ); $watch = true; } @@ -627,6 +632,7 @@ class ApiUpload extends ApiBase { ) ) ); $result['result'] = 'Poll'; + $result['stage'] = 'queued'; } else { /** @var $status Status */ $status = $this->mUpload->performUpload( $this->mParams['comment'], @@ -684,10 +690,6 @@ class ApiUpload extends ApiBase { ApiBase::PARAM_DFLT => '' ), 'text' => null, - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'watch' => array( ApiBase::PARAM_DFLT => false, ApiBase::PARAM_DEPRECATED => true, @@ -731,7 +733,6 @@ class ApiUpload extends ApiBase { public function getParamDescription() { $params = array( 'filename' => 'Target filename', - 'token' => 'Edit token. You can get one of these through prop=info', 'comment' => 'Upload comment. Also used as the initial page text for new ' . 'files if "text" is not specified', 'text' => 'Initial page text for new files', @@ -760,41 +761,6 @@ class ApiUpload extends ApiBase { return $params; } - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Warning', - 'Continue', - 'Queued' - ), - ), - 'filekey' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'sessionkey' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'offset' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'statuskey' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'filename' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return array( 'Upload a file, or get the status of pending uploads. Several methods are available:', @@ -802,50 +768,20 @@ class ApiUpload extends ApiBase { ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter', ' * Complete an earlier upload that failed due to warnings, using the "filekey" parameter', 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when', - 'sending the "file". Also you must get and send an edit token before doing any upload stuff.' - ); - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'filekey', 'file', 'url', 'statuskey' ) ), - array( - array( 'uploaddisabled' ), - array( 'invalid-file-key' ), - array( 'uploaddisabled' ), - array( 'mustbeloggedin', 'upload' ), - array( 'badaccess-groups' ), - array( 'code' => 'fetchfileerror', 'info' => '' ), - array( 'code' => 'nomodule', 'info' => 'No upload module set' ), - array( 'code' => 'empty-file', 'info' => 'The file you submitted was empty' ), - array( 'code' => 'filetype-missing', 'info' => 'The file is missing an extension' ), - array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), - array( 'code' => 'overwrite', 'info' => 'Overwriting an existing file is not allowed' ), - array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), - array( 'code' => 'publishfailed', 'info' => 'Publishing of stashed file failed' ), - array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), - array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ), - array( 'code' => 'stasherror', 'info' => 'An upload stash error occurred' ), - array( 'fileexists-forbidden' ), - array( 'fileexists-shared-forbidden' ), - ) + 'sending the "file".', ); } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( 'api.php?action=upload&filename=Wiki.png' . - '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png' + '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC' => 'Upload from a URL', - 'api.php?action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1' + 'api.php?action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC' => 'Complete an upload that failed due to warnings', ); } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index 0bed859356..c3ceb3457b 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -35,7 +35,7 @@ class ApiUserrights extends ApiBase { public function execute() { $params = $this->extractRequestParams(); - $user = $this->getUrUser(); + $user = $this->getUrUser( $params ); $form = new UserrightsPage; $form->setContext( $this->getContext() ); @@ -53,14 +53,14 @@ class ApiUserrights extends ApiBase { } /** + * @param array $params * @return User */ - private function getUrUser() { + private function getUrUser( array $params ) { if ( $this->mUser !== null ) { return $this->mUser; } - $params = $this->extractRequestParams(); $this->requireOnlyOneParameter( $params, 'user', 'userid' ); $user = isset( $params['user'] ) ? $params['user'] : '#' . $params['userid']; @@ -101,10 +101,6 @@ class ApiUserrights extends ApiBase { ApiBase::PARAM_TYPE => User::getAllGroups(), ApiBase::PARAM_ISMULTI => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => array( ApiBase::PARAM_DFLT => '' ) @@ -117,7 +113,10 @@ class ApiUserrights extends ApiBase { 'userid' => 'User id', 'add' => 'Add the user to these groups', 'remove' => 'Remove the user from these groups', - 'token' => 'A userrights token previously retrieved through list=users', + 'token' => array( + /* Standard description automatically prepended */ + 'For compatibility, the token used in the web UI is also accepted.' + ), 'reason' => 'Reason for the change', ); } @@ -127,11 +126,11 @@ class ApiUserrights extends ApiBase { } public function needsToken() { - return true; + return 'userrights'; } - public function getTokenSalt() { - return $this->getUrUser()->getName(); + protected function getWebUITokenSalt( array $params ) { + return $this->getUrUser( $params )->getName(); } public function getExamples() { diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index c5aa90ef26..e6a660b36f 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -84,6 +84,7 @@ class ApiWatch extends ApiBase { ); } + $this->logFeatureUsage( 'action=watch&title' ); $title = Title::newFromText( $params['title'] ); if ( !$title || !$title->isWatchable() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); @@ -165,10 +166,6 @@ class ApiWatch extends ApiBase { } public function needsToken() { - return true; - } - - public function getTokenSalt() { return 'watch'; } @@ -180,10 +177,6 @@ class ApiWatch extends ApiBase { ), 'unwatch' => false, 'uselang' => null, - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'continue' => '', ); if ( $flags ) { @@ -200,34 +193,14 @@ class ApiWatch extends ApiBase { 'title' => 'The page to (un)watch. use titles instead', 'unwatch' => 'If set the page will be unwatched rather than watched', 'uselang' => 'Language to show the message in', - 'token' => 'A token previously acquired via prop=info', 'continue' => 'When more results are available, use this to continue', ); } - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'unwatched' => 'boolean', - 'watched' => 'boolean', - 'message' => 'string' - ) - ); - } - public function getDescription() { return 'Add or remove pages from/to the current user\'s watchlist.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ), - array( 'invalidtitle', 'title' ), - array( 'hookaborted' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=watch&titles=Main_Page' => 'Watch the page "Main Page"', diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php index 76cc583636..48c063f4e5 100644 --- a/includes/cache/LinkBatch.php +++ b/includes/cache/LinkBatch.php @@ -180,6 +180,8 @@ class LinkBatch { * @return bool|ResultWrapper */ public function doQuery() { + global $wgContentHandlerUseDB; + if ( $this->isEmpty() ) { return false; } @@ -190,6 +192,11 @@ class LinkBatch { $table = 'page'; $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_len', 'page_is_redirect', 'page_latest' ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'page_content_model'; + } + $conds = $this->constructSet( 'page', $dbr ); // Do query diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index e80dfb3167..6925df9013 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -129,10 +129,10 @@ class LinkCache { * @param int $len Text's length * @param int $redir Whether the page is a redirect * @param int $revision Latest revision's ID - * @param int $model Latest revision's content model ID + * @param string|null $model Latest revision's content model ID */ public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, - $revision = 0, $model = 0 + $revision = 0, $model = null ) { $dbkey = $title->getPrefixedDBkey(); $this->mGoodLinks[$dbkey] = (int)$id; @@ -140,7 +140,7 @@ class LinkCache { 'length' => (int)$len, 'redirect' => (int)$redir, 'revision' => (int)$revision, - 'model' => (int)$model + 'model' => $model ? (string)$model : null, ); } diff --git a/includes/cache/LocalisationCache.php b/includes/cache/LocalisationCache.php index cf87129732..ae27fba3a4 100644 --- a/includes/cache/LocalisationCache.php +++ b/includes/cache/LocalisationCache.php @@ -20,8 +20,6 @@ * @file */ -define( 'MW_LC_VERSION', 2 ); - /** * Class for caching the contents of localisation files, Messages*.php * and *.i18n.php. @@ -35,6 +33,8 @@ define( 'MW_LC_VERSION', 2 ); * as grammatical transformation, is done by the caller. */ class LocalisationCache { + const VERSION = 2; + /** Configuration associative array */ private $conf; @@ -200,9 +200,6 @@ class LocalisationCache { case 'db': $storeClass = 'LCStoreDB'; break; - case 'accel': - $storeClass = 'LCStoreAccel'; - break; case 'detect': $storeClass = $wgCacheDirectory ? 'LCStoreCDB' : 'LCStoreDB'; break; @@ -404,7 +401,7 @@ class LocalisationCache { $deps = $this->store->get( $code, 'deps' ); $keys = $this->store->get( $code, 'list' ); $preload = $this->store->get( $code, 'preload' ); - // Different keys may expire separately, at least in LCStoreAccel + // Different keys may expire separately for some stores if ( $deps === null || $keys === null || $preload === null ) { wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" ); @@ -686,6 +683,7 @@ class LocalisationCache { * * @param string $code * @param array $deps + * @return array */ protected function readSourceFilesAndRegisterDeps( $code, &$deps ) { global $IP; @@ -773,7 +771,7 @@ class LocalisationCache { * * Returns true if any data from the extension array was used, false * otherwise. - * @param string $codeSequence + * @param array $codeSequence * @param string $key * @param mixed $value * @param mixed $fallbackValue @@ -842,78 +840,114 @@ class LocalisationCache { if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) { $coreData['fallbackSequence'][] = 'en'; } + } - # Load the fallback localisation item by item and merge it - foreach ( $coreData['fallbackSequence'] as $fbCode ) { - # Load the secondary localisation from the source file to - # avoid infinite cycles on cyclic fallbacks - $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps ); - if ( $fbData === false ) { - continue; - } + $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] ); - foreach ( self::$allKeys as $key ) { - if ( !isset( $fbData[$key] ) ) { - continue; - } + wfProfileIn( __METHOD__ . '-fallbacks' ); + + # Load non-JSON localisation data for extensions + $extensionData = array_combine( + $codeSequence, + array_fill( 0, count( $codeSequence ), $initialData ) ); + foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) { + if ( isset( $wgMessagesDirs[$extension] ) ) { + # This extension has JSON message data; skip the PHP shim + continue; + } + + $data = $this->readPHPFile( $fileName, 'extension' ); + $used = false; - if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) { - $this->mergeItem( $key, $coreData[$key], $fbData[$key] ); + foreach ( $data as $key => $item ) { + foreach ( $codeSequence as $csCode ) { + if ( isset( $item[$csCode] ) ) { + $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] ); + $used = true; } } } - } - $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] ); + if ( $used ) { + $deps[] = new FileDependency( $fileName ); + } + } - # Load core messages and the extension localisations. - wfProfileIn( __METHOD__ . '-extensions' ); + # Load the localisation data for each fallback, then merge it into the full array $allData = $initialData; - foreach ( $wgMessagesDirs as $dirs ) { - foreach ( (array)$dirs as $dir ) { - foreach ( $codeSequence as $csCode ) { + foreach ( $codeSequence as $csCode ) { + $csData = $initialData; + + # Load core messages and the extension localisations. + foreach ( $wgMessagesDirs as $dirs ) { + foreach ( (array)$dirs as $dir ) { $fileName = "$dir/$csCode.json"; $data = $this->readJSONFile( $fileName ); foreach ( $data as $key => $item ) { - $this->mergeItem( $key, $allData[$key], $item ); + $this->mergeItem( $key, $csData[$key], $item ); } $deps[] = new FileDependency( $fileName ); } } - } - foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) { - if ( isset( $wgMessagesDirs[$extension] ) ) { - # Already loaded the JSON files for this extension; skip the PHP shim - continue; + # Merge non-JSON extension data + if ( isset( $extensionData[$csCode] ) ) { + foreach ( $extensionData[$csCode] as $key => $item ) { + $this->mergeItem( $key, $csData[$key], $item ); + } } - $data = $this->readPHPFile( $fileName, 'extension' ); - $used = false; - - foreach ( $data as $key => $item ) { - if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) { - $used = true; + if ( $csCode === $code ) { + # Merge core data into extension data + foreach ( $coreData as $key => $item ) { + $this->mergeItem( $key, $csData[$key], $item ); + } + } else { + # Load the secondary localisation from the source file to + # avoid infinite cycles on cyclic fallbacks + $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps ); + if ( $fbData !== false ) { + # Only merge the keys that make sense to merge + foreach ( self::$allKeys as $key ) { + if ( !isset( $fbData[$key] ) ) { + continue; + } + + if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) { + $this->mergeItem( $key, $csData[$key], $fbData[$key] ); + } + } } } - if ( $used ) { - $deps[] = new FileDependency( $fileName ); + # Allow extensions an opportunity to adjust the data for this + # fallback + wfRunHooks( 'LocalisationCacheRecacheFallback', array( $this, $csCode, &$csData ) ); + + # Merge the data for this fallback into the final array + if ( $csCode === $code ) { + $allData = $csData; + } else { + foreach ( self::$allKeys as $key ) { + if ( !isset( $csData[$key] ) ) { + continue; + } + + if ( is_null( $allData[$key] ) || $this->isMergeableKey( $key ) ) { + $this->mergeItem( $key, $allData[$key], $csData[$key] ); + } + } } } - # Merge core data into extension data - foreach ( $coreData as $key => $item ) { - $this->mergeItem( $key, $allData[$key], $item ); - } - wfProfileOut( __METHOD__ . '-extensions' ); + wfProfileOut( __METHOD__ . '-fallbacks' ); # Add cache dependencies for any referenced globals $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' ); $deps['wgMessagesDirs'] = new GlobalDependency( 'wgMessagesDirs' ); - $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' ); + $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' ); # Add dependencies to the cache entry $allData['deps'] = $deps; @@ -983,7 +1017,7 @@ class LocalisationCache { # HACK: If using a null (i.e. disabled) storage backend, we # can't write to the MessageBlobStore either if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) { - MessageBlobStore::clear(); + MessageBlobStore::getInstance()->clear(); } wfProfileOut( __METHOD__ ); @@ -1097,56 +1131,6 @@ interface LCStore { function set( $key, $value ); } -/** - * LCStore implementation which uses PHP accelerator to store data. - * This will work if one of XCache, WinCache or APC cacher is configured. - * (See ObjectCache.php) - */ -class LCStoreAccel implements LCStore { - private $currentLang; - private $keys; - - public function __construct() { - $this->cache = wfGetCache( CACHE_ACCEL ); - } - - public function get( $code, $key ) { - $k = wfMemcKey( 'l10n', $code, 'k', $key ); - $r = $this->cache->get( $k ); - - return $r === false ? null : $r; - } - - public function startWrite( $code ) { - $k = wfMemcKey( 'l10n', $code, 'l' ); - $keys = $this->cache->get( $k ); - if ( $keys ) { - foreach ( $keys as $k ) { - $this->cache->delete( $k ); - } - } - $this->currentLang = $code; - $this->keys = array(); - } - - public function finishWrite() { - if ( $this->currentLang ) { - $k = wfMemcKey( 'l10n', $this->currentLang, 'l' ); - $this->cache->set( $k, array_keys( $this->keys ) ); - } - $this->currentLang = null; - $this->keys = array(); - } - - public function set( $key, $value ) { - if ( $this->currentLang ) { - $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key ); - $this->keys[$k] = true; - $this->cache->set( $k, $value ); - } - } -} - /** * LCStore implementation which uses the standard DB functions to store data. * This will work on any MediaWiki installation. @@ -1163,8 +1147,8 @@ class LCStoreDB implements LCStore { private $readOnly = false; public function get( $code, $key ) { - if ( $this->writesDone && $this->dbw ) { - $db = $this->dbw; + if ( $this->writesDone ) { + $db = wfGetDB( DB_MASTER ); } else { $db = wfGetDB( DB_SLAVE ); } @@ -1184,16 +1168,7 @@ class LCStoreDB implements LCStore { throw new MWException( __METHOD__ . ": Invalid language \"$code\"" ); } - // We must keep a separate connection to MySQL in order to avoid breaking - // main transactions. However, SQLite deadlocks when using two connections. - // @todo get this trick to work on PostgreSQL too - if ( wfGetDB( DB_MASTER )->getType() == 'mysql' ) { - $lb = wfGetLBFactory()->newMainLB(); - $this->dbw = $lb->getConnection( DB_MASTER ); - $this->dbw->clearFlag( DBO_TRX ); // auto-commit mode - } else { - $this->dbw = wfGetDB( DB_MASTER ); - } + $this->dbw = wfGetDB( DB_MASTER ); $this->currentLang = $code; $this->batch = array(); diff --git a/includes/cache/MapCacheLRU.php b/includes/cache/MapCacheLRU.php index a22d8023d6..95e3af769e 100644 --- a/includes/cache/MapCacheLRU.php +++ b/includes/cache/MapCacheLRU.php @@ -57,7 +57,7 @@ class MapCacheLRU { * @return void */ public function set( $key, $value ) { - if ( isset( $this->cache[$key] ) ) { + if ( array_key_exists( $key, $this->cache ) ) { $this->ping( $key ); // push to top } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) { reset( $this->cache ); @@ -74,7 +74,7 @@ class MapCacheLRU { * @return bool */ public function has( $key ) { - return isset( $this->cache[$key] ); + return array_key_exists( $key, $this->cache ); } /** @@ -86,7 +86,7 @@ class MapCacheLRU { * @return mixed */ public function get( $key ) { - if ( isset( $this->cache[$key] ) ) { + if ( array_key_exists( $key, $this->cache ) ) { $this->ping( $key ); // push to top return $this->cache[$key]; } else { diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index e34961c726..1ef7cc5840 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -573,7 +573,7 @@ class MessageCache { // Update the message in the message blob store global $wgContLang; - MessageBlobStore::updateMessage( $wgContLang->lcfirst( $msg ) ); + MessageBlobStore::getInstance()->updateMessage( $wgContLang->lcfirst( $msg ) ); wfRunHooks( 'MessageCacheReplace', array( $title, $text ) ); diff --git a/includes/cache/bloom/BloomCache.php b/includes/cache/bloom/BloomCache.php new file mode 100644 index 0000000000..236db9544c --- /dev/null +++ b/includes/cache/bloom/BloomCache.php @@ -0,0 +1,323 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Aaron Schulz + */ + +/** + * Persistent bloom filter used to avoid expensive lookups + * + * @since 1.24 + */ +abstract class BloomCache { + /** @var string Unique ID for key namespacing */ + protected $cacheID; + + /** @var array Map of (id => BloomCache) */ + protected static $instances = array(); + + /** + * @param string $id + * @return BloomCache + */ + final public static function get( $id ) { + global $wgBloomFilterStores; + + if ( !isset( self::$instances[$id] ) ) { + if ( isset( $wgBloomFilterStores[$id] ) ) { + $class = $wgBloomFilterStores[$id]['class']; + self::$instances[$id] = new $class( $wgBloomFilterStores[$id] ); + } else { + wfDebug( "No bloom filter store '$id'; using EmptyBloomCache." ); + return new EmptyBloomCache( array() ); + } + } + + return self::$instances[$id]; + } + + /** + * Create a new bloom cache instance from configuration. + * This should only be called from within BloomCache. + * + * @param array $config Parameters include: + * - cacheID : Prefix to all bloom filter names that is unique to this cache. + * It should only consist of alphanumberic, '-', and '_' characters. + * This ID is what avoids collisions if multiple logical caches + * use the same storage system, so this should be set carefully. + */ + public function __construct( array $config ) { + $this->cacheID = $config['cacheId']; + if ( !preg_match( '!^[a-zA-Z0-9-_]{1,32}$!', $this->cacheID ) ) { + throw new MWException( "Cache ID '{$this->cacheID}' is invalid." ); + } + } + + /** + * Check if a member is set in the bloom filter + * + * A member being set means that it *might* have been added. + * A member not being set means it *could not* have been added. + * + * This abstracts over isHit() to deal with filter updates and readiness. + * A class must exist with the name BloomFilter<type> and a static public + * mergeAndCheck() method. The later takes the following arguments: + * (BloomCache $bcache, $domain, $virtualKey, array $status) + * The method should return a bool indicating whether to use the filter. + * + * The 'shared' bloom key must be used for any updates and will be used + * for the membership check if the method returns true. Since the key is shared, + * the method should never use delete(). The filter cannot be used in cases where + * membership in the filter needs to be taken away. In such cases, code *cannot* + * use this method - instead, it can directly use the other BloomCache methods + * to manage custom filters with their own keys (e.g. not 'shared'). + * + * @param string $domain + * @param string $type + * @param string $member + * @return bool True if set, false if not (also returns true on error) + */ + final public function check( $domain, $type, $member ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + if ( method_exists( "BloomFilter{$type}", 'mergeAndCheck' ) ) { + try { + $virtualKey = "$domain:$type"; + + $status = $this->getStatus( $virtualKey ); + if ( $status == false ) { + wfDebug( "Could not query virtual bloom filter '$virtualKey'." ); + return null; + } + + $useFilter = call_user_func_array( + array( "BloomFilter{$type}", 'mergeAndCheck' ), + array( $this, $domain, $virtualKey, $status ) + ); + + if ( $useFilter ) { + return ( $this->isHit( 'shared', "$virtualKey:$member" ) !== false ); + } + } catch ( MWException $e ) { + MWExceptionHandler::logException( $e ); + return true; + } + } + + return true; + } + + /** + * Inform the bloom filter of a new member in order to keep it up to date + * + * @param string $domain + * @param string $type + * @param string|array $members + * @return bool Success + */ + final public function insert( $domain, $type, $members ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + if ( method_exists( "BloomFilter{$type}", 'mergeAndCheck' ) ) { + try { + $virtualKey = "$domain:$type"; + $prefixedMembers = array(); + foreach ( (array)$members as $member ) { + $prefixedMembers[] = "$virtualKey:$member"; + } + + return $this->add( 'shared', $prefixedMembers ); + } catch ( MWException $e ) { + MWExceptionHandler::logException( $e ); + return false; + } + } + + return true; + } + + /** + * Create a new bloom filter at $key (if one does not exist yet) + * + * @param string $key + * @param integer $size Bit length [default: 1000000] + * @param float $precision [default: .001] + * @return bool Success + */ + final public function init( $key, $size = 1000000, $precision = .001 ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + return $this->doInit( "{$this->cacheID}:$key", $size, min( .1, $precision ) ); + } + + /** + * Add a member to the bloom filter at $key + * + * @param string $key + * @param string|array $members + * @return bool Success + */ + final public function add( $key, $members ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + return $this->doAdd( "{$this->cacheID}:$key", (array)$members ); + } + + /** + * Check if a member is set in the bloom filter. + * + * A member being set means that it *might* have been added. + * A member not being set means it *could not* have been added. + * + * If this returns true, then the caller usually should do the + * expensive check (whatever that may be). It can be avoided otherwise. + * + * @param string $key + * @param string $member + * @return bool|null True if set, false if not, null on error + */ + final public function isHit( $key, $member ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + return $this->doIsHit( "{$this->cacheID}:$key", $member ); + } + + /** + * Destroy a bloom filter at $key + * + * @param string $key + * @return bool Success + */ + final public function delete( $key ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + return $this->doDelete( "{$this->cacheID}:$key" ); + } + + /** + * Set the status map of the virtual bloom filter at $key + * + * @param string $virtualKey + * @param array $values Map including some of (lastID, asOfTime, epoch) + * @return bool Success + */ + final public function setStatus( $virtualKey, array $values ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + return $this->doSetStatus( "{$this->cacheID}:$virtualKey", $values ); + } + + /** + * Get the status map of the virtual bloom filter at $key + * + * The map includes: + * - lastID : the highest ID of the items merged in + * - asOfTime : UNIX timestamp that the filter is up-to-date as of + * - epoch : UNIX timestamp that filter started being populated + * Unset fields will have a null value. + * + * @param string $virtualKey + * @return array|bool False on failure + */ + final public function getStatus( $virtualKey ) { + $section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ ); + + return $this->doGetStatus( "{$this->cacheID}:$virtualKey" ); + } + + /** + * Get an exclusive lock on a filter for updates + * + * @param string $virtualKey + * @return ScopedCallback|ScopedLock|null Returns null if acquisition failed + */ + public function getScopedLock( $virtualKey ) { + return null; + } + + /** + * @param string $key + * @param integer $size Bit length + * @param float $precision + * @return bool Success + */ + abstract protected function doInit( $key, $size, $precision ); + + /** + * @param string $key + * @param array $members + * @return bool Success + */ + abstract protected function doAdd( $key, array $members ); + + /** + * @param string $key + * @param string $member + * @return bool|null + */ + abstract protected function doIsHit( $key, $member ); + + /** + * @param string $key + * @return bool Success + */ + abstract protected function doDelete( $key ); + + /** + * @param string $virtualKey + * @param array $values + * @return bool Success + */ + abstract protected function doSetStatus( $virtualKey, array $values ); + + /** + * @param string $key + * @return array|bool + */ + abstract protected function doGetStatus( $key ); +} + +class EmptyBloomCache extends BloomCache { + public function __construct( array $config ) { + parent::__construct( array( 'cacheId' => 'none' ) ); + } + + protected function doInit( $key, $size, $precision ) { + return true; + } + + protected function doAdd( $key, array $members ) { + return true; + } + + protected function doIsHit( $key, $member ) { + return true; + } + + protected function doDelete( $key ) { + return true; + } + + protected function doSetStatus( $virtualKey, array $values ) { + return true; + } + + protected function doGetStatus( $virtualKey ) { + return array( 'lastID' => null, 'asOfTime' => null, 'epoch' => null ) ; + } +} diff --git a/includes/cache/bloom/BloomCacheRedis.php b/includes/cache/bloom/BloomCacheRedis.php new file mode 100644 index 0000000000..7bafc99369 --- /dev/null +++ b/includes/cache/bloom/BloomCacheRedis.php @@ -0,0 +1,370 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Aaron Schulz + */ + +/** + * Bloom filter implented using Redis + * + * The Redis server must be >= 2.6 and should have volatile-lru or volatile-ttl + * if there is any eviction policy. It should not be allkeys-* in any case. Also, + * this can be used in a simple master/slave setup or with Redis Sentinal preferably. + * + * Some bits are based on https://github.com/ErikDubbelboer/redis-lua-scaling-bloom-filter + * but are simplified to use a single filter instead of up to 32 filters. + * + * @since 1.24 + */ +class BloomCacheRedis extends BloomCache { + /** @var RedisConnectionPool */ + protected $redisPool; + /** @var RedisLockManager */ + protected $lockMgr; + /** @var array */ + protected $servers; + /** @var integer Federate each filter into this many redis bitfield objects */ + protected $segments = 128; + + /** + * @params include: + * - redisServers : list of servers (address:<port>) (the first is the master) + * - redisConf : additional redis configuration + * + * @param array $config + */ + public function __construct( array $config ) { + parent::__construct( $config ); + + $redisConf = $config['redisConfig']; + $redisConf['serializer'] = 'none'; // manage that in this class + $this->redisPool = RedisConnectionPool::singleton( $redisConf ); + $this->servers = $config['redisServers']; + $this->lockMgr = new RedisLockManager( array( + 'lockServers' => array( 'srv1' => $this->servers[0] ), + 'srvsByBucket' => array( 0 => array( 'srv1' ) ), + 'redisConfig' => $config['redisConfig'] + ) ); + } + + protected function doInit( $key, $size, $precision ) { + $conn = $this->getConnection( 'master' ); + if ( !$conn ) { + return false; + } + + // 80000000 items at p = .001 take up 500MB and fit into one value. + // Do not hit the 512MB redis value limit by reducing the demands. + $size = min( $size, 80000000 * $this->segments ); + $precision = max( round( $precision, 3 ), .001 ); + $epoch = microtime( true ); + + static $script = +<<<LUA + local kMetadata, kData = unpack(KEYS) + local aEntries, aPrec, aEpoch = unpack(ARGV) + if redis.call('EXISTS',kMetadata) == 0 or redis.call('EXISTS',kData) == 0 then + redis.call('DEL',kMetadata) + redis.call('HSET',kMetadata,'entries',aEntries) + redis.call('HSET',kMetadata,'precision',aPrec) + redis.call('HSET',kMetadata,'epoch',aEpoch) + redis.call('SET',kData,'') + return 1 + end + return 0 +LUA; + + $res = false; + try { + $conn->script( 'load', $script ); + $conn->multi( Redis::MULTI ); + for ( $i = 0; $i < $this->segments; ++$i ) { + $res = $conn->luaEval( $script, + array( + "$key:$i:bloom-metadata", # KEYS[1] + "$key:$i:bloom-data", # KEYS[2] + ceil( $size / $this->segments ), # ARGV[1] + $precision, # ARGV[2] + $epoch # ARGV[3] + ), + 2 # number of first argument(s) that are keys + ); + } + $results = $conn->exec(); + $res = $results && !in_array( false, $results, true ); + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + } + + return ( $res !== false ); + } + + protected function doAdd( $key, array $members ) { + $conn = $this->getConnection( 'master' ); + if ( !$conn ) { + return false; + } + + static $script = +<<<LUA + local kMetadata, kData = unpack(KEYS) + local aMember = unpack(ARGV) + + -- Check if the filter was initialized + if redis.call('EXISTS',kMetadata) == 0 or redis.call('EXISTS',kData) == 0 then + return false + end + + -- Initial expected entries and desired precision + local entries = 1*redis.call('HGET',kMetadata,'entries') + local precision = 1*redis.call('HGET',kMetadata,'precision') + local hash = redis.sha1hex(aMember) + + -- Based on the math from: http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives + -- 0.480453013 = ln(2)^2 + local bits = math.ceil((entries * math.log(precision)) / -0.480453013) + + -- 0.693147180 = ln(2) + local k = math.floor(0.693147180 * bits / entries) + + -- This uses a variation on: + -- 'Less Hashing, Same Performance: Building a Better Bloom Filter' + -- http://www.eecs.harvard.edu/~kirsch/pubs/bbbf/esa06.pdf + local h = { } + h[0] = tonumber(string.sub(hash, 1, 8 ), 16) + h[1] = tonumber(string.sub(hash, 9, 16), 16) + h[2] = tonumber(string.sub(hash, 17, 24), 16) + h[3] = tonumber(string.sub(hash, 25, 32), 16) + + for i=1, k do + local pos = (h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)]) % bits + redis.call('SETBIT', kData, pos, 1) + end + + return 1 +LUA; + + $res = false; + try { + $conn->script( 'load', $script ); + $conn->multi( Redis::PIPELINE ); + foreach ( $members as $member ) { + $i = $this->getSegment( $member ); + $conn->luaEval( $script, + array( + "$key:$i:bloom-metadata", # KEYS[1], + "$key:$i:bloom-data", # KEYS[2] + $member # ARGV[1] + ), + 2 # number of first argument(s) that are keys + ); + } + $results = $conn->exec(); + $res = $results && !in_array( false, $results, true ); + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + } + + if ( $res === false ) { + wfDebug( "Could not add to the '$key' bloom filter; it may be missing." ); + } + + return ( $res !== false ); + } + + protected function doSetStatus( $virtualKey, array $values ) { + $conn = $this->getConnection( 'master' ); + if ( !$conn ) { + return null; + } + + $res = false; + try { + $res = $conn->hMSet( "$virtualKey:filter-metadata", $values ); + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + } + + return ( $res !== false ); + } + + protected function doGetStatus( $virtualKey ) { + $conn = $this->getConnection( 'slave' ); + if ( !$conn ) { + return false; + } + + $res = false; + try { + $res = $conn->hGetAll( "$virtualKey:filter-metadata" ); + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + } + + if ( is_array( $res ) ) { + $res['lastID'] = isset( $res['lastID'] ) ? $res['lastID'] : null; + $res['asOfTime'] = isset( $res['asOfTime'] ) ? $res['asOfTime'] : null; + $res['epoch'] = isset( $res['epoch'] ) ? $res['epoch'] : null; + } + + return $res; + } + + protected function doIsHit( $key, $member ) { + $conn = $this->getConnection( 'slave' ); + if ( !$conn ) { + return null; + } + + static $script = +<<<LUA + local kMetadata, kData = unpack(KEYS) + local aMember = unpack(ARGV) + + -- Check if the filter was initialized + if redis.call('EXISTS',kMetadata) == 0 or redis.call('EXISTS',kData) == 0 then + return false + end + + -- Initial expected entries and desired precision. + -- This determines the size of the first and subsequent filters. + local entries = redis.call('HGET',kMetadata,'entries') + local precision = redis.call('HGET',kMetadata,'precision') + local hash = redis.sha1hex(aMember) + + -- This uses a variation on: + -- 'Less Hashing, Same Performance: Building a Better Bloom Filter' + -- http://www.eecs.harvard.edu/~kirsch/pubs/bbbf/esa06.pdf + local h = { } + h[0] = tonumber(string.sub(hash, 1, 8 ), 16) + h[1] = tonumber(string.sub(hash, 9, 16), 16) + h[2] = tonumber(string.sub(hash, 17, 24), 16) + h[3] = tonumber(string.sub(hash, 25, 32), 16) + + -- 0.480453013 = ln(2)^2 + local bits = math.ceil((entries * math.log(precision)) / -0.480453013) + + -- 0.693147180 = ln(2) + local k = math.floor(0.693147180 * bits / entries) + + local found = 1 + for i=1, k do + local pos = (h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)]) % bits + if redis.call('GETBIT', kData, pos) == 0 then + found = 0 + break + end + end + + return found +LUA; + + $res = null; + try { + $i = $this->getSegment( $member ); + $res = $conn->luaEval( $script, + array( + "$key:$i:bloom-metadata", # KEYS[1], + "$key:$i:bloom-data", # KEYS[2] + $member # ARGV[1] + ), + 2 # number of first argument(s) that are keys + ); + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + } + + return is_int( $res ) ? (bool)$res : null; + } + + protected function doDelete( $key ) { + $conn = $this->getConnection( 'master' ); + if ( !$conn ) { + return false; + } + + $res = false; + try { + $keys = array(); + for ( $i = 0; $i < $this->segments; ++$i ) { + $keys[] = "$key:$i:bloom-metadata"; + $keys[] = "$key:$i:bloom-data"; + } + $res = $conn->delete( $keys ); + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + } + + return ( $res !== false ); + } + + public function getScopedLock( $virtualKey ) { + $status = Status::newGood(); + return ScopedLock::factory( $this->lockMgr, + array( $virtualKey ), LockManager::LOCK_EX, $status ); + } + + /** + * @param string $member + * @return integer + */ + protected function getSegment( $member ) { + return hexdec( substr( md5( $member ), 0, 2 ) ) % $this->segments; + } + + /** + * $param string $to (master/slave) + * @return RedisConnRef|bool Returns false on failure + */ + protected function getConnection( $to ) { + if ( $to === 'master' ) { + $conn = $this->redisPool->getConnection( $this->servers[0] ); + } else { + static $lastServer = null; + + $conn = false; + if ( $lastServer ) { + $conn = $this->redisPool->getConnection( $lastServer ); + if ( $conn ) { + return $conn; // reuse connection + } + } + $servers = $this->servers; + $attempts = min( 3, count( $servers ) ); + for ( $i = 1; $i <= $attempts; ++$i ) { + $index = mt_rand( 0, count( $servers ) - 1 ); + $conn = $this->redisPool->getConnection( $servers[$index] ); + if ( $conn ) { + $lastServer = $servers[$index]; + return $conn; + } + unset( $servers[$index] ); // skip next time + } + } + + return $conn; + } + + /** + * @param RedisConnRef $conn + * @param Exception $e + */ + protected function handleException( RedisConnRef $conn, $e ) { + $this->redisPool->handleError( $conn, $e ); + } +} diff --git a/includes/cache/bloom/BloomFilters.php b/includes/cache/bloom/BloomFilters.php new file mode 100644 index 0000000000..9b710d7905 --- /dev/null +++ b/includes/cache/bloom/BloomFilters.php @@ -0,0 +1,79 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Aaron Schulz + */ + +/** + * @since 1.24 + */ +class BloomFilterTitleHasLogs { + public static function mergeAndCheck( + BloomCache $bcache, $domain, $virtualKey, array $status + ) { + $age = microtime( true ) - $status['asOfTime']; // seconds + $scopedLock = ( mt_rand( 1, (int)pow( 3, max( 0, 5 - $age ) ) ) == 1 ) + ? $bcache->getScopedLock( $virtualKey ) + : false; + + if ( $scopedLock ) { + $updates = self::merge( $bcache, $domain, $virtualKey, $status ); + if ( isset( $updates['asOfTime'] ) ) { + $age = ( microtime( true ) - $updates['asOfTime'] ); + } + } + + return ( $age < 30 ); + } + + public static function merge( + BloomCache $bcache, $domain, $virtualKey, array $status + ) { + $limit = 1000; + $dbr = wfGetDB( DB_SLAVE, array(), $domain ); + $res = $dbr->select( 'logging', + array( 'log_namespace', 'log_title', 'log_id', 'log_timestamp' ), + array( 'log_id > ' . $dbr->addQuotes( (int)$status['lastID'] ) ), + __METHOD__, + array( 'ORDER BY' => 'log_id', 'LIMIT' => $limit ) + ); + + $updates = array(); + if ( $res->numRows() > 0 ) { + $members = array(); + foreach ( $res as $row ) { + $members[] = "$virtualKey:{$row->log_namespace}:{$row->log_title}"; + } + $lastID = $row->log_id; + $lastTime = $row->log_timestamp; + if ( !$bcache->add( 'shared', $members ) ) { + return false; + } + $updates['lastID'] = $lastID; + $updates['asOfTime'] = wfTimestamp( TS_UNIX, $lastTime ); + } else { + $updates['asOfTime'] = microtime( true ); + } + + $updates['epoch'] = $status['epoch'] ?: microtime( true ); + + $bcache->setStatus( $virtualKey, $updates ); + + return $updates; + } +} diff --git a/includes/changes/ChangesFeed.php b/includes/changes/ChangesFeed.php index fb491e50b3..2d3b919dd9 100644 --- a/includes/changes/ChangesFeed.php +++ b/includes/changes/ChangesFeed.php @@ -180,6 +180,7 @@ class ChangesFeed { /** * Generate the feed items given a row from the database. * @param object $rows DatabaseBase resource with recentchanges rows + * @return array */ public static function buildItems( $rows ) { wfProfileIn( __METHOD__ ); diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index cd43f82934..03d1289f4b 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -36,6 +36,9 @@ class ChangesList extends ContextSource { protected $rclistOpen; protected $rcMoveIndex; + /** @var MapCacheLRU */ + protected $watchingCache; + /** * Changeslist constructor * @@ -50,6 +53,7 @@ class ChangesList extends ContextSource { $this->skin = $obj; } $this->preCacheMessages(); + $this->watchingCache = new MapCacheLRU( 50 ); } /** @@ -110,9 +114,8 @@ class ChangesList extends ContextSource { * @return string */ public function recentChangesFlags( $flags, $nothing = ' ' ) { - global $wgRecentChangesFlags; $f = ''; - foreach ( array_keys( $wgRecentChangesFlags ) as $flag ) { + foreach ( array_keys( $this->getConfig()->get( 'RecentChangesFlags' ) ) as $flag ) { $f .= isset( $flags[$flag] ) && $flags[$flag] ? self::flag( $flag ) : $nothing; @@ -188,8 +191,6 @@ class ChangesList extends ContextSource { * @return string */ public static function showCharacterDifference( $old, $new, IContextSource $context = null ) { - global $wgRCChangedSizeThreshold, $wgMiserMode; - if ( !$context ) { $context = RequestContext::getMain(); } @@ -199,10 +200,11 @@ class ChangesList extends ContextSource { $szdiff = $new - $old; $lang = $context->getLanguage(); + $config = $context->getConfig(); $code = $lang->getCode(); static $fastCharDiff = array(); if ( !isset( $fastCharDiff[$code] ) ) { - $fastCharDiff[$code] = $wgMiserMode || $context->msg( 'rc-change-size' )->plain() === '$1'; + $fastCharDiff[$code] = $config->get( 'MiserMode' ) || $context->msg( 'rc-change-size' )->plain() === '$1'; } $formattedSize = $lang->formatNum( $szdiff ); @@ -211,7 +213,7 @@ class ChangesList extends ContextSource { $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text(); } - if ( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { + if ( abs( $szdiff ) > abs( $config->get( 'RCChangedSizeThreshold' ) ) ) { $tag = 'strong'; } else { $tag = 'span'; @@ -438,8 +440,6 @@ class ChangesList extends ContextSource { } else { return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); } - - return ''; } /** @@ -462,14 +462,14 @@ class ChangesList extends ContextSource { * @return string */ protected function numberofWatchingusers( $count ) { - static $cache = array(); + $cache = $this->watchingCache; if ( $count > 0 ) { - if ( !isset( $cache[$count] ) ) { - $cache[$count] = $this->msg( 'number_of_watching_users_RCview' ) - ->numParams( $count )->escaped(); + if ( !$cache->has( $count ) ) { + $cache->set( $count, $this->msg( 'number_of_watching_users_RCview' ) + ->numParams( $count )->escaped() ); } - return $cache[$count]; + return $cache->get( $count ); } else { return ''; } diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php index b471ea5ebe..4ab772970d 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -160,8 +160,6 @@ class EnhancedChangesList extends ChangesList { * @return string */ protected function recentChangesBlockGroup( $block ) { - global $wgRCShowChangedSize; - wfProfileIn( __METHOD__ ); # Add the namespace and title of the block as part of the class @@ -191,6 +189,7 @@ class EnhancedChangesList extends ChangesList { $namehidden = true; $allLogs = true; $oldid = ''; + $RCShowChangedSize = $this->getConfig()->get( 'RCShowChangedSize' ); foreach ( $block as $rcObj ) { $oldid = $rcObj->mAttribs['rc_last_oldid']; if ( $rcObj->mAttribs['rc_type'] == RC_NEW ) { @@ -364,7 +363,7 @@ class EnhancedChangesList extends ChangesList { $r .= ' <span class="mw-changeslist-separator">. .</span> '; # Character difference (does not apply if only log items) - if ( $wgRCShowChangedSize && !$allLogs ) { + if ( $RCShowChangedSize && !$allLogs ) { $last = 0; $first = count( $block ) - 1; # Some events (like logs) have an "empty" size, so we need to skip those... @@ -442,7 +441,7 @@ class EnhancedChangesList extends ChangesList { $r .= ' <span class="mw-changeslist-separator">. .</span> '; # Character diff - if ( $wgRCShowChangedSize ) { + if ( $RCShowChangedSize ) { $cd = $this->formatCharacterDifference( $rcObj ); if ( $cd !== '' ) { $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> '; @@ -474,50 +473,6 @@ class EnhancedChangesList extends ChangesList { return $r; } - /** - * Generate HTML for an arrow or placeholder graphic - * @param string $dir One of '', 'd', 'l', 'r' - * @param string $alt - * @param string $title - * @return string HTML "<img>" tag - */ - protected function arrow( $dir, $alt = '', $title = '' ) { - global $wgStylePath; - $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' ); - $encAlt = htmlspecialchars( $alt ); - $encTitle = htmlspecialchars( $title ); - - return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" title=\"$encTitle\" />"; - } - - /** - * Generate HTML for a right- or left-facing arrow, - * depending on language direction. - * @return string HTML "<img>" tag - */ - protected function sideArrow() { - $dir = $this->getLanguage()->isRTL() ? 'l' : 'r'; - - return $this->arrow( $dir, '+', $this->msg( 'rc-enhanced-expand' )->text() ); - } - - /** - * Generate HTML for a down-facing arrow - * depending on language direction. - * @return string HTML "<img>" tag - */ - protected function downArrow() { - return $this->arrow( 'd', '-', $this->msg( 'rc-enhanced-hide' )->text() ); - } - - /** - * Generate HTML for a spacer image - * @return string HTML "<img>" tag - */ - protected function spacerArrow() { - return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space - } - /** * Enhanced RC ungrouped line. * @@ -525,8 +480,6 @@ class EnhancedChangesList extends ChangesList { * @return string A HTML formatted line (generated using $r) */ protected function recentChangesBlockLine( $rcObj ) { - global $wgRCShowChangedSize; - wfProfileIn( __METHOD__ ); $query['curid'] = $rcObj->mAttribs['rc_cur_id']; @@ -577,7 +530,7 @@ class EnhancedChangesList extends ChangesList { } $r .= ' <span class="mw-changeslist-separator">. .</span> '; # Character diff - if ( $wgRCShowChangedSize ) { + if ( $this->getConfig()->get( 'RCShowChangedSize' ) ) { $cd = $this->formatCharacterDifference( $rcObj ); if ( $cd !== '' ) { $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> '; diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index d590ff6472..4eed9262a8 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -21,6 +21,7 @@ */ class OldChangesList extends ChangesList { + /** * Format a line using the old system (aka without any javascript). * @@ -31,13 +32,8 @@ class OldChangesList extends ChangesList { * @return string|bool */ public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { - global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); - # Should patrol-related stuff be shown? - $unpatrolled = $this->showAsUnpatrolled( $rc ); - - $s = ''; $classes = array(); // use mw-line-even/mw-line-odd class only if linenumber is given (feature from bug 14468) if ( $linenumber ) { @@ -53,20 +49,53 @@ class OldChangesList extends ChangesList { $classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched'; + $html = $this->formatChangeLine( $rc, $classes, $watched ); + + if ( $this->watchlist ) { + $classes[] = Sanitizer::escapeClass( 'watchlist-' . + $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] ); + } + + if ( !wfRunHooks( 'OldChangesListRecentChangesLine', array( &$this, &$html, $rc, &$classes ) ) ) { + wfProfileOut( __METHOD__ ); + + return false; + } + + wfProfileOut( __METHOD__ ); + + $dateheader = ''; // $html now contains only <li>...</li>, for hooks' convenience. + $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); + + return "$dateheader<li class=\"" . implode( ' ', $classes ) . "\">" . $html . "</li>\n"; + } + + /** + * @param RecentChange $rc + * @param string[] &$classes + * @param boolean $watched + * + * @return string + */ + private function formatChangeLine( RecentChange $rc, array &$classes, $watched ) { + $html = ''; + if ( $rc->mAttribs['rc_log_type'] ) { $logtitle = SpecialPage::getTitleFor( 'Log', $rc->mAttribs['rc_log_type'] ); - $this->insertLog( $s, $logtitle, $rc->mAttribs['rc_log_type'] ); + $this->insertLog( $html, $logtitle, $rc->mAttribs['rc_log_type'] ); // Log entries (old format) or log targets, and special pages } elseif ( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) { - list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] ); + list( $name, $htmlubpage ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] ); if ( $name == 'Log' ) { - $this->insertLog( $s, $rc->getTitle(), $subpage ); + $this->insertLog( $html, $rc->getTitle(), $htmlubpage ); } // Regular entries } else { - $this->insertDiffHist( $s, $rc, $unpatrolled ); + $unpatrolled = $this->showAsUnpatrolled( $rc ); + + $this->insertDiffHist( $html, $rc, $unpatrolled ); # M, N, b and ! (minor, new, bot and unpatrolled) - $s .= $this->recentChangesFlags( + $html .= $this->recentChangesFlags( array( 'newpage' => $rc->mAttribs['rc_type'] == RC_NEW, 'minor' => $rc->mAttribs['rc_minor'], @@ -75,56 +104,40 @@ class OldChangesList extends ChangesList { ), '' ); - $this->insertArticleLink( $s, $rc, $unpatrolled, $watched ); + $this->insertArticleLink( $html, $rc, $unpatrolled, $watched ); } # Edit/log timestamp - $this->insertTimestamp( $s, $rc ); + $this->insertTimestamp( $html, $rc ); # Bytes added or removed - if ( $wgRCShowChangedSize ) { + if ( $this->getConfig()->get( 'RCShowChangedSize' ) ) { $cd = $this->formatCharacterDifference( $rc ); if ( $cd !== '' ) { - $s .= $cd . ' <span class="mw-changeslist-separator">. .</span> '; + $html .= $cd . ' <span class="mw-changeslist-separator">. .</span> '; } } if ( $rc->mAttribs['rc_type'] == RC_LOG ) { - $s .= $this->insertLogEntry( $rc ); + $html .= $this->insertLogEntry( $rc ); } else { # User tool links - $this->insertUserRelatedLinks( $s, $rc ); + $this->insertUserRelatedLinks( $html, $rc ); # LTR/RTL direction mark - $s .= $this->getLanguage()->getDirMark(); - $s .= $this->insertComment( $rc ); + $html .= $this->getLanguage()->getDirMark(); + $html .= $this->insertComment( $rc ); } # Tags - $this->insertTags( $s, $rc, $classes ); + $this->insertTags( $html, $rc, $classes ); # Rollback - $this->insertRollback( $s, $rc ); + $this->insertRollback( $html, $rc ); # For subclasses - $this->insertExtra( $s, $rc, $classes ); + $this->insertExtra( $html, $rc, $classes ); # How many users watch this page if ( $rc->numberofWatchingusers > 0 ) { - $s .= ' ' . $this->numberofWatchingusers( $rc->numberofWatchingusers ); - } - - if ( $this->watchlist ) { - $classes[] = Sanitizer::escapeClass( 'watchlist-' . - $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] ); + $html .= ' ' . $this->numberofWatchingusers( $rc->numberofWatchingusers ); } - if ( !wfRunHooks( 'OldChangesListRecentChangesLine', array( &$this, &$s, $rc, &$classes ) ) ) { - wfProfileOut( __METHOD__ ); - - return false; - } - - wfProfileOut( __METHOD__ ); - - $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience. - $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); - - return "$dateheader<li class=\"" . implode( ' ', $classes ) . "\">" . $s . "</li>\n"; + return $html; } } diff --git a/includes/changes/RecentChange.php b/includes/changes/RecentChange.php index cfebf409fe..e33274e8c6 100644 --- a/includes/changes/RecentChange.php +++ b/includes/changes/RecentChange.php @@ -136,7 +136,7 @@ class RecentChange { /** * Parsing RC_* constants to human-readable test * @since 1.24 - * @param int $rc_type + * @param int $rcType * @return string $type */ public static function parseFromRCType( $rcType ) { @@ -160,22 +160,6 @@ class RecentChange { return $type; } - /** - * No uses left in Gerrit on 2013-11-19. - * @deprecated since 1.22 - * @param mixed $row - * @return RecentChange - */ - public static function newFromCurRow( $row ) { - wfDeprecated( __METHOD__, '1.22' ); - $rc = new RecentChange; - $rc->loadFromCurRow( $row ); - $rc->notificationtimestamp = false; - $rc->numberofWatchingusers = false; - - return $rc; - } - /** * Obtain the recent change with a given rc_id value * @@ -350,38 +334,6 @@ class RecentChange { } } - /** - * @deprecated since 1.22, use notifyRCFeeds instead. - */ - public function notifyRC2UDP() { - wfDeprecated( __METHOD__, '1.22' ); - $this->notifyRCFeeds(); - } - - /** - * Send some text to UDP. - * @deprecated since 1.22 - */ - public static function sendToUDP( $line, $address = '', $prefix = '', $port = '' ) { - global $wgRC2UDPAddress, $wgRC2UDPInterwikiPrefix, $wgRC2UDPPort, $wgRC2UDPPrefix; - - wfDeprecated( __METHOD__, '1.22' ); - - # Assume default for standard RC case - $address = $address ? $address : $wgRC2UDPAddress; - $prefix = $prefix ? $prefix : $wgRC2UDPPrefix; - $port = $port ? $port : $wgRC2UDPPort; - - $engine = new UDPRCFeedEngine(); - $feed = array( - 'uri' => "udp://$address:$port/$prefix", - 'formatter' => 'IRCColourfulRCFeedFormatter', - 'add_interwiki_prefix' => $wgRC2UDPInterwikiPrefix, - ); - - $engine->send( $feed, $line ); - } - /** * Notify all the feeds about the change. * @param array $feeds Optional feeds to send to, defaults to $wgRCFeeds @@ -452,15 +404,6 @@ class RecentChange { return new $wgRCEngines[$scheme]; } - /** - * @deprecated since 1.22, moved to IRCColourfulRCFeedFormatter - */ - public static function cleanupForIRC( $text ) { - wfDeprecated( __METHOD__, '1.22' ); - - return IRCColourfulRCFeedFormatter::cleanupForIRC( $text ); - } - /** * Mark a given change as patrolled * @@ -793,42 +736,6 @@ class RecentChange { $this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set } - /** - * Makes a pseudo-RC entry from a cur row - * - * @deprecated since 1.22 - * @param mixed $row - */ - public function loadFromCurRow( $row ) { - wfDeprecated( __METHOD__, '1.22' ); - $this->mAttribs = array( - 'rc_timestamp' => wfTimestamp( TS_MW, $row->rev_timestamp ), - 'rc_user' => $row->rev_user, - 'rc_user_text' => $row->rev_user_text, - 'rc_namespace' => $row->page_namespace, - 'rc_title' => $row->page_title, - 'rc_comment' => $row->rev_comment, - 'rc_minor' => $row->rev_minor_edit ? 1 : 0, - 'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT, - 'rc_source' => $row->page_is_new ? self::SRC_NEW : self::SRC_EDIT, - 'rc_cur_id' => $row->page_id, - 'rc_this_oldid' => $row->rev_id, - 'rc_last_oldid' => isset( $row->rc_last_oldid ) ? $row->rc_last_oldid : 0, - 'rc_bot' => 0, - 'rc_ip' => '', - 'rc_id' => $row->rc_id, - 'rc_patrolled' => $row->rc_patrolled, - 'rc_new' => $row->page_is_new, # obsolete - 'rc_old_len' => $row->rc_old_len, - 'rc_new_len' => $row->rc_new_len, - 'rc_params' => isset( $row->rc_params ) ? $row->rc_params : '', - 'rc_log_type' => isset( $row->rc_log_type ) ? $row->rc_log_type : null, - 'rc_log_action' => isset( $row->rc_log_action ) ? $row->rc_log_action : null, - 'rc_logid' => isset( $row->rc_logid ) ? $row->rc_logid : 0, - 'rc_deleted' => $row->rc_deleted // MUST be set - ); - } - /** * Get an attribute value * diff --git a/includes/config/ConfigFactory.php b/includes/config/ConfigFactory.php index 312d4616ac..12b0c39911 100644 --- a/includes/config/ConfigFactory.php +++ b/includes/config/ConfigFactory.php @@ -61,6 +61,7 @@ class ConfigFactory { * Destroy the default instance * Should only be called inside unit tests * @throws MWException + * @codeCoverageIgnore */ public static function destroyDefaultInstance() { if ( !defined( 'MW_PHPUNIT_TEST' ) ) { diff --git a/includes/content/AbstractContent.php b/includes/content/AbstractContent.php index e09be5673a..683c9596be 100644 --- a/includes/content/AbstractContent.php +++ b/includes/content/AbstractContent.php @@ -503,7 +503,7 @@ abstract class AbstractContent implements Content { * * @param Title $title Context title for parsing * @param int|null $revId Revision ID (for {{REVISIONID}}) - * @param ParserOptions|null $options Parser options + * @param ParserOptions $options Parser options * @param bool $generateHtml Whether or not to generate HTML * @param ParserOutput &$output The output object to fill (reference). * diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index c8a9f1e352..ac41722307 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -743,9 +743,9 @@ abstract class ContentHandler { * * @since 1.21 * - * @param Content|string $oldContent The page's previous content. - * @param Content|string $myContent One of the page's conflicting contents. - * @param Content|string $yourContent One of the page's conflicting contents. + * @param Content $oldContent The page's previous content. + * @param Content $myContent One of the page's conflicting contents. + * @param Content $yourContent One of the page's conflicting contents. * * @return Content|bool Always false. */ diff --git a/includes/content/CssContent.php b/includes/content/CssContent.php index 2673084ebe..72414585d0 100644 --- a/includes/content/CssContent.php +++ b/includes/content/CssContent.php @@ -58,7 +58,7 @@ class CssContent extends TextContent { $text = $this->getNativeData(); $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); - return new CssContent( $pst ); + return new static( $pst ); } /** diff --git a/includes/content/CssContentHandler.php b/includes/content/CssContentHandler.php index fd326f01b1..1ab4ee2a8f 100644 --- a/includes/content/CssContentHandler.php +++ b/includes/content/CssContentHandler.php @@ -36,27 +36,8 @@ class CssContentHandler extends TextContentHandler { parent::__construct( $modelId, array( CONTENT_FORMAT_CSS ) ); } - /** - * @param string $text - * @param string $format - * - * @return CssContent - * - * @see ContentHandler::unserializeContent() - */ - public function unserializeContent( $text, $format = null ) { - $this->checkFormat( $format ); - - return new CssContent( $text ); - } - - /** - * @return CssContent A new CssContent object with empty text. - * - * @see ContentHandler::makeEmptyContent() - */ - public function makeEmptyContent() { - return new CssContent( '' ); + protected function getContentClass() { + return 'CssContent'; } /** diff --git a/includes/content/JSONContent.php b/includes/content/JSONContent.php new file mode 100644 index 0000000000..e563780159 --- /dev/null +++ b/includes/content/JSONContent.php @@ -0,0 +1,120 @@ +<?php +/** + * JSON Content Model + * + * @file + * + * @author Ori Livneh <ori@wikimedia.org> + * @author Kunal Mehta <legoktm@gmail.com> + */ + +/** + * Represents the content of a JSON content. + * @since 1.24 + */ +class JSONContent extends TextContent { + + public function __construct( $text, $modelId = CONTENT_MODEL_JSON ) { + parent::__construct( $text, $modelId ); + } + + /** + * Decodes the JSON into a PHP associative array. + * @return array + */ + public function getJsonData() { + return FormatJson::decode( $this->getNativeData(), true ); + } + + /** + * @return bool Whether content is valid JSON. + */ + public function isValid() { + return $this->getJsonData() !== null; + } + + /** + * Pretty-print JSON + * + * @return bool|null|string + */ + public function beautifyJSON() { + $decoded = FormatJson::decode( $this->getNativeData(), true ); + if ( !is_array( $decoded ) ) { + return null; + } + return FormatJson::encode( $decoded, true ); + + } + + /** + * Beautifies JSON prior to save. + * @param Title $title Title + * @param User $user User + * @param ParserOptions $popts + * @return JSONContent + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { + return new static( $this->beautifyJSON() ); + } + + /** + * Set the HTML and add the appropriate styles + * + * + * @param Title $title + * @param int $revId + * @param ParserOptions $options + * @param bool $generateHtml + * @param ParserOutput $output + */ + protected function fillParserOutput( Title $title, $revId, + ParserOptions $options, $generateHtml, ParserOutput &$output + ) { + if ( $generateHtml ) { + $output->setText( $this->objectTable( $this->getJsonData() ) ); + $output->addModuleStyles( 'mediawiki.content.json' ); + } else { + $output->setText( '' ); + } + } + /** + * Constructs an HTML representation of a JSON object. + * @param array $mapping + * @return string HTML + */ + protected function objectTable( $mapping ) { + $rows = array(); + + foreach ( $mapping as $key => $val ) { + $rows[] = $this->objectRow( $key, $val ); + } + return Xml::tags( 'table', array( 'class' => 'mw-json' ), + Xml::tags( 'tbody', array(), join( "\n", $rows ) ) + ); + } + + /** + * Constructs HTML representation of a single key-value pair. + * @param string $key + * @param mixed $val + * @return string HTML. + */ + protected function objectRow( $key, $val ) { + $th = Xml::elementClean( 'th', array(), $key ); + if ( is_array( $val ) ) { + $td = Xml::tags( 'td', array(), self::objectTable( $val ) ); + } else { + if ( is_string( $val ) ) { + $val = '"' . $val . '"'; + } else { + $val = FormatJson::encode( $val ); + } + + $td = Xml::elementClean( 'td', array( 'class' => 'value' ), $val ); + } + + return Xml::tags( 'tr', array(), $th . $td ); + } + +} diff --git a/includes/content/JSONContentHandler.php b/includes/content/JSONContentHandler.php new file mode 100644 index 0000000000..b0b7aaea48 --- /dev/null +++ b/includes/content/JSONContentHandler.php @@ -0,0 +1,54 @@ +<?php +/** + * JSON Schema Content Handler + * + * @file + * + * @author Ori Livneh <ori@wikimedia.org> + * @author Kunal Mehta <legoktm@gmail.com> + */ + +/** + * @since 1.24 + */ +class JSONContentHandler extends TextContentHandler { + + public function __construct( $modelId = CONTENT_MODEL_JSON ) { + parent::__construct( $modelId, array( CONTENT_FORMAT_JSON ) ); + } + + /** + * @return string + */ + protected function getContentClass() { + return 'JSONContent'; + } + + /** + * Returns the english language, because JSON is english, and should be handled as such. + * + * @param Title $title + * @param Content|null $content + * + * @return Language Return of wfGetLangObj( 'en' ) + * + * @see ContentHandler::getPageLanguage() + */ + public function getPageLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } + + /** + * Returns the english language, because JSON is english, and should be handled as such. + * + * @param Title $title + * @param Content|null $content + * + * @return Language Return of wfGetLangObj( 'en' ) + * + * @see ContentHandler::getPageLanguage() + */ + public function getPageViewLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } +} diff --git a/includes/content/JavaScriptContent.php b/includes/content/JavaScriptContent.php index 442b705282..0991f07683 100644 --- a/includes/content/JavaScriptContent.php +++ b/includes/content/JavaScriptContent.php @@ -57,7 +57,7 @@ class JavaScriptContent extends TextContent { $text = $this->getNativeData(); $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); - return new JavaScriptContent( $pst ); + return new static( $pst ); } /** diff --git a/includes/content/JavaScriptContentHandler.php b/includes/content/JavaScriptContentHandler.php index 122003fe53..8d62e2a3ad 100644 --- a/includes/content/JavaScriptContentHandler.php +++ b/includes/content/JavaScriptContentHandler.php @@ -36,27 +36,8 @@ class JavaScriptContentHandler extends TextContentHandler { parent::__construct( $modelId, array( CONTENT_FORMAT_JAVASCRIPT ) ); } - /** - * @param string $text - * @param string $format - * - * @return JavaScriptContent - * - * @see ContentHandler::unserializeContent() - */ - public function unserializeContent( $text, $format = null ) { - $this->checkFormat( $format ); - - return new JavaScriptContent( $text ); - } - - /** - * @return JavaScriptContent A new JavaScriptContent object with empty text. - * - * @see ContentHandler::makeEmptyContent() - */ - public function makeEmptyContent() { - return new JavaScriptContent( '' ); + protected function getContentClass() { + return 'JavaScriptContent'; } /** diff --git a/includes/content/MessageContent.php b/includes/content/MessageContent.php index abaac53f3a..edbd075ca9 100644 --- a/includes/content/MessageContent.php +++ b/includes/content/MessageContent.php @@ -106,7 +106,7 @@ class MessageContent extends AbstractContent { } /** - * @param int $maxLength Maximum length of the summary text, defaults to 250. + * @param int $maxlength Maximum length of the summary text, defaults to 250. * * @return string The summary text. * diff --git a/includes/content/TextContentHandler.php b/includes/content/TextContentHandler.php index b728d312ca..ffe1acbd20 100644 --- a/includes/content/TextContentHandler.php +++ b/includes/content/TextContentHandler.php @@ -60,9 +60,9 @@ class TextContentHandler extends ContentHandler { * * This text-based implementation uses wfMerge(). * - * @param Content|string $oldContent The page's previous content. - * @param Content|string $myContent One of the page's conflicting contents. - * @param Content|string $yourContent One of the page's conflicting contents. + * @param Content $oldContent The page's previous content. + * @param Content $myContent One of the page's conflicting contents. + * @param Content $yourContent One of the page's conflicting contents. * * @return Content|bool */ @@ -92,6 +92,19 @@ class TextContentHandler extends ContentHandler { return $mergedContent; } + /** + * Returns the name of the associated Content class, to + * be used when creating new objects. Override expected + * by subclasses. + * + * @since 1.24 + * + * @return string + */ + protected function getContentClass() { + return 'TextContent'; + } + /** * Unserializes a Content object of the type supported by this ContentHandler. * @@ -105,7 +118,8 @@ class TextContentHandler extends ContentHandler { public function unserializeContent( $text, $format = null ) { $this->checkFormat( $format ); - return new TextContent( $text ); + $class = $this->getContentClass(); + return new $class( $text ); } /** @@ -116,7 +130,8 @@ class TextContentHandler extends ContentHandler { * @return Content A new TextContent object with empty text. */ public function makeEmptyContent() { - return new TextContent( '' ); + $class = $this->getContentClass(); + return new $class( '' ); } } diff --git a/includes/content/WikitextContent.php b/includes/content/WikitextContent.php index 237029b0ba..3ab6a6dbea 100644 --- a/includes/content/WikitextContent.php +++ b/includes/content/WikitextContent.php @@ -52,7 +52,7 @@ class WikitextContent extends TextContent { if ( $sect === false ) { return false; } else { - return new WikitextContent( $sect ); + return new static( $sect ); } } @@ -104,7 +104,7 @@ class WikitextContent extends TextContent { $text = $wgParser->replaceSection( $oldtext, $sectionId, $text ); } - $newContent = new WikitextContent( $text ); + $newContent = new static( $text ); wfProfileOut( __METHOD__ ); @@ -125,7 +125,7 @@ class WikitextContent extends TextContent { $text .= "\n\n"; $text .= $this->getNativeData(); - return new WikitextContent( $text ); + return new static( $text ); } /** @@ -145,7 +145,7 @@ class WikitextContent extends TextContent { $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); rtrim( $pst ); - return ( $text === $pst ) ? $this : new WikitextContent( $pst ); + return ( $text === $pst ) ? $this : new static( $pst ); } /** @@ -164,7 +164,7 @@ class WikitextContent extends TextContent { $text = $this->getNativeData(); $plt = $wgParser->getPreloadText( $text, $title, $popts, $params ); - return new WikitextContent( $plt ); + return new static( $plt ); } /** @@ -246,7 +246,7 @@ class WikitextContent extends TextContent { '[[' . $target->getFullText() . ']]', $this->getNativeData(), 1 ); - return new WikitextContent( $newText ); + return new static( $newText ); } /** @@ -336,6 +336,7 @@ class WikitextContent extends TextContent { Article::getRedirectHeaderHtml( $title->getPageLanguage(), $chain, false ) . $output->getText() ); + $output->addModuleStyles( 'mediawiki.action.view.redirectPage' ); } } } diff --git a/includes/content/WikitextContentHandler.php b/includes/content/WikitextContentHandler.php index 5ae3e25d6e..c1db1de63a 100644 --- a/includes/content/WikitextContentHandler.php +++ b/includes/content/WikitextContentHandler.php @@ -34,19 +34,8 @@ class WikitextContentHandler extends TextContentHandler { parent::__construct( $modelId, array( CONTENT_FORMAT_WIKITEXT ) ); } - public function unserializeContent( $text, $format = null ) { - $this->checkFormat( $format ); - - return new WikitextContent( $text ); - } - - /** - * @return Content A new WikitextContent object with empty text. - * - * @see ContentHandler::makeEmptyContent - */ - public function makeEmptyContent() { - return new WikitextContent( '' ); + protected function getContentClass() { + return 'WikitextContent'; } /** @@ -79,7 +68,8 @@ class WikitextContentHandler extends TextContentHandler { $redirectText .= "\n" . $text; } - return new WikitextContent( $redirectText ); + $class = $this->getContentClass(); + return new $class( $redirectText ); } /** diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php index 2c051baa7a..ede10fe93c 100644 --- a/includes/context/RequestContext.php +++ b/includes/context/RequestContext.php @@ -123,10 +123,10 @@ class RequestContext implements IContextSource { /** * Set the Title object * - * @param Title $t + * @param Title $title */ - public function setTitle( Title $t ) { - $this->title = $t; + public function setTitle( Title $title ) { + $this->title = $title; // Erase the WikiPage so a new one with the new title gets created. $this->wikipage = null; } @@ -297,13 +297,12 @@ class RequestContext implements IContextSource { $e = new Exception; wfDebugLog( 'recursion-guard', "Recursion detected:\n" . $e->getTraceAsString() ); - global $wgLanguageCode; - $code = ( $wgLanguageCode ) ? $wgLanguageCode : 'en'; + $code = $this->getConfig()->get( 'LanguageCode' ) ?: 'en'; $this->lang = Language::factory( $code ); } elseif ( $this->lang === null ) { $this->recursion = true; - global $wgLanguageCode, $wgContLang; + global $wgContLang; try { $request = $this->getRequest(); @@ -314,7 +313,7 @@ class RequestContext implements IContextSource { wfRunHooks( 'UserGetLanguageObject', array( $user, &$code, $this ) ); - if ( $code === $wgLanguageCode ) { + if ( $code === $this->getConfig()->get( 'LanguageCode' ) ) { $this->lang = $wgContLang; } else { $obj = Language::factory( $code ); @@ -353,29 +352,36 @@ class RequestContext implements IContextSource { $skin = null; wfRunHooks( 'RequestContextCreateSkin', array( $this, &$skin ) ); + $factory = SkinFactory::getDefaultInstance(); // If the hook worked try to set a skin from it if ( $skin instanceof Skin ) { $this->skin = $skin; } elseif ( is_string( $skin ) ) { - $this->skin = Skin::newFromKey( $skin ); + // Normalize the key, just in case the hook did something weird. + $normalized = Skin::normalizeKey( $skin ); + $this->skin = $factory->makeSkin( $normalized ); } // If this is still null (the hook didn't run or didn't work) // then go through the normal processing to load a skin if ( $this->skin === null ) { - global $wgHiddenPrefs; - if ( !in_array( 'skin', $wgHiddenPrefs ) ) { + if ( !in_array( 'skin', $this->getConfig()->get( 'HiddenPrefs' ) ) ) { # get the user skin $userSkin = $this->getUser()->getOption( 'skin' ); $userSkin = $this->getRequest()->getVal( 'useskin', $userSkin ); } else { # if we're not allowing users to override, then use the default - global $wgDefaultSkin; - $userSkin = $wgDefaultSkin; + $userSkin = $this->getConfig()->get( 'DefaultSkin' ); } - $this->skin = Skin::newFromKey( $userSkin ); + // Normalize the key in case the user is passing gibberish + // or has old preferences (bug 69566). + $normalized = Skin::normalizeKey( $userSkin ); + + // Skin::normalizeKey will also validate it, so + // this won't throw an exception + $this->skin = $factory->makeSkin( $normalized ); } // After all that set a context on whatever skin got created @@ -415,6 +421,21 @@ class RequestContext implements IContextSource { return self::$instance; } + /** + * Get the RequestContext object associated with the main request + * and gives a warning to the log, to find places, where a context maybe is missing. + * + * @param string $func + * @return RequestContext + * @since 1.24 + */ + public static function getMainAndWarn( $func = __METHOD__ ) { + wfDebug( $func . ' called without context. ' . + "Using RequestContext::getMain() for sanity\n" ); + + return self::getMain(); + } + /** * Resets singleton returned by getMain(). Should be called only from unit tests. */ diff --git a/includes/db/Database.php b/includes/db/Database.php index 62d64eb35c..9b783a99d6 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -679,8 +679,6 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * - DBO_DEBUG: output some debug info (same as debug()) * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) * - DBO_TRX: automatically start transactions - * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode - * and removes it in command line mode * - DBO_PERSISTENT: use persistant database connection * @return bool */ @@ -710,19 +708,42 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Return a path to the DBMS-specific schema file, otherwise default to tables.sql + * Return a path to the DBMS-specific SQL file if it exists, + * otherwise default SQL file * + * @param string $filename * @return string */ - public function getSchemaPath() { + private function getSqlFilePath( $filename ) { global $IP; - if ( file_exists( "$IP/maintenance/" . $this->getType() . "/tables.sql" ) ) { - return "$IP/maintenance/" . $this->getType() . "/tables.sql"; + $dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename"; + if ( file_exists( $dbmsSpecificFilePath ) ) { + return $dbmsSpecificFilePath; } else { - return "$IP/maintenance/tables.sql"; + return "$IP/maintenance/$filename"; } } + /** + * Return a path to the DBMS-specific schema file, + * otherwise default to tables.sql + * + * @return string + */ + public function getSchemaPath() { + return $this->getSqlFilePath( 'tables.sql' ); + } + + /** + * Return a path to the DBMS-specific update key file, + * otherwise default to update-keys.sql + * + * @return string + */ + public function getUpdateKeysPath() { + return $this->getSqlFilePath( 'update-keys.sql' ); + } + # ------------------------------------------------------------------------------ # Other functions # ------------------------------------------------------------------------------ @@ -1130,7 +1151,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->mTrxIdleCallbacks = array(); // bug 65263 $this->mTrxPreCommitCallbacks = array(); // bug 65263 wfDebug( "Connection lost, reconnecting...\n" ); - + # Stash the last error values since ping() might clear them + $lastError = $this->lastError(); + $lastErrno = $this->lastErrno(); if ( $this->ping() ) { global $wgRequestTime; wfDebug( "Reconnected\n" ); @@ -1145,6 +1168,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( $hadTrx ) { # Leave $ret as false and let an error be reported. # Callers may catch the exception and continue to use the DB. + $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore ); } else { # Should be safe to silently retry (no trx and thus no callbacks) $ret = $this->doQuery( $commentedSql ); @@ -1728,7 +1752,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Estimate rows in dataset. + * Estimate the number of rows in dataset * * MySQL allows you to estimate the number of rows that would be returned * by a SELECT query, using EXPLAIN SELECT. The estimate is provided using @@ -1747,8 +1771,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @param array $options Options for select * @return int Row count */ - public function estimateRowCount( $table, $vars = '*', $conds = '', - $fname = __METHOD__, $options = array() + public function estimateRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) { $rows = 0; $res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options ); @@ -1761,6 +1785,36 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { return $rows; } + /** + * Get the number of rows in dataset + * + * This is useful when trying to do COUNT(*) but with a LIMIT for performance. + * + * Takes the same arguments as DatabaseBase::select(). + * + * @param string $table Table name + * @param string $vars Unused + * @param array|string $conds Filters on the table + * @param string $fname Function name for profiling + * @param array $options Options for select + * @return int Row count + * @since 1.24 + */ + public function selectRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() + ) { + $rows = 0; + $sql = $this->selectSQLText( $table, '1', $conds, $fname, $options ); + $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count" ); + + if ( $res ) { + $row = $this->fetchRow( $res ); + $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0; + } + + 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. @@ -4198,6 +4252,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * @since 1.19 + * @return string */ public function __toString() { return (string)$this->mConn; diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index c2f5b6dff6..3a4bb27058 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -340,6 +340,7 @@ class DatabaseMssql extends DatabaseBase { } /** + * @param array $err * @return string */ private function formatError( $err ) { @@ -1068,6 +1069,7 @@ class DatabaseMssql extends DatabaseBase { /** * Begin a transaction, committing any previously open transaction + * @param string $fname */ protected function doBegin( $fname = __METHOD__ ) { sqlsrv_begin_transaction( $this->mConn ); @@ -1076,6 +1078,7 @@ class DatabaseMssql extends DatabaseBase { /** * End a transaction + * @param string $fname */ protected function doCommit( $fname = __METHOD__ ) { sqlsrv_commit( $this->mConn ); @@ -1085,6 +1088,7 @@ class DatabaseMssql extends DatabaseBase { /** * Rollback a transaction. * No-op on non-transactional databases. + * @param string $fname */ protected function doRollback( $fname = __METHOD__ ) { sqlsrv_rollback( $this->mConn ); diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php index 5ad7c781c2..ba0f39ffde 100644 --- a/includes/db/DatabaseMysqlBase.php +++ b/includes/db/DatabaseMysqlBase.php @@ -901,7 +901,6 @@ abstract class DatabaseMysqlBase extends DatabaseBase { /** * @param bool $value - * @return null|bool|ResultWrapper */ public function setBigSelects( $value = true ) { if ( $value === 'default' ) { diff --git a/includes/db/DatabaseMysqli.php b/includes/db/DatabaseMysqli.php index b8d5d7925a..2ce6307918 100644 --- a/includes/db/DatabaseMysqli.php +++ b/includes/db/DatabaseMysqli.php @@ -292,6 +292,7 @@ class DatabaseMysqli extends DatabaseMysqlBase { * Give an id for the connection * * mysql driver used resource id, but mysqli objects cannot be cast to string. + * @return string */ public function __toString() { if ( $this->mConn instanceof Mysqli ) { diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index f5fdca1fb9..ce14d7a969 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -117,6 +117,7 @@ SQL; /** * @since 1.19 + * @return bool|mixed */ function defaultValue() { if ( $this->has_default ) { @@ -829,6 +830,7 @@ __INDEXATTR__; * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly. * * MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL. + * @see DatabaseBase::selectSQLText */ function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() @@ -1154,7 +1156,7 @@ __INDEXATTR__; return wfTimestamp( TS_POSTGRES, $ts ); } - /* + /** * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12 * to http://www.php.net/manual/en/ref.pgsql.php * @@ -1201,6 +1203,9 @@ __INDEXATTR__; /** * Return aggregated value function call + * @param array $valuedata + * @param string $valuename + * @return array */ public function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index 9a03a339ca..dd2e813e55 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -873,6 +873,9 @@ class DatabaseSqlite extends DatabaseBase { } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) { // DROP INDEX is database-wide, not table-specific, so no ON <table> clause. $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s ); + } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) { + // INSERT IGNORE --> INSERT OR IGNORE + $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s ); } return $s; diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php index 3923241dae..c1e80d3331 100644 --- a/includes/db/DatabaseUtility.php +++ b/includes/db/DatabaseUtility.php @@ -140,7 +140,7 @@ class ResultWrapper implements Iterator { * Fields can be retrieved with $row->fieldname, with fields acting like * member variables. * - * @return object + * @return stdClass * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchObject() { @@ -177,8 +177,8 @@ class ResultWrapper implements Iterator { $this->db->dataSeek( $this, $row ); } - /********************* - * Iterator functions + /* + * ======= Iterator functions ======= * Note that using these in combination with the non-iterator functions * above may cause rows to be skipped or repeated. */ @@ -192,7 +192,7 @@ class ResultWrapper implements Iterator { } /** - * @return int + * @return stdClass|array|bool */ function current() { if ( is_null( $this->currentRow ) ) { @@ -210,7 +210,7 @@ class ResultWrapper implements Iterator { } /** - * @return int + * @return stdClass */ function next() { $this->pos++; @@ -255,6 +255,9 @@ class FakeResultWrapper extends ResultWrapper { return count( $this->result ); } + /** + * @return array|bool + */ function fetchRow() { if ( $this->pos < count( $this->result ) ) { $this->currentRow = $this->result[$this->pos]; @@ -276,7 +279,10 @@ class FakeResultWrapper extends ResultWrapper { function free() { } - // Callers want to be able to access fields with $this->fieldName + /** + * Callers want to be able to access fields with $this->fieldName + * @return bool|stdClass + */ function fetchObject() { $this->fetchRow(); if ( $this->currentRow ) { @@ -291,6 +297,9 @@ class FakeResultWrapper extends ResultWrapper { $this->currentRow = null; } + /** + * @return bool|stdClass + */ function next() { return $this->fetchObject(); } diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php index bf49bbb56c..4dc693acc5 100644 --- a/includes/db/IORMTable.php +++ b/includes/db/IORMTable.php @@ -421,7 +421,7 @@ interface IORMTable { * * @since 1.20 * - * @param array|string $fields + * @param array $fields * * @return array */ diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index db4ed602b6..e517a0250c 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -28,19 +28,38 @@ * @ingroup Database */ class LoadBalancer { - private $mServers, $mConns, $mLoads, $mGroupLoads; + /** @var array Map of (server index => server config array) */ + private $mServers; + /** @var array Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */ + private $mConns; + /** @var array Map of (server index => weight) */ + private $mLoads; + /** @var array Map of (group => server index => weight) */ + private $mGroupLoads; + /** @var bool Whether to disregard slave lag as a factor in slave selection */ + private $mAllowLagged; + /** @var integer Seconds to spend waiting on slave lag to resolve */ + private $mWaitTimeout; + + /** @var array LBFactory information */ + private $mParentInfo; + /** @var string The LoadMonitor subclass name */ + private $mLoadMonitorClass; + /** @var LoadMonitor */ + private $mLoadMonitor; /** @var bool|DatabaseBase Database connection that caused a problem */ private $mErrorConnection; - private $mReadIndex, $mAllowLagged; - + /** @var integer The generic (not query grouped) slave index (of $mServers) */ + private $mReadIndex; /** @var bool|DBMasterPos False if not set */ private $mWaitForPos; - - private $mWaitTimeout; - private $mLaggedSlaveMode, $mLastError = 'Unknown error'; - private $mParentInfo, $mLagTimes; - private $mLoadMonitorClass, $mLoadMonitor; + /** @var bool Whether the generic reader fell back to a lagged slave */ + private $mLaggedSlaveMode; + /** @var string The last DB selection or connection error */ + private $mLastError = 'Unknown error'; + /** @var array Process cache of LoadMonitor::getLagTimes() */ + private $mLagTimes; /** * @param array $params Array with keys: diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index fa2dd99ff8..7281485b47 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -74,9 +74,14 @@ class LoadMonitorNull implements LoadMonitor { class LoadMonitorMySQL implements LoadMonitor { /** @var LoadBalancer */ public $parent; + /** @var BagOStuff */ + protected $cache; public function __construct( $parent ) { + global $wgMemc; + $this->parent = $parent; + $this->cache = $wgMemc ?: wfGetMainCache(); } public function scaleLoads( &$loads, $group = false, $wiki = false ) { @@ -93,14 +98,10 @@ class LoadMonitorMySQL implements LoadMonitor { $expiry = 5; $requestRate = 10; - global $wgMemc; - if ( empty( $wgMemc ) ) { - $wgMemc = wfGetMainCache(); - } - + $cache = $this->cache; $masterName = $this->parent->getServerName( 0 ); $memcKey = wfMemcKey( 'lag_times', $masterName ); - $times = $wgMemc->get( $memcKey ); + $times = $cache->get( $memcKey ); if ( is_array( $times ) ) { # Randomly recache with probability rising over $expiry $elapsed = time() - $times['timestamp']; @@ -116,10 +117,10 @@ class LoadMonitorMySQL implements LoadMonitor { } # Cache key missing or expired - if ( $wgMemc->add( "$memcKey:lock", 1, 10 ) ) { + if ( $cache->add( "$memcKey:lock", 1, 10 ) ) { # Let this process alone update the cache value - $unlocker = new ScopedCallback( function () use ( $wgMemc, $memcKey ) { - $wgMemc->delete( $memcKey ); + $unlocker = new ScopedCallback( function () use ( $cache, $memcKey ) { + $cache->delete( $memcKey ); } ); } elseif ( is_array( $times ) ) { # Could not acquire lock but an old cache exists, so use it @@ -136,12 +137,17 @@ class LoadMonitorMySQL implements LoadMonitor { $times[$i] = $conn->getLag(); } elseif ( false !== ( $conn = $this->parent->openConnection( $i, $wiki ) ) ) { $times[$i] = $conn->getLag(); + // Close the connection to avoid sleeper connections piling up. + // Note that the caller will pick one of these DBs and reconnect, + // which is slightly inefficient, but this only matters for the lag + // time cache miss cache, which is far less common that cache hits. + $this->parent->closeConnection( $conn ); } } # Add a timestamp key so we know when it was cached $times['timestamp'] = time(); - $wgMemc->set( $memcKey, $times, $expiry + 10 ); + $cache->set( $memcKey, $times, $expiry + 10 ); unset( $times['timestamp'] ); // hide from caller return $times; diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php index 24fa68c504..2f898b755b 100644 --- a/includes/db/ORMTable.php +++ b/includes/db/ORMTable.php @@ -748,7 +748,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @since 1.20 * - * @param array|string $fields + * @param array $fields * * @return array */ diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php index 0cea658908..c2f22233bb 100644 --- a/includes/debug/MWDebug.php +++ b/includes/debug/MWDebug.php @@ -182,7 +182,6 @@ class MWDebug { * @param int $callerOffset How far up the callstack is the original * caller. 2 = function that called the function that called * MWDebug::deprecated() (Added in 1.20). - * @return mixed */ public static function deprecated( $function, $version = false, $component = false, $callerOffset = 2 @@ -336,6 +335,28 @@ class MWDebug { return -1; } + // Replace invalid UTF-8 chars with a square UTF-8 character + // This prevents json_encode from erroring out due to binary SQL data + $sql = preg_replace( + '/( + [\xC0-\xC1] # Invalid UTF-8 Bytes + | [\xF5-\xFF] # Invalid UTF-8 Bytes + | \xE0[\x80-\x9F] # Overlong encoding of prior code point + | \xF0[\x80-\x8F] # Overlong encoding of prior code point + | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start + | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start + | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start + | (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle + | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4] + |[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence + | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence + | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence + | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2) + )/x', + '■', + $sql + ); + self::$query[] = array( 'sql' => $sql, 'function' => $function, @@ -539,7 +560,8 @@ class MWDebug { return array( 'mwVersion' => $wgVersion, - 'phpVersion' => PHP_VERSION, + 'phpEngine' => wfIsHHVM() ? 'HHVM' : 'PHP', + 'phpVersion' => wfIsHHVM() ? HHVM_VERSION : PHP_VERSION, 'gitRevision' => GitInfo::headSHA1(), 'gitBranch' => GitInfo::currentBranch(), 'gitViewUrl' => GitInfo::headViewUrl(), diff --git a/includes/deferred/DeferredUpdates.php b/includes/deferred/DeferredUpdates.php index 2178281c17..b0c1899f78 100644 --- a/includes/deferred/DeferredUpdates.php +++ b/includes/deferred/DeferredUpdates.php @@ -99,25 +99,29 @@ class DeferredUpdates { $dbw = wfGetDB( DB_MASTER ); } - /** @var DeferrableUpdate $update */ - foreach ( $updates as $update ) { - try { - $update->doUpdate(); + while ( $updates ) { + self::clearPendingUpdates(); - if ( $doCommit && $dbw->trxLevel() ) { - $dbw->commit( __METHOD__, 'flush' ); - } - } catch ( MWException $e ) { - // We don't want exceptions thrown during deferred updates to - // be reported to the user since the output is already sent. - // Instead we just log them. - if ( !$e instanceof ErrorPageError ) { - MWExceptionHandler::logException( $e ); + /** @var DeferrableUpdate $update */ + foreach ( $updates as $update ) { + try { + $update->doUpdate(); + + if ( $doCommit && $dbw->trxLevel() ) { + $dbw->commit( __METHOD__, 'flush' ); + } + } catch ( MWException $e ) { + // We don't want exceptions thrown during deferred updates to + // be reported to the user since the output is already sent. + // Instead we just log them. + if ( !$e instanceof ErrorPageError ) { + MWExceptionHandler::logException( $e ); + } } } + $updates = array_merge( $wgDeferredUpdateList, self::$updates ); } - self::clearPendingUpdates(); wfProfileOut( __METHOD__ ); } diff --git a/includes/deferred/SearchUpdate.php b/includes/deferred/SearchUpdate.php index 20f348a213..5d084afddd 100644 --- a/includes/deferred/SearchUpdate.php +++ b/includes/deferred/SearchUpdate.php @@ -115,6 +115,8 @@ class SearchUpdate implements DeferrableUpdate { * Clean text for indexing. Only really suitable for indexing in databases. * If you're using a real search engine, you'll probably want to override * this behavior and do something nicer with the original wikitext. + * @param string $text + * @return string */ public static function updateText( $text ) { global $wgContLang; @@ -179,6 +181,7 @@ class SearchUpdate implements DeferrableUpdate { * Get a string representation of a title suitable for * including in a search index * + * @param SearchEngine $search * @return string A stripped-down title string ready for the search index */ private function indexTitle( SearchEngine $search ) { diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 661330d0e7..50e08ca1d2 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -310,11 +310,11 @@ class DifferenceEngine extends ContextSource { 'undoafter' => $this->mOldid, 'undo' => $this->mNewid ) ), - 'title' => Linker::titleAttrib( 'undo' ) + 'title' => Linker::titleAttrib( 'undo' ), ), $this->msg( 'editundo' )->text() ); - $revisionTools[] = $undoLink; + $revisionTools['mw-diff-undo'] = $undoLink; } } @@ -387,8 +387,14 @@ class DifferenceEngine extends ContextSource { wfRunHooks( 'DiffRevisionTools', array( $this->mNewRev, &$revisionTools, $this->mOldRev ) ); $formattedRevisionTools = array(); // Put each one in parentheses (poor man's button) - foreach ( $revisionTools as $tool ) { - $formattedRevisionTools[] = $this->msg( 'parentheses' )->rawParams( $tool )->escaped(); + foreach ( $revisionTools as $key => $tool ) { + $toolClass = is_string( $key ) ? $key : 'mw-diff-tool'; + $element = Html::rawElement( + 'span', + array( 'class' => $toolClass ), + $this->msg( 'parentheses' )->rawParams( $tool )->escaped() + ); + $formattedRevisionTools[] = $element; } $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . ' ' . implode( ' ', $formattedRevisionTools ); @@ -1052,8 +1058,13 @@ class DifferenceEngine extends ContextSource { $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold'; $msg = $this->msg( $key )->escaped(); - $header .= ' ' . $this->msg( 'parentheses' )->rawParams( - Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain(); + $editLink = $this->msg( 'parentheses' )->rawParams( + Linker::linkKnown( $title, $msg, array( ), $editQuery ) )->plain(); + $header .= ' ' . Html::rawElement( + 'span', + array( 'class' => 'mw-diff-edit' ), + $editLink + ); if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $header = Html::rawElement( 'span', @@ -1140,6 +1151,8 @@ class DifferenceEngine extends ContextSource { /** * Use specified text instead of loading from the database + * @param Content $oldContent + * @param Content $newContent * @since 1.21 */ public function setContent( Content $oldContent, Content $newContent ) { @@ -1153,6 +1166,7 @@ class DifferenceEngine extends ContextSource { /** * Set the language in which the diff text is written * (Defaults to page content language). + * @param Language|string $lang * @since 1.19 */ public function setTextLanguage( $lang ) { diff --git a/includes/exception/MWException.php b/includes/exception/MWException.php index 58b07f5af8..074128f889 100644 --- a/includes/exception/MWException.php +++ b/includes/exception/MWException.php @@ -252,6 +252,7 @@ class MWException extends Exception { /** * Send a header, if we haven't already sent them. We shouldn't, * but sometimes we might in a weird case like Export + * @param string $header */ private static function header( $header ) { if ( !headers_sent() ) { diff --git a/includes/exception/UserNotLoggedIn.php b/includes/exception/UserNotLoggedIn.php index 9d89009100..03ba0b2039 100644 --- a/includes/exception/UserNotLoggedIn.php +++ b/includes/exception/UserNotLoggedIn.php @@ -19,12 +19,14 @@ */ /** - * Shows a generic "user is not logged in" error page. + * Redirect a user to the login page * * This is essentially an ErrorPageError exception which by default uses the * 'exception-nologin' as a title and 'exception-nologin-text' for the message. - * @see bug 37627 - * @since 1.20 + * + * @note In order for this exception to redirect, the error message passed to the + * constructor has to be explicitly added to LoginForm::validErrorMessages. Otherwise, + * the user will just be shown the message rather than redirected. * * @par Example: * @code @@ -43,11 +45,16 @@ * } * @endcode * + * @see bug 37627 + * @since 1.20 * @ingroup Exception */ class UserNotLoggedIn extends ErrorPageError { /** + * @note The value of the $reasonMsg parameter must be put into LoginForm::validErrorMessages + * if you want the user to be automatically redirected to the login form. + * * @param string $reasonMsg A message key containing the reason for the error. * Optional, default: 'exception-nologin-text' * @param string $titleMsg A message key to set the page title. @@ -62,4 +69,34 @@ class UserNotLoggedIn extends ErrorPageError { ) { parent::__construct( $titleMsg, $reasonMsg, $params ); } + + /** + * Redirect to Special:Userlogin if the specified message is compatible. Otherwise, + * show an error page as usual. + */ + public function report() { + // If an unsupported message is used, don't try redirecting to Special:Userlogin, + // since the message may not be compatible. + if ( !in_array( $this->msg, LoginForm::$validErrorMessages ) ) { + parent::report(); + } + + // Message is valid. Redirec to Special:Userlogin + + $context = RequestContext::getMain(); + + $output = $context->getOutput(); + $query = $context->getRequest()->getValues(); + // Title will be overridden by returnto + unset( $query['title'] ); + // Redirect to Special:Userlogin + $output->redirect( SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( array( + // Return to this page when the user logs in + 'returnto' => $context->getTitle()->getFullText(), + 'returntoquery' => wfArrayToCgi( $query ), + 'warning' => $this->msg, + ) ) ); + + $output->output(); + } } diff --git a/includes/externalstore/ExternalStoreDB.php b/includes/externalstore/ExternalStoreDB.php index 5774a24c78..952bf63b25 100644 --- a/includes/externalstore/ExternalStoreDB.php +++ b/includes/externalstore/ExternalStoreDB.php @@ -96,9 +96,6 @@ class ExternalStoreDB extends ExternalStoreMedium { if ( !$id ) { throw new MWException( __METHOD__ . ': no insert ID' ); } - if ( $dbw->getFlag( DBO_TRX ) ) { - $dbw->commit( __METHOD__ ); - } return "DB://$cluster/$id"; } @@ -134,7 +131,10 @@ class ExternalStoreDB extends ExternalStoreMedium { wfDebug( "writable external store\n" ); } - return $lb->getConnection( DB_SLAVE, array(), $wiki ); + $db = $lb->getConnection( DB_SLAVE, array(), $wiki ); + $db->clearFlag( DBO_TRX ); // sanity + + return $db; } /** @@ -147,7 +147,10 @@ class ExternalStoreDB extends ExternalStoreMedium { $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false; $lb = $this->getLoadBalancer( $cluster ); - return $lb->getConnection( DB_MASTER, array(), $wiki ); + $db = $lb->getConnection( DB_MASTER, array(), $wiki ); + $db->clearFlag( DBO_TRX ); // sanity + + return $db; } /** @@ -282,6 +285,10 @@ class ExternalStoreDB extends ExternalStoreMedium { } } + /** + * @param string $url + * @return array + */ protected function parseURL( $url ) { $path = explode( '/', $url ); diff --git a/includes/filebackend/FSFile.php b/includes/filebackend/FSFile.php index 47bacb5304..1659c62a34 100644 --- a/includes/filebackend/FSFile.php +++ b/includes/filebackend/FSFile.php @@ -118,9 +118,9 @@ class FSFile { $ext = self::extensionFromPath( $this->path ); } - # mime type according to file contents + # MIME type according to file contents $info['file-mime'] = $this->getMimeType(); - # logical mime type + # logical MIME type $info['mime'] = $magic->improveTypeFromExtension( $info['file-mime'], $ext ); list( $info['major_mime'], $info['minor_mime'] ) = File::splitMime( $info['mime'] ); diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php index 2820be26ad..8c0a61a1e6 100644 --- a/includes/filebackend/FileBackend.php +++ b/includes/filebackend/FileBackend.php @@ -135,19 +135,12 @@ abstract class FileBackend { */ public function __construct( array $config ) { $this->name = $config['name']; + $this->wikiId = $config['wikiId']; // e.g. "my_wiki-en_" if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) { - throw new FileBackendException( "Backend name `{$this->name}` is invalid." ); - } - if ( !isset( $config['wikiId'] ) ) { - $config['wikiId'] = wfWikiID(); - wfDeprecated( __METHOD__ . ' called without "wikiID".', '1.23' ); - } - if ( isset( $config['lockManager'] ) && !is_object( $config['lockManager'] ) ) { - $config['lockManager'] = - LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] ); - wfDeprecated( __METHOD__ . ' called with non-object "lockManager".', '1.23' ); + throw new FileBackendException( "Backend name '{$this->name}' is invalid." ); + } elseif ( !is_string( $this->wikiId ) ) { + throw new FileBackendException( "Backend wiki ID not provided for '{$this->name}'." ); } - $this->wikiId = $config['wikiId']; // e.g. "my_wiki-en_" $this->lockManager = isset( $config['lockManager'] ) ? $config['lockManager'] : new NullLockManager( array() ); @@ -218,7 +211,7 @@ abstract class FileBackend { /** * Check if the backend medium supports a field of extra features * - * @return int Bitfield of FileBackend::ATTR_* flags + * @param int $bitfield Bitfield of FileBackend::ATTR_* flags * @return bool * @since 1.23 */ diff --git a/includes/filebackend/README b/includes/filebackend/README index 569f337655..c06f6fc734 100644 --- a/includes/filebackend/README +++ b/includes/filebackend/README @@ -51,7 +51,7 @@ On files: * read a file into a string or several files into a map of path names to strings * download a file or set of files to a temporary file (on a mounted file system) * get the SHA1 hash of a file -* get various properties of a file (stat information, content time, mime information, ...) +* get various properties of a file (stat information, content time, MIME information, ...) On directories: * get a list of files directly under a directory diff --git a/includes/filebackend/SwiftFileBackend.php b/includes/filebackend/SwiftFileBackend.php index e9c8883d3d..f40ec46e2c 100644 --- a/includes/filebackend/SwiftFileBackend.php +++ b/includes/filebackend/SwiftFileBackend.php @@ -173,7 +173,7 @@ class SwiftFileBackend extends FileBackendStore { * Sanitize and filter the custom headers from a $params array. * We only allow certain Content- and X-Content- headers. * - * @param array $headers + * @param array $params * @return array Sanitized value of 'headers' field in $params */ protected function sanitizeHdrs( array $params ) { diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index a45fb7a2bd..59295257fd 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -546,7 +546,7 @@ class FileRepo { * * STUB * @param string $hash SHA-1 hash - * @return array + * @return File[] */ public function findBySha1( $hash ) { return array(); @@ -1014,6 +1014,7 @@ class FileRepo { } elseif ( is_array( $triple[2] ) && isset( $triple[2]['headers'] ) ) { $headers = $triple[2]['headers']; } + // @fixme: $headers might not be defined $operations[] = array( 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store', 'src' => $src, @@ -1345,13 +1346,16 @@ class FileRepo { * Checks existence of an array of files. * * @param array $files Virtual URLs (or storage paths) of files to check - * @return array|bool Either array of files and existence flags, or false + * @return array Map of files and existence flags, or false */ public function fileExistsBatch( array $files ) { + $paths = array_map( array( $this, 'resolveToStoragePath' ), $files ); + $this->backend->preloadFileStat( array( 'srcs' => $paths ) ); + $result = array(); foreach ( $files as $key => $file ) { - $file = $this->resolveToStoragePath( $file ); - $result[$key] = $this->backend->fileExists( array( 'src' => $file ) ); + $path = $this->resolveToStoragePath( $file ); + $result[$key] = $this->backend->fileExists( array( 'src' => $path ) ); } return $result; @@ -1447,6 +1451,7 @@ class FileRepo { * Delete files in the deleted directory if they are not referenced in the filearchive table * * STUB + * @param array $storageKeys */ public function cleanupDeletedBatch( array $storageKeys ) { $this->assertWritableRepo(); diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 96c8803c85..926fd0b8a8 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -49,7 +49,7 @@ class LocalRepo extends FileRepo { /** * @throws MWException - * @param array $row + * @param stdClass $row * @return LocalFile */ function newFileFromRow( $row ) { @@ -91,7 +91,7 @@ class LocalRepo extends FileRepo { $hashPath = $this->getDeletedHashPath( $key ); $path = "$root/$hashPath$key"; $dbw->begin( __METHOD__ ); - // Check for usage in deleted/hidden files and pre-emptively + // Check for usage in deleted/hidden files and preemptively // lock the key to avoid any future use until we are finished. $deleted = $this->deletedFileHasKey( $key, 'lock' ); $hidden = $this->hiddenFileHasKey( $key, 'lock' ); @@ -167,7 +167,7 @@ class LocalRepo extends FileRepo { * Checks if there is a redirect named as $title * * @param Title $title Title of file - * @return bool + * @return bool|Title */ function checkRedirect( Title $title ) { global $wgMemc; @@ -370,7 +370,7 @@ class LocalRepo extends FileRepo { * SHA-1 content hash. * * @param string $hash A sha1 hash to look for - * @return array + * @return File[] */ function findBySha1( $hash ) { $dbr = $this->getSlaveDB(); diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index fce3f7854b..fab4216281 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -170,7 +170,7 @@ class RepoGroup { /** * Search repositories for many files at once. * - * @param array $items An array of titles, or an array of findFile() options with + * @param array $inputItems An array of titles, or an array of findFile() options with * the "title" option giving the title. Example: * * $findItem = array( 'title' => $title, 'private' => true ); @@ -183,10 +183,6 @@ class RepoGroup { * The search title uses the input titles; the other is the final post-redirect title. * All titles are returned as string DB keys and the inner array is associative. * @return array Map of (file name => File objects) for matches - * - * @param array $inputItems - * @param int $flags - * @return array */ function findFiles( array $inputItems, $flags = 0 ) { if ( !$this->reposInitialised ) { @@ -221,7 +217,7 @@ class RepoGroup { /** * Interface for FileRepo::checkRedirect() * @param Title $title - * @return bool + * @return bool|Title */ function checkRedirect( Title $title ) { if ( !$this->reposInitialised ) { @@ -273,7 +269,7 @@ class RepoGroup { * Find all instances of files with this key * * @param string $hash Base 36 SHA-1 hash - * @return array Array of File objects + * @return File[] */ function findBySha1( $hash ) { if ( !$this->reposInitialised ) { @@ -409,6 +405,8 @@ class RepoGroup { /** * Create a repo class based on an info structure + * @param array $info + * @return FileRepo */ protected function newRepo( $info ) { $class = $info['class']; diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index 735bf8a555..8bf90402f1 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -383,7 +383,7 @@ class ArchivedFile { } /** - * Returns the mime type of the file. + * Returns the MIME type of the file. * @return string */ public function getMimeType() { @@ -407,6 +407,7 @@ class ArchivedFile { /** * Returns the number of pages of a multipage document, or false for * documents which aren't multipage documents + * @return bool|int */ function pageCount() { if ( !isset( $this->pageCount ) ) { diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index c6da1f18b4..f9e0a2dc93 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -679,7 +679,7 @@ abstract class File { } /** - * Returns the mime type of the file. + * Returns the MIME type of the file. * Overridden by LocalFile, UnregisteredLocalFile * STUB * @@ -714,7 +714,7 @@ abstract class File { */ function canRender() { if ( !isset( $this->canRender ) ) { - $this->canRender = $this->getHandler() && $this->handler->canRender( $this ); + $this->canRender = $this->getHandler() && $this->handler->canRender( $this ) && $this->exists(); } return $this->canRender; @@ -1261,7 +1261,7 @@ abstract class File { /** * Creates a temp FS file with the same extension and the thumbnail * @param string $thumbPath Thumbnail path - * @returns TempFSFile + * @return TempFSFile */ protected function makeTransformTmpFile( $thumbPath ) { $thumbExt = FileBackend::extensionFromPath( $thumbPath ); @@ -1287,6 +1287,7 @@ abstract class File { * Hook into transform() to allow migration of thumbnail files * STUB * Overridden by LocalFile + * @param string $thumbName */ function migrateThumbFile( $thumbName ) { } @@ -1311,16 +1312,16 @@ abstract class File { * @return ThumbnailImage */ function iconThumb() { - global $wgStylePath, $wgStyleDirectory; + global $wgScriptPath, $IP; + $assetsPath = "$wgScriptPath/assets/file-type-icons/"; + $assetsDirectory = "$IP/assets/file-type-icons/"; $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' ); foreach ( $try as $icon ) { - $path = '/common/images/icons/' . $icon; - $filepath = $wgStyleDirectory . $path; - if ( file_exists( $filepath ) ) { // always FS + if ( file_exists( $assetsDirectory . $icon ) ) { // always FS $params = array( 'width' => 120, 'height' => 120 ); - return new ThumbnailImage( $this, $wgStylePath . $path, false, $params ); + return new ThumbnailImage( $this, $assetsPath . $icon, false, $params ); } } @@ -1330,6 +1331,7 @@ abstract class File { /** * Get last thumbnailing error. * Largely obsolete. + * @return string */ function getLastError() { return $this->lastError; @@ -1937,7 +1939,7 @@ abstract class File { * @note Use getWidth()/getHeight() instead of this method unless you have a * a good reason. This method skips all caches. * - * @param string $fileName The path to the file (e.g. From getLocalPathRef() ) + * @param string $filePath The path to the file (e.g. From getLocalPathRef() ) * @return array The width, followed by height, with optionally more things after */ function getImageSize( $filePath ) { diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 1eff1dfec4..8824b66961 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -91,10 +91,10 @@ class LocalFile extends File { /** @var int Result of the query for the file's history (nextHistoryLine) */ private $historyRes; - /** @var string Major mime type */ + /** @var string Major MIME type */ private $major_mime; - /** @var string Minor mime type */ + /** @var string Minor MIME type */ private $minor_mime; /** @var string Upload timestamp */ @@ -379,6 +379,7 @@ class LocalFile extends File { /** * Load file metadata from the DB + * @param int $flags */ function loadFromDB( $flags = 0 ) { # Polymorphic function name to distinguish foreign and local fetches @@ -784,7 +785,7 @@ class LocalFile extends File { } /** - * Returns the mime type of the file. + * Returns the MIME type of the file. * @return string */ function getMimeType() { @@ -931,6 +932,7 @@ class LocalFile extends File { /** * Delete cached transformed files for the current version only. + * @param array $options */ function purgeThumbnails( $options = array() ) { global $wgUseSquid; @@ -2282,7 +2284,12 @@ class LocalFileDeleteBatch { $this->doDBInserts(); // Removes non-existent file from the batch, so we don't get errors. - $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch ); + $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch ); + if ( !$checkStatus->isGood() ) { + $this->status->merge( $checkStatus ); + return $this->status; + } + $this->deletionBatch = $checkStatus->value; // Execute the file deletion batch $status = $this->file->repo->deleteBatch( $this->deletionBatch ); @@ -2314,7 +2321,7 @@ class LocalFileDeleteBatch { /** * Removes non-existent files from a deletion batch. * @param array $batch - * @return array + * @return Status */ function removeNonexistentFiles( $batch ) { $files = $newBatch = array(); @@ -2325,6 +2332,10 @@ class LocalFileDeleteBatch { } $result = $this->file->repo->fileExistsBatch( $files ); + if ( in_array( null, $result, true ) ) { + return Status::newFatal( 'backend-fail-internal', + $this->file->repo->getBackend()->getName() ); + } foreach ( $batch as $batchItem ) { if ( $result[$batchItem[0]] ) { @@ -2332,7 +2343,7 @@ class LocalFileDeleteBatch { } } - return $newBatch; + return Status::newGood( $newBatch ); } } @@ -2371,6 +2382,7 @@ class LocalFileRestoreBatch { /** * Add a file by ID + * @param int $fa_id */ function addId( $fa_id ) { $this->ids[] = $fa_id; @@ -2378,6 +2390,7 @@ class LocalFileRestoreBatch { /** * Add a whole lot of files by ID + * @param int[] $ids */ function addIds( $ids ) { $this->ids = array_merge( $this->ids, $ids ); @@ -2571,7 +2584,12 @@ class LocalFileRestoreBatch { } // Remove missing files from batch, so we don't get errors when undeleting them - $storeBatch = $this->removeNonexistentFiles( $storeBatch ); + $checkStatus = $this->removeNonexistentFiles( $storeBatch ); + if ( !$checkStatus->isGood() ) { + $status->merge( $checkStatus ); + return $status; + } + $storeBatch = $checkStatus->value; // Run the store batch // Use the OVERWRITE_SAME flag to smooth over a common error @@ -2631,7 +2649,7 @@ class LocalFileRestoreBatch { /** * Removes non-existent files from a store batch. * @param array $triplets - * @return array + * @return Status */ function removeNonexistentFiles( $triplets ) { $files = $filteredTriplets = array(); @@ -2640,6 +2658,10 @@ class LocalFileRestoreBatch { } $result = $this->file->repo->fileExistsBatch( $files ); + if ( in_array( null, $result, true ) ) { + return Status::newFatal( 'backend-fail-internal', + $this->file->repo->getBackend()->getName() ); + } foreach ( $triplets as $file ) { if ( $result[$file[0]] ) { @@ -2647,7 +2669,7 @@ class LocalFileRestoreBatch { } } - return $filteredTriplets; + return Status::newGood( $filteredTriplets ); } /** @@ -2820,7 +2842,12 @@ class LocalFileMoveBatch { $status = $repo->newGood(); $triplets = $this->getMoveTriplets(); - $triplets = $this->removeNonexistentFiles( $triplets ); + $checkStatus = $this->removeNonexistentFiles( $triplets ); + if ( !$checkStatus->isGood() ) { + $status->merge( $checkStatus ); + return $status; + } + $triplets = $checkStatus->value; $destFile = wfLocalFile( $this->target ); $this->file->lock(); // begin @@ -2947,7 +2974,7 @@ class LocalFileMoveBatch { /** * Removes non-existent files from move batch. * @param array $triplets - * @return array + * @return Status */ function removeNonexistentFiles( $triplets ) { $files = array(); @@ -2957,8 +2984,12 @@ class LocalFileMoveBatch { } $result = $this->file->repo->fileExistsBatch( $files ); - $filteredTriplets = array(); + if ( in_array( null, $result, true ) ) { + return Status::newFatal( 'backend-fail-internal', + $this->file->repo->getBackend()->getName() ); + } + $filteredTriplets = array(); foreach ( $triplets as $file ) { if ( $result[$file[0]] ) { $filteredTriplets[] = $file; @@ -2967,12 +2998,13 @@ class LocalFileMoveBatch { } } - return $filteredTriplets; + return Status::newGood( $filteredTriplets ); } /** * Cleanup a partially moved array of triplets by deleting the target * files. Called if something went wrong half way. + * @param array $triplets */ function cleanupTarget( $triplets ) { // Create dest pairs from the triplets @@ -2988,6 +3020,7 @@ class LocalFileMoveBatch { /** * Cleanup a fully moved array of triplets by deleting the source files. * Called at the end of the move process if everything else went ok. + * @param array $triplets */ function cleanupSource( $triplets ) { // Create source file names from the triplets diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php index 0adcc73c62..710058fba6 100644 --- a/includes/filerepo/file/OldLocalFile.php +++ b/includes/filerepo/file/OldLocalFile.php @@ -404,4 +404,18 @@ class OldLocalFile extends LocalFile { return true; } + + /** + * If archive name is an empty string, then file does not "exist" + * + * This is the case for a couple files on Wikimedia servers where + * the old version is "lost". + */ + public function exists() { + $archiveName = $this->getArchiveName(); + if ( $archiveName === '' || !is_string( $archiveName ) ) { + return false; + } + return parent::exists(); + } } diff --git a/includes/gallery/ImageGalleryBase.php b/includes/gallery/ImageGalleryBase.php index 53c2e10c5e..b0a593de97 100644 --- a/includes/gallery/ImageGalleryBase.php +++ b/includes/gallery/ImageGalleryBase.php @@ -86,19 +86,25 @@ abstract class ImageGalleryBase extends ContextSource { * should use to get a gallery. * * @param string|bool $mode Mode to use. False to use the default + * @param IContextSource|null $context + * @return ImageGalleryBase * @throws MWException */ - static function factory( $mode = false ) { - global $wgGalleryOptions, $wgContLang; + static function factory( $mode = false, IContextSource $context = null ) { + global $wgContLang; self::loadModes(); + if ( !$context ) { + $context = RequestContext::getMainAndWarn( __METHOD__ ); + } if ( !$mode ) { - $mode = $wgGalleryOptions['mode']; + $galleryOpions = $context->getConfig()->get( 'GalleryOptions' ); + $mode = $galleryOpions['mode']; } $mode = $wgContLang->lc( $mode ); if ( isset( self::$modeMapping[$mode] ) ) { - return new self::$modeMapping[$mode]( $mode ); + return new self::$modeMapping[$mode]( $mode, $context ); } else { throw new MWException( "No gallery class registered for mode $mode" ); } @@ -123,18 +129,24 @@ abstract class ImageGalleryBase extends ContextSource { * * You should not call this directly, but instead use * ImageGalleryBase::factory(). + * @param string $mode + * @param IContextSource|null $context */ - function __construct( $mode = 'traditional' ) { - global $wgGalleryOptions; + function __construct( $mode = 'traditional', IContextSource $context = null ) { + if ( $context ) { + $this->setContext( $context ); + } + + $galleryOptions = $this->getConfig()->get( 'GalleryOptions' ); $this->mImages = array(); - $this->mShowBytes = $wgGalleryOptions['showBytes']; + $this->mShowBytes = $galleryOptions['showBytes']; $this->mShowFilename = true; $this->mParser = false; $this->mHideBadImages = false; - $this->mPerRow = $wgGalleryOptions['imagesPerRow']; - $this->mWidths = $wgGalleryOptions['imageWidth']; - $this->mHeights = $wgGalleryOptions['imageHeight']; - $this->mCaptionLength = $wgGalleryOptions['captionLength']; + $this->mPerRow = $galleryOptions['imagesPerRow']; + $this->mWidths = $galleryOptions['imageWidth']; + $this->mHeights = $galleryOptions['imageHeight']; + $this->mCaptionLength = $galleryOptions['captionLength']; $this->mMode = $mode; } @@ -154,6 +166,7 @@ abstract class ImageGalleryBase extends ContextSource { /** * Set bad image flag + * @param bool $flag */ function setHideBadImages( $flag = true ) { $this->mHideBadImages = $flag; diff --git a/includes/gallery/PackedImageGallery.php b/includes/gallery/PackedImageGallery.php index b004a9529e..52a49ddbc0 100644 --- a/includes/gallery/PackedImageGallery.php +++ b/includes/gallery/PackedImageGallery.php @@ -95,6 +95,7 @@ class PackedImageGallery extends TraditionalImageGallery { /** * Add javascript which auto-justifies the rows by manipulating the image sizes. * Also ensures that the hover version of this degrades gracefully. + * @return array */ protected function getModules() { return array( 'mediawiki.page.gallery' ); @@ -103,6 +104,7 @@ class PackedImageGallery extends TraditionalImageGallery { /** * Do not support per-row on packed. It really doesn't work * since the images have varying widths. + * @param int $num */ public function setPerRow( $num ) { return; diff --git a/includes/htmlform/HTMLAutoCompleteSelectField.php b/includes/htmlform/HTMLAutoCompleteSelectField.php new file mode 100644 index 0000000000..49053628de --- /dev/null +++ b/includes/htmlform/HTMLAutoCompleteSelectField.php @@ -0,0 +1,165 @@ +<?php + +/** + * Text field for selecting a value from a large list of possible values, with + * auto-completion and optionally with a select dropdown for selecting common + * options. + * + * If one of 'options-messages', 'options', or 'options-message' is provided + * and non-empty, the select dropdown will be shown. An 'other' key will be + * appended using message 'htmlform-selectorother-other' if not already + * present. + * + * Besides the parameters recognized by HTMLTextField, the following are + * recognized: + * options-messages - As for HTMLSelectField + * options - As for HTMLSelectField + * options-message - As for HTMLSelectField + * autocomplete - Associative array mapping display text to values. + * autocomplete-messages - Like autocomplete, but keys are message names. + * require-match - Boolean, if true the value must be in the options or the + * autocomplete. + * other-message - Message to use instead of htmlform-selectorother-other for + * the 'other' message. + * other - Raw text to use for the 'other' message + * + */ +class HTMLAutoCompleteSelectField extends HTMLTextField { + protected $autocomplete = array(); + + function __construct( $params ) { + $params += array( + 'require-match' => false, + ); + + parent::__construct( $params ); + + if ( array_key_exists( 'autocomplete-messages', $this->mParams ) ) { + foreach ( $this->mParams['autocomplete-messages'] as $key => $value ) { + $key = $this->msg( $key )->plain(); + $this->autocomplete[$key] = strval( $value ); + } + } elseif ( array_key_exists( 'autocomplete', $this->mParams ) ) { + foreach ( $this->mParams['autocomplete'] as $key => $value ) { + $this->autocomplete[$key] = strval( $value ); + } + } + if ( !is_array( $this->autocomplete ) || !$this->autocomplete ) { + throw new MWException( 'HTMLAutoCompleteSelectField called without any autocompletions' ); + } + + $this->getOptions(); + if ( $this->mOptions && !in_array( 'other', $this->mOptions, true ) ) { + if ( isset( $params['other-message'] ) ) { + $msg = wfMessage( $params['other-message'] )->text(); + } elseif ( isset( $params['other'] ) ) { + $msg = $params['other']; + } else { + $msg = wfMessage( 'htmlform-selectorother-other' )->text(); + } + $this->mOptions[$msg] = 'other'; + } + } + + function loadDataFromRequest( $request ) { + if ( $request->getCheck( $this->mName ) ) { + $val = $request->getText( $this->mName . '-select', 'other' ); + + if ( $val === 'other' ) { + $val = $request->getText( $this->mName ); + if ( isset( $this->autocomplete[$val] ) ) { + $val = $this->autocomplete[$val]; + } + } + + return $val; + } else { + return $this->getDefault(); + } + } + + function validate( $value, $alldata ) { + $p = parent::validate( $value, $alldata ); + + if ( $p !== true ) { + return $p; + } + + $validOptions = HTMLFormField::flattenOptions( $this->getOptions() ); + + if ( in_array( strval( $value ), $validOptions, true ) ) { + return true; + } elseif ( in_array( strval( $value ), $this->autocomplete, true ) ) { + return true; + } elseif ( $this->mParams['require-match'] ) { + return $this->msg( 'htmlform-select-badoption' )->parse(); + } + + return true; + } + + function getAttributes( array $list ) { + $attribs = array( + 'type' => 'text', + 'data-autocomplete' => FormatJson::encode( array_keys( $this->autocomplete ) ), + ) + parent::getAttributes( $list ); + + if ( $this->getOptions() ) { + $attribs['data-hide-if'] = FormatJson::encode( + array( '!==', $this->mName . '-select', 'other' ) + ); + } + + return $attribs; + } + + function getInputHTML( $value ) { + $oldClass = $this->mClass; + $this->mClass = (array)$this->mClass; + + $valInSelect = false; + $ret = ''; + + if ( $this->getOptions() ) { + if ( $value !== false ) { + $value = strval( $value ); + $valInSelect = in_array( + $value, HTMLFormField::flattenOptions( $this->getOptions() ), true + ); + } + + $selected = $valInSelect ? $value : 'other'; + $select = new XmlSelect( $this->mName . '-select', $this->mID . '-select', $selected ); + $select->addOptions( $this->getOptions() ); + $select->setAttribute( 'class', 'mw-htmlform-select-or-other' ); + + if ( !empty( $this->mParams['disabled'] ) ) { + $select->setAttribute( 'disabled', 'disabled' ); + } + + if ( isset( $this->mParams['tabindex'] ) ) { + $select->setAttribute( 'tabindex', $this->mParams['tabindex'] ); + } + + $ret = $select->getHTML() . "<br />\n"; + + $this->mClass[] = 'mw-htmlform-hide-if'; + } + + if ( $valInSelect ) { + $value = ''; + } else { + $key = array_search( strval( $value ), $this->autocomplete, true ); + if ( $key !== false ) { + $value = $key; + } + } + + $this->mClass[] = 'mw-htmlform-autocomplete'; + $ret .= parent::getInputHTML( $valInSelect ? '' : $value ); + $this->mClass = $oldClass; + + return $ret; + } + +} diff --git a/includes/htmlform/HTMLCheckField.php b/includes/htmlform/HTMLCheckField.php index a0dd37054d..5f70362a5a 100644 --- a/includes/htmlform/HTMLCheckField.php +++ b/includes/htmlform/HTMLCheckField.php @@ -5,6 +5,8 @@ */ class HTMLCheckField extends HTMLFormField { function getInputHTML( $value ) { + global $wgUseMediaWikiUIEverywhere; + if ( !empty( $this->mParams['invert'] ) ) { $value = !$value; } @@ -26,9 +28,19 @@ class HTMLCheckField extends HTMLFormField { ), Xml::check( $this->mName, $value, $attr ) . $this->mLabel ); } else { - return Xml::check( $this->mName, $value, $attr ) + $chkLabel = Xml::check( $this->mName, $value, $attr ) . ' ' . Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel ); + + if ( $wgUseMediaWikiUIEverywhere ) { + $chkLabel = Html::rawElement( + 'div', + array( 'class' => 'mw-ui-checkbox' ), + $chkLabel + ); + } + + return $chkLabel; } } diff --git a/includes/htmlform/HTMLCheckMatrix.php b/includes/htmlform/HTMLCheckMatrix.php index 606523b6ad..6c538fdd15 100644 --- a/includes/htmlform/HTMLCheckMatrix.php +++ b/includes/htmlform/HTMLCheckMatrix.php @@ -113,8 +113,9 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { foreach ( $columns as $columnTag ) { $thisTag = "$columnTag-$rowTag"; // Construct the checkbox + $thisId = "{$this->mID}-$thisTag"; $thisAttribs = array( - 'id' => "{$this->mID}-$thisTag", + 'id' => $thisId, 'value' => $thisTag, ); $checked = in_array( $thisTag, (array)$value, true ); @@ -125,10 +126,17 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { $checked = true; $thisAttribs['disabled'] = 1; } + $chkBox = Xml::check( "{$this->mName}[]", $checked, $attribs + $thisAttribs ); + if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { + $chkBox = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . + $chkBox . + Html::element( 'label', array( 'for' => $thisId ) ) . + Html::closeElement( 'div' ); + } $rowContents .= Html::rawElement( 'td', array(), - Xml::check( "{$this->mName}[]", $checked, $attribs + $thisAttribs ) + $chkBox ); } $tableContents .= Html::rawElement( 'tr', array(), "\n$rowContents\n" ); diff --git a/includes/htmlform/HTMLEditTools.php b/includes/htmlform/HTMLEditTools.php index 4f1b530224..77924ef24e 100644 --- a/includes/htmlform/HTMLEditTools.php +++ b/includes/htmlform/HTMLEditTools.php @@ -16,6 +16,8 @@ class HTMLEditTools extends HTMLFormField { } /** + * @param string $value + * @return string * @since 1.20 */ public function getDiv( $value ) { @@ -25,6 +27,8 @@ class HTMLEditTools extends HTMLFormField { } /** + * @param string $value + * @return string * @since 1.20 */ public function getRaw( $value ) { diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index 6cf8d0a7eb..c810f68d00 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -107,6 +107,7 @@ class HTMLForm extends ContextSource { 'select' => 'HTMLSelectField', 'radio' => 'HTMLRadioField', 'multiselect' => 'HTMLMultiSelectField', + 'limitselect' => 'HTMLSelectLimitField', 'check' => 'HTMLCheckField', 'toggle' => 'HTMLCheckField', 'int' => 'HTMLIntField', @@ -119,6 +120,7 @@ class HTMLForm extends ContextSource { 'edittools' => 'HTMLEditTools', 'checkmatrix' => 'HTMLCheckMatrix', 'cloner' => 'HTMLFormFieldCloner', + 'autocompleteselect' => 'HTMLAutoCompleteSelectField', // HTMLTextField will output the correct type="" attribute automagically. // There are about four zillion other HTML5 input types, like range, but // we don't use those at the moment, so no point in adding all of them. @@ -299,7 +301,11 @@ class HTMLForm extends ContextSource { * @return string */ public function getDisplayFormat() { - return $this->displayFormat; + $format = $this->displayFormat; + if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) { + $format = 'div'; + } + return $format; } /** @@ -839,8 +845,6 @@ class HTMLForm extends ContextSource { * @return string HTML. */ function getHiddenFields() { - global $wgArticlePath; - $html = ''; if ( $this->getMethod() == 'post' ) { $html .= Html::hidden( @@ -851,7 +855,8 @@ class HTMLForm extends ContextSource { $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; } - if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() == 'get' ) { + $articlePath = $this->getConfig()->get( 'ArticlePath' ); + if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() == 'get' ) { $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; } @@ -869,6 +874,7 @@ class HTMLForm extends ContextSource { */ function getButtons() { $buttons = ''; + $useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' ); if ( $this->mShowSubmit ) { $attribs = array(); @@ -887,15 +893,17 @@ class HTMLForm extends ContextSource { $attribs['class'] = array( 'mw-htmlform-submit' ); + if ( $this->isVForm() || $useMediaWikiUIEverywhere ) { + array_push( $attribs['class'], 'mw-ui-button', 'mw-ui-constructive' ); + } + if ( $this->isVForm() ) { // mw-ui-block is necessary because the buttons aren't necessarily in an // immediate child div of the vform. // @todo Let client specify if the primary submit button is progressive or destructive array_push( $attribs['class'], - 'mw-ui-button', 'mw-ui-big', - 'mw-ui-constructive', 'mw-ui-block' ); } @@ -928,6 +936,17 @@ class HTMLForm extends ContextSource { $attrs['id'] = $button['id']; } + if ( $this->isVForm() || $useMediaWikiUIEverywhere ) { + if ( isset( $attrs['class'] ) ) { + $attrs['class'] .= ' mw-ui-button'; + } else { + $attrs['class'] = 'mw-ui-button'; + } + if ( $this->isVForm() ) { + $attrs['class'] .= ' mw-ui-big mw-ui-block'; + } + } + $buttons .= Html::element( 'input', $attrs ) . "\n"; } @@ -1245,6 +1264,9 @@ class HTMLForm extends ContextSource { // Close enough to a div. $getFieldHtmlMethod = 'getDiv'; break; + case 'div': + $getFieldHtmlMethod = 'getDiv'; + break; default: $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat ); } @@ -1418,20 +1440,19 @@ class HTMLForm extends ContextSource { * @return string */ public function getAction() { - global $wgScript, $wgArticlePath; - // If an action is alredy provided, return it if ( $this->mAction !== false ) { return $this->mAction; } - // Check whether we are in GET mode and $wgArticlePath contains a "?" + $articlePath = $this->getConfig()->get( 'ArticlePath' ); + // Check whether we are in GET mode and the ArticlePath contains a "?" // meaning that getLocalURL() would return something like "index.php?title=...". // As browser remove the query string before submitting GET forms, - // it means that the title would be lost. In such case use $wgScript instead + // it means that the title would be lost. In such case use wfScript() instead // and put title in an hidden field (see getHiddenFields()). - if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() === 'get' ) { - return $wgScript; + if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) { + return wfScript(); } return $this->getTitle()->getLocalURL(); diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index 7e4b15b051..70b15354e8 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -593,6 +593,9 @@ abstract class HTMLFormField { $wrapperAttributes = array( 'class' => 'htmlform-tip', ); + if ( $this->mHelpClass !== false ) { + $wrapperAttributes['class'] .= " {$this->mHelpClass}"; + } if ( $this->mHideIf ) { $wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); $wrapperAttributes['class'] .= ' mw-htmlform-hide-if'; diff --git a/includes/htmlform/HTMLFormFieldCloner.php b/includes/htmlform/HTMLFormFieldCloner.php index 597a03f2ea..029911cdde 100644 --- a/includes/htmlform/HTMLFormFieldCloner.php +++ b/includes/htmlform/HTMLFormFieldCloner.php @@ -80,7 +80,7 @@ class HTMLFormFieldCloner extends HTMLFormField { * specified key. * * @param string $key Array key under which these fields should be named - * @return HTMLFormFields[] + * @return HTMLFormField[] */ protected function createFieldsForKey( $key ) { $fields = array(); @@ -303,6 +303,7 @@ class HTMLFormFieldCloner extends HTMLFormField { 'cssclass' => 'mw-htmlform-cloner-delete-button', 'default' => $this->msg( $label )->text(), ) ); + $field->mParent = $this->mParent; $v = $field->getDefault(); if ( $displayFormat === 'table' ) { @@ -373,6 +374,7 @@ class HTMLFormFieldCloner extends HTMLFormField { 'cssclass' => 'mw-htmlform-cloner-create-button', 'default' => $this->msg( $label )->text(), ) ); + $field->mParent = $this->mParent; $html .= $field->getInputHTML( $field->getDefault() ); return $html; diff --git a/includes/htmlform/HTMLHiddenField.php b/includes/htmlform/HTMLHiddenField.php index 6ea95ed144..e32c0bb2ce 100644 --- a/includes/htmlform/HTMLHiddenField.php +++ b/includes/htmlform/HTMLHiddenField.php @@ -21,6 +21,8 @@ class HTMLHiddenField extends HTMLFormField { } /** + * @param string $value + * @return string * @since 1.20 */ public function getDiv( $value ) { @@ -28,6 +30,8 @@ class HTMLHiddenField extends HTMLFormField { } /** + * @param string $value + * @return string * @since 1.20 */ public function getRaw( $value ) { diff --git a/includes/htmlform/HTMLInfoField.php b/includes/htmlform/HTMLInfoField.php index cff82025be..a422047ac4 100644 --- a/includes/htmlform/HTMLInfoField.php +++ b/includes/htmlform/HTMLInfoField.php @@ -23,6 +23,8 @@ class HTMLInfoField extends HTMLFormField { } /** + * @param string $value + * @return string * @since 1.20 */ public function getDiv( $value ) { @@ -34,6 +36,8 @@ class HTMLInfoField extends HTMLFormField { } /** + * @param string $value + * @return string * @since 1.20 */ public function getRaw( $value ) { diff --git a/includes/htmlform/HTMLMultiSelectField.php b/includes/htmlform/HTMLMultiSelectField.php index 576f5cd66a..1b71ab9513 100644 --- a/includes/htmlform/HTMLMultiSelectField.php +++ b/includes/htmlform/HTMLMultiSelectField.php @@ -47,6 +47,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable } else { $thisAttribs = array( 'id' => "{$this->mID}-$info", 'value' => $info ); + // @todo: Make this use checkLabel for consistency purposes $checkbox = Xml::check( $this->mName . '[]', in_array( $info, $value, true ), diff --git a/includes/htmlform/HTMLSelectLimitField.php b/includes/htmlform/HTMLSelectLimitField.php new file mode 100644 index 0000000000..e7f1c047ce --- /dev/null +++ b/includes/htmlform/HTMLSelectLimitField.php @@ -0,0 +1,35 @@ +<?php + +/** + * A limit dropdown, which accepts any valid number + */ +class HTMLSelectLimitField extends HTMLSelectField { + /** + * Basically don't do any validation. If it's a number that's fine. Also, + * add it to the list if it's not there already + * + * @param string $value + * @param array $alldata + * @return bool + */ + function validate( $value, $alldata ) { + if ( $value == '' ) { + return true; + } + + // Let folks pick an explicit limit not from our list, as long as it's a real numbr. + if ( !in_array( $value, $this->mParams['options'] ) + && $value == intval( $value ) + && $value > 0 + ) { + // This adds the explicitly requested limit value to the drop-down, + // then makes sure it's sorted correctly so when we output the list + // later, the custom option doesn't just show up last. + $this->mParams['options'][$this->mParent->getLanguage()->formatNum( $value )] = + intval( $value ); + asort( $this->mParams['options'] ); + } + + return true; + } +} diff --git a/includes/htmlform/HTMLTextAreaField.php b/includes/htmlform/HTMLTextAreaField.php index 4fd198976b..21173d2a63 100644 --- a/includes/htmlform/HTMLTextAreaField.php +++ b/includes/htmlform/HTMLTextAreaField.php @@ -15,7 +15,6 @@ class HTMLTextAreaField extends HTMLFormField { function getInputHTML( $value ) { $attribs = array( 'id' => $this->mID, - 'name' => $this->mName, 'cols' => $this->getCols(), 'rows' => $this->getRows(), ) + $this->getTooltipAndAccessKey(); @@ -34,7 +33,6 @@ class HTMLTextAreaField extends HTMLFormField { ); $attribs += $this->getAttributes( $allowedParams ); - - return Html::element( 'textarea', $attribs, $value ); + return Html::textarea( $this->mName, $value, $attribs ); } } diff --git a/includes/htmlform/HTMLTextField.php b/includes/htmlform/HTMLTextField.php index e584d886c4..10bc67f0be 100644 --- a/includes/htmlform/HTMLTextField.php +++ b/includes/htmlform/HTMLTextField.php @@ -41,13 +41,14 @@ class HTMLTextField extends HTMLFormField { # Implement tiny differences between some field variants # here, rather than creating a new class for each one which # is essentially just a clone of this one. + $type = 'text'; if ( isset( $this->mParams['type'] ) ) { switch ( $this->mParams['type'] ) { case 'int': - $attribs['type'] = 'number'; + $type = 'number'; break; case 'float': - $attribs['type'] = 'number'; + $type = 'number'; $attribs['step'] = 'any'; break; # Pass through @@ -55,11 +56,10 @@ class HTMLTextField extends HTMLFormField { case 'password': case 'file': case 'url': - $attribs['type'] = $this->mParams['type']; + $type = $this->mParams['type']; break; } } - - return Html::element( 'input', $attribs ); + return Html::input( $this->mName, $value, $type, $attribs ); } } diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php index 8a01b32b28..31b93c8855 100644 --- a/includes/installer/DatabaseInstaller.php +++ b/includes/installer/DatabaseInstaller.php @@ -163,19 +163,26 @@ abstract class DatabaseInstaller { } /** - * Create database tables from scratch. + * Apply a SQL source file to the database as part of running an installation step. * + * @param string $sourceFileMethod + * @param string $stepName + * @param string $archiveTableMustNotExist * @return Status */ - public function createTables() { + private function stepApplySourceFile( + $sourceFileMethod, + $stepName, + $archiveTableMustNotExist = false + ) { $status = $this->getConnection(); if ( !$status->isOK() ) { return $status; } $this->db->selectDB( $this->getVar( 'wgDBname' ) ); - if ( $this->db->tableExists( 'archive', __METHOD__ ) ) { - $status->warning( 'config-install-tables-exist' ); + if ( $archiveTableMustNotExist && $this->db->tableExists( 'archive', __METHOD__ ) ) { + $status->warning( "config-$stepName-tables-exist" ); $this->enableLB(); return $status; @@ -184,11 +191,13 @@ abstract class DatabaseInstaller { $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files $this->db->begin( __METHOD__ ); - $error = $this->db->sourceFile( $this->db->getSchemaPath() ); + $error = $this->db->sourceFile( + call_user_func( array( $this->db, $sourceFileMethod ) ) + ); if ( $error !== true ) { $this->db->reportQueryError( $error, 0, '', __METHOD__ ); $this->db->rollback( __METHOD__ ); - $status->fatal( 'config-install-tables-failed', $error ); + $status->fatal( "config-$stepName-tables-failed", $error ); } else { $this->db->commit( __METHOD__ ); } @@ -200,6 +209,24 @@ abstract class DatabaseInstaller { return $status; } + /** + * Create database tables from scratch. + * + * @return Status + */ + public function createTables() { + return $this->stepApplySourceFile( 'getSchemaPath', 'install', true ); + } + + /** + * Insert update keys into table to prevent running unneded updates. + * + * @return Status + */ + public function insertUpdateKeys() { + return $this->stepApplySourceFile( 'getUpdateKeysPath', 'updates', false ); + } + /** * Create the tables for each extension the user enabled * @return Status diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php index b25ea09c24..193d59209f 100644 --- a/includes/installer/DatabaseUpdater.php +++ b/includes/installer/DatabaseUpdater.php @@ -297,9 +297,9 @@ abstract class DatabaseUpdater { * @param string $tableName The table name * @param string $oldIndexName The old index name * @param string $newIndexName The new index name + * @param string $sqlPath The path to the SQL change path * @param bool $skipBothIndexExistWarning Whether to warn if both the old * and the new indexes exist. [facultative; by default, false] - * @param string $sqlPath The path to the SQL change path */ public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName, $sqlPath, $skipBothIndexExistWarning = false @@ -907,7 +907,7 @@ abstract class DatabaseUpdater { if ( $wgLocalisationCacheConf['manualRecache'] ) { $this->rebuildLocalisationCache(); } - MessageBlobStore::clear(); + MessageBlobStore::getInstance()->clear(); $this->output( "done.\n" ); } @@ -984,6 +984,7 @@ abstract class DatabaseUpdater { /** * Updates the timestamps in the transcache table + * @return bool */ protected function doUpdateTranscacheField() { if ( $this->updateRowExists( 'convert transcache field' ) ) { diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index 7d7741620a..987925c747 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -434,7 +434,11 @@ abstract class Installer { public function doEnvironmentChecks() { // Php version has already been checked by entry scripts // Show message here for information purposes - $this->showMessage( 'config-env-php', PHP_VERSION ); + if ( wfIsHHVM() ) { + $this->showMessage( 'config-env-hhvm', HHVM_VERSION ); + } else { + $this->showMessage( 'config-env-php', PHP_VERSION ); + } $good = true; // Must go here because an old version of PCRE can prevent other checks from completing @@ -535,6 +539,7 @@ abstract class Installer { // registration out of the global scope and into a real format. // @see https://bugzilla.wikimedia.org/67440 global $wgAutoloadClasses; + $wgAutoloadClasses = array(); wfSuppressWarnings(); $_lsExists = file_exists( "$IP/LocalSettings.php" ); @@ -735,6 +740,7 @@ abstract class Installer { /** * Environment check for register_globals. * Prevent installation if enabled + * @return bool */ protected function envCheckRegisterGlobals() { if ( wfIniGetBool( 'register_globals' ) ) { @@ -1518,6 +1524,7 @@ abstract class Installer { array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ), array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ), array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ), + array( 'name' => 'updates', 'callback' => array( $installer, 'insertUpdateKeys' ) ), array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ), array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ), ); diff --git a/includes/installer/MssqlInstaller.php b/includes/installer/MssqlInstaller.php index 83681b6b29..46bb86c010 100644 --- a/includes/installer/MssqlInstaller.php +++ b/includes/installer/MssqlInstaller.php @@ -702,7 +702,7 @@ class MssqlInstaller extends DatabaseInstaller { /** * Try to see if a given fulltext catalog exists * We assume we already have the appropriate database selected - * @param string $schemaName Catalog name to check + * @param string $catalogName Catalog name to check * @return bool */ private function catalogExists( $catalogName ) { diff --git a/includes/installer/MssqlUpdater.php b/includes/installer/MssqlUpdater.php index 4d86d116d7..ed11f8b683 100644 --- a/includes/installer/MssqlUpdater.php +++ b/includes/installer/MssqlUpdater.php @@ -52,6 +52,13 @@ class MssqlUpdater extends DatabaseUpdater { array( 'updateConstraints', 'media_type', 'image', 'img_media_type' ), array( 'updateConstraints', 'media_type', 'uploadstash', 'us_media_type' ), // END: Constraint updates + + array( 'modifyField', 'image', 'img_major_mime', + 'patch-img_major_mime-chemical.sql' ), + array( 'modifyField', 'oldimage', 'oi_major_mime', + 'patch-oi_major_mime-chemical.sql' ), + array( 'modifyField', 'filearchive', 'fa_major_mime', + 'patch-fa_major_mime-chemical.sql' ), ); } diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index dcf37b68a3..990b5b031e 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -254,11 +254,18 @@ class MysqlUpdater extends DatabaseUpdater { // 1.24 array( 'addField', 'page_props', 'pp_sortkey', 'patch-pp_sortkey.sql' ), array( 'dropField', 'recentchanges', 'rc_cur_time', 'patch-drop-rc_cur_time.sql' ), - array( 'addIndex', 'watchlist', 'wl_user_notificationtimestamp', 'patch-watchlist-user-notificationtimestamp-index.sql' ), + array( 'addIndex', 'watchlist', 'wl_user_notificationtimestamp', + 'patch-watchlist-user-notificationtimestamp-index.sql' ), array( 'addField', 'page', 'page_lang', 'patch-page_lang.sql' ), array( 'addField', 'pagelinks', 'pl_from_namespace', 'patch-pl_from_namespace.sql' ), array( 'addField', 'templatelinks', 'tl_from_namespace', 'patch-tl_from_namespace.sql' ), array( 'addField', 'imagelinks', 'il_from_namespace', 'patch-il_from_namespace.sql' ), + array( 'modifyField', 'image', 'img_major_mime', + 'patch-img_major_mime-chemical.sql' ), + array( 'modifyField', 'oldimage', 'oi_major_mime', + 'patch-oi_major_mime-chemical.sql' ), + array( 'modifyField', 'filearchive', 'fa_major_mime', + 'patch-fa_major_mime-chemical.sql' ), ); } diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php index 4caf90289c..6dcce23dec 100644 --- a/includes/installer/PostgresInstaller.php +++ b/includes/installer/PostgresInstaller.php @@ -158,7 +158,7 @@ class PostgresInstaller extends DatabaseInstaller { protected function openConnectionWithParams( $user, $password, $dbName, $schema ) { $status = Status::newGood(); try { - $db = Database::factory( 'postgres', array( + $db = DatabaseBase::factory( 'postgres', array( 'host' => $this->getVar( 'wgDBserver' ), 'user' => $user, 'password' => $password, diff --git a/includes/installer/WebInstaller.php b/includes/installer/WebInstaller.php index 68c2ebe36a..f3dba3a72d 100644 --- a/includes/installer/WebInstaller.php +++ b/includes/installer/WebInstaller.php @@ -667,7 +667,7 @@ class WebInstaller extends Installer { * Get HTML for an info box with an icon. * * @param string $text Wikitext, get this with wfMessage()->plain() - * @param string|bool $icon Icon name, file in skins/common/images. Default: false + * @param string|bool $icon Icon name, file in mw-config/images. Default: false * @param string|bool $class Additional class name to add to the wrapper div. Default: false. * * @return string @@ -675,11 +675,11 @@ class WebInstaller extends Installer { public function getInfoBox( $text, $icon = false, $class = false ) { $text = $this->parse( $text, true ); $icon = ( $icon == false ) ? - '../skins/common/images/info-32.png' : - '../skins/common/images/' . $icon; + 'images/info-32.png' : + 'images/' . $icon; $alt = wfMessage( 'config-information' )->text(); - return Html::infoBox( $text, $icon, $alt, $class, false ); + return Html::infoBox( $text, $icon, $alt, $class ); } /** diff --git a/includes/installer/WebInstallerOutput.php b/includes/installer/WebInstallerOutput.php index 174120f5de..3094d5571b 100644 --- a/includes/installer/WebInstallerOutput.php +++ b/includes/installer/WebInstallerOutput.php @@ -124,48 +124,55 @@ class WebInstallerOutput { * @return string */ public function getCSS() { - // Horrible, horrible hack: the installer is currently hardcoded to use the Vector skin, so load - // it here. Include instead of require, as this will work without it, it will just look bad. global $wgStyleDirectory; - include_once "$wgStyleDirectory/Vector/Vector.php"; $moduleNames = array( // See SkinTemplate::setupSkinUserCss 'mediawiki.legacy.shared', // See Vector::setupSkinUserCss 'mediawiki.skinning.interface', - 'skins.vector.styles', - - 'mediawiki.legacy.config', ); - $css = ''; + if ( file_exists( "$wgStyleDirectory/Vector/Vector.php" ) ) { + // Force loading Vector skin if available as a fallback skin + // for whatever ResourceLoader wants to have as the default. + + // Include instead of require, as this will work without it, it will just look bad. + // We need the 'global' statement for $wgResourceModules because the Vector skin adds the + // definitions for its RL modules there that we use implicitly below. + + // @codingStandardsIgnoreStart + global $wgResourceModules; // This is NOT UNUSED! + // @codingStandardsIgnoreEnd + + include_once "$wgStyleDirectory/Vector/Vector.php"; + + $moduleNames[] = 'skins.vector.styles'; + } + + $moduleNames[] = 'mediawiki.legacy.config'; $resourceLoader = new ResourceLoader(); $rlContext = new ResourceLoaderContext( $resourceLoader, new FauxRequest( array( 'debug' => 'true', 'lang' => $this->getLanguageCode(), 'only' => 'styles', - 'skin' => 'vector', ) ) ); + + $styles = array(); foreach ( $moduleNames as $moduleName ) { /** @var ResourceLoaderFileModule $module */ $module = $resourceLoader->getModule( $moduleName ); - // One of the modules will be missing if Vector is unavailable - if ( !$module ) { - continue; - } // Based on: ResourceLoaderFileModule::getStyles (without the DB query) - $styles = ResourceLoader::makeCombinedStyles( $module->readStyleFiles( - $module->getStyleFiles( $rlContext ), - $module->getFlip( $rlContext ) - ) ); - - $css .= implode( "\n", $styles ); + $styles = array_merge( $styles, ResourceLoader::makeCombinedStyles( + $module->readStyleFiles( + $module->getStyleFiles( $rlContext ), + $module->getFlip( $rlContext ) + ) ) ); } - return $css; + return implode( "\n", $styles ); } /** @@ -260,14 +267,14 @@ class WebInstallerOutput { <title><?php $this->outputTitle(); ?> getCssUrl() . "\n"; ?> getJQuery() . "\n"; ?> - + $this->getDir() ) ) . "\n"; ?>
-
-
+
+

outputTitle(); ?>

@@ -309,7 +316,7 @@ class WebInstallerOutput { <?php $this->outputTitle(); ?> getCssUrl() . "\n"; ?> getJQuery(); ?> - + diff --git a/includes/installer/WebInstallerPage.php b/includes/installer/WebInstallerPage.php index 9fdee766e6..2a9c54ce1f 100644 --- a/includes/installer/WebInstallerPage.php +++ b/includes/installer/WebInstallerPage.php @@ -187,7 +187,7 @@ abstract class WebInstallerPage { protected function startLiveBox() { $this->addHTML( '' . + '
' . '' . '
' . '', + '', Xml::textarea( 'name', '' ), 'textarea() with not content' ); @@ -244,7 +244,7 @@ class XmlTest extends MediaWikiTestCase { */ public function testTextareaAttribs() { $this->assertEquals( - '', + '', Xml::textarea( 'name', '', 20, 10 ), 'textarea() with custom attribs' ); diff --git a/tests/phpunit/includes/api/ApiMainTest.php b/tests/phpunit/includes/api/ApiMainTest.php index 8c5f5400e8..780cf9ed1f 100644 --- a/tests/phpunit/includes/api/ApiMainTest.php +++ b/tests/phpunit/includes/api/ApiMainTest.php @@ -55,7 +55,7 @@ class ApiMainTest extends ApiTestCase { * @dataProvider provideAssert * @param User $user * @param string $assert - * @param string|bool $error false if no error expected + * @param string|bool $error False if no error expected */ public function testAssert( $user, $assert, $error ) { try { diff --git a/tests/phpunit/includes/api/ApiModuleManagerTest.php b/tests/phpunit/includes/api/ApiModuleManagerTest.php new file mode 100644 index 0000000000..dab81e162a --- /dev/null +++ b/tests/phpunit/includes/api/ApiModuleManagerTest.php @@ -0,0 +1,318 @@ + array( + 'login', + 'action', + 'ApiLogin', + null, + ), + + 'with factory' => array( + 'login', + 'action', + 'ApiLogin', + array( $this, 'newApiLogin' ), + ), + + 'with closure' => array( + 'logout', + 'action', + 'ApiLogout', + function ( ApiMain $main, $action ) { + return new ApiLogout( $main, $action ); + }, + ), + ); + } + + /** + * @dataProvider addModuleProvider + */ + public function testAddModule( $name, $group, $class, $factory = null ) { + $moduleManager = $this->getModuleManager(); + $moduleManager->addModule( $name, $group, $class, $factory ); + + $this->assertTrue( $moduleManager->isDefined( $name, $group ), 'isDefined' ); + $this->assertNotNull( $moduleManager->getModule( $name, $group, true ), 'getModule' ); + } + + public function addModulesProvider() { + return array( + 'empty' => array( + array(), + 'action', + ), + + 'simple' => array( + array( + 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', + ), + 'action', + ), + + 'with factories' => array( + array( + 'login' => array( + 'class' => 'ApiLogin', + 'factory' => array( $this, 'newApiLogin' ), + ), + 'logout' => array( + 'class' => 'ApiLogout', + 'factory' => function ( ApiMain $main, $action ) { + return new ApiLogout( $main, $action ); + }, + ), + ), + 'action', + ), + ); + } + + /** + * @dataProvider addModulesProvider + */ + public function testAddModules( array $modules, $group ) { + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $modules, $group ); + + foreach ( array_keys( $modules ) as $name ) { + $this->assertTrue( $moduleManager->isDefined( $name, $group ), 'isDefined' ); + $this->assertNotNull( $moduleManager->getModule( $name, $group, true ), 'getModule' ); + } + + $this->assertTrue( true ); // Don't mark the test as risky if $modules is empty + } + + public function getModuleProvider() { + $modules = array( + 'feedrecentchanges' => 'ApiFeedRecentChanges', + 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ), + 'login' => array( + 'class' => 'ApiLogin', + 'factory' => array( $this, 'newApiLogin' ), + ), + 'logout' => array( + 'class' => 'ApiLogout', + 'factory' => function ( ApiMain $main, $action ) { + return new ApiLogout( $main, $action ); + }, + ), + ); + + return array( + 'legacy entry' => array( + $modules, + 'feedrecentchanges', + 'ApiFeedRecentChanges', + ), + + 'just a class' => array( + $modules, + 'feedcontributions', + 'ApiFeedContributions', + ), + + 'with factory' => array( + $modules, + 'login', + 'ApiLogin', + ), + + 'with closure' => array( + $modules, + 'logout', + 'ApiLogout', + ), + ); + } + + /** + * @covers ApiModuleManager::getModule + * @dataProvider getModuleProvider + */ + public function testGetModule( $modules, $name, $expectedClass ) { + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $modules, 'test' ); + + // should return the right module + $module1 = $moduleManager->getModule( $name, null, false ); + $this->assertInstanceOf( $expectedClass, $module1 ); + + // should pass group check (with caching disabled) + $module2 = $moduleManager->getModule( $name, 'test', true ); + $this->assertNotNull( $module2 ); + + // should use cached instance + $module3 = $moduleManager->getModule( $name, null, false ); + $this->assertSame( $module1, $module3 ); + + // should not use cached instance if caching is disabled + $module4 = $moduleManager->getModule( $name, null, true ); + $this->assertNotSame( $module1, $module4 ); + } + + /** + * @covers ApiModuleManager::getModule + */ + public function testGetModule_null() { + $modules = array( + 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', + ); + + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $modules, 'test' ); + + $this->assertNull( $moduleManager->getModule( 'quux' ), 'unknown name' ); + $this->assertNull( $moduleManager->getModule( 'login', 'bla' ), 'wrong group' ); + } + + /** + * @covers ApiModuleManager::getNames + */ + public function testGetNames() { + $fooModules = array( + 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', + ); + + $barModules = array( + 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ), + 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ), + ); + + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $fooModules, 'foo' ); + $moduleManager->addModules( $barModules, 'bar' ); + + $fooNames = $moduleManager->getNames( 'foo' ); + $this->assertArrayEquals( array_keys( $fooModules ), $fooNames ); + + $allNames = $moduleManager->getNames(); + $allModules = array_merge( $fooModules, $barModules ); + $this->assertArrayEquals( array_keys( $allModules ), $allNames ); + } + + /** + * @covers ApiModuleManager::getNamesWithClasses + */ + public function testGetNamesWithClasses() { + $fooModules = array( + 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', + ); + + $barModules = array( + 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ), + 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ), + ); + + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $fooModules, 'foo' ); + $moduleManager->addModules( $barModules, 'bar' ); + + $fooNamesWithClasses = $moduleManager->getNamesWithClasses( 'foo' ); + $this->assertArrayEquals( $fooModules, $fooNamesWithClasses ); + + $allNamesWithClasses = $moduleManager->getNamesWithClasses(); + $allModules = array_merge( $fooModules, array( + 'feedcontributions' => 'ApiFeedContributions', + 'feedrecentchanges' => 'ApiFeedRecentChanges', + ) ); + $this->assertArrayEquals( $allModules, $allNamesWithClasses ); + } + + /** + * @covers ApiModuleManager::getModuleGroup + */ + public function testGetModuleGroup() { + $fooModules = array( + 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', + ); + + $barModules = array( + 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ), + 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ), + ); + + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $fooModules, 'foo' ); + $moduleManager->addModules( $barModules, 'bar' ); + + $this->assertEquals( 'foo', $moduleManager->getModuleGroup( 'login' ) ); + $this->assertEquals( 'bar', $moduleManager->getModuleGroup( 'feedrecentchanges' ) ); + $this->assertNull( $moduleManager->getModuleGroup( 'quux' ) ); + } + + /** + * @covers ApiModuleManager::getGroups + */ + public function testGetGroups() { + $fooModules = array( + 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', + ); + + $barModules = array( + 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ), + 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ), + ); + + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $fooModules, 'foo' ); + $moduleManager->addModules( $barModules, 'bar' ); + + $groups = $moduleManager->getGroups(); + $this->assertArrayEquals( array( 'foo', 'bar' ), $groups ); + } + + /** + * @covers ApiModuleManager::getClassName + */ + public function testGetClassName() { + $fooModules = array( + 'login' => 'ApiLogin', + 'logout' => 'ApiLogout', + ); + + $barModules = array( + 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ), + 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ), + ); + + $moduleManager = $this->getModuleManager(); + $moduleManager->addModules( $fooModules, 'foo' ); + $moduleManager->addModules( $barModules, 'bar' ); + + $this->assertEquals( 'ApiLogin', $moduleManager->getClassName( 'login' ) ); + $this->assertEquals( 'ApiLogout', $moduleManager->getClassName( 'logout' ) ); + $this->assertEquals( 'ApiFeedContributions', $moduleManager->getClassName( 'feedcontributions' ) ); + $this->assertEquals( 'ApiFeedRecentChanges', $moduleManager->getClassName( 'feedrecentchanges' ) ); + $this->assertFalse( $moduleManager->getClassName( 'nonexistentmodule' ) ); + } + + +} diff --git a/tests/phpunit/includes/api/ApiRevisionDeleteTest.php b/tests/phpunit/includes/api/ApiRevisionDeleteTest.php index 21f4322ff2..b03836eb2b 100644 --- a/tests/phpunit/includes/api/ApiRevisionDeleteTest.php +++ b/tests/phpunit/includes/api/ApiRevisionDeleteTest.php @@ -9,8 +9,8 @@ */ class ApiRevisionDeleteTest extends ApiTestCase { - static $page = 'Help:ApiRevDel_test'; - var $revs = array(); + public static $page = 'Help:ApiRevDel_test'; + public $revs = array(); protected function setUp() { // Needs to be before setup since this gets cached diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php index 0976b1a9bb..cd141947b5 100644 --- a/tests/phpunit/includes/api/ApiTestCase.php +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -43,10 +43,10 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { /** * Edits or creates a page/revision - * @param string $pageName page title - * @param string $text content of the page - * @param string $summary optional summary string for the revision - * @param int $defaultNs optional namespace id + * @param string $pageName Page title + * @param string $text Content of the page + * @param string $summary Optional summary string for the revision + * @param int $defaultNs Optional namespace id * @return array Array as returned by WikiPage::doEditContent() */ protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) { @@ -119,10 +119,10 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { * requesting a "real" edit token. * * @param array $params Key-value API params - * @param array|null $session session array + * @param array|null $session Session array * @param User|null $user A User object for the context - * @return mixed Result of the API call - * @throws Exception in case wsToken is not set in the session + * @return array Result of the API call + * @throws Exception In case wsToken is not set in the session */ protected function doApiRequestWithToken( array $params, array $session = null, User $user = null @@ -133,7 +133,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { $session = $wgRequest->getSessionArray(); } - if ( $session['wsToken'] ) { + if ( isset( $session['wsToken'] ) && $session['wsToken'] ) { // add edit token to fake session $session['wsEditToken'] = $session['wsToken']; // add token to request parameters @@ -141,7 +141,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { return $this->doApiRequest( $params, $session, false, $user ); } else { - throw new Exception( "request data not in right format" ); + throw new Exception( "Session token not available" ); } } diff --git a/tests/phpunit/includes/api/ApiTestCaseUpload.php b/tests/phpunit/includes/api/ApiTestCaseUpload.php index 40407dc6f1..7e513394e4 100644 --- a/tests/phpunit/includes/api/ApiTestCaseUpload.php +++ b/tests/phpunit/includes/api/ApiTestCaseUpload.php @@ -30,7 +30,7 @@ abstract class ApiTestCaseUpload extends ApiTestCase { /** * Helper function -- remove files and associated articles by Title * - * @param Title $title title to be removed + * @param Title $title Title to be removed * * @return bool */ @@ -65,7 +65,7 @@ abstract class ApiTestCaseUpload extends ApiTestCase { /** * Helper function -- remove files and associated articles with a particular filename * - * @param string $fileName filename to be removed + * @param string $fileName Filename to be removed * * @return bool */ @@ -77,7 +77,7 @@ abstract class ApiTestCaseUpload extends ApiTestCase { * Helper function -- given a file on the filesystem, find matching * content in the db (and associated articles) and remove them. * - * @param string $filePath path to file on the filesystem + * @param string $filePath Path to file on the filesystem * * @return bool */ @@ -96,10 +96,10 @@ abstract class ApiTestCaseUpload extends ApiTestCase { * Fake an upload by dumping the file into temp space, and adding info to $_FILES. * (This is what PHP would normally do). * - * @param string $fieldName name this would have in the upload form - * @param string $fileName name to title this - * @param string $type mime type - * @param string $filePath path where to find file contents + * @param string $fieldName Name this would have in the upload form + * @param string $fileName Name to title this + * @param string $type MIME type + * @param string $filePath Path where to find file contents * * @throws Exception * @return bool diff --git a/tests/phpunit/includes/api/PrefixUniquenessTest.php b/tests/phpunit/includes/api/PrefixUniquenessTest.php index 38beb87208..13da33c700 100644 --- a/tests/phpunit/includes/api/PrefixUniquenessTest.php +++ b/tests/phpunit/includes/api/PrefixUniquenessTest.php @@ -10,12 +10,15 @@ class PrefixUniquenessTest extends MediaWikiTestCase { public function testPrefixes() { $main = new ApiMain( new FauxRequest() ); $query = new ApiQuery( $main, 'foo', 'bar' ); - $modules = $query->getModuleManager()->getNamesWithClasses(); + $moduleManager = $query->getModuleManager(); + + $modules = $moduleManager->getNames(); $prefixes = array(); - foreach ( $modules as $name => $class ) { - /** @var ApiQueryBase $module */ - $module = new $class( $query, $name ); + foreach ( $modules as $name ) { + $module = $moduleManager->getModule( $name ); + $class = get_class( $module ); + $prefix = $module->getModulePrefix(); if ( isset( $prefixes[$prefix] ) ) { $this->fail( "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" ); diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php index e3eea4cb3c..6374cfac81 100644 --- a/tests/phpunit/includes/api/RandomImageGenerator.php +++ b/tests/phpunit/includes/api/RandomImageGenerator.php @@ -328,7 +328,7 @@ class RandomImageGenerator { * This is used when simulating a rotated image capture with Exif orientation * @param array $spec Returned by getImageSpec * @param array $matrix 2x2 transformation matrix - * @return array transformed Spec + * @return array Transformed Spec */ private static function rotateImageSpec( &$spec, $matrix ) { $tSpec = array(); @@ -365,8 +365,8 @@ class RandomImageGenerator { /** * Given a matrix and a pair of images, return new position * @param array $matrix 2x2 rotation matrix - * @param int $x x-coordinate number - * @param int $y y-coordinate number + * @param int $x The x-coordinate number + * @param int $y The y-coordinate number * @return array Transformed with properties x, y */ private static function matrixMultiply2x2( $matrix, $x, $y ) { @@ -432,7 +432,7 @@ class RandomImageGenerator { * array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) ); * * @param int $number Number of pairs - * @return array two-element arrays + * @return array Two-element arrays */ private function getRandomWordPairs( $number ) { $lines = $this->getRandomLines( $number * 2 ); @@ -451,7 +451,7 @@ class RandomImageGenerator { * * Will throw exception if the file could not be read or if it had fewer lines than requested. * - * @param int $number_desired number of lines desired + * @param int $number_desired Number of lines desired * * @throws Exception * @return array Array of exactly n elements, drawn randomly from lines the file diff --git a/tests/phpunit/includes/api/format/ApiFormatNoneTest.php b/tests/phpunit/includes/api/format/ApiFormatNoneTest.php new file mode 100644 index 0000000000..cabd750b4b --- /dev/null +++ b/tests/phpunit/includes/api/format/ApiFormatNoneTest.php @@ -0,0 +1,16 @@ +apiRequest( 'none', array( 'action' => 'query', 'meta' => 'siteinfo' ) ); + + $this->assertEquals( '', $data ); // No output! + } +} diff --git a/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php index bd15f044a3..bce626853a 100644 --- a/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php +++ b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php @@ -37,8 +37,8 @@ abstract class ApiQueryContinueTestBase extends ApiQueryTestBase { * @param array $params Api parameters * @param int $expectedCount Max number of iterations * @param string $id Unit test id - * @param bool $useContinue true to use smart continue - * @return mixed Merged results data array + * @param bool $continue True to use smart continue + * @return array Merged results data array */ protected function checkC( $expected, $params, $expectedCount, $id, $continue = true ) { $result = $this->query( $params, $expectedCount, $id, $continue ); @@ -50,8 +50,8 @@ abstract class ApiQueryContinueTestBase extends ApiQueryTestBase { * @param array $params Api parameters * @param int $expectedCount Max number of iterations * @param string $id Unit test id - * @param bool $useContinue true to use smart continue - * @return mixed Merged results data array + * @param bool $useContinue True to use smart continue + * @return array Merged results data array * @throws Exception */ protected function query( $params, $expectedCount, $id, $useContinue = true ) { diff --git a/tests/phpunit/includes/api/query/ApiQueryTest.php b/tests/phpunit/includes/api/query/ApiQueryTest.php index 7b686a3154..bba22c77f7 100644 --- a/tests/phpunit/includes/api/query/ApiQueryTest.php +++ b/tests/phpunit/includes/api/query/ApiQueryTest.php @@ -96,7 +96,7 @@ class ApiQueryTest extends ApiTestCase { * @param string $titlePart * @param int $namespace * @param string $expected - * @param string $description + * @param string $expectException * @dataProvider provideTestTitlePartToKey */ function testTitlePartToKey( $titlePart, $namespace, $expected, $expectException ) { diff --git a/tests/phpunit/includes/api/query/ApiQueryTestBase.php b/tests/phpunit/includes/api/query/ApiQueryTestBase.php index 1b9c1ce86a..56c15b2387 100644 --- a/tests/phpunit/includes/api/query/ApiQueryTestBase.php +++ b/tests/phpunit/includes/api/query/ApiQueryTestBase.php @@ -34,7 +34,7 @@ STR; /** * Merges all requests parameter + expected values into one - * @param ... list of arrays, each of which contains exactly two + * @param array $v,... List of arrays, each of which contains exactly two * @return array */ protected function merge( /*...*/ ) { @@ -52,6 +52,8 @@ STR; /** * Check that the parameter is a valid two element array, * with the first element being API request and the second - expected result + * @param array $v + * @return array */ private function validateRequestExpectedPair( $v ) { $this->assertType( 'array', $v, self::PARAM_ASSERT ); @@ -66,6 +68,8 @@ STR; /** * Recursively merges the expected values in the $item into the $all + * @param array &$all + * @param array $item */ private function mergeExpected( &$all, $item ) { foreach ( $item as $k => $v ) { diff --git a/tests/phpunit/includes/cache/LocalisationCacheTest.php b/tests/phpunit/includes/cache/LocalisationCacheTest.php new file mode 100644 index 0000000000..fc06a50126 --- /dev/null +++ b/tests/phpunit/includes/cache/LocalisationCacheTest.php @@ -0,0 +1,91 @@ +setMwGlobals( array( + 'wgMessagesDirs' => array( "$IP/tests/phpunit/data/localisationcache" ), + 'wgExtensionMessagesFiles' => array(), + 'wgHooks' => array(), + ) ); + } + + public function testPuralRulesFallback() { + $cache = new LocalisationCache( array( 'store' => 'detect' ) ); + + $this->assertEquals( + $cache->getItem( 'ar', 'pluralRules' ), + $cache->getItem( 'arz', 'pluralRules' ), + 'arz plural rules (undefined) fallback to ar (defined)' + ); + + $this->assertEquals( + $cache->getItem( 'ar', 'compiledPluralRules' ), + $cache->getItem( 'arz', 'compiledPluralRules' ), + 'arz compiled plural rules (undefined) fallback to ar (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'pluralRules' ), + $cache->getItem( 'de', 'pluralRules' ), + 'ksh plural rules (defined) dont fallback to de (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'compiledPluralRules' ), + $cache->getItem( 'de', 'compiledPluralRules' ), + 'ksh compiled plural rules (defined) dont fallback to de (defined)' + ); + } + + public function testRecacheFallbacks() { + $lc = new LocalisationCache( array( 'store' => 'detect' ) ); + $lc->recache( 'uk' ); + $this->assertEquals( + array( + 'present-uk' => 'uk', + 'present-ru' => 'ru', + 'present-en' => 'en', + ), + $lc->getItem( 'uk', 'messages' ), + 'Fallbacks are only used to fill missing data' + ); + } + + public function testRecacheFallbacksWithHooks() { + global $wgHooks; + + // Use hook to provide updates for messages. This is what the + // LocalisationUpdate extension does. See bug 68781. + $wgHooks['LocalisationCacheRecacheFallback'][] = function ( + LocalisationCache $lc, + $code, + array &$cache + ) { + if ( $code === 'ru' ) { + $cache['messages']['present-uk'] = 'ru-override'; + $cache['messages']['present-ru'] = 'ru-override'; + $cache['messages']['present-en'] = 'ru-override'; + } + }; + + $lc = new LocalisationCache( array( 'store' => 'detect' ) ); + $lc->recache( 'uk' ); + $this->assertEquals( + array( + 'present-uk' => 'uk', + 'present-ru' => 'ru-override', + 'present-en' => 'ru-override', + ), + $lc->getItem( 'uk', 'messages' ), + 'Updates provided by hooks follow the normal fallback order.' + ); + } +} diff --git a/tests/phpunit/includes/cache/MessageCacheTest.php b/tests/phpunit/includes/cache/MessageCacheTest.php index 803acf7362..442e9f9f66 100644 --- a/tests/phpunit/includes/cache/MessageCacheTest.php +++ b/tests/phpunit/includes/cache/MessageCacheTest.php @@ -59,7 +59,7 @@ class MessageCacheTest extends MediaWikiLangTestCase { * Helper function for addDBData -- adds a simple page to the database * * @param string $title Title of page to be created - * @param string $lang Language and content of the created page + * @param string $lang Language and content of the created page * @param string|null $content Content of the created page, or null for a generic string * @param bool $createSubPage Set to false if a root page should be created */ diff --git a/tests/phpunit/includes/cache/RedisBloomCacheTest.php b/tests/phpunit/includes/cache/RedisBloomCacheTest.php new file mode 100644 index 0000000000..3d491e9062 --- /dev/null +++ b/tests/phpunit/includes/cache/RedisBloomCacheTest.php @@ -0,0 +1,71 @@ +delete( "unit-testing-" . self::$suffix ); + } else { + $this->markTestSkipped( 'The main bloom cache is not redis.' ); + } + } + + public function testBloomCache() { + $key = "unit-testing-" . self::$suffix; + $fcache = BloomCache::get( 'main' ); + $count = 1500; + + $this->assertTrue( $fcache->delete( $key ), "OK delete of filter '$key'." ); + $this->assertTrue( $fcache->init( $key, $count, .001 ), "OK init of filter '$key'." ); + + $members = array(); + for ( $i = 0; $i < $count; ++$i ) { + $members[] = "$i-value-$i"; + } + $this->assertTrue( $fcache->add( $key, $members ), "Addition of members to '$key' OK." ); + + for ( $i = 0; $i < $count; ++$i ) { + $this->assertTrue( $fcache->isHit( $key, "$i-value-$i" ), "Hit on member '$i-value-$i'." ); + } + + $falsePositives = array(); + for ( $i = $count; $i < 2 * $count; ++$i ) { + if ( $fcache->isHit( $key, "value$i" ) ) { + $falsePositives[] = "value$i"; + } + } + + $eFalsePositives = array( + 'value1763', + 'value2245', + 'value2353', + 'value2791', + 'value2898', + 'value2975' + ); + $this->assertEquals( $eFalsePositives, $falsePositives, "Correct number of false positives found." ); + } + + protected function tearDown() { + parent::tearDown(); + + $fcache = BloomCache::get( 'main' ); + if ( $fcache instanceof BloomCacheRedis ) { + $fcache->delete( "unit-testing-" . self::$suffix ); + } + } +} diff --git a/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/tests/phpunit/includes/changes/EnhancedChangesListTest.php new file mode 100644 index 0000000000..40a11d2d38 --- /dev/null +++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php @@ -0,0 +1,132 @@ + + */ +class EnhancedChangesListTest extends MediaWikiLangTestCase { + + /** + * @var TestRecentChangesHelper + */ + private $testRecentChangesHelper; + + public function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->testRecentChangesHelper = new TestRecentChangesHelper(); + } + + public function testBeginRecentChangesList_styleModules() { + $enhancedChangesList = $this->newEnhancedChangesList(); + $enhancedChangesList->beginRecentChangesList(); + + $styleModules = $enhancedChangesList->getOutput()->getModuleStyles(); + + $this->assertContains( + 'mediawiki.special.changeslist', + $styleModules, + 'has mediawiki.special.changeslist' + ); + + $this->assertContains( + 'mediawiki.special.changeslist.enhanced', + $styleModules, + 'has mediawiki.special.changeslist.enhanced' + ); + } + + public function testBeginRecentChangesList_jsModules() { + $enhancedChangesList = $this->newEnhancedChangesList(); + $enhancedChangesList->beginRecentChangesList(); + + $modules = $enhancedChangesList->getOutput()->getModules(); + + $this->assertContains( 'jquery.makeCollapsible', $modules, 'has jquery.makeCollapsible' ); + $this->assertContains( 'mediawiki.icon', $modules, 'has mediawiki.icon' ); + } + + public function testBeginRecentChangesList_html() { + $enhancedChangesList = $this->newEnhancedChangesList(); + $html = $enhancedChangesList->beginRecentChangesList(); + + $this->assertEquals( '
', $html ); + } + + /** + * @todo more tests + */ + public function testRecentChangesLine() { + $enhancedChangesList = $this->newEnhancedChangesList(); + $enhancedChangesList->beginRecentChangesList(); + + $recentChange = $this->getEditChange( '20131103092153' ); + $html = $enhancedChangesList->recentChangesLine( $recentChange, false ); + + $this->assertInternalType( 'string', $html ); + + $recentChange2 = $this->getEditChange( '20131103092253' ); + $html = $enhancedChangesList->recentChangesLine( $recentChange2, false ); + + $this->assertEquals( '', $html ); + } + + /** + * @todo more tests for actual formatting, this is more of a smoke test + */ + public function testEndRecentChangesList() { + $enhancedChangesList = $this->newEnhancedChangesList(); + $enhancedChangesList->beginRecentChangesList(); + + $recentChange = $this->getEditChange( '20131103092153' ); + $enhancedChangesList->recentChangesLine( $recentChange, false ); + + $recentChange2 = $this->getEditChange( '20131103092253' ); + $enhancedChangesList->recentChangesLine( $recentChange2, false ); + + $html = $enhancedChangesList->endRecentChangesList(); + + preg_match_all( '/td class="mw-enhanced-rc-nested"/', $html, $matches ); + $this->assertCount( 2, $matches[0] ); + } + + /** + * @return EnhancedChangesList + */ + private function newEnhancedChangesList() { + $user = User::newFromId( 0 ); + $context = $this->testRecentChangesHelper->getTestContext( $user ); + + return new EnhancedChangesList( $context ); + } + + /** + * @return RecentChange + */ + private function getEditChange( $timestamp ) { + $user = $this->getTestUser(); + $recentChange = $this->testRecentChangesHelper->makeEditRecentChange( + $user, 'Cat', $timestamp, 5, 191, 190, 0, 0 + ); + + return $recentChange; + } + + /** + * @return User + */ + private function getTestUser() { + $user = User::newFromName( 'TestRecentChangesUser' ); + + if ( !$user->getId() ) { + $user->addToDatabase(); + } + + return $user; + } + +} diff --git a/tests/phpunit/includes/changes/OldChangesListTest.php b/tests/phpunit/includes/changes/OldChangesListTest.php index 9783ae3ca4..3a36b9f302 100644 --- a/tests/phpunit/includes/changes/OldChangesListTest.php +++ b/tests/phpunit/includes/changes/OldChangesListTest.php @@ -3,6 +3,10 @@ /** * @covers OldChangesList * + * @todo add tests to cover article link, timestamp, character difference, + * log entry, user tool links, direction marks, tags, rollback, + * watching users, and date header. + * * @group Database * * @licence GNU GPL v2+ @@ -68,7 +72,7 @@ class OldChangesListTest extends MediaWikiLangTestCase { public function testRecentChangesLine_LogTitle() { $oldChangesList = $this->getOldChangesList(); - $recentChange = $this->getLogChange( 'delete' ); + $recentChange = $this->getLogChange( 'delete', 'delete' ); $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 ); @@ -100,21 +104,51 @@ class OldChangesListTest extends MediaWikiLangTestCase { ); } + public function testRecentChangesLine_Flags() { + $oldChangesList = $this->getOldChangesList(); + $recentChange = $this->getNewBotEditChange(); + + $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 ); + + $this->assertRegExp( + "/N<\/abbr>/", + $line, + 'new page flag' + ); + + $this->assertRegExp( + "/b<\/abbr>/", + $line, + 'bot flag' + ); + } + + public function testRecentChangesLine_Tags() { + $recentChange = $this->getEditChange(); + $recentChange->mAttribs['ts_tags'] = 'vandalism,newbie'; + + $oldChangesList = $this->getOldChangesList(); + $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 ); + + $this->assertRegExp( '/
  • /', $line ); + $this->assertRegExp( '/
  • /', $line ); + } + private function getNewBotEditChange() { $user = $this->getTestUser(); $recentChange = $this->testRecentChangesHelper->makeNewBotEditRecentChange( - $user, 'Abc', '20131103212153', 0, 0 + $user, 'Abc', '20131103212153', 5, 191, 190, 0, 0 ); return $recentChange; } - private function getLogChange( $logType ) { + private function getLogChange( $logType, $logAction ) { $user = $this->getTestUser(); $recentChange = $this->testRecentChangesHelper->makeLogRecentChange( - $logType, $user, 'Abc', '20131103212153', 0, 0 + $logType, $logAction, $user, 'Abc', '20131103212153', 0, 0 ); return $recentChange; diff --git a/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php b/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php index c3b8ce68fd..ee1a4d0e0f 100644 --- a/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php +++ b/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php @@ -123,6 +123,7 @@ class RCCacheEntryFactoryTest extends MediaWikiLangTestCase { $this->getContext(), $this->getMessages(), $this->testRecentChangesHelper->makeLogRecentChange( + 'delete', 'delete', $this->getTestUser(), 'Abc', diff --git a/tests/phpunit/includes/changes/TestRecentChangesHelper.php b/tests/phpunit/includes/changes/TestRecentChangesHelper.php index bb6ebecf48..0da07750e0 100644 --- a/tests/phpunit/includes/changes/TestRecentChangesHelper.php +++ b/tests/phpunit/includes/changes/TestRecentChangesHelper.php @@ -26,7 +26,7 @@ class TestRecentChangesHelper { return $this->makeRecentChange( $attribs, $counter, $watchingUsers ); } - public function makeLogRecentChange( $logType, User $user, $titleText, $timestamp, $counter, + public function makeLogRecentChange( $logType, $logAction, User $user, $titleText, $timestamp, $counter, $watchingUsers ) { $attribs = array_merge( @@ -42,7 +42,7 @@ class TestRecentChangesHelper { 'rc_type' => 3, 'rc_logid' => 25, 'rc_log_type' => $logType, - 'rc_log_action' => $logType, + 'rc_log_action' => $logAction, 'rc_source' => 'mw.log' ) ); diff --git a/tests/phpunit/includes/config/GlobalVarConfigTest.php b/tests/phpunit/includes/config/GlobalVarConfigTest.php index 7080b02335..a99908155a 100644 --- a/tests/phpunit/includes/config/GlobalVarConfigTest.php +++ b/tests/phpunit/includes/config/GlobalVarConfigTest.php @@ -2,6 +2,42 @@ class GlobalVarConfigTest extends MediaWikiTestCase { + /** + * @covers GlobalVarConfig::newInstance + */ + public function testNewInstance() { + $config = GlobalVarConfig::newInstance(); + $this->assertInstanceOf( 'GlobalVarConfig', $config ); + $this->maybeStashGlobal( 'wgBaz' ); + $GLOBALS['wgBaz'] = 'somevalue'; + // Check prefix is set to 'wg' + $this->assertEquals( 'somevalue', $config->get( 'Baz' ) ); + } + + /** + * @covers GlobalVarConfig::__construct + * @dataProvider provideConstructor + */ + public function testConstructor( $prefix ) { + $var = $prefix . 'GlobalVarConfigTest'; + $rand = wfRandomString(); + $this->maybeStashGlobal( $var ); + $GLOBALS[$var] = $rand; + $config = new GlobalVarConfig( $prefix ); + $this->assertInstanceOf( 'GlobalVarConfig', $config ); + $this->assertEquals( $rand, $config->get( 'GlobalVarConfigTest' ) ); + } + + public static function provideConstructor() { + return array( + array( 'wg' ), + array( 'ef' ), + array( 'smw' ), + array( 'blahblahblahblah' ), + array( '' ), + ); + } + public function provideGet() { $set = array( 'wgSomething' => 'default1', @@ -19,6 +55,7 @@ class GlobalVarConfigTest extends MediaWikiTestCase { array( 'Foo', 'wg', 'default2' ), array( 'Variable', 'ef', 'default3' ), array( 'BAR', '', 'default4' ), + array( 'ThisGlobalWasNotSetAbove', 'wg', false ) ); } @@ -28,9 +65,43 @@ class GlobalVarConfigTest extends MediaWikiTestCase { * @param string $expected * @dataProvider provideGet * @covers GlobalVarConfig::get + * @covers GlobalVarConfig::getWithPrefix */ public function testGet( $name, $prefix, $expected ) { $config = new GlobalVarConfig( $prefix ); + if ( $expected === false ) { + $this->setExpectedException( 'ConfigException', 'GlobalVarConfig::getWithPrefix: undefined variable:' ); + } $this->assertEquals( $config->get( $name ), $expected ); } + + public static function provideSet() { + return array( + array( 'Foo', 'wg', 'wgFoo' ), + array( 'SomethingRandom', 'wg', 'wgSomethingRandom' ), + array( 'FromAnExtension', 'eg', 'egFromAnExtension' ), + array( 'NoPrefixHere', '', 'NoPrefixHere' ), + ); + } + + private function maybeStashGlobal( $var ) { + if ( array_key_exists( $var, $GLOBALS ) ) { + // Will be reset after this test is over + $this->stashMwGlobals( $var ); + } + } + + /** + * @dataProvider provideSet + * @covers GlobalVarConfig::set + * @covers GlobalVarConfig::setWithPrefix + */ + public function testSet( $name, $prefix, $var ) { + $this->maybeStashGlobal( $var ); + $config = new GlobalVarConfig( $prefix ); + $random = wfRandomString(); + $config->set( $name, $random ); + $this->assertArrayHasKey( $var, $GLOBALS ); + $this->assertEquals( $random, $GLOBALS[$var] ); + } } diff --git a/tests/phpunit/includes/content/ContentHandlerTest.php b/tests/phpunit/includes/content/ContentHandlerTest.php index b564a29b7a..f7449734c4 100644 --- a/tests/phpunit/includes/content/ContentHandlerTest.php +++ b/tests/phpunit/includes/content/ContentHandlerTest.php @@ -448,7 +448,7 @@ class DummyContentForTesting extends AbstractContent { * Returns native represenation of the data. Interpretation depends on the data model used, * as given by getDataModel(). * - * @return mixed the native representation of the content. Could be a string, a nested array + * @return mixed The native representation of the content. Could be a string, a nested array * structure, an object, a binary blob... anything, really. */ public function getNativeData() { @@ -476,7 +476,7 @@ class DummyContentForTesting extends AbstractContent { * return $this. That is, $copy === $original may be true, but only for imutable content * objects. * - * @return Content. A copy of this object. + * @return Content A copy of this object */ public function copy() { return $this; @@ -486,7 +486,7 @@ class DummyContentForTesting extends AbstractContent { * Returns true if this content is countable as a "real" wiki page, provided * that it's also in a countable location (e.g. a current revision in the main namespace). * - * @param bool $hasLinks if it is known whether this content contains links, + * @param bool $hasLinks If it is known whether this content contains links, * provide this information here, to avoid redundant parsing to find out. * @return bool */ @@ -498,7 +498,7 @@ class DummyContentForTesting extends AbstractContent { * @param Title $title * @param int $revId Unused. * @param null|ParserOptions $options - * @param bool $generateHtml whether to generate Html (default: true). If false, the result + * @param bool $generateHtml Whether to generate Html (default: true). If false, the result * of calling getText() on the ParserOutput object returned by this method is undefined. * * @return ParserOutput @@ -514,7 +514,7 @@ class DummyContentForTesting extends AbstractContent { * * @param Title $title Context title for parsing * @param int|null $revId Revision ID (for {{REVISIONID}}) - * @param ParserOptions|null $options Parser options + * @param ParserOptions $options Parser options * @param bool $generateHtml Whether or not to generate HTML * @param ParserOutput &$output The output object to fill (reference). */ diff --git a/tests/phpunit/includes/content/JSONContentTest.php b/tests/phpunit/includes/content/JSONContentTest.php new file mode 100644 index 0000000000..acfdc0e59f --- /dev/null +++ b/tests/phpunit/includes/content/JSONContentTest.php @@ -0,0 +1,115 @@ +assertEquals( $isValid, $obj->isValid() ); + $this->assertEquals( $expected, $obj->getJsonData() ); + } + + public function provideValidConstruction() { + return array( + array( 'foo', CONTENT_MODEL_JSON, false, null ), + array( FormatJson::encode( array() ), CONTENT_MODEL_JSON, true, array() ), + array( FormatJson::encode( array( 'foo' ) ), CONTENT_MODEL_JSON, true, array( 'foo' ) ), + ); + } + + /** + * @dataProvider provideDataToEncode + */ + public function testBeautifyUsesFormatJson( $data ) { + $obj = new JSONContent( FormatJson::encode( $data ) ); + $this->assertEquals( FormatJson::encode( $data, true ), $obj->beautifyJSON() ); + } + + public function provideDataToEncode() { + return array( + array( array() ), + array( array( 'foo' ) ), + array( array( 'foo', 'bar' ) ), + array( array( 'baz' => 'foo', 'bar' ) ), + array( array( 'baz' => 1000, 'bar' ) ), + ); + } + + /** + * @dataProvider provideDataToEncode + */ + public function testPreSaveTransform( $data ) { + $obj = new JSONContent( FormatJson::encode( $data ) ); + $newObj = $obj->preSaveTransform( $this->getMockTitle(), $this->getMockUser(), $this->getMockParserOptions() ); + $this->assertTrue( $newObj->equals( new JSONContent( FormatJson::encode( $data, true ) ) ) ); + } + + private function getMockTitle() { + return $this->getMockBuilder( 'Title' ) + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockUser() { + return $this->getMockBuilder( 'User' ) + ->disableOriginalConstructor() + ->getMock(); + } + private function getMockParserOptions() { + return $this->getMockBuilder( 'ParserOptions' ) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @dataProvider provideDataAndParserText + */ + public function testFillParserOutput( $data, $expected ) { + $obj = new JSONContent( FormatJson::encode( $data ) ); + $parserOutput = $obj->getParserOutput( $this->getMockTitle(), null, null, true ); + $this->assertInstanceOf( 'ParserOutput', $parserOutput ); +// var_dump( $parserOutput->getText(), "\n" ); + $this->assertEquals( $expected, $parserOutput->getText() ); + } + + public function provideDataAndParserText() { + return array( + array( + array(), + '
    ' + ), + array( + array( 'foo' ), + '
    0"foo"
    ' + ), + array( + array( 'foo', 'bar' ), + '' . + "\n" . + '
    0"foo"
    1"bar"
    ' + ), + array( + array( 'baz' => 'foo', 'bar' ), + '' . + "\n" . + '
    baz"foo"
    0"bar"
    ' + ), + array( + array( 'baz' => 1000, 'bar' ), + '' . + "\n" . + '
    baz1000
    0"bar"
    ' + ), + array( + array( ''), + '
    0"<script>alert("evil!")</script>"
    ', + ), + ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php index 1db6faec53..98b4ca046c 100644 --- a/tests/phpunit/includes/db/DatabaseSqliteTest.php +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -162,6 +162,9 @@ class DatabaseSqliteTest extends MediaWikiTestCase { $this->assertEquals( "DROP INDEX foo -- dropping index", $this->replaceVars( "DROP INDEX /*i*/foo ON /*_*/bar -- dropping index" ) ); + $this->assertEquals( "INSERT OR IGNORE INTO foo VALUES ('bar')", + $this->replaceVars( "INSERT OR IGNORE INTO foo VALUES ('bar')" ) + ); } /** diff --git a/tests/phpunit/includes/db/DatabaseTestHelper.php b/tests/phpunit/includes/db/DatabaseTestHelper.php index 39c311f119..0c0b39029f 100644 --- a/tests/phpunit/includes/db/DatabaseTestHelper.php +++ b/tests/phpunit/includes/db/DatabaseTestHelper.php @@ -136,7 +136,7 @@ class DatabaseTestHelper extends DatabaseBase { return false; } - function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) { + function indexInfo( $table, $index, $fname = 'DatabaseBase::indexInfo' ) { return false; } diff --git a/tests/phpunit/includes/db/ORMRowTest.php b/tests/phpunit/includes/db/ORMRowTest.php index 4ff61655ab..447bf21997 100644 --- a/tests/phpunit/includes/db/ORMRowTest.php +++ b/tests/phpunit/includes/db/ORMRowTest.php @@ -115,7 +115,7 @@ abstract class ORMRowTest extends \MediaWikiTestCase { /** * @since 1.20 - * @return array of IORMRow + * @return array Array of IORMRow */ public function instanceProvider() { $instances = array(); diff --git a/tests/phpunit/includes/debug/MWDebugTest.php b/tests/phpunit/includes/debug/MWDebugTest.php index 17c6224cd9..6e41de7543 100644 --- a/tests/phpunit/includes/debug/MWDebugTest.php +++ b/tests/phpunit/includes/debug/MWDebugTest.php @@ -103,7 +103,7 @@ class MWDebugTest extends MediaWikiTestCase { $this->assertInstanceOf( 'ApiResult', $result ); $data = $result->getData(); - $expectedKeys = array( 'mwVersion', 'phpVersion', 'gitRevision', 'gitBranch', + $expectedKeys = array( 'mwVersion', 'phpEngine', 'phpVersion', 'gitRevision', 'gitBranch', 'gitViewUrl', 'time', 'log', 'debugLog', 'queries', 'request', 'memory', 'memoryPeak', 'includes', 'profile', '_element' ); diff --git a/tests/phpunit/includes/deferred/DeferredUpdatesTest.php b/tests/phpunit/includes/deferred/DeferredUpdatesTest.php new file mode 100644 index 0000000000..5348c85435 --- /dev/null +++ b/tests/phpunit/includes/deferred/DeferredUpdatesTest.php @@ -0,0 +1,38 @@ + 'deferred update 1', + '2' => 'deferred update 2', + '3' => 'deferred update 3', + '2-1' => 'deferred update 1 within deferred update 2', + ); + DeferredUpdates::addCallableUpdate( + function () use ( $updates ) { + echo $updates['1']; + } + ); + DeferredUpdates::addCallableUpdate( + function () use ( $updates ) { + echo $updates['2']; + DeferredUpdates::addCallableUpdate( + function () use ( $updates ) { + echo $updates['2-1']; + } + ); + } + ); + DeferredUpdates::addCallableUpdate( + function () use ( $updates ) { + echo $updates[3]; + } + ); + + $this->expectOutputString( implode( '', $updates ) ); + + DeferredUpdates::doUpdates(); + } + +} diff --git a/tests/phpunit/includes/diff/DifferenceEngineTest.php b/tests/phpunit/includes/diff/DifferenceEngineTest.php index e1a69e3bc5..5474b963e8 100644 --- a/tests/phpunit/includes/diff/DifferenceEngineTest.php +++ b/tests/phpunit/includes/diff/DifferenceEngineTest.php @@ -39,7 +39,7 @@ class DifferenceEngineTest extends MediaWikiTestCase { } /** - * @return int[] revision ids + * @return int[] Revision ids */ protected function doEdits() { $title = $this->getTitle(); diff --git a/tests/phpunit/includes/filerepo/file/FileTest.php b/tests/phpunit/includes/filerepo/file/FileTest.php index ffa9876625..8e8b8a9eb3 100644 --- a/tests/phpunit/includes/filerepo/file/FileTest.php +++ b/tests/phpunit/includes/filerepo/file/FileTest.php @@ -3,8 +3,8 @@ class FileTest extends MediaWikiMediaTestCase { /** - * @param $filename String - * @param $expected boolean + * @param string $filename + * @param bool $expected * @dataProvider providerCanAnimate */ function testCanAnimateThumbIfAppropriate( $filename, $expected ) { diff --git a/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php b/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php new file mode 100644 index 0000000000..2c7f50c9c4 --- /dev/null +++ b/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php @@ -0,0 +1,68 @@ + 'BGR', + 'Burkina Faso' => 'BFA', + 'Burundi' => 'BDI', + ); + + /** + * Verify that attempting to instantiate an HTMLAutoCompleteSelectField + * without providing any autocomplete options causes an exception to be + * thrown. + * + * @expectedException MWException + * @expectedExceptionMessage called without any autocompletions + */ + function testMissingAutocompletions() { + new HTMLAutoCompleteSelectField( array( 'fieldname' => 'Test' ) ); + } + + /** + * Verify that the autocomplete options are correctly encoded as + * the 'data-autocomplete' attribute of the field. + * + * @covers HTMLAutoCompleteSelectField::getAttributes + */ + function testGetAttributes() { + $field = new HTMLAutoCompleteSelectField( array( + 'fieldname' => 'Test', + 'autocomplete' => $this->options, + ) ); + + $attributes = $field->getAttributes( array() ); + $this->assertEquals( array_keys( $this->options ), + FormatJson::decode( $attributes['data-autocomplete'] ), + "The 'data-autocomplete' attribute encodes autocomplete option keys as a JSON array." + ); + } + + /** + * Test that the optional select dropdown is included or excluded based on + * the presence or absence of the 'options' parameter. + */ + function testOptionalSelectElement() { + $params = array( + 'fieldname' => 'Test', + 'autocomplete' => $this->options, + 'options' => $this->options, + ); + + $field = new HTMLAutoCompleteSelectField( $params ); + $html = $field->getInputHTML( false ); + $this->assertRegExp( '/select/', $html, + "When the 'options' parameter is set, the HTML includes a " ); + } +} diff --git a/tests/phpunit/includes/libs/CSSJanusTest.php b/tests/phpunit/includes/libs/CSSJanusTest.php index 407f11a64c..e4283b0bf5 100644 --- a/tests/phpunit/includes/libs/CSSJanusTest.php +++ b/tests/phpunit/includes/libs/CSSJanusTest.php @@ -15,15 +15,27 @@ class CSSJanusTest extends MediaWikiTestCase { if ( $cssB ) { $transformedA = CSSJanus::transform( $cssA ); - $this->assertEquals( $transformedA, $cssB, 'Test A-B transformation' ); + $this->assertEquals( + $transformedA, + str_replace( '/* @noflip */ ', '', $cssB ), + 'Test A-B transformation' + ); $transformedB = CSSJanus::transform( $cssB ); - $this->assertEquals( $transformedB, $cssA, 'Test B-A transformation' ); + $this->assertEquals( + $transformedB, + str_replace( '/* @noflip */ ', '', $cssA ), + 'Test B-A transformation' + ); } else { // If no B version is provided, it means - // the output should equal the input. + // the output should equal the input (modulo @noflip annotations). $transformedA = CSSJanus::transform( $cssA ); - $this->assertEquals( $transformedA, $cssA, 'Nothing was flipped' ); + $this->assertEquals( + $transformedA, + str_replace( '/* @noflip */ ', '', $cssA ), + 'Nothing was flipped' + ); } } diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php index 80e03cca19..7c977d5a45 100644 --- a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -23,7 +23,7 @@ class JpegMetadataExtractorTest extends MediaWikiTestCase { * We also use this test to test padding bytes don't * screw stuff up * - * @param string $file filename + * @param string $file Filename * * @dataProvider provideUtf8Comment */ diff --git a/tests/phpunit/includes/media/MediaWikiMediaTestCase.php b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php index 1b8ecf27d6..8f28158dcd 100644 --- a/tests/phpunit/includes/media/MediaWikiMediaTestCase.php +++ b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php @@ -33,7 +33,7 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { } /** - * @return Array Argument for FSRepo constructor + * @return array Argument for FSRepo constructor */ protected function getRepoOptions() { return array( @@ -47,7 +47,7 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { * The result of this method will set the file path to use, * as well as the protected member $filePath * - * @return String path where files are + * @return string Path where files are */ protected function getFilePath() { return __DIR__ . '/../../data/media/'; @@ -59,7 +59,7 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { * * Override this method if your test case creates thumbnails * - * @return boolean + * @return bool */ protected function createsThumbnails() { return false; @@ -69,8 +69,8 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { * Utility function: Get a new file object for a file on disk but not actually in db. * * File must be in the path returned by getFilePath() - * @param $name String File name - * @param $type String MIME type [optional] + * @param string $name File name + * @param string $type MIME type [optional] * @return UnregisteredLocalFile */ protected function dataFile( $name, $type = null ) { diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php index 9a4826ccc9..092be89eff 100644 --- a/tests/phpunit/includes/media/PNGTest.php +++ b/tests/phpunit/includes/media/PNGTest.php @@ -22,7 +22,7 @@ class PNGHandlerTest extends MediaWikiMediaTestCase { } /** - * @param string $filename basename of the file to check + * @param string $filename Basename of the file to check * @param bool $expected Expected result. * @dataProvider provideIsAnimated * @covers PNGHandler::isAnimatedImage diff --git a/tests/phpunit/includes/media/XCFTest.php b/tests/phpunit/includes/media/XCFTest.php index 7fc3275b2a..5b2de151f2 100644 --- a/tests/phpunit/includes/media/XCFTest.php +++ b/tests/phpunit/includes/media/XCFTest.php @@ -16,8 +16,8 @@ class XCFHandlerTest extends MediaWikiMediaTestCase { /** * @param string $filename - * @param int $expectedWidth width - * @param int $expectedHeigh height + * @param int $expectedWidth Width + * @param int $expectedHeight Height * @dataProvider provideGetImageSize * @covers XCFHandler::getImageSize */ diff --git a/tests/phpunit/includes/parser/MagicVariableTest.php b/tests/phpunit/includes/parser/MagicVariableTest.php index d36697eb15..17226113e5 100644 --- a/tests/phpunit/includes/parser/MagicVariableTest.php +++ b/tests/phpunit/includes/parser/MagicVariableTest.php @@ -171,7 +171,7 @@ class MagicVariableTest extends MediaWikiTestCase { * Main assertion helper for magic variables padding * @param string $magic Magic variable name * @param mixed $value Month or day - * @param string $format sprintf format for $value + * @param string $format Sprintf format for $value */ private function assertMagicPadding( $magic, $value, $format ) { # Initialize parser timestamp as year 2010 at 12h34 56s. diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php index a506c86269..a450972334 100644 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -45,7 +45,7 @@ class MediaWikiParserTest { * MediaWikiParserTest::suite( MediaWikiParserTest::WITH_ALL ); * @endcode * - * @param int $flags bitwise flag to filter out the $wgParserTestFiles that + * @param int $flags Bitwise flag to filter out the $wgParserTestFiles that * will be included. Default: MediaWikiParserTest::CORE_ONLY * * @return PHPUnit_Framework_TestSuite diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php index 0499f882e7..0df52f5e38 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -36,6 +36,10 @@ class NewParserTest extends MediaWikiTestCase { * @var DjVuSupport */ private $djVuSupport; + /** + * @var TidySupport + */ + private $tidySupport; protected $file = false; @@ -75,6 +79,7 @@ class NewParserTest extends MediaWikiTestCase { $tmpGlobals['wgExtensionAssetsPath'] = '/extensions'; $tmpGlobals['wgStylePath'] = '/skins'; $tmpGlobals['wgEnableUploads'] = true; + $tmpGlobals['wgUploadNavigationUrl'] = false; $tmpGlobals['wgThumbnailScriptPath'] = false; $tmpGlobals['wgLocalFileRepo'] = array( 'class' => 'LocalRepo', @@ -95,8 +100,6 @@ class NewParserTest extends MediaWikiTestCase { $tmpGlobals['wgUseImageResize'] = true; $tmpGlobals['wgAllowExternalImages'] = true; $tmpGlobals['wgRawHtml'] = false; - $tmpGlobals['wgUseTidy'] = false; - $tmpGlobals['wgAlwaysUseTidy'] = false; $tmpGlobals['wgWellFormedXml'] = true; $tmpGlobals['wgAllowMicrodataAttributes'] = true; $tmpGlobals['wgExperimentalHtmlIds'] = false; @@ -153,8 +156,19 @@ class NewParserTest extends MediaWikiTestCase { # see https://gerrit.wikimedia.org/r/111390 $tmpGlobals['wgExtraInterlanguageLinkPrefixes'] = array( 'mul' ); - //DjVu support + // DjVu support $this->djVuSupport = new DjVuSupport(); + // Tidy support + $this->tidySupport = new TidySupport(); + // We always set 'wgUseTidy' to false when parsing, but certain + // test-running modes still use tidy if available, so ensure + // that the tidy-related options are all set to their defaults. + $tmpGlobals['wgUseTidy'] = false; + $tmpGlobals['wgAlwaysUseTidy'] = false; + $tmpGlobals['wgDebugTidy'] = false; + $tmpGlobals['wgTidyConf'] = $IP . '/includes/tidy.conf'; + $tmpGlobals['wgTidyOpts'] = ''; + $tmpGlobals['wgTidyInternal'] = $this->tidySupport->isInternal(); $this->setMwGlobals( $tmpGlobals ); @@ -339,6 +353,9 @@ class NewParserTest extends MediaWikiTestCase { /** * Set up the global variables for a consistent environment for each test. * Ideally this should replace the global configuration entirely. + * @param array $opts + * @param string $config + * @return RequestContext */ protected function setupGlobals( $opts = array(), $config = '' ) { global $wgFileBackends; @@ -735,6 +752,14 @@ class NewParserTest extends MediaWikiTestCase { $output = $parser->parse( $input, $title, $options, true, true, 1337 ); $output->setTOCEnabled( !isset( $opts['notoc'] ) ); $out = $output->getText(); + if ( isset( $opts['tidy'] ) ) { + if ( !$this->tidySupport->isEnabled() ) { + $this->markTestSkipped( "SKIPPED: tidy extension is not installed.\n" ); + } else { + $out = MWTidy::tidy( $out ); + $out = preg_replace( '/\s+$/', '', $out ); + } + } if ( isset( $opts['showtitle'] ) ) { if ( $output->getTitleText() ) { @@ -745,21 +770,19 @@ class NewParserTest extends MediaWikiTestCase { } if ( isset( $opts['ill'] ) ) { - $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) ); + $out = implode( ' ', $output->getLanguageLinks() ); } elseif ( isset( $opts['cat'] ) ) { $outputPage = $context->getOutput(); $outputPage->addCategoryLinks( $output->getCategories() ); $cats = $outputPage->getCategoryLinks(); if ( isset( $cats['normal'] ) ) { - $out = $this->tidy( implode( ' ', $cats['normal'] ) ); + $out = implode( ' ', $cats['normal'] ); } else { $out = ''; } } $parser->mPreprocessor = null; - - $result = $this->tidy( $result ); } $this->teardownGlobals(); @@ -852,6 +875,7 @@ class NewParserTest extends MediaWikiTestCase { /** * Get an input dictionary from a set of parser test files * @param array $filenames + * @return string */ function getFuzzInput( $filenames ) { $dict = ''; @@ -870,6 +894,7 @@ class NewParserTest extends MediaWikiTestCase { /** * Get a memory usage breakdown + * @return array */ function getMemoryBreakdown() { $memStats = array(); @@ -906,6 +931,7 @@ class NewParserTest extends MediaWikiTestCase { /** * Get a Parser object * @param Preprocessor $preprocessor + * @return Parser */ function getParser( $preprocessor = null ) { global $wgParserConf; @@ -963,26 +989,10 @@ class NewParserTest extends MediaWikiTestCase { //Various "cleanup" functions - /** - * Run the "tidy" command on text if the $wgUseTidy - * global is true - * - * @param string $text The text to tidy - * @return string - */ - protected function tidy( $text ) { - global $wgUseTidy; - - if ( $wgUseTidy ) { - $text = MWTidy::tidy( $text ); - } - - return $text; - } - /** * Remove last character if it is a newline * @param string $s + * @return string */ public function removeEndingNewline( $s ) { if ( substr( $s, -1 ) === "\n" ) { @@ -1067,6 +1077,7 @@ class NewParserTest extends MediaWikiTestCase { * @param string $key Name of option val to retrieve * @param array $opts Options array to look in * @param mixed $default Default value returned if not found + * @return mixed */ protected static function getOptionValue( $key, $opts, $default ) { $key = strtolower( $key ); diff --git a/tests/phpunit/includes/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php index 12aac6916c..c869258d0e 100644 --- a/tests/phpunit/includes/parser/TidyTest.php +++ b/tests/phpunit/includes/parser/TidyTest.php @@ -26,6 +26,24 @@ class TidyTest extends MediaWikiTestCase { } public function provideTestWrapping() { + $testMathML = <<<'MathML' + + + a + + + x + 2 + + + + b + + x + + + c + + +MathML; return array( array( 'foo', @@ -40,6 +58,7 @@ class TidyTest extends MediaWikiTestCase { array( 'foo', 'foo', ' should survive tidy' ), array( "\nfoo", 'foo', ' should survive tidy' ), array( "\nfoo", 'foo', ' should survive tidy' ), + array( $testMathML, $testMathML, ' should survive tidy' ), ); } } diff --git a/tests/phpunit/includes/password/BcryptPasswordTest.php b/tests/phpunit/includes/password/BcryptPasswordTest.php index b4d5f99691..4d5c78ac58 100644 --- a/tests/phpunit/includes/password/BcryptPasswordTest.php +++ b/tests/phpunit/includes/password/BcryptPasswordTest.php @@ -3,7 +3,7 @@ /** * @group large */ -class BcryptPasswordTestCase extends MediaWikiPasswordTestCase { +class BcryptPasswordTestCase extends PasswordTestCase { protected function getTypeConfigs() { return array( 'bcrypt' => array( 'class' => 'BcryptPassword', diff --git a/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php index c5522533a6..03a742bb81 100644 --- a/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php +++ b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php @@ -1,6 +1,6 @@ array( diff --git a/tests/phpunit/includes/password/PasswordTestCase.php b/tests/phpunit/includes/password/PasswordTestCase.php new file mode 100644 index 0000000000..7820d535ff --- /dev/null +++ b/tests/phpunit/includes/password/PasswordTestCase.php @@ -0,0 +1,88 @@ +passwordFactory = new PasswordFactory(); + foreach ( $this->getTypeConfigs() as $type => $config ) { + $this->passwordFactory->register( $type, $config ); + } + } + + /** + * Return an array of configs to be used for this class's password type. + * + * @return array[] + */ + abstract protected function getTypeConfigs(); + + /** + * An array of tests in the form of (bool, string, string), where the first + * element is whether the second parameter (a password hash) and the third + * parameter (a password) should match. + * + * @return array + */ + abstract public function providePasswordTests(); + + /** + * @dataProvider providePasswordTests + */ + public function testHashing( $shouldMatch, $hash, $password ) { + $hash = $this->passwordFactory->newFromCiphertext( $hash ); + $password = $this->passwordFactory->newFromPlaintext( $password, $hash ); + $this->assertSame( $shouldMatch, $hash->equals( $password ) ); + } + + /** + * @dataProvider providePasswordTests + */ + public function testStringSerialization( $shouldMatch, $hash, $password ) { + $hashObj = $this->passwordFactory->newFromCiphertext( $hash ); + $serialized = $hashObj->toString(); + $unserialized = $this->passwordFactory->newFromCiphertext( $serialized ); + $this->assertTrue( $hashObj->equals( $unserialized ) ); + } + + /** + * @dataProvider providePasswordTests + * @covers InvalidPassword::equals + * @covers InvalidPassword::toString + */ + public function testInvalidUnequalNormal( $shouldMatch, $hash, $password ) { + $invalid = $this->passwordFactory->newFromCiphertext( null ); + $normal = $this->passwordFactory->newFromCiphertext( $hash ); + + $this->assertFalse( $invalid->equals( $normal ) ); + $this->assertFalse( $normal->equals( $invalid ) ); + } +} diff --git a/tests/phpunit/includes/password/Pbkdf2PasswordTest.php b/tests/phpunit/includes/password/Pbkdf2PasswordTest.php index c1b65d3c26..ae471207a5 100644 --- a/tests/phpunit/includes/password/Pbkdf2PasswordTest.php +++ b/tests/phpunit/includes/password/Pbkdf2PasswordTest.php @@ -3,7 +3,7 @@ /** * @group large */ -class Pbkdf2PasswordTest extends MediaWikiPasswordTestCase { +class Pbkdf2PasswordTest extends PasswordTestCase { protected function getTypeConfigs() { return array( 'pbkdf2' => array( 'class' => 'Pbkdf2Password', diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderLESSTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderLESSTest.php index 75e54d3582..a3d73e54f3 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderLESSTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderLESSTest.php @@ -22,7 +22,8 @@ class ResourceLoaderLESSTest extends MediaWikiTestCase { $expect = file_get_contents( $cssFile ); $content = file_get_contents( $lessFile ); - $result = ResourceLoader::getLessCompiler()->compile( $content, $lessFile ); + $result = ResourceLoader::getLessCompiler( RequestContext::getMain()->getConfig() ) + ->compile( $content, $lessFile ); $this->assertEquals( $expect, $result ); } } diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php index e98f0e86c3..b0edaaf7b7 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php @@ -5,9 +5,14 @@ class ResourceLoaderModuleTest extends ResourceLoaderTestCase { protected function setUp() { parent::setUp(); - $this->setMwGlobals( array( - 'wgValidSkinNames' => array( 'vector' => 'Vector' ), - ) ); + // The return value of the closure shouldn't matter since this test should + // never call it + SkinFactory::getDefaultInstance()->register( + 'fakeskin', + 'FakeSkin', + function () { + } + ); } /** @@ -29,7 +34,7 @@ class ResourceLoaderModuleTest extends ResourceLoaderTestCase { ), 'skinStyles' => array( 'default' => 'quux-fallback.less', - 'vector' => array( + 'fakeskin' => array( 'baz-vector.css', 'quux-vector.less', ), diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php index df08972a44..a189387328 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php @@ -9,10 +9,7 @@ class ResourceLoaderStartupModuleTest extends ResourceLoaderTestCase { 'modules' => array(), 'out' => ' mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - } + "local": "/w/load.php" } );mw.loader.register( [] );' ) ), array( array( @@ -22,10 +19,7 @@ mw.loader.addSource( { ), 'out' => ' mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - } + "local": "/w/load.php" } );mw.loader.register( [ [ "test.blank", @@ -42,10 +36,7 @@ mw.loader.addSource( { ), 'out' => ' mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - } + "local": "/w/load.php" } );mw.loader.register( [ [ "test.blank", @@ -73,10 +64,7 @@ mw.loader.addSource( { ), 'out' => ' mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - } + "local": "/w/load.php" } );mw.loader.register( [ [ "test.blank", @@ -97,14 +85,8 @@ mw.loader.addSource( { ), 'out' => ' mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - }, - "example": { - "loadScript": "http://example.org/w/load.php", - "apiScript": "http://example.org/w/api.php" - } + "local": "/w/load.php", + "example": "http://example.org/w/load.php" } );mw.loader.register( [ [ "test.blank", @@ -140,10 +122,7 @@ mw.loader.addSource( { ), 'out' => ' mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - } + "local": "/w/load.php" } );mw.loader.register( [ [ "test.x.core", @@ -238,14 +217,8 @@ mw.loader.addSource( { ), 'out' => ' mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - }, - "example": { - "loadScript": "http://example.org/w/load.php", - "apiScript": "http://example.org/w/api.php" - } + "local": "/w/load.php", + "example": "http://example.org/w/load.php" } );mw.loader.register( [ [ "test.blank", @@ -332,9 +305,10 @@ mw.loader.addSource( { $rl->register( $case['modules'] ); + $module = new ResourceLoaderStartUpModule(); $this->assertEquals( ltrim( $case['out'], "\n" ), - ResourceLoaderStartUpModule::getModuleRegistrations( $context ), + $module->getModuleRegistrations( $context ), $case['msg'] ); } @@ -366,14 +340,15 @@ mw.loader.addSource( { $context = self::getResourceLoaderContext(); $rl = $context->getResourceLoader(); $rl->register( $modules ); + $module = new ResourceLoaderStartUpModule(); $this->assertEquals( -'mw.loader.addSource({"local":{"loadScript":"/w/load.php","apiScript":"/w/api.php"}});' +'mw.loader.addSource({"local":"/w/load.php"});' . 'mw.loader.register([' . '["test.blank","1388534400"],' . '["test.min","1388534400",["test.blank"],null,"local",' . '"return!!(window.JSON\u0026\u0026JSON.parse\u0026\u0026JSON.stringify);"' . ']]);', - ResourceLoaderStartUpModule::getModuleRegistrations( $context ), + $module->getModuleRegistrations( $context ), 'Minified output' ); } @@ -385,12 +360,10 @@ mw.loader.addSource( { $context = self::getResourceLoaderContext(); $rl = $context->getResourceLoader(); $rl->register( $modules ); + $module = new ResourceLoaderStartUpModule(); $this->assertEquals( 'mw.loader.addSource( { - "local": { - "loadScript": "/w/load.php", - "apiScript": "/w/api.php" - } + "local": "/w/load.php" } );mw.loader.register( [ [ "test.blank", @@ -407,7 +380,7 @@ mw.loader.addSource( { "return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);" ] ] );', - ResourceLoaderStartUpModule::getModuleRegistrations( $context ), + $module->getModuleRegistrations( $context ), 'Unminified output' ); } diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php index bd6b3f2329..d2e118c543 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php @@ -90,6 +90,36 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] ); } + /** + * What happens when you mix @embed and @noflip? + * This really is an integration test, but oh well. + */ + public function testMixedCssAnnotations( ) { + $basePath = __DIR__ . '/../../data/css'; + $testModule = new ResourceLoaderFileModule( array( + 'localBasePath' => $basePath, + 'styles' => array( 'test.css' ), + ) ); + $expectedModule = new ResourceLoaderFileModule( array( + 'localBasePath' => $basePath, + 'styles' => array( 'expected.css' ), + ) ); + + $contextLtr = self::getResourceLoaderContext( 'en' ); + $contextRtl = self::getResourceLoaderContext( 'he' ); + + $this->assertEquals( + $expectedModule->getStyles( $contextLtr ), + $testModule->getStyles( $contextLtr ), + "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode" + ); + $this->assertEquals( + $expectedModule->getStyles( $contextLtr ), + $testModule->getStyles( $contextRtl ), + "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode" + ); + } + /** * @dataProvider providePackedModules * @covers ResourceLoader::makePackedModulesString @@ -131,6 +161,43 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { ); } + public static function provideAddSource() { + return array( + array( 'examplewiki', '//example.org/w/load.php', 'examplewiki' ), + array( 'example2wiki', array( 'loadScript' => '//example.com/w/load.php' ), 'example2wiki' ), + array( + array( 'foowiki' => '//foo.org/w/load.php', 'bazwiki' => '//baz.org/w/load.php' ), + null, + array( 'foowiki', 'bazwiki' ) + ), + array( + array( 'foowiki' => '//foo.org/w/load.php' ), + null, + false, + ), + ); + } + + /** + * @dataProvider provideAddSource + * @covers ResourceLoader::addSource + */ + public function testAddSource( $name, $info, $expected ) { + $rl = new ResourceLoader; + if ( $expected === false ) { + $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' ); + $rl->addSource( $name, $info ); + } + $rl->addSource( $name, $info ); + if ( is_array( $expected ) ) { + foreach ( $expected as $source ) { + $this->assertArrayHasKey( $source, $rl->getSources() ); + } + } else { + $this->assertArrayHasKey( $expected, $rl->getSources() ); + } + } + public static function fakeSources() { return array( 'examplewiki' => array( diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php new file mode 100644 index 0000000000..50f88c825f --- /dev/null +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php @@ -0,0 +1,67 @@ +getMockBuilder( 'ResourceLoaderWikiModuleTestModule' ) + ->setMethods( array( 'getTitleInfo', 'getGroup' ) ) + ->getMock(); + $module->expects( $this->any() ) + ->method( 'getTitleInfo' ) + ->will( $this->returnValue( $titleInfo ) ); + $module->expects( $this->any() ) + ->method( 'getGroup' ) + ->will( $this->returnValue( $group ) ); + $context = $this->getMockBuilder( 'ResourceLoaderContext' ) + ->disableOriginalConstructor() + ->getMock(); + $this->assertEquals( $expected, $module->isKnownEmpty( $context ) ); + } + + public function provideIsKnownEmpty() { + return array( + // No valid pages + array( array(), 'test1', true ), + // 'site' module with a non-empty page + array( + array( + 'MediaWiki:Common.js' => array( + 'timestamp' => 123456789, + 'length' => 1234 + ) + ), 'site', false, + ), + // 'site' module with an empty page + array( + array( + 'MediaWiki:Monobook.js' => array( + 'timestamp' => 987654321, + 'length' => 0, + ), + ), 'site', false, + ), + // 'user' module with a non-empty page + array( + array( + 'User:FooBar/common.js' => array( + 'timestamp' => 246813579, + 'length' => 25, + ), + ), 'user', false, + ), + // 'user' module with an empty page + array( + array( + 'User:FooBar/monobook.js' => array( + 'timestamp' => 1357924680, + 'length' => 0, + ), + ), 'user', true, + ), + ); + } +} diff --git a/tests/phpunit/includes/skins/SkinFactoryTest.php b/tests/phpunit/includes/skins/SkinFactoryTest.php new file mode 100644 index 0000000000..d3663c84ad --- /dev/null +++ b/tests/phpunit/includes/skins/SkinFactoryTest.php @@ -0,0 +1,70 @@ +register( 'fallback', 'Fallback', function () { + return new SkinFallback(); + } ); + $this->assertTrue( true ); // No exception thrown + $this->setExpectedException( 'InvalidArgumentException' ); + $factory->register( 'invalid', 'Invalid', 'Invalid callback' ); + } + + /** + * @covers SkinFactory::makeSkin + */ + public function testMakeSkinWithNoBuilders() { + $factory = new SkinFactory(); + $this->setExpectedException( 'SkinException' ); + $factory->makeSkin( 'nobuilderregistered' ); + } + + /** + * @covers SkinFactory::makeSkin + */ + public function testMakeSkinWithInvalidCallback() { + $factory = new SkinFactory(); + $factory->register( 'unittest', 'Unittest', function () { + return true; // Not a Skin object + } ); + $this->setExpectedException( 'UnexpectedValueException' ); + $factory->makeSkin( 'unittest' ); + } + + /** + * @covers SkinFactory::makeSkin + */ + public function testMakeSkinWithValidCallback() { + $factory = new SkinFactory(); + $factory->register( 'testfallback', 'TestFallback', function () { + return new SkinFallback(); + } ); + + $skin = $factory->makeSkin( 'testfallback' ); + $this->assertInstanceOf( 'Skin', $skin ); + $this->assertInstanceOf( 'SkinFallback', $skin ); + } + + /** + * @covers SkinFactory::getSkinNames + */ + public function testGetSkinNames() { + $factory = new SkinFactory(); + // A fake callback we can use that will never be called + $callback = function () { + // NOP + }; + $factory->register( 'skin1', 'Skin1', $callback ); + $factory->register( 'skin2', 'Skin2', $callback ); + $names = $factory->getSkinNames(); + $this->assertArrayHasKey( 'skin1', $names ); + $this->assertArrayHasKey( 'skin2', $names ); + $this->assertEquals( 'Skin1', $names['skin1'] ); + $this->assertEquals( 'Skin2', $names['skin2'] ); + } +} diff --git a/tests/phpunit/includes/skins/SkinTemplateTest.php b/tests/phpunit/includes/skins/SkinTemplateTest.php new file mode 100644 index 0000000000..baa995d4e6 --- /dev/null +++ b/tests/phpunit/includes/skins/SkinTemplateTest.php @@ -0,0 +1,43 @@ + + */ + +class SkinTemplateTest extends MediaWikiTestCase { + /** + * @dataProvider makeListItemProvider + */ + public function testMakeListItem( $expected, $key, $item, $options, $message ) { + $template = $this->getMockForAbstractClass( 'BaseTemplate' ); + + $this->assertEquals( + $expected, + $template->makeListItem( $key, $item, $options ), + $message + ); + } + + public function makeListItemProvider() { + return array( + array( + '
  • text
  • ', + '', + array( + 'class' => 'class', + 'itemtitle' => 'itemtitle', + 'href' => 'url', + 'title' => 'title', + 'text' => 'text' + ), + array(), + 'Test makteListItem with normal values' + ) + ); + } +} diff --git a/tests/phpunit/includes/specials/ImageListPagerTest.php b/tests/phpunit/includes/specials/ImageListPagerTest.php new file mode 100644 index 0000000000..8a0ac970b7 --- /dev/null +++ b/tests/phpunit/includes/specials/ImageListPagerTest.php @@ -0,0 +1,21 @@ +formatValue( 'invalid_field', 'invalid_value' ); + } +} diff --git a/tests/phpunit/includes/specials/SpecialListFilesTest.php b/tests/phpunit/includes/specials/SpecialListFilesTest.php deleted file mode 100644 index c042064ef7..0000000000 --- a/tests/phpunit/includes/specials/SpecialListFilesTest.php +++ /dev/null @@ -1,21 +0,0 @@ -formatValue( 'invalid_field', 'invalid_value' ); - } -} diff --git a/tests/phpunit/includes/specials/SpecialMIMESearchTest.php b/tests/phpunit/includes/specials/SpecialMIMESearchTest.php index e7bb35c126..bd952811ca 100644 --- a/tests/phpunit/includes/specials/SpecialMIMESearchTest.php +++ b/tests/phpunit/includes/specials/SpecialMIMESearchTest.php @@ -16,9 +16,9 @@ class SpecialMIMESearchTest extends MediaWikiTestCase { /** * @dataProvider providerMimeFiltering - * @param $par String subpage for special page - * @param $major String Major mime type we expect to look for - * @param $minor String Minor mime type we expect to look for + * @param string $par Subpage for special page + * @param string $major Major MIME type we expect to look for + * @param string $minor Minor MIME type we expect to look for */ function testMimeFiltering( $par, $major, $minor ) { $this->page->run( $par ); diff --git a/tests/phpunit/includes/specials/SpecialMyLanguageTest.php b/tests/phpunit/includes/specials/SpecialMyLanguageTest.php index c09d68ccd0..fd090431d5 100644 --- a/tests/phpunit/includes/specials/SpecialMyLanguageTest.php +++ b/tests/phpunit/includes/specials/SpecialMyLanguageTest.php @@ -26,10 +26,10 @@ class SpecialMyLanguageTest extends MediaWikiTestCase { /** * @covers SpecialMyLanguage::findTitle * @dataProvider provideFindTitle - * @param $expected - * @param $subpage - * @param $langCode - * @param $userLang + * @param string $expected + * @param string $subpage + * @param string $langCode + * @param string $userLang */ public function testFindTitle( $expected, $subpage, $langCode, $userLang ) { $this->setMwGlobals( 'wgLanguageCode', $langCode ); diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php index 1f1d75071f..83489c65b7 100644 --- a/tests/phpunit/includes/specials/SpecialSearchTest.php +++ b/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -8,7 +8,6 @@ */ class SpecialSearchTest extends MediaWikiTestCase { - private $search; /** * @covers SpecialSearch::load @@ -18,7 +17,8 @@ class SpecialSearchTest extends MediaWikiTestCase { * @param array $userOptions User options to test with. For example: * array('searchNs5' => 1 );. Null to use default options. * @param string $expectedProfile An expected search profile name - * @param array $expectedNs Expected namespaces + * @param array $expectedNS Expected namespaces + * @param string $message */ public function testProfileAndNamespaceLoading( $requested, $userOptions, $expectedProfile, $expectedNS, $message = 'Profile name and namespaces mismatches!' @@ -96,6 +96,7 @@ class SpecialSearchTest extends MediaWikiTestCase { /** * Helper to create a new User object with given options * User remains anonymous though + * @param array|null $opt */ function newUserWithSearchNS( $opt = null ) { $u = User::newFromId( 0 ); diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php index 7125247e8f..ec56b63e5c 100644 --- a/tests/phpunit/includes/upload/UploadFromUrlTest.php +++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -110,7 +110,7 @@ class UploadFromUrlTest extends ApiTestCase { $this->user->addGroup( 'sysop' ); $data = $this->doApiRequest( array( 'action' => 'upload', - 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png', 'asyncdownload' => 1, 'filename' => 'UploadFromUrlTest.png', 'token' => $token, @@ -182,7 +182,7 @@ class UploadFromUrlTest extends ApiTestCase { $data = $this->doApiRequest( array( 'action' => 'upload', 'filename' => 'UploadFromUrlTest.png', - 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png', 'ignorewarnings' => true, 'token' => $token, ), $data ); @@ -213,7 +213,7 @@ class UploadFromUrlTest extends ApiTestCase { $this->doApiRequest( array( 'action' => 'upload', 'filename' => 'UploadFromUrlTest.png', - 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png', 'asyncdownload' => 1, 'token' => $token, 'leavemessage' => 1, @@ -234,7 +234,7 @@ class UploadFromUrlTest extends ApiTestCase { $this->doApiRequest( array( 'action' => 'upload', 'filename' => 'UploadFromUrlTest.png', - 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png', 'asyncdownload' => 1, 'token' => $token, 'leavemessage' => 1, @@ -270,13 +270,16 @@ class UploadFromUrlTest extends ApiTestCase { * Helper function to perform an async upload, execute the job and fetch * the status * + * @param string $token + * @param bool $ignoreWarnings + * @param bool $leaveMessage * @return array The result of action=upload&statuskey=key */ private function doAsyncUpload( $token, $ignoreWarnings = false, $leaveMessage = false ) { $params = array( 'action' => 'upload', 'filename' => 'UploadFromUrlTest.png', - 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png', 'asyncdownload' => 1, 'token' => $token, ); diff --git a/tests/phpunit/includes/utils/StringUtilsTest.php b/tests/phpunit/includes/utils/StringUtilsTest.php index 89759e5c70..0fdb8e1594 100644 --- a/tests/phpunit/includes/utils/StringUtilsTest.php +++ b/tests/phpunit/includes/utils/StringUtilsTest.php @@ -35,7 +35,9 @@ class StringUtilsTest extends MediaWikiTestCase { } /** - * Print high range characters as an hexadecimal + * Print high range characters as a hexadecimal + * @param string $string + * @return string */ function escaped( $string ) { $escaped = ''; diff --git a/tests/phpunit/languages/LanguageArqTest.php b/tests/phpunit/languages/LanguageArqTest.php new file mode 100644 index 0000000000..3fa56d7819 --- /dev/null +++ b/tests/phpunit/languages/LanguageArqTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->formatNum( $value ) ); + } + + public static function provideNumber() { + return array( + array( '1.234.567', '1234567'), + array( '-12,89', -12.89 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php index ec514413c2..cff2e8fd68 100644 --- a/tests/phpunit/languages/LanguageTest.php +++ b/tests/phpunit/languages/LanguageTest.php @@ -276,7 +276,7 @@ class LanguageTest extends LanguageClassesTestCase { } /** - * @return array format is ($len, $ellipsis, $input, $expected) + * @return array Format is ($len, $ellipsis, $input, $expected) */ public static function provideHTMLTruncateData() { return array( diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php index dad30b7988..8b6aef53bf 100644 --- a/tests/phpunit/maintenance/DumpTestCase.php +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -30,8 +30,8 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { * * @param Page $page Page to add the revision to * @param string $text Revisions text - * @param string $text Revisions summare - * + * @param string $summary Revisions summare + * @return array * @throws MWException */ protected function addRevision( Page $page, $text, $summary ) { @@ -181,7 +181,7 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { * Asserts that the xml reader is at the final closing tag of an xml file and * closes the reader. * - * @param string $tag (optional) the name of the final tag + * @param string $name (optional) the name of the final tag * (e.g.: "mediawiki" for ) */ protected function assertDumpEnd( $name = "mediawiki" ) { @@ -304,9 +304,9 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { * @param string $text_sha1 The base36 SHA-1 of the revision's text * @param string|bool $text (optional) The revision's string, or false to check for a * revision stub + * @param int|bool $parentid (optional) id of the parent revision * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT) * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT) - * @param int|bool $parentid (optional) id of the parent revision */ protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false, diff --git a/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php index a13f7bf9a4..e2fc82474e 100644 --- a/tests/phpunit/maintenance/MaintenanceTest.php +++ b/tests/phpunit/maintenance/MaintenanceTest.php @@ -810,4 +810,21 @@ class MaintenanceTest extends MediaWikiTestCase { $m2->simulateShutdown(); $this->assertOutputPrePostShutdown( "foobar\n\n", false ); } + + /** + * @covers Maintenance::getConfig + */ + public function testGetConfig() { + $this->assertInstanceOf( 'Config', $this->m->getConfig() ); + $this->assertSame( ConfigFactory::getDefaultInstance()->makeConfig( 'main' ), $this->m->getConfig() ); + } + + /** + * @covers Maintenance::setConfig + */ + public function testSetConfig() { + $conf = $this->getMock( 'Config' ); + $this->m->setConfig( $conf ); + $this->assertSame( $conf, $this->m->getConfig() ); + } } diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php index 744cf616aa..5e0fe89d54 100644 --- a/tests/phpunit/maintenance/backupPrefetchTest.php +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -11,7 +11,7 @@ require_once __DIR__ . "/../../../maintenance/backupPrefetch.inc"; class BaseDumpTest extends MediaWikiTestCase { /** - * @var BaseDump the BaseDump instance used within a test. + * @var BaseDump The BaseDump instance used within a test. * * If set, this BaseDump gets automatically closed in tearDown. */ diff --git a/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php index 016b7e0cf1..a37a97c78f 100644 --- a/tests/phpunit/maintenance/backupTextPassTest.php +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -434,7 +434,7 @@ class TextPassDumperTest extends DumpTestCase { * revision id increase further and further, while the text * id of the first iteration is reused. The pages and revision * of iteration > 1 have no corresponding representation in the database. - * @return string absolute filename of the stub + * @return string Absolute filename of the stub */ private function setUpStub( $fname = null, $iterations = 1 ) { if ( $fname === null ) { diff --git a/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php index 43f2096676..4e38418a18 100644 --- a/tests/phpunit/maintenance/fetchTextTest.php +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -91,7 +91,7 @@ class FetchTextTest extends MediaWikiTestCase { private $exceptionFromAddDBData; /** - * @var FetchText the (mocked) FetchText that is to test + * @var FetchText The (mocked) FetchText that is to test */ private $fetchText; @@ -100,8 +100,8 @@ class FetchTextTest extends MediaWikiTestCase { * * @param WikiPage $page The page to add the revision to * @param string $text The revisions text - * @param string $text The revisions summare - * + * @param string $summary The revisions summare + * @return int * @throws MWException */ private function addRevision( $page, $text, $summary ) { diff --git a/tests/phpunit/mocks/media/MockImageHandler.php b/tests/phpunit/mocks/media/MockImageHandler.php index b2f7facf10..e0a72fd611 100644 --- a/tests/phpunit/mocks/media/MockImageHandler.php +++ b/tests/phpunit/mocks/media/MockImageHandler.php @@ -35,6 +35,13 @@ class MockImageHandler { * a thumbnail at all. That is merely returning a ThumbnailImage that * will be consumed by the unit test. There is no need to create a real * thumbnail on the filesystem. + * @param ImageHandler $that + * @param File $image + * @param string $dstPath + * @param string $dstUrl + * @param array $params + * @param int $flags + * @return ThumbnailImage */ static function doFakeTransform( $that, $image, $dstPath, $dstUrl, $params, $flags = 0 ) { # Example of what we receive: diff --git a/tests/phpunit/structure/ResourcesTest.php b/tests/phpunit/structure/ResourcesTest.php index 647386d867..2396ea2923 100644 --- a/tests/phpunit/structure/ResourcesTest.php +++ b/tests/phpunit/structure/ResourcesTest.php @@ -45,6 +45,7 @@ class ResourcesTest extends MediaWikiTestCase { $data = self::getAllModules(); $illegalDeps = array( 'jquery', 'mediawiki' ); + /** @var ResourceLoaderModule $module */ foreach ( $data['modules'] as $moduleName => $module ) { foreach ( $illegalDeps as $illegalDep ) { $this->assertNotContains( @@ -63,6 +64,7 @@ class ResourcesTest extends MediaWikiTestCase { $data = self::getAllModules(); $validDeps = array_keys( $data['modules'] ); + /** @var ResourceLoaderModule $module */ foreach ( $data['modules'] as $moduleName => $module ) { foreach ( $module->getDependencies() as $dep ) { $this->assertContains( @@ -85,6 +87,7 @@ class ResourcesTest extends MediaWikiTestCase { $data = self::getAllModules(); $validDeps = array_keys( $data['modules'] ); + /** @var ResourceLoaderModule $module */ foreach ( $data['modules'] as $moduleName => $module ) { $moduleTargets = $module->getTargets(); foreach ( $module->getDependencies() as $dep ) { @@ -103,6 +106,7 @@ class ResourcesTest extends MediaWikiTestCase { /** * Get all registered modules from ResouceLoader. + * @return array */ protected static function getAllModules() { global $wgEnableJavaScriptTest; diff --git a/tests/phpunit/structure/StructureTest.php b/tests/phpunit/structure/StructureTest.php index f5cd8927c6..14461be691 100644 --- a/tests/phpunit/structure/StructureTest.php +++ b/tests/phpunit/structure/StructureTest.php @@ -58,6 +58,8 @@ class StructureTest extends MediaWikiTestCase { /** * Filter to remove testUnitTestFileNamesEndWithTest false positives. + * @param string $filename + * @return bool */ public function filterSuites( $filename ) { return strpos( $filename, __DIR__ . '/../suites/' ) !== 0; diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php index bd82d21921..d4a7bd362a 100644 --- a/tests/phpunit/suites/UploadFromUrlTestSuite.php +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -95,6 +95,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { /** * Remove the dummy uploads directory + * @param string $dir */ private function teardownUploadDir( $dir ) { if ( $this->keepUploads ) { diff --git a/tests/qunit/data/generateJqueryMsgData.php b/tests/qunit/data/generateJqueryMsgData.php index 461ab87d0b..61ebbf8fc6 100644 --- a/tests/qunit/data/generateJqueryMsgData.php +++ b/tests/qunit/data/generateJqueryMsgData.php @@ -65,7 +65,7 @@ require __DIR__ . '/../../../maintenance/Maintenance.php'; class GenerateJqueryMsgData extends Maintenance { - static $keyToTestArgs = array( + public static $keyToTestArgs = array( 'undelete_short' => array( array( 0 ), array( 1 ), diff --git a/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js index 842817f582..e8c5121429 100644 --- a/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js @@ -41,18 +41,13 @@ // Add two characters using scary black magic spanText = $span.text(); d = findDivergenceIndex( origText, spanText ); - spanTextNew = spanText.substr( 0, d ) + origText[d] + origText[d] + '...'; + spanTextNew = spanText.slice( 0, d ) + origText[d] + origText[d] + '...'; assert.gt( spanTextNew.length, spanText.length, 'Verify that the new span-length is indeed greater' ); // Put this text in the span and verify it doesn't fit $span.text( spanTextNew ); - // In IE6 width works like min-width, allow IE6's width to be "equal to" - if ( $.client.test( { 'msie': 6 }, $.client.profile(), true ) ) { - assert.gtOrEq( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more) - IE6: Maybe equal to as well due to width behaving like min-width in IE6' ); - } else { - assert.gt( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more)' ); - } + assert.gt( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more)' ); } ); }( jQuery ) ); diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js index 9216ac9173..92dad9ffcf 100644 --- a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -1160,13 +1160,34 @@ '' ); $table.tablesorter(); - assert.equal( 0, - $table.find( '#A2' ).prop( 'headerIndex' ), - 'A2 should be a sort header' + assert.equal( $table.find( '#A2' ).prop( 'headerIndex' ), + undefined, + 'A2 should not be a sort header' ); - assert.equal( 1, // should be 2 - $table.find( '#C1' ).prop( 'headerIndex' ), - 'C1 should be a sort header, but will sort the wrong column' + assert.equal( $table.find( '#C1' ).prop( 'headerIndex' ), + 2, + 'C1 should be a sort header' + ); + } ); + + // bug 53527 + QUnit.test( 'td cells in thead should not be taken into account for longest row calculation', 2, function ( assert ) { + var $table = $( + '' + + '' + + '' + + '' + + '' + + '
    A1B1C1
    A2B2C2
    ' + ); + $table.tablesorter(); + assert.equal( $table.find( '#C2' ).prop( 'headerIndex' ), + 2, + 'C2 should be a sort header' + ); + assert.equal( $table.find( '#C1' ).prop( 'headerIndex' ), + undefined, + 'C1 should not be a sort header' ); } ); @@ -1197,7 +1218,9 @@ '' ); $table.tablesorter(); - assert.equal( 2, $table.find( 'tr:eq(1) th:eq(1)').prop('headerIndex'), 'Incorrect index of sort header' ); + assert.equal( $table.find( 'tr:eq(1) th:eq(1)').prop('headerIndex'), + 2, + 'Incorrect index of sort header' ); } ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js index bc4b253f97..906fd27f49 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js @@ -40,7 +40,8 @@ 'gender-msg-currentuser': '{{GENDER:|blue|pink|green}}', 'plural-msg': 'Found $1 {{PLURAL:$1|item|items}}', - + // See https://bugzilla.wikimedia.org/69993 + 'plural-msg-explicit-forms-nested': 'Found {{PLURAL:$1|$1 results|0=no results in {{SITENAME}}|1=$1 result}}', // Assume the grammar form grammar_case_foo is not valid in any language 'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}', @@ -134,10 +135,13 @@ ); } ); - QUnit.test( 'Plural', 3, function ( assert ) { + QUnit.test( 'Plural', 6, function ( assert ) { assert.equal( formatParse( 'plural-msg', 0 ), 'Found 0 items', 'Plural test for english with zero as count' ); assert.equal( formatParse( 'plural-msg', 1 ), 'Found 1 item', 'Singular test for english' ); assert.equal( formatParse( 'plural-msg', 2 ), 'Found 2 items', 'Plural test for english' ); + assert.equal( formatParse( 'plural-msg-explicit-forms-nested', 6 ), 'Found 6 results', 'Plural message with explicit plural forms' ); + assert.equal( formatParse( 'plural-msg-explicit-forms-nested', 0 ), 'Found no results in ' + mw.config.get( 'wgSiteName' ), 'Plural message with explicit plural forms, with nested {{SITENAME}}' ); + assert.equal( formatParse( 'plural-msg-explicit-forms-nested', 1 ), 'Found 1 result', 'Plural message with explicit plural forms with placeholder nested' ); } ); QUnit.test( 'Gender', 15, function ( assert ) { @@ -320,7 +324,7 @@ } ); // Tests that {{-transformation vs. general parsing are done as requested - QUnit.test( 'Curly brace transformation', 14, function ( assert ) { + QUnit.test( 'Curly brace transformation', 16, function ( assert ) { var oldUserLang = mw.config.get( 'wgUserLanguage' ); assertBothModes( assert, ['gender-msg', 'Bob', 'male'], 'Bob: blue', 'gender is resolved' ); @@ -369,6 +373,16 @@ 'Foo bar', 'External link message processed when format is \'parse\'' ); + assert.htmlEqual( + formatParse( 'external-link-replace', $( '' ) ), + 'Foo bar', + 'External link message processed as jQuery object when format is \'parse\'' + ); + assert.htmlEqual( + formatParse( 'external-link-replace', function () {} ), + 'Foo bar', + 'External link message processed as function when format is \'parse\'' + ); mw.config.set( 'wgUserLanguage', oldUserLang ); } ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js index 3bfabe4c23..16f90df8cf 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js @@ -17,6 +17,19 @@ assert.equal( mw.language.getData( 'en', 'invalidkey' ), undefined, 'Getter setter test for mw.language with invalid key' ); } ); + QUnit.test( 'mw.language.commafy test', 9, function ( assert ) { + // Number grouping patterns are as per http://cldr.unicode.org/translation/number-patterns + assert.equal( mw.language.commafy( 1234.567, '###0.#####' ), '1234.567', 'Pattern with no digit grouping separator defined' ); + assert.equal( mw.language.commafy( 123456789.567, '###0.#####' ), '123456789.567', 'Pattern with no digit grouping seperator defined, bigger decimal part' ); + assert.equal( mw.language.commafy( 0.567, '###0.#####' ), '0.567', 'Decimal part 0' ); + assert.equal( mw.language.commafy( '.567', '###0.#####' ), '0.567', 'Decimal part missing. replace with zero' ); + assert.equal( mw.language.commafy( 1234, '##,#0.#####' ), '12,34', 'Pattern with no fractional part' ); + assert.equal( mw.language.commafy( -1234.567, '###0.#####' ), '-1234.567', 'Negative number' ); + assert.equal( mw.language.commafy( -1234.567, '#,###.00' ), '-1,234.56', 'Fractional part bigger than pattern.' ); + assert.equal( mw.language.commafy( 123456789.567, '###,##0.00' ), '123,456,789.56', 'Decimal part as group of 3' ); + assert.equal( mw.language.commafy( 123456789.567, '###,###,#0.00' ), '1,234,567,89.56', 'Decimal part as group of 3 and last one 2' ); + } ); + function grammarTest( langCode, test ) { // The test works only if the content language is opt.language // because it requires [lang].js to be loaded. @@ -452,4 +465,11 @@ grammarTest( langCode, test ); } } ); + + QUnit.test( 'List to text test', 4, function ( assert ) { + assert.equal( mw.language.listToText( [] ), '', 'Blank list' ); + assert.equal( mw.language.listToText( ['a'] ), 'a', 'Single item' ); + assert.equal( mw.language.listToText( ['a', 'b'] ), 'a and b', 'Two items' ); + assert.equal( mw.language.listToText( ['a', 'b', 'c'] ), 'a, b and c', 'More than two items' ); + } ); }( mediaWiki, jQuery ) ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js index 96813305da..7e0ee917f3 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js @@ -32,9 +32,7 @@ mw.loader.addSource( 'testloader', - { - loadScript: QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' ) - } + QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' ) ); QUnit.test( 'Initial check', 8, function ( assert ) { @@ -809,7 +807,7 @@ // Confirm that mw.loader.load() works with protocol-relative URLs target = target.replace( /https?:/, '' ); - assert.equal( target.substr( 0, 2 ), '//', + assert.equal( target.slice( 0, 2 ), '//', 'URL must be relative to test relative URLs!' ); diff --git a/tests/qunit/suites/resources/startup.test.js b/tests/qunit/suites/resources/startup.test.js index 4e26bdcb1b..ed03418a9c 100644 --- a/tests/qunit/suites/resources/startup.test.js +++ b/tests/qunit/suites/resources/startup.test.js @@ -1,7 +1,6 @@ /*global isCompatible: true */ ( function ( $ ) { var testcases = { - // Supported: Compatible gradeA: [ // Chrome 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.205 Safari/534.16', @@ -15,11 +14,14 @@ 'Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Kindle Fire Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Safari/533.1', // Safari 5.0+ 'Mozilla/5.0 (Macintosh; I; Intel Mac OS X 10_6_7; ru-ru) AppleWebKit/534.31+ (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1', - // Opera 11+ - 'Opera/9.80 (Windows NT 6.1; U; ru) Presto/2.8.131 Version/11.10', - // Internet Explorer 6+ - 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)', - 'Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; en-US)', + // Opera 12+ (Presto-based) + 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00', + 'Opera/9.80 (Windows NT 5.1) Presto/2.12.388 Version/12.17', + // Opera 15+ (Chromium-based) + 'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36 OPR/15.0.1147.153', + 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36 OPR/16.0.1196.62', + 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36 OPR/23.0.1522.75', + // Internet Explorer 8+ 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)', 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)', 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)', @@ -39,16 +41,27 @@ // Android 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17' ], - // Supported: Uncompatible, serve basic content - gradeB: [ - // Internet Explorer < 6 + gradeC: [ + // Internet Explorer < 8 'Mozilla/2.0 (compatible; MSIE 3.03; Windows 3.1)', 'Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)', 'Mozilla/4.0 (compatible; MSIE 5.0; Windows 98;)', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', - // Firefox < 3.6 + 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)', + 'Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; en-US)', + // Firefox < 3 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2', 'Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.1.1) Gecko/20070311 Firefox/2.0.0.1', + // Opera < 12 + 'Mozilla/5.0 (Windows NT 5.0; U) Opera 7.54 [en]', + 'Opera/7.54 (Windows NT 5.0; U) [en]', + 'Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.0', + 'Opera/8.0 (X11; Linux i686; U; cs)', + 'Opera/9.00 (X11; Linux i686; U; de)', + 'Opera/9.62 (X11; Linux i686; U; en) Presto/2.1.1', + 'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.2.15 Version/10.00', + 'Opera/9.80 (Windows NT 6.1; U; ru) Presto/2.8.131 Version/11.10', + 'Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62', // BlackBerry < 6 'BlackBerry9300/5.0.0.716 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/133', 'BlackBerry7250/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1', @@ -75,8 +88,7 @@ // Google Glass 'Mozilla/5.0 (Linux; U; Android 4.0.4; en-us; Glass 1 Build/IMM76L; XE11) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30' ], - // No explicit support for or against these browsers, they're - // given a shot at Grade A at their own risk. + // No explicit support for or against these browsers, they're given a shot at Grade A. gradeX: [ // Firefox 3.6 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3', @@ -114,8 +126,8 @@ ); } ); - QUnit.test( 'isCompatible( Grade B )', testcases.gradeB.length, function ( assert ) { - $.each( testcases.gradeB, function ( i, ua ) { + QUnit.test( 'isCompatible( Grade C )', testcases.gradeC.length, function ( assert ) { + $.each( testcases.gradeC, function ( i, ua ) { assert.strictEqual( isCompatible( ua ), false, ua ); } ); diff --git a/tests/testHelpers.inc b/tests/testHelpers.inc index 717c5f34d7..62dccbf0b3 100644 --- a/tests/testHelpers.inc +++ b/tests/testHelpers.inc @@ -36,23 +36,33 @@ */ interface ITestRecorder { - /** Called at beginning of the parser test run */ + /** + * Called at beginning of the parser test run + */ public function start(); - /** Called after each test */ + /** + * Called after each test + * @param string $test + * @param bool $result + */ public function record( $test, $result ); - /** Called before finishing the test run */ + /** + * Called before finishing the test run + */ public function report(); - /** Called at the end of the parser test run */ + /** + * Called at the end of the parser test run + */ public function end(); } class TestRecorder implements ITestRecorder { - var $parent; - var $term; + public $parent; + public $term; function __construct( $parent ) { $this->parent = $parent; @@ -107,6 +117,7 @@ class DbTestPreviewer extends TestRecorder { /** * This should be called before the table prefix is changed + * @param TestRecorder $parent */ function __construct( $parent ) { parent::__construct( $parent ); @@ -220,6 +231,9 @@ class DbTestPreviewer extends TestRecorder { * Returns a string giving information about when a test last had a status change. * Could help to track down when regressions were introduced, as distinct from tests * which have never passed (which are more change requests than regressions). + * @param string $testname + * @param string $after + * @return string */ private function getTestStatusInfo( $testname, $after ) { // If we're looking at a test that has just been removed, then say when it first appeared. @@ -295,7 +309,7 @@ class DbTestPreviewer extends TestRecorder { } class DbTestRecorder extends DbTestPreviewer { - var $version; + public $version; /** * Set up result recording; insert a record for the run with the date @@ -364,6 +378,10 @@ class TestFileIterator implements Iterator { private $sectionData = array(); private $lineNum; private $eof; + # Create a fake parser tests which never run anything unless + # asked to do so. This will avoid running hooks for a disabled test + private $delayedParserTest; + private $nextSubTest = 0; function __construct( $file, $parserTest ) { $this->file = $file; @@ -374,6 +392,7 @@ class TestFileIterator implements Iterator { } $this->parserTest = $parserTest; + $this->delayedParserTest = new DelayedParserTest(); $this->lineNum = $this->index = 0; } @@ -412,12 +431,73 @@ class TestFileIterator implements Iterator { return $this->eof != true; } + function setupCurrentTest() { + // "input" and "result" are old section names allowed + // for backwards-compatibility. + $input = $this->checkSection( array( 'wikitext', 'input' ), false ); + $result = $this->checkSection( array( 'html/php', 'html/*', 'html', 'result' ), false ); + // some tests have "with tidy" and "without tidy" variants + $tidy = $this->checkSection( array( 'html/php+tidy', 'html+tidy' ), false ); + if ( $tidy != false ) { + if ( $this->nextSubTest == 0 ) { + if ( $result != false ) { + $this->nextSubTest = 1; // rerun non-tidy variant later + } + $result = $tidy; + } else { + $this->nextSubTest = 0; // go on to next test after this + $tidy = false; + } + } + + if ( !isset( $this->sectionData['options'] ) ) { + $this->sectionData['options'] = ''; + } + + if ( !isset( $this->sectionData['config'] ) ) { + $this->sectionData['config'] = ''; + } + + $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) && !$this->parserTest->runDisabled; + $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) && $result == 'html' && !$this->parserTest->runParsoid; + $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] ); + if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) { + # disabled test + return false; + } + + # We are really going to run the test, run pending hooks and hooks function + wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" ); + $hooksResult = $this->delayedParserTest->unleash( $this->parserTest ); + if ( !$hooksResult ) { + # Some hook reported an issue. Abort. + throw new MWException( "Problem running hook" ); + } + + $this->test = array( + 'test' => ParserTest::chomp( $this->sectionData['test'] ), + 'input' => ParserTest::chomp( $this->sectionData[$input] ), + 'result' => ParserTest::chomp( $this->sectionData[$result] ), + 'options' => ParserTest::chomp( $this->sectionData['options'] ), + 'config' => ParserTest::chomp( $this->sectionData['config'] ), + ); + if ( $tidy != false ) { + $this->test['options'] .= " tidy"; + } + return true; + } + function readNextTest() { - $this->clearSection(); + # Run additional subtests of previous test + while ( $this->nextSubTest > 0 ) { + if ( $this->setupCurrentTest() ) { + return true; + } + } - # Create a fake parser tests which never run anything unless - # asked to do so. This will avoid running hooks for a disabled test - $delayedParserTest = new DelayedParserTest(); + $this->clearSection(); + # Reset hooks for the delayed test object + $this->delayedParserTest->reset(); while ( false !== ( $line = fgets( $this->fh ) ) ) { $this->lineNum++; @@ -446,7 +526,7 @@ class TestFileIterator implements Iterator { $line = trim( $line ); if ( $line ) { - $delayedParserTest->requireHook( $line ); + $this->delayedParserTest->requireHook( $line ); } } @@ -462,7 +542,7 @@ class TestFileIterator implements Iterator { $line = trim( $line ); if ( $line ) { - $delayedParserTest->requireFunctionHook( $line ); + $this->delayedParserTest->requireFunctionHook( $line ); } } @@ -489,52 +569,15 @@ class TestFileIterator implements Iterator { if ( $this->section == 'end' ) { $this->checkSection( 'test' ); - // "input" and "result" are old section names allowed - // for backwards-compatibility. - $input = $this->checkSection( array( 'wikitext', 'input' ), false ); - $result = $this->checkSection( array( 'html/php', 'html/*', 'html', 'result' ), false ); - - if ( !isset( $this->sectionData['options'] ) ) { - $this->sectionData['options'] = ''; - } - - if ( !isset( $this->sectionData['config'] ) ) { - $this->sectionData['config'] = ''; - } - - if ( $input == false || $result == false || - ( ( preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) - && !$this->parserTest->runDisabled ) - || ( preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) - && $result != 'html/php' && !$this->parserTest->runParsoid ) - || !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] ) ) - ) { - # disabled test - $this->clearSection(); - - # Forget any pending hooks call since test is disabled - $delayedParserTest->reset(); - - continue; - } - - # We are really going to run the test, run pending hooks and hooks function - wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" ); - $hooksResult = $delayedParserTest->unleash( $this->parserTest ); - if ( !$hooksResult ) { - # Some hook reported an issue. Abort. - return false; - } - - $this->test = array( - 'test' => ParserTest::chomp( $this->sectionData['test'] ), - 'input' => ParserTest::chomp( $this->sectionData[$input] ), - 'result' => ParserTest::chomp( $this->sectionData[$result] ), - 'options' => ParserTest::chomp( $this->sectionData['options'] ), - 'config' => ParserTest::chomp( $this->sectionData['config'] ), - ); - - return true; + do { + if ( $this->setupCurrentTest() ) { + return true; + } + } while ( $this->nextSubTest > 0 ); + # go on to next test (since this was disabled) + $this->clearSection(); + $this->delayedParserTest->reset(); + continue; } if ( isset( $this->sectionData[$this->section] ) ) { @@ -570,10 +613,11 @@ class TestFileIterator implements Iterator { * Throw an exception if it is not set, referencing current section * and adding the current file name and line number * - * @param string|array $token Expected token(s) that should have been + * @param string|array $tokens Expected token(s) that should have been * mentioned before closing this section * @param bool $fatal True iff an exception should be thrown if * the section is not found. + * @return bool|string */ private function checkSection( $tokens, $fatal = true ) { if ( is_null( $this->section ) ) { @@ -646,6 +690,7 @@ class DelayedParserTest { * Called whenever we actually want to run the hook. * Should be the case if we found the parserTest is not disabled * @param ParserTest|NewParserTest $parserTest + * @return bool */ public function unleash( &$parserTest ) { if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest ) ) { @@ -702,7 +747,7 @@ class DelayedParserTest { /** * Similar to ParserTest object but does not run anything * Use unleash() to really execute the hook function - * @param string $fnHook + * @param string $hook */ public function requireTransparentHook( $hook ) { $this->transparentHooks[] = $hook; @@ -732,7 +777,7 @@ class DjVuSupport { } /** - * Returns if the DjVu tools are usable + * Returns true if the DjVu tools are usable * * @return bool */ @@ -745,3 +790,43 @@ class DjVuSupport { && is_executable( $wgDjvuTxt ); } } + +/** + * Initialize and detect the tidy support + */ +class TidySupport { + private $internalTidy; + private $externalTidy; + + /** + * Determine if there is a usable tidy. + */ + public function __construct() { + global $wgTidyBin; + + $this->internalTidy = extension_loaded( 'tidy' ) && + class_exists( 'tidy' ); + + $this->externalTidy = is_executable( $wgTidyBin ) || + Installer::locateExecutableInDefaultPaths( array( $wgTidyBin ) ) + !== false; + } + + /** + * Returns true if we should use internal tidy. + * + * @return bool + */ + public function isInternal() { + return $this->internalTidy; + } + + /** + * Returns true if tidy is usable + * + * @return bool + */ + public function isEnabled() { + return $this->internalTidy || $this->externalTidy; + } +} diff --git a/thumb.php b/thumb.php index d53c10c93e..d8ed246f96 100644 --- a/thumb.php +++ b/thumb.php @@ -24,7 +24,7 @@ define( 'MW_NO_OUTPUT_COMPRESSION', 1 ); require __DIR__ . '/includes/WebStart.php'; -// Don't use fancy mime detection, just check the file extension for jpg/gif/png +// Don't use fancy MIME detection, just check the file extension for jpg/gif/png $wgTrivialMimeDetection = true; if ( defined( 'THUMB_HANDLER' ) ) { @@ -553,7 +553,7 @@ function wfExtractThumbRequestInfo( $thumbRel ) { * file handler. * * @param File $file File object for file in question - * @param array $param Array of parameters so far + * @param array $params Array of parameters so far * @return array Parameters array with more parameters */ function wfExtractThumbParams( $file, $params ) { @@ -573,7 +573,7 @@ function wfExtractThumbParams( $file, $params ) { return $params; // valid thumbnail URL (via extension or config) } - // FIXME: Files in the temp zone don't set a mime type, which means + // FIXME: Files in the temp zone don't set a MIME type, which means // they don't have a handler. Which means we can't parse the param // string. However, not a big issue as what good is a param string // if you have no handler to make use of the param string and