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 and HTML entity encodings of  , <, 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 ==
+++ /dev/null
-== 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 and HTML entity encodings of  , <, 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.
* [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 ==
'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',
'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',
"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",
&$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
use SiteLookup;
use SiteStore;
use WatchedItemStore;
+use WatchedItemQueryService;
use SkinFactory;
use TitleFormatter;
use TitleParser;
return $this->getService( 'WatchedItemStore' );
}
+ /**
+ * @since 1.28
+ * @return WatchedItemQueryService
+ */
+ public function getWatchedItemQueryService() {
+ return $this->getService( 'WatchedItemQueryService' );
+ }
+
/**
* @since 1.28
* @return GenderCache
return $store;
},
+ 'WatchedItemQueryService' => function( MediaWikiServices $services ) {
+ return new WatchedItemQueryService( $services->getDBLoadBalancer() );
+ },
+
'LinkCache' => function( MediaWikiServices $services ) {
return new LinkCache(
$services->getTitleFormatter()
--- /dev/null
+<?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;
+ }
+
+}
* @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.
}
}
- $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',
}
}
- /* 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() );
}
$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'] );
}
}
}
}
}
- 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;
}
}
/* 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;
}
'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' => [
"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]].",
"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.",
"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/ 에 버그 신고를 해 주시기 바랍니다.."
}
use Kafka\MetaDataFromKafka;
use Kafka\Produce;
+use Kafka\Protocol\Decoder;
use MediaWiki\Logger\LoggerFactory;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
'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,
];
/**
$options['logExceptions'] = LoggerFactory::getInstance( $options['logExceptions'] );
}
+ if ( isset( $options['requireAck'] ) ) {
+ $produce->setRequireAck( $options['requireAck'] );
+ }
+
return new self( $produce, $options, $level, $bubble );
}
*/
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 );
}
}
}
"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": "стиснете да расклопите",
/**
* @param OutputPage $out
*/
- function initPage( OutputPage $out ) {
-
+ public function initPage( OutputPage $out ) {
$this->preloadExistence();
-
}
/**
/**
* 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();
$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";
$error = '';
if ( !Hooks::run( 'UploadVerification',
- [ $this->mDestName, $this->mTempPath, &$error ] )
+ [ $this->mDestName, $this->mTempPath, &$error ], '1.28' )
) {
return [ 'status' => self::HOOK_ABORTED, 'error' => $error ];
}
"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": "عربية موسعة",
"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}} نوسخه ایچری گتیریلدی",
"action-managechangetags": "стварэньне і (дэ)актывацыю метак",
"action-applychangetags": "дадаваньне метак пры рэдагаваньні",
"action-changetags": "дадаваньне і выдаленьне адвольных метак да асобных вэрсіяў і запісаў у журнале падзеяў",
+ "action-deletechangetags": "выдаленьне метак з базы зьвестак",
"nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}",
"enhancedrc-since-last-visit": "$1 {{PLURAL:$1|з апошняга візыту}}",
"enhancedrc-history": "гісторыя",
"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}}}}",
"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",
"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": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
"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 בייטים}})",
"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": "左右反転",
"ipbnounblockself": "자기 스스로를 차단 해제할 수 없습니다.",
"lockdb": "데이터베이스 잠그기",
"unlockdb": "데이터베이스 잠금 해제",
- "lockdbtext": "데이터베이스를 잠그면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스를 요구하는 모든 기능이 정지됩니다.\n정말로 잠가야 하는지를 다시 한번 확인해주세요. 관리 작업이 끝난 뒤에는 데이터베이스 잠금을 풀어야 합니다.",
- "unlockdbtext": "데이터베이스를 잠금 해제하면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스를 요구하는 모든 기능이 복구됩니다.\n정말로 잠금을 해제하려는지를 다시 한번 확인해주세요.",
+ "lockdbtext": "데이터베이스를 잠그면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스에서 변경을 요구하는 모든 기능이 정지됩니다.\n정말로 잠가야 하는지 확인해 주세요. 관리 작업이 끝난 뒤에는 데이터베이스 잠금을 풀어야 합니다.",
+ "unlockdbtext": "데이터베이스를 잠금 해제하면 모든 사용자의 편집, 환경 설정 바꾸기, 주시문서 편집 등 데이터베이스에서 변경을 요구하는 모든 기능이 복구됩니다.\n정말로 잠금을 해제해야 하는지 확인해주세요.",
"lockconfirm": "네, 데이터베이스를 잠급니다.",
"unlockconfirm": "네, 데이터베이스를 잠금 해제합니다.",
"lockbtn": "데이터베이스 잠그기",
"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": "Автоматското создавање на сметки е привремено оневозможено поради претходни грешки.",
"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",
"Kenrick95",
"McDutchie",
"Rv77ax",
- "William Surya Permana"
+ "William Surya Permana",
+ "Rachmat.Wahidi"
]
},
"ooui-outline-control-move-down": "Pindahkan butir ke bawah",
"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"
}
--- /dev/null
+{
+ "@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": "Хьадехьаяккха файл укхаз"
+}
"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é"
}
"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"
}
--- /dev/null
+{
+ "@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": "فائل ایتھے پاؤ"
+}
"@metadata": {
"authors": [
"Lloffiwr",
- "Muddyb Blast Producer"
+ "Muddyb Blast Producer",
+ "Muddyb"
]
},
"ooui-outline-control-move-down": "Sogeza kipengee chini",
/*!
- * 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 ) {
/*!
- * 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;
}
/*!
- * 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-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;
}
/*!
- * 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 ) {
* @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++ ) {
// 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 );
}
* @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 );
/*!
- * 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 ) {
/*!
- * 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 {
/*!
- * 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 {
/*!
- * 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 ) {
/*!
- * 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 {
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 {
/*!
- * 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 {
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 {
/*!
- * 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 ) {
* @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' );
+ }
};
/**
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();
+ }
}
- } } );
+ } );
}
};
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();
+ }
}
- } } );
+ } );
}
};
/*!
- * 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);
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 {
/*!
- * 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;
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);
/*!
- * 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 ) {
.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 );
};
* @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 );
} 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 );
}
} )
* 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',
// 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 ) );
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
'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 ],
--- /dev/null
+<?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 ]
+ );
+ }
+
+}
);
$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 ),
);
$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 ),
$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(
$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(
$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(
$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( [
$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' );
->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( [
$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() )