Merge "Add API module to receive CSP reports."
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 29 Jun 2016 15:26:23 +0000 (15:26 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 29 Jun 2016 15:26:23 +0000 (15:26 +0000)
56 files changed:
HISTORY
RELEASE-NOTES-1.27 [deleted file]
RELEASE-NOTES-1.28
autoload.php
composer.json
docs/hooks.txt
includes/MediaWikiServices.php
includes/ServiceWiring.php
includes/WatchedItemQueryService.php [new file with mode: 0644]
includes/api/ApiQueryWatchlist.php
includes/api/i18n/cs.json
includes/api/i18n/ko.json
includes/debug/logger/monolog/KafkaHandler.php
includes/installer/i18n/mk.json
includes/skins/Skin.php
includes/specials/SpecialSearch.php
includes/upload/UploadBase.php
languages/i18n/ar.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/cs.json
languages/i18n/diq.json
languages/i18n/got.json
languages/i18n/he.json
languages/i18n/ja.json
languages/i18n/ko.json
languages/i18n/mk.json
languages/i18n/sv.json
resources/lib/oojs-ui/i18n/id.json
resources/lib/oojs-ui/i18n/inh.json [new file with mode: 0644]
resources/lib/oojs-ui/i18n/jv.json
resources/lib/oojs-ui/i18n/ku-latn.json
resources/lib/oojs-ui/i18n/pnb.json [new file with mode: 0644]
resources/lib/oojs-ui/i18n/sw.json
resources/lib/oojs-ui/oojs-ui-apex.js
resources/lib/oojs-ui/oojs-ui-core-apex.css
resources/lib/oojs-ui/oojs-ui-core-mediawiki.css
resources/lib/oojs-ui/oojs-ui-core.js
resources/lib/oojs-ui/oojs-ui-mediawiki.js
resources/lib/oojs-ui/oojs-ui-toolbars-apex.css
resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css
resources/lib/oojs-ui/oojs-ui-toolbars.js
resources/lib/oojs-ui/oojs-ui-widgets-apex.css
resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css
resources/lib/oojs-ui/oojs-ui-widgets.js
resources/lib/oojs-ui/oojs-ui-windows-apex.css
resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css
resources/lib/oojs-ui/oojs-ui-windows.js
resources/src/jquery/jquery.accessKeyLabel.js
resources/src/jquery/jquery.suggestions.js
resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js
resources/src/mediawiki/mediawiki.searchSuggest.js
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php

diff --git a/HISTORY b/HISTORY
index e833154..868b21a 100644 (file)
--- a/HISTORY
+++ b/HISTORY
@@ -1,5 +1,588 @@
 Change notes from older releases. For current info see RELEASE-NOTES-1.28.
 
+= MediaWiki 1.27 =
+
+== MediaWiki 1.27.0 ==
+
+=== PHP version requirement in 1.27 ===
+As of 1.27, MediaWiki now requires PHP 5.5.9 or higher (see Compatibility
+section). Additionally, the following PHP extensions are required:
+* ctype
+* iconv
+* json
+* mbstring (new requirement in 1.27)
+* xml
+The following PHP extensions are strongly recommended:
+* openssl
+
+=== Configuration changes in 1.27 ===
+* $wgAllowMicrodataAttributes and $wgAllowRdfaAttributes were removed,
+  now always enabled. If you use RDFa on your wiki, you now have to explicitly
+  set $wgHtml5Version to 'HTML+RDFa 1.0' or 'XHTML+RDFa 1.0'.
+* $wgUseLinkNamespaceDBFields was removed.
+* Deprecated $wgResourceLoaderMinifierStatementsOnOwnLine and
+  $wgResourceLoaderMinifierMaxLineLength, because there was little value in
+  making the behavior configurable. The default values (`false` for the former,
+  1000 for the latter) are now hard-coded.
+* $wgDebugDumpSqlLength was removed (deprecated in 1.24).
+* $wgDebugDBTransactions was removed (deprecated in 1.20).
+* $wgUseXVO has been removed, as it provides functionality only used by
+  custom Wikimedia patches against Squid 2.x that probably noone uses in
+  production anymore. There is now $wgUseKeyHeader that provides similar
+  functionality but instead of the MediaWiki-specific X-Vary-Options header,
+  uses the draft Key header standard.
+* $wgScriptExtension (and support for '.php5' entry points) was removed. See the
+  deprecation notice in the release notes for version 1.25 for advice on how to
+  preserve support for '.php5' entry points via URL rewriting.
+* Password handling via the User object has been deprecated and partially
+  removed, pending the future introduction of AuthManager. In particular:
+** expirePassword(), getPasswordExpireDate(), resetPasswordExpiration(), and
+   getPasswordExpired() have been removed. They were unused outside of core.
+** The mPassword, mNewpassword, mNewpassTime, and mPasswordExpires fields are
+   now private and will be removed in the future.
+** The getPassword() and getTemporaryPassword() methods now throw
+   BadMethodCallException and will be removed in the future.
+** The ability to pass 'password' and 'newpassword' to createNew() has been
+   removed. The only users of it seem to have been using it to set invalid
+   passwords, and so shouldn't be greatly affected.
+** setPassword(), setInternalPassword(), and setNewpassword() have been
+   deprecated, pending the introduction of AuthManager.
+** User::randomPassword() is deprecated in favor of a new method
+   PasswordFactory::generateRandomPasswordString()
+** User::getPasswordFactory() is deprecated, callers should just create a
+   PasswordFactory themselves.
+** A new constructor, User::newSystemUser(), has been added to simplify the
+   creation of passwordless "system" users for logged actions.
+* $wgMaxSquidPurgeTitles was removed.
+* $wgAjaxWatch was removed. This is now enabled by default.
+* $wgUseInstantCommons now hotlinks Commons images by default instead of
+  downloading originals and thumbnailing them locally. This allows wikis to save
+  on CPU and bandwidth while reducing time to first byte for pages, even without
+  a thumbnail handler. See $wgForeignFileRepos documentation for tweaks.
+* (T27397) WebP is enabled by default as an uploadable filetype.
+* (T48998) $wgArticlePath must now be either a full url, or start with a "/".
+* $wgRateLimitLog was removed; use $wgDebugLogGroups['ratelimit'] instead.
+* Deprecated API formats dbg, txt, and yaml have been removed.
+* CLDRPluralRule* classes have been replaced with
+  wikimedia/cldr-plural-rule-parser.
+* Removed $wgProfilePerHost, $wgUDPProfilerHost, $wgUDPProfilerPort,
+  $wgUDPProfilerFormatString, $wgStatsMethod, $wgAggregateStatsID,
+  $wgStatsFormatString, and $wgProfileCallTree (deprecated since 1.20).
+* For proper operation of LocalIdLookup with shared user tables, ensure that
+  $wgSharedDB and $wgSharedTables are properly set even on the "central" wiki
+  that all others are sharing from and that $wgLocalDatabases is set to the
+  full list of sharing wikis on all those wikis.
+* Massive overhaul to session handling:
+** $wgSessionsInObjectCache is no longer supported and must be true, due to
+   MediaWiki\Session\SessionManager. $wgSessionHandler is similarly no longer
+   used.
+** ObjectCacheSessionHandler is removed, replaced with
+   MediaWiki\Session\PhpSessionHandler.
+** PHP session handling in general ($_SESSION, session_id(), and so on) is
+   deprecated. Use MediaWiki\Session\SessionManager instead. A new config
+   variable, $wgPHPSessionHandling, is available to cause use of $_SESSION to
+   issue a deprecation warning or to cause most PHP session handling to throw
+   exceptions.
+** Deprecated UserSetCookies hook. Session-handling extensions should generally
+   be creating a custom subclass of CookieSessionProvider. Other extensions
+   messing with cookies can no longer count on user data being saved in cookies
+   versus other methods.
+** Deprecated UserLoadFromSession hook, extensions should create a
+   MediaWiki\Session\SessionProvider.
+** The User cannot be loaded from session until after Setup.php completes.
+   Attempts to do so will be ignored and the User will remain unloaded.
+** CSRF tokens may be fetched from the MediaWiki\Session\Session, which uses
+   the MediaWiki\Session\Token class.
+* MediaWiki will now auto-create users as necessary, removing the need for
+  extensions to do so. An 'autocreateaccount' right is added to allow
+  auto-creation when 'createaccount' is not granted to all users.
+* Deprecated AuthPluginAutoCreate hook in favor of LocalUserCreated.
+* Most cookie-handling methods in User are deprecated.
+* $wgAllowAsyncCopyUploads and $CopyUploadAsyncTimeout were removed. This was an
+  experimental feature that has never worked.
+* Login and createaccount tokens now vary by timestamp.
+* LoginForm::getLoginToken() and LoginForm::getCreateaccountToken()
+  return a MediaWiki\Session\Token, and tokens must be checked using that
+  class's methods.
+* $wgEnotifUseJobQ was removed and the job queue is always used.
+* The functionality of the ApiSandbox extension has been merged into core. The
+  extension should no longer be used.
+* $wgPreloadJavaScriptMwUtil was removed (deprecated in 1.26).
+  Extensions, skins, gadgets and scripts that use the mediawiki.util module must
+  express a dependency on it.
+* $wgIncludeLegacyJavaScript, deprecated in MediaWiki 1.26, now defaults false.
+  Extensions, skins, gadgets and scripts that need the mediawiki.legacy.wikibits
+  module should express a dependency on it.
+* Removed configuration option $wgCopyrightIcon (deprecated since 1.18). Use
+  $wgFooterIcons['copyright']['copyright'] instead.
+* If the openssl and mcrypt PHP extensions are both unavailable, secure
+  session storage (used for login) will raise an exception. This exception may
+  be bypassed by setting $wgSessionInsecureSecrets = true.
+* Massive overhaul to authentication:
+** AuthPlugin and AuthPluginUser are deprecated.
+** LoginForm and associated templates are deprecated. Extensions which called
+   static LoginForm methods should be converted into authentication providers.
+** The following hooks are deprecated:
+*** AbortAutoAccount (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** AbortLogin (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** AbortNewAccount (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** AddNewAccount (use LocalUserCreated instead)
+*** AuthPluginSetup (create a MediaWiki\Auth\PrimaryAuthenticationProvider instead)
+*** ChangePasswordForm (use AuthChangeFormFields instead, or security levels)
+*** LoginUserMigrated (create a MediaWiki\Auth\PreAuthenticationProvider instead)
+*** UserCreateForm (create a MediaWiki\Auth\AuthenticationProvider of some type instead)
+*** UserLoginForm (create a MediaWiki\Auth\AuthenticationProvider of some type instead)
+** The following hooks are removed:
+*** AbortChangePassword
+*** LoginPasswordResetMessage
+*** PrefsPasswordAudit
+** The UserLoginComplete hook will no longer be called for all logins, only for
+   those via the web UI. Use UserLoggedIn if you need to do something on all
+   logins.
+** $wgRequirePasswordforEmailChange is removed.
+
+=== New features in 1.27 ===
+* $wgDataCenterUpdateStickTTL was also added. This decides how long a user
+  sticks to the primary DC (via cookies) after they make changes to the site.
+* Added a new hook, 'UserMailerTransformContent', to transform the contents
+  of an email. This is similar to the EmailUser hook but applies to all mail
+  sent via UserMailer.
+* Added a new hook, 'UserMailerTransformMessage', to transform the contents
+  of an emai after MIME encoding.
+* Added a new hook, 'UserMailerSplitTo', to control which users have to be
+  emailed separately (ie. there is a single address in the To: field) so
+  user-specific changes to the email can be applied safely.
+* $wgCdnMaxageLagged was added, which limits the CDN cache TTL
+  when any load balancer uses a DB that is lagged beyond the 'max lag'
+  setting in the relevant section of $wgLBFactoryConf.
+* User::newSystemUser() may be used to simplify the creation of passwordless
+  "system" users for logged actions from scripts and extensions.
+* Extensions can now return detailed error information via the API when
+  preventing user actions using 'getUserPermissionsErrors' and similar hooks
+  by using ApiMessage instances instead of strings for the $result value.
+* $wgAPIMaxLagThreshold was added to limit bot changes when databases lag
+  becomes too high.
+* Skins and extensions can now use FlexBox mixins (.flex-display(@display: flex)
+  and .flex(@grow: 1, @shrink: 1, @width: auto, @order: 1)) in Less to create
+  cross-browser-compatible FlexBox rules. Users will still need to add fallback
+  float rules or the like for compatibility with IE9- separately.
+* Added MWTimestamp::getTimezoneString() which returns the localized timezone
+  string, if available. To localize this string, see the comments of
+  $wgLocaltimezone in includes/DefaultSettings.php.
+* Added CentralIdLookup, a service that allows extensions needing a concept of
+  "central" users to get that without having to know about specific central
+  authentication extensions.
+* $wgMaxUserDBWriteDuration added to limit huge user-generated transactions.
+  Regular web request transactions that takes longer than this are aborted.
+* Added a new hook, 'TitleMoveCompleting', which runs before a page move is
+  committed.
+* $wgCdnReboundPurgeDelay was added to provide secondary delayed purges of URLs
+  from CDN to mitigate DB replication lag and WAN cache purge lag.
+* (T49162) Installer will default to setting CACHE_ACCEL as the main cache type
+  if it is available.
+* It is now possible to patrol file uploads (both for new files and new versions
+  of existing files). Special:NewFiles has gained an option to filter by patrol
+  status. This functionality can be disabled using $wgUseFilePatrol.
+* MediaWiki\Session infrastructure allows for easier use of session mechanisms
+  other than the usual cookies.
+** SessionMetadata and SessionCheckInfo hooks allow for setting and checking
+   custom session metadata.
+* Added MWGrants and associated configuration settings $wgGrantPermissions and
+  $wgGrantPermissionGroups to hold configuration for authentication features
+  such as OAuth that want to allow restricting the user rights a user may make
+  use of.
+** If you're already using the OAuth extension, these new variables are
+   identical to (and will replace) $wgMWOAuthGrantPermissions and
+   $wgMWOAuthGrantPermissionGroups.
+* Added MWRestrictions as a class to check restrictions on a WebRequest, e.g.
+  to assert that the request comes from a particular IP range.
+* Added bot passwords, a rights-restricted login mechanism for API-using bots.
+* Whitelisted the following HTML attributes for all elements in wikitext:
+  aria-describedby, aria-flowto, aria-label, aria-labelledby, aria-owns.
+* Removed "presentation" restriction on the HTML role attribute in wikitext.
+  All values are now allowed for the role attribute.
+* $wgContentHandlers now also supports callbacks to create an instance of the
+  appropriate ContentHandler subclass.
+* Added $wgAuthenticationTokenVersion, which if non-null prevents the
+  user_token database field from being exposed in cookies. Setting this would
+  be a good idea, but will log out all current sessions.
+* $wgEventRelayerConfig was added, for managing PubSub event relay configuration,
+  specifically for reliable CDN url purges.
+* Requests have unique IDs, equal to the UNIQUE_ID environment variable (when
+  MediaWiki is behind Apache+mod_unique_id or something similar) or a randomly-
+  generated 24-character string. This request ID is used to annotate log records
+  and error messages. It is available client-side via mw.config.get( 'wgRequestId' ).
+  The request ID supplants exception IDs. Accordingly, MWExceptionHandler::getLogId()
+  is deprecated.
+* (T33313) Add a preference for watching uploads by default, also applies
+  to API-based upload tools.
+* $wgJpegPixelFormat was added to override chroma subsampling for JPEG image
+  thumbnails created via ImageMagick. Defaults to 'yuv420', providing bandwidth
+  savings versus the previous behavior on many files.
+* MediaWiki\Auth infrastructure (called "AuthManager") allows for more flexible
+  configuration of multiple authentication pieces that was possible with
+  AuthPlugin. For example, it's now easy to plug in second-factor
+  authentication, or add additional checks to the login process, or to support
+  multiple login methods at once, or to support non-password-based login methods.
+** Providers are configured via the global setting $wgAuthManagerConfig.
+** A global, $wgDisableAuthManager, is temporarily available to disable
+   AuthManager until extensions are ready to support it.
+** New hook, AuthChangeFormFields, to adjust the form fields on
+   AuthManager-related special pages.
+** New hook, AuthManagerLoginAuthenticateAudit, for additional logging of
+   AuthManager-related authentication requests.
+** New hook, ChangeAuthenticationDataAudit, for additional logging of
+   AuthManager-related authentication data changes.
+** New hook, SecuritySensitiveOperationStatus, to work with the new mechanism
+   for requiring a recent login before taking security-sensitive operations
+   like changing a password.
+** Two new globals, $wgChangeCredentialsBlacklist and $wgRemoveCredentialsBlacklist
+   can be used to prevent the web UI and the API changing certain authentication data.
+* The file upload dialog (available if you install WikiEditor or VisualEditor)
+  can now be configured using $wgUploadDialog.
+
+=== External library changes in 1.27 ===
+
+==== Upgraded external libraries ====
+* Updated oojs/oojs-ui from v0.12.12 to v0.13.3.
+* Updated composer/semver from v1.0.0 to v1.2.0.
+* Updated liuggio/statsd-php-client to 1.0.18.
+* Updated QUnit from v1.18.0 to v1.22.0.
+
+==== New external libraries ====
+* Added wikimedia/base-convert v1.0.1.
+* Added wikimedia/cldr-plural-rule-parser v1.0.0.
+* Added wikimedia/relpath v1.0.3.
+* Added wikimedia/running-stat v1.1.0.
+* Added wikimedia/php-session-serializer v1.0.3.
+
+==== Removed and replaced external libraries ====
+
+=== Bug fixes in 1.27 ===
+* Special:Upload will now display correct maximum allowed file size when running
+  under HHVM (T116347).
+* (T54077) The APIEditBeforeSave hook will once again give only the content of
+  the section being edited, rather than the whole revision. This reverts the
+  change made in MediaWiki 1.22.
+
+=== Action API changes in 1.27 ===
+* Added list=allrevisions.
+* generator=recentchanges now has the option to generate revids.
+* ApiPageSet::setRedirectMergePolicy() was added. This allows generator
+  modules to define how generator data for a redirect source gets merged
+  into the redirect destination.
+* prop=imageinfo&iiprop=uploadwarning will no longer include the possibility of
+  "was-deleted" warning.
+* Added difftotextpst to query=revisions which preforms a pre-save transform on
+  the text before diffing it.
+* Deprecated formats dbg, txt, and yaml have been removed.
+* (T47988) The protect log event details now use new-style formatting.
+* The following response properties from action=login are deprecated, and may
+  be removed in the future: lgtoken, cookieprefix, sessionid. Clients should
+  handle cookies to properly manage session state.
+* action=login transparently allows login using bot passwords. Clients should
+  merely need to change the username and password used after setting up a bot
+  password.
+* action=upload no longer understands statuskey, asyncdownload or leavemessage.
+* Several changes when $wgDisableAuthManager is false:
+** action=login is deprecated for uses other than bot passwords.
+** list=users can now indicate if a missing username is creatable.
+** action=createaccount is changed in a non-backwards-compatible manner.
+** Added action=query&meta=authmanagerinfo.
+** Added action=clientlogin to be used to log into the main account instead of
+   action=login.
+** Added action=linkaccount.
+** Added action=unlinkaccount.
+** Added action=changeauthenticationdata.
+** Added action=removeauthenticationdata.
+** Added action=resetpassword.
+
+=== Action API internal changes in 1.27 ===
+* ApiQueryORM removed.
+* The following classes have been removed:
+** ApiFormatDbg
+** ApiFormatTxt
+** ApiFormatYaml
+* ApiBase::addTokenProperties() was removed (deprecated since 1.24).
+* ApiBase::getFinalPossibleErrors() was removed (deprecated since 1.24).
+* ApiBase::getFinalResultProperties() was removed (deprecated since 1.24).
+* ApiBase::getRequireAtLeastOneParameterErrorMessages() was removed (deprecated since 1.24).
+* ApiBase::getPossibleErrors() was removed (deprecated since 1.24).
+* ApiBase::getRequireMaxOneParameterErrorMessages() was removed (deprecated since 1.24).
+* ApiBase::getRequireOnlyOneParameterErrorMessages() was removed (deprecated since 1.24).
+* ApiBase::getResultProperties() was removed (deprecated since 1.24).
+* ApiBase::getTitleOrPageIdErrorMessage() was removed (deprecated since 1.24).
+* ApiBase::parseErrors() was removed (deprecated since 1.24).
+* ApiQueryBase::titleToKey(), ApiQueryBase::keyToTitle() and
+  ApiQueryBase::keyPartToTitle() all removed (deprecated since 1.24).
+* ApiQueryBase::checkRowCount() was removed (deprecated since 1.24).
+* ApiQueryBase::getDirectionDescription() was removed (deprecated since 1.25).
+* ApiQuery::getGenerators() was removed (deprecated since 1.21).
+* ApiQuery::getModules() was removed (deprecated since 1.21).
+* ApiQuery::getModuleType() was removed (deprecated since 1.21).
+* ApiQuery::setGeneratorContinue() was removed (deprecated since 1.24).
+* ApiMain::getModules() was removed (deprecated since 1.21).
+* ApiBase::getVersion() was removed (deprecated since 1.21).
+* ApiMain::getShowVersions() was removed (deprecated in 1.21).
+* ApiMain::addModule() was removed (deprecated in 1.21).
+* ApiMain::addFormat() was removed (deprecated in 1.21).
+* ApiMain::getFormats() was removed (deprecated in 1.21).
+* ApiPageSet::finishPageSetGeneration() was removed (deprecated in 1.21).
+* ApiCreateAccount is deprecated, and will be removed soon.
+
+=== Languages updated in 1.27 ===
+
+MediaWiki supports over 350 languages. Many localisations are updated
+regularly. Below only new and removed languages are listed, as well as
+changes to languages because of Phabricator reports.
+
+* (T113688) Change default numerals from Gurmukhi to Arabic for Punjabi locale.
+* (T116020) Aliases of magic words in MessagesXx.php are sorted by usage.
+
+=== Other changes in 1.27 ===
+* Added dependency injection (DI) infrastructure, see docs/injection.txt for details.
+  It is planned to incrementally move MediaWiki code towards using DI, using the
+  service locator (SL) pattern as a stepping stone.
+* ProfilerOutputUdp was removed. Note that there is a ProfilerOutputStats class.
+* WikiPage::doDeleteArticleReal() and WikiPage::doDeleteArticle() now
+  ignore the 2nd and 3rd arguments (formerly $id and $commit).
+* Removed "loaderScripts" option from ResourceLoaderFileModule class.
+* Removed ORM-like wrapper added in 1.20.
+* LinkCache::getGoodLinks and LinkCache::getBadLinks were removed
+  (deprecated in 1.26).
+* WikiPage::doQuickEdit() was removed (deprecated since 1.21).
+* Removed SiteObject and SiteArray classes (deprecated in 1.21).
+* MessageBlobStore::getInstance() was removed (deprecated since 1.25).
+* (T84937) Free external links ("autolinked" urls) will now be terminated
+  by &nbsp; and HTML entity encodings of &nbsp, <, and >.
+* (T36948) The default file revert message's timestamp is now in
+  $wgLocaltimezone, instead of UTC.
+* The default name of the 'suppress' group page has been changed from
+  'Project:Oversight' to 'Project:Suppress'.
+* DatabaseBase::resultObject() is now protected (use outside Database classes
+  not necessary since 1.11).
+* Calling ResourceLoaderFileModule::readStyleFiles() without a
+  ResourceLoaderContext instance is deprecated.
+* ResourceLoader::getLessCompiler() now takes an optional parameter of
+  additional LESS variables to set for the compiler.
+* wfBaseConvert() marked as deprecated, use Wikimedia\base_convert() directly
+  instead.
+* Obsolete maintenance scripts clearCacheStats.php and showCacheStats.php
+  were removed. The underlying data is sent to StatsD (see $wgStatsdServer).
+* Removed msg_resource_links database table and associated code.
+* Removed msg_resource database table and associated code.
+* Skin::getNamespaceNotice() was removed.
+* wfIsConfiguredProxy() was removed (deprecated since 1.24).
+* wfDebugTimer() was removed (deprecated since 1.25).
+* wfIsTrustedProxy() was removed (deprecated since 1.24).
+* wfGetIP() was removed (deprecated since 1.19).
+* MWHookException was removed.
+* OutputPage::appendSubtitle() was removed (deprecated since 1.19).
+* OutputPage::loginToUse() was removed (deprecated since 1.19).
+* Article::loadContent() was removed (deprecated since 1.19).
+* User::editToken() was removed (deprecated since 1.19).
+* Removed --force-normal option of dumpBackup.php, as it no longer served
+  any useful purpose since 1.22.
+* The functions processOption() and processArgs() on the BackupDumper and
+  TextPassDumper classes have been removed.
+* The maintenance/backupTextPass.inc file was deleted. You should include
+  maintenance/dumpTextPass.php instead.
+* WikiPage::getUsedTemplates() was removed (deprecated since 1.19).
+* wfEmptyMsg() was removed (deprecated since 1.18).
+* OutputPage::permissionRequired() was removed (deprecated since 1.18).
+* OutputPage::blockedPage() was removed (deprecated since 1.18).
+* User::getSkin() was removed (deprecated since 1.18).
+* OutputPage::includeJQuery() was removed (deprecated since 1.17).
+* WikiPage::updateRestrictions() was removed (deprecated since 1.19).
+* WikiPage::testPreSaveTransform() was removed (deprecated since 1.19).
+* LogPage::logName() was removed (deprecated since 1.19).
+* LogPage::logHeader() was removed (deprecated since 1.19).
+* wfCheckLimits() was removed (deprecated since 1.24).
+* Linker::makeKnownLinkObj() was removed (deprecated since 1.16).
+* Linker::makeLinkObj() was removed (deprecated since 1.16).
+* wfMsgForContentNoTrans() was removed (deprecated since 1.18).
+* ChangesList::usePatrol was removed (deprecated since 1.22).
+* wfMsgNoTrans() was removed (deprecated since 1.18).
+* Linker::makeImageLink2 was removed (deprecated since 1.20).
+* Title::userIsWatching() was removed (deprecated since 1.20).
+* Removed WaitForSlave maintenance script; use SELECT MASTER_POS_WAIT()
+  database function directly instead.
+* wfMsg() was removed (deprecated since 1.18).
+* wfMsgForContent() was removed (deprecated since 1.18).
+* wfMsgReal() was removed (deprecated since 1.18).
+* wfMsgGetKey() was removed (deprecated since 1.18).
+* wfMsgHtml() was removed (deprecated since 1.18).
+* wfMsgWikiHtml() was removed (deprecated since 1.18).
+* wfMsgExt() was removed (deprecated since 1.18).
+* Language::armourMath() was removed (deprecated since 1.22).
+* LanguageConverter::armourMath() was removed (deprecated since 1.22).
+* FakeConverter::armourMath() was removed (deprecated since 1.22).
+* The unused jquery.validate ResourceLoader module was removed.
+* FileRepo::getRootUrl() was removed (deprecated since 1.20).
+* User::generateToken() was removed (deprecated since 1.20).
+* WikiPage::getRawText() was removed (deprecated since 1.21).
+* ParserOutput::hasCustomDataUpdates() was removed (deprecated since 1.25).
+* ParserOutput::addSecondaryDataUpdate() was removed (deprecated since 1.25).
+* ParserOutput::getSecondaryDataUpdates() was removed (deprecated since 1.25).
+* Gallery images with multiple caption pipes no longer concatenate them all
+  together but instead pick the final one, similar to image syntax.
+* XML-like parser tags (such as <gallery>), when unclosed, will be left unparsed
+  rather than consume everything until the end of the page.
+* New maintenance script resetUserEmail.php allows sysadmins to reset user emails in case
+  a user forgot password/account was stolen.
+* wfCheckEntropy() was removed (deprecated in 1.27).
+* Browser support for Internet Explorer 8 lowered from Grade A to Grade C.
+* ContentHandler::supportsCategories method added. Default is true.
+  CategoryMembershipChangeJob updates are skipped for content that
+  does not support categories.
+* wikidiff difference engine is no longer supported, anyone still using it are encouraged
+  to upgrade to wikidiff2 which is actively maintained and has better package availability.
+* Database logic was removed from WatchedItem and a WatchedItemStore was created:
+** WatchedItem::IGNORE_USER_RIGHTS and WatchedItem::CHECK_USER_RIGHTS were deprecated.
+   User::IGNORE_USER_RIGHTS and User::CHECK_USER_RIGHTS were introduced.
+** WatchedItem::fromUserTitle was deprecated in favour of the constructor.
+** WatchedItem::resetNotificationTimestamp was deprecated.
+** WatchedItem::batchAddWatch was deprecated.
+** WatchedItem::addWatch was deprecated.
+** WatchedItem::removeWatch was deprecated.
+** WatchedItem::isWatched was deprecated.
+** WatchedItem::duplicateEntries was deprecated.
+** EmailNotification::updateWatchlistTimestamp was deprecated.
+** User::getWatchedItem was removed.
+* Unit tests don't work with external PHPUnit anymore, Composer is now the only supported
+  way. Run `composer install` to install it and other dev dependencies to run unit tests.
+* wl_id field added to the watchlist table.
+* Revision::getRawText() was removed (deprecated since 1.21).
+* WikiPage::replaceSection() was removed (deprecated since 1.21).
+* Article::replaceSection() was removed (deprecated since 1.21).
+* Language::getLangObj() was removed (deprecated since 1.24).
+* Language::getLanguageName() was removed (deprecated since 1.20).
+* Language::getLanguageNames() was removed (deprecated since 1.20).
+* Language::getTranslatedLanguageNames() was removed (deprecated since 1.20).
+* Language::specialPage() was removed (deprecated since 1.24).
+* MediaWikiTestCase::assertException() was removed (deprecated since 1.22).
+* OutputPage::getHeadItems() was removed (deprecated since 1.24).
+* OutputPage::getScript() was removed (deprecated since 1.24).
+* OutputPage::out() was removed (deprecated since 1.22).
+* OutputPage::setAllowedModules() was removed (deprecated since 1.24).
+* UserrightsPage::makeGroupNameListForLog() was removed (deprecated since 1.21).
+* MediaWikiSite::newFromGlobalId() was removed (deprecated since 1.21).
+* Title::newFromRedirect() was removed (deprecated since 1.21).
+* Skin::commonPrintStylesheet() was removed (deprecated since 1.22).
+* Skin::getCommonStylePath() was removed (deprecated since 1.24).
+* Skin::newFromKey() was removed (deprecated since 1.24).
+* Skin::getUsableSkins() was removed (deprecated since 1.23).
+* LoadBalancer::pickRandom() was removed (deprecated in 1.21).
+* Article::getUndoText() and WikiPage::getUndoText were removed (deprecated since
+  1.21).
+* DifferenceEngine::setText() was removed (deprecated in 1.21).
+* Title::newFromRedirectArray() was removed (deprecated in 1.21).
+* UserMailer::send() no longer accepts $replyto as the 5th argument and $contentType
+  as the 6th. These must be passed in the options array now.
+* Title::newFromRedirectRecurse() was removed (deprecated in 1.21).
+* Skin::accesskey was removed (deprecated since 1.21).
+* Skin::blockLink was removed (deprecated since 1.21).
+* Skin::buildRollbackLink was removed (deprecated since 1.21).
+* Skin::emailLink was removed (deprecated since 1.21).
+* Skin::formatComment was removed (deprecated since 1.21).
+* Skin::formatHiddenCategories was removed (deprecated since 1.21).
+* Skin::formatLinksInComment was removed (deprecated since 1.21).
+* Skin::formatRevisionSize was removed (deprecated since 1.21).
+* Skin::formatSize was removed (deprecated since 1.21).
+* Skin::formatTemplates was removed (deprecated since 1.21).
+* Skin::generateTOC was removed (deprecated since 1.21).
+* Skin::getInternalLinkAttributes was removed (deprecated since 1.21).
+* Skin::getInternalLinkAttributesObj was removed (deprecated since 1.21).
+* Skin::getInterwikiLinkAttributes was removed (deprecated since 1.21).
+* Skin::getInvalidTitleDescription was removed (deprecated since 1.21).
+* Skin::getLinkColour was removed (deprecated since 1.21).
+* Skin::getRevDeleteLink was removed (deprecated since 1.21).
+* Skin::getRollbackEditCount was removed (deprecated since 1.21).
+* Skin::makeBrokenImageLinkObj was removed (deprecated since 1.21).
+* Skin::makeCommentLink was removed (deprecated since 1.21).
+* Skin::makeExternalImage was removed (deprecated since 1.21).
+* Skin::makeExternalLink was removed (deprecated since 1.21).
+* Skin::makeHeadline was removed (deprecated since 1.21).
+* Skin::makeImageLink was removed (deprecated since 1.21).
+* Skin::makeMediaLinkFile was removed (deprecated since 1.21).
+* Skin::makeMediaLinkObj was removed (deprecated since 1.21).
+* Skin::makeSelfLinkObj was removed (deprecated since 1.21).
+* Skin::makeThumbLink2 was removed (deprecated since 1.21).
+* Skin::makeThumbLinkObj was removed (deprecated since 1.21).
+* Skin::normaliseSpecialPage was removed (deprecated since 1.21).
+* Skin::normalizeSubpageLink was removed (deprecated since 1.21).
+* Skin::processResponsiveImages was removed (deprecated since 1.21).
+* Skin::revComment was removed (deprecated since 1.21).
+* Skin::revDeleteLink was removed (deprecated since 1.21).
+* Skin::revDeleteLinkDisabled was removed (deprecated since 1.21).
+* Skin::revUserLink was removed (deprecated since 1.21).
+* Skin::revUserTools was removed (deprecated since 1.21).
+* Skin::specialLink was removed (deprecated since 1.21).
+* Skin::splitTrail was removed (deprecated since 1.21).
+* Skin::titleAttrib was removed (deprecated since 1.21).
+* Skin::tocIndent was removed (deprecated since 1.21).
+* Skin::tocLine was removed (deprecated since 1.21).
+* Skin::tocLineEnd was removed (deprecated since 1.21).
+* Skin::tocList was removed (deprecated since 1.21).
+* Skin::tocUnindent was removed (deprecated since 1.21).
+* Skin::tooltip was removed (deprecated since 1.21).
+* Skin::tooltipAndAccesskeyAttribs was removed (deprecated since 1.21).
+* Skin::userTalkLink was removed (deprecated since 1.21).
+* Skin::userToolLinksRedContribs was removed (deprecated since 1.21).
+* wikidiff3 is now the default and only PHP diff engine. It provides improved diff
+  performance on complex changes. $wgExternalDiffEngine = 'wikidiff3' therefore
+  makes no difference now. Users are still recommended to use wikidiff2 if possible,
+  though.
+* User::addNewUserLogEntry() was deprecated.
+* User::addNewUserLogEntryAutoCreate() was deprecated.
+* User::isPasswordReminderThrottled() was deprecated.
+* Bot-oriented parameters to Special:UserLogin (wpCookieCheck, wpSkipCookieCheck)
+  were removed.
+* Installer can now be customized without patching MediaWiki code, see
+  mw-config/overrides/README for details.
+
+=== Compatibility ===
+
+MediaWiki 1.27 requires PHP 5.5.9 or later. There is experimental support for
+HHVM 3.6.5 or later.
+
+MySQL is the recommended DBMS. PostgreSQL or SQLite can also be used, but
+support for them is somewhat less mature. There is experimental support for
+Oracle and Microsoft SQL Server.
+
+The supported versions are:
+
+* MySQL 5.0.3 or later
+* PostgreSQL 8.3 or later
+* SQLite 3.3.7 or later
+* Oracle 9.0.1 or later
+* Microsoft SQL Server 2005 (9.00.1399)
+
+=== Upgrading ===
+
+1.27 has several database changes since 1.26, and will not work without schema
+updates. Note that due to changes to some very large tables like the revision
+table, the schema update may take quite long (minutes on a medium sized site,
+many hours on a large site).
+
+If upgrading from before 1.11, and you are using a wiki as a commons
+repository, make sure that it is updated as well. Otherwise, errors may arise
+due to database schema changes.
+
+If upgrading from before 1.7, you may want to run refreshLinks.php to ensure
+new database fields are filled with data.
+
+If you are upgrading from MediaWiki 1.4.x or earlier, you should upgrade to
+1.5 first. The upgrade script maintenance/upgrade1_5.php has been removed
+with MediaWiki 1.21.
+
+Don't forget to always back up your database before upgrading!
+
+See the file UPGRADE for more detailed upgrade instructions.
+
+For notes on 1.26.x and older releases, see HISTORY.
+
+
 = MediaWiki 1.26 =
 
 == MediaWiki 1.26.2 ==
diff --git a/RELEASE-NOTES-1.27 b/RELEASE-NOTES-1.27
deleted file mode 100644 (file)
index 6c93676..0000000
+++ /dev/null
@@ -1,609 +0,0 @@
-== MediaWiki 1.27 ==
-
-THIS IS NOT A RELEASE YET
-
-MediaWiki 1.27 is an alpha-quality branch and is not recommended for use in
-production.
-
-=== PHP version requirement ===
-As of 1.27, MediaWiki now requires PHP 5.5.9 or higher (see Compatibility
-section). Additionally, the following PHP extensions are required:
-* ctype
-* iconv
-* json
-* mbstring (new requirement in 1.27)
-* xml
-The following PHP extensions are strongly recommended:
-* openssl
-
-=== Configuration changes in 1.27 ===
-* $wgAllowMicrodataAttributes and $wgAllowRdfaAttributes were removed,
-  now always enabled. If you use RDFa on your wiki, you now have to explicitly
-  set $wgHtml5Version to 'HTML+RDFa 1.0' or 'XHTML+RDFa 1.0'.
-* $wgUseLinkNamespaceDBFields was removed.
-* Deprecated $wgResourceLoaderMinifierStatementsOnOwnLine and
-  $wgResourceLoaderMinifierMaxLineLength, because there was little value in
-  making the behavior configurable. The default values (`false` for the former,
-  1000 for the latter) are now hard-coded.
-* $wgDebugDumpSqlLength was removed (deprecated in 1.24).
-* $wgDebugDBTransactions was removed (deprecated in 1.20).
-* $wgUseXVO has been removed, as it provides functionality only used by
-  custom Wikimedia patches against Squid 2.x that probably noone uses in
-  production anymore. There is now $wgUseKeyHeader that provides similar
-  functionality but instead of the MediaWiki-specific X-Vary-Options header,
-  uses the draft Key header standard.
-* $wgScriptExtension (and support for '.php5' entry points) was removed. See the
-  deprecation notice in the release notes for version 1.25 for advice on how to
-  preserve support for '.php5' entry points via URL rewriting.
-* Password handling via the User object has been deprecated and partially
-  removed, pending the future introduction of AuthManager. In particular:
-** expirePassword(), getPasswordExpireDate(), resetPasswordExpiration(), and
-   getPasswordExpired() have been removed. They were unused outside of core.
-** The mPassword, mNewpassword, mNewpassTime, and mPasswordExpires fields are
-   now private and will be removed in the future.
-** The getPassword() and getTemporaryPassword() methods now throw
-   BadMethodCallException and will be removed in the future.
-** The ability to pass 'password' and 'newpassword' to createNew() has been
-   removed. The only users of it seem to have been using it to set invalid
-   passwords, and so shouldn't be greatly affected.
-** setPassword(), setInternalPassword(), and setNewpassword() have been
-   deprecated, pending the introduction of AuthManager.
-** User::randomPassword() is deprecated in favor of a new method
-   PasswordFactory::generateRandomPasswordString()
-** User::getPasswordFactory() is deprecated, callers should just create a
-   PasswordFactory themselves.
-** A new constructor, User::newSystemUser(), has been added to simplify the
-   creation of passwordless "system" users for logged actions.
-* $wgMaxSquidPurgeTitles was removed.
-* $wgAjaxWatch was removed. This is now enabled by default.
-* $wgUseInstantCommons now hotlinks Commons images by default instead of
-  downloading originals and thumbnailing them locally. This allows wikis to save
-  on CPU and bandwidth while reducing time to first byte for pages, even without
-  a thumbnail handler. See $wgForeignFileRepos documentation for tweaks.
-* (T27397) WebP is enabled by default as an uploadable filetype.
-* (T48998) $wgArticlePath must now be either a full url, or start with a "/".
-* $wgRateLimitLog was removed; use $wgDebugLogGroups['ratelimit'] instead.
-* Deprecated API formats dbg, txt, and yaml have been removed.
-* CLDRPluralRule* classes have been replaced with
-  wikimedia/cldr-plural-rule-parser.
-* Removed $wgProfilePerHost, $wgUDPProfilerHost, $wgUDPProfilerPort,
-  $wgUDPProfilerFormatString, $wgStatsMethod, $wgAggregateStatsID,
-  $wgStatsFormatString, and $wgProfileCallTree (deprecated since 1.20).
-* For proper operation of LocalIdLookup with shared user tables, ensure that
-  $wgSharedDB and $wgSharedTables are properly set even on the "central" wiki
-  that all others are sharing from and that $wgLocalDatabases is set to the
-  full list of sharing wikis on all those wikis.
-* Massive overhaul to session handling:
-** $wgSessionsInObjectCache is no longer supported and must be true, due to
-   MediaWiki\Session\SessionManager. $wgSessionHandler is similarly no longer
-   used.
-** ObjectCacheSessionHandler is removed, replaced with
-   MediaWiki\Session\PhpSessionHandler.
-** PHP session handling in general ($_SESSION, session_id(), and so on) is
-   deprecated. Use MediaWiki\Session\SessionManager instead. A new config
-   variable, $wgPHPSessionHandling, is available to cause use of $_SESSION to
-   issue a deprecation warning or to cause most PHP session handling to throw
-   exceptions.
-** Deprecated UserSetCookies hook. Session-handling extensions should generally
-   be creating a custom subclass of CookieSessionProvider. Other extensions
-   messing with cookies can no longer count on user data being saved in cookies
-   versus other methods.
-** Deprecated UserLoadFromSession hook, extensions should create a
-   MediaWiki\Session\SessionProvider.
-** The User cannot be loaded from session until after Setup.php completes.
-   Attempts to do so will be ignored and the User will remain unloaded.
-** CSRF tokens may be fetched from the MediaWiki\Session\Session, which uses
-   the MediaWiki\Session\Token class.
-* MediaWiki will now auto-create users as necessary, removing the need for
-  extensions to do so. An 'autocreateaccount' right is added to allow
-  auto-creation when 'createaccount' is not granted to all users.
-* Deprecated AuthPluginAutoCreate hook in favor of LocalUserCreated.
-* Most cookie-handling methods in User are deprecated.
-* $wgAllowAsyncCopyUploads and $CopyUploadAsyncTimeout were removed. This was an
-  experimental feature that has never worked.
-* Login and createaccount tokens now vary by timestamp.
-* LoginForm::getLoginToken() and LoginForm::getCreateaccountToken()
-  return a MediaWiki\Session\Token, and tokens must be checked using that
-  class's methods.
-* $wgEnotifUseJobQ was removed and the job queue is always used.
-* The functionality of the ApiSandbox extension has been merged into core. The
-  extension should no longer be used.
-* $wgPreloadJavaScriptMwUtil was removed (deprecated in 1.26).
-  Extensions, skins, gadgets and scripts that use the mediawiki.util module must
-  express a dependency on it.
-* $wgIncludeLegacyJavaScript, deprecated in MediaWiki 1.26, now defaults false.
-  Extensions, skins, gadgets and scripts that need the mediawiki.legacy.wikibits
-  module should express a dependency on it.
-* Removed configuration option $wgCopyrightIcon (deprecated since 1.18). Use
-  $wgFooterIcons['copyright']['copyright'] instead.
-* If the openssl and mcrypt PHP extensions are both unavailable, secure
-  session storage (used for login) will raise an exception. This exception may
-  be bypassed by setting $wgSessionInsecureSecrets = true.
-* Massive overhaul to authentication:
-** AuthPlugin and AuthPluginUser are deprecated.
-** LoginForm and associated templates are deprecated. Extensions which called
-   static LoginForm methods should be converted into authentication providers.
-** The following hooks are deprecated:
-*** AbortAutoAccount (create a MediaWiki\Auth\PreAuthenticationProvider instead)
-*** AbortLogin (create a MediaWiki\Auth\PreAuthenticationProvider instead)
-*** AbortNewAccount (create a MediaWiki\Auth\PreAuthenticationProvider instead)
-*** AddNewAccount (use LocalUserCreated instead)
-*** AuthPluginSetup (create a MediaWiki\Auth\PrimaryAuthenticationProvider instead)
-*** ChangePasswordForm (use AuthChangeFormFields instead, or security levels)
-*** LoginUserMigrated (create a MediaWiki\Auth\PreAuthenticationProvider instead)
-*** UserCreateForm (create a MediaWiki\Auth\AuthenticationProvider of some type instead)
-*** UserLoginForm (create a MediaWiki\Auth\AuthenticationProvider of some type instead)
-** The following hooks are removed:
-*** AbortChangePassword
-*** LoginPasswordResetMessage
-*** PrefsPasswordAudit
-** The UserLoginComplete hook will no longer be called for all logins, only for
-   those via the web UI. Use UserLoggedIn if you need to do something on all
-   logins.
-** $wgRequirePasswordforEmailChange is removed.
-
-=== New features in 1.27 ===
-* $wgDataCenterUpdateStickTTL was also added. This decides how long a user
-  sticks to the primary DC (via cookies) after they make changes to the site.
-* Added a new hook, 'UserMailerTransformContent', to transform the contents
-  of an email. This is similar to the EmailUser hook but applies to all mail
-  sent via UserMailer.
-* Added a new hook, 'UserMailerTransformMessage', to transform the contents
-  of an emai after MIME encoding.
-* Added a new hook, 'UserMailerSplitTo', to control which users have to be
-  emailed separately (ie. there is a single address in the To: field) so
-  user-specific changes to the email can be applied safely.
-* $wgCdnMaxageLagged was added, which limits the CDN cache TTL
-  when any load balancer uses a DB that is lagged beyond the 'max lag'
-  setting in the relevant section of $wgLBFactoryConf.
-* User::newSystemUser() may be used to simplify the creation of passwordless
-  "system" users for logged actions from scripts and extensions.
-* Extensions can now return detailed error information via the API when
-  preventing user actions using 'getUserPermissionsErrors' and similar hooks
-  by using ApiMessage instances instead of strings for the $result value.
-* $wgAPIMaxLagThreshold was added to limit bot changes when databases lag
-  becomes too high.
-* Skins and extensions can now use FlexBox mixins (.flex-display(@display: flex)
-  and .flex(@grow: 1, @shrink: 1, @width: auto, @order: 1)) in Less to create
-  cross-browser-compatible FlexBox rules. Users will still need to add fallback
-  float rules or the like for compatibility with IE9- separately.
-* Added MWTimestamp::getTimezoneString() which returns the localized timezone
-  string, if available. To localize this string, see the comments of
-  $wgLocaltimezone in includes/DefaultSettings.php.
-* Added CentralIdLookup, a service that allows extensions needing a concept of
-  "central" users to get that without having to know about specific central
-  authentication extensions.
-* $wgMaxUserDBWriteDuration added to limit huge user-generated transactions.
-  Regular web request transactions that takes longer than this are aborted.
-* Added a new hook, 'TitleMoveCompleting', which runs before a page move is
-  committed.
-* $wgCdnReboundPurgeDelay was added to provide secondary delayed purges of URLs
-  from CDN to mitigate DB replication lag and WAN cache purge lag.
-* (T49162) Installer will default to setting CACHE_ACCEL as the main cache type
-  if it is available.
-* It is now possible to patrol file uploads (both for new files and new versions
-  of existing files). Special:NewFiles has gained an option to filter by patrol
-  status. This functionality can be disabled using $wgUseFilePatrol.
-* MediaWiki\Session infrastructure allows for easier use of session mechanisms
-  other than the usual cookies.
-** SessionMetadata and SessionCheckInfo hooks allow for setting and checking
-   custom session metadata.
-* Added MWGrants and associated configuration settings $wgGrantPermissions and
-  $wgGrantPermissionGroups to hold configuration for authentication features
-  such as OAuth that want to allow restricting the user rights a user may make
-  use of.
-** If you're already using the OAuth extension, these new variables are
-   identical to (and will replace) $wgMWOAuthGrantPermissions and
-   $wgMWOAuthGrantPermissionGroups.
-* Added MWRestrictions as a class to check restrictions on a WebRequest, e.g.
-  to assert that the request comes from a particular IP range.
-* Added bot passwords, a rights-restricted login mechanism for API-using bots.
-* Whitelisted the following HTML attributes for all elements in wikitext:
-  aria-describedby, aria-flowto, aria-label, aria-labelledby, aria-owns.
-* Removed "presentation" restriction on the HTML role attribute in wikitext.
-  All values are now allowed for the role attribute.
-* $wgContentHandlers now also supports callbacks to create an instance of the
-  appropriate ContentHandler subclass.
-* Added $wgAuthenticationTokenVersion, which if non-null prevents the
-  user_token database field from being exposed in cookies. Setting this would
-  be a good idea, but will log out all current sessions.
-* $wgEventRelayerConfig was added, for managing PubSub event relay configuration,
-  specifically for reliable CDN url purges.
-* Requests have unique IDs, equal to the UNIQUE_ID environment variable (when
-  MediaWiki is behind Apache+mod_unique_id or something similar) or a randomly-
-  generated 24-character string. This request ID is used to annotate log records
-  and error messages. It is available client-side via mw.config.get( 'wgRequestId' ).
-  The request ID supplants exception IDs. Accordingly, MWExceptionHandler::getLogId()
-  is deprecated.
-* (T33313) Add a preference for watching uploads by default, also applies
-  to API-based upload tools.
-* $wgJpegPixelFormat was added to override chroma subsampling for JPEG image
-  thumbnails created via ImageMagick. Defaults to 'yuv420', providing bandwidth
-  savings versus the previous behavior on many files.
-* MediaWiki\Auth infrastructure (called "AuthManager") allows for more flexible
-  configuration of multiple authentication pieces that was possible with
-  AuthPlugin. For example, it's now easy to plug in second-factor
-  authentication, or add additional checks to the login process, or to support
-  multiple login methods at once, or to support non-password-based login methods.
-** Providers are configured via the global setting $wgAuthManagerConfig.
-** A global, $wgDisableAuthManager, is temporarily available to disable
-   AuthManager until extensions are ready to support it.
-** New hook, AuthChangeFormFields, to adjust the form fields on
-   AuthManager-related special pages.
-** New hook, AuthManagerLoginAuthenticateAudit, for additional logging of
-   AuthManager-related authentication requests.
-** New hook, ChangeAuthenticationDataAudit, for additional logging of
-   AuthManager-related authentication data changes.
-** New hook, SecuritySensitiveOperationStatus, to work with the new mechanism
-   for requiring a recent login before taking security-sensitive operations
-   like changing a password.
-** Two new globals, $wgChangeCredentialsBlacklist and $wgRemoveCredentialsBlacklist
-   can be used to prevent the web UI and the API changing certain authentication data.
-* The file upload dialog (available if you install WikiEditor or VisualEditor)
-  can now be configured using $wgUploadDialog.
-
-=== External library changes in 1.27 ===
-
-==== Upgraded external libraries ====
-* Updated oojs/oojs-ui from v0.12.12 to v0.13.3.
-* Updated composer/semver from v1.0.0 to v1.2.0.
-* Updated liuggio/statsd-php-client to 1.0.18.
-* Updated QUnit from v1.18.0 to v1.22.0.
-
-==== New external libraries ====
-* Added wikimedia/base-convert v1.0.1.
-* Added wikimedia/cldr-plural-rule-parser v1.0.0.
-* Added wikimedia/relpath v1.0.3.
-* Added wikimedia/running-stat v1.1.0.
-* Added wikimedia/php-session-serializer v1.0.3.
-
-==== Removed and replaced external libraries ====
-
-=== Bug fixes in 1.27 ===
-* Special:Upload will now display correct maximum allowed file size when running
-  under HHVM (T116347).
-* (T54077) The APIEditBeforeSave hook will once again give only the content of
-  the section being edited, rather than the whole revision. This reverts the
-  change made in MediaWiki 1.22.
-
-=== Action API changes in 1.27 ===
-* Added list=allrevisions.
-* generator=recentchanges now has the option to generate revids.
-* ApiPageSet::setRedirectMergePolicy() was added. This allows generator
-  modules to define how generator data for a redirect source gets merged
-  into the redirect destination.
-* prop=imageinfo&iiprop=uploadwarning will no longer include the possibility of
-  "was-deleted" warning.
-* Added difftotextpst to query=revisions which preforms a pre-save transform on
-  the text before diffing it.
-* Deprecated formats dbg, txt, and yaml have been removed.
-* (T47988) The protect log event details now use new-style formatting.
-* The following response properties from action=login are deprecated, and may
-  be removed in the future: lgtoken, cookieprefix, sessionid. Clients should
-  handle cookies to properly manage session state.
-* action=login transparently allows login using bot passwords. Clients should
-  merely need to change the username and password used after setting up a bot
-  password.
-* action=upload no longer understands statuskey, asyncdownload or leavemessage.
-* Several changes when $wgDisableAuthManager is false:
-** action=login is deprecated for uses other than bot passwords.
-** list=users can now indicate if a missing username is creatable.
-** action=createaccount is changed in a non-backwards-compatible manner.
-** Added action=query&meta=authmanagerinfo.
-** Added action=clientlogin to be used to log into the main account instead of
-   action=login.
-** Added action=linkaccount.
-** Added action=unlinkaccount.
-** Added action=changeauthenticationdata.
-** Added action=removeauthenticationdata.
-** Added action=resetpassword.
-
-=== Action API internal changes in 1.27 ===
-* ApiQueryORM removed.
-* The following classes have been removed:
-** ApiFormatDbg
-** ApiFormatTxt
-** ApiFormatYaml
-* ApiBase::addTokenProperties() was removed (deprecated since 1.24).
-* ApiBase::getFinalPossibleErrors() was removed (deprecated since 1.24).
-* ApiBase::getFinalResultProperties() was removed (deprecated since 1.24).
-* ApiBase::getRequireAtLeastOneParameterErrorMessages() was removed (deprecated since 1.24).
-* ApiBase::getPossibleErrors() was removed (deprecated since 1.24).
-* ApiBase::getRequireMaxOneParameterErrorMessages() was removed (deprecated since 1.24).
-* ApiBase::getRequireOnlyOneParameterErrorMessages() was removed (deprecated since 1.24).
-* ApiBase::getResultProperties() was removed (deprecated since 1.24).
-* ApiBase::getTitleOrPageIdErrorMessage() was removed (deprecated since 1.24).
-* ApiBase::parseErrors() was removed (deprecated since 1.24).
-* ApiQueryBase::titleToKey(), ApiQueryBase::keyToTitle() and
-  ApiQueryBase::keyPartToTitle() all removed (deprecated since 1.24).
-* ApiQueryBase::checkRowCount() was removed (deprecated since 1.24).
-* ApiQueryBase::getDirectionDescription() was removed (deprecated since 1.25).
-* ApiQuery::getGenerators() was removed (deprecated since 1.21).
-* ApiQuery::getModules() was removed (deprecated since 1.21).
-* ApiQuery::getModuleType() was removed (deprecated since 1.21).
-* ApiQuery::setGeneratorContinue() was removed (deprecated since 1.24).
-* ApiMain::getModules() was removed (deprecated since 1.21).
-* ApiBase::getVersion() was removed (deprecated since 1.21).
-* ApiMain::getShowVersions() was removed (deprecated in 1.21).
-* ApiMain::addModule() was removed (deprecated in 1.21).
-* ApiMain::addFormat() was removed (deprecated in 1.21).
-* ApiMain::getFormats() was removed (deprecated in 1.21).
-* ApiPageSet::finishPageSetGeneration() was removed (deprecated in 1.21).
-* ApiCreateAccount is deprecated, and will be removed soon.
-
-=== Languages updated in 1.27 ===
-
-MediaWiki supports over 350 languages. Many localisations are updated
-regularly. Below only new and removed languages are listed, as well as
-changes to languages because of Phabricator reports.
-
-* (T113688) Change default numerals from Gurmukhi to Arabic for Punjabi locale.
-* (T116020) Aliases of magic words in MessagesXx.php are sorted by usage.
-
-=== Other changes in 1.27 ===
-* Added dependency injection (DI) infrastructure, see docs/injection.txt for details.
-  It is planned to incrementally move MediaWiki code towards using DI, using the
-  service locator (SL) pattern as a stepping stone.
-* ProfilerOutputUdp was removed. Note that there is a ProfilerOutputStats class.
-* WikiPage::doDeleteArticleReal() and WikiPage::doDeleteArticle() now
-  ignore the 2nd and 3rd arguments (formerly $id and $commit).
-* Removed "loaderScripts" option from ResourceLoaderFileModule class.
-* Removed ORM-like wrapper added in 1.20.
-* LinkCache::getGoodLinks and LinkCache::getBadLinks were removed
-  (deprecated in 1.26).
-* WikiPage::doQuickEdit() was removed (deprecated since 1.21).
-* Removed SiteObject and SiteArray classes (deprecated in 1.21).
-* MessageBlobStore::getInstance() was removed (deprecated since 1.25).
-* (T84937) Free external links ("autolinked" urls) will now be terminated
-  by &nbsp; and HTML entity encodings of &nbsp, <, and >.
-* (T36948) The default file revert message's timestamp is now in
-  $wgLocaltimezone, instead of UTC.
-* The default name of the 'suppress' group page has been changed from
-  'Project:Oversight' to 'Project:Suppress'.
-* DatabaseBase::resultObject() is now protected (use outside Database classes
-  not necessary since 1.11).
-* Calling ResourceLoaderFileModule::readStyleFiles() without a
-  ResourceLoaderContext instance is deprecated.
-* ResourceLoader::getLessCompiler() now takes an optional parameter of
-  additional LESS variables to set for the compiler.
-* wfBaseConvert() marked as deprecated, use Wikimedia\base_convert() directly
-  instead.
-* Obsolete maintenance scripts clearCacheStats.php and showCacheStats.php
-  were removed. The underlying data is sent to StatsD (see $wgStatsdServer).
-* Removed msg_resource_links database table and associated code.
-* Removed msg_resource database table and associated code.
-* Skin::getNamespaceNotice() was removed.
-* wfIsConfiguredProxy() was removed (deprecated since 1.24).
-* wfDebugTimer() was removed (deprecated since 1.25).
-* wfIsTrustedProxy() was removed (deprecated since 1.24).
-* wfGetIP() was removed (deprecated since 1.19).
-* MWHookException was removed.
-* OutputPage::appendSubtitle() was removed (deprecated since 1.19).
-* OutputPage::loginToUse() was removed (deprecated since 1.19).
-* Article::loadContent() was removed (deprecated since 1.19).
-* User::editToken() was removed (deprecated since 1.19).
-* Removed --force-normal option of dumpBackup.php, as it no longer served
-  any useful purpose since 1.22.
-* The functions processOption() and processArgs() on the BackupDumper and
-  TextPassDumper classes have been removed.
-* The maintenance/backupTextPass.inc file was deleted. You should include
-  maintenance/dumpTextPass.php instead.
-* WikiPage::getUsedTemplates() was removed (deprecated since 1.19).
-* wfEmptyMsg() was removed (deprecated since 1.18).
-* OutputPage::permissionRequired() was removed (deprecated since 1.18).
-* OutputPage::blockedPage() was removed (deprecated since 1.18).
-* User::getSkin() was removed (deprecated since 1.18).
-* OutputPage::includeJQuery() was removed (deprecated since 1.17).
-* WikiPage::updateRestrictions() was removed (deprecated since 1.19).
-* WikiPage::testPreSaveTransform() was removed (deprecated since 1.19).
-* LogPage::logName() was removed (deprecated since 1.19).
-* LogPage::logHeader() was removed (deprecated since 1.19).
-* wfCheckLimits() was removed (deprecated since 1.24).
-* Linker::makeKnownLinkObj() was removed (deprecated since 1.16).
-* Linker::makeLinkObj() was removed (deprecated since 1.16).
-* wfMsgForContentNoTrans() was removed (deprecated since 1.18).
-* ChangesList::usePatrol was removed (deprecated since 1.22).
-* wfMsgNoTrans() was removed (deprecated since 1.18).
-* Linker::makeImageLink2 was removed (deprecated since 1.20).
-* Title::userIsWatching() was removed (deprecated since 1.20).
-* Removed WaitForSlave maintenance script; use SELECT MASTER_POS_WAIT()
-  database function directly instead.
-* wfMsg() was removed (deprecated since 1.18).
-* wfMsgForContent() was removed (deprecated since 1.18).
-* wfMsgReal() was removed (deprecated since 1.18).
-* wfMsgGetKey() was removed (deprecated since 1.18).
-* wfMsgHtml() was removed (deprecated since 1.18).
-* wfMsgWikiHtml() was removed (deprecated since 1.18).
-* wfMsgExt() was removed (deprecated since 1.18).
-* Language::armourMath() was removed (deprecated since 1.22).
-* LanguageConverter::armourMath() was removed (deprecated since 1.22).
-* FakeConverter::armourMath() was removed (deprecated since 1.22).
-* The unused jquery.validate ResourceLoader module was removed.
-* FileRepo::getRootUrl() was removed (deprecated since 1.20).
-* User::generateToken() was removed (deprecated since 1.20).
-* WikiPage::getRawText() was removed (deprecated since 1.21).
-* ParserOutput::hasCustomDataUpdates() was removed (deprecated since 1.25).
-* ParserOutput::addSecondaryDataUpdate() was removed (deprecated since 1.25).
-* ParserOutput::getSecondaryDataUpdates() was removed (deprecated since 1.25).
-* Gallery images with multiple caption pipes no longer concatenate them all
-  together but instead pick the final one, similar to image syntax.
-* XML-like parser tags (such as <gallery>), when unclosed, will be left unparsed
-  rather than consume everything until the end of the page.
-* New maintenance script resetUserEmail.php allows sysadmins to reset user emails in case
-  a user forgot password/account was stolen.
-* wfCheckEntropy() was removed (deprecated in 1.27).
-* Browser support for Internet Explorer 8 lowered from Grade A to Grade C.
-* ContentHandler::supportsCategories method added. Default is true.
-  CategoryMembershipChangeJob updates are skipped for content that
-  does not support categories.
-* wikidiff difference engine is no longer supported, anyone still using it are encouraged
-  to upgrade to wikidiff2 which is actively maintained and has better package availability.
-* Database logic was removed from WatchedItem and a WatchedItemStore was created:
-** WatchedItem::IGNORE_USER_RIGHTS and WatchedItem::CHECK_USER_RIGHTS were deprecated.
-   User::IGNORE_USER_RIGHTS and User::CHECK_USER_RIGHTS were introduced.
-** WatchedItem::fromUserTitle was deprecated in favour of the constructor.
-** WatchedItem::resetNotificationTimestamp was deprecated.
-** WatchedItem::batchAddWatch was deprecated.
-** WatchedItem::addWatch was deprecated.
-** WatchedItem::removeWatch was deprecated.
-** WatchedItem::isWatched was deprecated.
-** WatchedItem::duplicateEntries was deprecated.
-** EmailNotification::updateWatchlistTimestamp was deprecated.
-** User::getWatchedItem was removed.
-* Unit tests don't work with external PHPUnit anymore, Composer is now the only supported
-  way. Run `composer install` to install it and other dev dependencies to run unit tests.
-* wl_id field added to the watchlist table.
-* Revision::getRawText() was removed (deprecated since 1.21).
-* WikiPage::replaceSection() was removed (deprecated since 1.21).
-* Article::replaceSection() was removed (deprecated since 1.21).
-* Language::getLangObj() was removed (deprecated since 1.24).
-* Language::getLanguageName() was removed (deprecated since 1.20).
-* Language::getLanguageNames() was removed (deprecated since 1.20).
-* Language::getTranslatedLanguageNames() was removed (deprecated since 1.20).
-* Language::specialPage() was removed (deprecated since 1.24).
-* MediaWikiTestCase::assertException() was removed (deprecated since 1.22).
-* OutputPage::getHeadItems() was removed (deprecated since 1.24).
-* OutputPage::getScript() was removed (deprecated since 1.24).
-* OutputPage::out() was removed (deprecated since 1.22).
-* OutputPage::setAllowedModules() was removed (deprecated since 1.24).
-* UserrightsPage::makeGroupNameListForLog() was removed (deprecated since 1.21).
-* MediaWikiSite::newFromGlobalId() was removed (deprecated since 1.21).
-* Title::newFromRedirect() was removed (deprecated since 1.21).
-* Skin::commonPrintStylesheet() was removed (deprecated since 1.22).
-* Skin::getCommonStylePath() was removed (deprecated since 1.24).
-* Skin::newFromKey() was removed (deprecated since 1.24).
-* Skin::getUsableSkins() was removed (deprecated since 1.23).
-* LoadBalancer::pickRandom() was removed (deprecated in 1.21).
-* Article::getUndoText() and WikiPage::getUndoText were removed (deprecated since
-  1.21).
-* DifferenceEngine::setText() was removed (deprecated in 1.21).
-* Title::newFromRedirectArray() was removed (deprecated in 1.21).
-* UserMailer::send() no longer accepts $replyto as the 5th argument and $contentType
-  as the 6th. These must be passed in the options array now.
-* Title::newFromRedirectRecurse() was removed (deprecated in 1.21).
-* Skin::accesskey was removed (deprecated since 1.21).
-* Skin::blockLink was removed (deprecated since 1.21).
-* Skin::buildRollbackLink was removed (deprecated since 1.21).
-* Skin::emailLink was removed (deprecated since 1.21).
-* Skin::formatComment was removed (deprecated since 1.21).
-* Skin::formatHiddenCategories was removed (deprecated since 1.21).
-* Skin::formatLinksInComment was removed (deprecated since 1.21).
-* Skin::formatRevisionSize was removed (deprecated since 1.21).
-* Skin::formatSize was removed (deprecated since 1.21).
-* Skin::formatTemplates was removed (deprecated since 1.21).
-* Skin::generateTOC was removed (deprecated since 1.21).
-* Skin::getInternalLinkAttributes was removed (deprecated since 1.21).
-* Skin::getInternalLinkAttributesObj was removed (deprecated since 1.21).
-* Skin::getInterwikiLinkAttributes was removed (deprecated since 1.21).
-* Skin::getInvalidTitleDescription was removed (deprecated since 1.21).
-* Skin::getLinkColour was removed (deprecated since 1.21).
-* Skin::getRevDeleteLink was removed (deprecated since 1.21).
-* Skin::getRollbackEditCount was removed (deprecated since 1.21).
-* Skin::makeBrokenImageLinkObj was removed (deprecated since 1.21).
-* Skin::makeCommentLink was removed (deprecated since 1.21).
-* Skin::makeExternalImage was removed (deprecated since 1.21).
-* Skin::makeExternalLink was removed (deprecated since 1.21).
-* Skin::makeHeadline was removed (deprecated since 1.21).
-* Skin::makeImageLink was removed (deprecated since 1.21).
-* Skin::makeMediaLinkFile was removed (deprecated since 1.21).
-* Skin::makeMediaLinkObj was removed (deprecated since 1.21).
-* Skin::makeSelfLinkObj was removed (deprecated since 1.21).
-* Skin::makeThumbLink2 was removed (deprecated since 1.21).
-* Skin::makeThumbLinkObj was removed (deprecated since 1.21).
-* Skin::normaliseSpecialPage was removed (deprecated since 1.21).
-* Skin::normalizeSubpageLink was removed (deprecated since 1.21).
-* Skin::processResponsiveImages was removed (deprecated since 1.21).
-* Skin::revComment was removed (deprecated since 1.21).
-* Skin::revDeleteLink was removed (deprecated since 1.21).
-* Skin::revDeleteLinkDisabled was removed (deprecated since 1.21).
-* Skin::revUserLink was removed (deprecated since 1.21).
-* Skin::revUserTools was removed (deprecated since 1.21).
-* Skin::specialLink was removed (deprecated since 1.21).
-* Skin::splitTrail was removed (deprecated since 1.21).
-* Skin::titleAttrib was removed (deprecated since 1.21).
-* Skin::tocIndent was removed (deprecated since 1.21).
-* Skin::tocLine was removed (deprecated since 1.21).
-* Skin::tocLineEnd was removed (deprecated since 1.21).
-* Skin::tocList was removed (deprecated since 1.21).
-* Skin::tocUnindent was removed (deprecated since 1.21).
-* Skin::tooltip was removed (deprecated since 1.21).
-* Skin::tooltipAndAccesskeyAttribs was removed (deprecated since 1.21).
-* Skin::userTalkLink was removed (deprecated since 1.21).
-* Skin::userToolLinksRedContribs was removed (deprecated since 1.21).
-* wikidiff3 is now the default and only PHP diff engine. It provides improved diff
-  performance on complex changes. $wgExternalDiffEngine = 'wikidiff3' therefore
-  makes no difference now. Users are still recommended to use wikidiff2 if possible,
-  though.
-* User::addNewUserLogEntry() was deprecated.
-* User::addNewUserLogEntryAutoCreate() was deprecated.
-* User::isPasswordReminderThrottled() was deprecated.
-* Bot-oriented parameters to Special:UserLogin (wpCookieCheck, wpSkipCookieCheck)
-  were removed.
-* Installer can now be customized without patching MediaWiki code, see
-  mw-config/overrides/README for details.
-
-== Compatibility ==
-
-MediaWiki 1.27 requires PHP 5.5.9 or later. There is experimental support for
-HHVM 3.6.5 or later.
-
-MySQL is the recommended DBMS. PostgreSQL or SQLite can also be used, but
-support for them is somewhat less mature. There is experimental support for
-Oracle and Microsoft SQL Server.
-
-The supported versions are:
-
-* MySQL 5.0.3 or later
-* PostgreSQL 8.3 or later
-* SQLite 3.3.7 or later
-* Oracle 9.0.1 or later
-* Microsoft SQL Server 2005 (9.00.1399)
-
-== Upgrading ==
-
-1.27 has several database changes since 1.26, and will not work without schema
-updates. Note that due to changes to some very large tables like the revision
-table, the schema update may take quite long (minutes on a medium sized site,
-many hours on a large site).
-
-If upgrading from before 1.11, and you are using a wiki as a commons
-repository, make sure that it is updated as well. Otherwise, errors may arise
-due to database schema changes.
-
-If upgrading from before 1.7, you may want to run refreshLinks.php to ensure
-new database fields are filled with data.
-
-If you are upgrading from MediaWiki 1.4.x or earlier, you should upgrade to
-1.5 first. The upgrade script maintenance/upgrade1_5.php has been removed
-with MediaWiki 1.21.
-
-Don't forget to always back up your database before upgrading!
-
-See the file UPGRADE for more detailed upgrade instructions.
-
-For notes on 1.26.x and older releases, see HISTORY.
-
-== Online documentation ==
-
-Documentation for both end-users and site administrators is available on
-MediaWiki.org, and is covered under the GNU Free Documentation License (except
-for pages that explicitly state that their contents are in the public domain):
-
-       https://www.mediawiki.org/wiki/Documentation
-
-== Mailing list ==
-
-A mailing list is available for MediaWiki user support and discussion:
-
-       https://lists.wikimedia.org/mailman/listinfo/mediawiki-l
-
-A low-traffic announcements-only list is also available:
-
-       https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce
-
-It's highly recommended that you sign up for one of these lists if you're
-going to run a public MediaWiki, so you can be notified of security fixes.
-
-== IRC help ==
-
-There's usually someone online in #mediawiki on irc.freenode.net.
index 4c075fc..d0bb57f 100644 (file)
@@ -54,6 +54,7 @@ changes to languages because of Phabricator reports.
 * [BREAKING CHANGE] $wgExtendedLoginCookies has been removed.  You can
   use or update a custom session provider if needed.
 * Deprecated APIEditBeforeSave hook in favor of EditFilterMergedContent.
+* The 'UploadVerification' hook is deprecated. Use 'UploadVerifyFile' instead.
 
 == Compatibility ==
 
index c02ae62..d994016 100644 (file)
@@ -608,6 +608,7 @@ $wgAutoloadLocalClasses = [
        'InstallerSessionProvider' => __DIR__ . '/includes/installer/InstallerSessionProvider.php',
        'Interwiki' => __DIR__ . '/includes/interwiki/Interwiki.php',
        'InvalidPassword' => __DIR__ . '/includes/password/InvalidPassword.php',
+       'InvalidateUserSesssions' => __DIR__ . '/maintenance/invalidateUserSessions.php',
        'IteratorDecorator' => __DIR__ . '/includes/utils/iterators/IteratorDecorator.php',
        'IuConverter' => __DIR__ . '/languages/classes/LanguageIu.php',
        'JSCompilerContext' => __DIR__ . '/includes/libs/jsminplus.php',
@@ -1486,6 +1487,7 @@ $wgAutoloadLocalClasses = [
        'WantedTemplatesPage' => __DIR__ . '/includes/specials/SpecialWantedtemplates.php',
        'WatchAction' => __DIR__ . '/includes/actions/WatchAction.php',
        'WatchedItem' => __DIR__ . '/includes/WatchedItem.php',
+       'WatchedItemQueryService' => __DIR__ . '/includes/WatchedItemQueryService.php',
        'WatchedItemStore' => __DIR__ . '/includes/WatchedItemStore.php',
        'WatchlistCleanup' => __DIR__ . '/maintenance/cleanupWatchlist.php',
        'WebInstaller' => __DIR__ . '/includes/installer/WebInstaller.php',
index a261449..0e512a6 100644 (file)
@@ -25,7 +25,7 @@
                "ext-xml": "*",
                "liuggio/statsd-php-client": "1.0.18",
                "mediawiki/at-ease": "1.1.0",
-               "oojs/oojs-ui": "0.17.4",
+               "oojs/oojs-ui": "0.17.5",
                "oyejorge/less.php": "1.7.0.10",
                "php": ">=5.5.9",
                "psr/log": "1.0.0",
index c0c01f4..8640228 100644 (file)
@@ -3285,8 +3285,8 @@ added to the descriptor
 &$radio: Boolean, if source type should be shown as radio button
 $selectedSourceType: The selected source type
 
-'UploadVerification': Additional chances to reject an uploaded file. Consider
-using UploadVerifyFile instead.
+'UploadVerification': DEPRECATED! Use UploadVerifyFile instead.
+Additional chances to reject an uploaded file.
 $saveName: (string) destination file name
 $tempName: (string) filesystem path to the temporary file for checks
 &$error: (string) output: message key for message to show if upload canceled by
index 6613db1..ff292cf 100644 (file)
@@ -23,6 +23,7 @@ use SearchEngineFactory;
 use SiteLookup;
 use SiteStore;
 use WatchedItemStore;
+use WatchedItemQueryService;
 use SkinFactory;
 use TitleFormatter;
 use TitleParser;
@@ -502,6 +503,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'WatchedItemStore' );
        }
 
+       /**
+        * @since 1.28
+        * @return WatchedItemQueryService
+        */
+       public function getWatchedItemQueryService() {
+               return $this->getService( 'WatchedItemQueryService' );
+       }
+
        /**
         * @since 1.28
         * @return GenderCache
index b076d07..9ee4236 100644 (file)
@@ -154,6 +154,10 @@ return [
                return $store;
        },
 
+       'WatchedItemQueryService' => function( MediaWikiServices $services ) {
+               return new WatchedItemQueryService( $services->getDBLoadBalancer() );
+       },
+
        'LinkCache' => function( MediaWikiServices $services ) {
                return new LinkCache(
                        $services->getTitleFormatter()
diff --git a/includes/WatchedItemQueryService.php b/includes/WatchedItemQueryService.php
new file mode 100644 (file)
index 0000000..14d6aac
--- /dev/null
@@ -0,0 +1,474 @@
+<?php
+
+use Wikimedia\Assert\Assert;
+
+/**
+ * Class performing complex database queries related to WatchedItems.
+ *
+ * @since 1.28
+ *
+ * @file
+ * @ingroup Watchlist
+ *
+ * @license GNU GPL v2+
+ */
+class WatchedItemQueryService {
+
+       const DIR_OLDER = 'older';
+       const DIR_NEWER = 'newer';
+
+       const INCLUDE_FLAGS = 'flags';
+       const INCLUDE_USER = 'user';
+       const INCLUDE_USER_ID = 'userid';
+       const INCLUDE_COMMENT = 'comment';
+       const INCLUDE_PATROL_INFO = 'patrol';
+       const INCLUDE_SIZES = 'sizes';
+       const INCLUDE_LOG_INFO = 'loginfo';
+
+       // FILTER_* constants are part of public API (are used
+       // in ApiQueryWatchlist class) and should not be changed.
+       // Changing values of those constants will result in a breaking change in the API
+       const FILTER_MINOR = 'minor';
+       const FILTER_NOT_MINOR = '!minor';
+       const FILTER_BOT = 'bot';
+       const FILTER_NOT_BOT = '!bot';
+       const FILTER_ANON = 'anon';
+       const FILTER_NOT_ANON = '!anon';
+       const FILTER_PATROLLED = 'patrolled';
+       const FILTER_NOT_PATROLLED = '!patrolled';
+       const FILTER_UNREAD = 'unread';
+       const FILTER_NOT_UNREAD = '!unread';
+
+       /**
+        * @var LoadBalancer
+        */
+       private $loadBalancer;
+
+       public function __construct( LoadBalancer $loadBalancer ) {
+               $this->loadBalancer = $loadBalancer;
+       }
+
+       /**
+        * @return DatabaseBase
+        * @throws MWException
+        */
+       private function getConnection() {
+               return $this->loadBalancer->getConnection( DB_SLAVE, [ 'watchlist' ] );
+       }
+
+       /**
+        * @param DatabaseBase $connection
+        * @throws MWException
+        */
+       private function reuseConnection( DatabaseBase $connection ) {
+               $this->loadBalancer->reuseConnection( $connection );
+       }
+
+       /**
+        * @param User $user
+        * @param array $options Allowed keys:
+        *        'includeFields'       => string[] RecentChange fields to be included in the result,
+        *                                 self::INCLUDE_* constants should be used
+        *        'filters'             => string[] optional filters to narrow down resulted items
+        *        'namespaceIds'        => int[] optional namespace IDs to filter by
+        *                                 (defaults to all namespaces)
+        *        'allRevisions'        => bool return multiple revisions of the same page if true,
+        *                                 only the most recent if false (default)
+        *        'rcTypes'             => int[] which types of RecentChanges to include
+        *                                 (defaults to all types), allowed values: RC_EDIT, RC_NEW,
+        *                                 RC_LOG, RC_EXTERNAL, RC_CATEGORIZE
+        *        'onlyByUser'          => string only list changes by a specified user
+        *        'notByUser'           => string do not incluide changes by a specified user
+        *        'dir'                 => string in which direction to enumerate, accepted values:
+        *                                 - DIR_OLDER list newest first
+        *                                 - DIR_NEWER list oldest first
+        *        'start'               => string (format accepted by wfTimestamp) requires 'dir' option,
+        *                                 timestamp to start enumerating from
+        *        'end'                 => string (format accepted by wfTimestamp) requires 'dir' option,
+        *                                 timestamp to end enumerating
+        *        'startFrom'           => [ string $rcTimestamp, int $rcId ] requires 'dir' option,
+        *                                 return items starting from the RecentChange specified by this,
+        *                                 $rcTimestamp should be in the format accepted by wfTimestamp
+        *        'watchlistOwner'      => User user whose watchlist items should be listed if different
+        *                                 than the one specified with $user param,
+        *                                 requires 'watchlistOwnerToken' option
+        *        'watchlistOwnerToken' => string a watchlist token used to access another user's
+        *                                 watchlist, used with 'watchlistOwnerToken' option
+        *        'limit'               => int maximum numbers of items to return
+        *        'usedInGenerator'     => bool include only RecentChange id field required by the
+        *                                 generator ('rc_cur_id' or 'rc_this_oldid') if true, or all
+        *                                 id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid')
+        *                                 if false (default)
+        * @return array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ),
+        *         where $recentChangeInfo contains the following keys:
+        *         - 'rc_id',
+        *         - 'rc_namespace',
+        *         - 'rc_title',
+        *         - 'rc_timestamp',
+        *         - 'rc_type',
+        *         - 'rc_deleted',
+        *         Additional keys could be added by specifying the 'includeFields' option
+        */
+       public function getWatchedItemsWithRecentChangeInfo( User $user, array $options = [] ) {
+               $options += [
+                       'includeFields' => [],
+                       'namespaceIds' => [],
+                       'filters' => [],
+                       'allRevisions' => false,
+                       'usedInGenerator' => false
+               ];
+
+               Assert::parameter(
+                       !isset( $options['rcTypes'] )
+                               || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
+                       '$options[\'rcTypes\']',
+                       'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
+               );
+               Assert::parameter(
+                       !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
+                       '$options[\'dir\']',
+                       'must be DIR_OLDER or DIR_NEWER'
+               );
+               Assert::parameter(
+                       !isset( $options['start'] ) && !isset( $options['end'] ) && !isset( $options['startFrom'] )
+                               || isset( $options['dir'] ),
+                       '$options[\'dir\']',
+                       'must be provided when providing any of options: start, end, startFrom'
+               );
+               Assert::parameter(
+                       !isset( $options['startFrom'] )
+                               || ( is_array( $options['startFrom'] ) && count( $options['startFrom'] ) === 2 ),
+                       '$options[\'startFrom\']',
+                       'must be a two-element array'
+               );
+               if ( array_key_exists( 'watchlistOwner', $options ) ) {
+                       Assert::parameterType(
+                               User::class,
+                               $options['watchlistOwner'],
+                               '$options[\'watchlistOwner\']'
+                       );
+                       Assert::parameter(
+                               isset( $options['watchlistOwnerToken'] ),
+                               '$options[\'watchlistOwnerToken\']',
+                               'must be provided when providing watchlistOwner option'
+                       );
+               }
+
+               $tables = [ 'recentchanges', 'watchlist' ];
+               if ( !$options['allRevisions'] ) {
+                       $tables[] = 'page';
+               }
+
+               $db = $this->getConnection();
+
+               $fields = $this->getFields( $options );
+               $conds = $this->getConds( $db, $user, $options );
+               $dbOptions = $this->getDbOptions( $options );
+               $joinConds = $this->getJoinConds( $options );
+
+               $res = $db->select(
+                       $tables,
+                       $fields,
+                       $conds,
+                       __METHOD__,
+                       $dbOptions,
+                       $joinConds
+               );
+
+               $this->reuseConnection( $db );
+
+               $items = [];
+               foreach ( $res as $row ) {
+                       $items[] = [
+                               new WatchedItem(
+                                       $user,
+                                       new TitleValue( (int)$row->rc_namespace, $row->rc_title ),
+                                       $row->wl_notificationtimestamp
+                               ),
+                               $this->getRecentChangeFieldsFromRow( $row )
+                       ];
+               }
+
+               return $items;
+       }
+
+       private function getRecentChangeFieldsFromRow( stdClass $row ) {
+               // This can be simplified to single array_filter call filtering by key value,
+               // once we stop supporting PHP 5.5
+               $allFields = get_object_vars( $row );
+               $rcKeys = array_filter(
+                       array_keys( $allFields ),
+                       function( $key ) {
+                               return substr( $key, 0, 3 ) === 'rc_';
+                       }
+               );
+               return array_intersect_key( $allFields, array_flip( $rcKeys ) );
+       }
+
+       private function getFields( array $options ) {
+               $fields = [
+                       'rc_id',
+                       'rc_namespace',
+                       'rc_title',
+                       'rc_timestamp',
+                       'rc_type',
+                       'rc_deleted',
+                       'wl_notificationtimestamp'
+               ];
+
+               $rcIdFields = [
+                       'rc_cur_id',
+                       'rc_this_oldid',
+                       'rc_last_oldid',
+               ];
+               if ( $options['usedInGenerator'] ) {
+                       if ( $options['allRevisions'] ) {
+                               $rcIdFields = [ 'rc_this_oldid' ];
+                       } else {
+                               $rcIdFields = [ 'rc_cur_id' ];
+                       }
+               }
+               $fields = array_merge( $fields, $rcIdFields );
+
+               if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
+               }
+               if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
+                       $fields[] = 'rc_user_text';
+               }
+               if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
+                       $fields[] = 'rc_user';
+               }
+               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
+                       $fields[] = 'rc_comment';
+               }
+               if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
+               }
+               if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
+               }
+               if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
+               }
+
+               return $fields;
+       }
+
+       private function getConds( DatabaseBase $db, User $user, array $options ) {
+               $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
+               $conds = [ 'wl_user' => $watchlistOwnerId ];
+
+               if ( !$options['allRevisions'] ) {
+                       $conds[] = $db->makeList(
+                               [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
+                               LIST_OR
+                       );
+               }
+
+               if ( $options['namespaceIds'] ) {
+                       $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
+               }
+
+               if ( array_key_exists( 'rcTypes', $options ) ) {
+                       $conds['rc_type'] = array_map( 'intval',  $options['rcTypes'] );
+               }
+
+               $conds = array_merge( $conds, $this->getFilterConds( $user, $options ) );
+
+               $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
+
+               if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
+                       if ( $db->getType() === 'mysql' ) {
+                               // This is an index optimization for mysql
+                               $conds[] = "rc_timestamp > ''";
+                       }
+               }
+
+               $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
+
+               $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
+               if ( $deletedPageLogCond ) {
+                       $conds[] = $deletedPageLogCond;
+               }
+
+               if ( array_key_exists( 'startFrom', $options ) ) {
+                       $conds[] = $this->getStartFromConds( $db, $options );
+               }
+
+               return $conds;
+       }
+
+       private function getWatchlistOwnerId( User $user, array $options ) {
+               if ( array_key_exists( 'watchlistOwner', $options ) ) {
+                       /** @var User $watchlistOwner */
+                       $watchlistOwner = $options['watchlistOwner'];
+                       $ownersToken = $watchlistOwner->getOption( 'watchlisttoken' );
+                       $token = $options['watchlistOwnerToken'];
+                       if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
+                               throw new UsageException(
+                                       'Incorrect watchlist token provided -- please set a correct token in Special:Preferences',
+                                       'bad_wltoken'
+                               );
+                       }
+                       return $watchlistOwner->getId();
+               }
+               return $user->getId();
+       }
+
+       private function getFilterConds( User $user, array $options ) {
+               $conds = [];
+
+               if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
+                       $conds[] = 'rc_minor != 0';
+               } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
+                       $conds[] = 'rc_minor = 0';
+               }
+
+               if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
+                       $conds[] = 'rc_bot != 0';
+               } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
+                       $conds[] = 'rc_bot = 0';
+               }
+
+               if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
+                       $conds[] = 'rc_user = 0';
+               } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
+                       $conds[] = 'rc_user != 0';
+               }
+
+               if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
+                       // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
+                       // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
+                       if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
+                               $conds[] = 'rc_patrolled != 0';
+                       } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
+                               $conds[] = 'rc_patrolled = 0';
+                       }
+               }
+
+               if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
+                       $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
+               } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
+                       // TODO: should this be changed to use Database::makeList?
+                       $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
+               }
+
+               return $conds;
+       }
+
+       private function getStartEndConds( DatabaseBase $db, array $options ) {
+               if ( !isset( $options['start'] ) && ! isset( $options['end'] ) ) {
+                       return [];
+               }
+
+               $conds = [];
+
+               if ( isset( $options['start'] ) ) {
+                       $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
+                       $conds[] = 'rc_timestamp ' . $after . ' ' . $db->addQuotes( $options['start'] );
+               }
+               if ( isset( $options['end'] ) ) {
+                       $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
+                       $conds[] = 'rc_timestamp ' . $before . ' ' . $db->addQuotes( $options['end'] );
+               }
+
+               return $conds;
+       }
+
+       private function getUserRelatedConds( DatabaseBase $db, User $user, array $options ) {
+               if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
+                       return [];
+               }
+
+               $conds = [];
+
+               if ( array_key_exists( 'onlyByUser', $options ) ) {
+                       $conds['rc_user_text'] = $options['onlyByUser'];
+               } elseif ( array_key_exists( 'notByUser', $options ) ) {
+                       $conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] );
+               }
+
+               // Avoid brute force searches (bug 17342)
+               $bitmask = 0;
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $bitmask = Revision::DELETED_USER;
+               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+               }
+               if ( $bitmask ) {
+                       $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
+               }
+
+               return $conds;
+       }
+
+       private function getExtraDeletedPageLogEntryRelatedCond( DatabaseBase $db, User $user ) {
+               // LogPage::DELETED_ACTION hides the affected page, too. So hide those
+               // entirely from the watchlist, or someone could guess the title.
+               $bitmask = 0;
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $bitmask = LogPage::DELETED_ACTION;
+               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
+               }
+               if ( $bitmask ) {
+                       return $db->makeList( [
+                               'rc_type != ' . RC_LOG,
+                               $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
+                       ], LIST_OR );
+               }
+               return '';
+       }
+
+       private function getStartFromConds( DatabaseBase $db, array $options ) {
+               $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
+               list( $rcTimestamp, $rcId ) = $options['startFrom'];
+               $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
+               $rcId = (int)$rcId;
+               return $db->makeList(
+                       [
+                               "rc_timestamp $op $rcTimestamp",
+                               $db->makeList(
+                                       [
+                                               "rc_timestamp = $rcTimestamp",
+                                               "rc_id $op= $rcId"
+                                       ],
+                                       LIST_AND
+                               )
+                       ],
+                       LIST_OR
+               );
+       }
+
+       private function getDbOptions( array $options ) {
+               $dbOptions = [];
+
+               if ( array_key_exists( 'dir', $options ) ) {
+                       $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
+                       $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
+               }
+
+               if ( array_key_exists( 'limit', $options ) ) {
+                       $dbOptions['LIMIT'] = (int)$options['limit'];
+               }
+
+               return $dbOptions;
+       }
+
+       private function getJoinConds( array $options ) {
+               $joinConds = [
+                       'watchlist' => [ 'INNER JOIN',
+                               [
+                                       'wl_namespace=rc_namespace',
+                                       'wl_title=rc_title'
+                               ]
+                       ]
+               ];
+               if ( !$options['allRevisions'] ) {
+                       $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
+               }
+               return $joinConds;
+       }
+
+}
index db2cf86..e2599d1 100644 (file)
@@ -24,6 +24,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * This query action allows clients to retrieve a list of recently modified pages
  * that are part of the logged-in user's watchlist.
@@ -85,96 +87,59 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        }
                }
 
-               $this->addFields( [
-                       'rc_id',
-                       'rc_namespace',
-                       'rc_title',
-                       'rc_timestamp',
-                       'rc_type',
-                       'rc_deleted',
-               ] );
+               $options = [
+                       'dir' => $params['dir'] === 'older'
+                               ? WatchedItemQueryService::DIR_OLDER
+                               : WatchedItemQueryService::DIR_NEWER,
+               ];
 
                if ( is_null( $resultPageSet ) ) {
-                       $this->addFields( [
-                               'rc_cur_id',
-                               'rc_this_oldid',
-                               'rc_last_oldid',
-                       ] );
-
-                       $this->addFieldsIf( [ 'rc_type', 'rc_minor', 'rc_bot' ], $this->fld_flags );
-                       $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid );
-                       $this->addFieldsIf( 'rc_user_text', $this->fld_user );
-                       $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment );
-                       $this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrol );
-                       $this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes );
-                       $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp );
-                       $this->addFieldsIf(
-                               [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
-                               $this->fld_loginfo
-                       );
-               } elseif ( $params['allrev'] ) {
-                       $this->addFields( 'rc_this_oldid' );
+                       $options['includeFields'] = $this->getFieldsToInclude();
                } else {
-                       $this->addFields( 'rc_cur_id' );
+                       $options['usedInGenerator'] = true;
                }
 
-               $this->addTables( [
-                       'recentchanges',
-                       'watchlist',
-               ] );
-
-               $userId = $wlowner->getId();
-               $this->addJoinConds( [ 'watchlist' => [ 'INNER JOIN',
-                       [
-                               'wl_user' => $userId,
-                               'wl_namespace=rc_namespace',
-                               'wl_title=rc_title'
-                       ]
-               ] ] );
-
-               $db = $this->getDB();
-
-               $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'],
-                       $params['start'], $params['end'] );
-               // Include in ORDER BY for uniqueness
-               $this->addWhereRange( 'rc_id', $params['dir'], null, null );
+               if ( $params['start'] ) {
+                       $options['start'] = $params['start'];
+               }
+               if ( $params['end'] ) {
+                       $options['end'] = $params['end'];
+               }
 
                if ( !is_null( $params['continue'] ) ) {
                        $cont = explode( '|', $params['continue'] );
                        $this->dieContinueUsageIf( count( $cont ) != 2 );
-                       $op = ( $params['dir'] === 'newer' ? '>' : '<' );
-                       $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
+                       $continueTimestamp = $cont[0];
                        $continueId = (int)$cont[1];
                        $this->dieContinueUsageIf( $continueId != $cont[1] );
-                       $this->addWhere( "rc_timestamp $op $continueTimestamp OR " .
-                               "(rc_timestamp = $continueTimestamp AND " .
-                               "rc_id $op= $continueId)"
-                       );
+                       $options['startFrom'] = [ $continueTimestamp, $continueId ];
                }
 
-               $this->addWhereFld( 'wl_namespace', $params['namespace'] );
+               if ( $wlowner !== $user ) {
+                       $options['watchlistOwner'] = $wlowner;
+                       $options['watchlistOwnerToken'] = $params['token'];
+               }
 
-               if ( !$params['allrev'] ) {
-                       $this->addTables( 'page' );
-                       $this->addJoinConds( [ 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ] ] );
-                       $this->addWhere( 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG );
+               if ( !is_null( $params['namespace'] ) ) {
+                       $options['namespaceIds'] = $params['namespace'];
+               }
+
+               if ( $params['allrev'] ) {
+                       $options['allRevisions'] = true;
                }
 
                if ( !is_null( $params['show'] ) ) {
                        $show = array_flip( $params['show'] );
 
                        /* Check for conflicting parameters. */
-                       if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
-                               || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
-                               || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
-                               || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
-                               || ( isset( $show['unread'] ) && isset( $show['!unread'] ) )
-                       ) {
+                       if ( $this->showParamsConflicting( $show ) ) {
                                $this->dieUsageMsg( 'show' );
                        }
 
                        // Check permissions.
-                       if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
+                       if ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
+                               || isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] )
+                       ) {
                                if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
                                        $this->dieUsage(
                                                'You need the patrol right to request the patrolled flag',
@@ -183,25 +148,12 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                                }
                        }
 
-                       /* Add additional conditions to query depending upon parameters. */
-                       $this->addWhereIf( 'rc_minor = 0', isset( $show['!minor'] ) );
-                       $this->addWhereIf( 'rc_minor != 0', isset( $show['minor'] ) );
-                       $this->addWhereIf( 'rc_bot = 0', isset( $show['!bot'] ) );
-                       $this->addWhereIf( 'rc_bot != 0', isset( $show['bot'] ) );
-                       $this->addWhereIf( 'rc_user = 0', isset( $show['anon'] ) );
-                       $this->addWhereIf( 'rc_user != 0', isset( $show['!anon'] ) );
-                       $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
-                       $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
-                       $this->addWhereIf( 'rc_timestamp >= wl_notificationtimestamp', isset( $show['unread'] ) );
-                       $this->addWhereIf(
-                               'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp',
-                               isset( $show['!unread'] )
-                       );
+                       $options['filters'] = array_keys( $show );
                }
 
                if ( !is_null( $params['type'] ) ) {
                        try {
-                               $this->addWhereFld( 'rc_type', RecentChange::parseToRCType( $params['type'] ) );
+                               $options['rcTypes'] = RecentChange::parseToRCType( $params['type'] );
                        } catch ( Exception $e ) {
                                ApiBase::dieDebug( __METHOD__, $e->getMessage() );
                        }
@@ -211,74 +163,46 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' );
                }
                if ( !is_null( $params['user'] ) ) {
-                       $this->addWhereFld( 'rc_user_text', $params['user'] );
+                       $options['onlyByUser'] = $params['user'];
                }
                if ( !is_null( $params['excludeuser'] ) ) {
-                       $this->addWhere( 'rc_user_text != ' . $db->addQuotes( $params['excludeuser'] ) );
+                       $options['notByUser'] = $params['excludeuser'];
                }
 
-               // This is an index optimization for mysql, as done in the Special:Watchlist page
-               $this->addWhereIf(
-                       "rc_timestamp > ''",
-                       !isset( $params['start'] ) && !isset( $params['end'] ) && $db->getType() == 'mysql'
-               );
-
-               // Paranoia: avoid brute force searches (bug 17342)
-               if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
-                       if ( !$user->isAllowed( 'deletedhistory' ) ) {
-                               $bitmask = Revision::DELETED_USER;
-                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
-                               $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
-                       } else {
-                               $bitmask = 0;
-                       }
-                       if ( $bitmask ) {
-                               $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" );
-                       }
-               }
-
-               // LogPage::DELETED_ACTION hides the affected page, too. So hide those
-               // entirely from the watchlist, or someone could guess the title.
-               if ( !$user->isAllowed( 'deletedhistory' ) ) {
-                       $bitmask = LogPage::DELETED_ACTION;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
-                       $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
-               } else {
-                       $bitmask = 0;
-               }
-               if ( $bitmask ) {
-                       $this->addWhere( $this->getDB()->makeList( [
-                               'rc_type != ' . RC_LOG,
-                               $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
-                       ], LIST_OR ) );
-               }
-
-               $this->addOption( 'LIMIT', $params['limit'] + 1 );
+               $options['limit'] = $params['limit'] + 1;
 
                $ids = [];
                $count = 0;
-               $res = $this->select( __METHOD__ );
+               $watchedItemQuery = MediaWikiServices::getInstance()->getWatchedItemQueryService();
+               $items = $watchedItemQuery->getWatchedItemsWithRecentChangeInfo( $wlowner, $options );
 
-               foreach ( $res as $row ) {
+               foreach ( $items as list ( $watchedItem, $recentChangeInfo ) ) {
+                       /** @var WatchedItem $watchedItem */
                        if ( ++$count > $params['limit'] ) {
                                // We've reached the one extra which shows that there are
                                // additional pages to be had. Stop here...
-                               $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
+                               $this->setContinueEnumParameter(
+                                       'continue',
+                                       $recentChangeInfo['rc_timestamp'] . '|' . $recentChangeInfo['rc_id']
+                               );
                                break;
                        }
 
                        if ( is_null( $resultPageSet ) ) {
-                               $vals = $this->extractRowInfo( $row );
+                               $vals = $this->extractOutputData( $watchedItem, $recentChangeInfo );
                                $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
+                                       $this->setContinueEnumParameter(
+                                               'continue',
+                                               $recentChangeInfo['rc_timestamp'] . '|' . $recentChangeInfo['rc_id']
+                                       );
                                        break;
                                }
                        } else {
                                if ( $params['allrev'] ) {
-                                       $ids[] = intval( $row->rc_this_oldid );
+                                       $ids[] = intval( $recentChangeInfo['rc_this_oldid'] );
                                } else {
-                                       $ids[] = intval( $row->rc_cur_id );
+                                       $ids[] = intval( $recentChangeInfo['rc_cur_id'] );
                                }
                        }
                }
@@ -295,56 +219,106 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                }
        }
 
-       private function extractRowInfo( $row ) {
+       private function getFieldsToInclude() {
+               $includeFields = [];
+               if ( $this->fld_flags ) {
+                       $includeFields[] = WatchedItemQueryService::INCLUDE_FLAGS;
+               }
+               if ( $this->fld_user || $this->fld_userid ) {
+                       $includeFields[] = WatchedItemQueryService::INCLUDE_USER_ID;
+               }
+               if ( $this->fld_user ) {
+                       $includeFields[] = WatchedItemQueryService::INCLUDE_USER;
+               }
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                       $includeFields[] = WatchedItemQueryService::INCLUDE_COMMENT;
+               }
+               if ( $this->fld_patrol ) {
+                       $includeFields[] = WatchedItemQueryService::INCLUDE_PATROL_INFO;
+               }
+               if ( $this->fld_sizes ) {
+                       $includeFields[] = WatchedItemQueryService::INCLUDE_SIZES;
+               }
+               if ( $this->fld_loginfo ) {
+                       $includeFields[] = WatchedItemQueryService::INCLUDE_LOG_INFO;
+               }
+               return $includeFields;
+       }
+
+       private function showParamsConflicting( array $show ) {
+               return ( isset( $show[WatchedItemQueryService::FILTER_MINOR] )
+                       && isset( $show[WatchedItemQueryService::FILTER_NOT_MINOR] ) )
+               || ( isset( $show[WatchedItemQueryService::FILTER_BOT] )
+                       && isset( $show[WatchedItemQueryService::FILTER_NOT_BOT] ) )
+               || ( isset( $show[WatchedItemQueryService::FILTER_ANON] )
+                       && isset( $show[WatchedItemQueryService::FILTER_NOT_ANON] ) )
+               || ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
+                       && isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) )
+               || ( isset( $show[WatchedItemQueryService::FILTER_UNREAD] )
+                       && isset( $show[WatchedItemQueryService::FILTER_NOT_UNREAD] ) );
+       }
+
+       private function extractOutputData( WatchedItem $watchedItem, array $recentChangeInfo ) {
                /* Determine the title of the page that has been changed. */
-               $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+               $title = Title::makeTitle(
+                       $watchedItem->getLinkTarget()->getNamespace(),
+                       $watchedItem->getLinkTarget()->getDBkey()
+               );
                $user = $this->getUser();
 
                /* Our output data. */
                $vals = [];
-               $type = intval( $row->rc_type );
+               $type = intval( $recentChangeInfo['rc_type'] );
                $vals['type'] = RecentChange::parseFromRCType( $type );
                $anyHidden = false;
 
                /* Create a new entry in the result for the title. */
                if ( $this->fld_title || $this->fld_ids ) {
                        // These should already have been filtered out of the query, but just in case.
-                       if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
+                       if ( $type === RC_LOG && ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) ) {
                                $vals['actionhidden'] = true;
                                $anyHidden = true;
                        }
                        if ( $type !== RC_LOG ||
-                               LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
+                               LogEventsList::userCanBitfield(
+                                       $recentChangeInfo['rc_deleted'],
+                                       LogPage::DELETED_ACTION,
+                                       $user
+                               )
                        ) {
                                if ( $this->fld_title ) {
                                        ApiQueryBase::addTitleInfo( $vals, $title );
                                }
                                if ( $this->fld_ids ) {
-                                       $vals['pageid'] = intval( $row->rc_cur_id );
-                                       $vals['revid'] = intval( $row->rc_this_oldid );
-                                       $vals['old_revid'] = intval( $row->rc_last_oldid );
+                                       $vals['pageid'] = intval( $recentChangeInfo['rc_cur_id'] );
+                                       $vals['revid'] = intval( $recentChangeInfo['rc_this_oldid'] );
+                                       $vals['old_revid'] = intval( $recentChangeInfo['rc_last_oldid'] );
                                }
                        }
                }
 
                /* Add user data and 'anon' flag, if user is anonymous. */
                if ( $this->fld_user || $this->fld_userid ) {
-                       if ( $row->rc_deleted & Revision::DELETED_USER ) {
+                       if ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_USER ) {
                                $vals['userhidden'] = true;
                                $anyHidden = true;
                        }
-                       if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_USER, $user ) ) {
+                       if ( Revision::userCanBitfield(
+                               $recentChangeInfo['rc_deleted'],
+                               Revision::DELETED_USER,
+                               $user
+                       ) ) {
                                if ( $this->fld_userid ) {
-                                       $vals['userid'] = (int)$row->rc_user;
+                                       $vals['userid'] = (int)$recentChangeInfo['rc_user'];
                                        // for backwards compatibility
-                                       $vals['user'] = (int)$row->rc_user;
+                                       $vals['user'] = (int)$recentChangeInfo['rc_user'];
                                }
 
                                if ( $this->fld_user ) {
-                                       $vals['user'] = $row->rc_user_text;
+                                       $vals['user'] = $recentChangeInfo['rc_user_text'];
                                }
 
-                               if ( !$row->rc_user ) {
+                               if ( !$recentChangeInfo['rc_user'] ) {
                                        $vals['anon'] = true;
                                }
                        }
@@ -352,65 +326,73 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
 
                /* Add flags, such as new, minor, bot. */
                if ( $this->fld_flags ) {
-                       $vals['bot'] = (bool)$row->rc_bot;
-                       $vals['new'] = $row->rc_type == RC_NEW;
-                       $vals['minor'] = (bool)$row->rc_minor;
+                       $vals['bot'] = (bool)$recentChangeInfo['rc_bot'];
+                       $vals['new'] = $recentChangeInfo['rc_type'] == RC_NEW;
+                       $vals['minor'] = (bool)$recentChangeInfo['rc_minor'];
                }
 
                /* Add sizes of each revision. (Only available on 1.10+) */
                if ( $this->fld_sizes ) {
-                       $vals['oldlen'] = intval( $row->rc_old_len );
-                       $vals['newlen'] = intval( $row->rc_new_len );
+                       $vals['oldlen'] = intval( $recentChangeInfo['rc_old_len'] );
+                       $vals['newlen'] = intval( $recentChangeInfo['rc_new_len'] );
                }
 
                /* Add the timestamp. */
                if ( $this->fld_timestamp ) {
-                       $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
+                       $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $recentChangeInfo['rc_timestamp'] );
                }
 
                if ( $this->fld_notificationtimestamp ) {
-                       $vals['notificationtimestamp'] = ( $row->wl_notificationtimestamp == null )
+                       $vals['notificationtimestamp'] = ( $watchedItem->getNotificationTimestamp() == null )
                                ? ''
-                               : wfTimestamp( TS_ISO_8601, $row->wl_notificationtimestamp );
+                               : wfTimestamp( TS_ISO_8601, $watchedItem->getNotificationTimestamp() );
                }
 
                /* Add edit summary / log summary. */
                if ( $this->fld_comment || $this->fld_parsedcomment ) {
-                       if ( $row->rc_deleted & Revision::DELETED_COMMENT ) {
+                       if ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_COMMENT ) {
                                $vals['commenthidden'] = true;
                                $anyHidden = true;
                        }
-                       if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) {
-                               if ( $this->fld_comment && isset( $row->rc_comment ) ) {
-                                       $vals['comment'] = $row->rc_comment;
+                       if ( Revision::userCanBitfield(
+                               $recentChangeInfo['rc_deleted'],
+                               Revision::DELETED_COMMENT,
+                               $user
+                       ) ) {
+                               if ( $this->fld_comment && isset( $recentChangeInfo['rc_comment'] ) ) {
+                                       $vals['comment'] = $recentChangeInfo['rc_comment'];
                                }
 
-                               if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
-                                       $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title );
+                               if ( $this->fld_parsedcomment && isset( $recentChangeInfo['rc_comment'] ) ) {
+                                       $vals['parsedcomment'] = Linker::formatComment( $recentChangeInfo['rc_comment'], $title );
                                }
                        }
                }
 
                /* Add the patrolled flag */
                if ( $this->fld_patrol ) {
-                       $vals['patrolled'] = $row->rc_patrolled == 1;
-                       $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
+                       $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] == 1;
+                       $vals['unpatrolled'] = ChangesList::isUnpatrolled( (object)$recentChangeInfo, $user );
                }
 
-               if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
-                       if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
+               if ( $this->fld_loginfo && $recentChangeInfo['rc_type'] == RC_LOG ) {
+                       if ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) {
                                $vals['actionhidden'] = true;
                                $anyHidden = true;
                        }
-                       if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
-                               $vals['logid'] = intval( $row->rc_logid );
-                               $vals['logtype'] = $row->rc_log_type;
-                               $vals['logaction'] = $row->rc_log_action;
-                               $vals['logparams'] = LogFormatter::newFromRow( $row )->formatParametersForApi();
+                       if ( LogEventsList::userCanBitfield(
+                               $recentChangeInfo['rc_deleted'],
+                               LogPage::DELETED_ACTION,
+                               $user
+                       ) ) {
+                               $vals['logid'] = intval( $recentChangeInfo['rc_logid'] );
+                               $vals['logtype'] = $recentChangeInfo['rc_log_type'];
+                               $vals['logaction'] = $recentChangeInfo['rc_log_action'];
+                               $vals['logparams'] = LogFormatter::newFromRow( $recentChangeInfo )->formatParametersForApi();
                        }
                }
 
-               if ( $anyHidden && ( $row->rc_deleted & Revision::DELETED_RESTRICTED ) ) {
+               if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_RESTRICTED ) ) {
                        $vals['suppressed'] = true;
                }
 
@@ -473,16 +455,16 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        'show' => [
                                ApiBase::PARAM_ISMULTI => true,
                                ApiBase::PARAM_TYPE => [
-                                       'minor',
-                                       '!minor',
-                                       'bot',
-                                       '!bot',
-                                       'anon',
-                                       '!anon',
-                                       'patrolled',
-                                       '!patrolled',
-                                       'unread',
-                                       '!unread',
+                                       WatchedItemQueryService::FILTER_MINOR,
+                                       WatchedItemQueryService::FILTER_NOT_MINOR,
+                                       WatchedItemQueryService::FILTER_BOT,
+                                       WatchedItemQueryService::FILTER_NOT_BOT,
+                                       WatchedItemQueryService::FILTER_ANON,
+                                       WatchedItemQueryService::FILTER_NOT_ANON,
+                                       WatchedItemQueryService::FILTER_PATROLLED,
+                                       WatchedItemQueryService::FILTER_NOT_PATROLLED,
+                                       WatchedItemQueryService::FILTER_UNREAD,
+                                       WatchedItemQueryService::FILTER_NOT_UNREAD,
                                ]
                        ],
                        'type' => [
index fbddc8d..e0be24b 100644 (file)
@@ -9,7 +9,8 @@
                        "Utar",
                        "Macofe",
                        "Danny B.",
-                       "LordMsz"
+                       "LordMsz",
+                       "Dvorapa"
                ]
        },
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Dokumentace]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-mailová konference]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Oznámení k API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Chyby a požadavky]\n</div>\n<strong>Stav:</strong> Všechny funkce uvedené na této stránce by měly fungovat, ale API se stále aktivně vyvíjí a může se kdykoli změnit. Upozornění na změny získáte přihlášením se k [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-mailové konferenci mediawiki-api-announce].\n\n<strong>Chybné požadavky:</strong> Pokud jsou do API zaslány chybné požadavky, bude vrácena HTTP hlavička s klíčem „MediaWiki-API-Error“ a hodnota této hlavičky a chybový kód budou nastaveny na stejnou hodnotu. Více informací najdete [[mw:API:Errors_and_warnings|v dokumentaci]].\n\n<strong>Testování:</strong> Pro jednoduché testování požadavků na API zkuste [[Special:ApiSandbox]].",
@@ -74,7 +75,7 @@
        "apihelp-edit-param-text": "Obsah stránky.",
        "apihelp-edit-param-minor": "Malá editace.",
        "apihelp-edit-param-notminor": "Nemalá editace.",
-       "apihelp-edit-param-bot": "Označit tuto editaci jako editaci bota.",
+       "apihelp-edit-param-bot": "Označit tuto editaci jako editaci robota.",
        "apihelp-edit-param-createonly": "Needitovat stránku, pokud již existuje.",
        "apihelp-edit-param-nocreate": "Pokud stránka neexistuje, vrátit chybu.",
        "apihelp-edit-param-watch": "Přidat stránku na seznam sledovaných.",
index 64b6c1f..c580a33 100644 (file)
        "apihelp-expandtemplates-param-text": "변환할 위키텍스트.",
        "apihelp-expandtemplates-paramvalue-prop-wikitext": "확장된 위키텍스트.",
        "apihelp-expandtemplates-paramvalue-prop-parsetree": "입력값의 XML 파서 트리.",
+       "apihelp-expandtemplates-param-generatexml": "XML 구문 분석 트리를 생성합니다. ($1prop=parsetree로 대체함)",
+       "apihelp-expandtemplates-example-simple": "위키텍스트 <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>를 확장합니다.",
        "apihelp-feedcontributions-description": "사용자 기여 피드를 반환합니다.",
        "apihelp-feedcontributions-param-feedformat": "피드 포맷.",
        "apihelp-feedcontributions-param-user": "기여를 읽을 사용자 이름.",
        "apihelp-login-param-name": "계정 이름.",
        "apihelp-login-param-password": "비밀번호.",
        "apihelp-login-param-domain": "도메인 (선택).",
+       "apihelp-login-example-gettoken": "로그인 토큰을 검색합니다.",
        "apihelp-login-example-login": "로그인.",
        "apihelp-logout-description": "로그아웃하고 세션 데이터를 지웁니다.",
        "apihelp-logout-example-logout": "현재 사용자를 로그아웃합니다.",
        "apihelp-options-example-reset": "모든 설정 초기화",
        "apihelp-paraminfo-description": "API 모듈의 정보를 가져옵니다.",
        "apihelp-paraminfo-param-helpformat": "도움말 문자열 포맷.",
+       "apihelp-parse-param-summary": "구문 분석할 요약입니다.",
+       "apihelp-parse-paramvalue-prop-categorieshtml": "분류의 HTML 버전을 제공합니다.",
+       "apihelp-parse-paramvalue-prop-links": "구문 분석된 위키텍스트의 내부 링크를 제공합니다.",
+       "apihelp-parse-paramvalue-prop-templates": "구문 분석된 위키텍스트의 틀을 제공합니다.",
+       "apihelp-parse-paramvalue-prop-images": "구문 분석된 위키텍스트의 그림을 제공합니다.",
+       "apihelp-parse-paramvalue-prop-externallinks": "구문 분석된 위키텍스트의 외부 링크를 제공합니다.",
+       "apihelp-parse-paramvalue-prop-sections": "구문 분석된 위키텍스트의 문단을 제공합니다.",
+       "apihelp-parse-paramvalue-prop-revid": "구문 분석된 페이지의 판 ID를 추가합니다.",
+       "apihelp-parse-paramvalue-prop-displaytitle": "구문 분석된 위키텍스트의 제목을 추가합니다.",
+       "apihelp-parse-paramvalue-prop-iwlinks": "구문 분석된 위키텍스트의 인터위키 링크를 제공합니다.",
+       "apihelp-parse-paramvalue-prop-wikitext": "구문 분석된 위키텍스트 원문을 제공합니다.",
+       "apihelp-parse-paramvalue-prop-properties": "구문 분석된 위키텍스트에 정의된 다양한 속성을 제공합니다.",
+       "apihelp-parse-param-disablelimitreport": "파서 출력에서 제한 보고서(\"NewPP limit report\")를 제외합니다.",
+       "apihelp-parse-param-disablepp": "<var>$1disablelimitreport</var>를 대신 사용합니다.",
+       "apihelp-parse-param-disableeditsection": "파서 출력에서 문단 편집 링크를 제외합니다.",
+       "apihelp-parse-param-disabletidy": "파서 출력에서 HTML 정리(예: tidy)를 수행하지 않습니다.",
+       "apihelp-parse-param-preview": "미리 보기 모드에서 구문 분석을 합니다.",
+       "apihelp-parse-param-sectionpreview": "문단 미리 보기 모드에서 구문 분석을 합니다. (미리 보기 모드도 활성화함)",
+       "apihelp-parse-param-disabletoc": "출력에서 목차를 제외합니다.",
        "apihelp-parse-example-page": "페이지의 구문을 분석합니다.",
        "apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.",
        "apihelp-parse-example-summary": "요약을 구문 분석합니다.",
        "apihelp-query+allcategories-param-prop": "얻고자 하는 속성:",
        "apihelp-query+allcategories-paramvalue-prop-size": "페이지 수를 분류에 추가합니다.",
        "apihelp-query+allfileusages-paramvalue-prop-title": "파일의 제목을 추가합니다.",
+       "apihelp-query+alllinks-paramvalue-prop-title": "링크의 제목을 추가합니다.",
+       "apihelp-query+alllinks-param-namespace": "열거할 이름공간.",
        "apihelp-query+alllinks-param-limit": "반환할 총 항목 수입니다.",
+       "apihelp-query+allmessages-description": "이 사이트에서 반환할 메시지.",
        "apihelp-query+allmessages-example-ipb": "<kbd>ipb-</kbd>로 시작하는 메시지를 보입니다.",
        "apihelp-query+allpages-param-namespace": "열거할 이름공간.",
        "apihelp-query+allredirects-paramvalue-prop-title": "넘겨주기의 제목을 추가합니다.",
+       "apihelp-query+allredirects-param-namespace": "열거할 이름공간.",
+       "apihelp-query+allredirects-param-limit": "반환할 총 항목 수입니다.",
        "apihelp-query+allrevisions-description": "모든 판 표시.",
        "apihelp-query+mystashedfiles-param-limit": "가져올 파일의 갯수.",
        "apihelp-query+alltransclusions-param-namespace": "열거할 이름공간.",
+       "apihelp-query+allusers-description": "등록된 모든 사용자를 열거합니다.",
+       "apihelp-query+allusers-param-witheditsonly": "편집을 한 사용자만 나열합니다.",
+       "apihelp-query+allusers-example-Y": "<kbd>Y</kbd>로 시작하는 사용자를 나열합니다.",
        "apihelp-query+authmanagerinfo-description": "현재의 인증 상태에 대한 정보를 검색합니다.",
        "apihelp-query+backlinks-param-namespace": "열거할 이름공간.",
        "apihelp-query+blocks-description": "차단된 모든 사용자와 IP 주소를 나열합니다.",
        "apihelp-query+blocks-param-end": "나열을 끝낼 타임스탬프",
        "apihelp-query+blocks-param-ids": "나열할 차단 ID 목록 (선택 사항).",
        "apihelp-query+blocks-param-users": "검색할 사용자 목록 (선택 사항).",
+       "apihelp-query+blocks-param-prop": "얻고자 하는 속성:",
+       "apihelp-query+blocks-paramvalue-prop-user": "차단된 사용자의 사용자 이름을 추가합니다.",
+       "apihelp-query+blocks-paramvalue-prop-userid": "차단된 사용자의 사용자 ID를 추가합니다.",
+       "apihelp-query+blocks-paramvalue-prop-by": "차단을 수행하는 사용자의 사용자 이름을 추가합니다.",
+       "apihelp-query+blocks-paramvalue-prop-byid": "차단을 수행하는 사용자의 사용자 ID를 추가합니다.",
+       "apihelp-query+blocks-paramvalue-prop-timestamp": "차단된 시점의 타임스탬프를 추가합니다.",
+       "apihelp-query+blocks-paramvalue-prop-expiry": "차단 만료 시점의 타임스탬프를 추가합니다.",
+       "apihelp-query+blocks-paramvalue-prop-reason": "차단 이유를 추가합니다.",
+       "apihelp-query+blocks-paramvalue-prop-range": "차단에 영향을 받는 IP 주소 대역을 추가합니다.",
        "apihelp-query+categories-param-limit": "반환할 분류의 갯수입니다.",
        "apihelp-query+categorymembers-paramvalue-prop-ids": "페이지 ID를 추가합니다.",
        "apihelp-query+categorymembers-paramvalue-prop-title": "문서의 제목과 이름공간 ID를 추가합니다.",
        "apihelp-query+categorymembers-paramvalue-prop-timestamp": "문서가 포함된 시기의 타임스탬프를 추가합니다.",
        "apihelp-query+categorymembers-param-limit": "반환할 문서의 최대 수입니다.",
+       "apihelp-query+categorymembers-param-startsortkey": "$1starthexsortkey를 대신 사용해 주십시오.",
+       "apihelp-query+categorymembers-param-endsortkey": "$1endhexsortkey를 대신 사용해 주십시오.",
        "apihelp-query+deletedrevs-paraminfo-modes": "{{PLURAL:$1|모드|모드}}: $2",
        "apihelp-query+deletedrevs-param-start": "나열을 시작할 타임스탬프",
        "apihelp-query+deletedrevs-param-end": "나열을 끝낼 타임스탬프",
        "apihelp-query+deletedrevs-param-limit": "나열할 판의 최대 양.",
        "apihelp-query+duplicatefiles-param-limit": "반환할 중복 파일의 수.",
+       "apihelp-query+embeddedin-param-namespace": "열거할 이름공간.",
        "apihelp-query+extlinks-param-limit": "반환할 링크의 수.",
+       "apihelp-query+exturlusage-param-prop": "포함할 정보:",
        "apihelp-query+exturlusage-paramvalue-prop-ids": "문서의 ID를 추가합니다.",
        "apihelp-query+exturlusage-paramvalue-prop-title": "문서의 제목과 이름공간 ID를 추가합니다.",
        "apihelp-query+exturlusage-paramvalue-prop-url": "문서에 사용된 URL을 추가합니다.",
        "apihelp-query+exturlusage-param-namespace": "열거할 문서 이름공간.",
        "apihelp-query+exturlusage-param-limit": "반환할 문서 수.",
+       "apihelp-query+filearchive-description": "삭제된 모든 파일을 순서대로 열거합니다.",
        "apihelp-query+filearchive-paramvalue-prop-mime": "그림의 MIME를 추가합니다.",
        "apihelp-query+filearchive-paramvalue-prop-mediatype": "그림의 미디어 유형을 추가합니다.",
        "apihelp-query+filearchive-paramvalue-prop-metadata": "그림의 버전에 대한 Exif 메타데이터를 나열합니다.",
+       "apihelp-query+filearchive-example-simple": "삭제된 모든 파일의 목록을 표시합니다.",
+       "apihelp-query+filerepoinfo-description": "위키에 구성된 그림 저장소에 대한 메타 정보를 반환합니다.",
        "apihelp-query+filerepoinfo-example-simple": "파일 저장소의 정보를 가져옵니다.",
+       "apihelp-query+fileusage-param-prop": "얻고자 하는 속성:",
+       "apihelp-query+fileusage-paramvalue-prop-pageid": "각 문서의 페이지 ID.",
+       "apihelp-query+fileusage-paramvalue-prop-title": "각 문서의 제목.",
+       "apihelp-query+fileusage-paramvalue-prop-redirect": "문서가 넘겨주기이면 표시합니다.",
+       "apihelp-query+fileusage-param-namespace": "이 이름공간의 문서만 포함합니다.",
+       "apihelp-query+fileusage-param-limit": "반환할 항목 수.",
+       "apihelp-query+fileusage-param-show": "이 기준을 충족하는 항목만 표시합니다:\n;redirect:넘겨주기만 표시합니다.\n;!redirect:넘겨주기가 아닌 항목만 표시합니다.",
+       "apihelp-query+imageinfo-paramvalue-prop-mediatype": "파일의 미디어 유형을 추가합니다.",
+       "apihelp-query+images-param-limit": "반환할 파일 수.",
+       "apihelp-query+imageusage-param-namespace": "열거할 이름공간.",
+       "apihelp-query+imageusage-example-generator": "[[:File:Albert Einstein Head.jpg]]를 이용하여 페이지의 정보를 가져옵니다.",
+       "apihelp-query+info-description": "기본 페이지 정보를 가져옵니다.",
+       "apihelp-query+info-param-prop": "얻고자 하는 추가 속성:",
+       "apihelp-query+info-paramvalue-prop-protection": "각 문서의 보호 수준을 나열합니다.",
+       "apihelp-query+iwbacklinks-param-prefix": "인터위키의 접두사.",
+       "apihelp-query+iwbacklinks-param-title": "검색할 인터위키 링크. <var>$1blprefix</var>와 함께 사용해야 합니다.",
+       "apihelp-query+iwbacklinks-param-limit": "반활한 총 문서 수.",
+       "apihelp-query+iwbacklinks-param-prop": "얻고자 하는 속성:",
+       "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "인터위키의 접두사를 추가합니다.",
+       "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "인터위키의 제목을 추가합니다.",
+       "apihelp-query+iwlinks-paramvalue-prop-url": "전체 URL을 추가합니다.",
+       "apihelp-query+langbacklinks-paramvalue-prop-lllang": "언어 링크의 언어 코드를 추가합니다.",
+       "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "언어 링크의 제목을 추가합니다.",
+       "apihelp-query+langlinks-paramvalue-prop-url": "전체 URL을 추가합니다.",
+       "apihelp-query+langlinks-param-lang": "이 언어 코드의 언어 링크만 반환합니다.",
+       "apihelp-query+linkshere-param-namespace": "이 이름공간의 문서만 포함합니다.",
+       "apihelp-query+linkshere-param-limit": "반환할 항목 수.",
+       "apihelp-query+linkshere-param-show": "이 기준을 충족하는 항목만 표시합니다:\n;redirect:넘겨주기만 표시합니다.\n;!redirect:넘겨주기가 아닌 항목만 표시합니다.",
+       "apihelp-query+pagepropnames-param-limit": "반환할 이름의 최대 수.",
+       "apihelp-query+pageswithprop-param-prop": "포함할 정보:",
+       "apihelp-query+pageswithprop-paramvalue-prop-ids": "페이지 ID를 추가합니다.",
+       "apihelp-query+pageswithprop-paramvalue-prop-title": "문서의 제목과 이름공간 ID를 추가합니다.",
        "apihelp-query+pageswithprop-param-limit": "나타낼 문서의 최대 수입니다.",
        "apihelp-query+pageswithprop-param-dir": "정렬 순서",
        "apihelp-query+prefixsearch-param-search": "문자열 검색",
+       "apihelp-query+prefixsearch-param-namespace": "검색할 이름공간.",
+       "apihelp-query+prefixsearch-param-limit": "반환할 결과의 최대 수",
        "apihelp-query+prefixsearch-param-profile": "검색 프로파일 사용",
+       "apihelp-query+protectedtitles-paramvalue-prop-level": "보호 수준을 추가합니다.",
+       "apihelp-query+recentchanges-param-prop": "추가 정보를 포함합니다:",
+       "apihelp-query+recentchanges-paramvalue-prop-user": "편집에 임할 사용자를 추가하고 IP인 경우 태그합니다.",
+       "apihelp-query+recentchanges-paramvalue-prop-userid": "편집에 임할 사용자를 추가합니다.",
        "apihelp-query+search-param-qiprofile": "쿼리 독립적인 프로파일 사용(순위 알고리즘에 영향있음)",
+       "apihelp-query+tags-param-limit": "나열할 태그의 최대 수.",
+       "apihelp-query+templates-param-limit": "반환할 틀 수.",
+       "apihelp-query+transcludedin-paramvalue-prop-pageid": "각 문서의 페이지 ID.",
+       "apihelp-query+transcludedin-paramvalue-prop-title": "각 문서의 제목.",
+       "apihelp-query+transcludedin-paramvalue-prop-redirect": "문서가 넘겨주기이면 표시합니다.",
+       "apihelp-query+transcludedin-param-namespace": "이 이름공간의 문서만 포함합니다.",
+       "apihelp-query+transcludedin-param-limit": "반환할 항목 수.",
+       "apihelp-query+transcludedin-param-show": "이 기준을 충족하는 항목만 표시합니다:\n;redirect:넘겨주기만 표시합니다.\n;!redirect:넘겨주기가 아닌 항목만 표시합니다.",
+       "apihelp-query+usercontribs-description": "한 사용자의 모든 편집을 가져옵니다.",
+       "apihelp-query+usercontribs-param-limit": "반환할 기여의 최대 수.",
+       "apihelp-query+usercontribs-paramvalue-prop-ids": "페이지 ID와 판 ID를 추가합니다.",
+       "apihelp-query+usercontribs-paramvalue-prop-title": "문서의 제목과 이름공간 ID를 추가합니다.",
+       "apihelp-query+usercontribs-paramvalue-prop-timestamp": "편집의 타임스탬프를 추가합니다.",
+       "apihelp-query+usercontribs-paramvalue-prop-size": "편집의 새로운 크기를 추가합니다.",
+       "apihelp-query+usercontribs-paramvalue-prop-flags": "편집의 플래그를 추가합니다.",
+       "apihelp-query+usercontribs-paramvalue-prop-patrolled": "점검한 편집을 태그합니다.",
+       "apihelp-query+usercontribs-paramvalue-prop-tags": "편집의 태그를 나열합니다.",
+       "apihelp-query+usercontribs-param-tag": "이 태그로 태그된 판만을 나열합니다.",
+       "apihelp-query+usercontribs-param-toponly": "최신 판인 변경 사항만 나열합니다.",
+       "apihelp-query+usercontribs-example-user": "사용자 <kbd>Example</kbd>의 기여를 표시합니다.",
+       "apihelp-query+usercontribs-example-ipprefix": "<kbd>192.0.2.</kbd>로 시작하는 모든 IP 주소의 기여를 표시합니다.",
+       "apihelp-query+userinfo-description": "현재 사용자의 정보를 가져옵니다.",
+       "apihelp-query+userinfo-param-prop": "포함할 정보:",
+       "apihelp-query+userinfo-paramvalue-prop-blockinfo": "현재 사용자가 차단되면 누구에 의해 무슨 이유로 차단되었는지 태그합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-hasmsg": "현재 사용자가 대기 중인 메시지가 있다면 <samp>messages</samp> 태그를 추가합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-groups": "현재 사용자가 소속된 모든 그룹을 나열합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "현재 사용자가 자동으로 소속된 모든 그룹을 나열합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-rights": "현재 사용자가 가진 모든 권한을 나열합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "현재 사용자가 추가 및 제거할 수 있는 그룹을 나열합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-options": "현재 사용자가 설정한 모든 설정을 나열합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-editcount": "현재 사용자의 편집 수를 추가합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-realname": "사용자의 실명을 추가합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-email": "사용자의 이메일 주소와 이메일 인증 날짜를 추가합니다.",
+       "apihelp-query+userinfo-paramvalue-prop-registrationdate": "사용자의 등록 날짜를 추가합니다.",
+       "apihelp-query+userinfo-example-simple": "현재 사용자의 정보를 가져옵니다.",
+       "apihelp-query+userinfo-example-data": "현재 사용자의 추가 정보를 가져옵니다.",
+       "apihelp-query+users-description": "사용자 목록에 대한 정보를 가져옵니다.",
+       "apihelp-query+users-param-prop": "포함할 정보:",
+       "apihelp-query+users-paramvalue-prop-editcount": "사용자의 편집 수를 추가합니다.",
+       "apihelp-query+users-paramvalue-prop-registration": "사용자의 등록 타임스탬프를 추가합니다.",
+       "apihelp-query+users-example-simple": "사용자 <kbd>Example</kbd>의 정보를 반환합니다.",
+       "apihelp-query+watchlist-description": "현재 사용자의 주시목록의 문서의 최근 바뀜을 가져옵니다.",
+       "apihelp-query+watchlist-param-user": "이 사용자의 변경 사항만 나열합니다.",
+       "apihelp-query+watchlist-param-excludeuser": "이 사용자의 변경 사항을 나열하지 않습니다.",
+       "apihelp-query+watchlist-paramvalue-prop-loginfo": "적절한 곳에 로그 정보를 추가합니다.",
+       "apihelp-removeauthenticationdata-description": "현재 사용자의 인증 데이터를 제거합니다.",
+       "apihelp-revisiondelete-description": "판을 삭제하거나 되살립니다.",
        "apihelp-rollback-param-tags": "되돌리기를 적용하기 위해 태그합니다.",
+       "apihelp-stashedit-param-sectiontitle": "새 문단을 위한 제목.",
+       "apihelp-stashedit-param-text": "문서 내용.",
+       "apihelp-stashedit-param-contentmodel": "새 콘텐츠의 콘텐츠 모델.",
+       "apihelp-tag-param-reason": "변경 이유.",
+       "apihelp-tokens-param-type": "요청할 토큰의 종류.",
        "apihelp-unblock-description": "사용자를 차단 해제합니다.",
+       "apihelp-unblock-param-reason": "차단 해제 이유.",
+       "apihelp-userrights-param-user": "사용자 이름.",
+       "apihelp-userrights-param-userid": "사용자 ID.",
        "apihelp-rawfm-description": "출력 데이터, 디버깅 요소를 포함, (HTML에 포함된)JSON형식.",
+       "apihelp-xml-param-includexmlnamespace": "지정하면 XML 이름공간을 추가합니다.",
+       "api-format-title": "미디어위키 API 결과",
        "api-help-title": "미디어위키 API 도움말",
        "api-help-lead": "이 페이지는 자동으로 생성된 미디어위키 API 도움말 문서입니다.\n\n설명 문서 및 예시: https://www.mediawiki.org/wiki/API",
        "api-help-main-header": "메인 모듈",
        "api-help-flag-deprecated": "이 모듈은 사용되지 않습니다.",
+       "api-help-flag-internal": "<strong>이 모듈은 내부용이거나 불안정합니다.</strong> 동작은 예고 없이 변경될 수 있습니다.",
        "api-help-flag-readrights": "이 모듈은 read 권한을 요구합니다.",
        "api-help-flag-writerights": "이 모듈은 write 권한을 요구합니다.",
        "api-help-flag-mustbeposted": "이 모듈은 POST 요청만을 허용합니다.",
+       "api-help-flag-generator": "이 모듈은 생성기로 사용할 수 있습니다.",
        "api-help-source": "출처: $1",
+       "api-help-source-unknown": "소스: <span class=\"apihelp-unknown\">알 수 없음</span>",
        "api-help-license": "라이선스: [[$1|$2]]",
        "api-help-license-noname": "라이선스: [[$1|링크 참조]]",
        "api-help-license-unknown": "라이선스: <span class=\"apihelp-unknown\">알 수 없음</span>",
        "api-help-param-required": "이 변수는 필수 입력 사항입니다.",
        "api-help-datatypes-header": "데이터 유형",
        "api-help-datatypes": "API 요청 내 몇몇 매개변수형에 대해 더 자세히 설명해보겠습니다:\n;boolean\n:Boolean 매개변수들은 HTML 체크박스처럼 동작합니다: 만약 매개변수가 지정되었다면, 값에 상관없이 참의 값으로 여겨집니다. 거짓값은 매개변수 전체를 생략하세요.\n;timestamp\n:타임스탬프들은 여러 형식으로 표현될 수 있으나 ISO 8601 날짜와 시간이 추천됩니다. 모든 시간은 UTC이어야 하며, 포함된 시간대는 모두 무시됩니다.\n:* ISO 8601 날짜와 시간, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (구두점과 <kbd>Z</kbd>는 선택입니다.)\n:* ISO 8601 날짜와 시간과 (무시되는) 소수 초, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (대시, 콜론과 <kbd>Z</kbd>는 선택입니다.)\n:* 미디어위키 형식, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* 일반적인 수 형식 <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (<kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, 또는 <kbd>-<var>##</var></kbd>와 같은 선택적 시간대는 무시됩니다)\n:*RFC 2822 형식 (시간대는 생략될 수 있음), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850 형식 (시간대는 생략될 수 있음), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime 형식, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* 1부터 13자리까지의 숫자로 표현된 1970-01-01T00:00:00Z 부터 흐른 시간(초) (<kbd>0</kbd>을 제외)\n:* 문자열 <kbd>now</kbd>",
+       "api-help-param-type-limit": "유형: 정수 또는 <kbd>max</kbd>",
        "api-help-param-type-integer": "유형: {{PLURAL:$1|1=정수|2=정수 목록}}",
        "api-help-param-type-boolean": "유형: 부울 ([[Special:ApiHelp/main#main/datatypes|자세한 정보]])",
        "api-help-param-list": "{{PLURAL:$1|1=하나의 값|2=값 (\"{{!}}\"로 구분)}}: $2",
+       "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=비어 있어야 함|비어 있을 수 있거나 $2}}",
+       "api-help-param-limit": "$1 초과는 허용되지 않습니다.",
+       "api-help-param-limit2": "$1 초과는 허용되지 않습니다. (봇의 경우 $2)",
+       "api-help-param-integer-min": "{{PLURAL:$1|1=값|2=값들}}은 $2 이상이어야 합니다.",
+       "api-help-param-integer-max": "{{PLURAL:$1|1=값|2=값들}}은 $3 이하여야 합니다.",
+       "api-help-param-integer-minmax": "{{PLURAL:$1|1=값|2=값들}}은 $2와 $3 사이여야 합니다.",
+       "api-help-param-multi-max": "값들의 최대 수는 {{PLURAL:$1|$1}}입니다. (봇의 경우 {{PLURAL:$2|$2}})",
        "api-help-param-default": "기본값: $1",
        "api-help-param-default-empty": "기본값: <span class=\"apihelp-empty\">(비어 있음)</span>",
+       "api-help-param-continue": "더 많은 결과를 이용할 수 있을 때, 계속하려면 이것을 사용하십시오.",
        "api-help-param-no-description": "<span class=\"apihelp-empty\">(설명 없음)</span>",
        "api-help-examples": "{{PLURAL:$1|예시}}:",
        "api-help-permissions": "{{PLURAL:$1|권한}}:",
        "api-help-permissions-granted-to": "{{PLURAL:$1|다음 그룹에 부여됨}}: $2",
+       "api-help-open-in-apisandbox": "<small>[연습장에서 열기]</small>",
        "api-credits": "API 개발자:\n* Roan Kattouw (선임 개발자, 2007년 9월–2009년)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (초기 개발자, 선임 개발자 2006년 9월~2007년 9월)\n* Brad Jorsch (선임 개발자 2013년–현재)\n\n당신의 의견이나 제안, 질문은 mediawiki-api@lists.wikimedia.org 로 보내주시거나,\nhttps://phabricator.wikimedia.org/ 에 버그 신고를 해 주시기 바랍니다.."
 }
index 0fd3b08..432a9e1 100644 (file)
@@ -22,6 +22,7 @@ namespace MediaWiki\Logger\Monolog;
 
 use Kafka\MetaDataFromKafka;
 use Kafka\Produce;
+use Kafka\Protocol\Decoder;
 use MediaWiki\Logger\LoggerFactory;
 use Monolog\Handler\AbstractProcessingHandler;
 use Monolog\Logger;
@@ -68,6 +69,7 @@ class KafkaHandler extends AbstractProcessingHandler {
                'alias' => [], // map from monolog channel to kafka topic
                'swallowExceptions' => false, // swallow exceptions sending records
                'logExceptions' => null, // A PSR3 logger to inform about errors
+               'requireAck' => 0,
        ];
 
        /**
@@ -118,6 +120,10 @@ class KafkaHandler extends AbstractProcessingHandler {
                        $options['logExceptions'] = LoggerFactory::getInstance( $options['logExceptions'] );
                }
 
+               if ( isset( $options['requireAck'] ) ) {
+                       $produce->setRequireAck( $options['requireAck'] );
+               }
+
                return new self( $produce, $options, $level, $bubble );
        }
 
@@ -165,13 +171,42 @@ class KafkaHandler extends AbstractProcessingHandler {
         */
        protected function send() {
                try {
-                       $this->produce->send();
+                       $response = $this->produce->send();
                } catch ( \Kafka\Exception $e ) {
                        $ignore = $this->warning(
                                'Error sending records to kafka: {exception}',
                                [ 'exception' => $e ] );
                        if ( !$ignore ) {
                                throw $e;
+                       } else {
+                               return;
+                       }
+               }
+
+               if ( is_bool( $response ) ) {
+                       return;
+               }
+
+               $errors = [];
+               foreach ( $response as $topicName => $partitionResponse ) {
+                       foreach ( $partitionResponse as $partition => $info ) {
+                               if ( $info['errCode'] === 0 ) {
+                                       // no error
+                                       continue;
+                               }
+                               $errors[] = sprintf(
+                                       'Error producing to %s (errno %d): %s',
+                                       $topicName,
+                                       $info['errCode'],
+                                       Decoder::getError( $info['errCode'] )
+                               );
+                       }
+               }
+
+               if ( $errors ) {
+                       $error = implode( "\n", $errors );
+                       if ( !$this->warning( $error ) ) {
+                               throw new \RuntimeException( $error );
                        }
                }
        }
index dd94192..36ec508 100644 (file)
        "config-install-extension-tables": "Изработка на табели за овозможени додатоци",
        "config-install-mainpage-failed": "Не можев да вметнам главна страница: $1",
        "config-install-done": "<strong>Честитаме!</strong>\nУспешно го воспоставивте МедијаВики.\n\nВоспоставувачот создаде податотека <code>LocalSettings.php</code>.\nТаму се содржат сите ваши нагодувања.\n\nЌе треба да ја преземете и да ја ставите во основата на воспоставката (истата папка во која се наоѓа index.php). Преземањето треба да е започнато автоматски.\n\nАко не ви е понудено преземање, или пак ако сте го откажале, можете да го почнете одново стискајќи на следнава врска:\n\n$3\n\n<strong>Напомена</strong>: Ако ова не го направите сега, податотеката со поставки повеќе нема да биде на достапна.\n\nОткога ќе завршите со тоа, можете да <strong>[$2 влезете на вашето вики]</strong>.",
+       "config-install-done-path": "<strong>Честитаме!</strong>\nГо воспоставивте МедијаВики.\n\nВоспоставувачот создаде податотека <code>LocalSettings.php</code>.\nТаму се содржат сите ваши нагодувања.\n\nЌе треба да ја преземете и да ја ставите во <code>$4</code>. Преземањето треба да е започнато автоматски.\n\nАко не ви е понудено преземање, или пак ако сте го откажале, можете да го почнете одново стискајќи на следнава врска:\n\n$3\n\n<strong>Напомена</strong>: Ако ова не го направите сега, создадената податотека со поставки повеќе нема да биде на достапна, освен ако не ја преземете пред да излезете.\n\nОткога ќе завршите со тоа, можете да <strong>[$2 влезете на вашето вики]</strong>.",
        "config-download-localsettings": "Преземи го <code>LocalSettings.php</code>",
        "config-help": "помош",
        "config-help-tooltip": "стиснете да расклопите",
index fce68bb..fa46a46 100644 (file)
@@ -141,10 +141,8 @@ abstract class Skin extends ContextSource {
        /**
         * @param OutputPage $out
         */
-       function initPage( OutputPage $out ) {
-
+       public function initPage( OutputPage $out ) {
                $this->preloadExistence();
-
        }
 
        /**
@@ -199,30 +197,29 @@ abstract class Skin extends ContextSource {
        /**
         * Preload the existence of three commonly-requested pages in a single query
         */
-       function preloadExistence() {
+       protected function preloadExistence() {
                $titles = [];
 
-               $user = $this->getUser();
-               $title = $this->getRelevantTitle();
-
                // User/talk link
+               $user = $this->getUser();
                if ( $user->isLoggedIn() ) {
                        $titles[] = $user->getUserPage();
                        $titles[] = $user->getTalkPage();
                }
 
                // Check, if the page can hold some kind of content, otherwise do nothing
-               if ( !$title->canExist() ) {
-                       // nothing
-               } elseif ( $title->isTalkPage() ) {
-                       $titles[] = $title->getSubjectPage();
-               } else {
-                       $titles[] = $title->getTalkPage();
+               $title = $this->getRelevantTitle();
+               if ( $title->canExist() ) {
+                       if ( $title->isTalkPage() ) {
+                               $titles[] = $title->getSubjectPage();
+                       } else {
+                               $titles[] = $title->getTalkPage();
+                       }
                }
 
                Hooks::run( 'SkinPreloadExistence', [ &$titles, $this ] );
 
-               if ( count( $titles ) ) {
+               if ( $titles ) {
                        $lb = new LinkBatch( $titles );
                        $lb->setCaller( __METHOD__ );
                        $lb->execute();
index 45d2415..aed2fa6 100644 (file)
@@ -720,7 +720,7 @@ class SpecialSearch extends SpecialPage {
 
                $out .= "<ul class='mw-search-results'>\n";
                while ( $result ) {
-                       $out .= $this->showHit( $result, $terms, ++$pos );
+                       $out .= $this->showHit( $result, $terms, $pos++ );
                        $result = $matches->next();
                }
                $out .= "</ul>\n";
index 5ec49ba..08fbeea 100644 (file)
@@ -353,7 +353,7 @@ abstract class UploadBase {
 
                $error = '';
                if ( !Hooks::run( 'UploadVerification',
-                       [ $this->mDestName, $this->mTempPath, &$error ] )
+                       [ $this->mDestName, $this->mTempPath, &$error ], '1.28' )
                ) {
                        return [ 'status' => self::HOOK_ABORTED, 'error' => $error ];
                }
index bcf278c..6f908cc 100644 (file)
        "special-characters-group-ipa": "صوتية دولية",
        "special-characters-group-symbols": "رموز",
        "special-characters-group-greek": "يونانية",
+       "special-characters-group-greekextended": "يونانية موسعة",
        "special-characters-group-cyrillic": "كيريلية",
        "special-characters-group-arabic": "عربية",
        "special-characters-group-arabicextended": "عربية موسعة",
index 7542b9f..4803cb6 100644 (file)
        "preferences": "ترجیحلر",
        "mypreferences": "ترجیحلر",
        "prefs-edits": "دَییشمه‌لرین سایی:",
-       "prefsnologintext2": "خواهیش اولونور تنظیملرینیزی دَییشمک اوچون گیریش ائدین.",
+       "prefsnologintext2": "لوطفا تنظیملرینیزی دَییشمک اوچون گیریش ائدین.",
        "prefs-skin": "قابیق",
        "skin-preview": "اؤن‌گؤستریش",
        "datedefault": "سئچیم‌سیز",
        "import-options-wrong": "{{PLURAL:$2|جزئیات| جزئیات}} یانلیش: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "وئریلن کؤک صحیفه‌‌سی اعتبارسیز آددیر.",
        "import-rootpage-nosubpage": "آد فضا سی  \"$1\" آنا باسئ ٔآلت صحیفه اوچون اجازه وئرمیر.",
-       "importlogpage": "Ú\86Û\8cخارÛ\8cÙ\84Ù\85ا Ú¯Ù\88Ù\86دÙ\87â\80\8cÙ\84Û\8cÚ¯ی",
+       "importlogpage": "Ú\86Û\8cخارÛ\8cÙ\84Ù\85ا Ú\98Ù\88رÙ\86اÙ\84ی",
        "importlogpagetext": "آیری ویکیلردن، دَییشیکلیک گئچمیشلریله بیرلیک‌ده گتیریلمیش صفحه‌لر.",
        "import-logentry-upload-detail": "{{PLURAL:$1|بیر|$1}} نوسخه ایچری گتیریلدی",
        "import-logentry-interwiki-detail": "$2-دن {{PLURAL:$1|بیر|$1}} نوسخه ایچری گتیریلدی",
index 17863f5..ba0b142 100644 (file)
        "action-managechangetags": "стварэньне і (дэ)актывацыю метак",
        "action-applychangetags": "дадаваньне метак пры рэдагаваньні",
        "action-changetags": "дадаваньне і выдаленьне адвольных метак да асобных вэрсіяў і запісаў у журнале падзеяў",
+       "action-deletechangetags": "выдаленьне метак з базы зьвестак",
        "nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|з апошняга візыту}}",
        "enhancedrc-history": "гісторыя",
index 9588f8f..4ce8b80 100644 (file)
        "group": "Skupina:",
        "group-user": "Uživatelé",
        "group-autoconfirmed": "Automaticky schválení uživatelé",
-       "group-bot": "Boti",
+       "group-bot": "Roboti",
        "group-sysop": "Správci",
        "group-bureaucrat": "Byrokraté",
        "group-suppress": "Utajovatelé",
        "group-all": "(všichni)",
        "group-user-member": "{{GENDER:$1|uživatel|uživatelka|uživatel}}",
        "group-autoconfirmed-member": "automaticky {{GENDER:$1|schválený uživatel|schválená uživatelka|schválený uživatel}}",
-       "group-bot-member": "{{GENDER:$1|bot|botka|bot}}",
+       "group-bot-member": "{{GENDER:$1|robot|robotka}}",
        "group-sysop-member": "{{GENDER:$1|správce|správkyně|správce}}",
        "group-bureaucrat-member": "{{GENDER:$1|byrokrat|byrokratka|byrokrat}}",
        "group-suppress-member": "{{GENDER:$1|utajovatel|utajovatelka|utajovatel}}",
        "recentchanges-feed-description": "Na tomto kanále sledujte poslední změny na {{grammar:6sg|{{SITENAME}}}}.",
        "recentchanges-label-newpage": "Touto editací byla založena nová stránka",
        "recentchanges-label-minor": "Toto je malá editace",
-       "recentchanges-label-bot": "Tuto editaci provedl bot",
+       "recentchanges-label-bot": "Tuto editaci provedl robot",
        "recentchanges-label-unpatrolled": "Tato změna dosud nebyla prověřena",
        "recentchanges-label-plusminus": "Velikost stránky se změnila o tolik bajtů",
        "recentchanges-legend-heading": "<strong>Legenda:</strong>",
        "noscript.css": "/* Zde uvedené CSS bude ovlivňovat uživatele s vypnutým JavaScriptem */",
        "group-autoconfirmed.css": "/* Zde uvedené CSS bude ovlivňovat pouze automaticky schválené uživatele */",
        "group-user.css": "/* Zde uvedené CSS bude ovlivňovat pouze registrované uživatele */",
-       "group-bot.css": "/* Zde uvedené CSS bude ovlivňovat pouze boty */",
+       "group-bot.css": "/* Zde uvedené CSS bude ovlivňovat pouze roboty */",
        "group-sysop.css": "/* Zde uvedené CSS bude ovlivňovat pouze správce */",
        "group-bureaucrat.css": "/* Zde uvedené CSS bude ovlivňovat pouze byrokraty */",
        "common.js": "/* Zde uvedený JavaScript bude použit pro všechny uživatele při načtení každé stránky. */",
        "group-autoconfirmed.js": "/* Zde uvedený JavaScript bude použit pouze pro automaticky schválené uživatele */",
        "group-user.js": "/* Zde uvedený JavaScript bude použit pouze pro registrované uživatele */",
-       "group-bot.js": "/* Zde uvedený JavaScript bude použit pouze pro boty */",
+       "group-bot.js": "/* Zde uvedený JavaScript bude použit pouze pro roboty */",
        "group-sysop.js": "/* Zde uvedený JavaScript bude použit pouze pro správce */",
        "group-bureaucrat.js": "/* Zde uvedený JavaScript bude použit pouze pro byrokraty */",
        "anonymous": "{{PLURAL:$1|anonymního uživatele|anonymních uživatelů}} {{GRAMMAR:2sg|{{SITENAME}}}}",
index e1d48f1..1a8254b 100644 (file)
        "apihelp": "Peştiya APIyi",
        "apihelp-no-such-module": "Modulê \"$1\" çıniyo.",
        "apisandbox": "API qumdor",
+       "apisandbox-fullscreen": "Panela hera kerdışi",
        "apisandbox-submit": "Bıwazê",
        "apisandbox-reset": "Bestere",
        "apisandbox-retry": "Fına",
        "apisandbox-dynamic-parameters": "Parametreya debyayi",
        "apisandbox-dynamic-parameters-add-label": "Parametre dek:",
        "apisandbox-dynamic-parameters-add-placeholder": "Nmaey parametrey",
+       "apisandbox-submit-invalid-fields-title": "Tay çiy ters şı",
        "apisandbox-results": "Neticey",
        "apisandbox-sending-request": "API waştış rışêno...",
        "apisandbox-request-url-label": "URL waştış:",
        "file-info-png-frames": "$1 {{PLURAL:$1|çerçeve|çerçeveyi}}",
        "file-no-thumb-animation": "'''Not: Dılet tekniko limit, gırd agozneya resm de qıckek de animasyoni miyan dı nêbo.'''",
        "file-no-thumb-animation-gif": "'''Not: Dılet tekniko limit, gırd agozneya resm de qıckek de  GIF imaci de animasyon do nêbo.'''",
-       "newimages": "Galeriye Dosyan dê newan",
+       "newimages": "Galeriya dosyanê neweyan",
        "imagelisttext": "Cêr de yew listeyê '''$1''' esto {{PLURAL:$1|dosya|dosyayi}} veçiniya $2.",
        "newimages-summary": "Ena pela xasi dosyayi ke peni de bar biyayeyi mocnane.",
        "newimages-legend": "Avrêc",
index 9e7a396..9650318 100644 (file)
        "portal": "𐌱𐌰𐌿𐍂𐌲𐍃 𐌲𐌰𐍅𐌹",
        "portal-url": "Project:𐌱𐌰𐌿𐍂𐌲𐍃 𐌲𐌰𐍅𐌹",
        "privacy": "𐌲𐌰𐍂𐌴𐌳𐌴𐌹𐌽𐍉𐍃 𐍃𐌿𐌽𐌳𐍂𐍉𐍅𐌹𐍃𐌰𐌽𐌰",
-       "privacypage": "Project:ð\90\8c²ð\90\8c°ð\90\8d\82ð\90\8c´𐌳𐌴𐌹𐌽𐍉𐍃 𐍃𐌿𐌽𐌳𐍂𐍉𐍅𐌹𐍃𐌰𐌽𐌰",
+       "privacypage": "Project:ð\90\8c²ð\90\8c°ð\90\8d\82ð\90\8c°ð\90\8c¹𐌳𐌴𐌹𐌽𐍉𐍃 𐍃𐌿𐌽𐌳𐍂𐍉𐍅𐌹𐍃𐌰𐌽𐌰",
        "retrievedfrom": "𐌲𐌰𐌽𐌿𐌼𐌰𐌽 𐍆𐍂𐌰𐌼 \"$1\"",
        "youhavenewmessages": "𐌸𐌿 𐌷𐌰𐌱𐌹𐍃 $1 ($2).",
        "editsection": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
index 18d80d8..884349d 100644 (file)
        "page_last": "אחרון",
        "histlegend": "בחירת גרסאות להשוואה: סמנו את תיבות האפשרויות של הגרסאות המיועדות להשוואה, והקישו על Enter או על הכפתור למטה.<br />\nמקרא: '''({{int:cur}})''' = השוואה עם הגרסה הנוכחית, '''({{int:last}})''' = השוואה עם הגרסה הקודמת, '''{{int:minoreditletter}}''' = שינוי משני.",
        "history-fieldset-title": "חיפוש בהיסטוריית הדף",
-       "history-show-deleted": "רק ×\9e×\97×\95ק×\95ת",
+       "history-show-deleted": "ער×\99×\9b×\95ת ×\9e×\95סתר×\95ת ×\91×\9c×\91×\93",
        "histfirst": "הישנות ביותר",
        "histlast": "החדשות ביותר",
        "historysize": "({{PLURAL:$1|בייט אחד|$1 בייטים}})",
index 41f08db..3b74420 100644 (file)
        "rightslogtext": "以下は利用者権限の変更記録です。",
        "action-read": "このページの閲覧",
        "action-edit": "このページの編集",
-       "action-createpage": "ページの作成",
-       "action-createtalk": "議論ページの作成",
+       "action-createpage": "ã\81\93ã\81®ã\83\9aã\83¼ã\82¸ã\81®ä½\9cæ\88\90",
+       "action-createtalk": "この議論ページの作成",
        "action-createaccount": "この利用者アカウントの作成",
        "action-autocreateaccount": "この外部利用者アカウントを自動的に作成",
        "action-history": "このページの履歴の閲覧",
        "action-managechangetags": "タグの作成、有効化および無効化",
        "action-applychangetags": "自分の編集にタグを適用する",
        "action-changetags": "個々の版および記録項目への任意のタグの追加と除去",
+       "action-deletechangetags": "データベースからタグの削除",
        "nchanges": "$1 {{PLURAL:$1|回の変更}}",
        "enhancedrc-since-last-visit": "最終閲覧以降 $1 {{PLURAL:$1|件}}",
        "enhancedrc-history": "履歴",
        "upload-too-many-redirects": "そのURLに含まれるリダイレクトが多すぎます",
        "upload-http-error": "HTTP エラー発生: $1",
        "upload-copy-upload-invalid-domain": "このドメインからのアップロードは許可されていません。",
+       "upload-foreign-cant-upload": "このウィキでは、要求された外部ファイルリポジトリにファイルをアップロードできるように設定されていません。",
        "upload-foreign-cant-load-config": "外部ファイルリポジトリへのファイルアップロードに使用される設定の読み込みに失敗しました。",
+       "upload-dialog-disabled": "このウィキでは、このダイアログを使用するファイルのアップロードが無効にされています。",
        "upload-dialog-title": "ファイルをアップロード",
        "upload-dialog-button-cancel": "中止",
        "upload-dialog-button-done": "完了",
        "uploadstash-badtoken": "操作を実行できませんでした。編集するための認証の期限切れが原因である可能性があります。再度試してください。",
        "uploadstash-errclear": "ファイルの消去に失敗しました。",
        "uploadstash-refresh": "ファイルの一覧を更新",
+       "uploadstash-thumbnail": "サムネイルを表示",
        "invalid-chunk-offset": "無効なチャンクオフセット",
        "img-auth-accessdenied": "アクセスが拒否されました",
        "img-auth-nopathinfo": "PATH_INFO が見つかりません。\nサーバーが、この情報を渡すように構成されていません。\nCGI ベースであるため、img_auth に対応できない可能性もあります。\nhttps://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization をご覧ください。",
        "apisandbox-dynamic-parameters-add-placeholder": "引数名",
        "apisandbox-dynamic-error-exists": "引数名 \"$1\" は既に存在しています。",
        "apisandbox-deprecated-parameters": "廃止予定の引数",
+       "apisandbox-fetch-token": "トークンを自動入力します",
        "apisandbox-submit-invalid-fields-title": "いくつかの欄が不正です。",
        "apisandbox-submit-invalid-fields-message": "印が付いている欄を訂正し、再試行してください。",
        "apisandbox-results": "結果",
        "trackingcategories-msg": "追跡用カテゴリ",
        "trackingcategories-name": "メッセージ名",
        "trackingcategories-desc": "カテゴリに入る基準",
+       "restricted-displaytitle-ignored": "ページ名の表示が無視されているページ",
        "restricted-displaytitle-ignored-desc": "実際のページ名と等価でないために <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> が無視されているページ。",
        "noindex-category-desc": "このページは <code><nowiki>__NOINDEX__</nowiki></code> というマジックワードを含んでおり、その印が有効になっている名前空間にあるため、ロボットによる収集の対象になりません。",
        "index-category-desc": "このページは <code><nowiki>__INDEX__</nowiki></code> というマジックワードを含んでおり、その印が有効になっている名前空間にあるため、通常と異なり、ロボットによる収集の対象になります。",
        "exif-copyrighted-false": "著作権情報未設定",
        "exif-photometricinterpretation-0": "黒と白 (白が0)",
        "exif-photometricinterpretation-1": "黒と白(黒が0)",
+       "exif-photometricinterpretation-9": "CIE L*a*b* (ICC エンコード)",
+       "exif-photometricinterpretation-10": "CIE L*a*b* (ITU エンコード)",
+       "exif-photometricinterpretation-32803": "カラーフィルター配列",
+       "exif-photometricinterpretation-34892": "リニア RAW",
        "exif-unknowndate": "不明な日付",
        "exif-orientation-1": "通常",
        "exif-orientation-2": "左右反転",
index dba1a82..befc978 100644 (file)
        "ipbnounblockself": "자기 스스로를 차단 해제할 수 없습니다.",
        "lockdb": "데이터베이스 잠그기",
        "unlockdb": "데이터베이스 잠금 해제",
-       "lockdbtext": "데이터베이스를 잠그면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스를 요구하는 모든 기능이 정지됩니다.\n정말로 잠가야 하는지를 다시 한번 확인해주세요. 관리 작업이 끝난 뒤에는 데이터베이스 잠금을 풀어야 합니다.",
-       "unlockdbtext": "데이터베이스를 잠금 해제하면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스를 요구하는 모든 기능이 복구됩니다.\n정말로 잠금을 해제하려는지를 다시 한번 확인해주세요.",
+       "lockdbtext": "데이터베이스를 잠그면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스에서 변경을 요구하는 모든 기능이 정지됩니다.\n정말로 잠가야 하는지 확인해 주세요. 관리 작업이 끝난 뒤에는 데이터베이스 잠금을 풀어야 합니다.",
+       "unlockdbtext": "데이터베이스를 잠금 해제하면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스에서 변경을 요구하는 모든 기능이 복구됩니다.\n정말로 잠금을 해제해야 하는지 확인해주세요.",
        "lockconfirm": "네, 데이터베이스를 잠급니다.",
        "unlockconfirm": "네, 데이터베이스를 잠금 해제합니다.",
        "lockbtn": "데이터베이스 잠그기",
index f76875c..3d6534a 100644 (file)
        "log-action-filter-managetags-activate": "Активирање на ознаки",
        "log-action-filter-managetags-deactivate": "Деактивирање на ознаки",
        "log-action-filter-move-move": "Преместување без запис врз пренасочувања",
+       "log-action-filter-upload-upload": "Ново подигање",
+       "log-action-filter-upload-overwrite": "Преподигање",
        "authmanager-authplugin-setpass-bad-domain": "Неважечки домен.",
        "authmanager-autocreate-noperm": "Автоматското создавање на сметки не е дозволено.",
        "authmanager-autocreate-exception": "Автоматското создавање на сметки е привремено оневозможено поради претходни грешки.",
index 15e0e8d..ed486ad 100644 (file)
        "upload-http-error": "Ett HTTP-fel uppstod: $1",
        "upload-copy-upload-invalid-domain": "Uppladdning av kopior är inte tillgängligt från denna domän.",
        "upload-foreign-cant-upload": "Denna wiki är inte konfigurerad för att ladda upp filer till det begärda externa filförvaret.",
-       "upload-foreign-cant-load-config": "Misslyckades att läsa in filuppladdningskonfigurationen för det externa filförvaret.",
+       "upload-foreign-cant-load-config": "Misslyckades att läsa in konfigurationen för filuppladdningar till det externa filförvaret.",
        "upload-dialog-disabled": "Filuppladdningar med denna dialogruta har inaktiverats på denna wiki.",
        "upload-dialog-title": "Ladda upp fil",
        "upload-dialog-button-cancel": "Avbryt",
index 400a432..ac7f6c6 100644 (file)
@@ -8,7 +8,8 @@
                        "Kenrick95",
                        "McDutchie",
                        "Rv77ax",
-                       "William Surya Permana"
+                       "William Surya Permana",
+                       "Rachmat.Wahidi"
                ]
        },
        "ooui-outline-control-move-down": "Pindahkan butir ke bawah",
@@ -23,6 +24,8 @@
        "ooui-dialog-process-dismiss": "Tutup",
        "ooui-dialog-process-retry": "Coba lagi",
        "ooui-dialog-process-continue": "Lanjutkan",
+       "ooui-selectfile-button-select": "Pilih berkas",
        "ooui-selectfile-not-supported": "Peilihan berkas tidak didukung",
-       "ooui-selectfile-placeholder": "Tidak ada berkas yang terpilih"
+       "ooui-selectfile-placeholder": "Tidak ada berkas yang terpilih",
+       "ooui-selectfile-dragdrop-placeholder": "Letakkan berkas di sini"
 }
diff --git a/resources/lib/oojs-ui/i18n/inh.json b/resources/lib/oojs-ui/i18n/inh.json
new file mode 100644 (file)
index 0000000..a897ed5
--- /dev/null
@@ -0,0 +1,24 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Adam-Yourist",
+                       "ElizaMag"
+               ]
+       },
+       "ooui-outline-control-move-down": "Элемент Iолохеяккха",
+       "ooui-outline-control-move-up": "Элемент Iолакхеяккха",
+       "ooui-outline-control-remove": "ДIаяккха пункт",
+       "ooui-toolbar-more": "Кхы а",
+       "ooui-toolgroup-expand": "Дукха",
+       "ooui-toolgroup-collapse": "КӀезига",
+       "ooui-dialog-message-accept": "ОК",
+       "ooui-dialog-message-reject": "Эшац",
+       "ooui-dialog-process-error": "Харцахь хилар цхьа хIама",
+       "ooui-dialog-process-dismiss": "ДIакъовла",
+       "ooui-dialog-process-retry": "Кхы цкъа де гIорта",
+       "ooui-dialog-process-continue": "ДIаьхде",
+       "ooui-selectfile-button-select": "Файл хьахаржа",
+       "ooui-selectfile-not-supported": "Файл харжа вIаштаь дац",
+       "ooui-selectfile-placeholder": "Файл хержа яц",
+       "ooui-selectfile-dragdrop-placeholder": "Хьадехьаяккха файл укхаз"
+}
index 1024d2a..8da8ef1 100644 (file)
@@ -8,18 +8,20 @@
                        "Meursault2004"
                ]
        },
-       "ooui-outline-control-move-down": "Pindhahaken butir mangandhap",
-       "ooui-outline-control-move-up": "Pindhah kara menyang dhuwur",
-       "ooui-outline-control-remove": "Busak kara",
-       "ooui-toolbar-more": "Manèh",
-       "ooui-toolgroup-expand": "Manèh",
-       "ooui-toolgroup-collapse": "Suda",
-       "ooui-dialog-message-accept": "Oké",
-       "ooui-dialog-message-reject": "Batal",
+       "ooui-outline-control-move-down": "Lih barang mangisor",
+       "ooui-outline-control-move-up": "Lih barang mandhuwur",
+       "ooui-outline-control-remove": "Buwang barang",
+       "ooui-toolbar-more": "Liyané",
+       "ooui-toolgroup-expand": "Liyané",
+       "ooui-toolgroup-collapse": "Sacukupé",
+       "ooui-dialog-message-accept": "Ha'a",
+       "ooui-dialog-message-reject": "Wurungaké",
        "ooui-dialog-process-error": "Ana sing klèru",
        "ooui-dialog-process-dismiss": "Tutup",
        "ooui-dialog-process-retry": "Jajal manèh",
-       "ooui-dialog-process-continue": "Tutug",
-       "ooui-selectfile-not-supported": "Pilihan berkas ora disokong",
-       "ooui-selectfile-placeholder": "Ora ana berkas sing kapilih"
+       "ooui-dialog-process-continue": "Banjuraké",
+       "ooui-selectfile-button-select": "Pilih barkas",
+       "ooui-selectfile-not-supported": "Barkas pilihan ora disengkuyung",
+       "ooui-selectfile-placeholder": "Ora ana barkas sing dipilih",
+       "ooui-selectfile-dragdrop-placeholder": "Dèkèk barkas ing kéné"
 }
index 94b4687..9954744 100644 (file)
@@ -12,5 +12,6 @@
        "ooui-dialog-message-reject": "Betal bike",
        "ooui-dialog-process-retry": "Dîsa hewl bide",
        "ooui-dialog-process-continue": "Bidomîne",
+       "ooui-selectfile-button-select": "Dosyeyekê hilbijêre",
        "ooui-selectfile-placeholder": "Ti dosye nehatiye hilbijartin"
 }
diff --git a/resources/lib/oojs-ui/i18n/pnb.json b/resources/lib/oojs-ui/i18n/pnb.json
new file mode 100644 (file)
index 0000000..8bd4be8
--- /dev/null
@@ -0,0 +1,21 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Saanvel"
+               ]
+       },
+       "ooui-outline-control-move-down": "شیہ تھلے کرو",
+       "ooui-outline-control-move-up": "شیہ اتے کرو",
+       "ooui-outline-control-remove": "شیہ مٹاؤ",
+       "ooui-toolbar-more": "ہور",
+       "ooui-toolgroup-expand": "ہور",
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "مکاؤ",
+       "ooui-dialog-process-error": "کوئی رپھڑ پے گیا اے۔",
+       "ooui-dialog-process-dismiss": "مکاؤ",
+       "ooui-dialog-process-retry": "فیر کرو",
+       "ooui-dialog-process-continue": "چلاؤ",
+       "ooui-selectfile-button-select": "فائل چنو",
+       "ooui-selectfile-placeholder": "کوئی فائل نئی چنی ہوئی",
+       "ooui-selectfile-dragdrop-placeholder": "فائل ایتھے پاؤ"
+}
index 510e468..598acbc 100644 (file)
@@ -2,7 +2,8 @@
        "@metadata": {
                "authors": [
                        "Lloffiwr",
-                       "Muddyb Blast Producer"
+                       "Muddyb Blast Producer",
+                       "Muddyb"
                ]
        },
        "ooui-outline-control-move-down": "Sogeza kipengee chini",
index 537a9d7..026bbb3 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:52Z
+ * Date: 2016-06-29T13:27:08Z
  */
 ( function ( OO ) {
 
index fdfd3e4..fa5ec0a 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-element-hidden {
        display: none !important;
 .oo-ui-textInputWidget textarea {
        display: block;
        width: 100%;
-       resize: none;
        -webkit-box-sizing: border-box;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
 }
 .oo-ui-textInputWidget textarea {
        overflow: auto;
+       resize: none;
 }
 .oo-ui-textInputWidget [type="search"] {
        -webkit-appearance: textfield;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
+       cursor: default;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
           -moz-user-select: none;
 .oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
        position: absolute;
 }
+.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
+       cursor: pointer;
+}
 .oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
        z-index: 1;
        width: 100%;
 }
-.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
-       cursor: pointer;
-}
 .oo-ui-dropdownWidget:last-child {
        margin-right: 0;
 }
index 05f3838..03380b2 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-element-hidden {
        display: none !important;
@@ -67,7 +67,6 @@
 .oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button {
        padding-left: 0.25em;
        padding-right: 0.25em;
-       color: #333333;
 }
 .oo-ui-buttonElement-frameless.oo-ui-widget-enabled > .oo-ui-buttonElement-button:focus {
        box-shadow: inset 0 0 0 1px #347bff, 0 0 0 1px #347bff;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:focus {
        border-color: #347bff;
-       box-shadow: inset 0 0 0 1px #ffffff;
+       box-shadow: inset 0 0 0 1px #347bff, inset 0 0 0 2px #ffffff;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button {
        color: #ffffff;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:focus {
        border-color: #347bff;
-       box-shadow: inset 0 0 0 1px #ffffff;
+       box-shadow: inset 0 0 0 1px #347bff, inset 0 0 0 2px #ffffff;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
        color: #ffffff;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:focus {
        border-color: #d11d13;
-       box-shadow: inset 0 0 0 1px #ffffff;
+       box-shadow: inset 0 0 0 1px #d11d13, inset 0 0 0 2px #ffffff;
 }
 .oo-ui-clippableElement-clippable {
        -webkit-box-sizing: border-box;
 .oo-ui-textInputWidget textarea {
        display: block;
        width: 100%;
-       resize: none;
        -webkit-box-sizing: border-box;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
 }
 .oo-ui-textInputWidget textarea {
        overflow: auto;
+       resize: none;
 }
 .oo-ui-textInputWidget [type="search"] {
        -webkit-appearance: textfield;
 .oo-ui-textInputWidget input,
 .oo-ui-textInputWidget textarea {
        padding: 0.5em;
-       line-height: 1.275em;
        margin: 0;
        font-size: inherit;
        font-family: inherit;
        color: #000000;
        border: 1px solid #cccccc;
        border-radius: 2px;
-       -webkit-box-sizing: border-box;
-          -moz-box-sizing: border-box;
-               box-sizing: border-box;
+}
+.oo-ui-textInputWidget textarea {
+       line-height: 1.275em;
 }
 .oo-ui-textInputWidget .oo-ui-pendingElement-pending {
        background-color: transparent;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
+       cursor: default;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
           -moz-user-select: none;
 .oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
        position: absolute;
 }
+.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
+       cursor: pointer;
+}
 .oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
        z-index: 1;
        width: 100%;
 }
-.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
-       cursor: pointer;
-}
 .oo-ui-dropdownWidget:last-child {
        margin-right: 0;
 }
index 6836553..e230a49 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:52Z
+ * Date: 2016-06-29T13:27:08Z
  */
 ( function ( OO ) {
 
@@ -2219,7 +2219,7 @@ OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
  * @chainable
  */
 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
-       var i, len, item, event, events, currentIndex,
+       var i, len, item, itemEvent, events, currentIndex,
                itemElements = [];
 
        for ( i = 0, len = items.length; i < len; i++ ) {
@@ -2237,8 +2237,8 @@ OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
                // Add the item
                if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
                        events = {};
-                       for ( event in this.aggregateItemEvents ) {
-                               events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
+                       for ( itemEvent in this.aggregateItemEvents ) {
+                               events[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
                        }
                        item.connect( this, events );
                }
@@ -2271,22 +2271,19 @@ OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
  * @chainable
  */
 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
-       var i, len, item, index, remove, itemEvent;
+       var i, len, item, index, events, itemEvent;
 
        // Remove specific items
        for ( i = 0, len = items.length; i < len; i++ ) {
                item = items[ i ];
                index = this.items.indexOf( item );
                if ( index !== -1 ) {
-                       if (
-                               item.connect && item.disconnect &&
-                               !$.isEmptyObject( this.aggregateItemEvents )
-                       ) {
-                               remove = {};
-                               if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
-                                       remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
+                       if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
+                               events = {};
+                               for ( itemEvent in this.aggregateItemEvents ) {
+                                       events[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
                                }
-                               item.disconnect( this, remove );
+                               item.disconnect( this, events );
                        }
                        item.setElementGroup( null );
                        this.items.splice( index, 1 );
index 750ab66..15d4d44 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:52Z
+ * Date: 2016-06-29T13:27:08Z
  */
 ( function ( OO ) {
 
index 3f1910e..cb5eeda 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
 .oo-ui-popupTool .oo-ui-popupWidget-anchor {
index dd59a23..09730d4 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
 .oo-ui-popupTool .oo-ui-popupWidget-anchor {
index 3b1e15d..97a8fc9 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:52Z
+ * Date: 2016-06-29T13:27:08Z
  */
 ( function ( OO ) {
 
index 0767ab7..68156c7 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
@@ -17,7 +17,6 @@
        cursor:         grab;
 }
 .oo-ui-draggableElement-handle:active {
-       cursor: move;
        cursor: url(images/grabbing.cur );
        cursor: -webkit-grabbing;
        cursor:    -moz-grabbing;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
 }
-.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
-       position: absolute;
-       top: 0;
-       bottom: 0;
-       right: 0;
-       left: 0;
-       -webkit-touch-callout: none;
-       -webkit-user-select: none;
-          -moz-user-select: none;
-           -ms-user-select: none;
-               user-select: none;
-}
-.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
-       display: none;
-}
 .oo-ui-toggleSwitchWidget:last-child {
        margin-right: 0;
 }
 .oo-ui-toggleSwitchWidget.oo-ui-widget-disabled {
        opacity: 0.5;
 }
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover,
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover .oo-ui-toggleSwitchWidget-grip {
+       border-color: #aaaaaa;
+}
 .oo-ui-toggleSwitchWidget-grip {
        top: 0.25em;
        left: 0.25em;
        background-image:         linear-gradient(to bottom, #ffffff 0, #dddddd 100%);
        -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#ffdddddd' )";
 }
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover,
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover .oo-ui-toggleSwitchWidget-grip {
-       border-color: #aaaaaa;
-}
-.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
+.oo-ui-toggleSwitchWidget-glow {
+       position: absolute;
+       top: 0;
+       bottom: 0;
+       right: 0;
+       left: 0;
        border-radius: 1em;
        box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
        -webkit-transition: opacity 250ms ease;
        background-image:    -moz-linear-gradient(top, #b0d9ee 0, #eaf4fa 100%);
        background-image:         linear-gradient(to bottom, #b0d9ee 0, #eaf4fa 100%);
        -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffb0d9ee', endColorstr='#ffeaf4fa' )";
-}
-.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-glow {
-       opacity: 1;
-}
-.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
-       left: 2.25em;
-       margin-left: -2px;
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+          -moz-user-select: none;
+           -ms-user-select: none;
+               user-select: none;
 }
 .oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
-       display: block;
        opacity: 0;
 }
 .oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-grip {
        left: 0.25em;
        margin-left: 0;
 }
+.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-glow {
+       opacity: 1;
+}
+.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
+       left: 2.25em;
+       margin-left: -2px;
+}
 .oo-ui-progressBarWidget {
        max-width: 50em;
        background-color: #ffffff;
 .oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
        position: absolute;
 }
+.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
+       cursor: default;
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+          -moz-user-select: none;
+           -ms-user-select: none;
+               user-select: none;
+}
 .oo-ui-selectFileWidget .oo-ui-selectFileWidget-clearButton {
        z-index: 2;
 }
        display: block;
 }
 .oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget {
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+          -moz-user-select: none;
+           -ms-user-select: none;
+               user-select: none;
+}
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
 .oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget,
 .oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget .oo-ui-buttonElement-button,
 .oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget .oo-ui-buttonElement-button {
index 82d0b5f..048e732 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
@@ -17,7 +17,6 @@
        cursor:         grab;
 }
 .oo-ui-draggableElement-handle:active {
-       cursor: move;
        cursor: url(images/grabbing.cur );
        cursor: -webkit-grabbing;
        cursor:    -moz-grabbing;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
 }
-.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
-       position: absolute;
-       top: 0;
-       bottom: 0;
-       right: 0;
-       left: 0;
-       -webkit-touch-callout: none;
-       -webkit-user-select: none;
-          -moz-user-select: none;
-           -ms-user-select: none;
-               user-select: none;
-}
-.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
-       display: none;
-}
 .oo-ui-toggleSwitchWidget:last-child {
        margin-right: 0;
 }
        content: "";
        display: block;
        position: absolute;
-       top: 0;
-       left: 0;
-       bottom: 0;
-       right: 0;
+       top: 1px;
+       left: 1px;
+       bottom: 1px;
+       right: 1px;
        border: 1px solid transparent;
        border-radius: 1em;
        z-index: 1;
+       -webkit-transition: border-color 100ms;
+          -moz-transition: border-color 100ms;
+               transition: border-color 100ms;
 }
 .oo-ui-toggleSwitchWidget-grip {
        top: 0.35em;
+       min-width: 16px;
        width: 1.2em;
+       min-height: 16px;
        height: 1.2em;
        border-radius: 1.2em;
        background-color: #555555;
 .oo-ui-toggleSwitchWidget-glow {
        display: none;
 }
-.oo-ui-toggleSwitchWidget.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
-       left: 1.9em;
-       margin-left: -2px;
-}
 .oo-ui-toggleSwitchWidget.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-grip {
        left: 0.4em;
        margin-left: 0;
 }
+.oo-ui-toggleSwitchWidget.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
+       left: 1.9em;
+       margin-left: -2px;
+}
 .oo-ui-toggleSwitchWidget.oo-ui-widget-enabled.oo-ui-toggleWidget-on {
        background-color: #347bff;
        border-color: #347bff;
 .oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
        position: absolute;
 }
+.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
+       cursor: default;
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+          -moz-user-select: none;
+           -ms-user-select: none;
+               user-select: none;
+}
 .oo-ui-selectFileWidget .oo-ui-selectFileWidget-clearButton {
        z-index: 2;
 }
        display: block;
 }
 .oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget {
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+          -moz-user-select: none;
+           -ms-user-select: none;
+               user-select: none;
+}
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
 .oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget,
 .oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget .oo-ui-buttonElement-button,
 .oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget .oo-ui-buttonElement-button {
index cc010a6..c4486d3 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:52Z
+ * Date: 2016-06-29T13:27:08Z
  */
 ( function ( OO ) {
 
@@ -374,18 +374,22 @@ OO.ui.mixin.DraggableGroupElement.prototype.reorder = function ( item, newIndex
  * @param {OO.ui.mixin.DraggableElement} item Dragged item
  */
 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
-       this.dragItem = item;
-       this.$element.on( 'dragover', this.onDragOver.bind( this ) );
-       this.$element.addClass( 'oo-ui-draggableGroupElement-dragging' );
+       if ( this.dragItem !== item ) {
+               this.dragItem = item;
+               this.$element.on( 'dragover', this.onDragOver.bind( this ) );
+               this.$element.addClass( 'oo-ui-draggableGroupElement-dragging' );
+       }
 };
 
 /**
  * Unset the current dragged item
  */
 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
-       this.dragItem = null;
-       this.$element.off( 'dragover' );
-       this.$element.removeClass( 'oo-ui-draggableGroupElement-dragging' );
+       if ( this.dragItem ) {
+               this.dragItem = null;
+               this.$element.off( 'dragover' );
+               this.$element.removeClass( 'oo-ui-draggableGroupElement-dragging' );
+       }
 };
 
 /**
@@ -1770,11 +1774,13 @@ OO.ui.BookletLayout.prototype.onStackLayoutVisibleItemChange = function ( page )
 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
        var layout = this;
        if ( !this.scrolling && page ) {
-               page.scrollElementIntoView( { complete: function () {
-                       if ( layout.autoFocus ) {
-                               layout.focus();
+               page.scrollElementIntoView( {
+                       complete: function () {
+                               if ( layout.autoFocus ) {
+                                       layout.focus();
+                               }
                        }
-               } );
+               } );
        }
 };
 
@@ -2278,11 +2284,13 @@ OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
        var layout = this;
        if ( card ) {
-               card.scrollElementIntoView( { complete: function () {
-                       if ( layout.autoFocus ) {
-                               layout.focus();
+               card.scrollElementIntoView( {
+                       complete: function () {
+                               if ( layout.autoFocus ) {
+                                       layout.focus();
+                               }
                        }
-               } );
+               } );
        }
 };
 
index de80299..b55dd25 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-actionWidget.oo-ui-pendingElement-pending {
        background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
@@ -64,8 +64,7 @@
        bottom: 0;
 }
 .oo-ui-dialog-content > .oo-ui-window-foot {
-       overflow: hidden;
-       z-index: 1;
+       z-index: 3;
        bottom: 0;
 }
 .oo-ui-dialog-content > .oo-ui-window-body {
index 5aaedc1..727e874 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:56Z
+ * Date: 2016-06-29T13:27:11Z
  */
 .oo-ui-window {
        background: transparent;
        bottom: 0;
 }
 .oo-ui-dialog-content > .oo-ui-window-foot {
-       overflow: hidden;
-       z-index: 1;
+       z-index: 3;
        bottom: 0;
 }
-.oo-ui-dialog-content > .oo-ui-window-body {
-       outline: 1px solid #aaaaaa;
-}
 .oo-ui-messageDialog-actions-horizontal {
        display: table;
        table-layout: fixed;
@@ -98,6 +94,9 @@
        display: inline;
        white-space: nowrap;
 }
+.oo-ui-messageDialog-content > .oo-ui-window-foot {
+       outline: 1px solid #aaaaaa;
+}
 .oo-ui-messageDialog-title,
 .oo-ui-messageDialog-message {
        display: block;
        border-bottom-width: 0;
 }
 .oo-ui-messageDialog-actions .oo-ui-actionWidget {
-       height: 3.4em;
+       min-height: 3.4em;
        margin-right: 0;
 }
 .oo-ui-messageDialog-actions .oo-ui-actionWidget:last-child {
 }
 .oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
        text-align: center;
-       line-height: 3.4em;
+       line-height: 3.4;
 }
 .oo-ui-messageDialog-actions .oo-ui-actionWidget:hover {
        background-color: rgba(0, 0, 0, 0.05);
index 17bab01..8234b6d 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.4
+ * OOjs UI v0.17.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-05-31T21:50:52Z
+ * Date: 2016-06-29T13:27:08Z
  */
 ( function ( OO ) {
 
@@ -3114,7 +3114,11 @@ OO.ui.ProcessDialog.prototype.initialize = function () {
                .append( this.$errors );
        this.$navigation
                .addClass( 'oo-ui-processDialog-navigation' )
-               .append( this.$safeActions, this.$location, this.$primaryActions );
+               // Note: Order of appends below is important. These are in the order
+               // we want tab to go through them. Display-order is handled entirely
+               // by CSS absolute-positioning. As such, primary actions like "done"
+               // should go first.
+               .append( this.$primaryActions, this.$location, this.$safeActions );
        this.$head.append( this.$navigation );
        this.$foot.append( this.$otherActions );
 };
index 519e39b..6ff2e01 100644 (file)
@@ -113,18 +113,19 @@ function getAccessKeyLabel( element ) {
  * @param {HTMLElement} titleElement Element with the title to update (may be the same as `element`)
  */
 function updateTooltipOnElement( element, titleElement ) {
-       var array = ( mw.msg( 'word-separator' ) + mw.msg( 'brackets' ) ).split( '$1' ),
-               regexp = new RegExp( $.map( array, mw.RegExp.escape ).join( '.*?' ) + '$' ),
-               oldTitle = titleElement.title,
-               rawTitle = oldTitle.replace( regexp, '' ),
-               newTitle = rawTitle,
-               accessKeyLabel = getAccessKeyLabel( element );
-
-       // don't add a title if the element didn't have one before
+       var oldTitle, parts, regexp, newTitle, accessKeyLabel;
+
+       oldTitle = titleElement.title;
        if ( !oldTitle ) {
+               // don't add a title if the element didn't have one before
                return;
        }
 
+       parts = ( mw.msg( 'word-separator' ) + mw.msg( 'brackets' ) ).split( '$1' );
+       regexp = new RegExp( $.map( parts, mw.RegExp.escape ).join( '.*?' ) + '$' );
+       newTitle = oldTitle.replace( regexp, '' );
+       accessKeyLabel = getAccessKeyLabel( element );
+
        if ( accessKeyLabel ) {
                // Should be build the same as in Linker::titleAttrib
                newTitle += mw.msg( 'word-separator' ) + mw.msg( 'brackets', accessKeyLabel );
index 38e5a1f..884ecb6 100644 (file)
                                        } else if ( selected.is( '.suggestions-special' ) ) {
                                                if ( typeof context.config.special.select === 'function' ) {
                                                        // Allow the callback to decide whether to prevent default or not
-                                                       if ( context.config.special.select.call( selected, context.data.$textbox ) === true ) {
+                                                       if ( context.config.special.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) {
                                                                preventDefault = false;
                                                        }
                                                }
                                        } else {
                                                if ( typeof context.config.result.select === 'function' ) {
                                                        // Allow the callback to decide whether to prevent default or not
-                                                       if ( context.config.result.select.call( selected, context.data.$textbox ) === true ) {
+                                                       if ( context.config.result.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) {
                                                                preventDefault = false;
                                                        }
                                                }
                                                                if ( $result.get( 0 ) !== $other.get( 0 ) ) {
                                                                        return;
                                                                }
+                                                               $.suggestions.highlight( context, $result, true );
+                                                               if ( typeof context.config.result.select === 'function' ) {
+                                                                       context.config.result.select.call( $result, context.data.$textbox, 'mouse' );
+                                                               }
                                                                // Don't interfere with special clicks (e.g. to open in new tab)
                                                                if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) {
-                                                                       $.suggestions.highlight( context, $result, true );
-                                                                       if ( typeof context.config.result.select === 'function' ) {
-                                                                               context.config.result.select.call( $result, context.data.$textbox );
-                                                                       }
                                                                        // This will hide the link we're just clicking on, which causes problems
                                                                        // when done synchronously in at least Firefox 3.6 (bug 62858).
                                                                        setTimeout( function () {
                                                                if ( $special.get( 0 ) !== $other.get( 0 ) ) {
                                                                        return;
                                                                }
+                                                               if ( typeof context.config.special.select === 'function' ) {
+                                                                       context.config.special.select.call( $special, context.data.$textbox, 'mouse' );
+                                                               }
                                                                // Don't interfere with special clicks (e.g. to open in new tab)
                                                                if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) {
-                                                                       if ( typeof context.config.special.select === 'function' ) {
-                                                                               context.config.special.select.call( $special, context.data.$textbox );
-                                                                       }
                                                                        // This will hide the link we're just clicking on, which causes problems
                                                                        // when done synchronously in at least Firefox 3.6 (bug 62858).
                                                                        setTimeout( function () {
                                                $.suggestions.keypress( e, context, context.data.keypressed );
                                        } )
                                        .keyup( function ( e ) {
-                                               // Some browsers won't throw keypress() for arrow keys. If we got a keydown and a keyup without a
-                                               // keypress in between, solve it
-                                               if ( context.data.keypressedCount === 0 ) {
+                                               // The keypress event is fired when a key is pressed down and that key normally
+                                               // produces a character value. We also want to handle some keys that don't
+                                               // produce a character value so we also attach to the keydown/keyup events.
+                                               // List of codes sourced from
+                                               // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
+                                               var allowed = [
+                                                       40, // up arrow
+                                                       38, // down arrow
+                                                       27, // escape
+                                                       13, // enter
+                                                       46, // delete
+                                                       8   // backspace
+                                               ];
+                                               if ( context.data.keypressedCount === 0
+                                                       && e.which === context.data.keypressed
+                                                       && $.inArray( e.which, allowed ) !== -1
+                                               ) {
                                                        $.suggestions.keypress( e, context, context.data.keypressed );
                                                }
                                        } )
index df03679..d816335 100755 (executable)
         *  used (header or content).
         */
        mw.widgets.SearchInputWidget = function MwWidgetsSearchInputWidget( config ) {
+               // The parent constructors will detach this from the DOM, and won't
+               // be reattached until after this function is completed. As such
+               // grab a handle here. If no config.$input is passed tracking of
+               // form submissions won't work.
+               var $form = config.$input ? config.$input.closest( 'form' ) : $();
+
                config = $.extend( {
                        type: 'search',
                        icon: 'search',
@@ -36,6 +42,7 @@
                // Initialization
                this.$element.addClass( 'mw-widget-searchInputWidget' );
                this.lookupMenu.$element.addClass( 'mw-widget-searchWidget-menu' );
+               this.lastLookupItems = [];
                if ( !config.pushPending ) {
                        this.pushPending = false;
                }
                        this.performSearchOnClick = config.performSearchOnClick;
                }
                this.setLookupsDisabled( !this.suggestions );
+
+               $form.on( 'submit', function () {
+                       mw.track( 'mw.widgets.SearchInputWidget', {
+                               action: 'submit-form',
+                               numberOfResults: this.lastLookupItems.length,
+                               $form: $form,
+                               inputLocation: this.dataLocation || 'header',
+                               index: this.lastLookupItems.indexOf(
+                                       this.$input.val()
+                               )
+                       } );
+               }.bind( this ) );
        };
 
        /* Setup */
        /**
         * @inheritdoc
         */
-       mw.widgets.SearchInputWidget.prototype.onLookupMenuItemChoose = function ( item ) {
-               var items;
-
-               // get items which was suggested before the input changes
-               items = this.lookupMenu.items;
-
+       mw.widgets.SearchInputWidget.prototype.onLookupMenuItemChoose = function () {
                mw.widgets.SearchInputWidget.parent.prototype.onLookupMenuItemChoose.apply( this, arguments );
 
-               mw.track( 'mw.widgets.SearchInputWidget', {
-                       action: 'click-result',
-                       numberOfResults: items.length,
-                       clickIndex: items.indexOf( item ) + 1
-               } );
-
                if ( this.performSearchOnClick ) {
                        this.$element.closest( 'form' ).submit();
                }
        };
 
+       /**
+        * @inheritdoc
+        */
+       mw.widgets.SearchInputWidget.prototype.getLookupMenuOptionsFromData = function () {
+               var items = mw.widgets.SearchInputWidget.parent.prototype.getLookupMenuOptionsFromData.apply(
+                       this, arguments
+               );
+
+               this.lastLookupItems = items.map( function ( item ) {
+                       return item.data;
+               } );
+
+               return items;
+       };
+
 }( jQuery, mediaWiki ) );
index 2d603bf..7c7aca3 100644 (file)
                        mw.track( 'mediawiki.searchSuggest', {
                                action: 'render-one',
                                formData: formData,
-                               index: context.config.suggestions.indexOf( text ) + 1
+                               index: context.config.suggestions.indexOf( text )
                        } );
 
                        // this is the container <div>, jQueryfied
                }
 
                // The function used when the user makes a selection
-               function selectFunction( $input ) {
+               function selectFunction( $input, source ) {
                        var context = $input.data( 'suggestionsContext' ),
                                text = $input.val();
 
-                       mw.track( 'mediawiki.searchSuggest', {
-                               action: 'click-result',
-                               numberOfResults: context.config.suggestions.length,
-                               clickIndex: context.config.suggestions.indexOf( text ) + 1
-                       } );
+                       // Selecting via keyboard triggers a form submission. That will fire
+                       // the submit-form event in addition to this click-result event.
+                       if ( source !== 'keyboard' ) {
+                               mw.track( 'mediawiki.searchSuggest', {
+                                       action: 'click-result',
+                                       numberOfResults: context.config.suggestions.length,
+                                       index: context.config.suggestions.indexOf( text )
+                               } );
+                       }
 
                        // allow the form to be submitted
                        return true;
                        // linkParams object is modified and reused
                        formData.linkParams[ formData.textParam ] = query;
 
+                       mw.track( 'mediawiki.searchSuggest', {
+                               action: 'render-one',
+                               formData: formData,
+                               index: context.config.suggestions.indexOf( query )
+                       } );
+
                        if ( $el.children().length === 0 ) {
                                $el
                                        .append(
                        },
                        special: {
                                render: specialRenderFunction,
-                               select: function ( $input ) {
-                                       $input.closest( 'form' )
-                                               .append( $( '<input type="hidden" name="fulltext" value="1"/>' ) );
+                               select: function ( $input, source ) {
+                                       var context = $input.data( 'suggestionsContext' ),
+                                               text = $input.val();
+                                       if ( source === 'mouse' ) {
+                                               // mouse click won't trigger form submission, so we need to send a click event
+                                               mw.track( 'mediawiki.searchSuggest', {
+                                                       action: 'click-result',
+                                                       numberOfResults: context.config.suggestions.length,
+                                                       index: context.config.suggestions.indexOf( text )
+                                               } );
+                                       } else {
+                                               $input.closest( 'form' )
+                                                       .append( $( '<input type="hidden" name="fulltext" value="1"/>' ) );
+                                       }
                                        return true; // allow the form to be submitted
                                }
                        },
                                        action: 'submit-form',
                                        numberOfResults: context.config.suggestions.length,
                                        $form: context.config.$region.closest( 'form' ),
-                                       inputLocation: getInputLocation( context )
+                                       inputLocation: getInputLocation( context ),
+                                       index: context.config.suggestions.indexOf(
+                                               context.data.$textbox.val()
+                                       )
                                } );
                        } )
                        // If the form includes any fallback fulltext search buttons, remove them
index 0e13721..d20344d 100644 (file)
@@ -315,6 +315,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                        'DBLoadBalancerFactory' => [ 'DBLoadBalancerFactory', 'LBFactory' ],
                        'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' ],
                        'WatchedItemStore' => [ 'WatchedItemStore', WatchedItemStore::class ],
+                       'WatchedItemQueryService' => [ 'WatchedItemQueryService', WatchedItemQueryService::class ],
                        'GenderCache' => [ 'GenderCache', GenderCache::class ],
                        'LinkCache' => [ 'LinkCache', LinkCache::class ],
                        'LinkRenderer' => [ 'LinkRenderer', LinkRenderer::class ],
diff --git a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
new file mode 100644 (file)
index 0000000..b63a1f4
--- /dev/null
@@ -0,0 +1,1013 @@
+<?php
+
+/**
+ * @covers WatchedItemQueryService
+ */
+class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseBase
+        */
+       private function getMockDb() {
+               $mock = $this->getMockBuilder( DatabaseBase::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $mock->expects( $this->any() )
+                       ->method( 'makeList' )
+                       ->with(
+                               $this->isType( 'array' ),
+                               $this->isType( 'int' )
+                       )
+                       ->will( $this->returnCallback( function( $a, $conj ) {
+                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
+                               return join( $sqlConj, array_map( function( $s ) {
+                                       return '(' . $s . ')';
+                               }, $a
+                               ) );
+                       } ) );
+
+               $mock->expects( $this->any() )
+                       ->method( 'addQuotes' )
+                       ->will( $this->returnCallback( function( $value ) {
+                               return "'$value'";
+                       } ) );
+
+               $mock->expects( $this->any() )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnArgument( 0 ) );
+
+               $mock->expects( $this->any() )
+                       ->method( 'bitAnd' )
+                       ->willReturnCallback( function( $a, $b ) {
+                               return "($a & $b)";
+                       } );
+
+               return $mock;
+       }
+
+       /**
+        * @param $mockDb
+        * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
+        */
+       private function getMockLoadBalancer( $mockDb ) {
+               $mock = $this->getMockBuilder( LoadBalancer::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mock->expects( $this->any() )
+                       ->method( 'getConnection' )
+                       ->with( DB_SLAVE )
+                       ->will( $this->returnValue( $mockDb ) );
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockNonAnonUserWithId( $id ) {
+               $mock = $this->getMock( User::class );
+               $mock->expects( $this->any() )
+                       ->method( 'isAnon' )
+                       ->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )
+                       ->method( 'getId' )
+                       ->will( $this->returnValue( $id ) );
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockUnrestrictedNonAnonUserWithId( $id ) {
+               $mock = $this->getMockNonAnonUserWithId( $id );
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowed' )
+                       ->will( $this->returnValue( true ) );
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowedAny' )
+                       ->will( $this->returnValue( true ) );
+               $mock->expects( $this->any() )
+                       ->method( 'useRCPatrol' )
+                       ->will( $this->returnValue( true ) );
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @param string $notAllowedAction
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
+               $mock = $this->getMockNonAnonUserWithId( $id );
+
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowed' )
+                       ->will( $this->returnCallback( function( $action ) use ( $notAllowedAction ) {
+                               return $action !== $notAllowedAction;
+                       } ) );
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowedAny' )
+                       ->will( $this->returnCallback( function() use ( $notAllowedAction ) {
+                               $actions = func_get_args();
+                               return !in_array( $notAllowedAction, $actions );
+                       } ) );
+
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
+               $mock = $this->getMockNonAnonUserWithId( $id );
+
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowed' )
+                       ->will( $this->returnValue( true ) );
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowedAny' )
+                       ->will( $this->returnValue( true ) );
+
+               $mock->expects( $this->any() )
+                       ->method( 'useRCPatrol' )
+                       ->will( $this->returnValue( false ) );
+               $mock->expects( $this->any() )
+                       ->method( 'useNPPatrol' )
+                       ->will( $this->returnValue( false ) );
+
+               return $mock;
+       }
+
+       private function getFakeRow( array $rowValues ) {
+               $fakeRow = new stdClass();
+               foreach ( $rowValues as $valueName => $value ) {
+                       $fakeRow->$valueName = $value;
+               }
+               return $fakeRow;
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+                                       'rc_cur_id',
+                                       'rc_this_oldid',
+                                       'rc_last_oldid',
+                               ],
+                               [
+                                       'wl_user' => 1,
+                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
+                               ],
+                               $this->isType( 'string' ),
+                               [],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                                       'page' => [
+                                               'LEFT JOIN',
+                                               'rc_cur_id=page_id',
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [
+                               $this->getFakeRow( [
+                                       'rc_id' => 1,
+                                       'rc_namespace' => 0,
+                                       'rc_title' => 'Foo1',
+                                       'rc_timestamp' => '20151212010101',
+                                       'rc_type' => RC_NEW,
+                                       'rc_deleted' => 0,
+                                       'wl_notificationtimestamp' => '20151212010101',
+                               ] ),
+                               $this->getFakeRow( [
+                                       'rc_id' => 2,
+                                       'rc_namespace' => 1,
+                                       'rc_title' => 'Foo2',
+                                       'rc_timestamp' => '20151212010102',
+                                       'rc_type' => RC_NEW,
+                                       'rc_deleted' => 0,
+                                       'wl_notificationtimestamp' => null,
+                               ] ),
+                       ] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user );
+
+               $this->assertInternalType( 'array', $items );
+               $this->assertCount( 2, $items );
+
+               foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
+                       $this->assertInstanceOf( WatchedItem::class, $watchedItem );
+                       $this->assertInternalType( 'array', $recentChangeInfo );
+               }
+
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
+                       $items[0][0]
+               );
+               $this->assertEquals(
+                       [
+                               'rc_id' => 1,
+                               'rc_namespace' => 0,
+                               'rc_title' => 'Foo1',
+                               'rc_timestamp' => '20151212010101',
+                               'rc_type' => RC_NEW,
+                               'rc_deleted' => 0,
+                       ],
+                       $items[0][1]
+               );
+
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
+                       $items[1][0]
+               );
+               $this->assertEquals(
+                       [
+                               'rc_id' => 2,
+                               'rc_namespace' => 1,
+                               'rc_title' => 'Foo2',
+                               'rc_timestamp' => '20151212010102',
+                               'rc_type' => RC_NEW,
+                               'rc_deleted' => 0,
+                       ],
+                       $items[1][1]
+               );
+       }
+
+       public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
+               return [
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
+                               [ 'rc_type', 'rc_minor', 'rc_bot' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
+                               [ 'rc_user_text' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
+                               [ 'rc_user' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               [ 'rc_comment' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
+                               [ 'rc_patrolled', 'rc_log_type' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
+                               [ 'rc_old_len', 'rc_new_len' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
+                               [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'namespaceIds' => [ 0, 1 ] ],
+                               [],
+                               [ 'wl_namespace' => [ 0, 1 ] ],
+                               [],
+                       ],
+                       [
+                               [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
+                               [],
+                               [ 'wl_namespace' => [ 0, 1 ] ],
+                               [],
+                       ],
+                       [
+                               [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
+                               [],
+                               [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [],
+                               [],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
+                               [],
+                               [],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
+                               [],
+                               [ "rc_timestamp <= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
+                               [],
+                               [ "rc_timestamp >= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                       ],
+                       [
+                               [
+                                       'dir' => WatchedItemQueryService::DIR_OLDER,
+                                       'start' => '20151212020101',
+                                       'end' => '20151212010101'
+                               ],
+                               [],
+                               [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
+                               [],
+                               [ "rc_timestamp >= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
+                               [],
+                               [ "rc_timestamp <= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                       ],
+                       [
+                               [
+                                       'dir' => WatchedItemQueryService::DIR_NEWER,
+                                       'start' => '20151212010101',
+                                       'end' => '20151212020101'
+                               ],
+                               [],
+                               [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                       ],
+                       [
+                               [ 'limit' => 10 ],
+                               [],
+                               [],
+                               [ 'LIMIT' => 10 ],
+                       ],
+                       [
+                               [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
+                               [],
+                               [],
+                               [ 'LIMIT' => 10 ],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
+                               [],
+                               [ 'rc_minor != 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
+                               [],
+                               [ 'rc_minor = 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
+                               [],
+                               [ 'rc_bot != 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
+                               [],
+                               [ 'rc_bot = 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
+                               [],
+                               [ 'rc_user = 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
+                               [],
+                               [ 'rc_user != 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
+                               [],
+                               [ 'rc_patrolled != 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ],
+                               [],
+                               [ 'rc_patrolled = 0' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
+                               [],
+                               [ 'rc_timestamp >= wl_notificationtimestamp' ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
+                               [],
+                               [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
+                               [],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               [],
+                               [ 'rc_user_text' => 'SomeOtherUser' ],
+                               [],
+                       ],
+                       [
+                               [ 'notByUser' => 'SomeOtherUser' ],
+                               [],
+                               [ "rc_user_text != 'SomeOtherUser'" ],
+                               [],
+                       ],
+                       [
+                               [ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [],
+                               [
+                                       "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
+                               ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                       ],
+                       [
+                               [ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_NEWER ],
+                               [],
+                               [
+                                       "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
+                               ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                       ],
+                       [
+                               [
+                                       'startFrom' => [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
+                                       'dir' => WatchedItemQueryService::DIR_OLDER
+                               ],
+                               [],
+                               [
+                                       "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
+                               ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
+               array $options,
+               array $expectedExtraFields,
+               array $expectedExtraConds,
+               array $expectedDbOptions
+       ) {
+               $expectedFields = array_merge(
+                       [
+                               'rc_id',
+                               'rc_namespace',
+                               'rc_title',
+                               'rc_timestamp',
+                               'rc_type',
+                               'rc_deleted',
+                               'wl_notificationtimestamp',
+
+                               'rc_cur_id',
+                               'rc_this_oldid',
+                               'rc_last_oldid',
+                       ],
+                       $expectedExtraFields
+               );
+               $expectedConds = array_merge(
+                       [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
+                       $expectedExtraConds
+               );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $expectedFields,
+                               $expectedConds,
+                               $this->isType( 'string' ),
+                               $expectedDbOptions,
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                                       'page' => [
+                                               'LEFT JOIN',
+                                               'rc_cur_id=page_id',
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function filterPatrolledOptionProvider() {
+               return [
+                       [ WatchedItemQueryService::FILTER_PATROLLED ],
+                       [ WatchedItemQueryService::FILTER_NOT_PATROLLED ],
+               ];
+       }
+
+       /**
+        * @dataProvider filterPatrolledOptionProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
+               $filtersOption
+       ) {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $this->isType( 'array' ),
+                               [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'filters' => [ $filtersOption ] ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function mysqlIndexOptimizationProvider() {
+               return [
+                       [
+                               'mysql',
+                               [],
+                               [ "rc_timestamp > ''" ],
+                       ],
+                       [
+                               'mysql',
+                               [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ "rc_timestamp <= '20151212010101'" ],
+                       ],
+                       [
+                               'mysql',
+                               [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ "rc_timestamp >= '20151212010101'" ],
+                       ],
+                       [
+                               'postgres',
+                               [],
+                               [],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider mysqlIndexOptimizationProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
+               $dbType,
+               array $options,
+               array $expectedExtraConds
+       ) {
+               $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
+               $conds = array_merge( $commonConds, $expectedExtraConds );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $this->isType( 'array' ),
+                               $conds,
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+               $mockDb->expects( $this->any() )
+                       ->method( 'getType' )
+                       ->will( $this->returnValue( $dbType ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function userPermissionRelatedExtraChecksProvider() {
+               return [
+                       [
+                               [],
+                               'deletedhistory',
+                               [
+                                       '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
+                                               LogPage::DELETED_ACTION . ')'
+                               ],
+                       ],
+                       [
+                               [],
+                               'suppressrevision',
+                               [
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+                       [
+                               [],
+                               'viewsuppressed',
+                               [
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               'deletedhistory',
+                               [
+                                       'rc_user_text' => 'SomeOtherUser',
+                                       '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
+                                       '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
+                                               LogPage::DELETED_ACTION . ')'
+                               ],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               'suppressrevision',
+                               [
+                                       'rc_user_text' => 'SomeOtherUser',
+                                       '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
+                                               ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               'viewsuppressed',
+                               [
+                                       'rc_user_text' => 'SomeOtherUser',
+                                       '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
+                                               ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider userPermissionRelatedExtraChecksProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
+               array $options,
+               $notAllowedAction,
+               array $expectedExtraConds
+       ) {
+               $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
+               $conds = array_merge( $commonConds, $expectedExtraConds );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $this->isType( 'array' ),
+                               $conds,
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+
+                                       'rc_cur_id',
+                                       'rc_this_oldid',
+                                       'rc_last_oldid',
+                               ],
+                               [ 'wl_user' => 1, ],
+                               $this->isType( 'string' ),
+                               [],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
+               return [
+                       [
+                               [ 'rcTypes' => [ 1337 ] ],
+                               'Bad value for parameter $options[\'rcTypes\']',
+                       ],
+                       [
+                               [ 'rcTypes' => [ 'edit' ] ],
+                               'Bad value for parameter $options[\'rcTypes\']',
+                       ],
+                       [
+                               [ 'rcTypes' => [ RC_EDIT, 1337 ] ],
+                               'Bad value for parameter $options[\'rcTypes\']',
+                       ],
+                       [
+                               [ 'dir' => 'foo' ],
+                               'Bad value for parameter $options[\'dir\']',
+                       ],
+                       [
+                               [ 'start' => '20151212010101' ],
+                               'Bad value for parameter $options[\'dir\']: must be provided',
+                       ],
+                       [
+                               [ 'end' => '20151212010101' ],
+                               'Bad value for parameter $options[\'dir\']: must be provided',
+                       ],
+                       [
+                               [ 'startFrom' => [ '20151212010101', 123 ] ],
+                               'Bad value for parameter $options[\'dir\']: must be provided',
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => '20151212010101' ],
+                               'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => [ '20151212010101' ] ],
+                               'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
+                       ],
+                       [
+                               [
+                                       'dir' => WatchedItemQueryService::DIR_OLDER,
+                                       'startFrom' => [ '20151212010101', 123, 'foo' ]
+                               ],
+                               'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
+                       ],
+                       [
+                               [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
+                               'Bad value for parameter $options[\'watchlistOwnerToken\']',
+                       ],
+                       [
+                               [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
+                               'Bad value for parameter $options[\'watchlistOwner\']',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
+               array $options,
+               $expectedInExceptionMessage
+       ) {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( $this->anything() );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
+               $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+                                       'rc_cur_id',
+                               ],
+                               [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
+                               $this->isType( 'string' ),
+                               [],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                                       'page' => [
+                                               'LEFT JOIN',
+                                               'rc_cur_id=page_id',
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'usedInGenerator' => true ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+                                       'rc_this_oldid',
+                               ],
+                               [ 'wl_user' => 1 ],
+                               $this->isType( 'string' ),
+                               [],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'usedInGenerator' => true, 'allRevisions' => true, ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' ),
+                               [
+                                       'wl_user' => 2,
+                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
+                               ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
+               $otherUser->expects( $this->once() )
+                       ->method( 'getOption' )
+                       ->with( 'watchlisttoken' )
+                       ->willReturn( '0123456789abcdef' );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function invalidWatchlistTokenProvider() {
+               return [
+                       [ 'wrongToken' ],
+                       [ '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidWatchlistTokenProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( $this->anything() );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
+               $otherUser->expects( $this->once() )
+                       ->method( 'getOption' )
+                       ->with( 'watchlisttoken' )
+                       ->willReturn( '0123456789abcdef' );
+
+               $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
+               $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
+               );
+       }
+
+}
index 898b58e..eaeb3ae 100644 (file)
@@ -821,8 +821,13 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
                );
                $this->watchPages( $user, [ $target ] );
 
-               $resultMinor = $this->doListWatchlistRequest( [ 'wlshow' => 'minor', 'wlprop' => 'flags' ] );
-               $resultNotMinor = $this->doListWatchlistRequest( [ 'wlshow' => '!minor', 'wlprop' => 'flags' ] );
+               $resultMinor = $this->doListWatchlistRequest( [
+                       'wlshow' => WatchedItemQueryService::FILTER_MINOR,
+                       'wlprop' => 'flags'
+               ] );
+               $resultNotMinor = $this->doListWatchlistRequest( [
+                       'wlshow' => WatchedItemQueryService::FILTER_NOT_MINOR, 'wlprop' => 'flags'
+               ] );
 
                $this->assertArraySubsetsEqual(
                        $this->getItemsFromApiResponse( $resultMinor ),
@@ -845,8 +850,12 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
                );
                $this->watchPages( $user, [ $target ] );
 
-               $resultBot = $this->doListWatchlistRequest( [ 'wlshow' => 'bot' ] );
-               $resultNotBot = $this->doListWatchlistRequest( [ 'wlshow' => '!bot' ] );
+               $resultBot = $this->doListWatchlistRequest( [
+                       'wlshow' => WatchedItemQueryService::FILTER_BOT
+               ] );
+               $resultNotBot = $this->doListWatchlistRequest( [
+                       'wlshow' => WatchedItemQueryService::FILTER_NOT_BOT
+               ] );
 
                $this->assertArraySubsetsEqual(
                        $this->getItemsFromApiResponse( $resultBot ),
@@ -870,11 +879,11 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
 
                $resultAnon = $this->doListWatchlistRequest( [
                        'wlprop' => 'user',
-                       'wlshow' => 'anon'
+                       'wlshow' => WatchedItemQueryService::FILTER_ANON
                ] );
                $resultNotAnon = $this->doListWatchlistRequest( [
                        'wlprop' => 'user',
-                       'wlshow' => '!anon'
+                       'wlshow' => WatchedItemQueryService::FILTER_NOT_ANON
                ] );
 
                $this->assertArraySubsetsEqual(
@@ -914,11 +923,11 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
 
                $resultUnread = $this->doListWatchlistRequest( [
                        'wlprop' => 'notificationtimestamp|title',
-                       'wlshow' => 'unread'
+                       'wlshow' => WatchedItemQueryService::FILTER_UNREAD
                ] );
                $resultNotUnread = $this->doListWatchlistRequest( [
                        'wlprop' => 'notificationtimestamp|title',
-                       'wlshow' => '!unread'
+                       'wlshow' => WatchedItemQueryService::FILTER_NOT_UNREAD
                ] );
 
                $this->assertEquals(
@@ -951,11 +960,11 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
 
                $resultPatrolled = $this->doListWatchlistRequest( [
                        'wlprop' => 'patrol',
-                       'wlshow' => 'patrolled'
+                       'wlshow' => WatchedItemQueryService::FILTER_PATROLLED
                ], $user );
                $resultNotPatrolled = $this->doListWatchlistRequest( [
                        'wlprop' => 'patrol',
-                       'wlshow' => '!patrolled'
+                       'wlshow' => WatchedItemQueryService::FILTER_NOT_PATROLLED
                ], $user );
 
                $this->assertEquals(
index 68ce640..d6249bb 100644 (file)
@@ -55,6 +55,9 @@ class KafkaHandlerTest extends MediaWikiTestCase {
                $produce->expects( $this->once() )
                        ->method( 'setMessages' )
                        ->with( $expect, $this->anything(), $this->anything() );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
 
                $handler = new KafkaHandler( $produce, $options );
                $handler->handle( [
@@ -86,6 +89,9 @@ class KafkaHandlerTest extends MediaWikiTestCase {
                $produce->expects( $this->any() )
                        ->method( 'getAvailablePartitions' )
                        ->will( $this->throwException( new \Kafka\Exception ) );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
 
                if ( $expectException ) {
                        $this->setExpectedException( 'Kafka\Exception' );
@@ -144,6 +150,9 @@ class KafkaHandlerTest extends MediaWikiTestCase {
                        ->will( $this->returnValue( [ 'A' ] ) );
                $mockMethod = $produce->expects( $this->exactly( 2 ) )
                        ->method( 'setMessages' );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
                // evil hax
                \TestingAccessWrapper::newFromObject( $mockMethod )->matcher->parametersMatcher =
                        new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( [
@@ -178,6 +187,9 @@ class KafkaHandlerTest extends MediaWikiTestCase {
                $produce->expects( $this->once() )
                        ->method( 'setMessages' )
                        ->with( $this->anything(), $this->anything(), [ 'words', 'lines' ] );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
 
                $formatter = $this->getMock( 'Monolog\Formatter\FormatterInterface' );
                $formatter->expects( $this->any() )