"PhanAccessMethodInternal",
// approximate error count: 17
"PhanCommentParamOnEmptyParamList",
- // approximate error count: 30
+ // approximate error count: 29
"PhanCommentParamWithoutRealParam",
// approximate error count: 2
"PhanCompatibleNegativeStringOffset",
- // approximate error count: 1
- "PhanEmptyFQSENInCallable",
- // approximate error count: 1
- "PhanInvalidCommentForDeclarationType",
- // approximate error count: 6
- "PhanNonClassMethodCall",
// approximate error count: 21
"PhanParamReqAfterOpt",
- // approximate error count: 27
+ // approximate error count: 26
"PhanParamSignatureMismatch",
// approximate error count: 4
"PhanParamSignatureMismatchInternal",
- // approximate error count: 1
- "PhanParamSignatureRealMismatchTooFewParameters",
- // approximate error count: 1
- "PhanParamSuspiciousOrder",
// approximate error count: 127
"PhanParamTooMany",
// approximate error count: 2
- "PhanParamTooManyCallable",
- // approximate error count: 1
- "PhanParamTooManyInternal",
- // approximate error count: 2
"PhanPluginDuplicateExpressionBinaryOp",
// approximate error count: 2
"PhanTraitParentReference",
- // approximate error count: 27
+ // approximate error count: 30
"PhanTypeArraySuspicious",
- // approximate error count: 33
+ // approximate error count: 27
"PhanTypeArraySuspiciousNullable",
// approximate error count: 26
"PhanTypeComparisonFromArray",
// approximate error count: 2
"PhanTypeComparisonToArray",
- // approximate error count: 1
- "PhanTypeConversionFromArray",
- // approximate error count: 2
- "PhanTypeExpectedObjectOrClassName",
// approximate error count: 7
"PhanTypeExpectedObjectPropAccess",
- // approximate error count: 3
- "PhanTypeInstantiateAbstract",
- // approximate error count: 1
- "PhanTypeInvalidCallableArraySize",
- // approximate error count: 62
+ // approximate error count: 63
"PhanTypeInvalidDimOffset",
- // approximate error count: 10
+ // approximate error count: 6
"PhanTypeInvalidExpressionArrayDestructuring",
- // approximate error count: 1
- "PhanTypeInvalidLeftOperand",
// approximate error count: 7
"PhanTypeInvalidLeftOperandOfIntegerOp",
// approximate error count: 2
"PhanTypeInvalidRightOperand",
// approximate error count: 2
"PhanTypeInvalidRightOperandOfIntegerOp",
- // approximate error count: 1
- "PhanTypeMagicVoidWithReturn",
- // approximate error count: 152
+ // approximate error count: 154
"PhanTypeMismatchArgument",
- // approximate error count: 28
+ // approximate error count: 27
"PhanTypeMismatchArgumentInternal",
// approximate error count: 1
"PhanTypeMismatchBitwiseBinaryOperands",
- // approximate error count: 1
- "PhanTypeMismatchDeclaredParam",
// approximate error count: 2
"PhanTypeMismatchDimEmpty",
- // approximate error count: 29
+ // approximate error count: 27
"PhanTypeMismatchDimFetch",
// approximate error count: 10
"PhanTypeMismatchForeach",
// approximate error count: 77
"PhanTypeMismatchProperty",
- // approximate error count: 88
+ // approximate error count: 84
"PhanTypeMismatchReturn",
- // approximate error count: 43
- "PhanTypeMissingReturn",
- // approximate error count: 1
- "PhanTypeNoAccessiblePropertiesForeach",
- // approximate error count: 4
- "PhanTypeNonVarPassByRef",
// approximate error count: 12
"PhanTypeObjectUnsetDeclaredProperty",
// approximate error count: 9
"PhanUndeclaredConstant",
// approximate error count: 3
"PhanUndeclaredInvokeInCallable",
- // approximate error count: 242
+ // approximate error count: 237
"PhanUndeclaredMethod",
- // approximate error count: 847
+ // approximate error count: 846
"PhanUndeclaredProperty",
- // approximate error count: 1
- "PhanUndeclaredTypeReturnType",
- // approximate error count: 3
- "PhanUndeclaredTypeThrowsType",
// approximate error count: 2
"PhanUndeclaredVariableAssignOp",
// approximate error count: 55
"PhanUndeclaredVariableDim",
- // approximate error count: 4
- "PhanUnextractableAnnotationElementName",
- // approximate error count: 4
- "PhanUnextractableAnnotationSuffix",
] );
$cfg['ignore_undeclared_variables_in_global_scope'] = true;
"jakub-onderka/php-console-highlighter": "0.3.2",
"jakub-onderka/php-parallel-lint": "0.9.2",
"justinrainbow/json-schema": "~5.2",
- "mediawiki/mediawiki-codesniffer": "24.0.0",
+ "mediawiki/mediawiki-codesniffer": "25.0.0",
"monolog/monolog": "~1.22.1",
"nikic/php-parser": "3.1.5",
"seld/jsonlint": "1.7.1",
'MediaWiki\\Edit\\' => __DIR__ . '/edit/',
'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
+ 'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/',
'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
'MediaWiki\\Revision\\' => __DIR__ . '/Revision/',
'MediaWiki\\Session\\' => __DIR__ . '/session/',
) {
if ( $config->get( 'StatsdServer' ) && $stats->hasData() ) {
try {
- $statsdServer = explode( ':', $config->get( 'StatsdServer' ) );
+ $statsdServer = explode( ':', $config->get( 'StatsdServer' ), 2 );
$statsdHost = $statsdServer[0];
$statsdPort = $statsdServer[1] ?? 8125;
$statsdSender = new SocketSender( $statsdHost, $statsdPort );
use IBufferingStatsdDataFactory;
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use MediaWiki\Http\HttpRequestFactory;
+use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Preferences\PreferencesFactory;
use MediaWiki\Shell\CommandFactory;
use MediaWiki\Revision\RevisionRenderer;
return $this->getService( 'PerDbNameStatsdDataFactory' );
}
+ /**
+ * @since 1.33
+ * @return PermissionManager
+ */
+ public function getPermissionManager() {
+ return $this->getService( 'PermissionManager' );
+ }
+
/**
* @since 1.31
* @return PreferencesFactory
* Can also be used to revert after a DB failure.
*
* @private
- * @param Title Old location to move the file from.
- * @param Title New location to move the file to.
+ * @param Title $oldTitle Old location to move the file from.
+ * @param Title $newTitle New location to move the file to.
* @return Status
*/
private function moveFile( $oldTitle, $newTitle ) {
* Parse wikitext and return the HTML (internal implementation helper)
*
* @param string $text
- * @param Title The title to use
+ * @param Title $title The title to use
* @param bool $linestart Is this the start of a line?
* @param bool $tidy Whether the output should be tidied
* @param bool $interface Use interface language (instead of content language) while parsing
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+namespace MediaWiki\Permissions;
+
+use Action;
+use Exception;
+use FatalError;
+use Hooks;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Special\SpecialPageFactory;
+use MessageSpecifier;
+use MWException;
+use MWNamespace;
+use RequestContext;
+use SpecialPage;
+use Title;
+use User;
+use WikiPage;
+
+/**
+ * A service class for checking permissions
+ * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
+ *
+ * @since 1.33
+ */
+class PermissionManager {
+
+ /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
+ const RIGOR_QUICK = 'quick';
+
+ /** @var string Does cheap and expensive checks possibly from a replica DB */
+ const RIGOR_FULL = 'full';
+
+ /** @var string Does cheap and expensive checks, using the master as needed */
+ const RIGOR_SECURE = 'secure';
+
+ /** @var SpecialPageFactory */
+ private $specialPageFactory;
+
+ /** @var string[] List of pages names anonymous user may see */
+ private $whitelistRead;
+
+ /** @var string[] Whitelists publicly readable titles with regular expressions */
+ private $whitelistReadRegexp;
+
+ /** @var bool Require users to confirm email address before they can edit */
+ private $emailConfirmToEdit;
+
+ /** @var bool If set to true, blocked users will no longer be allowed to log in */
+ private $blockDisablesLogin;
+
+ /**
+ * @param SpecialPageFactory $specialPageFactory
+ * @param string[] $whitelistRead
+ * @param string[] $whitelistReadRegexp
+ * @param bool $emailConfirmToEdit
+ * @param bool $blockDisablesLogin
+ */
+ public function __construct(
+ SpecialPageFactory $specialPageFactory,
+ $whitelistRead,
+ $whitelistReadRegexp,
+ $emailConfirmToEdit,
+ $blockDisablesLogin
+ ) {
+ $this->specialPageFactory = $specialPageFactory;
+ $this->whitelistRead = $whitelistRead;
+ $this->whitelistReadRegexp = $whitelistReadRegexp;
+ $this->emailConfirmToEdit = $emailConfirmToEdit;
+ $this->blockDisablesLogin = $blockDisablesLogin;
+ }
+
+ /**
+ * Can $user perform $action on a page?
+ *
+ * The method is intended to replace Title::userCan()
+ * The $user parameter need to be superseded by UserIdentity value in future
+ * The $title parameter need to be superseded by PageIdentity value in future
+ *
+ * @see Title::userCan()
+ *
+ * @param string $action
+ * @param User $user
+ * @param LinkTarget $page
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ *
+ * @return bool
+ * @throws Exception
+ */
+ public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
+ return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
+ }
+
+ /**
+ * Can $user perform $action on a page?
+ *
+ * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
+ *
+ * @param string $action Action that permission needs to be checked for
+ * @param User $user User to check
+ * @param LinkTarget $page
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param array $ignoreErrors Array of Strings Set this to a list of message keys
+ * whose corresponding errors may be ignored.
+ *
+ * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
+ * @throws Exception
+ */
+ public function getPermissionErrors(
+ $action,
+ User $user,
+ LinkTarget $page,
+ $rigor = self::RIGOR_SECURE,
+ $ignoreErrors = []
+ ) {
+ $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
+
+ // Remove the errors being ignored.
+ foreach ( $errors as $index => $error ) {
+ $errKey = is_array( $error ) ? $error[0] : $error;
+
+ if ( in_array( $errKey, $ignoreErrors ) ) {
+ unset( $errors[$index] );
+ }
+ if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
+ unset( $errors[$index] );
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Check if user is blocked from editing a particular article
+ *
+ * @param User $user
+ * @param LinkTarget $page Title to check
+ * @param bool $fromReplica Whether to check the replica DB instead of the master
+ *
+ * @return bool
+ * @throws FatalError
+ * @throws MWException
+ */
+ public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
+ $blocked = $user->isHidden();
+
+ // TODO: remove upon further migration to LinkTarget
+ $page = Title::newFromLinkTarget( $page );
+
+ if ( !$blocked ) {
+ $block = $user->getBlock( $fromReplica );
+ if ( $block ) {
+ // Special handling for a user's own talk page. The block is not aware
+ // of the user, so this must be done here.
+ if ( $page->equals( $user->getTalkPage() ) ) {
+ $blocked = $block->appliesToUsertalk( $page );
+ } else {
+ $blocked = $block->appliesToTitle( $page );
+ }
+ }
+ }
+
+ // only for the purpose of the hook. We really don't need this here.
+ $allowUsertalk = $user->isAllowUsertalk();
+
+ Hooks::run( 'UserIsBlockedFrom', [ $user, $page, &$blocked, &$allowUsertalk ] );
+
+ return $blocked;
+ }
+
+ /**
+ * Can $user perform $action on a page? This is an internal function,
+ * with multiple levels of checks depending on performance needs; see $rigor below.
+ * It does not check wfReadOnly().
+ *
+ * @param string $action Action that permission needs to be checked for
+ * @param User $user User to check
+ * @param LinkTarget $page
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Set this to true to stop after the first permission error.
+ *
+ * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
+ * @throws Exception
+ */
+ private function getPermissionErrorsInternal(
+ $action,
+ User $user,
+ LinkTarget $page,
+ $rigor = self::RIGOR_SECURE,
+ $short = false
+ ) {
+ if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
+ throw new Exception( "Invalid rigor parameter '$rigor'." );
+ }
+
+ # Read has special handling
+ if ( $action == 'read' ) {
+ $checks = [
+ 'checkPermissionHooks',
+ 'checkReadPermissions',
+ 'checkUserBlock', // for wgBlockDisablesLogin
+ ];
+ # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
+ # or checkUserConfigPermissions here as it will lead to duplicate
+ # error messages. This is okay to do since anywhere that checks for
+ # create will also check for edit, and those checks are called for edit.
+ } elseif ( $action == 'create' ) {
+ $checks = [
+ 'checkQuickPermissions',
+ 'checkPermissionHooks',
+ 'checkPageRestrictions',
+ 'checkCascadingSourcesRestrictions',
+ 'checkActionPermissions',
+ 'checkUserBlock'
+ ];
+ } else {
+ $checks = [
+ 'checkQuickPermissions',
+ 'checkPermissionHooks',
+ 'checkSpecialsAndNSPermissions',
+ 'checkSiteConfigPermissions',
+ 'checkUserConfigPermissions',
+ 'checkPageRestrictions',
+ 'checkCascadingSourcesRestrictions',
+ 'checkActionPermissions',
+ 'checkUserBlock'
+ ];
+ }
+
+ $errors = [];
+ foreach ( $checks as $method ) {
+ $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
+
+ if ( $short && $errors !== [] ) {
+ break;
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Check various permission hooks
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ * @throws FatalError
+ * @throws MWException
+ */
+ private function checkPermissionHooks(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove when LinkTarget usage will expand further
+ $page = Title::newFromLinkTarget( $page );
+ // Use getUserPermissionsErrors instead
+ $result = '';
+ if ( !Hooks::run( 'userCan', [ &$page, &$user, $action, &$result ] ) ) {
+ return $result ? [] : [ [ 'badaccess-group0' ] ];
+ }
+ // Check getUserPermissionsErrors hook
+ if ( !Hooks::run( 'getUserPermissionsErrors', [ &$page, &$user, $action, &$result ] ) ) {
+ $errors = $this->resultToError( $errors, $result );
+ }
+ // Check getUserPermissionsErrorsExpensive hook
+ if (
+ $rigor !== self::RIGOR_QUICK
+ && !( $short && count( $errors ) > 0 )
+ && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$page, &$user, $action, &$result ] )
+ ) {
+ $errors = $this->resultToError( $errors, $result );
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Add the resulting error code to the errors array
+ *
+ * @param array $errors List of current errors
+ * @param array $result Result of errors
+ *
+ * @return array List of errors
+ */
+ private function resultToError( $errors, $result ) {
+ if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
+ // A single array representing an error
+ $errors[] = $result;
+ } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
+ // A nested array representing multiple errors
+ $errors = array_merge( $errors, $result );
+ } elseif ( $result !== '' && is_string( $result ) ) {
+ // A string representing a message-id
+ $errors[] = [ $result ];
+ } elseif ( $result instanceof MessageSpecifier ) {
+ // A message specifier representing an error
+ $errors[] = [ $result ];
+ } elseif ( $result === false ) {
+ // a generic "We don't want them to do that"
+ $errors[] = [ 'badaccess-group0' ];
+ }
+ return $errors;
+ }
+
+ /**
+ * Check that the user is allowed to read this page.
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ * @throws FatalError
+ * @throws MWException
+ */
+ private function checkReadPermissions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove when LinkTarget usage will expand further
+ $page = Title::newFromLinkTarget( $page );
+
+ $whitelisted = false;
+ if ( User::isEveryoneAllowed( 'read' ) ) {
+ # Shortcut for public wikis, allows skipping quite a bit of code
+ $whitelisted = true;
+ } elseif ( $user->isAllowed( 'read' ) ) {
+ # If the user is allowed to read pages, he is allowed to read all pages
+ $whitelisted = true;
+ } elseif ( $this->isSameSpecialPage( 'Userlogin', $page )
+ || $this->isSameSpecialPage( 'PasswordReset', $page )
+ || $this->isSameSpecialPage( 'Userlogout', $page )
+ ) {
+ # Always grant access to the login page.
+ # Even anons need to be able to log in.
+ $whitelisted = true;
+ } elseif ( is_array( $this->whitelistRead ) && count( $this->whitelistRead ) ) {
+ # Time to check the whitelist
+ # Only do these checks is there's something to check against
+ $name = $page->getPrefixedText();
+ $dbName = $page->getPrefixedDBkey();
+
+ // Check for explicit whitelisting with and without underscores
+ if ( in_array( $name, $this->whitelistRead, true )
+ || in_array( $dbName, $this->whitelistRead, true ) ) {
+ $whitelisted = true;
+ } elseif ( $page->getNamespace() == NS_MAIN ) {
+ # Old settings might have the title prefixed with
+ # a colon for main-namespace pages
+ if ( in_array( ':' . $name, $this->whitelistRead ) ) {
+ $whitelisted = true;
+ }
+ } elseif ( $page->isSpecialPage() ) {
+ # If it's a special page, ditch the subpage bit and check again
+ $name = $page->getDBkey();
+ list( $name, /* $subpage */ ) =
+ $this->specialPageFactory->resolveAlias( $name );
+ if ( $name ) {
+ $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
+ if ( in_array( $pure, $this->whitelistRead, true ) ) {
+ $whitelisted = true;
+ }
+ }
+ }
+ }
+
+ if ( !$whitelisted && is_array( $this->whitelistReadRegexp )
+ && !empty( $this->whitelistReadRegexp ) ) {
+ $name = $page->getPrefixedText();
+ // Check for regex whitelisting
+ foreach ( $this->whitelistReadRegexp as $listItem ) {
+ if ( preg_match( $listItem, $name ) ) {
+ $whitelisted = true;
+ break;
+ }
+ }
+ }
+
+ if ( !$whitelisted ) {
+ # If the title is not whitelisted, give extensions a chance to do so...
+ Hooks::run( 'TitleReadWhitelist', [ $page, $user, &$whitelisted ] );
+ if ( !$whitelisted ) {
+ $errors[] = $this->missingPermissionError( $action, $short );
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Get a description array when the user doesn't have the right to perform
+ * $action (i.e. when User::isAllowed() returns false)
+ *
+ * @param string $action The action to check
+ * @param bool $short Short circuit on first error
+ * @return array Array containing an error message key and any parameters
+ */
+ private function missingPermissionError( $action, $short ) {
+ // We avoid expensive display logic for quickUserCan's and such
+ if ( $short ) {
+ return [ 'badaccess-group0' ];
+ }
+
+ // TODO: it would be a good idea to replace the method below with something else like
+ // maybe callback injection
+ return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
+ }
+
+ /**
+ * Returns true if this title resolves to the named special page
+ *
+ * @param string $name The special page name
+ * @param LinkTarget $page
+ *
+ * @return bool
+ */
+ private function isSameSpecialPage( $name, LinkTarget $page ) {
+ if ( $page->getNamespace() == NS_SPECIAL ) {
+ list( $thisName, /* $subpage */ ) =
+ $this->specialPageFactory->resolveAlias( $page->getDBkey() );
+ if ( $name == $thisName ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check that the user isn't blocked from editing.
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ * @throws MWException
+ */
+ private function checkUserBlock(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // Account creation blocks handled at userlogin.
+ // Unblocking handled in SpecialUnblock
+ if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
+ return $errors;
+ }
+
+ // Optimize for a very common case
+ if ( $action === 'read' && !$this->blockDisablesLogin ) {
+ return $errors;
+ }
+
+ if ( $this->emailConfirmToEdit
+ && !$user->isEmailConfirmed()
+ && $action === 'edit'
+ ) {
+ $errors[] = [ 'confirmedittext' ];
+ }
+
+ $useReplica = ( $rigor !== self::RIGOR_SECURE );
+ $block = $user->getBlock( $useReplica );
+
+ // If the user does not have a block, or the block they do have explicitly
+ // allows the action (like "read" or "upload").
+ if ( !$block || $block->appliesToRight( $action ) === false ) {
+ return $errors;
+ }
+
+ // Determine if the user is blocked from this action on this page.
+ // What gets passed into this method is a user right, not an action name.
+ // There is no way to instantiate an action by restriction. However, this
+ // will get the action where the restriction is the same. This may result
+ // in actions being blocked that shouldn't be.
+ $actionObj = null;
+ if ( Action::exists( $action ) ) {
+ // TODO: this drags a ton of dependencies in, would be good to avoid WikiPage
+ // instantiation and decouple it creating an ActionPermissionChecker interface
+ $wikiPage = WikiPage::factory( Title::newFromLinkTarget( $page, 'clone' ) );
+ // Creating an action will perform several database queries to ensure that
+ // the action has not been overridden by the content type.
+ // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
+ // probably we may use fake context object since it's unlikely that Action uses it
+ // anyway. It would be nice if we could avoid instantiating the Action at all.
+ $actionObj = Action::factory( $action, $wikiPage, RequestContext::getMain() );
+ // Ensure that the retrieved action matches the restriction.
+ if ( $actionObj && $actionObj->getRestriction() !== $action ) {
+ $actionObj = null;
+ }
+ }
+
+ // If no action object is returned, assume that the action requires unblock
+ // which is the default.
+ if ( !$actionObj || $actionObj->requiresUnblock() ) {
+ if ( $this->isBlockedFrom( $user, $page, $useReplica ) ) {
+ // @todo FIXME: Pass the relevant context into this function.
+ $errors[] = $block->getPermissionsError( RequestContext::getMain() );
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Permissions checks that fail most often, and which are easiest to test.
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ * @throws FatalError
+ * @throws MWException
+ */
+ private function checkQuickPermissions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove when LinkTarget usage will expand further
+ $page = Title::newFromLinkTarget( $page );
+
+ if ( !Hooks::run( 'TitleQuickPermissions',
+ [ $page, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
+ ) {
+ return $errors;
+ }
+
+ $isSubPage = MWNamespace::hasSubpages( $page->getNamespace() ) ?
+ strpos( $page->getText(), '/' ) !== false : false;
+
+ if ( $action == 'create' ) {
+ if (
+ ( MWNamespace::isTalk( $page->getNamespace() ) && !$user->isAllowed( 'createtalk' ) ) ||
+ ( !MWNamespace::isTalk( $page->getNamespace() ) && !$user->isAllowed( 'createpage' ) )
+ ) {
+ $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
+ }
+ } elseif ( $action == 'move' ) {
+ if ( !$user->isAllowed( 'move-rootuserpages' )
+ && $page->getNamespace() == NS_USER && !$isSubPage ) {
+ // Show user page-specific message only if the user can move other pages
+ $errors[] = [ 'cant-move-user-page' ];
+ }
+
+ // Check if user is allowed to move files if it's a file
+ if ( $page->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
+ $errors[] = [ 'movenotallowedfile' ];
+ }
+
+ // Check if user is allowed to move category pages if it's a category page
+ if ( $page->getNamespace() == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
+ $errors[] = [ 'cant-move-category-page' ];
+ }
+
+ if ( !$user->isAllowed( 'move' ) ) {
+ // User can't move anything
+ $userCanMove = User::groupHasPermission( 'user', 'move' );
+ $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
+ if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
+ // custom message if logged-in users without any special rights can move
+ $errors[] = [ 'movenologintext' ];
+ } else {
+ $errors[] = [ 'movenotallowed' ];
+ }
+ }
+ } elseif ( $action == 'move-target' ) {
+ if ( !$user->isAllowed( 'move' ) ) {
+ // User can't move anything
+ $errors[] = [ 'movenotallowed' ];
+ } elseif ( !$user->isAllowed( 'move-rootuserpages' )
+ && $page->getNamespace() == NS_USER && !$isSubPage ) {
+ // Show user page-specific message only if the user can move other pages
+ $errors[] = [ 'cant-move-to-user-page' ];
+ } elseif ( !$user->isAllowed( 'move-categorypages' )
+ && $page->getNamespace() == NS_CATEGORY ) {
+ // Show category page-specific message only if the user can move other pages
+ $errors[] = [ 'cant-move-to-category-page' ];
+ }
+ } elseif ( !$user->isAllowed( $action ) ) {
+ $errors[] = $this->missingPermissionError( $action, $short );
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Check against page_restrictions table requirements on this
+ * page. The user must possess all required rights for this
+ * action.
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ */
+ private function checkPageRestrictions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove & rework upon further use of LinkTarget
+ $page = Title::newFromLinkTarget( $page );
+ foreach ( $page->getRestrictions( $action ) as $right ) {
+ // Backwards compatibility, rewrite sysop -> editprotected
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected';
+ }
+ // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected';
+ }
+ if ( $right == '' ) {
+ continue;
+ }
+ if ( !$user->isAllowed( $right ) ) {
+ $errors[] = [ 'protectedpagetext', $right, $action ];
+ } elseif ( $page->areRestrictionsCascading() && !$user->isAllowed( 'protect' ) ) {
+ $errors[] = [ 'protectedpagetext', 'protect', $action ];
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Check restrictions on cascading pages.
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ */
+ private function checkCascadingSourcesRestrictions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove & rework upon further use of LinkTarget
+ $page = Title::newFromLinkTarget( $page );
+ if ( $rigor !== self::RIGOR_QUICK && !$page->isUserConfigPage() ) {
+ # We /could/ use the protection level on the source page, but it's
+ # fairly ugly as we have to establish a precedence hierarchy for pages
+ # included by multiple cascade-protected pages. So just restrict
+ # it to people with 'protect' permission, as they could remove the
+ # protection anyway.
+ list( $cascadingSources, $restrictions ) = $page->getCascadeProtectionSources();
+ # Cascading protection depends on more than this page...
+ # Several cascading protected pages may include this page...
+ # Check each cascading level
+ # This is only for protection restrictions, not for all actions
+ if ( isset( $restrictions[$action] ) ) {
+ foreach ( $restrictions[$action] as $right ) {
+ // Backwards compatibility, rewrite sysop -> editprotected
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected';
+ }
+ // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected';
+ }
+ if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
+ $wikiPages = '';
+ foreach ( $cascadingSources as $wikiPage ) {
+ $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
+ }
+ $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
+ }
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Check action permissions not already checked in checkQuickPermissions
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ * @throws Exception
+ */
+ private function checkActionPermissions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ global $wgDeleteRevisionsLimit, $wgLang;
+
+ // TODO: remove & rework upon further use of LinkTarget
+ $page = Title::newFromLinkTarget( $page );
+
+ if ( $action == 'protect' ) {
+ if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
+ // If they can't edit, they shouldn't protect.
+ $errors[] = [ 'protect-cantedit' ];
+ }
+ } elseif ( $action == 'create' ) {
+ $title_protection = $page->getTitleProtection();
+ if ( $title_protection ) {
+ if ( $title_protection['permission'] == ''
+ || !$user->isAllowed( $title_protection['permission'] )
+ ) {
+ $errors[] = [
+ 'titleprotected',
+ // TODO: get rid of the User dependency
+ User::whoIs( $title_protection['user'] ),
+ $title_protection['reason']
+ ];
+ }
+ }
+ } elseif ( $action == 'move' ) {
+ // Check for immobile pages
+ if ( !MWNamespace::isMovable( $page->getNamespace() ) ) {
+ // Specific message for this case
+ $errors[] = [ 'immobile-source-namespace', $page->getNsText() ];
+ } elseif ( !$page->isMovable() ) {
+ // Less specific message for rarer cases
+ $errors[] = [ 'immobile-source-page' ];
+ }
+ } elseif ( $action == 'move-target' ) {
+ if ( !MWNamespace::isMovable( $page->getNamespace() ) ) {
+ $errors[] = [ 'immobile-target-namespace', $page->getNsText() ];
+ } elseif ( !$page->isMovable() ) {
+ $errors[] = [ 'immobile-target-page' ];
+ }
+ } elseif ( $action == 'delete' ) {
+ $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $page );
+ if ( !$tempErrors ) {
+ $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
+ $user, $tempErrors, $rigor, true, $page );
+ }
+ if ( $tempErrors ) {
+ // If protection keeps them from editing, they shouldn't be able to delete.
+ $errors[] = [ 'deleteprotected' ];
+ }
+ if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit
+ && !$this->userCan( 'bigdelete', $user, $page ) && $page->isBigDeletion()
+ ) {
+ $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
+ }
+ } elseif ( $action === 'undelete' ) {
+ if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
+ // Undeleting implies editing
+ $errors[] = [ 'undelete-cantedit' ];
+ }
+ if ( !$page->exists()
+ && count( $this->getPermissionErrorsInternal( 'create', $user, $page, $rigor, true ) )
+ ) {
+ // Undeleting where nothing currently exists implies creating
+ $errors[] = [ 'undelete-cantcreate' ];
+ }
+ }
+ return $errors;
+ }
+
+ /**
+ * Check permissions on special pages & namespaces
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ */
+ private function checkSpecialsAndNSPermissions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove & rework upon further use of LinkTarget
+ $page = Title::newFromLinkTarget( $page );
+
+ # Only 'createaccount' can be performed on special pages,
+ # which don't actually exist in the DB.
+ if ( $page->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) {
+ $errors[] = [ 'ns-specialprotected' ];
+ }
+
+ # Check $wgNamespaceProtection for restricted namespaces
+ if ( $page->isNamespaceProtected( $user ) ) {
+ $ns = $page->getNamespace() == NS_MAIN ?
+ wfMessage( 'nstab-main' )->text() : $page->getNsText();
+ $errors[] = $page->getNamespace() == NS_MEDIAWIKI ?
+ [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Check sitewide CSS/JSON/JS permissions
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ */
+ private function checkSiteConfigPermissions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove & rework upon further use of LinkTarget
+ $page = Title::newFromLinkTarget( $page );
+
+ if ( $action != 'patrol' ) {
+ $error = null;
+ // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
+ // editinterface right. That's implemented as a restriction so no check needed here.
+ if ( $page->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
+ $error = [ 'sitecssprotected', $action ];
+ } elseif ( $page->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
+ $error = [ 'sitejsonprotected', $action ];
+ } elseif ( $page->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
+ $error = [ 'sitejsprotected', $action ];
+ } elseif ( $page->isRawHtmlMessage() ) {
+ // Raw HTML can be used to deploy CSS or JS so require rights for both.
+ if ( !$user->isAllowed( 'editsitejs' ) ) {
+ $error = [ 'sitejsprotected', $action ];
+ } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
+ $error = [ 'sitecssprotected', $action ];
+ }
+ }
+
+ if ( $error ) {
+ if ( $user->isAllowed( 'editinterface' ) ) {
+ // Most users / site admins will probably find out about the new, more restrictive
+ // permissions by failing to edit something. Give them more info.
+ // TODO remove this a few release cycles after 1.32
+ $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
+ }
+ $errors[] = $error;
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Check CSS/JSON/JS sub-page permissions
+ *
+ * @param string $action The action to check
+ * @param User $user User to check
+ * @param array $errors List of current errors
+ * @param string $rigor One of PermissionManager::RIGOR_ constants
+ * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
+ * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
+ * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
+ * @param bool $short Short circuit on first error
+ *
+ * @param LinkTarget $page
+ *
+ * @return array List of errors
+ */
+ private function checkUserConfigPermissions(
+ $action,
+ User $user,
+ $errors,
+ $rigor,
+ $short,
+ LinkTarget $page
+ ) {
+ // TODO: remove & rework upon further use of LinkTarget
+ $page = Title::newFromLinkTarget( $page );
+
+ # Protect css/json/js subpages of user pages
+ # XXX: this might be better using restrictions
+
+ if ( $action === 'patrol' ) {
+ return $errors;
+ }
+
+ if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $page->getText() ) ) {
+ // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
+ if (
+ $page->isUserCssConfigPage()
+ && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
+ ) {
+ $errors[] = [ 'mycustomcssprotected', $action ];
+ } elseif (
+ $page->isUserJsonConfigPage()
+ && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
+ ) {
+ $errors[] = [ 'mycustomjsonprotected', $action ];
+ } elseif (
+ $page->isUserJsConfigPage()
+ && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
+ ) {
+ $errors[] = [ 'mycustomjsprotected', $action ];
+ }
+ } else {
+ // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
+ // deletion/suppression which cannot be used for attacks and we want to avoid the
+ // situation where an unprivileged user can post abusive content on their subpages
+ // and only very highly privileged users could remove it.
+ if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
+ if (
+ $page->isUserCssConfigPage()
+ && !$user->isAllowed( 'editusercss' )
+ ) {
+ $errors[] = [ 'customcssprotected', $action ];
+ } elseif (
+ $page->isUserJsonConfigPage()
+ && !$user->isAllowed( 'edituserjson' )
+ ) {
+ $errors[] = [ 'customjsonprotected', $action ];
+ } elseif (
+ $page->isUserJsConfigPage()
+ && !$user->isAllowed( 'edituserjs' )
+ ) {
+ $errors[] = [ 'customjsprotected', $action ];
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+}
) {
if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
$mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
+ // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive
$slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
} else {
// XXX: do we need the same kind of caching here
use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Preferences\PreferencesFactory;
use MediaWiki\Preferences\DefaultPreferencesFactory;
use MediaWiki\Revision\MainSlotRoleHandler;
);
},
+ 'PermissionManager' => function ( MediaWikiServices $services ) : PermissionManager {
+ $config = $services->getMainConfig();
+ return new PermissionManager(
+ $services->getSpecialPageFactory(),
+ $config->get( 'WhitelistRead' ),
+ $config->get( 'WhitelistReadRegexp' ),
+ $config->get( 'EmailConfirmToEdit' ),
+ $config->get( 'BlockDisablesLogin' ) );
+ },
+
'PreferencesFactory' => function ( MediaWikiServices $services ) : PreferencesFactory {
$factory = new DefaultPreferencesFactory(
$services->getMainConfig(),
* @file
*/
+use MediaWiki\Permissions\PermissionManager;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
*/
const GAID_FOR_UPDATE = 1;
+ /**
+ * Flag for use with factory methods like newFromLinkTarget() that have
+ * a $forceClone parameter. If set, the method must return a new instance.
+ * Without this flag, some factory methods may return existing instances.
+ */
+ const NEW_CLONE = 'clone';
+
/**
* @name Private member variables
* Please use the accessor functions instead.
}
/**
- * Create a new Title from a TitleValue
+ * Returns a Title given a TitleValue.
+ * If the given TitleValue is already a Title instance, that instance is returned,
+ * unless $forceClone is "clone". If $forceClone is "clone" and the given TitleValue
+ * is already a Title instance, that instance is copied using the clone operator.
*
* @param TitleValue $titleValue Assumed to be safe.
+ * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned.
*
* @return Title
*/
- public static function newFromTitleValue( TitleValue $titleValue ) {
- return self::newFromLinkTarget( $titleValue );
+ public static function newFromTitleValue( TitleValue $titleValue, $forceClone = '' ) {
+ return self::newFromLinkTarget( $titleValue, $forceClone );
}
/**
- * Create a new Title from a LinkTarget
+ * Returns a Title given a LinkTarget.
+ * If the given LinkTarget is already a Title instance, that instance is returned,
+ * unless $forceClone is "clone". If $forceClone is "clone" and the given LinkTarget
+ * is already a Title instance, that instance is copied using the clone operator.
*
* @param LinkTarget $linkTarget Assumed to be safe.
+ * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned.
*
* @return Title
*/
- public static function newFromLinkTarget( LinkTarget $linkTarget ) {
+ public static function newFromLinkTarget( LinkTarget $linkTarget, $forceClone = '' ) {
if ( $linkTarget instanceof Title ) {
// Special case if it's already a Title object
- return $linkTarget;
+ if ( $forceClone === self::NEW_CLONE ) {
+ return clone $linkTarget;
+ } else {
+ return $linkTarget;
+ }
}
return self::makeTitle(
$linkTarget->getNamespace(),
* Title objects returned by this method are guaranteed to be valid, and
* thus return true from the isValid() method.
*
+ * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
+ * It may instead be a cached instance created previously, with references to it remaining
+ * elsewhere.
+ *
* @param string|int|null $text The link text; spaces, prefixes, and an
* initial ':' indicating the main namespace are accepted.
* @param int $defaultNamespace The namespace to use if none is specified
* Title objects returned by this method are guaranteed to be valid, and
* thus return true from the isValid() method.
*
+ * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
+ * It may instead be a cached instance created previously, with references to it remaining
+ * elsewhere.
+ *
* @see Title::newFromText
*
* @since 1.25
/**
* Create a new Title for the Main Page
*
+ * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
+ * It may instead be a cached instance created previously, with references to it remaining
+ * elsewhere.
+ *
* @return Title The new object
*/
public static function newMainPage() {
// Allow unicode if a single high-bit character appears
$r0 = sprintf( '\x%02x', $ord0 );
$allowUnicode = true;
+ // @phan-suppress-next-line PhanParamSuspiciousOrder false positive
} elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
$r0 = '\\' . $d0;
} else {
*
* @param string $action Action that permission needs to be checked for
* @param User|null $user User to check (since 1.19); $wgUser will be used if not provided.
+ *
* @return bool
+ * @throws Exception
+ *
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(..) instead
+ *
*/
public function quickUserCan( $action, $user = null ) {
return $this->userCan( $action, $user, false );
* @param User|null $user User to check (since 1.19); $wgUser will be used if not
* provided.
* @param string $rigor Same format as Title::getUserPermissionsErrors()
+ *
* @return bool
+ * @throws Exception
+ *
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->userCan(..) instead
+ *
*/
- public function userCan( $action, $user = null, $rigor = 'secure' ) {
+ public function userCan( $action, $user = null, $rigor = PermissionManager::RIGOR_SECURE ) {
if ( !$user instanceof User ) {
global $wgUser;
$user = $wgUser;
}
- return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
+ // TODO: this is for b/c, eventually will be removed
+ if ( $rigor === true ) {
+ $rigor = PermissionManager::RIGOR_SECURE; // b/c
+ } elseif ( $rigor === false ) {
+ $rigor = PermissionManager::RIGOR_QUICK; // b/c
+ }
+
+ return MediaWikiServices::getInstance()->getPermissionManager()
+ ->userCan( $action, $user, $this, $rigor );
}
/**
* - secure : does cheap and expensive checks, using the master as needed
* @param array $ignoreErrors Array of Strings Set this to a list of message keys
* whose corresponding errors may be ignored.
+ *
* @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
- */
- public function getUserPermissionsErrors(
- $action, $user, $rigor = 'secure', $ignoreErrors = []
- ) {
- $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
-
- // Remove the errors being ignored.
- foreach ( $errors as $index => $error ) {
- $errKey = is_array( $error ) ? $error[0] : $error;
-
- if ( in_array( $errKey, $ignoreErrors ) ) {
- unset( $errors[$index] );
- }
- if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
- unset( $errors[$index] );
- }
- }
-
- return $errors;
- }
-
- /**
- * Permissions checks that fail most often, and which are easiest to test.
+ * @throws Exception
*
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissionsErrors()
*
- * @return array List of errors
*/
- private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
- if ( !Hooks::run( 'TitleQuickPermissions',
- [ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
- ) {
- return $errors;
- }
-
- if ( $action == 'create' ) {
- if (
- ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
- ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
- ) {
- $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
- }
- } elseif ( $action == 'move' ) {
- if ( !$user->isAllowed( 'move-rootuserpages' )
- && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
- // Show user page-specific message only if the user can move other pages
- $errors[] = [ 'cant-move-user-page' ];
- }
-
- // Check if user is allowed to move files if it's a file
- if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
- $errors[] = [ 'movenotallowedfile' ];
- }
-
- // Check if user is allowed to move category pages if it's a category page
- if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
- $errors[] = [ 'cant-move-category-page' ];
- }
-
- if ( !$user->isAllowed( 'move' ) ) {
- // User can't move anything
- $userCanMove = User::groupHasPermission( 'user', 'move' );
- $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
- if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
- // custom message if logged-in users without any special rights can move
- $errors[] = [ 'movenologintext' ];
- } else {
- $errors[] = [ 'movenotallowed' ];
- }
- }
- } elseif ( $action == 'move-target' ) {
- if ( !$user->isAllowed( 'move' ) ) {
- // User can't move anything
- $errors[] = [ 'movenotallowed' ];
- } elseif ( !$user->isAllowed( 'move-rootuserpages' )
- && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
- // Show user page-specific message only if the user can move other pages
- $errors[] = [ 'cant-move-to-user-page' ];
- } elseif ( !$user->isAllowed( 'move-categorypages' )
- && $this->mNamespace == NS_CATEGORY ) {
- // Show category page-specific message only if the user can move other pages
- $errors[] = [ 'cant-move-to-category-page' ];
- }
- } elseif ( !$user->isAllowed( $action ) ) {
- $errors[] = $this->missingPermissionError( $action, $short );
+ public function getUserPermissionsErrors(
+ $action, $user, $rigor = PermissionManager::RIGOR_SECURE, $ignoreErrors = []
+ ) {
+ // TODO: this is for b/c, eventually will be removed
+ if ( $rigor === true ) {
+ $rigor = PermissionManager::RIGOR_SECURE; // b/c
+ } elseif ( $rigor === false ) {
+ $rigor = PermissionManager::RIGOR_QUICK; // b/c
}
- return $errors;
+ return MediaWikiServices::getInstance()->getPermissionManager()
+ ->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors );
}
/**
return $errors;
}
- /**
- * Check various permission hooks
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
- // Use getUserPermissionsErrors instead
- $result = '';
- // Avoid PHP 7.1 warning from passing $this by reference
- $titleRef = $this;
- if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) {
- return $result ? [] : [ [ 'badaccess-group0' ] ];
- }
- // Check getUserPermissionsErrors hook
- // Avoid PHP 7.1 warning from passing $this by reference
- $titleRef = $this;
- if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) {
- $errors = $this->resultToError( $errors, $result );
- }
- // Check getUserPermissionsErrorsExpensive hook
- if (
- $rigor !== 'quick'
- && !( $short && count( $errors ) > 0 )
- && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$user, $action, &$result ] )
- ) {
- $errors = $this->resultToError( $errors, $result );
- }
-
- return $errors;
- }
-
- /**
- * Check permissions on special pages & namespaces
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
- # Only 'createaccount' can be performed on special pages,
- # which don't actually exist in the DB.
- if ( $this->isSpecialPage() && $action !== 'createaccount' ) {
- $errors[] = [ 'ns-specialprotected' ];
- }
-
- # Check $wgNamespaceProtection for restricted namespaces
- if ( $this->isNamespaceProtected( $user ) ) {
- $ns = $this->mNamespace == NS_MAIN ?
- wfMessage( 'nstab-main' )->text() : $this->getNsText();
- $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
- [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
- }
-
- return $errors;
- }
-
- /**
- * Check sitewide CSS/JSON/JS permissions
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short ) {
- if ( $action != 'patrol' ) {
- $error = null;
- // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
- // editinterface right. That's implemented as a restriction so no check needed here.
- if ( $this->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
- $error = [ 'sitecssprotected', $action ];
- } elseif ( $this->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
- $error = [ 'sitejsonprotected', $action ];
- } elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
- $error = [ 'sitejsprotected', $action ];
- } elseif ( $this->isRawHtmlMessage() ) {
- // Raw HTML can be used to deploy CSS or JS so require rights for both.
- if ( !$user->isAllowed( 'editsitejs' ) ) {
- $error = [ 'sitejsprotected', $action ];
- } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
- $error = [ 'sitecssprotected', $action ];
- }
- }
-
- if ( $error ) {
- if ( $user->isAllowed( 'editinterface' ) ) {
- // Most users / site admins will probably find out about the new, more restrictive
- // permissions by failing to edit something. Give them more info.
- // TODO remove this a few release cycles after 1.32
- $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
- }
- $errors[] = $error;
- }
- }
-
- return $errors;
- }
-
- /**
- * Check CSS/JSON/JS sub-page permissions
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
- # Protect css/json/js subpages of user pages
- # XXX: this might be better using restrictions
-
- if ( $action === 'patrol' ) {
- return $errors;
- }
-
- if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
- // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
- if (
- $this->isUserCssConfigPage()
- && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
- ) {
- $errors[] = [ 'mycustomcssprotected', $action ];
- } elseif (
- $this->isUserJsonConfigPage()
- && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
- ) {
- $errors[] = [ 'mycustomjsonprotected', $action ];
- } elseif (
- $this->isUserJsConfigPage()
- && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
- ) {
- $errors[] = [ 'mycustomjsprotected', $action ];
- }
- } else {
- // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
- // deletion/suppression which cannot be used for attacks and we want to avoid the
- // situation where an unprivileged user can post abusive content on their subpages
- // and only very highly privileged users could remove it.
- if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
- if (
- $this->isUserCssConfigPage()
- && !$user->isAllowed( 'editusercss' )
- ) {
- $errors[] = [ 'customcssprotected', $action ];
- } elseif (
- $this->isUserJsonConfigPage()
- && !$user->isAllowed( 'edituserjson' )
- ) {
- $errors[] = [ 'customjsonprotected', $action ];
- } elseif (
- $this->isUserJsConfigPage()
- && !$user->isAllowed( 'edituserjs' )
- ) {
- $errors[] = [ 'customjsprotected', $action ];
- }
- }
- }
-
- return $errors;
- }
-
- /**
- * Check against page_restrictions table requirements on this
- * page. The user must possess all required rights for this
- * action.
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
- foreach ( $this->getRestrictions( $action ) as $right ) {
- // Backwards compatibility, rewrite sysop -> editprotected
- if ( $right == 'sysop' ) {
- $right = 'editprotected';
- }
- // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
- if ( $right == 'autoconfirmed' ) {
- $right = 'editsemiprotected';
- }
- if ( $right == '' ) {
- continue;
- }
- if ( !$user->isAllowed( $right ) ) {
- $errors[] = [ 'protectedpagetext', $right, $action ];
- } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
- $errors[] = [ 'protectedpagetext', 'protect', $action ];
- }
- }
-
- return $errors;
- }
-
- /**
- * Check restrictions on cascading pages.
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
- if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
- # We /could/ use the protection level on the source page, but it's
- # fairly ugly as we have to establish a precedence hierarchy for pages
- # included by multiple cascade-protected pages. So just restrict
- # it to people with 'protect' permission, as they could remove the
- # protection anyway.
- list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
- # Cascading protection depends on more than this page...
- # Several cascading protected pages may include this page...
- # Check each cascading level
- # This is only for protection restrictions, not for all actions
- if ( isset( $restrictions[$action] ) ) {
- foreach ( $restrictions[$action] as $right ) {
- // Backwards compatibility, rewrite sysop -> editprotected
- if ( $right == 'sysop' ) {
- $right = 'editprotected';
- }
- // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
- if ( $right == 'autoconfirmed' ) {
- $right = 'editsemiprotected';
- }
- if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
- $pages = '';
- foreach ( $cascadingSources as $page ) {
- $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
- }
- $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
- }
- }
- }
- }
-
- return $errors;
- }
-
- /**
- * Check action permissions not already checked in checkQuickPermissions
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
- global $wgDeleteRevisionsLimit, $wgLang;
-
- if ( $action == 'protect' ) {
- if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
- // If they can't edit, they shouldn't protect.
- $errors[] = [ 'protect-cantedit' ];
- }
- } elseif ( $action == 'create' ) {
- $title_protection = $this->getTitleProtection();
- if ( $title_protection ) {
- if ( $title_protection['permission'] == ''
- || !$user->isAllowed( $title_protection['permission'] )
- ) {
- $errors[] = [
- 'titleprotected',
- User::whoIs( $title_protection['user'] ),
- $title_protection['reason']
- ];
- }
- }
- } elseif ( $action == 'move' ) {
- // Check for immobile pages
- if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
- // Specific message for this case
- $errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
- } elseif ( !$this->isMovable() ) {
- // Less specific message for rarer cases
- $errors[] = [ 'immobile-source-page' ];
- }
- } elseif ( $action == 'move-target' ) {
- if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
- $errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
- } elseif ( !$this->isMovable() ) {
- $errors[] = [ 'immobile-target-page' ];
- }
- } elseif ( $action == 'delete' ) {
- $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
- if ( !$tempErrors ) {
- $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
- $user, $tempErrors, $rigor, true );
- }
- if ( $tempErrors ) {
- // If protection keeps them from editing, they shouldn't be able to delete.
- $errors[] = [ 'deleteprotected' ];
- }
- if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
- && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
- ) {
- $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
- }
- } elseif ( $action === 'undelete' ) {
- if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
- // Undeleting implies editing
- $errors[] = [ 'undelete-cantedit' ];
- }
- if ( !$this->exists()
- && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
- ) {
- // Undeleting where nothing currently exists implies creating
- $errors[] = [ 'undelete-cantcreate' ];
- }
- }
- return $errors;
- }
-
- /**
- * Check that the user isn't blocked from editing.
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
- global $wgEmailConfirmToEdit, $wgBlockDisablesLogin;
- // Account creation blocks handled at userlogin.
- // Unblocking handled in SpecialUnblock
- if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
- return $errors;
- }
-
- // Optimize for a very common case
- if ( $action === 'read' && !$wgBlockDisablesLogin ) {
- return $errors;
- }
-
- if ( $wgEmailConfirmToEdit
- && !$user->isEmailConfirmed()
- && $action === 'edit'
- ) {
- $errors[] = [ 'confirmedittext' ];
- }
-
- $useReplica = ( $rigor !== 'secure' );
- $block = $user->getBlock( $useReplica );
-
- // If the user does not have a block, or the block they do have explicitly
- // allows the action (like "read" or "upload").
- if ( !$block || $block->appliesToRight( $action ) === false ) {
- return $errors;
- }
-
- // Determine if the user is blocked from this action on this page.
- // What gets passed into this method is a user right, not an action name.
- // There is no way to instantiate an action by restriction. However, this
- // will get the action where the restriction is the same. This may result
- // in actions being blocked that shouldn't be.
- $actionObj = null;
- if ( Action::exists( $action ) ) {
- // Clone the title to prevent mutations to this object which is done
- // by Title::loadFromRow() in WikiPage::loadFromRow().
- $page = WikiPage::factory( clone $this );
- // Creating an action will perform several database queries to ensure that
- // the action has not been overridden by the content type.
- // @todo FIXME: Pass the relevant context into this function.
- $actionObj = Action::factory( $action, $page, RequestContext::getMain() );
- // Ensure that the retrieved action matches the restriction.
- if ( $actionObj && $actionObj->getRestriction() !== $action ) {
- $actionObj = null;
- }
- }
-
- // If no action object is returned, assume that the action requires unblock
- // which is the default.
- if ( !$actionObj || $actionObj->requiresUnblock() ) {
- if ( $user->isBlockedFrom( $this, $useReplica ) ) {
- // @todo FIXME: Pass the relevant context into this function.
- $errors[] = $block->getPermissionsError( RequestContext::getMain() );
- }
- }
-
- return $errors;
- }
-
- /**
- * Check that the user is allowed to read this page.
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
- global $wgWhitelistRead, $wgWhitelistReadRegexp;
-
- $whitelisted = false;
- if ( User::isEveryoneAllowed( 'read' ) ) {
- # Shortcut for public wikis, allows skipping quite a bit of code
- $whitelisted = true;
- } elseif ( $user->isAllowed( 'read' ) ) {
- # If the user is allowed to read pages, he is allowed to read all pages
- $whitelisted = true;
- } elseif ( $this->isSpecial( 'Userlogin' )
- || $this->isSpecial( 'PasswordReset' )
- || $this->isSpecial( 'Userlogout' )
- ) {
- # Always grant access to the login page.
- # Even anons need to be able to log in.
- $whitelisted = true;
- } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
- # Time to check the whitelist
- # Only do these checks is there's something to check against
- $name = $this->getPrefixedText();
- $dbName = $this->getPrefixedDBkey();
-
- // Check for explicit whitelisting with and without underscores
- if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
- $whitelisted = true;
- } elseif ( $this->mNamespace == NS_MAIN ) {
- # Old settings might have the title prefixed with
- # a colon for main-namespace pages
- if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
- $whitelisted = true;
- }
- } elseif ( $this->isSpecialPage() ) {
- # If it's a special page, ditch the subpage bit and check again
- $name = $this->mDbkeyform;
- list( $name, /* $subpage */ ) =
- MediaWikiServices::getInstance()->getSpecialPageFactory()->
- resolveAlias( $name );
- if ( $name ) {
- $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
- if ( in_array( $pure, $wgWhitelistRead, true ) ) {
- $whitelisted = true;
- }
- }
- }
- }
-
- if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
- $name = $this->getPrefixedText();
- // Check for regex whitelisting
- foreach ( $wgWhitelistReadRegexp as $listItem ) {
- if ( preg_match( $listItem, $name ) ) {
- $whitelisted = true;
- break;
- }
- }
- }
-
- if ( !$whitelisted ) {
- # If the title is not whitelisted, give extensions a chance to do so...
- Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
- if ( !$whitelisted ) {
- $errors[] = $this->missingPermissionError( $action, $short );
- }
- }
-
- return $errors;
- }
-
- /**
- * Get a description array when the user doesn't have the right to perform
- * $action (i.e. when User::isAllowed() returns false)
- *
- * @param string $action The action to check
- * @param bool $short Short circuit on first error
- * @return array Array containing an error message key and any parameters
- */
- private function missingPermissionError( $action, $short ) {
- // We avoid expensive display logic for quickUserCan's and such
- if ( $short ) {
- return [ 'badaccess-group0' ];
- }
-
- return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
- }
-
- /**
- * Can $user perform $action on this page? This is an internal function,
- * with multiple levels of checks depending on performance needs; see $rigor below.
- * It does not check wfReadOnly().
- *
- * @param string $action Action that permission needs to be checked for
- * @param User $user User to check
- * @param string $rigor One of (quick,full,secure)
- * - quick : does cheap permission checks from replica DBs (usable for GUI creation)
- * - full : does cheap and expensive checks possibly from a replica DB
- * - secure : does cheap and expensive checks, using the master as needed
- * @param bool $short Set this to true to stop after the first permission error.
- * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
- */
- protected function getUserPermissionsErrorsInternal(
- $action, $user, $rigor = 'secure', $short = false
- ) {
- if ( $rigor === true ) {
- $rigor = 'secure'; // b/c
- } elseif ( $rigor === false ) {
- $rigor = 'quick'; // b/c
- } elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
- throw new Exception( "Invalid rigor parameter '$rigor'." );
- }
-
- # Read has special handling
- if ( $action == 'read' ) {
- $checks = [
- 'checkPermissionHooks',
- 'checkReadPermissions',
- 'checkUserBlock', // for wgBlockDisablesLogin
- ];
- # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
- # or checkUserConfigPermissions here as it will lead to duplicate
- # error messages. This is okay to do since anywhere that checks for
- # create will also check for edit, and those checks are called for edit.
- } elseif ( $action == 'create' ) {
- $checks = [
- 'checkQuickPermissions',
- 'checkPermissionHooks',
- 'checkPageRestrictions',
- 'checkCascadingSourcesRestrictions',
- 'checkActionPermissions',
- 'checkUserBlock'
- ];
- } else {
- $checks = [
- 'checkQuickPermissions',
- 'checkPermissionHooks',
- 'checkSpecialsAndNSPermissions',
- 'checkSiteConfigPermissions',
- 'checkUserConfigPermissions',
- 'checkPageRestrictions',
- 'checkCascadingSourcesRestrictions',
- 'checkActionPermissions',
- 'checkUserBlock'
- ];
- }
-
- $errors = [];
- foreach ( $checks as $method ) {
- $errors = $this->$method( $action, $user, $errors, $rigor, $short );
-
- if ( $short && $errors !== [] ) {
- break;
- }
- }
-
- return $errors;
- }
-
/**
* Get a filtered list of all restriction types supported by this wiki.
* @param bool $exists True to get all restriction types that apply to
protected $undo = 0, $undoafter = 0, $cur = 0;
- /** @param RevisionRecord|null */
+ /** @var RevisionRecord|null */
protected $curRev = null;
public function getName() {
// $results if all are done.
unset( $targets[$placeholder] );
$placeholder = '{' . $placeholder . '}';
+ // @phan-suppress-next-line PhanTypeNoAccessiblePropertiesForeach
foreach ( $results[$target] as $value ) {
if ( !preg_match( '/^[^{}]*$/', $value ) ) {
// Skip values that make invalid parameter names.
// Some modules are unfinished: include those params, and copy
// the generator params.
foreach ( $continuationData as $module => $kvp ) {
+ // XXX: Not sure why phan is complaining here...
+ // @phan-suppress-next-line PhanTypeInvalidLeftOperand
$data += $kvp;
}
$generatorParams = [];
"apihelp-query+userinfo-paramvalue-prop-registrationdate": "يضيف تاريخ تسجيل المستخدم.",
"apihelp-query+userinfo-paramvalue-prop-unreadcount": "يضيف عدد الصفحات غير المقروءة في قائمة مراقبة المستخدم (بحد أقصى $1; ترجع <samp>$2</samp> إذا كان أكثر).",
"apihelp-query+userinfo-paramvalue-prop-centralids": "يضيف المعرفات المركزية وحالة المرفقات للمستخدم.",
+ "apihelp-query+userinfo-paramvalue-prop-latestcontrib": "يضيف تاريخ آخر مساهمة للمستخدم.",
"apihelp-query+userinfo-param-attachedwiki": "باستخدام <kbd>$1prop=centralids</kbd>، حدد ما إذا كان المستخدم مرتبطا بالويكي المحدد بواسطة هذا المعرف.",
"apihelp-query+userinfo-example-simple": "الحصول على معلومات حول المستخدم الحالي.",
"apihelp-query+userinfo-example-data": "الحصول على معلومات حول المستخدم الحالي.",
"apihelp-query+userinfo-paramvalue-prop-options": "Listet alle Einstellungen auf, die der aktuelle Benutzer festgelegt hat.",
"apihelp-query+userinfo-paramvalue-prop-editcount": "Ergänzt den Bearbeitungszähler des aktuellen Benutzers.",
"apihelp-query+userinfo-paramvalue-prop-realname": "Fügt den bürgerlichen Namen des Benutzers hinzu.",
+ "apihelp-query+userinfo-paramvalue-prop-latestcontrib": "Ergänzt das Datum des letzten Benutzerbeitrags.",
"apihelp-query+userinfo-example-simple": "Informationen über den aktuellen Benutzer abrufen",
"apihelp-query+userinfo-example-data": "Ruft zusätzliche Informationen über den aktuellen Benutzer ab.",
"apihelp-query+users-summary": "Informationen über eine Liste von Benutzern abrufen.",
"apihelp-query+userinfo-paramvalue-prop-registrationdate": "Ajoute la date d’inscription de l’utilisateur.",
"apihelp-query+userinfo-paramvalue-prop-unreadcount": "Ajoute le compteur de pages non lues de la liste de suivi de l’utilisateur (au maximum $1 ; renvoie <samp>$2</samp> s’il y en a plus).",
"apihelp-query+userinfo-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.",
+ "apihelp-query+userinfo-paramvalue-prop-latestcontrib": "Ajoute la date de la dernière contribution de l'utilisateur.",
"apihelp-query+userinfo-param-attachedwiki": "Avec <kbd>$1prop=centralids</kbd>, indiquer si l’utilisateur est attaché au wiki identifié par cet ID.",
"apihelp-query+userinfo-example-simple": "Obtenir des informations sur l’utilisateur actuel.",
"apihelp-query+userinfo-example-data": "Obtenir des informations supplémentaires sur l’utilisateur actuel.",
"apihelp-query+userinfo-paramvalue-prop-registrationdate": "添加使用者的註冊日期。",
"apihelp-query+userinfo-paramvalue-prop-unreadcount": "添加未讀頁面數目在使用者的監視清單(最多 $1,若有更多則回傳 <samp>$2</samp>)。",
"apihelp-query+userinfo-paramvalue-prop-centralids": "替使用者添加中心 ID 與附加狀態。",
+ "apihelp-query+userinfo-paramvalue-prop-latestcontrib": "添加使用者最新貢獻的日期。",
"apihelp-query+userinfo-param-attachedwiki": "以 <kbd>$1prop=centralids</kbd> 來表明使用者是否附加於由此 ID 所識別出的 wiki。",
"apihelp-query+userinfo-example-simple": "取得目前使用者的資訊。",
"apihelp-query+userinfo-example-data": "取得目前使用者的額外資訊。",
* @return AuthenticationRequest
*/
public static function __set_state( $data ) {
+ // @phan-suppress-next-line PhanTypeInstantiateAbstract
$ret = new static();
foreach ( $data as $k => $v ) {
$ret->$k = $v;
use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\Restriction\Restriction;
+use MWException;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
* @inheritDoc
*/
public static function newFromRow( \stdClass $row ) {
+ // @phan-suppress-next-line PhanTypeInstantiateAbstract
return new static( $row->ir_ipb_id, $row->ir_value );
}
$version = substr( $version, 0, $dashPosition );
}
- $version = implode( '.', array_pad( explode( '.', $version ), 4, '0' ) );
+ $version = implode( '.', array_pad( explode( '.', $version, 4 ), 4, '0' ) );
if ( $dashPosition !== false ) {
$version .= $suffix;
$newText,
2
);
-
- // Log a warning in case the configuration value is set to not silently ignore it
- if ( $this->wikiDiff2MovedParagraphDetectionCutoff > 0 ) {
- wfLogWarning( '$wgWikiDiff2MovedParagraphDetectionCutoff is set but has no
- effect since the used version of WikiDiff2 does not support it.' );
- }
}
return $text;
* HTML to build the textbox1 on edit conflicts
*
* @param array $customAttribs
- * @return string HTML
*/
public function getEditConflictMainTextBox( array $customAttribs = [] ) {
$builder = new TextboxBuilder();
}
$config['logger'] = LoggerFactory::getInstance( 'LockManager' );
+ // @phan-suppress-next-line PhanTypeInstantiateAbstract
$this->managers[$name]['instance'] = new $class( $config );
}
* @ingroup FileAbstraction
*/
+// @phan-file-suppress PhanTypeMissingReturn false positives
/**
* Implements some public methods and some protected utility functions which
* are required by multiple child classes. Contains stub functionality for
/**
* @param int $page
- * @return int|number
+ * @return int
*/
public function getWidth( $page = 1 ) {
return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0;
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\DBUnexpectedError;
+// @phan-file-suppress PhanTypeMissingReturn false positives
/**
* Foreign file with an accessible MediaWiki database
*
/** isVisible inherited */
/**
+ * Checks if this file exists in its parent repo, as referenced by its
+ * virtual URL.
+ *
* @return bool
*/
function isMissing() {
if ( $this->missing === null ) {
- list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
+ $fileExists = $this->repo->fileExists( $this->getVirtualUrl() );
$this->missing = !$fileExists;
}
[ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
}
+ /**
+ * @inheritDoc
+ * @return OOUI\PanelLayout
+ */
protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) {
// to get a user visible effect, wrap the fieldset into a framed panel layout
$layout = new OOUI\PanelLayout( [
/**
* @param WebRequest $request
*
- * @return array("<overall message>","<select value>","<text field value>")
+ * @return array ["<overall message>","<select value>","<text field value>"]
*/
public function loadDataFromRequest( $request ) {
if ( $request->getCheck( $this->mName ) ) {
/** @var array */
public static $sourceRegistrations = [];
- /** @var string */
+ /** @var ImportSource */
private $mSource;
/** @var string */
parent::applyPatch( $path, $isFullPath, $msg );
$this->db->scrollableCursor( $prevScroll );
$this->db->prepareStatements( $prevPrep );
+ return true;
}
/**
if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
$this->output( "...skipping: '$table' table doesn't exist yet.\n" );
- return;
+ return true;
}
// Second requirement: the new index must be missing
" $old should be manually removed if not needed anymore.\n" );
}
- return;
+ return true;
}
// Third requirement: the old index must exist
if ( !$this->db->indexExists( $table, $old, __METHOD__ ) ) {
$this->output( "...skipping: index $old doesn't exist.\n" );
- return;
+ return true;
}
$this->db->query( "ALTER INDEX $old RENAME TO $new" );
+ return true;
}
protected function dropPgField( $table, $field ) {
$this->parent->restoreLinkPopups();
$this->endForm( false, false );
+ return '';
}
}
$this->parent->output->addWikiTextAsInterface( $text );
$this->startForm();
$this->endForm( false );
+ return '';
}
/**
"config-sqlite-cant-create-db": "Impossibile creare il file di database <code>$1</code> .",
"config-sqlite-fts3-downgrade": "Il PHP è mancante del supporto FTS3, declassamento tabelle in corso",
"config-can-upgrade": "Ci sono tabelle di MediaWiki in questo database.\nPer aggiornarle a MediaWiki $1, fai clic su '''continua'''.",
+ "config-upgrade-error": "Si è verificato un errore durante l'aggiornamento delle tabelle MediaWiki nel tuo database.\n\nPer ulteriori informazioni guarda nel registro qui sopra, per riprovare clicca su <strong>Continua</strong>.",
"config-upgrade-done": "Aggiornamento completo.\n\nPuoi [$1 iniziare ad usare il tuo wiki].\n\nSe vuoi rigenerare il tuo file <code>LocalSettings.php</code>, clicca sul pulsante sotto. Questa operazione '''non è raccomandata''', a meno che non hai problemi con il tuo wiki.",
"config-upgrade-done-no-regenerate": "Aggiornamento completo.\n\nPuoi [$1 iniziare ad usare il tuo wiki].",
"config-regenerate": "Rigenera LocalSettings.php →",
"config-install-mainpage-failed": "Impossibile inserire la pagina principale: $1",
"config-install-done": "<strong>Complimenti!</strong>\nHai installato MediaWiki.\n\nIl programma di installazione ha generato un file <code>LocalSettings.php</code> che contiene tutte le impostazioni.\n\nDevi scaricarlo ed inserirlo nella directory base del tuo wiki (la stessa dove è presente index.php). Il download dovrebbe partire automaticamente.\n\nSe il download non si avvia, o se è stato annullato, puoi riavviarlo cliccando sul collegamento di seguito:\n\n$3\n\n<strong>Nota:</strong> se esci ora dall'installazione senza scaricare il file di configurazione che è stato generato, questo poi non sarà più disponibile in seguito.\n\nQuando hai fatto, puoi <strong>[$2 entrare nel tuo wiki]</strong>.",
"config-install-done-path": "<strong>Complimenti!</strong>\nHai installato MediaWiki.\n\nIl programma di installazione ha generato un file <code>LocalSettings.php</code> che contiene tutte le impostazioni.\n\nDevi scaricarlo ed inserirlo in <code>$4</code>. Il download dovrebbe partire automaticamente.\n\nSe il download non si avvia, o se è stato annullato, puoi riavviarlo cliccando sul collegamento seguente:\n\n$3\n\n<strong>Nota:</strong> se esci ora dall'installazione senza scaricare il file di configurazione che è stato generato, questo poi non sarà più disponibile in seguito.\n\nQuando hai fatto, puoi <strong>[$2 entrare nel tuo wiki]</strong>.",
+ "config-install-success": "MediaWiki è stato installato corretamente. Ora puoi visitare <$1$2> per vedere il tuo wiki.\nSe hai domande, controlla la nostra lista di domande frequenti:\n<https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ> o usa usa uno dei forum di supporto riepilogati su quella pagina.",
+ "config-install-db-success": "Il database è stato configurato correttamente",
"config-download-localsettings": "Scarica <code>LocalSettings.php</code>",
"config-help": "aiuto",
"config-help-tooltip": "fai clic per espandere",
* Create the appropriate object to handle a specific job
*
* @param string $command Job command
- * @param array $params Job parameters
+ * @param array|Title $params Job parameters
* @throws InvalidArgumentException
* @return Job
*/
/**
* @param string $command
- * @param array $params
+ * @param array|Title|null $params
*/
- public function __construct( $command, $params = [] ) {
+ public function __construct( $command, $params = null ) {
if ( $params instanceof Title ) {
// Backwards compatibility for old signature ($command, $title, $params)
$title = $params;
- $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
+ $params = func_get_arg( 2 );
} else {
// Subclasses can override getTitle() to return something more meaningful
$title = Title::makeTitle( NS_SPECIAL, 'Blankpage' );
$this->command = $command;
$this->title = $title;
- $this->params = is_array( $params ) ? $params : []; // sanity
+ $this->params = is_array( $params ) ? $params : [];
if ( !isset( $this->params['requestId'] ) ) {
$this->params['requestId'] = WebRequest::getRequestId();
}
$firstBatch = false;
}
} while ( $idsToUpdate );
+ return true;
}
}
* to an integer network and a number of bits
*
* @param string $range IP with CIDR prefix
- * @return array(int or string, int)
+ * @return array [int or string, int]
*/
public static function parseCIDR( $range ) {
if ( self::isIPv6( $range ) ) {
*
* @param string $range
*
- * @return array(string, int)
+ * @return array [string, int]
*/
private static function parseCIDR6( $range ) {
# Explode into <expanded IP,range>
*
* @param string $range
*
- * @return array(string, string)
+ * @return array [string, string]
*/
private static function parseRange6( $range ) {
# Expand any IPv6 IP
if ( $eocdrPos !== false ) {
$this->logger->info( __METHOD__ . ": ZIP signature present in $file\n" );
// Check if it really is a ZIP file, make sure the EOCDR is at the end (T40432)
+ // FIXME: unpack()'s third argument was added in PHP 7.1
+ // @phan-suppress-next-line PhanParamTooManyInternal
$commentLength = unpack( "n", $tail, $eocdrPos + 20 )[0];
if ( $eocdrPos + 22 + $commentLength !== strlen( $tail ) ) {
$this->logger->info( __METHOD__ . ": ZIP EOCDR not at end. Not a ZIP file." );
* Additional parsing options
*/
private $parserOptions = [
- 'processing_instruction_handler' => '',
+ 'processing_instruction_handler' => null,
'external_dtd_handler' => '',
'dtd_handler' => '',
'require_safe_dtd' => true
* Memcache initializer
*
* @param array $args Associative array of settings
- *
- * @return mixed
*/
public function __construct( $args ) {
$this->set_servers( $args['servers'] ?? array() );
return $params;
}
+ /**
+ * @suppress PhanTypeNonVarPassByRef
+ */
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$this->debugLog( "get($key)" );
if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0
/**
* @param BagOStuff $store
- * @param array[] $client Map of (ip: <IP>, agent: <user-agent> [, clientId: <hash>] )
+ * @param array $client Map of (ip: <IP>, agent: <user-agent> [, clientId: <hash>] )
* @param int|null $posIndex Write counter index [optional]
* @since 1.27
*/
list( $phpCallback ) = $callback;
$this->clearFlag( self::DBO_TRX ); // make each query its own transaction
try {
+ // @phan-suppress-next-line PhanParamTooManyCallable
call_user_func( $phpCallback, $trigger, $this );
} catch ( Exception $ex ) {
call_user_func( $this->errorLogger, $ex );
*
* @param callable $callback
* @param string $fname Caller name
- * @return mixed
* @since 1.28
*/
public function onTransactionResolution( callable $callback, $fname = __METHOD__ );
*
* @param callable $callback
* @param string $fname
- * @return mixed
* @since 1.20
* @deprecated Since 1.32
*/
*
* @param string $name Callback name
* @param callable|null $callback Use null to unset a listener
- * @return mixed
* @since 1.28
*/
public function setTransactionListener( $name, callable $callback = null );
* the aliases can be removed, and then the old X-named indexes dropped.
*
* @param string[] $aliases
- * @return mixed
* @since 1.31
*/
public function setIndexAliases( array $aliases );
/** @noinspection PhpMissingParentConstructorInspection */
/**
- * @param string $data
+ * @param Blob|array|string $data
*/
public function __construct( $data ) {
if ( $data instanceof MssqlBlob ) {
- return $data;
+ $this->data = $data->data;
} elseif ( $data instanceof Blob ) {
$this->data = $data->fetch();
} elseif ( is_array( $data ) && is_object( $data ) ) {
* the aliases can be removed, and then the old X-named indexes dropped.
*
* @param string[] $aliases
- * @return mixed
* @since 1.31
*/
public function setIndexAliases( array $aliases );
* the aliases can be removed, and then the old X-named indexes dropped.
*
* @param string[] $aliases
- * @return mixed
* @since 1.31
*/
public function setIndexAliases( array $aliases );
* @ingroup Media
*/
+use MediaWiki\Shell\Shell;
+
/**
* JPEG specific handler.
* Inherits most stuff from BitmapHandler, just here to do the metadata handler differently.
$rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
if ( $wgJpegTran && is_executable( $wgJpegTran ) ) {
- $cmd = wfEscapeShellArg( $wgJpegTran ) .
- " -rotate " . wfEscapeShellArg( $rotation ) .
- " -outfile " . wfEscapeShellArg( $params['dstPath'] ) .
- " " . wfEscapeShellArg( $params['srcPath'] );
- wfDebug( __METHOD__ . ": running jpgtran: $cmd\n" );
- $retval = 0;
- $err = wfShellExecWithStderr( $cmd, $retval );
- if ( $retval !== 0 ) {
- $this->logErrorForExternalProcess( $retval, $err, $cmd );
-
- return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
+ $command = Shell::command( $wgJpegTran,
+ '-rotate',
+ $rotation,
+ '-outfile',
+ $params['dstPath'],
+ $params['srcPath']
+ );
+ $result = $command
+ ->includeStderr()
+ ->execute();
+ if ( $result->getExitCode() !== 0 ) {
+ $this->logErrorForExternalProcess( $result->getExitCode(),
+ $result->getStdout(),
+ $command
+ );
+
+ return new MediaTransformError( 'thumbnail_error', 0, 0, $result->getStdout() );
}
return false;
return false;
}
- $cmd = wfEscapeShellArg( $wgExiftool,
+ $result = Shell::command(
+ $wgExiftool,
'-EXIF:ColorSpace',
'-ICC_Profile:ProfileDescription',
'-S',
'-T',
$filepath
- );
-
- $output = wfShellExecWithStderr( $cmd, $retval );
+ )
+ ->includeStderr()
+ ->execute();
// Explode EXIF data into an array with [0 => Color Space, 1 => Device Model Desc]
- $data = explode( "\t", trim( $output ) );
+ $data = explode( "\t", trim( $result->getStdout() ) );
- if ( $retval !== 0 ) {
+ if ( $result->getExitCode() !== 0 ) {
return false;
}
return false;
}
- $cmd = wfEscapeShellArg( $wgExiftool,
+ $command = Shell::command( $wgExiftool,
'-overwrite_original',
'-icc_profile<=' . $profileFilepath,
$filepath
);
-
- $output = wfShellExecWithStderr( $cmd, $retval );
-
- if ( $retval !== 0 ) {
- $this->logErrorForExternalProcess( $retval, $output, $cmd );
+ $result = $command
+ ->includeStderr()
+ ->execute();
+
+ if ( $result->getExitCode() !== 0 ) {
+ $this->logErrorForExternalProcess( $result->getExitCode(),
+ $result->getStdout(),
+ $command
+ );
return false;
}
* @return TitleArray|Title[]
*/
public function getForeignCategories() {
- $this->mPage->getForeignCategories();
+ return $this->mPage->getForeignCategories();
}
}
* @param Content|null $content Page content to be used when determining
* the required updates. This may be needed because $this->getContent()
* may already return null when the page proper was deleted.
- * @param RevisionRecord|Revision|null $revision The current page revision at the time of
+ * @param Revision|null $revision The current page revision at the time of
* deletion, used when determining the required updates. This may be needed because
* $this->getRevision() may already return null when the page proper was deleted.
* @param User|null $user The user that caused the deletion
}
public function getFunctionStats() {
+ return [];
}
public function getOutput() {
+ return '';
}
public function close() {
public function send( array $feed, $line ) {
$transport = UDPTransport::newFromString( $feed['uri'] );
$transport->emit( $line );
+ return true;
}
}
* @throws MWException If a duplicate module registration is attempted
* @throws MWException If a module name contains illegal characters (pipes or commas)
* @throws MWException If something other than a ResourceLoaderModule is being registered
- * @return bool False if there were any errors, in which case one or more modules were
- * not registered
*/
public function register( $name, $info = null ) {
$moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
* @return $this
*/
public function setFlag( $flag, $unset = false ) {
+ return $this;
}
/**
// Extension::OATHAuth.
// Unseal and check
- $pieces = explode( '.', $encrypted );
+ $pieces = explode( '.', $encrypted, 4 );
if ( count( $pieces ) !== 3 ) {
$ex = new \Exception( 'Invalid sealed-secret format' );
$this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
* are shown closer to the bottom; weight defaults to 0. Negative weight is allowed.)
* Keep order if weights are equal.
* @param array &$formDescriptor
- * @return array
*/
protected static function sortFormDescriptorFields( array &$formDescriptor ) {
$i = 0;
*
* There is light processing to simplify core maintenance.
* @param array $definition
+ * @phan-param array<int,array{class:string}> $definition
*/
protected function registerFiltersFromDefinitions( array $definition ) {
$autoFillPriority = -1;
// Gotta override this since it's abstract
function formatResult( $skin, $result ) {
+ return false;
}
/**
* the HTMLForm
* @param WebRequest|null $request Optionally try and get data from a request too
* @return array [ User|string|null, Block::TYPE_ constant|null ]
+ * @phan-return array{0:User|string|null,1:int|null}
*/
public static function getTargetAndType( $par, WebRequest $request = null ) {
$i = 0;
* Show a form for filtering namespace and username
*
* @param string|null $par
- * @return string
*/
public function execute( $par ) {
$this->setHeaders();
* Form to ask for target user name.
*
* @param string $name User name submitted.
- * @return string Form asking for user name.
*/
protected function userForm( $name ) {
$htmlForm = HTMLForm::factory( 'ooui', [
*
* @param string $title Value for context title field
* @param string $input Value for input textbox
- * @return string
*/
private function makeForm( $title, $input ) {
$fields = [
->setMethod( 'get' )
->prepareForm()
->displayForm( false );
+ return '';
}
protected function getSuggestionsForTypes() {
protected function preText() {
$this->getOutput()->addModules( 'mediawiki.special.pageLanguage' );
+ return parent::preText();
}
protected function getFormFields() {
*
* @param Revision $previousRev
* @param Revision $currentRev
- * @return string HTML
*/
function showDiff( $previousRev, $currentRev ) {
$diffContext = clone $this->getContext();
*/
public function show() {
$this->addUploadJS();
- parent::show();
+ return parent::show();
}
/**
. htmlspecialchars( $time )
. "</i><br />\n"
);
+ return '';
}
}
* @param Title $title Title to check
* @param bool $fromReplica Whether to check the replica DB instead of the master
* @return bool
+ * @throws MWException
+ *
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..)
+ *
*/
public function isBlockedFrom( $title, $fromReplica = false ) {
- $blocked = $this->isHidden();
-
- if ( !$blocked ) {
- $block = $this->getBlock( $fromReplica );
- if ( $block ) {
- // Special handling for a user's own talk page. The block is not aware
- // of the user, so this must be done here.
- if ( $title->equals( $this->getTalkPage() ) ) {
- $blocked = $block->appliesToUsertalk( $title );
- } else {
- $blocked = $block->appliesToTitle( $title );
- }
- }
- }
-
- // only for the purpose of the hook. We really don't need this here.
- $allowUsertalk = $this->mAllowUsertalk;
-
- Hooks::run( 'UserIsBlockedFrom', [ $this, $title, &$blocked, &$allowUsertalk ] );
-
- return $blocked;
+ return MediaWikiServices::getInstance()->getPermissionManager()
+ ->isBlockedFrom( $this, $title, $fromReplica );
}
/**
// XXX it's not clear whether central ID providers are supposed to obey this
return $this->getName() === $user->getName();
}
+
+ /**
+ * Checks if usertalk is allowed
+ *
+ * @return bool
+ */
+ public function isAllowUsertalk() {
+ return $this->mAllowUsertalk;
+ }
+
}
$this->reader = $reader;
$this->writer = $writer;
$this->generator = $generator;
- $this->output = function () {
+ $this->output = function ( $text ) {
}; // nop
}
public $mVariantFallbacks;
public $mVariantNames;
public $mTablesLoaded = false;
+
+ /**
+ * @var ReplacementArray[]
+ * @phan-var array<string,ReplacementArray>
+ */
public $mTables;
+
// 'bidirectional' 'unidirectional' 'disable' for each variant
public $mManualLevel;
"version-entrypoints-header-url": "ইউআৰএল",
"version-libraries-version": "সংস্কৰণ",
"redirect": "ফাইল, সদস্য, পৃষ্ঠা বা সংশোধন বা লগ আই ডি-ৰে পুনঃনিৰ্দেশ",
- "redirect-summary": "এই বিশেষ পৃষ্ঠাটোৱে আপোনাক অন্য এটা ফাইললৈ (ফাইলৰ নাম), এটা পৃষ্ঠালৈ (সংশোধন আই ডি বা পৃষ্ঠা আই ডি), অথবা অন্য সদস্যৰ পৃষ্ঠালৈ (সদস্যৰ সাংখ্যিক আই ডি) পুনঃনির্দেশিত কৰিছে।\nব্যৱহাৰ: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], বা [[{{#Special:Redirect}}/user/101]]।",
+ "redirect-summary": "এই বিশেষ পৃষ্ঠাটোৱে আপোনাক অন্য এটা ফাইললৈ (ফাইলৰ নাম), এটা পৃষ্ঠালৈ (সংশোধন আই ডি বা পৃষ্ঠা আই ডি), অথবা অন্য সদস্যৰ পৃষ্ঠালৈ (সদস্যৰ সাংখ্যিক আই ডি), এটা সূচী ভুক্তিলৈ (প্ৰদত্ত সূচী ভুক্তি) পুনঃনির্দেশিত কৰিছে।\nব্যৱহাৰ: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] বা [[{{#Special:Redirect}}/logid/186]]।",
"redirect-submit": "যাওক",
"redirect-lookup": "চাওক:",
"redirect-value": "মূল্য:",
"tog-norollbackdiff": "Geri qaytardıqdan sonra dəyişikliklər arasındakı fərqi göstərmə",
"tog-useeditwarning": "Qeyd edilməmiş dəyişikliyə sahib bir dəyişiklik səhifəsindən çıxarkən məni xəbərdar et",
"tog-prefershttps": "Daxil olarkən hər zaman mühafizə edilən bağlantıdan istifadə et.",
+ "tog-showrollbackconfirmation": "Bir rollback linkinə kliklədiyinizdə təsdiq sorğusunu göstərin",
"underline-always": "Həmişə",
"underline-never": "Heç vaxt",
"underline-default": "Susmaya görə brouzer",
"createacct-reason-help": "Hesab yaratma qeydlərində göstərilən mesaj",
"createacct-submit": "İstifadəçi hesabı yarat",
"createacct-another-submit": "İstifadəçi hesabı yarat",
+ "createacct-continue-submit": "Hesab yaratmağı davam etdirin",
+ "createacct-another-continue-submit": "Hesab yaratmağı davam etdirin",
"createacct-benefit-heading": "{{SITENAME}} sizin kimi insanlar tərəfindən yaradılır.",
"createacct-benefit-body1": "{{PLURAL:$1|redaktə}}",
"createacct-benefit-body2": "{{PLURAL:$1|səhifə|səhifə}}",
"createacct-benefit-body3": "yeni {{PLURAL:$1|redaktor}}",
"badretype": "Daxil etdiyiniz parol uyğun gəlmir.",
+ "usernameinprogress": "Bu istifadəçi adı üçün bir hesab yaratma artıq başlamışdır.\nZəhmət olmasa, gözləyin.",
"userexists": "Daxil edilmiş ad artıq istifadədədir.\nLütfən başqa ad seçin.",
+ "createacct-normalization": "Texniki məhdudiyyətlər səbəbiylə istifadəçi adınız \"$2\" olaraq düzəldiləcək.",
"loginerror": "Daxil olma xətası",
"createacct-error": "Hesab yaratma xətası",
"createaccounterror": "Bu istifadəçi adını yaratmaq mümkün olmadı: $1",
"nocookieslogin": "{{SITENAME}} istifadəçilərin daxil ola bilməsi üçün \"cookie\"lərdən istifadə edir. Siz \"cookie\"lərin qəbuluna qadağa qoymusunuz. Lütfən, onların qəbuluna icazə verin və bir daha daxil olmağa cəhd edin.",
"nocookiesfornew": "İstifadəçinin akkauntu yaradılmayıb, ona görə də biz onun mənbəsini təsdiqləyə bilmədik.\nKukların qoşulmasına əmin olduqdan sonra səhifəni yeniləyib bir daha sınayın.",
"nocookiesforlogin": "{{int:nocookieslogin}}",
+ "createacct-loginerror": "Hesab müvəffəqiyyətlə yaradılıb, lakin avtomatik olaraq daxil olma mümkün olmadə. Zəhmət olmasa, [[Special:UserLogin|daxil olma]] səhifəsinə keçin.",
"noname": "Siz mövcud olan istifadəçi adı daxil etməmisiniz.",
"loginsuccesstitle": "Daxil oldunuz",
"loginsuccess": "'''\"$1\" adı ilə sistemə daxil oldunuz.'''",
"nosuchusershort": "\"$1\" adlı istifadəçi mövcud deyil. Yazdığınızı yoxlayın.",
"nouserspecified": "İstifadəçi adı daxil etməlisiniz.",
"login-userblocked": "Bu istifadəçi bloklanıb. Sistemə giriş üçün icazə verilmir.",
- "wrongpassword": "Səhv parol. Təkrar yazın.",
+ "wrongpassword": "Yanlış istifadəçi adı və ya parol.\nZəhmət olmasa bir daha cəhd edin.",
"wrongpasswordempty": "Parol boş. Təkrar yazın.",
"passwordtooshort": "Parolda ən azı {{PLURAL:$1|1 hərf yaxud simvol|$1 hərf yaxud simvol}} olmalıdır.",
+ "passwordtoolong": "Parolda ən azı {{PLURAL:$1|1 hərf yaxud simvol|$1 hərf yaxud simvol}} olmalıdır.",
+ "passwordtoopopular": "Çox istifadə edilən parollar seçilə bilməz. Xahiş edirik təxmin edilməsi daha çətin olan bir parol seçin.",
+ "passwordinlargeblacklist": "Daxil edilən parol çox istifadə edilən parolların siyahısında mövcuddur. Xahiş edirik daha bənzərsiz parol seçin.",
"password-name-match": "Parol adınızdan fərqli olmalıdır.",
"password-login-forbidden": "Bu istifadəçi adından və paroldan istifadə qadağan olunub.",
"mailmypassword": "E-mail ilə yeni parol göndər",
"passwordremindertitle": "{{SITENAME}} parol xatırladıcı",
- "passwordremindertext": "Kimsə (ehtimal ki, siz özünüz, $1 IP ünvanından) {{SITENAME}} ($4) layihəsi \nüçün yeni bir parol göndərilməsini istəyib. \"$2\" adlı istifadəçi üçün müvəqqəti \nolaraq \"$3\" parolu yaradılıb. Əgər bu sizin istəyiniz əsasında olubsa, \nhesabınıza daxil olaraq yeni bir parol yaratmağınız vacibdir. Müvəqqəti parolunuz\n{{PLURAL:$5|1 gün|$5 gün}} ərzində qüvvədə olacaqdır.\n\nParol dəyişdirməni siz istəməmisinizsə və ya parolunuzu xatırladınızsa \nvə artıq parolunuzu dəyişdirmək istəmirsinizsə, bu mesaja əhəmiyyət vermədən \nəvvəlki parolunuzdan istifadə etməyə davam edə bilərsiniz.",
+ "passwordremindertext": "Kimsə ($1 IP ünvanından) {{SITENAME}} ($4) layihəsi \nüçün yeni bir parol göndərilməsini istəyib. \"$2\" adlı istifadəçi üçün müvəqqəti \nolaraq \"$3\" parolu yaradılıb. Əgər bu sizin istəyiniz əsasında olubsa, \nhesabınıza daxil olaraq yeni bir parol yaratmağınız vacibdir. Müvəqqəti parolunuz\n{{PLURAL:$5|1 gün|$5 gün}} ərzində qüvvədə olacaqdır.\n\nParol dəyişdirməni siz istəməmisinizsə və ya parolunuzu xatırladınızsa \nvə artıq parolunuzu dəyişdirmək istəmirsinizsə, bu mesaja əhəmiyyət vermədən \nəvvəlki parolunuzdan istifadə etməyə davam edə bilərsiniz.",
"noemail": "\"$1\" adlı istifadəçi e-poçt ünvanını qeyd etməmişdir.",
"noemailcreate": "Düzgün e-poçt ünvanı qeyd etməlisiniz",
"passwordsent": "Yeni parol \"$1\" üçün qeydiyyata alınan e-poçt ünvanına göndərilmişdir.\nXahiş edirik, e-məktubu aldıqdan sonra yenidən daxil olasınız.",
"createaccount-text": "Biriləri {{SITENAME}} saytında ($4) sizin e-poçt ünvanınızdan istifadə edərək, parolu \"$3\" olan, \"$2\" adlı bir hesab yaratdı.\n\nSayta daxil olmalı və parolunuzu dəyişdirməlisiniz.\n\nƏgər istifadəçi hesabını səhvən yaratmısınızsa, bu mesajı gözardı edə bilərsiniz.",
"login-throttled": "Sistemə daxil olmaq üçün həddən artıq cəhd etmisiniz.\nYeni cəhd etməzdən əvvəl $1 gözləyin.",
"login-abort-generic": "Giriş uğursuz alındı — ləğv olundu",
+ "login-migrated-generic": "Hesabınız köçürüldü və bu vikidə istifadəçi adınız artıq mövcud deyil.",
"loginlanguagelabel": "Dil: $1",
"suspicious-userlogout": "Sizin çıxış üçün cəhdiniz uğursuz alındı. Bu, brouzerin yaxud proksi-keşləmənin düzgün işləməməsindən qaynaqlanır.",
"createacct-another-realname-tip": "Gərçək adınız istəyə bağlıdır.\nƏgər gərçək adınızı göstərsəniz, çalışmalarınıza müraciət etmək üçün istifadə ediləcəkdir.",
"pt-login": "Daxil ol",
"pt-login-button": "Daxil ol",
+ "pt-login-continue-button": "Daxil olmağa davam edin",
"pt-createaccount": "Hesab yarat",
"pt-userlogout": "Çıxış",
"php-mail-error-unknown": "PHP-nin mail() funksiyasında naməlum xəta",
"retypenew": "Yeni parolu təkrar yazın:",
"resetpass_submit": "Parol yaradın və sistemə daxil olun",
"changepassword-success": "Parolunuz dəyişdirildi!",
+ "changepassword-throttled": "Sistemə daxil olmaq üçün həddən artıq cəhd etmisiniz.\nYeni cəhd etməzdən əvvəl $1 gözləyin.",
+ "botpasswords": "Bot parolları",
+ "botpasswords-summary": "<em>Bot parolları</em> hesabın əsas giriş etmə məlumatlarını istifadə etmədən API vasitəsilə bir istifadəçi hesabına giriş imkanı verir. Bir bot parol ilə daxil olduqda mövcud istifadəçi hüquqları məhdudlaşdırıla bilər.\n\nNiyə bunu edə biləcəyinizi bilmirsinizsə, bunu etməməlisiniz. Heç kim sizdən bunlardan birini yaratmağı və ona verməyinizi istəməyəcək.",
+ "botpasswords-disabled": "Bot parolları söndürüldü.",
+ "botpasswords-no-central-id": "Bot parollarını istifadə etmək üçün mərkəzləşdirilmiş bir hesaba daxil olmalısınız.",
+ "botpasswords-existing": "Mövcud bot parolları",
+ "botpasswords-createnew": "Yeni bot parolu yarat",
+ "botpasswords-editexisting": "Mövcud bot parolunu redaktə et",
+ "botpasswords-label-needsreset": "(parol sıfırlanmalıdır)",
"botpasswords-label-appid": "Bot adı:",
"botpasswords-label-create": "Yarat",
"botpasswords-label-update": "Yenilə",
"botpasswords-label-cancel": "Ləğv et",
"botpasswords-label-delete": "Sil",
+ "botpasswords-label-resetpassword": "Parolu sıfırla",
+ "botpasswords-label-grants": "Tətbiq edilən hüquqlar:",
+ "botpasswords-help-grants": "Hüquqlar, istifadəçi hesabınız tərəfindən artıq saxlanılan hüquqlara giriş imkanı verir. Burada bir hüquq verilərkən, istifadəçi hesabınızın başqa cür olmadığı heç bir haqqı təmin etmir. Daha ətraflı məlumat üçün [[Special:ListGrants| hüquqlar]] səhifəsinə baxın.",
+ "botpasswords-label-grants-column": "Hüquq verildi",
+ "botpasswords-bad-appid": "\"$1\" bot adı etibarlı deyil.",
+ "botpasswords-insert-failed": "Bot adı \"$1\" əlavə etməyib. Artıq əlavə edilsin?",
+ "botpasswords-update-failed": "\"$1\" bot adı yenilənmədi. Silinsin?",
+ "botpasswords-created-title": "Bot parolu yaradıldı",
+ "botpasswords-created-body": "\"$2\" adlı istifadəçinin \"$1\" bot adı üçün bot parolu yaradıldı.",
+ "botpasswords-updated-title": "Bot parolu yeniləndi",
+ "botpasswords-updated-body": "\"$2\" adlı istifadəçinin \"$1\" bot adı üçün bot parolu yeniləndi.",
+ "botpasswords-deleted-title": "Bot parolu silindi",
+ "botpasswords-deleted-body": "\"$2\" adlı istifadəçinin \"$1\" bot adı üçün bot parolu silindi.",
+ "botpasswords-newpassword": "<strong>$1</strong> ilə daxil olmaq üçün yeni parol: <strong>$2</strong>. <em> Gələcək arayış üçün qeyd edin. </em> <br> (Giriş adının nüfuzlu istifadəçi adı ilə eyni olmasını tələb edən köhnə botlara görə <strong>$3</strong> istifadəçi adını və <strong>$4</strong> parolunu da istifadə edə bilərsiniz)",
+ "botpasswords-no-provider": "BotPasswordsSessionProvider mövcud deyil.",
+ "botpasswords-restriction-failed": "Bot parol məhdudiyyətləri bu girişə mane olur.",
+ "botpasswords-invalid-name": "Göstərilən istifadəçi adı bot ayırıcısını (\"$1\") ehtiva etmir.",
+ "botpasswords-not-exist": "\"$1\" istifadəçisinin \"$2\" adlı bot parolu yoxdur.",
+ "botpasswords-needs-reset": "\"$1\" adlı istifadəçinin \"$2\" bot adı üçün bot parolu sıfırlanmalıdır.",
+ "botpasswords-locked": "Hesabınız kilidləndiyinə görə bot parolu ilə giriş edə bilməzsiniz.",
"resetpass_forbidden": "Parolu dəyişmək mümkün deyil",
+ "resetpass_forbidden-reason": "Parolu dəyişmək mümkün deyil: $1",
"resetpass-no-info": "Bu səhifəni birbaşa açmaq üçün sistemə daxil olmalısınız.",
"resetpass-submit-loggedin": "Parolu dəyiş",
"resetpass-submit-cancel": "Ləğv et",
"resetpass-wrong-oldpass": "Müvəqqəti və ya daimi parolda yanlışlıq var.\nOla bilər siz parolu müvəffəqiyyətlə dəyişmisiniz, yaxud yeni müvəqqəti parol üçün müraciət etmisiniz.",
+ "resetpass-recycled": "Şifrənizi, mövcud parolunuzdan başqa bir şeyə dəyişdirin.",
+ "resetpass-temp-emailed": "Siz müvəqqəti e-poçt kodu ilə daxil oldunuz. \nDaxil olmağı bitirmək üçün buraya yeni bir parol yazmalısınız:",
"resetpass-temp-password": "Müvəqqəti parol:",
"resetpass-abort-generic": "Parol dəyişikliyi bir genişlənmə tərəfindən ləğv edildi.",
+ "resetpass-expired": "Parolunuzun müddəti doldu. Giriş etmək üçün yeni bir parol təyin edin.",
+ "resetpass-expired-soft": "Parolunuzun müddəti başa çatdı və dəyişdirilməlidir. Xahiş edirik yeni bir parol seçin və ya daha sonra dəyişdirmək üçün \"{{int:authprovider-resetpass-skip-label}}\" düyməsini basın.",
+ "resetpass-validity": "Parolunuz etibarlı deyil: $1\n\nGiriş etmək üçün yeni bir parol təyin edin.",
+ "resetpass-validity-soft": "Parolunuz etibarlı deyil: $1\n\nXahiş edirik yeni bir parol seçin və ya daha sonra dəyişdirmək üçün \"{{int: authprovider-resetpass-skip-label}}\" düyməsini basın.",
"passwordreset": "Parolu yenilə",
"passwordreset-text-one": "Parolunuzu sıfırlamaq üçün bu formanı doldurun.",
"passwordreset-text-many": "{{PLURAL:$1|Parolunuzu sıfırlamaq üçün sahələrdən birini doldurun.}}",
"passwordreset-email": "E-mail ünvanı:",
"passwordreset-emailtitle": "{{SITENAME}} hesabın yaradılması",
"passwordreset-emailtext-ip": "Kimsə, (ehtimal ki siz özünüz, $1 IP adresindən) {{SITENAME}} ($4) layihəsindəki hesabınızın \nparolunun yenilənməsini istəyib. Aşağıdakı istifadəçi {{PLURAL:$3|hesabı|hesabları}} bu e-poçt adresinə bağlıdır:\n\n$2\n\nBu müvəqqəti {{PLURAL:$3|parol|parollar}} {{PLURAL:$5|bir gün|$5 gün}} qüvvədə olacaqdır.\nSiz müvəqqəti parolla daxil olub yeni bir parol seçməlisiniz. Əgər parolun dəyişdirilməsini siz istəməmisinizsə və ya parolunuzu xatırladınızsa və artıq onu dəyişmək istəmirsinizsə, bu məktuba əhəmiyyət verməyərək köhnə parolunuzu istifadə etməyə davam edə bilərsiniz.",
+ "passwordreset-emailtext-user": "Kimsə, ($1 IP adresindən) {{SITENAME}} ($4) layihəsindəki hesabınızın \nparolunun sıfırlanmasını istəyib. Aşağıdakı istifadəçi {{PLURAL:$3|hesabı|hesabları}} bu e-poçt adresinə bağlıdır:\n\n$2\n\nBu müvəqqəti {{PLURAL:$3|parol|parollar}} {{PLURAL:$5|bir gün|$5 gün}} qüvvədə olacaqdır.\nSiz müvəqqəti parolla daxil olub yeni bir parol seçməlisiniz. Əgər parolun dəyişdirilməsini siz istəməmisinizsə və ya parolunuzu xatırladınızsa və artıq onu dəyişmək istəmirsinizsə, bu məktuba əhəmiyyət verməyərək köhnə parolunuzu istifadə etməyə davam edə bilərsiniz.",
"passwordreset-emailelement": "İstifadəçi adı: \n$1\n\nMüvəqqəti parol: \n$2",
"passwordreset-emailsentemail": "Əgər bu imeyl sizin istifadəçi hesabınıza bağlıdırsa, o halda parol sıfırlama məktubu ora göndəriləcək.",
+ "passwordreset-emailsentusername": "Əgər bu e-poçt sizin istifadəçi hesabınıza bağlıdırsa, o halda parol sıfırlama məktubu ora göndəriləcək.",
+ "passwordreset-nocaller": "Çağırıcı təmin edilməlidir",
+ "passwordreset-nosuchcaller": "Çağırıcı yoxdur: $1",
+ "passwordreset-ignored": "Parolun sıfırlanması işlənməmişdir. Bəlkə heç bir provayder qurulmayıb?",
"passwordreset-invalidemail": "Səhv e-poçt",
+ "passwordreset-nodata": "Nə bir istifadəçi adı, nə də bir e-poçt ünvanı verilmədi.",
"changeemail": "E-məktub ünvanını dəyiş və ya sil",
+ "changeemail-header": "E-poçt ünvanınızı dəyişdirmək üçün bu formanı tamamlayın. Hesabınızdakı hər hansı bir e-poçt ünvanının birləşməsini aradan qaldırmaq istəyirsinizsə, formu təqdim edərkən yeni e-poçt ünvanını boş buraxın.",
+ "changeemail-no-info": "Bu səhifəni birbaşa açmaq üçün sistemə daxil olmalısınız.",
"changeemail-oldemail": "Hazırkı e-poçt ünvanı:",
"changeemail-newemail": "Yeni e-poçt ünvanı:",
+ "changeemail-newemail-help": "E-poçt ünvanınızı çıxarmaq istəyirsinizsə, bu sahə boş olmalıdır. Unudulan bir parolu sıfırlaya bilməyəcəksiniz və e-poçt ünvanı çıxarıldıqda bu vikidən e-poçt ala bilməyəcəksiniz.",
"changeemail-none": "(yoxdur)",
+ "changeemail-password": "Sizin {{SITENAME}} parolunuz:",
"changeemail-submit": "E-poçtu dəyiş",
+ "changeemail-throttled": "Sistemə daxil olmaq üçün həddən artıq cəhd etmisiniz.\nYeni cəhd etməzdən əvvəl $1 gözləyin.",
+ "changeemail-nochange": "Fərqli bir yeni e-poçt ünvanı daxil edin.",
+ "resettokens": "Jetonları sıfırla",
+ "resettokens-text": "Hesabınızla əlaqəli müəyyən şəxsi məlumatlara giriş imkanı verən jetonlar sıfırlana bilər.\n\nTəsadüfən kimsə ilə paylaşdığınız təqdirdə və ya hesabınız pozulduğu halda bunu etməlisiniz.",
+ "resettokens-no-tokens": "Sıfırlanacaq heç bir jeton yoxdur.",
+ "resettokens-tokens": "Jetonlar:",
+ "resettokens-token-label": "$1 (cari dəyər: $2)",
+ "resettokens-watchlist-token": "[[Special:Watchlist|İzləmə siyahınızdakı]] səhifələrdə dəyişikliklərin web feed-i (Atom / RSS) üçün jeton",
+ "resettokens-done": "Jetonlar sıfırlandı.",
+ "resettokens-resetbutton": "Seçilmiş jetonları sıfırla",
"bold_sample": "Qalın mətn",
"bold_tip": "Qalın mətn",
"italic_sample": "Kursiv mətn",
"publishchanges": "Dəyişiklikləri yayımla",
"savearticle-start": "Səhifəni dərc et...",
"savechanges-start": "Dəyişiklikləri yadda saxla...",
+ "publishpage-start": "Səhifəni yayımla...",
+ "publishchanges-start": "Dəyişiklikləri yayımla...",
"preview": "Sınaq görüntüsü",
"showpreview": "Sınaq göstərişi",
"showdiff": "Dəyişiklikləri göstər",
+ "blankarticle": "<strong>Xəbərdarlıq:</strong> Yaratdığınız səhifə boşdur. Əgər bir daha \"$1\" düyməsinə klikləsəniz, səhifə heç bir məzmun olmadan yaradılacaq.",
"anoneditwarning": "<strong>Diqqət:</strong> Siz sistemə daxil olmamısınız. Hər hansı dəyişiklik etsəniz, sizin IP-ünvanınız hamıya görünəcək. Əgər <strong>[$1 daxil olsanız]</strong> və ya <strong>[$2 hesab yaratsanız]</strong>, redaktələriniz sizin istifadəçi adınıza yazılacaq və digər üstünlüklər də qazanacaqsınız.",
"anonpreviewwarning": "Sistemə daxil olmamısınız. \"Səhifəni qeyd et\" düyməsini bassanız IP ünvanınız səhifənin tarixçəsində qeyd olunacaq.",
"missingsummary": "'''Xatırlatma.''' Siz dəyişikliklərin qısa şərhini verməmisiniz. \"Səhifəni qeyd et\" düyməsinə təkrar basandan sonra sizin dəyişiklikləriniz şərhsiz qeyd olunacaq.",
+ "selfredirect": "<strong>Xəbərdarlıq:</strong> Bu səhifəni özünüzə istiqamətləndirirsiniz.\nİstiqamətləndirmə üçün yanlış hədəf göstərə bilərsiniz və ya səhv səhifəni redaktə edə bilərsiniz.\nYenidən \"$1\" düyməsinə bassanız, yenidən istiqamətləndirmə yaradılacaq.",
"missingcommenttext": "Zəhmət olmasa, şərh yazın.",
+ "missingcommentheader": "<strong>Xatırlatma:</strong> Bu şərh üçün bir mövzu vermədiniz.\nYenidən \"$1\" düyməsinə basarsanız, düzəlişiniz heçnəsiz saxlanacaq.",
"summary-preview": "Dəyişikliyin izahının görünüşü:",
"subject-preview": "Sərlövhə belə olacaq:",
+ "previewerrortext": "Redaktələrinizin sınaq görüntüsü göstərilərkən bir səhv baş verdi.",
"blockedtitle": "İstifadəçi bloklanıb",
+ "blocked-email-user": "<strong>İstifadəçi adınız e-poçt göndərməkdən bloklanmışdır. Siz hələ də bu vikidə redaktə edə bilərsiniz.</strong> Tam blok detallarını [[Special:MyContributions|istifadəçi fəaliyyətləri]] səhifəsində görə bilərsiniz.\n\nBloklayan: $1\nVerilmiş səbəb:<em>$2</em>\n\n*Blokun başlaması: $8\n*Blokun bitməsi: $6\n*Blok məqsədi: $7\n*Blok ID:#$5",
+ "blockedtext-partial": "<strong>İstifadəçi adınız bu səhifəni redaktə etməkdən bloklanmışdır. Siz bu vikidə digər səhifələrdə redaktə edə bilərsiniz.</strong> Tam blok detallarını [[Special:MyContributions|istifadəçi fəaliyyətləri]] səhifəsində görə bilərsiniz.\n\nBloklayan: $1\nVerilmiş səbəb:<em>$2</em>\n\n*Blokun başlaması: $8\n*Blokun bitməsi: $6\n*Blok məqsədi: $7\n*Blok ID:#$5",
+ "blockedtext": "<strong>Sizin istifadəçi adınız və ya IP ünvanınız bloklanmışdır.</strong>\n\nBloklayan: $1\nSəbəb: <em>$2</em>\n\n*Blokun başlaması: $8\n*Blokun bitməsi: $6\n*Blok məqsədi:$7\n\nSiz $1 ilə və ya [[{{MediaWiki:Grouppage-sysop}}|digər idarəçilərlə]] bloku müzakirə edə bilərsiniz.\n[[Special:Preferences|Nizamlamalarda]] etibarlı bir e-poçt ünvanı göstərilmədikcə və onu istifadə etməmisinizsə, \"{{int: emailuser}}\" funksiyasından istifadə edə bilməzsiniz.\nSizin IP ünvanınız: $3\nBlok ID: #$5\nXahiş etdiyiniz hər hansı bir sorğuda yuxarıda göstərilən məlumatları daxil edin.",
+ "autoblockedtext": "Sizin IP ünvanınız avtomatik olaraq bloklanıb, çünki digər bloklanmış istifadəçi tərəfindən istifadə edilib.\n\nBloklayan: $1\nSəbəb: <em>$2</em>\n\n*Blokun başlaması: $8\n*Blokun bitməsi: $6\n*Blok məqsədi:$7\n\nSiz $1 ilə və ya [[{{MediaWiki:Grouppage-sysop}}|digər idarəçilərlə]] bloku müzakirə edə bilərsiniz.\n[[Special:Preferences|Nizamlamalarda]] etibarlı bir e-poçt ünvanı göstərilmədikcə və onu istifadə etməmisinizsə, \"{{int: emailuser}}\" funksiyasından istifadə edə bilməzsiniz.\nSizin IP ünvanınız: $3\nBlok ID: #$5\nXahiş etdiyiniz hər hansı bir sorğuda yuxarıda göstərilən məlumatları daxil edin.",
+ "systemblockedtext": "Sizin istifadəçi adınız və ya IP ünvanınız MediaWiki tərəfindən avtomatik olaraq bloklanıb.\n\nSəbəb: <em>$2</em>\n\n*Blokun başlaması: $8\n*Blokun bitməsi: $6\n*Blok məqsədi:$7\n\n\nSizin IP ünvanınız: $3\nXahiş etdiyiniz hər hansı bir sorğuda yuxarıda göstərilən məlumatları daxil edin.",
"blockednoreason": "səbəb göstərilməyib",
"whitelistedittext": "Dəyişiklik edə bilmək üçün $1.",
"confirmedittext": "Siz elektron ünvanınızı səhifədə dəyişiklik etməzdən əvvəl göstərməlisiniz.\nZəhmət olmasa elektron ünvanınızı [[Special:Preferences|istifadəçi nizamlaması]] səhifənizdə göstərib təsdiq ediniz.",
"accmailtext": "[[User talk:$1|$1]] üçün təsadüfi yolla yaradılmış parol $2 ünvanına göndərildi.\nHesabınıza daxil olduqdan sonra, parolunuzu ''[[Special:ChangePassword|parolu dəyiş]]'' səhifəsində dəyişdirə bilərsiniz.",
"newarticle": "(Yeni)",
"newarticletext": "Mövcud olmayan səhifəyə olan keçidi izlədiniz. Aşağıdakı sahəyə məzmununu yazaraq bu səhifəni '''siz''' yarada bilərsiniz. (əlavə məlumat üçün [$1 kömək səhifəsinə] baxın). Əgər bu səhifəyə səhvən gəlmisinizsə sadəcə olaraq brauzerin '''geri''' düyməsinə vurun.",
- "anontalkpagetext": "----\n<em>Bu səhifə qeydiyyatdan keçməmiş və ya daxil olmamış anonim istifadəçiyə aid müzakirə səhifəsidir.</em>\nOna görə bu istifadəçini rəqəmlərdən ibarət IP ünvanı ilə müəyyən etmək məcburiyyətindəyik.\nBelə IP-ünvan bir neçə fərd tərəfindən istifadədə ola bilər.\nƏgər siz anonim istifadəçisinizsə və bu mesajın sizə aid olmadığını düşünürsünüzsə, onda [[Special:CreateAccount|qeydiyyatdan keçin]] və ya [[Special:UserLogin|daxil olun]] ki, digər anonim istifadəçilərlə qarışıqlıq yaşamayasınız.",
+ "anontalkpagetext": "----\n<em>Bu səhifə qeydiyyatdan keçməmiş və ya daxil olmamış anonim istifadəçiyə aid müzakirə səhifəsidir.</em>\nOna görə bu istifadəçini rəqəmlərdən ibarət IP ünvanı ilə müəyyən etmək məcburiyyətindəyik.\nBelə IP ünvan bir neçə fərd tərəfindən istifadədə ola bilər.\nƏgər siz anonim istifadəçisinizsə və bu mesajın sizə aid olmadığını düşünürsünüzsə, onda [[Special:CreateAccount|qeydiyyatdan keçin]] və ya [[Special:UserLogin|daxil olun]] ki, digər anonim istifadəçilərlə qarışıqlıq yaşamayasınız.",
"noarticletext": "Hal-hazırda bu səhifə boşdur. Başqa səhifələrdə eyni adda səhifəni [[Special:Search/{{PAGENAME}}|axtara]], əlaqəli qeydlərə\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} baxa] və ya [{{fullurl:{{FULLPAGENAME}}|action=edit}} bu adda səhifəni yarada]</span> bilərsiniz.",
"noarticletext-nopermission": "Hal-hazırda bu səhifə boşdur. Başqa səhifələrdə eyni adlı səhifəni [[Special:Search/{{PAGENAME}}| axtara]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} əlaqəli qeydlərə baxa] və ya səhifəni [{{fullurl:{{FULLPAGENAME}}|action=edit}} redaktə edə bilərsiniz]</span>, lakin sizin bu məqaləni yaratmaq hüququnuz yoxdur.",
+ "missing-revision": "\"{{FULLPAGENAME}}\" adlı səhifənin #$1 versiyası mövcud deyil.\n\nBu adətən silinmiş bir səhifəyə köhnəlmiş keçid bağlantısı ilə bağlıdır. Detallar üçün [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} silmə qeydlərinə] baxın.",
"userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" istifadəçi adı qeydiyyata alınmayıb.\nƏgər siz bu səhifəni yaratmaq/redaktə etmək istəyirsinizsə, xahiş edirik bunu yoxlayın.",
"userpage-userdoesnotexist-view": "\"$1\" istifadəçi hesabı qeydiyyatda deyil",
"blocked-notice-logextract": "Bu istifadəçi hal-hazırda bloklanmışdır.\nBloklama qeydlərinin sonuncusu aşağıda göstərilmişdir:",
"alllogstext": "Сумесны паказ усіх журналаў падзеяў {{GRAMMAR:родны|{{SITENAME}}}}.\nВы можаце адфільтраваць вынікі па тыпе журналу, удзельніку (улічваецца рэгістар) ці старонцы (таксама ўлічваецца рэгістар).",
"logempty": "Падобных запісаў у журнале няма.",
"log-title-wildcard": "Шукаць назвы, якія пачынаюцца з гэтага тэксту",
- "showhideselectedlogentries": "Ð\9fаказаÑ\86Ñ\8c/Ñ\81Ñ\85аваÑ\86Ñ\8c вÑ\8bбÑ\80анÑ\8bÑ\8f запÑ\96Ñ\81Ñ\8b Ñ\9e журнале",
+ "showhideselectedlogentries": "Ð\97Ñ\8cмÑ\8fнÑ\96Ñ\86Ñ\8c баÑ\87наÑ\81Ñ\8cÑ\86Ñ\8c абÑ\80анÑ\8bÑ\85 запÑ\96Ñ\81аÑ\9e Ñ\83 журнале",
"log-edit-tags": "Рэдагаваць меткі да абраных запісаў у журнале падзеяў",
"checkbox-select": "Выбраць: $1",
"checkbox-all": "усе",
"histfirst": "সবচেয়ে পুরনো",
"histlast": "সবচেয়ে নতুন",
"historysize": "({{PLURAL:$1|১ বাইট|$1 বাইট}})",
- "historyempty": "(খালি)",
+ "historyempty": "খালি",
"history-feed-title": "সংশোধনের ইতিহাস",
"history-feed-description": "এই উইকিতে এই পাতার সংশোধনের ইতিহাস",
"history-feed-item-nocomment": "$2-এ $1",
"brokenredirectstext": "Tato přesměrování vedou na neexistující stránky:",
"brokenredirects-edit": "editovat",
"brokenredirects-delete": "smazat",
- "withoutinterwiki": "Stránky bez mezijazykových odkazů (interwiki)",
+ "withoutinterwiki": "Stránky bez mezijazykových odkazů",
"withoutinterwiki-summary": "Tyto stránky neobsahují žádný mezijazykový odkaz:",
"withoutinterwiki-legend": "Prefix",
"withoutinterwiki-submit": "Zobrazit",
"pageinfo-robot-index": "Erlaubt",
"pageinfo-robot-noindex": "Nicht erlaubt",
"pageinfo-watchers": "Anzahl der Beobachter dieser Seite",
- "pageinfo-visiting-watchers": "Anzahl der Beobachter dieser Seite, die die letzten Bearbeitungen besucht haben",
+ "pageinfo-visiting-watchers": "Anzahl der Beobachter dieser Seite, welche die letzten Bearbeitungen besucht haben",
"pageinfo-few-watchers": "Weniger als {{PLURAL:$1|ein|$1}} Beobachter",
"pageinfo-few-visiting-watchers": "Es könnte einen beobachtenden Benutzer geben oder nicht, der die letzten Bearbeitungen besucht hat",
"pageinfo-redirects-name": "Anzahl der Weiterleitungen zu dieser Seite",
"deleting-subpages-warning": "<strong>Oharra:</strong> Ezabatuko duzun orrialdeak [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|a subpage|$1 subpages|51=over 50 subpages}}]] dauka.",
"rollback": "Desegin aldaketak",
"rollback-confirmation-confirm": "Mesedez baieztatu:",
+ "rollback-confirmation-yes": "Desegin",
"rollback-confirmation-no": "Utzi",
"rollbacklink": "desegin",
"rollbacklinkcount": "desegin {{PLURAL:$1|edizio bat|$1 edizio}}",
"confirm-unwatch-top": "Orrialde hau zure jarraipen-zerrendatik kendu?",
"confirm-rollback-button": "Ados",
"confirm-rollback-top": "Orrialde honen edizioak leheneratu?",
+ "confirm-rollback-bottom": "Ekintza honek orrialde honetan hautatutako aldaketak zuzenean desegingo ditu.",
"confirm-mcrrestore-title": "Errebisio bat berritu",
"confirm-mcrundo-title": "Aldaketa bat desegin",
"mcrundofailed": "Desegiteak akatsa",
"exif-compression-6": "JPEG (vecchio)",
"exif-copyrighted-true": "Protetto da copyright",
"exif-copyrighted-false": "Status del copyright non impostato",
+ "exif-photometricinterpretation-0": "Bianco e nero (bianco è 0)",
"exif-photometricinterpretation-1": "Bianco e nero (nero è 0)",
+ "exif-photometricinterpretation-3": "Tavolozza",
+ "exif-photometricinterpretation-4": "Maschera di trasparenza",
+ "exif-photometricinterpretation-5": "Separato (probabilmente CMYK)",
+ "exif-photometricinterpretation-8": "CIE L*a*b*",
+ "exif-photometricinterpretation-9": "CIE L*a*b* (codifica ICC)",
+ "exif-photometricinterpretation-10": "CIE L*a*b* (codifica ITU)",
"exif-unknowndate": "Data sconosciuta",
"exif-orientation-1": "Normale",
"exif-orientation-2": "Capovolto orizzontalmente",
"exif-gpsspeed-n": "Јазли",
"exif-gpsdestdistance-k": "Километри",
"exif-gpsdestdistance-m": "Милји",
- "exif-gpsdestdistance-n": "Ð\9dаÑ\83Ñ\82иÑ\87ки милји",
+ "exif-gpsdestdistance-n": "Ð\9cоÑ\80Ñ\81ки милји",
"exif-gpsdop-excellent": "Одлична ($1)",
"exif-gpsdop-good": "Добра ($1)",
"exif-gpsdop-moderate": "Умерена ($1)",
"createacct-loginerror": "Tunnus luotiin onnistuneesti, mutta automaattista sisäänkirjautumista ei voitu tehdä. Siirry [[Special:UserLogin|manuaaliseen kirjautumiseen]].",
"noname": "Et ole määritellyt kelvollista käyttäjänimeä.",
"loginsuccesstitle": "Olet kirjautunut sisään",
- "loginsuccess": "'''Olet kirjautunut sivustolle {{SITENAME}} käyttäjänä $1.'''",
+ "loginsuccess": "<strong>Olet kirjautunut {{GRAMMAR:illative|{{SITENAME}}}} käyttäjänä \"$1\".</strong>",
"nosuchuser": "Käyttäjää ”$1” ei ole olemassa.\nNimet ovat kirjainkoosta riippuvaisia. \nTarkista, kirjoititko nimen oikein, tai [[Special:CreateAccount|luo uusi käyttäjätunnus]].",
"nosuchusershort": "Käyttäjää nimeltä ”$1” ei ole. Kirjoititko nimen oikein?",
"nouserspecified": "Käyttäjätunnusta ei ole määritelty.",
"upload-form-label-own-work": "Tämä on oma työni",
"upload-form-label-infoform-categories": "Luokat",
"upload-form-label-infoform-date": "Päivämäärä",
- "upload-form-label-own-work-message-generic-local": "Vakuutan, että tallennan tämän tiedoston noudattaen sivustolla {{SITENAME}} voimassa olevia käyttöehtoja sekä lisenssejä koskevia käytäntöjä.",
+ "upload-form-label-own-work-message-generic-local": "Vakuutan, että tallennan tämän tiedoston noudattaen {{GRAMMAR:inessive|{{SITENAME}}}} voimassa olevia käyttöehtoja sekä lisenssejä koskevia käytäntöjä.",
"upload-form-label-not-own-work-message-generic-local": "Jos et kykene tallentamaan tätä tiedostoa noudattaen niitä käytäntöjä, jotka ovat voimassa sivustolla {{SITENAME}}, sulje tämä dialogi ja kokeile jotain toista menetelmää.",
"upload-form-label-not-own-work-local-generic-local": "Voit myös kokeilla [[Special:Upload|yleistä tallentamista]].",
"upload-form-label-own-work-message-generic-foreign": "Ymmärrän, että olen tallentamassa tätä tiedostoa yhteiseen mediasäilöön. Vakuutan, että teen tämän noudattaen asiaankuuluvia käyttöehtoja ja lisenssejä koskevia käytäntöjä.",
"emailuser-title-target": "Lähetä sähköpostia tälle {{GENDER:$1|käyttäjälle}}",
"emailuser-title-notarget": "Lähetä sähköpostia käyttäjälle",
"emailpagetext": "Jos tämä {{GENDER:$1|käyttäjä}} on antanut asetuksissaan kelvollisen sähköpostiosoitteen, alla olevalla lomakkeella voit lähettää hänelle viestin. [[Special:Preferences|Omissa asetuksissasi]] annettu sähköpostiosoite näkyy sähköpostin lähettäjän osoitteena, jotta vastaanottaja voi suoraan vastata viestiin.",
- "defemailsubject": "Sähköpostia käyttäjältä $1 sivustolta {{SITENAME}}",
+ "defemailsubject": "Sähköpostia käyttäjältä $1 {{GRAMMAR:elative|{{SITENAME}}}}",
"usermaildisabled": "Käyttäjien sähköposti poistettu käytöstä",
"usermaildisabledtext": "Et voi lähettää sähköpostia muille käyttäjille tässä wikissä",
"noemailtitle": "Ei sähköpostiosoitetta",
"confirmemail_body_set": "Joku, todennäköisesti sinä, IP-osoitteesta $1 on vaihtanut {{GRAMMAR:inessive|{{SITENAME}}}} tunnuksen $2 sähköpostiosoitteeksi tämän osoitteen.\n\nVarmenna, että tämä tunnus kuuluu sinulle ja aktivoi sähköpostitoiminnot uudelleen avaamalla seuraava linkki selaimellasi:\n\n$3\n\nJos tunnus ei kuulu sinulle, peruuta sähköpostiosoitteen varmennus avaamalla seuraava linkki:\n\n$5\n\nVarmennuskoodi vanhenee $4.",
"confirmemail_invalidated": "Sähköpostiosoitteen varmennus peruutettiin",
"invalidateemail": "Sähköpostiosoitteen varmennuksen peruuttaminen",
- "notificationemail_subject_changed": "Sivuston {{SITENAME}} rekisteröity sähköpostiosoite on vaihdettu",
- "notificationemail_subject_removed": "Sivuston {{SITENAME}} rekisteröity sähköpostiosoite on poistettu",
+ "notificationemail_subject_changed": "{{GRAMMAR:genitive|{{SITENAME}}}} rekisteröity sähköpostiosoite on vaihdettu",
+ "notificationemail_subject_removed": "{{GRAMMAR:genitive|{{SITENAME}}}} rekisteröity sähköpostiosoite on poistettu",
"notificationemail_body_changed": "Joku, todennäköisesti sinä, IP-osoitteesta $1 on vaihtanut tunnuksen ”$2” sähköpostiosoitteeksi ”$3” sivustolla {{SITENAME}}.\n\nJos se et ollut sinä, ota yhteyttä sivuston ylläpitäjään välittömästi.",
"notificationemail_body_removed": "Joku, todennäköisesti sinä, IP-osoitteesta $1 on poistanut tunnuksen ”$2” sähköpostiosoitteen sivustolla {{SITENAME}}.\n\nJos se et ollut sinä, ota yhteyttä sivuston ylläpitäjään välittömästi.",
"scarytranscludedisabled": "[Wikienvälinen sisällytys ei ole käytössä]",
"tog-watchdefault": "Sides dy't jo feroare hawwe folgje",
"tog-watchmoves": "Siden dy't jo werneamd hawwe folgje",
"tog-watchdeletion": "Siden dy't jo wiske hawwe folgje",
+ "tog-watchrollback": "Siden dêr't ik wizigings weromdraaid haw oan myn folchlist taheakje",
"tog-minordefault": "Markearje alle feroarings standert as fan lytse betsjutting",
"tog-previewontop": "By it neisjen, bewurkingsfjild ûnderoan sette",
"tog-previewonfirst": "Lit foarbyld sjen by earste wiziging",
"tog-diffonly": "Side-ynhâld dy't feroare wurdt net sjen litte",
"tog-showhiddencats": "Ferburgen kategoryen werjaan",
"tog-norollbackdiff": "Gjin ferskillen sjen litte nei it útfieren fan weromdraaien",
+ "tog-showrollbackconfirmation": "Befêstigingsdialooch sjen litte by it klikken op 'weromdraaie'",
"underline-always": "Altyd",
"underline-never": "Nea",
"underline-default": "Webblêder-standert",
"accmailtitle": "Wachtwurd ferstjoerd.",
"accmailtext": "Samar in wachtwurd foar [[User talk:$1|$1]] is ferstjoerd nei $2. It kin wizige wurde op 'e side \n<em>[[Special:ChangePassword|Wachtwurd feroarje]]</em> nei oanmelden.",
"newarticle": "(Nij)",
- "newarticletext": "Jo hawwe in keppeling folge nei in side dêr't noch gjin tekst op stiet.\nOm sels tekst te meistjsen kinne jo dy gewoan yntype in dit bewurkingsfjild\n([$1 Mear ynformaasje oer bewurkjen].)\nOars kinne jo tebek mei de tebek-knop fan jo blêder.",
+ "newarticletext": "Jo hawwe in keppeling folge nei in side dêr't noch gjin tekst op stiet.\nBegjin mei skriuwen yn it fjild hjirûnder om de side oan te meitsjen (sjoch de [$1 helpside] foar mear ynformaasje).\nKlik op de <strong>tebek</strong>-knop fan jo webblêder at jo hjir by ûngelok telâne kommen binne.",
"anontalkpagetext": "----\n<em>Dit is de oerlisside fan in ûnbekende meidogger; in meidogger dy't him/har net oanmeld hat.</em>\nOm't der gjin namme bekend is, wurdt it ynternet-adres brûkt om oan te jaan om wa't it giet.\nMar faak is it sa dat sa'n adres net altyd troch deselde persoan brûkt wurdt.\nAs jo it idee hawwe dat jo as ûnbekende meidogger opmerkings foar in oar krije, dan kinne jo jo [[Special:CreateAccount|registrearje]], of jo [[Special:UserLogin|oanmelde]]. Fan in oanmelde meidogger is it ynternet-adres net sichtber, en as oanmelde meidogger krije jo allinnich opmerkings dy't foar josels bedoeld binne.",
"noarticletext": "Der stiet noch gjin tekst op dizze side.\nJo kinne [[Special:Search/{{PAGENAME}}|nei dizze sidenamme sykje]] yn oare siden,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de besibbe lochs trochsykje]\nof [{{fullurl:{{FULLPAGENAME}}|action=edit}} dizze side oanmeitsje]</span>.",
"userpage-userdoesnotexist": "Jo bewurkje in meidoggerside fan in meidogger dy't net bestiet (meidogger \"<nowiki>$1</nowiki>\").\nKontrolearje oft jo dizze side wol oanmeitsje/bewurkje wolle.",
"right-siteadmin": "De database blokkearje en wer frij jaan",
"right-override-export-depth": "Alle siden oant en mei in keppelingsdjipte fan fiif fuortskriuwe",
"grant-group-email": "E-mail stjoere",
+ "grant-rollback": "Wizigings oan siden weromdraaie",
"newuserlogpage": "Ynskriuwingsloch",
"newuserlogpagetext": "Dit is in loch fan meidoggers dy't de lêste tiid ynskreaun binne.",
"rightslog": "Rjochtenloch",
"rcfilters-legend-heading": "<strong>List fan ôfkoartings:</strong>",
"rcnotefrom": "Dit binne de feroarings sûnt <b>$2</b> (maksimaal <b>$1</b>).",
"rclistfrom": "Jou nije feroarings, begjinnende mei $3 $2",
- "rcshowhideminor": "lytse feroarings $1",
+ "rcshowhideminor": "Lytse feroarings $1",
"rcshowhideminor-show": "werjaan",
"rcshowhideminor-hide": "ferbergje",
- "rcshowhidebots": "bots $1",
+ "rcshowhidebots": "Bots $1",
"rcshowhidebots-show": "werjaan",
"rcshowhidebots-hide": "ferbergje",
"rcshowhideliu": "Registrearre meidoggers $1",
"rcshowhideliu-show": "werjaan",
"rcshowhideliu-hide": "ferbergje",
- "rcshowhideanons": "$1 anonimen",
+ "rcshowhideanons": "Anonime meidoggers $1",
"rcshowhideanons-show": "werjaan",
"rcshowhideanons-hide": "ferbergje",
"rcshowhidepatr": "kontrolearre bewurkings $1",
"rcshowhidepatr-show": "werjaan",
"rcshowhidepatr-hide": "ferbergje",
- "rcshowhidemine": "$1 eigen bewurkings",
+ "rcshowhidemine": "Myn bewurkings $1",
"rcshowhidemine-show": "werjaan",
"rcshowhidemine-hide": "ferbergje",
"rclinks": "Jou $1 nije feroarings yn de lêste $2 dagen",
"deletereasonotherlist": "Oare reden",
"deletereason-dropdown": "*Faak-brûkte redenen\n** Frege troch de skriuwer\n** Skeining fan auteursrjocht\n** Fandalisme",
"rollback": "Wizigings weromdraaie",
+ "rollback-confirmation-yes": "Weromdraaie",
"rollbacklink": "weromdraaie",
"rollbacklinkcount": "$1 {{PLURAL:$1|bewurking|bewurkings}} weromdraaie",
"rollbacklinkcount-morethan": "mear as $1 {{PLURAL:$1|bewurking|bewurkings}} weromdraaie",
"tooltip-ca-talk": "Oerlis oer de ynhâldlike side",
"tooltip-ca-edit": "Dizze side bewurkje",
"tooltip-ca-addsection": "In opmerking tafoegje oan de oerlis-side.",
- "tooltip-ca-viewsource": "Dizze side is befeilige, mar jo kinne de boarne wol besjen.",
+ "tooltip-ca-viewsource": "Dizze side is skoattele, mar jo kinne de boarne wol besjen.",
"tooltip-ca-history": "Eardere ferzjes fan dizze side",
"tooltip-ca-protect": "Dizze side befeiligje",
"tooltip-ca-delete": "Dizze side weidwaan",
"markedaspatrollederror-noautopatrol": "Jo meie jo eigen bewurkings net sels markearre.",
"previousdiff": "← Eardere ferskillen",
"nextdiff": "Neikommende ferskillen →",
- "imagemaxsize": "Behein ôfmjittings fan ôfbyld op beskriuwingsside ta:",
- "thumbsize": "Mjitte fan miniatueren:",
+ "imagemaxsize": "Beheining fan 'e ôfbyldingsgrutte op bestânsbeskriuwingssiden:",
+ "thumbsize": "Miniatuergrutte:",
"widthheight": "$1 × $2",
"widthheightpage": "$1 × $2, $3 {{PLURAL:$3|side|siden}}",
"file-info": "bestânsgrutte: $1, MIME-type: $2",
"noimages": "Neat te sjen.",
"ilsubmit": "Sykje",
"bydate": "datum",
+ "sp-newimages-showfrom": "Nije bestannen besjen fan $2, $1 ôf",
"video-dims": "$1, $2 × $3",
"seconds-abbrev": "$1 s",
"minutes-abbrev": "$1 min",
"confirm-watch-button": "OK",
"confirm-unwatch-button": "OK",
"confirm-unwatch-top": "Dizze side fan myn folchlist ôfhelje",
+ "confirm-rollback-bottom": "Dizze hanneling sil de oanjûne sidewizigings fuortdaliks weromdraaie.",
"semicolon-separator": "; ",
"comma-separator": ", ",
"colon-separator": ": ",
"tag-mw-blank": "Leech meitsjen",
"tag-mw-replace": "Ferfongen",
"tag-mw-rollback": "Weromdraaid",
+ "tag-mw-rollback-description": "Bewurkings mei de keppeling 'weromdraaie', dy't foargeande wizigings ûngedien makke hawwe",
"tag-mw-undo": "Ungedien meitsjen",
"tags-source-header": "Boarne",
"tags-active-header": "Aktyf?",
"edit-already-exists": "Հրարավոր չէ նոր էջ ստեղծել․ այն արդեն գոյություն ունի։",
"defaultmessagetext": "Լռելյան տեքստը",
"editwarning-warning": "Այս էջը լքելով դուք կարող եք կորցնել ձեր կատարած փոփոխությունները։\nԵթե դուք գրանցված եք համակարգում, կարող եք անջատել այս նախազգուշացումը ձեր նախընրությունների «{{int:prefs-editing}}» բաժնում։",
+ "slot-name-main": "Հիմնական",
"content-model-wikitext": "վիքիտեքստ",
"content-model-text": "պարզ տեքստ",
"content-model-javascript": "ՋավաՍկրիպտ",
"rcfilters-other-review-tools": "Վերանայման այլ գործիքներ",
"rcfilters-group-results-by-page": "Արդյունքները խմբավորել էջերով",
"rcfilters-activefilters": "Ակտիվ զտիչներ",
+ "rcfilters-activefilters-hide": "Թաքցնել",
+ "rcfilters-activefilters-show": "Ցուցադրել",
"rcfilters-advancedfilters": "Ընդլայնված ֆիլտրեր",
"rcfilters-limit-title": "Ցուցադրվող արդյունքներ",
"rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|փոփոխություն|փոփոխություններ}}, $2",
"upload-misc-error-text": "Տեղի ունեցավ անհայտ սխալ բեռնման ընթացքում։ Խնդրում ենք ստուգել URL-հասցեի ճշտությունն ու հասանելիությունը և փորձել կրկին։ Սխալի կրկնման դեպքում կապնվեք համակարգային ադմինիստրատորի հետ։",
"upload-dialog-title": "Բեռնել նիշք",
"upload-dialog-button-cancel": "Չեղարկել",
+ "upload-dialog-button-back": "Հետ",
"upload-dialog-button-done": "Արված է",
"upload-dialog-button-save": "Հիշել",
"upload-dialog-button-upload": "Բեռնել",
"license-nopreview": "(Նախադիտումը մատչելի չէ)",
"upload_source_url": " (գործուն, հանրամատչելի URL-հասցե)",
"upload_source_file": " (նիշք ձեր համակարգչի վրա)",
+ "listfiles-delete": "ջնջել",
"listfiles_search_for": "Որոնել պատկերի անվամբ.",
"imgfile": "նիշք",
"listfiles": "Նիշքերի ցանկ",
"protectedpages": "Պաշտպանված էջեր",
"protectedpages-noredirect": "Թաքցնել վերահղումները",
"protectedpagesempty": "Ներկայումս չկան պաշտպանված էջեր նշված պարամետրերով։",
+ "protectedpages-page": "Էջ",
+ "protectedpages-expiry": "Լրանում է",
"protectedpages-submit": "Ցույց տալ էջերը",
+ "protectedpages-unknown-timestamp": "Անհայտ",
"protectedtitles": "Պաշտպանված անվանումներ",
"protectedtitles-submit": "Ցույց տալ վերնագրերը",
"listusers": "Մասնակիցների ցանկ",
"notargettext": "Դուք չեք նշել նպատակային էջ կամ մասնակից այս ֆունկցիայի գործածման համար։",
"pager-newer-n": "{{PLURAL:$1|ավելի թարմ 1|ավելի թարմ $1}}",
"pager-older-n": "{{PLURAL:$1|ավելի հին 1|ավելի հին $1}}",
+ "apisandbox-reset": "Մաքրել",
+ "apisandbox-retry": "Կրկնել",
+ "apisandbox-examples": "Օրինակներ",
+ "apisandbox-add-multi": "Ավելացնել",
+ "apisandbox-results": "Արդյունք",
+ "apisandbox-continue": "Շարունակել",
+ "apisandbox-continue-clear": "Մաքրել",
"booksources": "Գրքային աղբյուրներ",
"booksources-search-legend": "Գրքի մասին տեղեկությունների որոնում",
"booksources-search": "Որոնել",
"alllogstext": "{{SITENAME}} կայքի տեղեկամատյանների համընդհանուր ցանկ։\nԴուք կարող եք սահմանափակել արդյունքները ըստ տեղեկամատյանի տեսակի, մասնակցի անունի կամ համապատասխան էջի։",
"logempty": "Տեղեկամատյանում չկան համընկնող տարրեր։",
"log-title-wildcard": "Որոնել այս տեքստով սկսվող անվանումներ",
+ "checkbox-all": "Բոլորը",
+ "checkbox-none": "Ոչ մեկը",
"allpages": "Բոլոր էջերը",
"nextpage": "Հաջորդ էջը ($1)",
"prevpage": "Նախորդ էջը ($1)",
"activeusers": "Ակտիվ մասնակիցների ցանկ",
"activeusers-noresult": "Այդպիսի մասնակիցներ չեն գտնվել։",
"activeusers-submit": "Ցույց տալ ակտիվ մասնակիցներին",
+ "listgrouprights-group": "Խումբ",
"listgrouprights-members": "(անդամների ցանկ)",
"listgrouprights-addgroup": "Ավելացնեել {{PLURAL:$2|խումբ|խմբեր}}՝ $1",
"mailnologin": "Ուղարկման հասցե չկա",
"delete-edit-reasonlist": "Խմբագրել ջնջման պատճառները",
"deleting-backlinks-warning": "'''Զգուշացում''', ձեր կողմից ջնջվող էջին հղվում են [[Special:WhatLinksHere/{{FULLPAGENAME}}|այլ հոդվածներ]]:",
"rollback": "Հետ գլորել խմբագրումները",
+ "rollback-confirmation-no": "Չեղարկել",
"rollbacklink": "հետ գլորել",
"rollbacklinkcount": "հետ գլորել $1 {{PLURAL:$1|խմբագրում}}",
"rollbacklinkcount-morethan": "հետ գլորել ավելի քան $1 {{PLURAL:$1|խմբագրում|խմբագրում}}",
"rollback-success": "Հետ են շրջվել $1 մասնակցի խմբագրումները. վերադարձվել է $2 մասնակցի վերջին տարբերակին։",
"sessionfailure-title": "Սեսիայի խափանում",
"sessionfailure": "Կարծես խնդիր է առաջացել կապված ձեր ընթացիկ աշխատանքային սեսիայի հետ.\nայս գործողությունը բեկանվել է սեսիայի հափշտակման կանխման նպատակով։\nԽնդրում ենք սեղմել «back» կոճակը և վերբեռնել այն էջը որտեղից եկել եք ու փորձել կրկին։",
+ "changecontentmodel-submit": "Փոխել",
"protectlogpage": "Պաշտպանման տեղեկամատյան",
"protectlogtext": "Ստորև բերված է պաշտպանված և պաշտպանումից հանված էջերի ցանկը։ Տես նաև [[Special:ProtectedPages|ներկայումս պաշտպանված էջերի ցանկը]]։",
"protectedarticle": "պաշտպանվեց «[[$1]]» էջը",
"createaccountblock": "մասնակցային հաշվի ստեղծումը արգելափակված է",
"emailblock": "էլ-փոստը արգելափակված",
"blocklist-nousertalk": "չի կարող խմբագրել իր քննարկման էջը",
+ "blocklist-editing-page": "էջեր",
+ "blocklist-editing-ns": "անվանատարածքներ",
"ipblocklist-empty": "Արգելափակումների ցանկը դատարկ է։",
"ipblocklist-no-results": "Նշված IP-հասցեն կամ մասնակցի անունը արգելափակված չէ։",
"blocklink": "արգելափակել",
"allmessages-filter-all": "Բոլորը",
"allmessages-language": "Լեզու",
"allmessages-filter-submit": "Անցնել",
+ "allmessages-filter-translate": "Թարգմանել",
"thumbnail-more": "Ընդարձակել",
"filemissing": "Նման նիշք չկա",
"thumbnail_error": "Պատկերիկի ստեղծման սխալ. $1",
"pageinfo-display-title": "Վերնագիր",
"pageinfo-default-sort": "Լռելյայն տեսակավորման բանալի",
"pageinfo-length": "Ծավալ (բայթերով)",
+ "pageinfo-namespace": "Անվանատարածք",
"pageinfo-article-id": "Էջի N",
"pageinfo-language": "Բովանդակության լեզու",
"pageinfo-language-change": "փոխել",
"pageinfo-content-model": "Էջի բովանդակության մոդելը",
+ "pageinfo-content-model-change": "փոխել",
"pageinfo-robot-policy": "Կարգավիճակը որոնողական համակարգերում",
"pageinfo-robot-index": "ինդեքսավորվող",
"pageinfo-robot-noindex": "ինդեքսավորվող չէ",
"confirm-watch-top": "Ավելացնե՞լ ձեր հսկացանկին",
"confirm-unwatch-button": "Լավ",
"confirm-unwatch-top": "Հեռացնե՞լ Ձեր հսկացանկից։",
+ "confirm-rollback-button": "Լավ",
"imgmultipageprev": "← նախորդ էջ",
"imgmultipagenext": "հաջորդ էջ →",
"imgmultigo": "Անցնե՛լ",
"histfirst": "le plus ancian",
"histlast": "le plus nove",
"historysize": "({{PLURAL:$1|1 byte|$1 bytes}})",
- "historyempty": "(vacue)",
+ "historyempty": "vacue",
"history-feed-title": "Historia de versiones",
"history-feed-description": "Historia del versiones de iste pagina in le wiki",
"history-feed-item-nocomment": "$1 a $2",
"right-reupload-own": "Superscriber un file anteriormente incargate per uno mesme",
"right-reupload-shared": "Supplantar localmente le files del respositorio commun de media",
"right-upload_by_url": "Incargar un file ab un adresse URL",
- "right-purge": "Purgar le cache de un pagina in le sito sin confirmation",
+ "right-purge": "Purgar le cache de un pagina in le sito",
"right-autoconfirmed": "Non esser subjecte al limites de frequentia a base de adresse IP",
"right-bot": "Esser tractate como processo automatic",
"right-nominornewtalk": "Non reciper notification de nove messages quando se face modificationes minor in le pagina de discussion",
"mycontris": "Contributiones",
"anoncontribs": "Contributiones",
"contribsub2": "Pro {{GENDER:$3|$1}} ($2)",
+ "contributions-subtitle": "Pro {{GENDER:$3|$1}}",
"contributions-userdoesnotexist": "Le conto de usator \"$1\" non es registrate.",
"negative-namespace-not-supported": "Le spatios de nomines con valores negative non es supportate.",
"nocontribs": "Nulle modification correspondente a iste criterios ha essite trovate.",
"botpasswords-label-cancel": "Hætta við",
"botpasswords-label-delete": "Eyða",
"botpasswords-label-resetpassword": "Endurstilla lykilorðið",
+ "botpasswords-label-grants": "Tiltækar heimildir:",
"botpasswords-bad-appid": "Vélmennanafnið „$1“ er ógilt.",
"botpasswords-created-title": "Vélmennalykilorð var búið til",
"botpasswords-updated-title": "Vélmennalykilorð var uppfært",
"prefs-help-prefershttps": "Þessi stilling tekur gildi í næsta skiptið sem þú skráir þig inn.",
"prefswarning-warning": "Þú hefur gert breytingar á kjörstillingum þínum sem ekki er búið að vista.\nEf þú ferð af þessari síðu án þess að smella á \"$1\" verða kjörstillingar þínar ekki uppfærðar.",
"prefs-tabs-navigation-hint": "Ábending: Þú getur notað vinstri og hægri örvalyklana til að flakka á milli flipa í flipalistanum.",
- "userrights": "Notandaréttindi",
+ "userrights": "Réttindi notenda",
"userrights-lookup-user": "Velja notanda",
"userrights-user-editname": "Skráðu notandanafn:",
"editusergroup": "Hlaða inn notanda hópum",
"speciallogtitlelabel": "Beinist að (titill eða {{ns:user}}:notandanafn fyrir notanda):",
"log": "Aðgerðaskrár",
"logeventslist-submit": "Birta",
+ "logeventslist-patrol-log": "Yfirferðarskrá",
+ "logeventslist-tag-log": "Aðgerðaskrá yfir merki",
"all-logs-page": "Allar aðgerðir",
"alllogstext": "Safn allra aðgerðaskráa {{SITENAME}}.\nÞú getur takmarkað listann með því að velja tegund aðgerðaskráar, notandanafn, eða síðu.",
"logempty": "Engin slík aðgerð fannst.",
"listgrouprights-namespaceprotection-header": "Takmarkanir nafnrýmis",
"listgrouprights-namespaceprotection-namespace": "Nafnrými",
"listgrouprights-namespaceprotection-restrictedto": "Réttindi sem leyfa notanda að breyta",
+ "listgrants": "Veittar heimildir",
"listgrants-rights": "Réttindi",
"listgrants-grant-display": "$1 <code>($2)</code>",
"trackingcategories-name": "Heiti skilaboða",
"specialpages-group-highuse": "Mest notuðu síðurnar",
"specialpages-group-pages": "Listar yfir síður",
"specialpages-group-pagetools": "Síðuverkfæri",
- "specialpages-group-wiki": "Gögn og tól",
+ "specialpages-group-wiki": "Gögn og verkfæri",
"specialpages-group-redirects": "Tilvísaðar kerfissíður",
"specialpages-group-spam": "Amasendingasíur",
"specialpages-group-developer": "Forritaratól",
"histfirst": "ഏറ്റവും പഴയവ",
"histlast": "ഏറ്റവും പുതിയവ",
"historysize": "({{PLURAL:$1|1 ബൈറ്റ്|$1 ബൈറ്റുകൾ}})",
- "historyempty": "(ശൂന്യം)",
+ "historyempty": "ശൂന്യം",
"history-feed-title": "നാൾവഴി",
"history-feed-description": "വിക്കിയിൽ ഈ താളിന്റെ നാൾവഴി",
"history-feed-item-nocomment": "$2 സമയത്ത് $1",
"right-reupload-own": "സ്വയം അപ്ലോഡ് ചെയ്ത പ്രമാണങ്ങൾക്കു മുകളിലേയ്ക്ക് പ്രമാണങ്ങൾ അപ്ലോഡ് ചെയ്യുക",
"right-reupload-shared": "പങ്ക് വെയ്ക്കപ്പെട്ട മീഡിയ സംഭരണിയെ പ്രാദേശികമായി അതിലംഘിക്കുക",
"right-upload_by_url": "യു.ആർ.എല്ലിൽ നിന്നും പ്രമാണങ്ങൾ അപ്ലോഡ് ചെയ്യുക",
- "right-purge": "à´¸àµ\8dഥിരàµ\80à´\95à´°à´£à´\82 à´\92à´¨àµ\8dà´¨àµ\81à´\82 à´\87à´²àµ\8dലാതàµ\86 à´¸àµ\88à´±àµ\8dറിനàµ\8dà´±àµ\86 à´\95ാഷàµ\86 à´\92à´°àµ\81 താളിനായി പർà´\9càµ\8d à´\9aàµ\86à´¯àµ\8dà´¯àµ\81à´\95",
+ "right-purge": "സൈറ്റിന്റെ കാഷെ ഒരു താളിനായി പർജ് ചെയ്യുക",
"right-autoconfirmed": "ഐ.പി. അധിഷ്ഠിത പരിധികൾ ബാധകമല്ല",
"right-bot": "യാന്ത്രിക പ്രവൃത്തിയായി കണക്കാക്കപ്പെടുന്നു",
"right-nominornewtalk": "സംവാദം താളുകളിലെ ചെറുതിരുത്തലുകൾ പുതിയ സന്ദേശങ്ങളുണ്ടെന്ന അറിയിപ്പിനു കാരണമാകരുത്",
"histfirst": "oudste",
"histlast": "nieuwste",
"historysize": "({{PLURAL:$1|1 byte|$1 bytes}})",
- "historyempty": "(leeg)",
+ "historyempty": "leeg",
"history-feed-title": "Bewerkingsoverzicht",
"history-feed-description": "Bewerkingsoverzicht voor deze pagina op de wiki",
"history-feed-item-nocomment": "$1 op $3 om $4",
"previewerrortext": "Wystąpił błąd podczas próby podglądu Twoich zmian.",
"blockedtitle": "Użytkownik jest zablokowany",
"blocked-email-user": "<strong>Twoje konto zostało wyłączone z wysyłania wiadomości emaili innym użytkownikom. Wciąż możesz edytować inne strony na wiki.</strong> Więcej szczegółów na temat blokady znajdziesz na swojej [[Special:MyContributions|stroni wkładu]].\n\nBlokada została nałożona przez $1.\n\nPodany powód to: <em>$2</em>.\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zablokowany został: $7\n* Identyfikator blokady: #$5",
- "blockedtext-partial": "<strong>Twoje konto lub adres IP zostało wyłączone z dokonywania zmian na tej stronie. Wciąż możesz edytować inne strony na wiki.</strong> Więcej szczegółów na temat blokady znajdziesz na swojej [[Special:MyContributions|stroni wkładu]].\n\nBlokada została nałożona przez $1.\n\nPodany powód to: <em>$2</em>.\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zablokowany został: $7\n* Identyfikator blokady: #$5",
+ "blockedtext-partial": "<strong>Twoje konto lub adres IP zostało wyłączone z dokonywania zmian na tej stronie. Wciąż możesz edytować inne strony na wiki.</strong> Więcej szczegółów na temat blokady znajdziesz na swojej [[Special:MyContributions|stronie swojego wkładu]].\n\nBlokada została nałożona przez $1.\n\nPodany powód to: <em>$2</em>.\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zablokowany został: $7\n* Identyfikator blokady: #$5",
"blockedtext": "<strong>Twoje konto lub adres IP zostały zablokowane.</strong>\n\nBlokada została nałożona przez $1.\nPodany powód to: <em>$2</em>.\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zablokowany został: $7\n\nW celu wyjaśnienia przyczyny zablokowania możesz się skontaktować z $1 lub innym [[{{MediaWiki:Grouppage-sysop}}|administratorem]].\nNie możesz użyć funkcji „{{int:emailuser}}”, jeśli brak jest poprawnego adresu e‐mail w Twoich [[Special:Preferences|preferencjach]] lub jeśli taka możliwość została Ci zablokowana.\nTwój obecny adres IP to $3, a numer identyfikacyjny blokady to #$5.\nProsimy o podanie obu tych informacji przy wyjaśnianiu blokady.",
"autoblockedtext": "Ten adres IP został zablokowany automatycznie, gdyż korzysta z niego inny użytkownik, zablokowany przez administratora $1.\nPowód blokady:\n\n:<em>$2</em>\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zablokowany został: $7\n\nMożesz skontaktować się z $1 lub jednym z pozostałych [[{{MediaWiki:Grouppage-sysop}}|administratorów]] w celu uzyskania informacji o blokadzie.\n\nNie możesz użyć funkcji „{{int:emailuser}}”, jeśli brak jest poprawnego adresu e‐mail w Twoich [[Special:Preferences|preferencjach]] lub jeśli taka możliwość została Ci zablokowana.\n\nTwój obecny adres IP to $3, a numer identyfikacyjny blokady to #$5.\nProsimy o podanie obu tych numerów przy wyjaśnianiu blokady.",
"systemblockedtext": "Twoja nazwa użytkownika lub adres IP zostały automatycznie zablokowane przez MediaWiki.\nPodany powód to:\n\n:<em>$2</em>\n\n* Początek blokady: $8\n* Wygaśnięcie blokady: $6\n* Zamierzano zablokować: $7\n\nTwój obecny adres IP to $3.\nProsimy o dołączenie powyższych szczegółów w jakichkolwiek zadawanych pytaniach.",
"histfirst": "ᱢᱟᱨᱮᱱᱟᱜ",
"histlast": "ᱱᱟᱣᱟᱱᱟᱜ",
"historysize": "({{PLURAL:$1 1 ᱵᱟᱭᱤᱴ $1 ᱵᱟᱭᱤᱴᱥ}})",
- "historyempty": "(ᱠᱷᱟᱹᱞᱤ)",
+ "historyempty": "ᱠᱷᱟᱹᱞᱤ",
"history-feed-title": "ᱥᱩᱫᱷᱨᱟᱹᱣ ᱱᱟᱜᱟᱢ",
"history-feed-description": "ᱣᱤᱠᱤᱨᱮ ᱱᱤᱭᱟᱹ ᱥᱟᱦᱴᱟ ᱵᱚᱫᱚᱞ ᱨᱮᱱᱟᱜ ᱱᱟᱜᱟᱢ",
"history-feed-item-nocomment": "$2 ᱨᱮ $1",
"enhancedrc-history": "ᱱᱟᱜᱟᱢ",
"recentchanges": "ᱨᱚᱠᱟ ᱵᱚᱫᱚᱞᱠᱚ",
"recentchanges-legend": "ᱱᱟᱣᱟᱱᱟ ᱵᱚᱫᱚᱞ ᱛᱮᱭᱟᱜᱠᱚ",
- "recentchanges-summary": "á±±á±\9aá±£á±\9f á±¥á±\9fᱦᱴá±\9fᱨᱮ ᱩá±á± ᱤ ᱨᱮá±á±\9fá±\9c ᱡá±\9aá±\9bá±\9a á± á±·á±\9aá±± á±±á±\9fá±£á±\9f á±µá±\9aᱫá±\9aá±\9eá± á±\9a ᱯá±\9fᱸᱡá±\9fᱸá±á±¢á±®᱾",
+ "recentchanges-summary": "á±±á±\9aᱶá±\9f á±¥á±\9fᱦᱴá±\9fᱨᱮ ᱣᱤᱠᱤ ᱨᱮá±á±\9fá±\9c ᱡá±\9aá±\9bá±\9a á± á±·á±\9aá±± ᱨá±\9aá± á±\9f á±µá±\9aᱫá±\9aá±\9eá± á±\9a ᱯá±\9fᱸᱡá±\9fᱸá±á±¢á±® ᱾",
"recentchanges-noresult": "ᱮᱢᱞᱮᱱ ᱥᱚᱢᱚᱭ ᱵᱷᱤᱛᱤᱨ ᱨᱮ ᱵᱚᱫᱚᱞᱟᱜ ᱠᱚ ᱵᱟᱭ ᱢᱤᱫᱩᱜ ᱠᱟᱱᱟ ᱾",
- "recentchanges-feed-description": "ᱱᱚᱣᱟ feed ᱨᱮ ᱩᱭᱠᱤ ᱨᱮᱭᱟᱜ ᱡᱚᱛᱚ ᱠᱷᱚᱱ ᱱᱟᱣᱟ ᱵᱚᱫᱚᱞᱠᱚ ᱯᱟᱸᱡᱟᱸᱭᱢᱮ᱾",
+ "recentchanges-feed-description": "ᱱᱚᱣᱟ ᱯᱷᱤᱰ ᱨᱮ ᱣᱤᱠᱤ ᱨᱮᱭᱟᱜ ᱡᱚᱛᱚ ᱠᱷᱚᱱ ᱱᱟᱣᱟ ᱵᱚᱫᱚᱞᱠᱚ ᱯᱟᱸᱡᱟᱸᱭᱢᱮ᱾",
"recentchanges-label-newpage": "ᱱᱚᱣᱟ ᱥᱟᱯᱲᱟᱣ ᱢᱤᱫᱴᱮᱱ ᱱᱟᱣᱟ ᱥᱟᱦᱴᱟᱭ ᱛᱮᱭᱟᱨᱠᱮᱫᱟ",
"recentchanges-label-minor": "ᱱᱚᱣᱟ ᱫᱚ ᱦᱩᱰᱤᱧ ᱥᱟᱯᱲᱟᱣ ᱠᱟᱱᱟ",
"recentchanges-label-bot": "ᱱᱚᱣᱟ ᱥᱟᱯᱲᱟᱣ ᱫᱚ ᱵᱚᱴ ᱮ ᱠᱚᱨᱟᱣᱠᱟᱫᱟ",
"mergehistory-comment": "Spojeno [[:$1]] u [[:$2]]: $3",
"mergehistory-same-destination": "Izvorne i odredišne stranice ne mogu biti iste",
"mergehistory-reason": "Razlog:",
- "mergelog": "Registar spajanja",
+ "mergelog": "Evidencija spajanja",
"revertmerge": "Ukini spajanje",
"mergelogpagetext": "Ispod je spisak nedavnih spajanja historija stranica.",
"history-title": "Historija izmjena stranice \"$1\"",
"grant-viewdeleted": "Pregled obrisanih datoteka i stranica",
"grant-viewmywatchlist": "Pregled vaših praćenja",
"grant-viewrestrictedlogs": "Pregledanje ograničenih unosa u zapisniku",
- "newuserlogpage": "Registar novih korisnika",
+ "newuserlogpage": "Evidencija novih korisnika",
"newuserlogpagetext": "Ovo je evidencija registracije novih korisnika.",
"rightslog": "Evidencija korisničkih prava",
"rightslogtext": "Ovo je evidencija izmjene korisničkih prava.",
"upload-permitted": "{{PLURAL:$2|Podržana vrsta|Podržane vrste}} datoteka: $1.",
"upload-preferred": "{{PLURAL:$2|Preferirana vrsta|Preferirane vrste}} datoteka: $1.",
"upload-prohibited": "{{PLURAL:$2|Zabranjena vrsta|Zabranjene vrste}} datoteka: $1.",
- "uploadlogpage": "Registar postavljanja",
+ "uploadlogpage": "Evidencija postavljanja",
"uploadlogpagetext": "Ispod je popis najnovijih postavljanja datoteka.\nVidi [[Special:NewFiles|galeriju novih datoteka]] za slikovitiji pregled.",
"filename": "Ime fajla / Име датотеке",
"filedesc": "Sažetak - Опис",
"changecontentmodel-nodirectediting": "Model sadržaja $1 ne podržava izravno uređivanje",
"changecontentmodel-emptymodels-title": "Nema dostupnih modela sadržaja",
"changecontentmodel-emptymodels-text": "Model sadržaja stranice [[:$1]] se ne može pretvoriti ni u jedan drugi tip.",
- "log-name-contentmodel": "Zapisnik promjene modela sadržaja",
+ "log-name-contentmodel": "Evidencija promjene modela sadržaja",
"log-description-contentmodel": "Ova stranica navodi promjene u modelu sadržaja stranica, kao i stranice stvorene s modelom sadržaja koji se razlikuje od predodređenog.",
"logentry-contentmodel-new": "$1 {{GENDER:$2|napravio je|napravila je}} stranicu $3 s nestandardnim modelom sadržaja \"$5\"",
"logentry-contentmodel-change": "$1 {{GENDER:$2|promijenio|promijenila}} je model sadržaja stranice $3 iz \"$4\" u \"$5\"",
"mainpage": "پہلا پرت",
"mainpage-description": "پہلا پرت",
"policy-url": "Project:پالیسی",
- "portal": "بیٹھک",
+ "portal": "برادری دا پھاٹک",
"portal-url": "Project:دیوان عام",
"privacy": "پرائیویسی پالیسی",
"privacypage": "Project:پرائیویسی پالیسی",
"moredotdotdot": "ดูเพิ่ม...",
"morenotlisted": "รายการนี้อาจไม่สมบูรณ์",
"mypage": "หน้า",
- "mytalk": "à¸\9eูà¸\94à¸\84ุย",
- "anontalk": "à¸\9eูà¸\94à¸\84ุย",
+ "mytalk": "คุย",
+ "anontalk": "คุย",
"navigation": "การนำทาง",
"and": " และ",
"faq": "คำถามที่พบบ่อย",
"protect_change": "เปลี่ยน",
"unprotect": "เปลี่ยนการล็อก",
"newpage": "หน้าใหม่",
- "talkpagelinktext": "à¸\9eูà¸\94à¸\84ุย",
+ "talkpagelinktext": "คุย",
"specialpage": "หน้าพิเศษ",
"personaltools": "เครื่องมือส่วนตัว",
"talk": "อภิปราย",
"cannotchangeemail": "ไม่สามารถเปลี่ยนที่อยู่อีเมลบนวิกินี้",
"emaildisabled": "เว็บไซต์นี้ไม่สามารถส่งอีเมล",
"accountcreated": "สร้างบัญชีแล้ว",
- "accountcreatedtext": "สรà¹\89าà¸\87à¸\9aัà¸\8dà¸\8aีà¸\9cูà¹\89à¹\83à¸\8aà¹\89สำหรัà¸\9a [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|à¸\9eูà¸\94à¸\84ุย]]) à¹\81ลà¹\89ว",
+ "accountcreatedtext": "สร้างบัญชีผู้ใช้สำหรับ [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|คุย]]) แล้ว",
"createaccount-title": "การสร้างบัญชีสำหรับ {{SITENAME}}",
"createaccount-text": "มีบางคนสร้างบัญชีโดยใช้ที่อยู่อีเมลของคุณบน {{SITENAME}} ($4) โดยใช้ชื่อ \"$2\" และรหัสผ่าน \"$3\" \nคุณควรเข้าสู่ระบบและเปลี่ยนรหัสผ่านทันที\n\nคุณอาจเพิกเฉยข้อความนี้ หากการสร้างบัญชีนี้เป็นความผิดพลาด",
"login-throttled": "ที่ผ่านมาคุณพยายามล็อกอินมากครั้งเกินไป\nกรุณารอ $1 ก่อนลองอีกครั้ง",
"categoriesfrom": "แสดงหมวดหมู่เริ่มจาก:",
"deletedcontributions": "การเข้ามีส่วนร่วมของผู้ใช้ที่ถูกลบ",
"deletedcontributions-title": "การเข้ามีส่วนร่วมของผู้ใช้ที่ถูกลบ",
- "sp-deletedcontributions-contribs": "à¹\80รืà¹\88à¸à¸\87à¸\97ีà¹\88มีส่วนร่วม",
+ "sp-deletedcontributions-contribs": "à¸\81ารà¹\80à¸\82à¹\89ามีส่วนร่วม",
"linksearch": "ค้นหาลิงก์ภายนอก",
"linksearch-pat": "รูปแบบการค้นหา:",
"linksearch-ns": "เนมสเปซ:",
"tog-norollbackdiff": "進行反轉之後唔睇差異",
"tog-useeditwarning": "當我離開未保存好嘅修改嗰陣警告我",
"tog-prefershttps": "簽到後繼續用加密連線",
+ "tog-showrollbackconfirmation": "撳「反轉」掣嘅時候要撳確認",
"underline-always": "全部",
"underline-never": "永不",
"underline-default": "瀏覽器或瀏覽器膚色預設",
"grant-editprotected": "改保護咗嘅版",
"grant-highvolume": "大量編輯",
"grant-oversight": "收埋用戶同禁止顯示修訂",
+ "grant-rollback": "反轉一啲版面嘅修改",
"grant-sendemail": "寄電郵畀其他用戶",
"grant-uploadeditmovefile": "上載、𠖫同搬檔",
"grant-uploadfile": "上載新檔案",
"deleteprotected": "你唔可以刪呢版,因為佢畀人保護咗。",
"deleting-backlinks-warning": "<strong>警告:</strong>有[[Special:WhatLinksHere/{{FULLPAGENAME}}|其他版]]連過來或嵌咗你準備刪嘅呢版。",
"rollback": "反轉修改",
+ "rollback-confirmation-yes": "反轉",
"rollbacklink": "反轉",
"rollbacklinkcount": "反轉 $1 次修改",
"rollbacklinkcount-morethan": "反轉超過$1次嘅{{PLURAL:$1|edit|修改}}",
"confirm-unwatch-button": "好",
"confirm-unwatch-top": "喺你嘅監視清單度刪走呢一版?",
"confirm-rollback-button": "好",
+ "confirm-rollback-bottom": "呢個動作會立即反轉晒揀咗嘅修改。",
+ "confirm-mcrundo-title": "還原一個改動",
"comma-separator": "、",
"word-separator": "",
"parentheses": "($1)",
"tag-mw-blank": "清空",
"tag-mw-replace": "換咗",
"tag-mw-rollback": "反轉",
+ "tag-mw-rollback-description": "用「反轉」掣將之前修改打回頭嘅修改",
+ "tag-mw-undo": "還原",
+ "tag-mw-undo-description": "用「還原」掣還原之前修改嘅修改",
"tags-title": "標籤",
"tags-intro": "呢一版列示咗個軟件標示嘅編輯,同埋佢哋嘅解釋。",
"tags-tag": "標籤名",
"tags-hitcount": "$1次更改",
"tags-create-reason": "原因:",
"tags-create-submit": "開",
+ "tags-delete-explanation-warning": "呢個動作<strong>冇得返轉頭</strong>,係<strong>還原唔到</strong>嘅,就算資料庫管理員都還原唔到。唔該諗清楚你係咪想刪走呢個標籤。",
"tags-delete-reason": "原因:",
"tags-activate-title": "啟用標籤",
"tags-activate-reason": "原因:",
"histfirst": "最舊",
"histlast": "最新",
"historysize": "($1 位元組)",
- "historyempty": "(空)",
+ "historyempty": "空",
"history-feed-title": "修訂歷史",
"history-feed-description": "本 Wiki 上此頁面的修訂歷史",
"history-feed-item-nocomment": "$1 於 $2",
"print.css": "/* 此處的 CSS 會影響列印輸出 */",
"noscript.css": "/* 此 CSS 會影響沒有啟用 JavaScript 的使用者 */",
"group-autoconfirmed.css": "/* 此 CSS 會影響自動確認的使用者 */",
+ "group-user.css": "/* 置於此處的CSS只會影響已註冊使用者 */",
"group-bot.css": "/* 此 CSS 會影響機器人 */",
"group-sysop.css": "/* 這裡的 CSS 會影響管理員 */",
"group-bureaucrat.css": "/* 此 CSS 會影響行政員 */",
"common.json": "/* 在此的任一 JavaScript 會為全部使用者在所有頁面裡載入。 */",
"common.js": "/* 此 JavaScript 會用於使用者載入的每一個頁面。 */",
+ "group-autoconfirmed.js": "/* 這裡的任何JavaScript只會為自動確認的使用者載入 */",
+ "group-user.js": "/* 這裡的任何JavaScript只會為已註冊使用者載入 */",
+ "group-bot.js": "/* 這裡的任何JavaScript只會為機器人載入 */",
"group-sysop.js": "/* 這裡的 JavaScript 會影響管理員 */",
+ "group-bureaucrat.js": "/* 這裡的任何JavaScript只會為行政員載入 */",
"anonymous": "{{SITENAME}} 的匿名{{PLURAL:$1|使用者}}",
"siteuser": "{{SITENAME}}使用者 $1",
"anonuser": "{{SITENAME}} 匿名使用者 $1",
'Ancientpages' => [ 'Nejstarší_stránky', 'Staré_stránky', 'Stare_stranky' ],
'ApiHelp' => [ 'Nápověda_k_API' ],
'ApiSandbox' => [ 'API_pískoviště' ],
+ 'AutoblockList' => [ 'Seznam_automatických_blokování' ],
'Badtitle' => [ 'Neplatný_název' ],
'Blankpage' => [ 'Prázdná_stránka' ],
'Block' => [ 'Blokování', 'Blokovani', 'Blokovat_uživatele', 'Blokovat_IP', 'Blokovat_uzivatele' ],
'BrokenRedirects' => [ 'Přerušená_přesměrování', 'Prerusena_presmerovani' ],
'Categories' => [ 'Kategorie' ],
'ChangeContentModel' => [ 'Změnit_model_obsahu_stránky' ],
+ 'ChangeCredentials' => [ 'Změna_přihlašovacích_údajů' ],
'ChangeEmail' => [ 'Změna_emailu', 'Zmena_emailu' ],
'ChangePassword' => [ 'Změna_hesla', 'Zmena_hesla' ],
'ComparePages' => [ 'Porovnání_stránek', 'PorovnáníStránek', 'Porovnani_stranek', 'PorovnaniStranek' ],
'Mypage' => [ 'Moje_stránka', 'Moje_stranka', 'Má_stránka' ],
'Mytalk' => [ 'Moje_diskuse', 'Má_diskuse' ],
'Myuploads' => [ 'Moje_soubory', 'Mé_soubory' ],
- 'Newimages' => [ 'Nové_obrázky', 'Galerie_nových_obrázků', 'Nove_obrazky' ],
+ 'Newimages' => [ 'Nové_soubory', 'Nové_obrázky', 'Galerie_nových_obrázků', 'Nove_obrazky' ],
'Newpages' => [ 'Nové_stránky', 'Nove_stranky', 'Nejnovější_stránky', 'Nejnovejsi_stranky' ],
'PagesWithProp' => [ 'Stránky_s_vlastností', 'Stránky_s_vlastnostmi' ],
'PasswordReset' => [ 'Reset_hesla', 'Resetovat_heslo', 'Obnova_hesla', 'Obnovit_heslo' ],
'Recentchanges' => [ 'Poslední_změny', 'Posledni_zmeny' ],
'Recentchangeslinked' => [ 'Související_změny', 'Souvisejici_zmeny' ],
'Redirect' => [ 'Přesměrování', 'Přesměrovat' ],
+ 'RemoveCredentials' => [ 'Odstranění_přihlašovacích_údajů' ],
+ 'ResetTokens' => [ 'Reinicializace_klíčů' ],
'Revisiondelete' => [ 'Smazat_revizi' ],
'Search' => [ 'Hledání', 'Hledani' ],
'Shortpages' => [ 'Nejkratší_stránky', 'Nejkratsi_stranky' ],
'Listgrants' => [ 'ListGrants' ],
'Listredirects' => [ 'ListRedirects' ],
'ListDuplicatedFiles' => [ 'ListDuplicatedFiles', 'ListFileDuplicates' ],
- 'Listusers' => [ 'ListUsers', 'UserList' ],
+ 'Listusers' => [ 'ListUsers', 'UserList', 'Users' ],
'Lockdb' => [ 'LockDB' ],
'Log' => [ 'Log', 'Logs' ],
'Lonelypages' => [ 'LonelyPages', 'OrphanedPages' ],
}
$this->output( "Populating page_props.pp_sortkey complete.\n" );
+ return true;
}
protected function getUpdateKey() {
class PPFuzzUser extends User {
public $ppfz_test, $mDataLoaded;
- function load() {
+ function load( $flags = null ) {
if ( $this->mDataLoaded ) {
return;
}
function dispatch( /*...*/ ) {
$args = func_get_args();
$pipes = $this->replicaPipes;
- $numPipes = stream_select( $x = [], $pipes, $y = [], 3600 );
+ $x = [];
+ $y = [];
+ $numPipes = stream_select( $x, $pipes, $y, 3600 );
if ( !$numPipes ) {
$this->critical( "Error waiting to write to replica DBs. Aborting" );
exit( 1 );
// Clear memcached so old passwords are wiped out
foreach ( $updateUsers as $user ) {
- $user->clearSharedCache();
+ $user->clearSharedCache( 'refresh' );
}
} while ( $res->numRows() );
}
JobQueueGroup::singleton()->get( $type )->delete();
}
+ // T219673: close any connections from code that failed to call reuseConnection()
+ // or is still holding onto a DBConnRef instance (e.g. in a singleton).
+ MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->closeAll();
CloneDatabase::changePrefix( self::$oldTablePrefix );
self::$oldTablePrefix = false;
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Permissions;
+
+use Action;
+use Block;
+use MediaWikiLangTestCase;
+use RequestContext;
+use Title;
+use User;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
+use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Permissions\PermissionManager;
+
+/**
+ * @group Database
+ *
+ * @covers \MediaWiki\Permissions\PermissionManager
+ */
+class PermissionManagerTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var string
+ */
+ protected $userName, $altUserName;
+
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var User
+ */
+ protected $user, $anonUser, $userUser, $altUser;
+
+ /**
+ * @var PermissionManager
+ */
+ protected $permissionManager;
+
+ /** Constant for self::testIsBlockedFrom */
+ const USER_TALK_PAGE = '<user talk page>';
+
+ protected function setUp() {
+ parent::setUp();
+
+ $localZone = 'UTC';
+ $localOffset = date( 'Z' ) / 60;
+
+ $this->setMwGlobals( [
+ 'wgLocaltimezone' => $localZone,
+ 'wgLocalTZoffset' => $localOffset,
+ 'wgNamespaceProtection' => [
+ NS_MEDIAWIKI => 'editinterface',
+ ],
+ ] );
+ // Without this testUserBlock will use a non-English context on non-English MediaWiki
+ // installations (because of how Title::checkUserBlock is implemented) and fail.
+ RequestContext::resetMain();
+
+ $this->userName = 'Useruser';
+ $this->altUserName = 'Altuseruser';
+ date_default_timezone_set( $localZone );
+
+ $this->title = Title::makeTitle( NS_MAIN, "Main Page" );
+ if ( !isset( $this->userUser ) || !( $this->userUser instanceof User ) ) {
+ $this->userUser = User::newFromName( $this->userName );
+
+ if ( !$this->userUser->getId() ) {
+ $this->userUser = User::createNew( $this->userName, [
+ "email" => "test@example.com",
+ "real_name" => "Test User" ] );
+ $this->userUser->load();
+ }
+
+ $this->altUser = User::newFromName( $this->altUserName );
+ if ( !$this->altUser->getId() ) {
+ $this->altUser = User::createNew( $this->altUserName, [
+ "email" => "alttest@example.com",
+ "real_name" => "Test User Alt" ] );
+ $this->altUser->load();
+ }
+
+ $this->anonUser = User::newFromId( 0 );
+
+ $this->user = $this->userUser;
+ }
+
+ $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+ $this->overrideMwServices();
+ }
+
+ protected function setUserPerm( $perm ) {
+ // Setting member variables is evil!!!
+
+ if ( is_array( $perm ) ) {
+ $this->user->mRights = $perm;
+ } else {
+ $this->user->mRights = [ $perm ];
+ }
+ }
+
+ protected function setTitle( $ns, $title = "Main_Page" ) {
+ $this->title = Title::makeTitle( $ns, $title );
+ }
+
+ protected function setUser( $userName = null ) {
+ if ( $userName === 'anon' ) {
+ $this->user = $this->anonUser;
+ } elseif ( $userName === null || $userName === $this->userName ) {
+ $this->user = $this->userUser;
+ } else {
+ $this->user = $this->altUser;
+ }
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ *
+ * This test is failing per T201776.
+ *
+ * @group Broken
+ * @covers \MediaWiki\Permissions\PermissionManager::checkQuickPermissions
+ */
+ public function testQuickPermissions() {
+ $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
+ getFormattedNsText( NS_PROJECT );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [ [ "nocreatetext" ] ], $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [ [ 'nocreatetext' ] ], $res );
+
+ $this->setUser( $this->userName );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title );
+ $this->assertEquals( [ [ 'nocreate-loggedin' ] ], $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'cant-move-user-page' ], [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+ $this->setUser( $this->userName );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ], $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ], $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenologintext' ] ], $res );
+
+ $this->setUser( $this->userName );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions(
+ 'move',
+ [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ]
+ );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', [ [ 'movenotallowedfile' ] ] );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions(
+ 'move',
+ [ [ 'movenotallowedfile' ], [ 'movenotallowed' ] ],
+ [ [ 'movenotallowedfile' ], [ 'movenologintext' ] ]
+ );
+
+ if ( $this->isWikitextNS( NS_MAIN ) ) {
+ // NOTE: some content models don't allow moving
+ // @todo find a Wikitext namespace for testing
+
+ $this->setTitle( NS_MAIN );
+ $this->setUser( 'anon' );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', [] );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ],
+ [ [ 'movenologintext' ] ] );
+
+ $this->setUser( $this->userName );
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', [ [ 'movenotallowed' ] ] );
+
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', [] );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( 'move' );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setUserPerm( '' );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title );
+ $this->assertEquals( [ [ 'movenotallowed' ] ], $res );
+ }
+
+ $this->setTitle( NS_USER );
+ $this->setUser( $this->userName );
+ $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setUserPerm( "move" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title );
+ $this->assertEquals( [ [ 'cant-move-to-user-page' ] ], $res );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setTitle( NS_USER, "User/subpage" );
+ $this->setUserPerm( [ "move", "move-rootuserpages" ] );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setUserPerm( "move" );
+ $res = $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title );
+ $this->assertEquals( [], $res );
+
+ $this->setUser( 'anon' );
+ $check = [
+ 'edit' => [
+ [ [ 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ] ],
+ [ [ 'badaccess-group0' ] ],
+ [],
+ true
+ ],
+ 'protect' => [
+ [ [
+ 'badaccess-groups',
+ "[[$prefix:Administrators|Administrators]]", 1 ],
+ [ 'protect-cantedit'
+ ] ],
+ [ [ 'badaccess-group0' ], [ 'protect-cantedit' ] ],
+ [ [ 'protect-cantedit' ] ],
+ false
+ ],
+ '' => [ [], [], [], true ]
+ ];
+
+ foreach ( [ "edit", "protect", "" ] as $action ) {
+ $this->setUserPerm( null );
+ $this->assertEquals( $check[$action][0],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, true ) );
+ $this->assertEquals( $check[$action][0],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
+ $this->assertEquals( $check[$action][0],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
+
+ global $wgGroupPermissions;
+ $old = $wgGroupPermissions;
+ $wgGroupPermissions = [];
+
+ $this->assertEquals( $check[$action][1],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, true ) );
+ $this->assertEquals( $check[$action][1],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
+ $this->assertEquals( $check[$action][1],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
+ $wgGroupPermissions = $old;
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][2],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, true ) );
+ $this->assertEquals( $check[$action][2],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, 'full' ) );
+ $this->assertEquals( $check[$action][2],
+ $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][3],
+ $this->permissionManager->userCan( $action, $this->user, $this->title, true ) );
+ $this->assertEquals( $check[$action][3],
+ $this->permissionManager->userCan( $action, $this->user, $this->title,
+ PermissionManager::RIGOR_QUICK ) );
+ # count( User::getGroupsWithPermissions( $action ) ) < 1
+ }
+ }
+
+ protected function runGroupPermissions( $action, $result, $result2 = null ) {
+ global $wgGroupPermissions;
+
+ if ( $result2 === null ) {
+ $result2 = $result;
+ }
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title );
+ $this->assertEquals( $result, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = $this->permissionManager
+ ->getPermissionErrors( $action, $this->user, $this->title );
+ $this->assertEquals( $result2, $res );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions
+ */
+ public function testSpecialsAndNSPermissions() {
+ global $wgNamespaceProtection;
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_SPECIAL );
+
+ $this->assertEquals( [ [ 'badaccess-group0' ], [ 'ns-specialprotected' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( '' );
+ $this->assertEquals( [ [ 'badaccess-group0' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+ $wgNamespaceProtection[NS_USER] = [ 'bogus' ];
+
+ $this->setTitle( NS_USER );
+ $this->setUserPerm( '' );
+ $this->assertEquals( [ [ 'badaccess-group0' ],
+ [ 'namespaceprotected', 'User', 'bogus' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( [ [ 'protectedinterface', 'bogus' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+ $wgNamespaceProtection = null;
+
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+ $this->assertEquals( true,
+ $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+
+ $this->setUserPerm( '' );
+ $this->assertEquals( [ [ 'badaccess-group0' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testJsConfigEditPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->userName . '/test.js' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testJsonConfigEditPermissions() {
+ $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
+ getFormattedNsText( NS_PROJECT );
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->userName . '/test.json' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testCssConfigEditPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->userName . '/test.css' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testOtherJsConfigEditPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.js' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testOtherJsonConfigEditPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.json' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testOtherCssConfigEditPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.css' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+ [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testOtherNonConfigEditPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/tempo' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ] ],
+
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ /**
+ * @todo This should use data providers like the other methods here.
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
+ */
+ public function testPatrolActionConfigEditPermissions() {
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_USER, 'ToPatrolOrNotToPatrol' );
+ $this->runConfigEditPermissions(
+ [ [ 'badaccess-group0' ] ],
+
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-group0' ] ],
+ [ [ 'badaccess-groups' ] ]
+ );
+ }
+
+ protected function runConfigEditPermissions(
+ $resultNone,
+ $resultMyCss,
+ $resultMyJson,
+ $resultMyJs,
+ $resultUserCss,
+ $resultUserJson,
+ $resultUserJs,
+ $resultPatrol
+ ) {
+ $this->setUserPerm( '' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( $resultNone, $result );
+
+ $this->setUserPerm( 'editmyusercss' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( $resultMyCss, $result );
+
+ $this->setUserPerm( 'editmyuserjson' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( $resultMyJson, $result );
+
+ $this->setUserPerm( 'editmyuserjs' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( $resultMyJs, $result );
+
+ $this->setUserPerm( 'editusercss' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( $resultUserCss, $result );
+
+ $this->setUserPerm( 'edituserjson' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( $resultUserJson, $result );
+
+ $this->setUserPerm( 'edituserjs' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( $resultUserJs, $result );
+
+ $this->setUserPerm( '' );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'patrol', $this->user, $this->title );
+ $this->assertEquals( reset( $resultPatrol[0] ), reset( $result[0] ) );
+
+ $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] );
+ $result = $this->permissionManager
+ ->getPermissionErrors( 'bogus', $this->user, $this->title );
+ $this->assertEquals( [ [ 'badaccess-group0' ] ], $result );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ *
+ * This test is failing per T201776.
+ *
+ * @group Broken
+ * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions
+ */
+ public function testPageRestrictions() {
+ $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
+ getFormattedNsText( NS_PROJECT );
+
+ $this->setTitle( NS_MAIN );
+ $this->title->mRestrictionsLoaded = true;
+ $this->setUserPerm( "edit" );
+ $this->title->mRestrictions = [ "bogus" => [ 'bogus', "sysop", "protect", "" ] ];
+
+ $this->assertEquals( [],
+ $this->permissionManager->getPermissionErrors( 'edit',
+ $this->user, $this->title ) );
+
+ $this->assertEquals( true,
+ $this->permissionManager->userCan( 'edit', $this->user, $this->title,
+ PermissionManager::RIGOR_QUICK ) );
+
+ $this->title->mRestrictions = [ "edit" => [ 'bogus', "sysop", "protect", "" ],
+ "bogus" => [ 'bogus', "sysop", "protect", "" ] ];
+
+ $this->assertEquals( [ [ 'badaccess-group0' ],
+ [ 'protectedpagetext', 'bogus', 'bogus' ],
+ [ 'protectedpagetext', 'editprotected', 'bogus' ],
+ [ 'protectedpagetext', 'protect', 'bogus' ] ],
+ $this->permissionManager->getPermissionErrors( 'bogus',
+ $this->user, $this->title ) );
+ $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
+ [ 'protectedpagetext', 'editprotected', 'edit' ],
+ [ 'protectedpagetext', 'protect', 'edit' ] ],
+ $this->permissionManager->getPermissionErrors( 'edit',
+ $this->user, $this->title ) );
+ $this->setUserPerm( "" );
+ $this->assertEquals( [ [ 'badaccess-group0' ],
+ [ 'protectedpagetext', 'bogus', 'bogus' ],
+ [ 'protectedpagetext', 'editprotected', 'bogus' ],
+ [ 'protectedpagetext', 'protect', 'bogus' ] ],
+ $this->permissionManager->getPermissionErrors( 'bogus',
+ $this->user, $this->title ) );
+ $this->assertEquals( [ [ 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ],
+ [ 'protectedpagetext', 'bogus', 'edit' ],
+ [ 'protectedpagetext', 'editprotected', 'edit' ],
+ [ 'protectedpagetext', 'protect', 'edit' ] ],
+ $this->permissionManager->getPermissionErrors( 'edit',
+ $this->user, $this->title ) );
+ $this->setUserPerm( [ "edit", "editprotected" ] );
+ $this->assertEquals( [ [ 'badaccess-group0' ],
+ [ 'protectedpagetext', 'bogus', 'bogus' ],
+ [ 'protectedpagetext', 'protect', 'bogus' ] ],
+ $this->permissionManager->getPermissionErrors( 'bogus',
+ $this->user, $this->title ) );
+ $this->assertEquals( [
+ [ 'protectedpagetext', 'bogus', 'edit' ],
+ [ 'protectedpagetext', 'protect', 'edit' ] ],
+ $this->permissionManager->getPermissionErrors( 'edit',
+ $this->user, $this->title ) );
+
+ $this->title->mCascadeRestriction = true;
+ $this->setUserPerm( "edit" );
+
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'bogus', $this->user, $this->title,
+ PermissionManager::RIGOR_QUICK ) );
+
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'edit', $this->user, $this->title,
+ PermissionManager::RIGOR_QUICK ) );
+
+ $this->assertEquals( [ [ 'badaccess-group0' ],
+ [ 'protectedpagetext', 'bogus', 'bogus' ],
+ [ 'protectedpagetext', 'editprotected', 'bogus' ],
+ [ 'protectedpagetext', 'protect', 'bogus' ] ],
+ $this->permissionManager->getPermissionErrors( 'bogus',
+ $this->user, $this->title ) );
+ $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
+ [ 'protectedpagetext', 'editprotected', 'edit' ],
+ [ 'protectedpagetext', 'protect', 'edit' ] ],
+ $this->permissionManager->getPermissionErrors( 'edit',
+ $this->user, $this->title ) );
+
+ $this->setUserPerm( [ "edit", "editprotected" ] );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'bogus', $this->user, $this->title,
+ PermissionManager::RIGOR_QUICK ) );
+
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'edit', $this->user, $this->title,
+ PermissionManager::RIGOR_QUICK ) );
+
+ $this->assertEquals( [ [ 'badaccess-group0' ],
+ [ 'protectedpagetext', 'bogus', 'bogus' ],
+ [ 'protectedpagetext', 'protect', 'bogus' ],
+ [ 'protectedpagetext', 'protect', 'bogus' ] ],
+ $this->permissionManager->getPermissionErrors( 'bogus',
+ $this->user, $this->title ) );
+ $this->assertEquals( [ [ 'protectedpagetext', 'bogus', 'edit' ],
+ [ 'protectedpagetext', 'protect', 'edit' ],
+ [ 'protectedpagetext', 'protect', 'edit' ] ],
+ $this->permissionManager->getPermissionErrors( 'edit',
+ $this->user, $this->title ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions
+ */
+ public function testCascadingSourcesRestrictions() {
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->setUserPerm( [ "edit", "bogus" ] );
+
+ $this->title->mCascadeSources = [
+ Title::makeTitle( NS_MAIN, "Bogus" ),
+ Title::makeTitle( NS_MAIN, "UnBogus" )
+ ];
+ $this->title->mCascadingRestrictions = [
+ "bogus" => [ 'bogus', "sysop", "protect", "" ]
+ ];
+
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'bogus', $this->user, $this->title ) );
+ $this->assertEquals( [
+ [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
+ [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
+ [ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ] ],
+ $this->permissionManager->getPermissionErrors( 'bogus', $this->user, $this->title ) );
+
+ $this->assertEquals( true,
+ $this->permissionManager->userCan( 'edit', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager->getPermissionErrors( 'edit', $this->user, $this->title ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions
+ */
+ public function testActionPermissions() {
+ $this->setUserPerm( [ "createpage" ] );
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->title->mTitleProtection['permission'] = '';
+ $this->title->mTitleProtection['user'] = $this->user->getId();
+ $this->title->mTitleProtection['expiry'] = 'infinity';
+ $this->title->mTitleProtection['reason'] = 'test';
+ $this->title->mCascadeRestriction = false;
+
+ $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title ) );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+ $this->title->mTitleProtection['permission'] = 'editprotected';
+ $this->setUserPerm( [ 'createpage', 'protect' ] );
+ $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title ) );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+ $this->setUserPerm( [ 'createpage', 'editprotected' ] );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title ) );
+ $this->assertEquals( true,
+ $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+ $this->setUserPerm( [ 'createpage' ] );
+ $this->assertEquals( [ [ 'titleprotected', 'Useruser', 'test' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'create', $this->user, $this->title ) );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'create', $this->user, $this->title ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->setUserPerm( [ "move" ] );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+ $this->assertEquals( [ [ 'immobile-source-namespace', 'Media' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title ) );
+
+ $this->setTitle( NS_HELP, "test page" );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title ) );
+ $this->assertEquals( true,
+ $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+
+ $this->title->mInterwiki = "no";
+ $this->assertEquals( [ [ 'immobile-source-page' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'move', $this->user, $this->title ) );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'move', $this->user, $this->title ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+ $this->assertEquals( [ [ 'immobile-target-namespace', 'Media' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+
+ $this->setTitle( NS_HELP, "test page" );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+ $this->assertEquals( true,
+ $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+
+ $this->title->mInterwiki = "no";
+ $this->assertEquals( [ [ 'immobile-target-page' ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+ $this->assertEquals( false,
+ $this->permissionManager->userCan( 'move-target', $this->user, $this->title ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
+ */
+ public function testUserBlock() {
+ $this->setMwGlobals( [
+ 'wgEmailConfirmToEdit' => true,
+ 'wgEmailAuthentication' => true,
+ ] );
+
+ $this->overrideMwServices();
+ $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+ $this->setUserPerm( [
+ 'createpage',
+ 'edit',
+ 'move',
+ 'rollback',
+ 'patrol',
+ 'upload',
+ 'purge'
+ ] );
+ $this->setTitle( NS_HELP, "test page" );
+
+ # $wgEmailConfirmToEdit only applies to 'edit' action
+ $this->assertEquals( [],
+ $this->permissionManager->getPermissionErrors( 'move-target',
+ $this->user, $this->title ) );
+ $this->assertContains( [ 'confirmedittext' ],
+ $this->permissionManager
+ ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+
+ $this->setMwGlobals( 'wgEmailConfirmToEdit', false );
+ $this->overrideMwServices();
+ $this->permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+ $this->assertNotContains( [ 'confirmedittext' ],
+ $this->permissionManager
+ ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+
+ # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount'
+ $this->assertEquals( [],
+ $this->permissionManager->getPermissionErrors( 'move-target',
+ $this->user, $this->title ) );
+
+ global $wgLang;
+ $prev = time();
+ $now = time() + 120;
+ $this->user->mBlockedby = $this->user->getId();
+ $this->user->mBlock = new Block( [
+ 'address' => '127.0.8.1',
+ 'by' => $this->user->getId(),
+ 'reason' => 'no reason given',
+ 'timestamp' => $prev + 3600,
+ 'auto' => true,
+ 'expiry' => 0
+ ] );
+ $this->user->mBlock->mTimestamp = 0;
+ $this->assertEquals( [ [ 'autoblockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, 'infinite', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ] ],
+ $this->permissionManager->getPermissionErrors( 'move-target',
+ $this->user, $this->title ) );
+
+ $this->assertEquals( false, $this->permissionManager
+ ->userCan( 'move-target', $this->user, $this->title ) );
+ // quickUserCan should ignore user blocks
+ $this->assertEquals( true, $this->permissionManager
+ ->userCan( 'move-target', $this->user, $this->title,
+ PermissionManager::RIGOR_QUICK ) );
+
+ global $wgLocalTZoffset;
+ $wgLocalTZoffset = -60;
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( [
+ 'address' => '127.0.8.1',
+ 'by' => $this->user->getId(),
+ 'reason' => 'no reason given',
+ 'timestamp' => $now,
+ 'auto' => false,
+ 'expiry' => 10,
+ ] );
+ $this->assertEquals( [ [ 'blockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ],
+ $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+ # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this )
+ # $user->blockedFor() == ''
+ # $user->mBlock->mExpiry == 'infinity'
+
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( [
+ 'address' => '127.0.8.1',
+ 'by' => $this->user->getId(),
+ 'reason' => 'no reason given',
+ 'timestamp' => $now,
+ 'auto' => false,
+ 'expiry' => 10,
+ 'systemBlock' => 'test',
+ ] );
+
+ $errors = [ [ 'systemblockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', 'test', '23:00, 31 December 1969', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
+
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'upload', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'purge', $this->user, $this->title ) );
+
+ // partial block message test
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( [
+ 'address' => '127.0.8.1',
+ 'by' => $this->user->getId(),
+ 'reason' => 'no reason given',
+ 'timestamp' => $now,
+ 'sitewide' => false,
+ 'expiry' => 10,
+ ] );
+
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'upload', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'purge', $this->user, $this->title ) );
+
+ $this->user->mBlock->setRestrictions( [
+ ( new PageRestriction( 0, $this->title->getArticleID() ) )->setTitle( $this->title ),
+ ] );
+
+ $errors = [ [ 'blockedtext-partial',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
+
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'move-target', $this->user, $this->title ) );
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'rollback', $this->user, $this->title ) );
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'patrol', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'upload', $this->user, $this->title ) );
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'purge', $this->user, $this->title ) );
+
+ // Test no block.
+ $this->user->mBlockedby = null;
+ $this->user->mBlock = null;
+
+ $this->assertEquals( [],
+ $this->permissionManager
+ ->getPermissionErrors( 'edit', $this->user, $this->title ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
+ *
+ * Tests to determine that the passed in permission does not get mixed up with
+ * an action of the same name.
+ */
+ public function testUserBlockAction() {
+ global $wgLang;
+
+ $tester = $this->getMockBuilder( Action::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $tester->method( 'getName' )
+ ->willReturn( 'tester' );
+ $tester->method( 'getRestriction' )
+ ->willReturn( 'test' );
+ $tester->method( 'requiresUnblock' )
+ ->willReturn( false );
+
+ $this->setMwGlobals( [
+ 'wgActions' => [
+ 'tester' => $tester,
+ ],
+ 'wgGroupPermissions' => [
+ '*' => [
+ 'tester' => true,
+ ],
+ ],
+ ] );
+
+ $now = time();
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( [
+ 'address' => '127.0.8.1',
+ 'by' => $this->user->getId(),
+ 'reason' => 'no reason given',
+ 'timestamp' => $now,
+ 'auto' => false,
+ 'expiry' => 'infinity',
+ ] );
+
+ $errors = [ [ 'blockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, 'infinite', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ];
+
+ $this->assertEquals( $errors,
+ $this->permissionManager
+ ->getPermissionErrors( 'tester', $this->user, $this->title ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom
+ */
+ public function testBlockInstanceCache() {
+ // First, check the user isn't blocked
+ $user = $this->getMutableTestUser()->getUser();
+ $ut = Title::makeTitle( NS_USER_TALK, $user->getName() );
+ $this->assertNull( $user->getBlock( false ), 'sanity check' );
+ //$this->assertSame( '', $user->blockedBy(), 'sanity check' );
+ //$this->assertSame( '', $user->blockedFor(), 'sanity check' );
+ //$this->assertFalse( (bool)$user->isHidden(), 'sanity check' );
+ $this->assertFalse( $this->permissionManager
+ ->isBlockedFrom( $user, $ut ), 'sanity check' );
+
+ // Block the user
+ $blocker = $this->getTestSysop()->getUser();
+ $block = new Block( [
+ 'hideName' => true,
+ 'allowUsertalk' => false,
+ 'reason' => 'Because',
+ ] );
+ $block->setTarget( $user );
+ $block->setBlocker( $blocker );
+ $res = $block->insert();
+ $this->assertTrue( (bool)$res['id'], 'sanity check: Failed to insert block' );
+
+ // Clear cache and confirm it loaded the block properly
+ $user->clearInstanceCache();
+ $this->assertInstanceOf( Block::class, $user->getBlock( false ) );
+ //$this->assertSame( $blocker->getName(), $user->blockedBy() );
+ //$this->assertSame( 'Because', $user->blockedFor() );
+ //$this->assertTrue( (bool)$user->isHidden() );
+ $this->assertTrue( $this->permissionManager->isBlockedFrom( $user, $ut ) );
+
+ // Unblock
+ $block->delete();
+
+ // Clear cache and confirm it loaded the not-blocked properly
+ $user->clearInstanceCache();
+ $this->assertNull( $user->getBlock( false ) );
+ //$this->assertSame( '', $user->blockedBy() );
+ //$this->assertSame( '', $user->blockedFor() );
+ //$this->assertFalse( (bool)$user->isHidden() );
+ $this->assertFalse( $this->permissionManager->isBlockedFrom( $user, $ut ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom
+ * @dataProvider provideIsBlockedFrom
+ * @param string|null $title Title to test.
+ * @param bool $expect Expected result from User::isBlockedFrom()
+ * @param array $options Additional test options:
+ * - 'blockAllowsUTEdit': (bool, default true) Value for $wgBlockAllowsUTEdit
+ * - 'allowUsertalk': (bool, default false) Passed to Block::__construct()
+ * - 'pageRestrictions': (array|null) If non-empty, page restriction titles for the block.
+ */
+ public function testIsBlockedFrom( $title, $expect, array $options = [] ) {
+ $this->setMwGlobals( [
+ 'wgBlockAllowsUTEdit' => $options['blockAllowsUTEdit'] ?? true,
+ ] );
+
+ $user = $this->getTestUser()->getUser();
+
+ if ( $title === self::USER_TALK_PAGE ) {
+ $title = $user->getTalkPage();
+ } else {
+ $title = Title::newFromText( $title );
+ }
+
+ $restrictions = [];
+ foreach ( $options['pageRestrictions'] ?? [] as $pagestr ) {
+ $page = $this->getExistingTestPage(
+ $pagestr === self::USER_TALK_PAGE ? $user->getTalkPage() : $pagestr
+ );
+ $restrictions[] = new PageRestriction( 0, $page->getId() );
+ }
+ foreach ( $options['namespaceRestrictions'] ?? [] as $ns ) {
+ $restrictions[] = new NamespaceRestriction( 0, $ns );
+ }
+
+ $block = new Block( [
+ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+ 'allowUsertalk' => $options['allowUsertalk'] ?? false,
+ 'sitewide' => !$restrictions,
+ ] );
+ $block->setTarget( $user );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
+ if ( $restrictions ) {
+ $block->setRestrictions( $restrictions );
+ }
+ $block->insert();
+
+ try {
+ $this->assertSame( $expect, $this->permissionManager->isBlockedFrom( $user, $title ) );
+ } finally {
+ $block->delete();
+ }
+ }
+
+ public static function provideIsBlockedFrom() {
+ return [
+ 'Sitewide block, basic operation' => [ 'Test page', true ],
+ 'Sitewide block, not allowing user talk' => [
+ self::USER_TALK_PAGE, true, [
+ 'allowUsertalk' => false,
+ ]
+ ],
+ 'Sitewide block, allowing user talk' => [
+ self::USER_TALK_PAGE, false, [
+ 'allowUsertalk' => true,
+ ]
+ ],
+ 'Sitewide block, allowing user talk but $wgBlockAllowsUTEdit is false' => [
+ self::USER_TALK_PAGE, true, [
+ 'allowUsertalk' => true,
+ 'blockAllowsUTEdit' => false,
+ ]
+ ],
+ 'Partial block, blocking the page' => [
+ 'Test page', true, [
+ 'pageRestrictions' => [ 'Test page' ],
+ ]
+ ],
+ 'Partial block, not blocking the page' => [
+ 'Test page 2', false, [
+ 'pageRestrictions' => [ 'Test page' ],
+ ]
+ ],
+ 'Partial block, not allowing user talk but user talk page is not blocked' => [
+ self::USER_TALK_PAGE, false, [
+ 'allowUsertalk' => false,
+ 'pageRestrictions' => [ 'Test page' ],
+ ]
+ ],
+ 'Partial block, allowing user talk but user talk page is blocked' => [
+ self::USER_TALK_PAGE, true, [
+ 'allowUsertalk' => true,
+ 'pageRestrictions' => [ self::USER_TALK_PAGE ],
+ ]
+ ],
+ 'Partial block, user talk page is not blocked but $wgBlockAllowsUTEdit is false' => [
+ self::USER_TALK_PAGE, false, [
+ 'allowUsertalk' => false,
+ 'pageRestrictions' => [ 'Test page' ],
+ 'blockAllowsUTEdit' => false,
+ ]
+ ],
+ 'Partial block, user talk page is blocked and $wgBlockAllowsUTEdit is false' => [
+ self::USER_TALK_PAGE, true, [
+ 'allowUsertalk' => true,
+ 'pageRestrictions' => [ self::USER_TALK_PAGE ],
+ 'blockAllowsUTEdit' => false,
+ ]
+ ],
+ 'Partial user talk namespace block, not allowing user talk' => [
+ self::USER_TALK_PAGE, true, [
+ 'allowUsertalk' => false,
+ 'namespaceRestrictions' => [ NS_USER_TALK ],
+ ]
+ ],
+ 'Partial user talk namespace block, allowing user talk' => [
+ self::USER_TALK_PAGE, false, [
+ 'allowUsertalk' => true,
+ 'namespaceRestrictions' => [ NS_USER_TALK ],
+ ]
+ ],
+ 'Partial user talk namespace block, where $wgBlockAllowsUTEdit is false' => [
+ self::USER_TALK_PAGE, true, [
+ 'allowUsertalk' => true,
+ 'namespaceRestrictions' => [ NS_USER_TALK ],
+ 'blockAllowsUTEdit' => false,
+ ]
+ ],
+ ];
+ }
+
+}
/**
* @group Database
*
- * @covers Title::getUserPermissionsErrors
- * @covers Title::getUserPermissionsErrorsInternal
+ * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrors
+ * @covers \MediaWiki\Permissions\PermissionManager::getPermissionErrorsInternal
*/
class TitlePermissionTest extends MediaWikiLangTestCase {
* This test is failing per T201776.
*
* @group Broken
- * @covers Title::checkQuickPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkQuickPermissions
*/
public function testQuickPermissions() {
$prefix = MediaWikiServices::getInstance()->getContentLanguage()->
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkSpecialsAndNSPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions
*/
public function testSpecialsAndNSPermissions() {
global $wgNamespaceProtection;
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testJsConfigEditPermissions() {
$this->setUser( $this->userName );
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testJsonConfigEditPermissions() {
$prefix = MediaWikiServices::getInstance()->getContentLanguage()->
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testCssConfigEditPermissions() {
$this->setUser( $this->userName );
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testOtherJsConfigEditPermissions() {
$this->setUser( $this->userName );
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testOtherJsonConfigEditPermissions() {
$this->setUser( $this->userName );
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testOtherCssConfigEditPermissions() {
$this->setUser( $this->userName );
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testOtherNonConfigEditPermissions() {
$this->setUser( $this->userName );
/**
* @todo This should use data providers like the other methods here.
- * @covers Title::checkUserConfigPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserConfigPermissions
*/
public function testPatrolActionConfigEditPermissions() {
$this->setUser( 'anon' );
* This test is failing per T201776.
*
* @group Broken
- * @covers Title::checkPageRestrictions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkPageRestrictions
*/
public function testPageRestrictions() {
$prefix = MediaWikiServices::getInstance()->getContentLanguage()->
}
/**
- * @covers Title::checkCascadingSourcesRestrictions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions
*/
public function testCascadingSourcesRestrictions() {
$this->setTitle( NS_MAIN, "test page" );
/**
* @todo This test method should be split up into separate test methods and
* data providers
- * @covers Title::checkActionPermissions
+ * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions
*/
public function testActionPermissions() {
$this->setUserPerm( [ "createpage" ] );
}
/**
- * @covers Title::checkUserBlock
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
*/
public function testUserBlock() {
$this->setMwGlobals( [
'wgEmailConfirmToEdit' => true,
'wgEmailAuthentication' => true,
] );
+ $this->overrideMwServices();
$this->setUserPerm( [ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ] );
$this->setTitle( NS_HELP, "test page" );
$this->title->getUserPermissionsErrors( 'edit', $this->user ) );
$this->setMwGlobals( 'wgEmailConfirmToEdit', false );
+ $this->overrideMwServices();
+
$this->assertNotContains( [ 'confirmedittext' ],
$this->title->getUserPermissionsErrors( 'edit', $this->user ) );
}
/**
- * @covers Title::checkUserBlock
+ * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
*
* Tests to determine that the passed in permission does not get mixed up with
* an action of the same name.
<?php
+use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
/**
* @param string $action
* @param array|string|bool $expected Required error
*
- * @covers Title::checkReadPermissions
+ * @covers \Mediawiki\Permissions\PermissionManager::checkReadPermissions
* @dataProvider dataWgWhitelistReadRegexp
*/
public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) {
$this->assertEquals( $value->getFragment(), $title->getFragment() );
}
+ /**
+ * @covers Title::newFromLinkTarget
+ * @dataProvider provideNewFromTitleValue
+ */
+ public function testNewFromLinkTarget( LinkTarget $value ) {
+ $title = Title::newFromLinkTarget( $value );
+
+ $dbkey = str_replace( ' ', '_', $value->getText() );
+ $this->assertEquals( $dbkey, $title->getDBkey() );
+ $this->assertEquals( $value->getNamespace(), $title->getNamespace() );
+ $this->assertEquals( $value->getFragment(), $title->getFragment() );
+ }
+
+ /**
+ * @covers Title::newFromLinkTarget
+ */
+ public function testNewFromLinkTarget_clone() {
+ $title = Title::newFromText( __METHOD__ );
+ $this->assertSame( $title, Title::newFromLinkTarget( $title ) );
+
+ // The Title::NEW_CLONE flag should ensure that a fresh instance is returned.
+ $clone = Title::newFromLinkTarget( $title, Title::NEW_CLONE );
+ $this->assertNotSame( $title, $clone );
+ $this->assertTrue( $clone->equals( $title ) );
+ }
+
public static function provideGetTitleValue() {
return [
[ 'Foo' ],
* @covers LoadBalancer::getAnyOpenConnection()
*/
function testOpenConnection() {
- global $wgDBname;
-
- $lb = new LoadBalancer( [
- 'servers' => [ $this->makeServerConfig() ],
- 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
- ] );
+ $lb = $this->newSingleServerLocalLoadBalancer();
$i = $lb->getWriterIndex();
$this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
*
* File must be in the path returned by getFilePath()
* @param string $name File name
- * @param string|null $type MIME type [optional]
+ * @param string|false $type MIME type [optional]
* @return UnregisteredLocalFile
*/
- protected function dataFile( $name, $type = null ) {
- if ( !$type ) {
- // Autodetect by file extension for the lazy.
- $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
- $parts = explode( $name, '.' );
- $type = $magic->guessTypesForExtension( $parts[count( $parts ) - 1] );
- }
+ protected function dataFile( $name, $type = false ) {
return new UnregisteredLocalFile( false, $this->repo,
"mwstore://localtesting/data/$name", $type );
}