* User::isBlocked() is deprecated since it does not tell you if the user is
blocked from editing a particular page. Use User::getBlock() or
PermissionManager::isBlockedFrom() or PermissionManager::userCan() instead.
+* User::isLocallyBlockedProxy and User::inDnsBlacklist are deprecated and moved
+ to the BlockManager as private helper methods.
+* User::isDnsBlacklisted is deprecated. Use BlockManager::isDnsBlacklisted
+ instead.
* …
=== Other changes in 1.34 ===
const TYPE_ID = 5;
/**
- * Create a new block with specified parameters on a user, IP or IP range.
+ * Create a new block with specified option parameters on a user, IP or IP range.
*
* @param array $options Parameters of the block:
* address string|User Target user name, User object, IP address or IP range
* actions, except those specifically allowed by
* other block flags
*
- * @since 1.26 accepts $options array instead of individual parameters; order
- * of parameters above reflects the original order
+ * @since 1.26 $options array
*/
- function __construct( $options = [] ) {
+ public function __construct( array $options = [] ) {
$defaults = [
'address' => '',
'user' => null,
$start = Wikimedia\base_convert( $block->getRangeStart(), 16, 10 );
$size = log( $end - $start + 1, 2 );
- # This has the nice property that a /32 block is ranked equally with a
- # single-IP block, which is exactly what it is...
+ # Rank a range block covering a single IP equally with a single-IP block
$score = self::TYPE_RANGE - 1 + ( $size / 128 );
} else {
* @since 1.32 changed allowed flags
* @var int An appropriate combination of SCHEMA_COMPAT_XXX flags.
*/
-$wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
+$wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_NEW;
/**
* Flag to enable Partial Blocks. This allows an admin to prevent a user from editing specific pages
use Hooks;
use IBufferingStatsdDataFactory;
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
+use MediaWiki\Block\BlockManager;
use MediaWiki\Block\BlockRestrictionStore;
use MediaWiki\Http\HttpRequestFactory;
use MediaWiki\Permissions\PermissionManager;
return $this->getService( 'BlobStoreFactory' );
}
+ /**
+ * @since 1.34
+ * @return BlockManager
+ */
+ public function getBlockManager() : BlockManager {
+ return $this->getService( 'BlockManager' );
+ }
+
/**
* @since 1.33
* @return BlockRestrictionStore
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use MediaWiki\Auth\AuthManager;
+use MediaWiki\Block\BlockManager;
use MediaWiki\Block\BlockRestrictionStore;
use MediaWiki\Config\ConfigRepository;
use MediaWiki\Interwiki\ClassicInterwikiLookup;
);
},
+ 'BlockManager' => function ( MediaWikiServices $services ) : BlockManager {
+ $config = $services->getMainConfig();
+ $context = RequestContext::getMain();
+ return new BlockManager(
+ $context->getUser(),
+ $context->getRequest(),
+ $config->get( 'ApplyIpBlocksToXff' ),
+ $config->get( 'CookieSetOnAutoblock' ),
+ $config->get( 'CookieSetOnIpBlock' ),
+ $config->get( 'DnsBlacklistUrls' ),
+ $config->get( 'EnableDnsBlacklist' ),
+ $config->get( 'ProxyList' ),
+ $config->get( 'ProxyWhitelist' ),
+ $config->get( 'SoftBlockRanges' )
+ );
+ },
+
'BlockRestrictionStore' => function ( MediaWikiServices $services ) : BlockRestrictionStore {
return new BlockRestrictionStore(
$services->getDBLoadBalancer()
"apihelp-edit-param-text": "محتوى الصفحة",
"apihelp-edit-param-summary": "ملخص التعديل. أيضا عنوان القسم عند عدم تعيين $1section=new and $1sectiontitle.",
"apihelp-edit-param-tags": "عدل الوسوم لتطبيق المراجعة.",
- "apihelp-edit-param-minor": "تعدÙ\8aÙ\84 Ø·Ù\81Ù\8aÙ\81",
- "apihelp-edit-param-notminor": "تعدÙ\8aÙ\84 غÙ\8aر Ø·Ù\81Ù\8aÙ\81.",
+ "apihelp-edit-param-minor": "اÙ\84تعÙ\84Ù\8aÙ\85 عÙ\84Ù\89 Ù\87ذا اÙ\84تعدÙ\8aÙ\84 Ù\83تعدÙ\8aÙ\84 Ø·Ù\81Ù\8aÙ\81.",
+ "apihelp-edit-param-notminor": "عدÙ\85 اÙ\84تعÙ\84Ù\8aÙ\85 عÙ\84Ù\89 Ù\87ذا اÙ\84تعدÙ\8aÙ\84 Ù\83تعدÙ\8aÙ\84 Ø·Ù\81Ù\8aÙ\81 ØتÙ\89 إذا تÙ\85 تعÙ\8aÙ\8aÙ\86 تÙ\81ضÙ\8aÙ\84 اÙ\84Ù\85ستخدÙ\85 \"{{int:tog-minordefault}}\".",
"apihelp-edit-param-bot": "علم على هذا التعديل كتعديل بوت.",
"apihelp-edit-param-basetimestamp": "الطابع الزمني للمراجعة الأساسية، ويُستخدَم للكشف عن الحروب التحريرية، ويمكن الحصول عليها من خلال [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
"apihelp-edit-param-starttimestamp": "الطابع الزمني عند بدء عملية التحرير، ويُستخدَم للكشف عن الحروب التحريرية، ويمكن الحصول عليها من خلال <var>[[Special:ApiHelp/main|curtimestamp]]</var> when beginning the edit process (e.g. when loading the page content to edit).",
"Dvorapa",
"Matěj Suchánek",
"Ilimanaq29",
- "Patriccck"
+ "Patriccck",
+ "Ján Kepler"
]
},
"apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentace]]\n* [[mw:Special:MyLanguage/API:FAQ|Otázky a odpovědi]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-mailová konference]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Oznámení k API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Chyby a požadavky]\n</div>\n<strong>Stav:</strong> Všechny funkce uvedené na této stránce by měly fungovat, ale API se stále aktivně vyvíjí a může se kdykoli změnit. Upozornění na změny získáte přihlášením se k [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-mailové konferenci mediawiki-api-announce].\n\n<strong>Chybné požadavky:</strong> Pokud jsou do API zaslány chybné požadavky, bude vrácena HTTP hlavička s klíčem „MediaWiki-API-Error“ a hodnota této hlavičky a chybový kód budou nastaveny na stejnou hodnotu. Více informací najdete [[mw:Special:MyLanguage/API:Errors_and_warnings|v dokumentaci]].\n\n<p class=\"mw-apisandbox-link\"><strong>Testování:</strong> Pro jednoduché testování požadavků na API zkuste [[Special:ApiSandbox]].</p>",
"apihelp-edit-param-pageid": "ID stránky, která se má editovat. Není možné použít společně s <var>$1title</var>.",
"apihelp-edit-param-sectiontitle": "Název nové sekce.",
"apihelp-edit-param-text": "Obsah stránky.",
- "apihelp-edit-param-minor": "Malá editace.",
+ "apihelp-edit-param-minor": "Označit toto jako malou editaci",
"apihelp-edit-param-notminor": "Nemalá editace.",
"apihelp-edit-param-bot": "Označit tuto editaci jako editaci robota.",
"apihelp-edit-param-createonly": "Needitovat stránku, pokud již existuje.",
"apihelp-edit-param-text": "Contenu de la page.",
"apihelp-edit-param-summary": "Modifier le résumé. Également le titre de la section quand $1section=new et $1sectiontitle n’est pas défini.",
"apihelp-edit-param-tags": "Modifier les balises à appliquer à la version.",
- "apihelp-edit-param-minor": "Modification mineure.",
- "apihelp-edit-param-notminor": "Modification non mineure.",
+ "apihelp-edit-param-minor": "Marquer cette modification comme étant mineure.",
+ "apihelp-edit-param-notminor": "Ne pas marquer cette modification comme mineure, même si la préférence utilisateur « {{int:tog-minordefault}} » est positionnée.",
"apihelp-edit-param-bot": "Marquer cette modification comme effectuée par un robot.",
"apihelp-edit-param-basetimestamp": "Horodatage de la révision de base, utilisé pour détecter les conflits de modification. Peut être obtenu via [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
"apihelp-edit-param-starttimestamp": "L'horodatage, lorsque le processus d'édition est démarré, est utilisé pour détecter les conflits de modification. Une valeur appropriée peut être obtenue en utilisant <var>[[Special:ApiHelp/main|curtimestamp]]</var> lors du démarrage du processus d'édition (par ex. en chargeant le contenu de la page à modifier).",
"apihelp-edit-param-text": "Contenuto della pagina.",
"apihelp-edit-param-summary": "Oggetto della modifica. Anche titolo della sezione se $1sezione=new e $1sectiontitle non è impostato.",
"apihelp-edit-param-tags": "Cambia i tag da applicare alla revisione.",
- "apihelp-edit-param-minor": "Modifica minore.",
- "apihelp-edit-param-notminor": "Modifica non minore.",
+ "apihelp-edit-param-minor": "Contrassegna questa modifica come minore.",
+ "apihelp-edit-param-notminor": "Non contrassegnare questa modifica come minore anche se la preferenza \"{{int:tog-minordefault}}\" è impostata.",
"apihelp-edit-param-bot": "Contrassegna questa modifica come eseguita da un bot.",
"apihelp-edit-param-createonly": "Non modificare la pagina se già esiste.",
"apihelp-edit-param-nocreate": "Genera un errore se la pagina non esiste.",
"apihelp-edit-param-text": "Conteúdo da página.",
"apihelp-edit-param-summary": "Edit o resumo. Também o título da seção quando $1section=new e $1sectiontitle não está definido.",
"apihelp-edit-param-tags": "Alterar as tags para aplicar à revisão.",
- "apihelp-edit-param-minor": "Edição menor.",
- "apihelp-edit-param-notminor": "Edição não-menor.",
+ "apihelp-edit-param-minor": "Marque esta edição como uma edição menor.",
+ "apihelp-edit-param-notminor": "Não marque esta edição como uma edição menor, mesmo se a preferência do usuário \"{{int:tog-minordefault}}\" é definida.",
"apihelp-edit-param-bot": "Marcar esta edição como uma edição de bot.",
"apihelp-edit-param-basetimestamp": "Timestamp da revisão base, usada para detectar conflitos de edição. Pode ser obtido através de [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
"apihelp-edit-param-starttimestamp": "Timestamp quando o processo de edição começou, usado para detectar conflitos de edição. Um valor apropriado pode ser obtido usando <var>[[Special:ApiHelp/main|curtimestamp]]</var> ao iniciar o processo de edição (por exemplo, ao carregar o conteúdo da página a editar).",
"apihelp-edit-param-text": "Sidans innehåll.",
"apihelp-edit-param-summary": "Redigeringssammanfattning. Även avsnittets rubrik när $1section=new och $1sectiontitle inte anges.",
"apihelp-edit-param-tags": "Ändra taggar till att gälla för revideringen.",
- "apihelp-edit-param-minor": "Mindre redigering.",
- "apihelp-edit-param-notminor": "Icke-mindre redigering.",
+ "apihelp-edit-param-minor": "Markera denna redigering som en mindre redigering.",
+ "apihelp-edit-param-notminor": "Markera inte denna redigering som en mindre redigering även om användarinställningen \"{{int:tog-minordefault}}\" är inställd.",
"apihelp-edit-param-bot": "Markera denna redigering som en robotredigering.",
"apihelp-edit-param-basetimestamp": "Tidsstämpel för grundversionen, används för att upptäcka redigeringskonflikter. Kan erhållas genom [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
"apihelp-edit-param-starttimestamp": "Tidsstämpel för när redigeringsprocessen började, används för att upptäcka redigeringskonflikter. Ett lämpligt värde kan erhållas via <var>[[Special:ApiHelp/main|curtimestamp]]</var> när redigeringsprocessen startas (t.ex. när sidans innehåll laddas för redigering).",
}
$ip = $this->getRequest()->getIP();
- if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
+ if (
+ MediaWikiServices::getInstance()->getBlockManager()
+ ->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ )
+ ) {
return Status::newFatal( 'sorbs_create_account_reason' );
}
--- /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\Block;
+
+use Block;
+use IP;
+use User;
+use WebRequest;
+use Wikimedia\IPSet;
+use MediaWiki\User\UserIdentity;
+
+/**
+ * A service class for checking blocks.
+ * To obtain an instance, use MediaWikiServices::getInstance()->getBlockManager().
+ *
+ * @since 1.34 Refactored from User and Block.
+ */
+class BlockManager {
+ // TODO: This should be UserIdentity instead of User
+ /** @var User */
+ private $currentUser;
+
+ /** @var WebRequest */
+ private $currentRequest;
+
+ /** @var bool */
+ private $applyIpBlocksToXff;
+
+ /** @var bool */
+ private $cookieSetOnAutoblock;
+
+ /** @var bool */
+ private $cookieSetOnIpBlock;
+
+ /** @var array */
+ private $dnsBlacklistUrls;
+
+ /** @var bool */
+ private $enableDnsBlacklist;
+
+ /** @var array */
+ private $proxyList;
+
+ /** @var array */
+ private $proxyWhitelist;
+
+ /** @var array */
+ private $softBlockRanges;
+
+ /**
+ * @param User $currentUser
+ * @param WebRequest $currentRequest
+ * @param bool $applyIpBlocksToXff
+ * @param bool $cookieSetOnAutoblock
+ * @param bool $cookieSetOnIpBlock
+ * @param array $dnsBlacklistUrls
+ * @param bool $enableDnsBlacklist
+ * @param array $proxyList
+ * @param array $proxyWhitelist
+ * @param array $softBlockRanges
+ */
+ public function __construct(
+ $currentUser,
+ $currentRequest,
+ $applyIpBlocksToXff,
+ $cookieSetOnAutoblock,
+ $cookieSetOnIpBlock,
+ $dnsBlacklistUrls,
+ $enableDnsBlacklist,
+ $proxyList,
+ $proxyWhitelist,
+ $softBlockRanges
+ ) {
+ $this->currentUser = $currentUser;
+ $this->currentRequest = $currentRequest;
+ $this->applyIpBlocksToXff = $applyIpBlocksToXff;
+ $this->cookieSetOnAutoblock = $cookieSetOnAutoblock;
+ $this->cookieSetOnIpBlock = $cookieSetOnIpBlock;
+ $this->dnsBlacklistUrls = $dnsBlacklistUrls;
+ $this->enableDnsBlacklist = $enableDnsBlacklist;
+ $this->proxyList = $proxyList;
+ $this->proxyWhitelist = $proxyWhitelist;
+ $this->softBlockRanges = $softBlockRanges;
+ }
+
+ /**
+ * Get the blocks that apply to a user and return the most relevant one.
+ *
+ * TODO: $user should be UserIdentity instead of User
+ *
+ * @internal This should only be called by User::getBlockedStatus
+ * @param User $user
+ * @param bool $fromReplica Whether to check the replica DB first.
+ * To improve performance, non-critical checks are done against replica DBs.
+ * Check when actually saving should be done against master.
+ * @return Block|null The most relevant block, or null if there is no block.
+ */
+ public function getUserBlock( User $user, $fromReplica ) {
+ $isAnon = $user->getId() === 0;
+
+ // TODO: If $user is the current user, we should use the current request. Otherwise,
+ // we should not look for XFF or cookie blocks.
+ $request = $user->getRequest();
+
+ # We only need to worry about passing the IP address to the Block generator if the
+ # user is not immune to autoblocks/hardblocks, and they are the current user so we
+ # know which IP address they're actually coming from
+ $ip = null;
+ $sessionUser = $this->currentUser;
+ // the session user is set up towards the end of Setup.php. Until then,
+ // assume it's a logged-out user.
+ $globalUserName = $sessionUser->isSafeToLoad()
+ ? $sessionUser->getName()
+ : IP::sanitizeIP( $this->currentRequest->getIP() );
+ if ( $user->getName() === $globalUserName && !$user->isAllowed( 'ipblock-exempt' ) ) {
+ $ip = $this->currentRequest->getIP();
+ }
+
+ // User/IP blocking
+ // TODO: remove dependency on Block
+ $block = Block::newFromTarget( $user, $ip, !$fromReplica );
+
+ // Cookie blocking
+ if ( !$block instanceof Block ) {
+ $block = $this->getBlockFromCookieValue( $user, $request );
+ }
+
+ // Proxy blocking
+ if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $this->proxyWhitelist ) ) {
+ // Local list
+ if ( $this->isLocallyBlockedProxy( $ip ) ) {
+ $block = new Block( [
+ 'byText' => wfMessage( 'proxyblocker' )->text(),
+ 'reason' => wfMessage( 'proxyblockreason' )->plain(),
+ 'address' => $ip,
+ 'systemBlock' => 'proxy',
+ ] );
+ } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
+ $block = new Block( [
+ 'byText' => wfMessage( 'sorbs' )->text(),
+ 'reason' => wfMessage( 'sorbsreason' )->plain(),
+ 'address' => $ip,
+ 'systemBlock' => 'dnsbl',
+ ] );
+ }
+ }
+
+ // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
+ if ( !$block instanceof Block
+ && $this->applyIpBlocksToXff
+ && $ip !== null
+ && !in_array( $ip, $this->proxyWhitelist )
+ ) {
+ $xff = $request->getHeader( 'X-Forwarded-For' );
+ $xff = array_map( 'trim', explode( ',', $xff ) );
+ $xff = array_diff( $xff, [ $ip ] );
+ // TODO: remove dependency on Block
+ $xffblocks = Block::getBlocksForIPList( $xff, $isAnon, !$fromReplica );
+ // TODO: remove dependency on Block
+ $block = Block::chooseBlock( $xffblocks, $xff );
+ if ( $block instanceof Block ) {
+ # Mangle the reason to alert the user that the block
+ # originated from matching the X-Forwarded-For header.
+ $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
+ }
+ }
+
+ if ( !$block instanceof Block
+ && $ip !== null
+ && $isAnon
+ && IP::isInRanges( $ip, $this->softBlockRanges )
+ ) {
+ $block = new Block( [
+ 'address' => $ip,
+ 'byText' => 'MediaWiki default',
+ 'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
+ 'anonOnly' => true,
+ 'systemBlock' => 'wgSoftBlockRanges',
+ ] );
+ }
+
+ return $block;
+ }
+
+ /**
+ * Try to load a Block from an ID given in a cookie value.
+ *
+ * @param UserIdentity $user
+ * @param WebRequest $request
+ * @return Block|bool The Block object, or false if none could be loaded.
+ */
+ private function getBlockFromCookieValue(
+ UserIdentity $user,
+ WebRequest $request
+ ) {
+ $blockCookieVal = $request->getCookie( 'BlockID' );
+ $response = $request->response();
+
+ // Make sure there's something to check. The cookie value must start with a number.
+ if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
+ return false;
+ }
+ // Load the Block from the ID in the cookie.
+ // TODO: remove dependency on Block
+ $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
+ if ( $blockCookieId !== null ) {
+ // An ID was found in the cookie.
+ // TODO: remove dependency on Block
+ $tmpBlock = Block::newFromID( $blockCookieId );
+ if ( $tmpBlock instanceof Block ) {
+ switch ( $tmpBlock->getType() ) {
+ case Block::TYPE_USER:
+ $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
+ $useBlockCookie = ( $this->cookieSetOnAutoblock === true );
+ break;
+ case Block::TYPE_IP:
+ case Block::TYPE_RANGE:
+ // If block is type IP or IP range, load only if user is not logged in (T152462)
+ $blockIsValid = !$tmpBlock->isExpired() && $user->getId() === 0;
+ $useBlockCookie = ( $this->cookieSetOnIpBlock === true );
+ break;
+ default:
+ $blockIsValid = false;
+ $useBlockCookie = false;
+ }
+
+ if ( $blockIsValid && $useBlockCookie ) {
+ // Use the block.
+ return $tmpBlock;
+ }
+
+ // If the block is not valid, remove the cookie.
+ // TODO: remove dependency on Block
+ Block::clearCookie( $response );
+ } else {
+ // If the block doesn't exist, remove the cookie.
+ // TODO: remove dependency on Block
+ Block::clearCookie( $response );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if an IP address is in the local proxy list
+ *
+ * @param string $ip
+ * @return bool
+ */
+ private function isLocallyBlockedProxy( $ip ) {
+ if ( !$this->proxyList ) {
+ return false;
+ }
+
+ if ( !is_array( $this->proxyList ) ) {
+ // Load values from the specified file
+ $this->proxyList = array_map( 'trim', file( $this->proxyList ) );
+ }
+
+ $resultProxyList = [];
+ $deprecatedIPEntries = [];
+
+ // backward compatibility: move all ip addresses in keys to values
+ foreach ( $this->proxyList as $key => $value ) {
+ $keyIsIP = IP::isIPAddress( $key );
+ $valueIsIP = IP::isIPAddress( $value );
+ if ( $keyIsIP && !$valueIsIP ) {
+ $deprecatedIPEntries[] = $key;
+ $resultProxyList[] = $key;
+ } elseif ( $keyIsIP && $valueIsIP ) {
+ $deprecatedIPEntries[] = $key;
+ $resultProxyList[] = $key;
+ $resultProxyList[] = $value;
+ } else {
+ $resultProxyList[] = $value;
+ }
+ }
+
+ if ( $deprecatedIPEntries ) {
+ wfDeprecated(
+ 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
+ implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
+ }
+
+ $proxyListIPSet = new IPSet( $resultProxyList );
+ return $proxyListIPSet->match( $ip );
+ }
+
+ /**
+ * Whether the given IP is in a DNS blacklist.
+ *
+ * @param string $ip IP to check
+ * @param bool $checkWhitelist Whether to check the whitelist first
+ * @return bool True if blacklisted.
+ */
+ public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
+ if ( !$this->enableDnsBlacklist ||
+ ( $checkWhitelist && in_array( $ip, $this->proxyWhitelist ) )
+ ) {
+ return false;
+ }
+
+ return $this->inDnsBlacklist( $ip, $this->dnsBlacklistUrls );
+ }
+
+ /**
+ * Whether the given IP is in a given DNS blacklist.
+ *
+ * @param string $ip IP to check
+ * @param array $bases Array of Strings: URL of the DNS blacklist
+ * @return bool True if blacklisted.
+ */
+ private function inDnsBlacklist( $ip, array $bases ) {
+ $found = false;
+ // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
+ if ( IP::isIPv4( $ip ) ) {
+ // Reverse IP, T23255
+ $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
+
+ foreach ( $bases as $base ) {
+ // Make hostname
+ // If we have an access key, use that too (ProjectHoneypot, etc.)
+ $basename = $base;
+ if ( is_array( $base ) ) {
+ if ( count( $base ) >= 2 ) {
+ // Access key is 1, base URL is 0
+ $host = "{$base[1]}.$ipReversed.{$base[0]}";
+ } else {
+ $host = "$ipReversed.{$base[0]}";
+ }
+ $basename = $base[0];
+ } else {
+ $host = "$ipReversed.$base";
+ }
+
+ // Send query
+ $ipList = gethostbynamel( $host );
+
+ if ( $ipList ) {
+ wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
+ $found = true;
+ break;
+ }
+
+ wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
+ }
+ }
+
+ return $found;
+ }
+
+}
return $this->$name;
}
- $qualifiedName = __CLASS__ . '::$' . $name;
- if ( $this->deprecationHelperGetPropertyOwner( $name ) ) {
+ $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
+ $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
+ if ( $ownerClass ) {
// Someone tried to access a normal non-public property. Try to behave like PHP would.
trigger_error( "Cannot access non-public property $qualifiedName", E_USER_ERROR );
} else {
return;
}
- $qualifiedName = __CLASS__ . '::$' . $name;
- if ( $this->deprecationHelperGetPropertyOwner( $name ) ) {
+ $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
+ $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
+ if ( $ownerClass ) {
// Someone tried to access a normal non-public property. Try to behave like PHP would.
trigger_error( "Cannot access non-public property $qualifiedName", E_USER_ERROR );
} else {
* Like property_exists but also check for non-visible private properties and returns which
* class in the inheritance chain declared the property.
* @param string $property
- * @return string|bool Best guess for the class in which the property is defined.
+ * @return string|bool Best guess for the class in which the property is defined. False if
+ * the object does not have such a property.
*/
private function deprecationHelperGetPropertyOwner( $property ) {
- // Easy branch: check for protected property / private property of the current class.
- if ( property_exists( $this, $property ) ) {
- // The class name is not necessarily correct here but getting the correct class
- // name would be expensive, this will work most of the time and getting it
- // wrong is not a big deal.
- return __CLASS__;
- }
- // property_exists() returns false when the property does exist but is private (and not
- // defined by the current class, for some value of "current" that differs slightly
- // between engines).
- // Since PHP triggers an error on public access of non-public properties but happily
- // allows public access to undefined properties, we need to detect this case as well.
- // Reflection is slow so use array cast hack to check for that:
+ // Returning false is a non-error path and should avoid slow checks like reflection.
+ // Use array cast hack instead.
$obfuscatedProps = array_keys( (array)$this );
$obfuscatedPropTail = "\0$property";
foreach ( $obfuscatedProps as $obfuscatedProp ) {
if ( strpos( $obfuscatedProp, $obfuscatedPropTail, 1 ) !== false ) {
$classname = substr( $obfuscatedProp, 1, -strlen( $obfuscatedPropTail ) );
if ( $classname === '*' ) {
- // sanity; this shouldn't be possible as protected properties were handled earlier
- $classname = __CLASS__;
+ // protected property; we didn't get the name, but we are on an error path
+ // now so it's fine to use reflection
+ return ( new ReflectionProperty( $this, $property ) )->getDeclaringClass()->getName();
}
return $classname;
}
"config-upgrade-key-missing": "Έχει εντοπιστεί μια υπάρχουσα εγκατάσταση του MediaWiki.\nΓια να αναβαθμίσετε αυτήν την εγκατάσταση, παρακαλούμε να βάλετε την ακόλουθη γραμμή στο κάτω μέρος του <code>LocalSettings.php</code> σας:\n\n$1",
"config-localsettings-incomplete": "Το υπάρχον <code>LocalSettings.php</code> φαίνεται να είναι ελλιπές.\nΤο $1 μεταβλητή δεν έχει οριστεί.\nΠαρακαλούμε να αλλάξετε το <code>LocalSettings.php</code> έτσι ώστε αυτή η μεταβλητή έχει οριστεί, και κάντε κλικ στο \"{{int:Config-continue}}\".",
"config-localsettings-connection-error": "Ένα σφάλμα παρουσιάστηκε κατά τη σύνδεση με τη βάση δεδομένων και με τη χρήση των ρυθμίσεων που ορίστηκαν στο <code>LocalSettings.php</code>. Παρακαλούμε διορθώστε αυτές τις ρυθμίσεις και δοκιμάστε ξανά.\n\n$1",
- "config-session-error": "ΣÏ\86άλμα καÏ\84ά Ï\84ην εκκίνηÏ\83η Ï\83Ï\85νεδÏ\81ίας: $1",
- "config-session-expired": "Τα δεδομÎνα Ï\83Ï\85νÏ\8cδοÏ\85 Ï\86αίνεÏ\84αι να ÎÏ\87οÏ\85ν λήξει.\nΣÏ\85νεδÏ\81ίεÏ\82 ÎÏ\87οÏ\85ν Ï\81Ï\85θμιÏ\83Ï\84εί για μια διάÏ\81κεια ζÏ\89ήÏ\82 $1.\nÎ\9cÏ\80οÏ\81είÏ\84ε να αÏ\85ξήÏ\83εÏ\84ε αÏ\85Ï\84Ï\8c βάζονÏ\84αÏ\82 <code>session.gc_maxlifetime</code> στο php.ini.\nΚάντε επανεκκίνηση της διαδικασίας εγκατάστασης.",
- "config-no-session": "Î\97 Ï\83Ï\85νεδÏ\81ία δεδομÎνÏ\89ν Ï\83αÏ\82 ÎÏ\87ει Ï\87αθεί!Î\95λÎγξÏ\84ε Ï\84ο αÏ\81Ï\87είο php.ini και βεβαιÏ\89θείÏ\84ε Ï\8cÏ\84ι Ï\84ο <code>session.save_path</code> ÎÏ\87ει μÏ\80ει στον κατάλληλο κατάλογο.",
+ "config-session-error": "ΣÏ\86άλμα καÏ\84ά Ï\84ην εκκίνηÏ\83η Ï\84ηÏ\82 Ï\80εÏ\81ιÏ\8cδοÏ\85 Ï\83Ï\8dνδεÏ\83ης: $1",
+ "config-session-expired": "Τα δεδομÎνα Ï\84ηÏ\82 Ï\80εÏ\81ιÏ\8cδοÏ\85 Ï\83Ï\8dνδεÏ\83ηÏ\82 Ï\86αίνεÏ\84αι να ÎÏ\87οÏ\85ν λήξει.\nÎ\9fι Ï\80εÏ\81ίοδοι Ï\83Ï\8dνδεÏ\83ηÏ\82 είναι Ï\81Ï\85θμιÏ\83μÎνεÏ\82 για διάÏ\81κεια ζÏ\89ήÏ\82 $1.\nÎ\9cÏ\80οÏ\81είÏ\84ε να Ï\84ην αÏ\85ξήÏ\83εÏ\84ε θÎÏ\84ονÏ\84αÏ\82 Ï\84ο <code>session.gc_maxlifetime</code> στο php.ini.\nΚάντε επανεκκίνηση της διαδικασίας εγκατάστασης.",
+ "config-no-session": "Τα δεδομÎνα Ï\84ηÏ\82 Ï\80εÏ\81ιÏ\8cδοÏ\85 Ï\83Ï\8dνδεÏ\83ήÏ\82 Ï\83αÏ\82 ÎÏ\87οÏ\85ν Ï\87αθεί!\nÎ\95λÎγξÏ\84ε Ï\84ο php.ini Ï\83αÏ\82 και βεβαιÏ\89θείÏ\84ε Ï\8cÏ\84ι Ï\84ο <code>session.save_path</code> ÎÏ\87ει οÏ\81ιÏ\83Ï\84εί στον κατάλληλο κατάλογο.",
"config-your-language": "Η γλώσσα σας:",
"config-your-language-help": "Επιλέξτε μία γλώσσα για τη διαδικασία της εγκατάστασης.",
"config-wiki-language": "Γλώσσα του wiki:",
"config-type-oracle": "Oracle",
"config-type-mssql": "Microsoft SQL Server",
"config-support-info": "MediaWiki подржава следеће системе база података:\n\n$1\n\nАко не видите систем који покушавате да користите на листи испод, онда пратите повезана упутства изнад како бисте омогућили подршку.",
- "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] је примарна мета за MediaWiki и најбоље је подржана. MediaWiki такође ради са [{{int:version-db-mysql-url}} MySQL-ом] и [{{int:version-db-percona-url}} Percona Server-ом], који су компатибилни са MariaDB-ом. ([https://www.php.net/manual/en/mysqli.installation.php Како компајлирати PHP са подршком MySQL-а])",
+ "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] је примарна мета за Медијавики и најбоље је подржана. Медијавики ради и са [{{int:version-db-mysql-url}} MySQL-ом] и [{{int:version-db-percona-url}} Percona Server-ом], који су компатибилни са MariaDB-ом. ([https://www.php.net/manual/en/mysqli.installation.php Како компајлирати PHP са подршком MySQL-а])",
"config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] је популаран систем база података отвореног кода кaо алтернатива MySQL-у. ([https://www.php.net/manual/en/pgsql.installation.php Како компајлирати PHP са подршком PostgreSQL-а])",
"config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] је лаган систем базе података који је веома добро подржан. ([https://www.php.net/manual/en/pdo.installation.php Како компајлирати PHP са подршком SQLite-а], користи PDO)",
"config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] је база података комерцијалних предузећа. ([https://www.php.net/manual/en/oci8.installation.php Како компајлирати PHP са подршком OCI8-а])",
return [ [ 'alreadyrolled',
htmlspecialchars( $this->mTitle->getPrefixedText() ),
htmlspecialchars( $fromP ),
- htmlspecialchars( $targetEditorForPublic ? $targetEditorForPublic->getName() : '' )
+ htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
] ];
}
* @param string|string[] ...$args
* @return $this
*/
- public function params( ...$args ) {
+ public function params( ...$args ): Command {
if ( count( $args ) === 1 && is_array( reset( $args ) ) ) {
// If only one argument has been passed, and that argument is an array,
// treat it as a list of arguments
* @param string|string[] ...$args
* @return $this
*/
- public function unsafeParams( ...$args ) {
+ public function unsafeParams( ...$args ): Command {
if ( count( $args ) === 1 && is_array( reset( $args ) ) ) {
// If only one argument has been passed, and that argument is an array,
// treat it as a list of arguments
* filesize (for ulimit -f), memory, time, walltime.
* @return $this
*/
- public function limits( array $limits ) {
+ public function limits( array $limits ): Command {
if ( !isset( $limits['walltime'] ) && isset( $limits['time'] ) ) {
// Emulate the behavior of old wfShellExec() where walltime fell back on time
// if the latter was overridden and the former wasn't
* @param string[] $env array of variable name => value
* @return $this
*/
- public function environment( array $env ) {
+ public function environment( array $env ): Command {
$this->env = $env;
return $this;
* @param string $method
* @return $this
*/
- public function profileMethod( $method ) {
+ public function profileMethod( $method ): Command {
$this->method = $method;
return $this;
* @param string|null $inputString
* @return $this
*/
- public function input( $inputString ) {
+ public function input( $inputString ): Command {
$this->inputString = is_null( $inputString ) ? null : (string)$inputString;
return $this;
* @param bool $yesno
* @return $this
*/
- public function includeStderr( $yesno = true ) {
+ public function includeStderr( $yesno = true ): Command {
$this->doIncludeStderr = $yesno;
return $this;
* @param bool $yesno
* @return $this
*/
- public function logStderr( $yesno = true ) {
+ public function logStderr( $yesno = true ): Command {
$this->doLogStderr = $yesno;
return $this;
* @param string|false $cgroup Absolute file path to the cgroup, or false to not use a cgroup
* @return $this
*/
- public function cgroup( $cgroup ) {
+ public function cgroup( $cgroup ): Command {
$this->cgroup = $cgroup;
return $this;
* @param int $restrictions
* @return $this
*/
- public function restrict( $restrictions ) {
+ public function restrict( $restrictions ): Command {
$this->restrictions |= $restrictions;
return $this;
*
* @return $this
*/
- public function whitelistPaths( array $paths ) {
+ public function whitelistPaths( array $paths ): Command {
// Default implementation is a no-op
return $this;
}
*
* @return Command
*/
- public function create() {
+ public function create(): Command {
if ( $this->restrictionMethod === 'firejail' ) {
$command = new FirejailCommand( $this->findFirejail() );
$command->restrict( Shell::RESTRICT_DEFAULT );
/**
* @inheritDoc
*/
- public function whitelistPaths( array $paths ) {
+ public function whitelistPaths( array $paths ): Command {
$this->whitelistedPaths = array_merge( $this->whitelistedPaths, $paths );
return $this;
}
* Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'"
* @return Command
*/
- public static function command( ...$commands ) {
+ public static function command( ...$commands ): Command {
if ( count( $commands ) === 1 && is_array( reset( $commands ) ) ) {
// If only one argument has been passed, and that argument is an array,
// treat it as a list of arguments
* 'wrapper': Path to a PHP wrapper to handle the maintenance script
* @return Command
*/
- public static function makeScriptCommand( $script, $parameters, $options = [] ) {
+ public static function makeScriptCommand( $script, $parameters, $options = [] ): Command {
global $wgPhpCli;
// Give site config file a chance to run the script in a wrapper.
// The caller may likely want to call wfBasename() on $script.
$username = $target->getName();
$userpage = $target->getUserPage();
$talkpage = $target->getTalkPage();
+ $isIP = IP::isValid( $username );
+ $isRange = IP::isValidRange( $username );
$linkRenderer = $sp->getLinkRenderer();
# No talk pages for IP ranges.
- if ( !IP::isValidRange( $username ) ) {
+ if ( !$isRange ) {
$tools['user-talk'] = $linkRenderer->makeLink(
$talkpage,
$sp->msg( 'sp-contributions-talk' )->text()
);
}
- if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) {
- if ( $sp->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
- if ( $target->getBlock() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
- $tools['block'] = $linkRenderer->makeKnownLink( # Change block link
- SpecialPage::getTitleFor( 'Block', $username ),
- $sp->msg( 'change-blocklink' )->text()
- );
- $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
- SpecialPage::getTitleFor( 'Unblock', $username ),
- $sp->msg( 'unblocklink' )->text()
- );
- } else { # User is not blocked
- $tools['block'] = $linkRenderer->makeKnownLink( # Block link
- SpecialPage::getTitleFor( 'Block', $username ),
- $sp->msg( 'blocklink' )->text()
- );
- }
+ if ( $sp->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
+ if ( $target->getBlock() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
+ $tools['block'] = $linkRenderer->makeKnownLink( # Change block link
+ SpecialPage::getTitleFor( 'Block', $username ),
+ $sp->msg( 'change-blocklink' )->text()
+ );
+ $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
+ SpecialPage::getTitleFor( 'Unblock', $username ),
+ $sp->msg( 'unblocklink' )->text()
+ );
+ } else { # User is not blocked
+ $tools['block'] = $linkRenderer->makeKnownLink( # Block link
+ SpecialPage::getTitleFor( 'Block', $username ),
+ $sp->msg( 'blocklink' )->text()
+ );
}
+ }
- # Block log link
- $tools['log-block'] = $linkRenderer->makeKnownLink(
- SpecialPage::getTitleFor( 'Log', 'block' ),
- $sp->msg( 'sp-contributions-blocklog' )->text(),
+ # Block log link
+ $tools['log-block'] = $linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( 'Log', 'block' ),
+ $sp->msg( 'sp-contributions-blocklog' )->text(),
+ [],
+ [ 'page' => $userpage->getPrefixedText() ]
+ );
+
+ # Suppression log link (T61120)
+ if ( $sp->getUser()->isAllowed( 'suppressionlog' ) ) {
+ $tools['log-suppression'] = $linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( 'Log', 'suppress' ),
+ $sp->msg( 'sp-contributions-suppresslog', $username )->text(),
[],
- [ 'page' => $userpage->getPrefixedText() ]
+ [ 'offender' => $username ]
);
-
- # Suppression log link (T61120)
- if ( $sp->getUser()->isAllowed( 'suppressionlog' ) ) {
- $tools['log-suppression'] = $linkRenderer->makeKnownLink(
- SpecialPage::getTitleFor( 'Log', 'suppress' ),
- $sp->msg( 'sp-contributions-suppresslog', $username )->text(),
- [],
- [ 'offender' => $username ]
- );
- }
}
# Don't show some links for IP ranges
- if ( !IP::isValidRange( $username ) ) {
- # Uploads
- $tools['uploads'] = $linkRenderer->makeKnownLink(
- SpecialPage::getTitleFor( 'Listfiles', $username ),
- $sp->msg( 'sp-contributions-uploads' )->text()
- );
+ if ( !$isRange ) {
+ # Uploads: hide if IPs cannot upload (T220674)
+ if ( !$isIP || $target->isAllowed( 'upload' ) ) {
+ $tools['uploads'] = $linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( 'Listfiles', $username ),
+ $sp->msg( 'sp-contributions-uploads' )->text()
+ );
+ }
# Other logs link
+ # Todo: T146628
$tools['logs'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'Log', $username ),
$sp->msg( 'sp-contributions-logs' )->text()
);
# Add link to deleted user contributions for priviledged users
+ # Todo: T183457
if ( $sp->getUser()->isAllowed( 'deletedhistory' ) ) {
$tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'DeletedContributions', $username ),
private $templateParser;
public function __construct( IContextSource $context, array $options ) {
+ // Set ->target and ->contribs before calling parent::__construct() so
+ // parent can call $this->getIndexField() and get the right result. Set
+ // the rest too just to keep things simple.
+ $this->target = $options['target'] ?? '';
+ $this->contribs = $options['contribs'] ?? 'users';
+ $this->namespace = $options['namespace'] ?? '';
+ $this->tagFilter = $options['tagfilter'] ?? false;
+ $this->nsInvert = $options['nsInvert'] ?? false;
+ $this->associated = $options['associated'] ?? false;
+
+ $this->deletedOnly = !empty( $options['deletedOnly'] );
+ $this->topOnly = !empty( $options['topOnly'] );
+ $this->newOnly = !empty( $options['newOnly'] );
+ $this->hideMinor = !empty( $options['hideMinor'] );
+
parent::__construct( $context );
$msgs = [
$this->messages[$msg] = $this->msg( $msg )->escaped();
}
- $this->target = $options['target'] ?? '';
- $this->contribs = $options['contribs'] ?? 'users';
- $this->namespace = $options['namespace'] ?? '';
- $this->tagFilter = $options['tagfilter'] ?? false;
- $this->nsInvert = $options['nsInvert'] ?? false;
- $this->associated = $options['associated'] ?? false;
-
- $this->deletedOnly = !empty( $options['deletedOnly'] );
- $this->topOnly = !empty( $options['topOnly'] );
- $this->newOnly = !empty( $options['newOnly'] );
- $this->hideMinor = !empty( $options['hideMinor'] );
-
// Date filtering: use timestamp if available
$startTimestamp = '';
$endTimestamp = '';
return new FakeResultWrapper( $result );
}
+ /**
+ * Return the table targeted for ordering and continuation
+ *
+ * See T200259 and T221380.
+ *
+ * @warning Keep this in sync with self::getQueryInfo()!
+ *
+ * @return string
+ */
+ private function getTargetTable() {
+ if ( $this->contribs == 'newbie' ) {
+ return 'revision';
+ }
+
+ $user = User::newFromName( $this->target, false );
+ $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null;
+ if ( $ipRangeConds ) {
+ return 'ip_changes';
+ } else {
+ $conds = ActorMigration::newMigration()->getWhere( $this->mDb, 'rev_user', $user );
+ if ( isset( $conds['orconds']['actor'] ) ) {
+ // @todo: This will need changing when revision_actor_temp goes away
+ return 'revision_actor_temp';
+ }
+ }
+
+ return 'revision';
+ }
+
function getQueryInfo() {
$revQuery = Revision::getQueryInfo( [ 'page', 'user' ] );
$queryInfo = [
'join_conds' => $revQuery['joins'],
];
+ // WARNING: Keep this in sync with getTargetTable()!
+
if ( $this->contribs == 'newbie' ) {
$max = $this->mDb->selectField( 'user', 'max(user_id)', '', __METHOD__ );
$queryInfo['conds'][] = $revQuery['fields']['rev_user'] . ' >' . (int)( $max - $max / 100 );
$ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null;
if ( $ipRangeConds ) {
$queryInfo['tables'][] = 'ip_changes';
- /**
- * These aliases make `ORDER BY rev_timestamp, rev_id` from {@see getIndexField} and
- * {@see getExtraSortFields} use the replicated `ipc_rev_timestamp` and `ipc_rev_id`
- * columns from the `ip_changes` table, for more efficient queries.
- * @see https://phabricator.wikimedia.org/T200259#4832318
- */
- $queryInfo['fields'] = array_merge(
- [
- 'rev_timestamp' => 'ipc_rev_timestamp',
- 'rev_id' => 'ipc_rev_id',
- ],
- array_diff( $queryInfo['fields'], [
- 'rev_timestamp',
- 'rev_id',
- ] )
- );
$queryInfo['join_conds']['ip_changes'] = [
'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
];
$queryInfo['conds'][] = $conds['conds'];
// Force the appropriate index to avoid bad query plans (T189026)
if ( isset( $conds['orconds']['actor'] ) ) {
- // @todo: This will need changing when revision_comment_temp goes away
+ // @todo: This will need changing when revision_actor_temp goes away
$queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
- // Alias 'rev_timestamp' => 'revactor_timestamp' and 'rev_id' => 'revactor_rev' so
- // "ORDER BY rev_timestamp, rev_id" is interpreted to use denormalized revision_actor_temp
- // fields instead.
- $queryInfo['fields'] = array_merge(
- array_diff( $queryInfo['fields'], [ 'rev_timestamp', 'rev_id' ] ),
- [ 'rev_timestamp' => 'revactor_timestamp', 'rev_id' => 'revactor_rev' ]
- );
} else {
$queryInfo['options']['USE INDEX']['revision'] =
isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp';
' != ' . Revision::SUPPRESSED_USER;
}
- // For IPv6, we use ipc_rev_timestamp on ip_changes as the index field,
- // which will be referenced when parsing the results of a query.
- if ( self::isQueryableRange( $this->target ) ) {
- $queryInfo['fields'][] = 'ipc_rev_timestamp';
+ // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it.
+ $indexField = $this->getIndexField();
+ if ( $indexField !== 'rev_timestamp' ) {
+ $queryInfo['fields'][] = $indexField;
}
ChangeTags::modifyDisplayQuery(
* @return string
*/
public function getIndexField() {
- // Note this is run via parent::__construct() *before* $this->target is set!
- return 'rev_timestamp';
+ // The returned column is used for sorting and continuation, so we need to
+ // make sure to use the right denormalized column depending on which table is
+ // being targeted by the query to avoid bad query plans.
+ // See T200259, T204669, T220991, and T221380.
+ $target = $this->getTargetTable();
+ switch ( $target ) {
+ case 'revision':
+ return 'rev_timestamp';
+ case 'ip_changes':
+ return 'ipc_rev_timestamp';
+ case 'revision_actor_temp':
+ return 'revactor_timestamp';
+ default:
+ wfWarn(
+ __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
+ );
+ return 'rev_timestamp';
+ }
}
/**
* @return string[]
*/
protected function getExtraSortFields() {
- // Note this is run via parent::__construct() *before* $this->target is set!
- return [ 'rev_id' ];
+ // The returned columns are used for sorting, so we need to make sure
+ // to use the right denormalized column depending on which table is
+ // being targeted by the query to avoid bad query plans.
+ // See T200259, T204669, T220991, and T221380.
+ $target = $this->getTargetTable();
+ switch ( $target ) {
+ case 'revision':
+ return [ 'rev_id' ];
+ case 'ip_changes':
+ return [ 'ipc_rev_id' ];
+ case 'revision_actor_temp':
+ return [ 'revactor_rev' ];
+ default:
+ wfWarn(
+ __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
+ );
+ return [ 'rev_id' ];
+ }
}
protected function doBatchLookups() {
/**
* Get blocking information
+ *
+ * TODO: Move this into the BlockManager, along with block-related properties.
+ *
* @param bool $fromReplica Whether to check the replica DB first.
* To improve performance, non-critical checks are done against replica DBs.
* Check when actually saving should be done against master.
*/
private function getBlockedStatus( $fromReplica = true ) {
- global $wgProxyWhitelist, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
-
if ( $this->mBlockedby != -1 ) {
return;
}
// overwriting mBlockedby, surely?
$this->load();
- # We only need to worry about passing the IP address to the Block generator if the
- # user is not immune to autoblocks/hardblocks, and they are the current user so we
- # know which IP address they're actually coming from
- $ip = null;
- $sessionUser = RequestContext::getMain()->getUser();
- // the session user is set up towards the end of Setup.php. Until then,
- // assume it's a logged-out user.
- $globalUserName = $sessionUser->isSafeToLoad()
- ? $sessionUser->getName()
- : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
- if ( $this->getName() === $globalUserName && !$this->isAllowed( 'ipblock-exempt' ) ) {
- $ip = $this->getRequest()->getIP();
- }
-
- // User/IP blocking
- $block = Block::newFromTarget( $this, $ip, !$fromReplica );
-
- // Cookie blocking
- if ( !$block instanceof Block ) {
- $block = $this->getBlockFromCookieValue( $this->getRequest()->getCookie( 'BlockID' ) );
- }
-
- // Proxy blocking
- if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $wgProxyWhitelist ) ) {
- // Local list
- if ( self::isLocallyBlockedProxy( $ip ) ) {
- $block = new Block( [
- 'byText' => wfMessage( 'proxyblocker' )->text(),
- 'reason' => wfMessage( 'proxyblockreason' )->plain(),
- 'address' => $ip,
- 'systemBlock' => 'proxy',
- ] );
- } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
- $block = new Block( [
- 'byText' => wfMessage( 'sorbs' )->text(),
- 'reason' => wfMessage( 'sorbsreason' )->plain(),
- 'address' => $ip,
- 'systemBlock' => 'dnsbl',
- ] );
- }
- }
-
- // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
- if ( !$block instanceof Block
- && $wgApplyIpBlocksToXff
- && $ip !== null
- && !in_array( $ip, $wgProxyWhitelist )
- ) {
- $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
- $xff = array_map( 'trim', explode( ',', $xff ) );
- $xff = array_diff( $xff, [ $ip ] );
- $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$fromReplica );
- $block = Block::chooseBlock( $xffblocks, $xff );
- if ( $block instanceof Block ) {
- # Mangle the reason to alert the user that the block
- # originated from matching the X-Forwarded-For header.
- $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
- }
- }
-
- if ( !$block instanceof Block
- && $ip !== null
- && $this->isAnon()
- && IP::isInRanges( $ip, $wgSoftBlockRanges )
- ) {
- $block = new Block( [
- 'address' => $ip,
- 'byText' => 'MediaWiki default',
- 'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
- 'anonOnly' => true,
- 'systemBlock' => 'wgSoftBlockRanges',
- ] );
- }
+ $block = MediaWikiServices::getInstance()->getBlockManager()->getUserBlock(
+ $this,
+ $fromReplica
+ );
if ( $block instanceof Block ) {
wfDebug( __METHOD__ . ": Found block.\n" );
Hooks::run( 'GetBlockedStatus', [ &$thisUser ] );
}
- /**
- * Try to load a Block from an ID given in a cookie value.
- * @param string|null $blockCookieVal The cookie value to check.
- * @return Block|bool The Block object, or false if none could be loaded.
- */
- protected function getBlockFromCookieValue( $blockCookieVal ) {
- // Make sure there's something to check. The cookie value must start with a number.
- if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
- return false;
- }
- // Load the Block from the ID in the cookie.
- $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
- if ( $blockCookieId !== null ) {
- // An ID was found in the cookie.
- $tmpBlock = Block::newFromID( $blockCookieId );
- if ( $tmpBlock instanceof Block ) {
- $config = RequestContext::getMain()->getConfig();
-
- switch ( $tmpBlock->getType() ) {
- case Block::TYPE_USER:
- $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
- $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true );
- break;
- case Block::TYPE_IP:
- case Block::TYPE_RANGE:
- // If block is type IP or IP range, load only if user is not logged in (T152462)
- $blockIsValid = !$tmpBlock->isExpired() && !$this->isLoggedIn();
- $useBlockCookie = ( $config->get( 'CookieSetOnIpBlock' ) === true );
- break;
- default:
- $blockIsValid = false;
- $useBlockCookie = false;
- }
-
- if ( $blockIsValid && $useBlockCookie ) {
- // Use the block.
- return $tmpBlock;
- }
-
- // If the block is not valid, remove the cookie.
- Block::clearCookie( $this->getRequest()->response() );
- } else {
- // If the block doesn't exist, remove the cookie.
- Block::clearCookie( $this->getRequest()->response() );
- }
- }
- return false;
- }
-
/**
* Whether the given IP is in a DNS blacklist.
*
+ * @deprecated since 1.34 Use BlockManager::isDnsBlacklisted.
* @param string $ip IP to check
* @param bool $checkWhitelist Whether to check the whitelist first
* @return bool True if blacklisted.
*/
public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
- global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
-
- if ( !$wgEnableDnsBlacklist ||
- ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
- ) {
- return false;
- }
-
- return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
+ return MediaWikiServices::getInstance()->getBlockManager()
+ ->isDnsBlacklisted( $ip, $checkWhitelist );
}
/**
* Whether the given IP is in a given DNS blacklist.
*
+ * @deprecated since 1.34 Check via BlockManager::isDnsBlacklisted instead.
* @param string $ip IP to check
* @param string|array $bases Array of Strings: URL of the DNS blacklist
* @return bool True if blacklisted.
*/
public function inDnsBlacklist( $ip, $bases ) {
+ wfDeprecated( __METHOD__, '1.34' );
+
$found = false;
// @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
if ( IP::isIPv4( $ip ) ) {
/**
* Check if an IP address is in the local proxy list
*
+ * @deprecated since 1.34 Use BlockManager::getUserBlock instead.
* @param string $ip
- *
* @return bool
*/
public static function isLocallyBlockedProxy( $ip ) {
+ wfDeprecated( __METHOD__, '1.34' );
+
global $wgProxyList;
if ( !$wgProxyList ) {
$newTouched = $this->newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
- $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $newTouched ) {
+ $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $newTouched ) {
global $wgActorTableSchemaMigrationStage;
$dbw->update( 'user',
$fields["user_$name"] = $value;
}
- return $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $fields ) {
+ return $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $fields ) {
$dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
if ( $dbw->affectedRows() ) {
$newUser = self::newFromId( $dbw->insertId() );
$this->mTouched = $this->newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
- $status = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
+ $status = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
$noPass = PasswordFactory::newInvalidPassword()->toString();
$dbw->insert( 'user',
[
"action-editmyusercss": "рэдагаваньне вашых уласных CSS-файлаў",
"action-editmyuserjson": "рэдагаваньне вашых уласных JSON-файлаў",
"action-editmyuserjs": "рэдагаваньне вашых уласных JavaScript-файлаў",
+ "action-viewsuppressed": "прагляд вэрсіяў, схаваных ад усіх удзельнікаў",
+ "action-hideuser": "блякаваньне імя ўдзельніка і яго хаваньне",
"nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}",
"enhancedrc-since-last-visit": "$1 {{PLURAL:$1|з апошняга візыту}}",
"enhancedrc-history": "гісторыя",
"edit-gone-missing": "পাতাটি হালনাগাদ হয়নি।\nসম্ভবতঃ পাতাটি মুছে ফেলা হয়েছে।",
"edit-conflict": "সম্পাদনা সংঘাত।",
"edit-no-change": "আপনার সম্পাদনাটি উপেক্ষা করা হয়েছে, কারণ লেখাতে কোনো পরিবর্তন করা হয়নি।",
+ "edit-slots-cannot-add": "নিচের {{PLURAL:$1|পাতাটি|পাতাসমূহ}} এখানে সমর্থিত নয়: $2।",
+ "edit-slots-cannot-remove": "নিচের {{PLURAL:$1|স্লট|স্লটসমূহ}} প্রয়োজন এবং বাদ দেওয়া যাবে না: $2।",
+ "edit-slots-missing": "নিচের {{PLURAL:$1|স্লট|স্লটসমূহ}} পাওয়া যায়নি: $2।",
"postedit-confirmation-created": "পাতাটি তৈরি করা হয়েছে।",
"postedit-confirmation-restored": "পাতাটি পুনরুদ্ধার করা হয়েছে।",
"postedit-confirmation-saved": "আপনার সম্পাদনা সংরক্ষিত হয়েছে।",
"converter-manual-rule-error": "ম্যানুয়াল ভাষা রূপান্তর নিয়মে ত্রুটি পাওয়া গিয়েছে",
"undo-success": "সম্পাদনাটি বাতিল করা যাবে। অনুগ্রহ করে নিচের তুলনাটি পরীক্ষা করে দেখুন ও নিশ্চিত করুন যে এটাই আপনি করতে চান, এবং তারপর নিচের সম্পাদনাগুলি সংরক্ষণ করে সম্পাদনাটির বাতিল প্রক্রিয়া সমাপ্ত করুন।",
"undo-failure": "এ সম্পাদনা মধ্যবর্তী সম্পাদনাসমূহের কারণে পূর্বাবস্থায় ফিরিয়ে নেওয়া যাবে না।",
+ "undo-main-slot-only": "এই সম্পাদনাটি পূর্বাবস্থায় নেওয়া যাবে না কারণ এখানকার বিষয়বস্তু প্রধান স্লটের বাইরে।",
"undo-norev": "সম্পাদনাটি বাতিল করা যাচ্ছেনা কারণ এটি আর নেই বা মুছে ফেলা হয়েছে।",
"undo-nochange": "সম্পাদনাটি পূর্বেই বাতিল করা হয়েছে।",
"undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|আলাপ]])-এর সম্পাদিত $1 নম্বর সংশোধনটি বাতিল করা হয়েছে",
"action-changetags": "নির্দিষ্ট সংস্করণ এবং লগ ভুক্তিগুলিতে যথেচ্ছভাবে ট্যাগ সংযোজন ও অপসারণ করা",
"action-deletechangetags": "ডাটাবেজ থেকে ট্যাগ অপসরণ করার",
"action-purge": "এই পাতাটি শোধন করুন",
+ "action-apihighlimits": "API কোয়েরি হিসাবে আরও উচ্চ লিমিট ব্যবহার করুন",
+ "action-autoconfirmed": "আইপি-ভিত্তিক রেট সীমানা দ্বারা প্রভাবিত নয়।",
+ "action-bigdelete": "বিশাল ইতিহাস সম্বলিত পাতা মুছে ফেলো",
+ "action-blockemail": "ই-মেইল পাঠাতে কোনো ব্যবহারকারীকে বাঁধা দাও",
+ "action-bot": "সয়ংক্রিয় পদ্ধতি হিসাবে চিহ্নিত করণ",
"action-editprotected": "\"{{int:protect-level-sysop}}\" হিসেবে সুরক্ষিত পাতা সম্পাদনা করার",
"action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" হিসেবে সুরক্ষিত পাতা সম্পাদনা করার",
+ "action-editinterface": "ব্যবহারকারী ইন্টারফেস সম্পাদনা",
+ "action-editusercss": "অন্য ব্যবহারকারীগণের CSS ফাইল সম্পাদনা",
+ "action-edituserjson": "অন্য ব্যবহারকারীগণের JSON ফাইল সম্পাদনা",
+ "action-edituserjs": "অন্য ব্যবহারকারীগণের জাভাস্ক্রিপ্ট ফাইল সম্পাদনা",
"action-editsitecss": "সাইটব্যাপী CSS সম্পাদনা করার",
"action-editsitejson": "সাইটব্যাপী JSON সম্পাদনা করার",
"action-editsitejs": "সাইটব্যাপী জাভাস্ক্রিপ্ট সম্পাদনা করার",
"action-editmyusercss": "স্ব ব্যবহারকারীর CSS ফাইল সম্পাদনা করার",
+ "action-editmyuserjson": "আপনার নিজস্ব ব্যবহারকারী JSON ফাইল সম্পাদনা করা",
+ "action-editmyuserjs": "আপনার নিজস্ব ব্যবহারকারী জাভাস্ক্রিপ্ট ফাইল সম্পাদনা করুন",
+ "action-viewsuppressed": "যেকোন ব্যবহারকারীর কাছ থেকে লুকানো সংস্করণগুলি দেখুন",
+ "action-hideuser": "ব্যবহারকারীকে বাধা দিন, এবং সর্বসাধারণের দৃষ্টিসীমা থেকে সরিয়ে নিন",
+ "action-ipblock-exempt": "আইপি বাধা, স্বয়ংক্রিয় বাধা ও পরিসীমার বাধা এড়ানো",
"action-unblockself": "নিজেকে বাধামুক্ত করার",
+ "action-noratelimit": "রেট লিমিটের ভিত্তিতে পরিবর্তন হবে না",
+ "action-reupload-own": "নিজের দ্বারা আপলোডকৃত ফাইল যা ইতিমধ্যেই বিদ্যমান, সেটি মুছে পুনরায় নতুন করে আপলোড করা",
+ "action-nominornewtalk": "বার্তা লেখার মত আলাপ পাতায় কোনো অনুল্লেখ্য সম্পাদনা নেই",
+ "action-markbotedits": "ফেরত আনা সম্পাদনাসমূহকে বট সম্পাদনা হিসেবে চিহ্নিত করে",
+ "action-patrolmarks": "সাম্প্রতিক পরিবর্তনের পরীক্ষিত চিহ্ন দেখাও",
+ "action-override-export-depth": "লিংকসহ পাতা যার গভীরতা ৫ এর মধ্যে সেগুলো রপ্তানি করুন",
+ "action-suppressredirect": "পাতা স্থানান্তরের সময় মূল পাতা থেকে পুনর্নির্দেশ তৈরী করছে না",
"nchanges": "$1টি {{PLURAL:$1|পরিবর্তন}}",
"enhancedrc-since-last-visit": "{{PLURAL:$1|সর্বশেষ প্রদর্শনের পর}} $1টি",
"enhancedrc-history": "ইতিহাস",
"rcfilters-filter-watchlist-notwatched-description": "আপনার নজরতালিকায় থাকা পাতাগুলি ব্যতীয় সবকিছু।",
"rcfilters-filtergroup-watchlistactivity": "নজরতালিকার কার্যক্রম",
"rcfilters-filter-watchlistactivity-unseen-label": "অদেখা পরিবর্তন",
+ "rcfilters-filter-watchlistactivity-unseen-description": "আপনার নজরতালিকায় থাকা পাতাগুলিতে পরিবর্তন যেগুলিতে আপনি সম্পাদনা করার পর আর যাননি।",
"rcfilters-filter-watchlistactivity-seen-label": "দেখা পরিবর্তন",
+ "rcfilters-filter-watchlistactivity-seen-description": "আপনার নজরতালিকায় থাকা পাতাগুলিতে পরিবর্তন যেগুলিতে আপনি সম্পাদনা করার পর আর যাননি।",
"rcfilters-filtergroup-changetype": "পরিবর্তনের ধরন",
"rcfilters-filter-pageedits-label": "পাতার সম্পাদনা",
"rcfilters-filter-pageedits-description": "উইকি বিষয়বস্তু, আলোচনা, বিষয়শ্রেণীর বিবরণ... ইত্যাদিতে সম্পাদনা",
"rcfilters-preference-help": "ছাঁকনিগুলি অনুসন্ধান বা আলোকপাতকরণ কার্যকারিতা ছাড়া সাম্প্রতিক পরিবর্তন লোড করে",
"rcfilters-watchlist-preference-label": "জাভাস্ক্রিপ্টহীন ইন্টারফেস ব্যবহার করুন",
"rcfilters-watchlist-preference-help": "ছাঁকনি অনুসন্ধান বা আলোকপাতকরণ বৈশিষ্ট্য ছাড়া নজরতালিকা লোড করে।",
+ "rcfilters-filter-showlinkedfrom-label": "লিংক করা এমন পাতাগুলোর পরিবর্তন দেখান",
+ "rcfilters-filter-showlinkedfrom-option-label": "নির্বাচিত পাতা থেকে <strong>পাতা লিংক করা</strong>",
+ "rcfilters-filter-showlinkedto-label": "পাতা লিংক করা এমন পাতাসমূহের পরিবর্তন দেখান",
+ "rcfilters-filter-showlinkedto-option-label": "নির্বাচিত পাতা থেকে <strong>পাতা লিংক করা</strong>",
"rcfilters-target-page-placeholder": "একটি পাতার নাম (বা বিষয়শ্রেণী) লিখুন",
"rcnotefrom": "<strong>$2</strong>টা থেকে সংঘটিত পরিবর্তনগুলি (সর্বোচ্চ <strong>$1টি</strong> দেখানো হয়েছে)।",
"rclistfromreset": "তারিখ নির্বাচন পুনঃস্থাপন করুন",
"uploaded-script-svg": "আপলোডকৃত SVG ফাইলে স্ক্রিপ্টযোগ্য উপাদান \"$1\" পাওয়া গেছে।",
"uploaded-hostile-svg": "আপলোড করা SVG ফাইলের শৈলী উপাদানে অনিরাপদ সিএসএস পাওয়া গেছে।",
"uploaded-event-handler-on-svg": "এসভিজি ফাইলের জন্য <code>$1=\"$2\"</code> ইভেন্ট-হ্যান্ডলার বৈশিষ্ট্যটি নির্ধারণ করা অনুমোদিত নয়।",
- "uploaded-href-attribute-svg": "এসভিজি ফাইলের href বৈশিষ্ট্যগুলির জন্য কেবলমাত্র http:// বা https:// লক্ষ্যগুলি অনুমোদিত; কিন্তু <code><$1 $2=\"$3\"></code> পাওয়া গেছে।",
+ "uploaded-href-attribute-svg": "এসভিজি ফাইলের href বৈশিষ্ট্যগুলির জন্য কেবলমাত্র http:// বা https:// লক্ষ্যগুলি অনুমোদিত। অন্য বিষয় যেমন, <image>, শুধুমাত্র উপাত্ত ও বৈশিষ্ঠগুলো গ্রহণযোগ্য। <code><$1 $2=\"$3\"></code> পাওয়া গেছে।",
"uploaded-href-unsafe-target-svg": "অনিরাপদ উপাত্তে href পাওয়া গেছে: আপলোডকৃত SVG ফাইলে URI লক্ষ্য ছিল <code><$1 $2=\"$3\"></code>।",
"uploaded-animate-svg": "\"animate\" ট্যাগটি পাওয়া গেছে যা আপলোডকৃত এসভিজি ফাইলের <code><$1 $2=\"$3\"></code> - এই \"from\" অ্যাট্রিবিউটটি ব্যবহার করে href পরিবর্তন করতে পারে।",
"uploaded-setting-event-handler-svg": "ইভেন্ট-হ্যান্ডলার অ্যাট্রিবিউট নির্ধারণ করতে বাধা দেওয়া হয়েছে। আপলোডকৃত এসভিজি ফাইলে <code><$1 $2=\"$3\"></code> খুঁজে পাওয়া গেছে।",
"ipb_expiry_old": "মেয়াদোত্তীর্ণের সময় অতীত হয়েছে।",
"ipb_expiry_temp": "লুকানো ব্যবহারকারীনাম বাধা চিরস্থায়ী হতে হবে।",
"ipb_hide_invalid": "এই অ্যাকাউন্ট বাধা দেয়া সম্ভব নয়; এটি {{PLURAL:$1|একের অধিক|$1টি}} সম্পাদনা করেছে।",
+ "ipb_hide_partial": "লুকায়িত ব্যবহারকারী নামের বাধাদান অবশ্যই সাইটওয়াইড হতে হবে।",
"ipb_already_blocked": "\"$1\" ইতিমধ্যে বাধাপ্রাপ্ত।",
"ipb-needreblock": "$1 ইতিমধ্যেই বাধাপ্রাপ্ত আছেন। আপনি কি সেটিংস পরিবর্তন করতে চান?",
"ipb-otherblocks-header": "অন্যান্য {{PLURAL:$1|বাধা|বাধাসমূহ}}",
"Fnielsen",
"Weblars",
"Kranix",
- "Psl85"
+ "Psl85",
+ "Dipsacus fullonum"
]
},
"tog-underline": "Understreg link:",
"tog-hideminor": "Skjul mindre ændringer i listen over seneste ændringer",
- "tog-hidepatrolled": "Skjul overvågede redigeringer i seneste ændringer",
- "tog-newpageshidepatrolled": "Skjul overvågede sider på listen over nye sider",
+ "tog-hidepatrolled": "Skjul patruljerede redigeringer i seneste ændringer",
+ "tog-newpageshidepatrolled": "Skjul patruljerede sider på listen over nye sider",
"tog-hidecategorization": "Skjul kategorisering af sider",
"tog-extendwatchlist": "Udvid overvågningslisten til at vise alle ændringer og ikke kun den nyeste",
"tog-usenewrc": "Gruppér ændringer efter side i listen over seneste ændringer og i overvågningslisten",
"autosumm-replace": "Erstatter sidens indhold med \"$1\"",
"autoredircomment": "Omdirigering til [[$1]] oprettet",
"autosumm-removed-redirect": "Fjernede omdirigering til [[$1]]",
+ "autosumm-changed-redirect-target": "Ændrede omdirigeringsmål fra [[$1]] til [[$2]]",
"autosumm-new": "Oprettede siden med \"$1\"",
"autosumm-newblank": "Oprettede tom side",
"lag-warn-normal": "Ændringer som er nyere end {{PLURAL:$1|et sekund|$1 sekunder}}, vises muligvis ikke i denne liste.",
"tooltip-watchlistedit-raw-submit": "Beobachtungsliste aktualisieren",
"tooltip-recreate": "Seite neu erstellen, obwohl sie gelöscht wurde",
"tooltip-upload": "Hochladen starten",
- "tooltip-rollback": "Macht alle letzten Änderungen der Seite, die vom gleichen Benutzer vorgenommen worden sind, durch nur einen Klick rückgängig.",
+ "tooltip-rollback": "Macht alle letzten Änderungen der Seite, die vom selben Benutzer vorgenommen worden sind, durch nur einen Klick rückgängig.",
"tooltip-undo": "Macht lediglich diese eine Änderung rückgängig und zeigt das Resultat in der Vorschau an, damit in der Zusammenfassungszeile eine Begründung angegeben werden kann.",
"tooltip-preferences-save": "Einstellungen speichern",
"tooltip-summary": "Gib eine kurze Zusammenfassung ein.",
"viewpagelogs": "Qeydanê na pele bımocne",
"nohistory": "Verorê vurnayışanê na perer çıni yo.",
"currentrev": "Çımraviyarnayışo rocane",
- "currentrev-asof": "$1 ra tepeya çım ra viyarnayışê cı'yo peyên",
+ "currentrev-asof": "Çımraviyarnayışê $1iyo peyên",
"revisionasof": "Çımraviyarnayışê $1",
"revision-info": "Vurnayışo ke $1 de terefê {{GENDER:$6|$2}}$7 ra biyo",
"previousrevision": "← Çımraviyarnayışo kıhanêr",
"accmailtext": "Ένας τυχαία παρηγμένος κωδικός για {{GENDER:$1|τον|την}} [[User talk:$1|$1]] έχει σταλεί στο $2.\n\nΜπορεί να αλλαχθεί από την σελίδα ''[[Special:ChangePassword|αλλαγή κωδικού]]'' μετά τη σύνδεση.",
"newarticle": "(Νέο)",
"newarticletext": "Ακολουθήσατε ένα σύνδεσμο προς μια σελίδα που δεν υπάρχει ακόμα. \nΓια να δημιουργήσετε τη σελίδα, αρχίστε να πληκτρολογείτε στο παρακάτω πλαίσιο (δείτε τη [$1 σελίδα βοήθειας] για περισσότερες πληροφορίες).\nΑν έχετε βρεθεί εδώ κατά λάθος, πατήστε το κουμπί '''πίσω''' στον περιηγητή σας.",
- "anontalkpagetext": "----''Αυτή η σελίδα συζήτησης προορίζεται για ανώνυμο χρήστη που δεν έχει δημιουργήσει ακόμα λογαριασμό ή που δεν τον χρησιμοποιεί. Έτσι για την ταυτοποίηση ενός ανώνυμου χρήστη χρησιμοποιείται η διεύθυνση IP του. Είναι όμως πιθανόν η διεύθυνση αυτή να είναι κοινή για πολλούς διαφορετικούς χρήστες. Αν είστε ανώνυμος χρήστης και νομίζετε ότι άσχετα σχόλια απευθύνθηκαν σε σας, παρακαλούμε να [[Special:CreateAccount|δημιουργήσετε ένα λογαριασμό]] ή να [[Special:UserLogin|συνδεθείτε]] για να αποφεύγεται η μελλοντική σύγχυση με άλλους ανώνυμους χρήστες.''",
+ "anontalkpagetext": "----''Αυτή η σελίδα συζήτησης προορίζεται για ανώνυμο χρήστη που δεν έχει δημιουργήσει ακόμα λογαριασμό ή που δεν τον χρησιμοποιεί. Έτσι για την ταυτοποίηση ενός ανώνυμου χρήστη χρησιμοποιείται η διεύθυνση IP τους. Είναι όμως πιθανόν η διεύθυνση αυτή να είναι κοινή για πολλούς διαφορετικούς χρήστες. Αν είστε ανώνυμος χρήστης και νομίζετε ότι άσχετα σχόλια απευθύνθηκαν σε σας, παρακαλούμε να [[Special:CreateAccount|δημιουργήσετε ένα λογαριασμό]] ή να [[Special:UserLogin|συνδεθείτε]] για να αποφεύγεται η μελλοντική σύγχυση με άλλους ανώνυμους χρήστες.''",
"noarticletext": "Δεν υπάρχει προς το παρόν κείμενο σε αυτή τη σελίδα. \nΜπορείτε να [[Special:Search/{{PAGENAME}}|αναζητήσετε αυτόν τον τίτλο σελίδας]] σε άλλες σελίδες,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} να αναζητήσετε τις σχετικές καταγραφές],\nή να [{{fullurl:{{FULLPAGENAME}}|action=edit}} δημιουργήσετε αυτή τη σελίδα]</span>.",
"noarticletext-nopermission": "Δεν υπάρχει προς το παρόν κείμενο σε αυτή τη σελίδα.\nΜπορείτε να [[Special:Search/{{PAGENAME}}|αναζητήσετε αυτόν τον τίτλο σελίδας]] σε άλλες σελίδες, ή <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} να ψάξετε στις σχετικές καταγραφές]</span>, αλλά δεν έχετε την άδεια να δημιουργήσετε αυτή τη σελίδα.",
"missing-revision": "Δεν υπάρχει αναθεώρηση με αριθμό $1 για τη σελίδα με όνομα «{{FULLPAGENAME}}».\n\nΑυτό συνήθως προκαλείται από παλιό σύνδεσμο ιστορικού προς σελίδα που έχει διαγραφεί.\nΛεπτομέρειες θα βρείτε στο [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ημερολόγιο καταγραφής διαγραφών].",
"page_first": "πρώτη",
"page_last": "τελευταία",
"histlegend": "Επιλογή διαφορών: Μαρκάρετε τα κουτάκια επιλογής των εκδόσεων που θέλετε να συγκρίνετε και πατήστε το enter ή το κουμπί στο κάτω μέρος.<br />\nΥπόμνημα: '''({{int:cur}})''' = διαφορά από την τελευταία έκδοση, '''({{int:last}})''' = διαφορά από την προηγούμενη έκδοση, '''{{int:minoreditletter}}''' = μικροαλλαγή.",
- "history-fieldset-title": "ΠεÏ\81ιήγηÏ\83η Ï\83Ï\84ο ιÏ\83Ï\84οÏ\81ικÏ\8c αλλαγών",
+ "history-fieldset-title": "ΦιλÏ\84Ï\81άÏ\81ιÏ\83μα αλλαγών",
"history-show-deleted": "Διαγεγραμμένες μόνο",
"histfirst": "η πιο παλιά",
"histlast": "η πιο πρόσφατη",
"revertpage": "Ανάκληση των αλλαγών [[Special:Contributions/$2|$2]] ([[User talk:$2|συζήτηση]]) επιστροφή στην προηγούμενη αναθεώρηση [[User:$1|$1]]",
"revertpage-nouser": "Αναστράφηκαν οι επεξεργασίες από τον (όνομα χρήστη αφαιρέθηκε) στη τελευταία έκδοση από τον/την {{GENDER:$1|[[User:$1|$1]]}}φ",
"rollback-success": "Αναστροφή επεξεργασιών από {{GENDER:$3|τον|την}} $1, επιστροφή στην προηγούμενη έκδοση από {{GENDER:$4|τον|την}} $2.",
- "sessionfailure-title": "Î\97 Ï\83Ï\85νεδÏ\81ία αÏ\80ÎÏ\84Ï\85Ï\87ε",
- "sessionfailure": "Î¥Ï\80άÏ\81Ï\87ει Ï\80Ï\81Ï\8cβλημα με Ï\84η Ï\83Ï\8dνδεÏ\83ή Ï\83αÏ\82 -η ενÎÏ\81γεια αÏ\85Ï\84ή ακÏ\85Ï\81Ï\8eθηκε Ï\80Ï\81οληÏ\80Ï\84ικά για Ï\84ην ανÏ\84ιμεÏ\84Ï\8eÏ\80ιÏ\83η Ï\84Ï\85Ï\87Ï\8cν Ï\80ειÏ\81αÏ\84είαÏ\82 Ï\83Ï\85νÏ\8cδοÏ\85 (session hijacking). ΠαÏ\81ακαλoÏ\8dμε Ï\80αÏ\84ήÏ\83Ï\84ε \"Î\95Ï\80ιÏ\83Ï\84Ï\81οÏ\86ή\", ξαναÏ\86οÏ\81Ï\84Ï\8eÏ\83Ï\84ε Ï\84η Ï\83ελίδα αÏ\80Ï\8c Ï\84ην οÏ\80οία Ï\86θάÏ\83αÏ\84ε εδÏ\8e και Ï\80Ï\81οÏ\83Ï\80αθήÏ\83Ï\84ε ξανά.",
+ "sessionfailure-title": "Î\91Ï\80οÏ\84Ï\85Ï\87ία Ï\80εÏ\81ιÏ\8cδοÏ\85 Ï\83Ï\8dνδεÏ\83ηÏ\82",
+ "sessionfailure": "ΦαίνεÏ\84αι Ï\8cÏ\84ι Ï\85Ï\80άÏ\81Ï\87ει κάÏ\80οιο Ï\80Ï\81Ï\8cβλημα με Ï\84ην Ï\80εÏ\81ίοδο Ï\83Ï\8dνδεÏ\83ήÏ\82 Ï\83αÏ\82.\nÎ\91Ï\85Ï\84ή η ενÎÏ\81γεια ακÏ\85Ï\81Ï\8eθηκε Ï\89Ï\82 Ï\80Ï\81οÏ\86Ï\8dλαξη για Ï\84ην ανÏ\84ιμεÏ\84Ï\8eÏ\80ιÏ\83η Ï\84Ï\85Ï\87Ï\8cν Ï\83Ï\86εÏ\84εÏ\81ιÏ\83μοÏ\8d Ï\84ηÏ\82 Ï\80εÏ\81ιÏ\8cδοÏ\85 Ï\83Ï\8dνδεÏ\83ηÏ\82 αÏ\80Ï\8c κάÏ\80οιον Ï\84Ï\81ίÏ\84ο (session hijacking).\nΠαÏ\81ακαλοÏ\8dμε Ï\85Ï\80οβάλεÏ\84ε ξανά Ï\84η Ï\86Ï\8cÏ\81μα.",
"changecontentmodel": "Αλλαγή μοντέλου περιεχομένου της σελίδας",
"changecontentmodel-legend": "Μοντέλο περιεχομένου σελίδας",
"changecontentmodel-title-label": "Τίτλος σελίδας",
"expand_templates_generate_xml": "Εμφάνιση δέντρου συντακτικής ανάλυσης XML",
"expand_templates_generate_rawhtml": "Εμφάνιση ανεπεξέργαστης HTML",
"expand_templates_preview": "Προεπισκόπηση",
- "expand_templates_preview_fail_html": "<em>Επειδή το {{SITENAME}} επιτρέπει την εισαγωγή ακατέργαστου HTML και υπήρξε μια απώλεια των δεδομένων συνόδου, η προεπισκόπηση είναι κρυμμένη ως ένα προληπτικό μέτρο κατά επιθέσεων JavaScript.</em>\n\n<strong>Αν αυτή είναι μια θεμιτή προσπάθεια προεπισκόπησης, παρακαλούμε δοκιμάστε ξανά.</strong>\nΑν εξακολουθεί να μην λειτουργεί, δοκιμάστε να [[Special:UserLogout|αποσυνδεθείτε]] και να συνδεθείτε ξανά και βεβαιωθείτε ότι το πρόγραμμα περιήγησής σας επιτρέπει cookies από αυτόν τον ιστότοπο.",
+ "expand_templates_preview_fail_html": "<em>Επειδή το {{SITENAME}} επιτρέπει την εισαγωγή ακατέργαστου HTML και υπήρξε μια απώλεια δεδομένων της περιόδου σύνδεσης, η προεπισκόπηση είναι κρυμμένη ως προληπτικό μέτρο κατά επιθέσεων JavaScript.</em>\n\n<strong>Αν αυτή είναι μια θεμιτή απόπειρα προεπισκόπησης, παρακαλούμε δοκιμάστε ξανά.</strong>\nΑν εξακολουθεί να μην λειτουργεί, δοκιμάστε να [[Special:UserLogout|αποσυνδεθείτε]] και να συνδεθείτε ξανά και βεβαιωθείτε ότι το πρόγραμμα περιήγησής σας επιτρέπει cookies από αυτόν τον ιστότοπο.",
"expand_templates_preview_fail_html_anon": "<em>Επειδή το {{SITENAME}} έχει ενεργοποιημένη raw HTML και δεν είστε συνδεδεμένοι, η προεπισκόπηση είναι κρυμμένη ως ένα προληπτικό μέτρο ενάντια σε επιθέσεις JavaScript.</em>\n\n<strong>Αν αυτό είναι δικαιολογημένη απόπειρα προεπισκόπησης, παρακαλούμε να [[Special:UserLogin|συνδεθείτε]] και δοκιμάστε πάλι.</strong>",
"pagelanguage": "Αλλαγή γλώσσας σελίδας",
"pagelang-name": "Σελίδα",
"mw-widgets-titlesmultiselect-placeholder": "Προσθήκη περισσότερων...",
"date-range-from": "Από ημερομηνία:",
"date-range-to": "Έως ημερομηνία:",
- "sessionprovider-generic": "$1 συνεδρίες",
- "sessionprovider-mediawiki-session-cookiesessionprovider": "Ï\83Ï\85νεδÏ\81ίεÏ\82 με βάÏ\83η Ï\84α cookies",
+ "sessionprovider-generic": "Περίοδοι σύνδεσης $1",
+ "sessionprovider-mediawiki-session-cookiesessionprovider": "Ï\80εÏ\81ίοδοι Ï\83Ï\8dνδεÏ\83ηÏ\82 βαÏ\83ιÏ\83μÎνεÏ\82 Ï\83ε cookies",
"sessionprovider-nocookies": "Τα Cookies μπορούν να απενεργοποιηθούν. Βεβαιωθείτε ότι έχετε ενεργοποιημένα τα cookies και ξεκινήστε πάλι.",
"randomrootpage": "Τυχαία κύρια σελίδα",
"log-action-filter-block": "Τύπος φραγής:",
"exif-compression-4": "CCITT Grupa 4 faks kodiranje",
"exif-copyrighted-true": "Zaštićeno autorskim pravom",
"exif-copyrighted-false": "Status autorskih prava nije postavljen",
+ "exif-photometricinterpretation-0": "Crno-bijelo (bijela je 0)",
"exif-photometricinterpretation-1": "Crno-bijelo (crna je 0)",
+ "exif-photometricinterpretation-3": "Paleta",
+ "exif-photometricinterpretation-4": "Maska prozirnosti",
+ "exif-photometricinterpretation-5": "Separirano (vjerojatno CMYK)",
+ "exif-photometricinterpretation-8": "CIE L*a*b*",
+ "exif-photometricinterpretation-9": "CIE L*a*b* (ICC kodiranje)",
+ "exif-photometricinterpretation-10": "CIE L*a*b* (ITU kodiranje)",
"exif-unknowndate": "nepoznat datum",
"exif-orientation-1": "Normalno",
"exif-orientation-2": "Zrcaljeno po horizontali",
"delete-confirm": "Poista ”$1”",
"delete-legend": "Sivun poisto",
"historywarning": "<strong>Varoitus:</strong> Sivulla, jota olet poistamassa, on muokkaushistoriaa ja sitä on muokattu $1 {{PLURAL:$1|kerran|kertaa}}:",
- "historyaction-submit": "Näytä muokkaushistoria",
+ "historyaction-submit": "Näytä versiot",
"confirmdeletetext": "Olet poistamassa sivun ja kaiken sen historian.\nVahvista, että olet aikeissa tehdä tämän ja että ymmärrät teon seuraukset ja teet poiston [[{{MediaWiki:Policy-url}}|käytäntöjen]] mukaisesti.",
"actioncomplete": "Toiminto suoritettu",
"actionfailed": "Toiminto epäonnistui",
"mycontris": "Bydragen",
"anoncontribs": "Bydragen",
"contribsub2": "Foar {{GENDER:$3|$1}} ($2)",
+ "contributions-subtitle": "Foar {{GENDER:$3|$1}}",
"nocontribs": "Der binne gjin feroarings fûn dy't oan dizze kritearia foldwaan.",
"uctop": "lêste feroaring",
"month": "Fan moanne (en earder):",
"nolicense": "Ništa nije odabrano",
"licenses-edit": "Uredi izbor licencija",
"license-nopreview": "(Prikaz nije moguć)",
- "upload_source_url": " (izabrali ste datoteku s valjanog, javno dostupnog URL-a)",
- "upload_source_file": "(izabrali ste datoteku s Vašeg računala)",
+ "upload_source_url": "(izabrana datoteka s valjanog, javno dostupnog URL-a)",
+ "upload_source_file": "(izabrana datoteka s Vašeg računala)",
"listfiles-delete": "izbriši",
"listfiles-summary": "Ova stranica pokazuje sve postavljene datoteke.\nKad je filtriran po suradniku, popis prikazuje samo one datoteke čije je posljednje inačice postavio taj suradnik.",
"listfiles_search_for": "Traži ime slike:",
"tog-norollbackdiff": "巻き戻し後の差分を表示しない",
"tog-useeditwarning": "変更を保存せずに編集画面から離れようとしたら警告",
"tog-prefershttps": "ログインする際、常に安全な接続を使用する",
+ "tog-showrollbackconfirmation": "巻き戻しリンクをクリックした際に確認画面を表示する",
"underline-always": "常に付ける",
"underline-never": "常に付けない",
"underline-default": "外装またはブラウザーの既定値を使用",
"badretype": "入力したパスワードが一致しません。",
"usernameinprogress": "この利用者名のためのアカウント作成は、すでに進行中です。お待ちください。",
"userexists": "入力した利用者名は既に使用されています。\n別の利用者名を指定してください。",
+ "createacct-normalization": "技術的制限により指定された利用者名は「$2」として登録されます。",
"loginerror": "ログインのエラー",
"createacct-error": "アカウント作成エラー",
"createaccounterror": "アカウントを作成できませんでした: $1",
"page_first": "先頭",
"page_last": "末尾",
"histlegend": "差分の選択: 比較したい版のラジオボタンを選択し、Enterキーを押すか、下部のボタンを押します。<br />\n凡例: <strong>({{int:cur}})</strong>=最新版との比較、<strong>({{int:last}})</strong>=直前の版との比較、<strong>{{int:minoreditletter}}</strong>=細部の編集",
- "history-fieldset-title": "ç\89\88ã\81®æ¤\9cç´¢",
+ "history-fieldset-title": "ç\89\88ã\82\92ã\83\95ã\82£ã\83«ã\82¿ã\83¼",
"history-show-deleted": "削除版のみ",
"histfirst": "最古",
"histlast": "最新",
"rcfilters-savedqueries-already-saved": "これらのフィルタは既に保存されています。設定を変更して、新しい保存フィルタを作成します。",
"rcfilters-restore-default-filters": "標準設定の絞り込み条件を適用",
"rcfilters-clear-all-filters": "すべてのフィルターをクリア",
- "rcfilters-show-new-changes": "最新の変更を表示",
+ "rcfilters-show-new-changes": "$1 から最新の変更を表示",
"rcfilters-search-placeholder": "絞り込みを行う(メニューから選択、またはフィルター名で検索)",
"rcfilters-invalid-filter": "無効なフィルター",
"rcfilters-empty-filter": "絞り込みは行われていません。全ての項目が表示されます。",
"ipb-confirm": "ブロックの確認",
"ipb-sitewide": "サイト全体",
"ipb-partial": "部分的",
+ "ipb-sitewide-help": "ウィキにおける各ページとその他の投稿操作。",
"ipb-partial-help": "特定のページまたは名前空間。",
"ipb-pages-label": "ページ",
"ipb-namespaces-label": "名前空間",
"blocklist-userblocks": "アカウントのブロックを非表示",
"blocklist-tempblocks": "期限付きブロックを非表示",
"blocklist-addressblocks": "単一 IP のブロックを非表示",
+ "blocklist-type": "種類:",
+ "blocklist-type-opt-all": "すべて",
+ "blocklist-type-opt-sitewide": "サイト全体",
+ "blocklist-type-opt-partial": "部分的",
"blocklist-rangeblocks": "範囲ブロックを非表示",
"blocklist-timestamp": "日時",
"blocklist-target": "対象",
"blocklist-editing-page": "ページ",
"blocklist-editing-ns": "名前空間",
"ipblocklist-empty": "ブロック一覧は空です。",
- "ipblocklist-no-results": "æ\8c\87å®\9aã\81\95ã\82\8cã\81\9fIPã\82¢ã\83\89ã\83¬ã\82¹ã\81¾ã\81\9fã\81¯å\88©ç\94¨è\80\85å\90\8dã\81¯ã\83\96ã\83ã\83\83ã\82¯ã\81\95ã\82\8cã\81¦ã\81\84ã\81¾ã\81\9bã\82\93。",
+ "ipblocklist-no-results": "æ\8c\87å®\9aã\81\95ã\82\8cã\81\9fIPã\82¢ã\83\89ã\83¬ã\82¹ã\81¾ã\81\9fã\81¯å\88©ç\94¨è\80\85å\90\8dã\81«ä¸\80è\87´ã\81\99ã\82\8bã\83\96ã\83ã\83\83ã\82¯ã\81¯è¦\8bã\81¤ã\81\8bã\82\8aã\81¾ã\81\9bã\82\93ã\81§ã\81\97ã\81\9f。",
"blocklink": "ブロック",
"unblocklink": "ブロック解除",
"change-blocklink": "設定を変更",
"confirm-unwatch-top": "このページをウォッチリストから除去しますか?",
"confirm-rollback-button": "OK",
"confirm-rollback-top": "このページの編集を差し戻しますか?",
+ "confirm-rollback-bottom": "この操作はこのページに対する指定した変更即座に巻き戻します。",
"confirm-mcrrestore-title": "版を復帰",
"confirm-mcrundo-title": "直前の変更を取り消す",
"mcrundofailed": "取り消しに失敗しました",
"logentry-block-block": "$1 が {{GENDER:$4|$3}} を$5{{GENDER:$2|ブロックしました}} $6",
"logentry-block-unblock": "$1 が {{GENDER:$4|$3}} の{{GENDER:$2|ブロックを解除しました}}",
"logentry-block-reblock": "$1 が {{GENDER:$4|$3}} のブロックの期限を$5に{{GENDER:$2|変更しました}} $6",
+ "logentry-partialblock-block-page": "{{PLURAL:$1|ページ}} $2",
+ "logentry-partialblock-block-ns": "{{PLURAL:$1|名前空間}} $2",
+ "logentry-partialblock-block": "$1 が {{GENDER:$4|$3}} に対して $7 からの編集を $5 {{GENDER:$2||ブロックしました}} $6",
+ "logentry-partialblock-reblock": "$1 が {{GENDER:$4|$3}} に対する $7 のブロックの期限を $5 に{{GENDER:$2|変更しました}} $6",
"logentry-suppress-block": "$1 が {{GENDER:$4|$3}} を$5で{{GENDER:$2|ブロックしました}} $6",
"logentry-suppress-reblock": "$1 が {{GENDER:$4|$3}} のブロックの期限を$5に{{GENDER:$2|変更しました}} $6",
"logentry-import-upload": "$1 がファイルをアップロードして $3 を{{GENDER:$2|インポートしました}}",
"passwordpolicies-policy-passwordcannotmatchblacklist": "パスワードは、特にブラックリストに載っているものと一致するものは設定できません",
"passwordpolicies-policy-maximalpasswordlength": "パスワードは$1{{PLURAL:$1|文字}}以下でなければなりません",
"passwordpolicies-policy-passwordcannotbepopular": "パスワードは{{PLURAL:$1|一般的なものにすることはできません|一般的な$1個のパスワードのリストと一致するものにすることはできません}}",
+ "passwordpolicies-policy-passwordnotinlargeblacklist": "一般的に使われるパスワード10万項目のリストに含まれるパスワードは使用できません。",
+ "passwordpolicies-policyflag-forcechange": "ログイン時に変更を強制",
+ "passwordpolicies-policyflag-suggestchangeonlogin": "ログイン時に変更を提案",
"easydeflate-invaliddeflate": "提供されたコンテンツが適切に圧縮されていません",
- "unprotected-js": "セキュリティ上の理由から、JavaScriptは保護されていないページからは読み込みできません。MediaWiki: 名前空間内、利用者下位ページのいずれかでのみjavascriptを作成してください。"
+ "unprotected-js": "セキュリティ上の理由から、JavaScriptは保護されていないページからは読み込みできません。MediaWiki: 名前空間内、利用者下位ページのいずれかでのみjavascriptを作成してください。",
+ "userlogout-continue": "ログアウトを行いたい場合、[$1 ログアウトページから実施]してください。",
+ "userlogout-sessionerror": "セッションエラーによりログアウトに失敗しました。再度 [$1 試行して]ください。"
}
"authprovider-confirmlink-request-label": "Сметки кои треба да се поврзат",
"authprovider-confirmlink-success-line": "$1: Успешно поврзано.",
"authprovider-confirmlink-failed": "Поврзувањето на сметката не е целосно успешно: $1",
- "authprovider-confirmlink-ok-help": "Ð\9fÑ\80одолжи да пÑ\80икажÑ\83ваÑ\88 пораки за неуспешно поврзување.",
+ "authprovider-confirmlink-ok-help": "Ð\9fÑ\80одолжи поÑ\81ле пÑ\80икажÑ\83ваÑ\9aеÑ\82о пораки за неуспешно поврзување.",
"authprovider-resetpass-skip-label": "Прескокни",
"authprovider-resetpass-skip-help": "Прескокни го задавањето на нова лозинка.",
"authform-nosession-login": "Заверката е успешна, но вашиот прелистувач не може да „запомни“ дека сте најавени.\n\n$1",
"recentchanges-summary": "Up disse syde kün jy de lätste wysigingen van disse wiki bekyken.",
"recentchanges-noresult": "Der waren in disse periode gien wiezigingen die an de kriteria voldoon.",
"recentchanges-feed-description": "Zeuk naor de alderleste wiezingen op disse wiki in disse voer.",
- "recentchanges-label-newpage": "Mid disse bewarking is een nye syde an-emaked",
+ "recentchanges-label-newpage": "Mid disse bewarking is een nye syde anemaked",
"recentchanges-label-minor": "Dit is een kleine wysiging",
"recentchanges-label-bot": "Disse bewarking is uutevoord döär een bot",
"recentchanges-label-unpatrolled": "Disse bewarking is noch neet nå-ekeaken",
"speciallogtitlelabel": "Mål (tittel eller {{ns:user}}:brukarnamn for brukar):",
"log": "Loggar",
"logeventslist-submit": "Vis",
+ "logeventslist-more-filters": "Vis fleire loggar:",
"all-logs-page": "Alle offentlege loggar",
"alllogstext": "Kombinert vising av alle loggane på {{SITENAME}}. Du kan avgrense resultatet ved å velje loggtype, brukarnamn eller den sida som er påverka (hugs å skilje mellom store og små bokstavar)",
"logempty": "Ingen element i loggen passar.",
"logentry-rights-autopromote": "$1 vart automatisk {{GENDER:$2|forfremja}} frå $4 til $5",
"logentry-upload-upload": "$1 {{GENDER:$2|lasta opp}} $3",
"logentry-upload-overwrite": "$1 {{GENDER:$2|lasta opp}} ein ny versjon av $3",
+ "log-name-managetags": "Merkehandsamingslogg",
"log-name-tag": "Merkelogg",
"rightsnone": "(ingen)",
"rightslogentry-temporary-group": "$1 (mellombels, fram til $2)",
"DeRudySoulStorm",
"Railfail536",
"Vlad5250",
- "CiaPan"
+ "CiaPan",
+ "BadDog"
]
},
"tog-underline": "Podkreślenie linków:",
"page_first": "ᱯᱟᱹᱦᱤᱞ",
"page_last": "ᱢᱩᱪᱟᱹᱫ",
"histlegend": "ᱮᱴᱟᱜ ᱵᱟᱪᱷᱟᱣ: ᱱᱟᱣᱟ ᱵᱚᱫᱚᱞᱠᱚ ᱛᱩᱞᱟᱹᱣ ᱢᱮᱱᱠᱷᱟᱱ, ᱨᱮᱰᱤᱭᱳ ᱵᱟᱠᱥᱚᱨᱮ ᱪᱤᱱ ᱮᱢ ᱠᱟᱛᱮ ᱵᱚᱞᱚᱜ ᱥᱮ ᱞᱟᱛᱟᱨ ᱨᱮᱱᱟᱜ ᱵᱟᱴᱚᱱ ᱞᱤᱱᱢᱮ᱾<br />\nᱩᱱᱩᱫᱩᱜ: <strong>({{int:cur}})</strong> = ᱱᱮᱛᱟᱨ ᱥᱩᱫᱷᱨᱟᱹᱣ ᱥᱟᱶᱛᱮ ᱥᱚᱝ, <strong>({{int:last}})</strong> = ᱞᱟᱦᱟ ᱨᱮᱭᱟᱜ ᱱᱟᱣᱟ ᱥᱩᱫᱷᱨᱟᱹᱣ ᱥᱟᱶᱛᱮ ᱥᱚᱝ, <strong>{{int:minoreditletter}}</strong> = ᱦᱩᱰᱤᱧ ᱥᱟᱯᱲᱟᱣ ᱾",
- "history-fieldset-title": "ᱧᱮá±\9e á±\9fᱹᱨᱩ á±\9eá±\9fá±¹á±\9cᱤᱫ ᱥᱮᱸᱫᱽᱨá±\9f",
+ "history-fieldset-title": "ᱧᱮá±\9e á±\9fᱹᱨᱩ ᱪᱷá±\9fᱹᱱᱤ",
"history-show-deleted": "ᱠᱷᱟᱹᱞᱤ ᱜᱮᱫ ᱜᱤᱰᱤᱭᱟᱜ ᱠᱚᱜᱮ",
"histfirst": "ᱢᱟᱨᱮᱱᱟᱜ",
"histlast": "ᱱᱟᱣᱟᱱᱟᱜ",
"authmanager-provider-password": "Verifikacija lozinkom",
"authmanager-provider-password-domain": "Verifikacija lozinkom i domenom",
"authmanager-provider-temporarypassword": "Privremena lozinka",
+ "authprovider-confirmlink-request-label": "Računi koji se trebaju povezati",
+ "authprovider-confirmlink-success-line": "$1: Uspješno povezano.",
+ "authprovider-confirmlink-failed": "Povezivanje računa nije uspjelo u potpunosti: $1",
+ "authprovider-confirmlink-ok-help": "Nastavi nakon prikazivanja poruka za neuspješno povezivanje.",
+ "authprovider-resetpass-skip-label": "Preskoči",
+ "authprovider-resetpass-skip-help": "Preskoči zadavanje nove lozinke.",
+ "authform-nosession-login": "Verifikacija je uspješna, ali vaš preglednik ne može \"zapamtiti\" da ste prijavljeni.\n\n$1",
+ "authform-nosession-signup": "Račun je napravljen, ali vaš preglednik ne može \"zapamtiti\" da ste prijavljeni.\n\n$1",
+ "authform-newtoken": "Nedostaje token. $1",
+ "authform-notoken": "Nedostaje token",
+ "authform-wrongtoken": "Pogrešan token",
+ "specialpage-securitylevel-not-allowed-title": "Nije dozvoljeno",
+ "specialpage-securitylevel-not-allowed": "Žao nam je, nije Vam dozvoljeno korištenje ove stranice jer nije moguće potvrditi vaš identitet.",
+ "authpage-cannot-login": "Ne mogu započeti prijavu.",
+ "authpage-cannot-login-continue": "Ne mogu nastaviti s prijavom. Najvjerovatnije vaša sesija je istekla.",
+ "authpage-cannot-create": "Ne mogu započeti stvaranje računa.",
+ "authpage-cannot-create-continue": "Ne mogu nastaviti s stvaranjem računa. Najvjerovatnije vaša sesija je istekla.",
+ "authpage-cannot-link": "Ne mogu započeti spajanje računa.",
+ "authpage-cannot-link-continue": "Ne mogu nastaviti sa spajanjem računa. Najvjerovatnije vaša sesija je istekla.",
+ "cannotauth-not-allowed-title": "Pristup je odbijen",
+ "cannotauth-not-allowed": "Nije vam dozvoljeno da koristite ovu stranicu",
"userjsispublic": "Napomena: podstranice s JavaScriptom ne bi trebale sadržavati povjerljive podatke budući da ih drugi korisnici mogu vidjeti.",
"userjsonispublic": "Imajte na umu: Podstranice s JSONom ne bi trebale sadržavati povjerljive podatke budući da su vidljive drugim korisnicima.",
"usercssispublic": "Napomena: podstranice s CSS-om ne bi trebale sadržavati povjerljive podatke budući da ih drugi korisnici mogu vidjeti.",
"page_first": "prva",
"page_last": "zadnja",
"histlegend": "Izbira primerjave: označite okroglo polje ob redakciji za primerjavo in stisnite enter ali gumb na dnu strani.<br />\nLegenda: '''({{int:cur}})''' = primerjava s trenutno redakcijo, '''({{int:last}})''' = primerjava s prejšnjo redakcijo, '''{{int:minoreditletter}}''' = manjše urejanje.",
- "history-fieldset-title": "Iskanje redakcij",
+ "history-fieldset-title": "Filtrirajte redakcije",
"history-show-deleted": "Samo izbrisana redakcija",
"histfirst": "najstarejše",
"histlast": "najnovejše",
"right-editsemiprotected": "Urejanje strani, zaščitenih kot »{{int:protect-level-autoconfirmed}}«",
"right-editcontentmodel": "Urejanje vsebinskega modela strani",
"right-editinterface": "Urejanje uporabniškega vmesnika",
- "right-editusercss": "Urejanje CSS datotek drugih uporabnikov",
+ "right-editusercss": "Urejanje CSS-datotek drugih uporabnikov",
"right-edituserjson": "Urejanje JSON-datotek drugih uporabnikov",
"right-edituserjs": "Urejanje JavaScript datotek drugih uporabnikov",
"right-editsitecss": "Urejanje CSS spletišča",
"right-userrights": "Urejanje vseh uporabniških pravic",
"right-userrights-interwiki": "Urejanje uporabniških pravic uporabnikov na drugih wikijih",
"right-siteadmin": "Zaklepanje in odklepanje baze podatkov",
- "right-override-export-depth": "Izvoz strani, vključno s povezaimi straneh do globine 5",
+ "right-override-export-depth": "Izvoz strani, vključno s povezanimi stranmi do globine 5",
"right-sendemail": "Pošiljanje e-pošte drugim uporabnikom",
"right-managechangetags": "Ustvarjanje in (dez)aktivacijo [[Special:Tags|oznak]]",
"right-applychangetags": "Uveljavitev [[Special:Tags|oznak]] skupaj s spremembami",
"action-changetags": "dodajanje in odstranjevanje poljubnih oznak na posameznih redakcijah in dnevniških vnosih",
"action-deletechangetags": "izbris oznak iz zbirke podatkov",
"action-purge": "počiščenje strani",
+ "action-apihighlimits": "uporabo višje omejitve poizvedb API",
+ "action-autoconfirmed": "neomejitev dejavnosti glede na IP",
+ "action-bigdelete": "brisanje strani z obsežno zgodovino",
+ "action-blockemail": "preprečite pošiljanja e-pošte drugemu uporabniku",
+ "action-bot": "obravnavo kot avtomatiziran postopek",
+ "action-editprotected": "urejanje strani, zaščitenih kot »{{int:protect-level-sysop}}«,",
+ "action-editsemiprotected": "urejanje strani, zaščitenih kot »{{int:protect-level-autoconfirmed}}«,",
+ "action-editinterface": "urejanje uporabniškega vmesnika",
+ "action-editusercss": "urejanje CSS-datotek drugih uporabnikov",
+ "action-edituserjson": "urejanje JSON-datotek drugih uporabnikov",
+ "action-edituserjs": "urejanje JavaScript datotek drugih uporabnikov",
+ "action-editsitecss": "urejanje CSS spletišča",
+ "action-editsitejson": "urejanje JSON spletišča",
+ "action-editsitejs": "urejanje JavaScripta spletišča",
+ "action-editmyusercss": "urejanje svojih uporabniških datotek CSS",
+ "action-editmyuserjson": "urejanje svojih uporabniških datotek JSON",
+ "action-editmyuserjs": "urejanje svojih uporabniških datotek JavaScript",
+ "action-viewsuppressed": "ogled redakcij skritih pred vsemi uporabniki",
+ "action-hideuser": "blokiranje uporabnika in skritje pred javnostjo",
+ "action-ipblock-exempt": "izogib blokadam IP-naslova, samodejnim blokadam in blokadam območij",
+ "action-unblockself": "odblokiranje samega sebe",
+ "action-noratelimit": "izogib omejitvam dejavnosti",
+ "action-reupload-own": "nadomeščanje obstoječih lastnih datotek",
+ "action-nominornewtalk": "to, da urejanja pogovornih strani, ki niso označena kot manjša, ne sprožijo obvestila o novem sporočilu,",
+ "action-markbotedits": "označitev vrnjenih urejanj kot urejanja botov",
+ "action-patrolmarks": "ogled oznak nadzorov v zadnjih spremembah",
+ "action-override-export-depth": "izvoz strani, vključno s povezanimi stranmi do globine 5,",
+ "action-suppressredirect": "možnost izpuščanja preusmeritve pri premikanju strani",
"nchanges": "$1 {{PLURAL:$1|sprememba|spremembi|spremembe|sprememb|sprememb}}",
"enhancedrc-since-last-visit": "$1 {{PLURAL:$1|od zadnjega obiska}}",
"enhancedrc-history": "zgodovina",
"rcfilters-savedqueries-already-saved": "Te filtre ste že shranili. Uporabite svoje nastavitve, da ustvarite nov Shranjen filter.",
"rcfilters-restore-default-filters": "Obnovi privzete filtre",
"rcfilters-clear-all-filters": "Počisti vse filtre",
- "rcfilters-show-new-changes": "Ogled najnovejših sprememb",
+ "rcfilters-show-new-changes": "Ogled novih sprememb od $1",
"rcfilters-search-placeholder": "Filtriraj zadnje spremembe (uporabi meni ali vnesi ime filtra)",
"rcfilters-invalid-filter": "Neveljaven filter",
"rcfilters-empty-filter": "Ni dejavnih filtrov. Prikazani so vsi prispevki.",
"blocklist-userblocks": "skrij blokade računov",
"blocklist-tempblocks": "skrij začasne blokade",
"blocklist-addressblocks": "skrij blokade posameznih IP-naslovov",
+ "blocklist-type": "Vrsta:",
+ "blocklist-type-opt-all": "Vse",
+ "blocklist-type-opt-sitewide": "Po celotni strani",
+ "blocklist-type-opt-partial": "Delno",
"blocklist-rangeblocks": "skrij blokade razponov",
"blocklist-timestamp": "Časovni žig",
"blocklist-target": "Cilj",
"blocklist-editing-page": "strani",
"blocklist-editing-ns": "imenski prostori",
"ipblocklist-empty": "Seznam blokad je prazen.",
- "ipblocklist-no-results": "Zahtevan IP-naslov ali uporabniško ime ni blokirano.",
+ "ipblocklist-no-results": "Ne najdemo ujemajočih blokov za zahtevan IP-naslov ali uporabniško ime.",
"blocklink": "blokiraj",
"unblocklink": "deblokiraj",
"change-blocklink": "spremeni blokado",
"passwordpolicies-policyflag-forcechange": "treba spremeniti ob prijavi",
"passwordpolicies-policyflag-suggestchangeonlogin": "predlagaj zamenjavo ob prijavi",
"easydeflate-invaliddeflate": "Dana vsebina ni pravilno stisnjena",
- "unprotected-js": "Iz varnostnih razlogov JavaScripta ni možno naložiti z nezaščitenih strani. Prosimo, da JavaScript ustvarite samo v imenskem prostoru MediaWiki ali kot uporabniško podstran."
+ "unprotected-js": "Iz varnostnih razlogov JavaScripta ni možno naložiti z nezaščitenih strani. Prosimo, da JavaScript ustvarite samo v imenskem prostoru MediaWiki ali kot uporabniško podstran.",
+ "userlogout-continue": "Če se želite odjaviti, [$1 pojdite na stran za odjavo].",
+ "userlogout-sessionerror": "Odjava je spodletela zaradi napake seje. Prosimo, [$1 poskusite znova]."
}
"아라",
"Macofe",
"Fitoschido",
- "Ghiutun"
+ "Ghiutun",
+ "ToBeFree"
]
},
"tog-underline": "Verknipfonga unterstreeicha:",
"tooltip-watch": "Fiege diese Seite denner Beobachtungsliste hinzu",
"tooltip-recreate": "Seite neu erstella, obwohl se geläscht wurde.",
"tooltip-upload": "Huchloada starta",
- "tooltip-rollback": "Moacht olle letzta Änderunga dar Seite, de vum gleichen Benutzer vurgenumma waan sein, dorch ocke eenen Klick rieckgängig.",
+ "tooltip-rollback": "Moacht olle letzta Änderunga dar Seite, de vum selben Benutzer vurgenumma waan sein, dorch ocke eenen Klick rieckgängig.",
"tooltip-undo": "Moacht lediglich diese eene Änderung rieckgängig on zeigt doas Resultat ei dar Vorschau oa, damit ei dar Zusommafassungszeile eene Begründung angegeba waan koan.",
"tooltip-summary": "Gib eine kurze Zusammenfassung ein",
"anonymous": "{{PLURAL:$1|Anonymer Nutzer|Anonyme Nutzer}} uff {{SITENAME}}",
"blockedtext-partial": "<strong>Вашем корисничком имену или IP адреси је блокирано прављење промена на овој страници. Још увек можете да уређујете друге странице на овом викију.</strong> Можете да видите потпуне детаље блокаде на [[Special:MyContributions|доприносима налога]].\n\nБлокаду је извршио/ла $1.\n\nНаведен је следећи разлог: <em>$2</em>.\n\n* Почетак блокаде: $8\n* Истек блокаде: $6\n* Намењена кориснику/ци или IP адреси: $7\n* ID блокаде #$5",
"blockedtext": "<strong>Ваше корисничко име или IP адреса је блокирана.</strong>\n\nБлокирање је {{GENDER:$4|извршио|извршила}} $1.\nРазлог је <em>$2</em>.\n\n* Почетак блокирања: $8\n* Истек блокирања: $6\n* Блокирани: $7\n\nМожете да се обратите {{GENDER:$4|кориснику|корисници}} $1 или [[{{MediaWiki:Grouppage-sysop}}|администратору]] ради дискусије о блокирању.\nНе можете да користите функцију „{{int:emailuser}}” осим ако сте унели важећу е-адресу у својим [[Special:Preferences|подешавањима]] налога и нисте блокирани од коришћења исте.\nВаша тренутна IP адреса је $3, а ID блокирања #$5.\nНаведите све информације одозго при стварању било каквих упита.",
"autoblockedtext": "Ваша IP адреса је аутоматски блокирана јер ју је користио други корисник, кога је {{GENDER:$4|блокирао|блокирала|блокирао/ла}} $1.\nРазлог:\n\n:<em>$2</em>\n\n* Почетак блокаде: $8\n* Крај блокаде: $6\n* Име корисника: $7\n\nМожете да контактирате {{GENDER:$4|корисника|корисницу|корисника/цу}} $1 или другог [[{{MediaWiki:Grouppage-sysop}}|администратора]] да бисте расправљали о блокади.\n\nЗапамтите да не можете да користите функцију „{{int:emailuser}}“ осим ако сте навели важећу е-адресу у [[Special:Preferences|подешавањима]].\n\nВаша тренутна IP адреса је $3, а ID блокаде $5.\nУкључите све горње детаље при прављењу било каквих упита.",
+ "systemblockedtext": "Медијавики је аутоматски блокирао ваше корисничко име или IP адресу.\nНаведен је следећи разлог:\n\n:<em>$2</em>\n\n* Почетак блокирања: $8\n* Истек блокирања: $6\n* Блокирање је намењено за: $7\n\nВаша тренурна IP адреса $3.\nУкључите све горенаведене детаље при прављењу било којих упита.",
"blockednoreason": "разлог није наведен",
"whitelistedittext": "$1 да бисте уређивали странице.",
"confirmedittext": "Морате да потврдите е-адресу пре уређивања страница.\nПоставите и проверите ваљаност адресе преко [[Special:Preferences|подешавања]].",
"page_first": "прва",
"page_last": "последња",
"histlegend": "Избор разлика: означите кутијице измена за упоређивање и притисните enter или дугме на дну.<br />\nОбјашњење: <strong>({{int:cur}})</strong> = разлика са најновијом изменом, <strong>({{int:last}})</strong> = разлика са претходном изменом, <strong>{{int:minoreditletter}}</strong> = мања измена.",
- "history-fieldset-title": "Ð\9fÑ\80еÑ\82Ñ\80ага измена",
+ "history-fieldset-title": "ФилÑ\82Ñ\80иÑ\80аÑ\9aе измена",
"history-show-deleted": "Само избрисане измене",
"histfirst": "најстарије",
"histlast": "најновије",
"diff-paragraph-moved-toold": "Пасус је премештен. Кликните да пређете на стару локацију.",
"difference-missing-revision": "{{PLURAL:$2|Једна измена|$2 измене}} ове разлике ($1) не {{PLURAL:$2|постоји|постоје}}.\n\nОво се обично дешава када пратите застарелу везу до странице која је избрисана.\nДетаље можете да пронађете у [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дневнику брисања].",
"searchresults": "Резултати претраге",
+ "search-filter-title-prefix": "Само претражује на страницама чији наслов почиње са „$1”",
"search-filter-title-prefix-reset": "Претражи све странице",
"searchresults-title": "Резултати претраге за „$1“",
"titlematches": "Наслов странице одговара",
"right-reupload-own": "замењивање сопствених датотека",
"right-reupload-shared": "локално замењивање датотека на дељеном спремишту медија",
"right-upload_by_url": "отпремање датотека са УРЛ-а",
- "right-purge": "чишћење кеш меморије странице без потврде",
+ "right-purge": "чишћење кеш меморије странице",
"right-autoconfirmed": "без ограничавања ставки за IP адресе",
"right-bot": "сматрање измена као аутоматски процес",
"right-nominornewtalk": "непоседовање мањих измена на страницама за разговор отвара прозор за нове поруке",
"right-editusercss": "уређивање туђих Це-Ес-Ес датотека",
"right-edituserjson": "уређивање туђих ЈСОН датотека",
"right-edituserjs": "уређивање туђих јаваскрипт датотека",
+ "right-editsitecss": "уређивање CSS-а на нивоу сајта",
+ "right-editsitejson": "уређивање JSON-а на нивоу сајта",
+ "right-editsitejs": "Уређивање JavaScript-а на нивоу сајта",
"right-editmyusercss": "уређивање сопствених Це-Ес-Ес датотека",
"right-editmyuserjson": "уређивање сопствених ЈСОН датотека",
"right-editmyuserjs": "уређивање сопствених јаваскрипт датотека",
"action-changetags": "додате и уклоните разне ознаке на појединачним изменама и уносима у дневницима",
"action-deletechangetags": "бришете ознаке из базе података",
"action-purge": "освежите ову страницу",
+ "action-blockemail": "блокирате кориснику слање е-порука",
+ "action-editsitecss": "уређујете CSS на новоу сајта",
+ "action-editsitejson": "уређујете JSON на нивоу сајта",
+ "action-editsitejs": "уређујете JavaScript на новоу сајта",
+ "action-hideuser": "блокирате корисничко име, сакривајући га од јавности",
"nchanges": "$1 {{PLURAL:$1|промена|промене|промена}}",
"ntimes": "$1×",
"enhancedrc-since-last-visit": "$1 {{PLURAL:$1|измена од ваше последње посете}}",
"recentchanges-network": "Због техничког проблема, није могуће учитати резултате. Покушајте да освежите страницу.",
"recentchanges-notargetpage": "Унесите име странице изнад да бисте видели промене сродне с овом страницом",
"recentchanges-feed-description": "Пратите недавне промене на викију у овом фиду.",
- "recentchanges-label-newpage": "Ð\9dова страница",
- "recentchanges-label-minor": "Ð\9cања измена",
- "recentchanges-label-bot": "Ð\91оÑ\82овÑ\81ка измена",
- "recentchanges-label-unpatrolled": "Ð\9dепаÑ\82Ñ\80олиÑ\80ана измена",
+ "recentchanges-label-newpage": "Ð\9eвом изменом напÑ\80авÑ\99ена Ñ\98е нова страница",
+ "recentchanges-label-minor": "Ð\9eво Ñ\98е мања измена",
+ "recentchanges-label-bot": "Ð\9eвÑ\83 изменÑ\83 Ñ\98е напÑ\80авио боÑ\82",
+ "recentchanges-label-unpatrolled": "Ð\9eва измена Ñ\98оÑ\88 ниÑ\98е паÑ\82Ñ\80олиÑ\80ана",
"recentchanges-label-plusminus": "Промена величине странице у бајтовима",
"recentchanges-legend-heading": "<strong>Легенда:</strong>",
- "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|списак нових страница]])",
+ "recentchanges-legend-newpage": "Нова страница ([[Special:NewPages|списак]])",
"recentchanges-legend-plusminus": "(<em>±123</em>)",
"recentchanges-submit": "Прикажи",
"rcfilters-tag-remove": "Уклоните филтер „$1“",
"rcfilters-savedqueries-already-saved": "Ови филтери су већ сачувани. Промените своја подешавања да бисте направили нове сачуване филтере.",
"rcfilters-restore-default-filters": "Врати подразумеване филтере",
"rcfilters-clear-all-filters": "Обришите све филтере",
- "rcfilters-show-new-changes": "Ð\9dаÑ\98новиÑ\98е пÑ\80омене",
+ "rcfilters-show-new-changes": "Ð\9fÑ\80икажи нове пÑ\80омене од $1",
"rcfilters-search-placeholder": "Филтрирајте промене (користите мени или претрагу за име филтера)",
"rcfilters-invalid-filter": "Неважећи филтер",
"rcfilters-empty-filter": "Нема активних филтера. Сви доприноси су приказани.",
"delete-confirm": "Брисање странице „$1“",
"delete-legend": "Брисање",
"historywarning": "<strong>Упозорење:</strong> Страница коју желите да избришете има историју са $1 {{PLURAL:$1|ревизијом|измене|измена}}:",
- "historyaction-submit": "Прикажи",
+ "historyaction-submit": "Прикажи измене",
"confirmdeletetext": "Управо ћете избрисати страницу, укључујући и њену историју.\nПотврдите своју намеру, да разумете последице и да ово радите у складу са [[{{MediaWiki:Policy-url}}|правилима]].",
"actioncomplete": "Радња је завршена",
"actionfailed": "Радња није успела",
"blocklist-userblocks": "Сакриј блокаде налога",
"blocklist-tempblocks": "Сакриј привремене блокаде",
"blocklist-addressblocks": "Сакриј појединачне блокаде IP-а",
+ "blocklist-type-opt-sitewide": "На новоу сајта",
+ "blocklist-type-opt-partial": "Делимично",
"blocklist-rangeblocks": "Сакриј блокаде опсега",
"blocklist-timestamp": "Временска ознака",
"blocklist-target": "Корисник",
"blocklist-editing-page": "странице",
"blocklist-editing-ns": "именски простори",
"ipblocklist-empty": "Списак блокирања је празан.",
- "ipblocklist-no-results": "ТÑ\80ажена IP адÑ\80еÑ\81а или коÑ\80иÑ\81ниÑ\87ко име ниÑ\98е блокиÑ\80ано.",
+ "ipblocklist-no-results": "Ð\9dиÑ\81Ñ\83 пÑ\80онаÑ\92ена одговаÑ\80аÑ\98Ñ\83Ñ\9bа блокиÑ\80аÑ\9aа Ñ\82Ñ\80ажене IP адÑ\80еÑ\81е или коÑ\80иÑ\81ниÑ\87ког имена.",
"blocklink": "блокирај",
"unblocklink": "деблокирај",
"change-blocklink": "промени блокаду",
"ipb_expiry_old": "Време истека је у прошлости.",
"ipb_expiry_temp": "Сакривене блокаде корисника морају бити трајне.",
"ipb_hide_invalid": "Не могу да потиснем овај налог; има више од {{PLURAL:$1|једне измене|$1 измена}}.",
+ "ipb_hide_partial": "Блокирања сакривених корисничких имена морају бити на нивоу сајта.",
"ipb_already_blocked": "„$1“ је већ блокиран.",
"ipb-needreblock": "$1 је већ блокиран. Желите ли да промените подешавања?",
"ipb-otherblocks-header": "{{PLURAL:$1|Друга блокада|Друге блокаде}}",
"logentry-block-unblock": "$1 је {{GENDER:$2|деблокирао|деблокирала}} {{GENDER:$4|$3}}",
"logentry-block-reblock": "$1 је {{GENDER:$2|променио|променила}} подешавања за блокирање {{GENDER:$4|корисника|кориснице}} {{GENDER:$4|$3}} у трајању од $5 $6",
"logentry-partialblock-block-page": "{{PLURAL:$1|странице|страница}} $2",
- "logentry-partialblock-block": "$1 је {{GENDER:$2|блокирао|блокирала}} уређивање $7 {{GENDER:$4|кориснику|корисници|кориснику/ци}} $3 са временом истека од $5 $6",
+ "logentry-partialblock-block-ns": "{{PLURAL:$1|именског простора|именских простора}} $2",
+ "logentry-partialblock-block": "$1 је {{GENDER:$2|блокирао|блокирала}} уређивање $7 {{GENDER:$4|кориснику|корисници}} $3 са временом истека од $5 $6",
+ "logentry-partialblock-reblock": "$1 је {{GENDER:$2|променио}} подешавања блокирања {{GENDER:$4|корисника|кориснице}} $3 спречавањем измена $7 са временом истека од $5 $6",
"logentry-non-editing-block-block": "$1 је {{GENDER:$2|блокирао|блокирала}} одређене неуређивачке радње {{GENDER:$4|кориснику|корисници|кориснику/ци}} $3 са временом истека од $5 $6",
"logentry-non-editing-block-reblock": "$1 је {{GENDER:$2|променио|променила}} подешавања блокаде одређених неуређивачких радњи {{GENDER:$4|кориснику|корисници|кориснику/ци}} $3 са временом истека од $5 $6",
"logentry-suppress-block": "$1 је {{GENDER:$2|блокирао|блокирала}} {{GENDER:$4|$3}} у трајању од $5 $6",
"passwordpolicies-policyflag-forcechange": "måste ändras vid inloggning",
"passwordpolicies-policyflag-suggestchangeonlogin": "föreslå ändring vid inloggning",
"easydeflate-invaliddeflate": "Innehåll som tillhandahålls är inte helt komprimerat",
- "unprotected-js": "Av säkerhetsskäl kan inte JavaScript läsas in från oskyddade sidor. Skapa endast JavaScript i namnrymden MediaWiki: eller som en användarundersida."
+ "unprotected-js": "Av säkerhetsskäl kan inte JavaScript läsas in från oskyddade sidor. Skapa endast JavaScript i namnrymden MediaWiki: eller som en användarundersida.",
+ "userlogout-continue": "Om du vill logga ut, var god [$1 fortsätt till utloggningssidan].",
+ "userlogout-sessionerror": "Utloggning misslyckades p.g.a. sessionsfel. Var god [$1 försök igen]."
}
"userrights-groupsmember": "สมาชิกของ:",
"userrights-groupsmember-auto": "สมาชิกโดยปริยายของ:",
"userrights-groupsmember-type": "$1",
- "userrights-groups-help": "à¸\84ุà¸\93สามารà¸\96à¹\80à¸\9bลีà¹\88ยà¸\99à¹\81à¸\9bลà¸\87à¸\81ลุà¹\88มà¸\97ีà¹\88à¸\9cูà¹\89à¹\83à¸\8aà¹\89รายà¸\99ีà¹\89à¸à¸¢à¸¹à¹\88:\n* à¸\81ลà¹\88à¸à¸\87à¸\97ีà¹\88มีà¹\80à¸\84รืà¹\88à¸à¸\87หมายà¸\96ูà¸\81 หมายà¸\84วามวà¹\88า à¸\9cูà¹\89à¹\83à¸\8aà¹\89à¸à¸¢à¸¹à¹\88à¹\83à¸\99à¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99\n* à¸\81ลà¹\88à¸à¸\87à¸\97ีà¹\88à¹\84มà¹\88มีà¹\80à¸\84รืà¹\88à¸à¸\87หมายà¸\96ูà¸\81 หมายà¸\84วามวà¹\88า à¸\9cูà¹\89à¹\83à¸\8aà¹\89à¹\84มà¹\88à¹\84à¸\94à¹\89à¸à¸¢à¸¹à¹\88à¹\83à¸\99à¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99\n* à¹\80à¸\84รืà¹\88à¸à¸\87หมาย * à¸\8aีà¹\89วà¹\88าà¸\84ุà¸\93à¹\84มà¹\88สามารà¸\96à¸\99ำà¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99à¸à¸à¸\81à¹\84à¸\94à¹\89à¹\80มืà¹\88à¸à¸\84ุà¸\93à¹\80à¸\9eิà¹\88มà¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99à¹\84à¸\9bà¹\81ลà¹\89ว หรืà¸à¸\81ลัà¸\9aà¸\81ัà¸\99\n* à¹\80à¸\84รืà¹\88à¸à¸\87หมาย # à¸\9aี้ว่าคุณสามารถแก้คืนเวลาหมดอายุของสมาชิกภาพกลุ่มนี้เท่านั้น คุณไม่สามารถร่นเวลาหมดอายุได้",
+ "userrights-groups-help": "à¸\84ุà¸\93สามารà¸\96à¹\80à¸\9bลีà¹\88ยà¸\99à¹\81à¸\9bลà¸\87à¸\81ลุà¹\88มà¸\97ีà¹\88à¸\9cูà¹\89à¹\83à¸\8aà¹\89รายà¸\99ีà¹\89à¸à¸¢à¸¹à¹\88:\n* à¸\81ลà¹\88à¸à¸\87à¸\97ีà¹\88มีà¹\80à¸\84รืà¹\88à¸à¸\87หมายà¸\96ูà¸\81 หมายà¸\84วามวà¹\88า à¸\9cูà¹\89à¹\83à¸\8aà¹\89à¸à¸¢à¸¹à¹\88à¹\83à¸\99à¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99\n* à¸\81ลà¹\88à¸à¸\87à¸\97ีà¹\88à¹\84มà¹\88มีà¹\80à¸\84รืà¹\88à¸à¸\87หมายà¸\96ูà¸\81 หมายà¸\84วามวà¹\88า à¸\9cูà¹\89à¹\83à¸\8aà¹\89à¹\84มà¹\88à¹\84à¸\94à¹\89à¸à¸¢à¸¹à¹\88à¹\83à¸\99à¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99\n* à¹\80à¸\84รืà¹\88à¸à¸\87หมาย * à¸\8aีà¹\89วà¹\88าà¸\84ุà¸\93à¹\84มà¹\88สามารà¸\96à¸\99ำà¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99à¸à¸à¸\81à¹\84à¸\94à¹\89à¹\80มืà¹\88à¸à¸\84ุà¸\93à¹\80à¸\9eิà¹\88มà¸\81ลุà¹\88มà¸\99ัà¹\89à¸\99à¹\84à¸\9bà¹\81ลà¹\89ว หรืà¸à¸\81ลัà¸\9aà¸\81ัà¸\99\n* à¹\80à¸\84รืà¹\88à¸à¸\87หมาย # à¸\8aี้ว่าคุณสามารถแก้คืนเวลาหมดอายุของสมาชิกภาพกลุ่มนี้เท่านั้น คุณไม่สามารถร่นเวลาหมดอายุได้",
"userrights-reason": "เหตุผล:",
"userrights-no-interwiki": "คุณไม่มีสิทธิแก้ไขสิทธิผู้ใช้บนวิกิอื่น",
"userrights-nodatabase": "ไม่มีฐานข้อมูล $1 หรือฐานข้อมูลอยู่บนเครื่องอื่น",
"rcfilters-savedqueries-already-saved": "ตัวกรองเหล่านี้บันทุกแล้ว เปลี่ยนการตั้งค่าของคุณเพื่อสร้างตัวกรองที่บันทึกแล้วใหม่",
"rcfilters-restore-default-filters": "คืนค่าตัวกรองปริยาย",
"rcfilters-clear-all-filters": "ล้างตัวกรองทั้งหมด",
- "rcfilters-show-new-changes": "à¸\94ูà¸\81ารà¹\80à¸\9bลีà¹\88ยà¸\99à¹\81à¸\9bลà¸\87ลà¹\88าสุà¸\94",
+ "rcfilters-show-new-changes": "à¸\94ูà¸\81ารà¹\80à¸\9bลีà¹\88ยà¸\99à¹\81à¸\9bลà¸\87à¹\83หมà¹\88à¸\95ัà¹\89à¸\87à¹\81à¸\95à¹\88 $1",
"rcfilters-search-placeholder": "กรองการเปลี่ยนแปลง (ใช้รายการเลือกหรือค้นหาชื่อตัวกรอง)",
"rcfilters-invalid-filter": "ตัวกรองไม่ถูกต้อง",
"rcfilters-empty-filter": "ไม่มีตัวกรองเปิดใช้งาน แสดงการแก้ไขทั้งหมด",
"blocklist-editing-page": "页面",
"blocklist-editing-ns": "名字空间",
"ipblocklist-empty": "封禁列表为空。",
- "ipblocklist-no-results": "请æ±\82ç\9a\84IPå\9c°å\9d\80æ\88\96ç\94¨æ\88·å\90\8d没æ\9c\89被封禁。",
+ "ipblocklist-no-results": "请æ±\82ç\9a\84IPå\9c°å\9d\80æ\88\96ç\94¨æ\88·å\90\8dæ\9cª被封禁。",
"blocklink": "封禁",
"unblocklink": "解封",
"change-blocklink": "更改封禁",
$ok = false;
while ( !$ok ) {
try {
- $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
+ $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
$dbw->insert( 'revision', self::$dummyRev, $fname );
$id = $dbw->insertId();
$toDelete[] = $id;
self::$dummyRev = self::makeDummyRevisionRow( $dbw );
}
- $updates = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $arIds ) {
+ $updates = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $arIds ) {
// Create new rev_ids by inserting dummy rows into revision and then deleting them.
$dbw->insert( 'revision', array_fill( 0, count( $arIds ), self::$dummyRev ), $fname );
$revIds = $dbw->selectFieldValues(
ol:lang( lrc ) li,
ol:lang( luz ) li,
ol:lang( mzn ) li {
- list-style-type: -moz-persian;
list-style-type: persian;
}
ol:lang( ckb ) li,
ol:lang( sdh ) li {
- list-style-type: -moz-arabic-indic;
list-style-type: arabic-indic;
}
ol:lang( mai ) li,
ol:lang( mr ) li,
ol:lang( ne ) li {
- list-style-type: -moz-devanagari;
list-style-type: devanagari;
}
ol:lang( as ) li,
ol:lang( bn ) li {
- list-style-type: -moz-bengali;
list-style-type: bengali;
}
ol:lang( or ) li {
- list-style-type: -moz-oriya;
list-style-type: oriya;
}
* @param string $text Content of the page
* @param string $summary Optional summary string for the revision
* @param int $defaultNs Optional namespace id
- * @return array Array as returned by WikiPage::doEditContent()
+ * @return Status Object as returned by WikiPage::doEditContent()
* @throws MWException If this test cases's needsDB() method doesn't return true.
* Test cases can use "@group Database" to enable database test support,
* or list the tables under testing in $this->tablesUsed, or override the
$callback( 1, [] );
}
- public function testInsertUserIdentity() {
+ /**
+ * @dataProvider provideStages
+ * @param int $stage
+ */
+ public function testInsertUserIdentity( $stage ) {
$this->setMwGlobals( [
// for User::getActorId()
- 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+ 'wgActorTableSchemaMigrationStage' => $stage
] );
$this->overrideMwServices();
- $user = $this->getTestUser()->getUser();
+ $user = $this->getMutableTestUser()->getUser();
$userIdentity = $this->getMock( UserIdentity::class );
$userIdentity->method( 'getId' )->willReturn( $user->getId() );
$userIdentity->method( 'getName' )->willReturn( $user->getName() );
list( $cFields, $cCallback ) = MediaWikiServices::getInstance()->getCommentStore()
->insertWithTempTable( $this->db, 'rev_comment', '' );
- $m = $this->makeMigration( SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW );
+ $m = $this->makeMigration( $stage );
list( $fields, $callback ) =
$m->getInsertValuesWithTempTable( $this->db, 'rev_user', $userIdentity );
$extraFields = [
);
$this->assertSame( $user->getId(), (int)$row->rev_user );
$this->assertSame( $user->getName(), $row->rev_user_text );
- $this->assertSame( $user->getActorId(), (int)$row->rev_actor );
+ $this->assertSame(
+ ( $stage & SCHEMA_COMPAT_READ_NEW ) ? $user->getActorId() : 0,
+ (int)$row->rev_actor
+ );
- $m = $this->makeMigration( SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW );
+ $m = $this->makeMigration( $stage );
$fields = $m->getInsertValues( $this->db, 'dummy_user', $userIdentity );
- $this->assertSame( $user->getId(), $fields['dummy_user'] );
- $this->assertSame( $user->getName(), $fields['dummy_user_text'] );
- $this->assertSame( $user->getActorId(), $fields['dummy_actor'] );
+ if ( $stage & SCHEMA_COMPAT_WRITE_OLD ) {
+ $this->assertSame( $user->getId(), $fields['dummy_user'] );
+ $this->assertSame( $user->getName(), $fields['dummy_user_text'] );
+ } else {
+ $this->assertArrayNotHasKey( 'dummy_user', $fields );
+ $this->assertArrayNotHasKey( 'dummy_user_text', $fields );
+ }
+ if ( $stage & SCHEMA_COMPAT_WRITE_NEW ) {
+ $this->assertSame( $user->getActorId(), $fields['dummy_actor'] );
+ } else {
+ $this->assertArrayNotHasKey( 'dummy_actor', $fields );
+ }
}
public function testNewMigration() {
$this->setMwGlobals( [
'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
- 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
] );
$this->overrideMwServices();
$queryInfo = $store->getQueryInfo( [ 'user' ] );
$row = get_object_vars( $row );
+
+ // Use aliased fields from $queryInfo, e.g. rev_user
+ $keys = array_keys( $row );
+ $keys = array_combine( $keys, $keys );
+ $fields = array_intersect_key( $queryInfo['fields'], $keys ) + $keys;
+
+ // assertSelect() fails unless the orders match.
+ ksort( $fields );
+ ksort( $row );
+
$this->assertSelect(
$queryInfo['tables'],
- array_keys( $row ),
+ $fields,
[ 'rev_id' => $rev->getId() ],
[ array_values( $row ) ],
[],
'rev_page' => (string)$rev->getPage(),
'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
'rev_user_text' => (string)$rev->getUserText(),
- 'rev_user' => (string)$rev->getUser(),
+ 'rev_user' => (string)$rev->getUser() ?: null,
'rev_minor_edit' => $rev->isMinor() ? '1' : '0',
'rev_deleted' => (string)$rev->getVisibility(),
'rev_len' => (string)$rev->getSize(),
/** @var Revision $rev */
$rev = $page->doEditContent(
new WikitextContent( $text ),
- __METHOD__
+ __METHOD__,
+ 0,
+ false,
+ $this->getMutableTestUser()->getUser()
)->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
$this->setMwGlobals( [
'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
- 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
] );
$this->overrideMwServices();
* @covers Revision::loadFromTitle
*/
public function testLoadFromTitle() {
- $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
$this->overrideMwServices();
$title = $this->getMockTitle();
$this->equalTo( [
'revision', 'page', 'user',
'temp_rev_comment' => 'revision_comment_temp', 'comment_rev_comment' => 'comment',
+ 'temp_rev_user' => 'revision_actor_temp', 'actor_rev_user' => 'actor',
] ),
// We don't really care about the fields are they come from the selectField methods
$this->isType( 'array' ),
$wgActorTableSchemaMigrationStage = $v;
$this->overrideMwServices();
}, [ $wgActorTableSchemaMigrationStage ] );
- $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD;
+ // Needs to WRITE_BOTH so READ_OLD tests below work. READ mode here doesn't really matter.
+ $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
$this->overrideMwServices();
$users = [
],
'wgProxyWhitelist' => [],
] );
+ $this->overrideMwServices();
$status = $this->manager->checkAccountCreatePermissions( new \User );
$this->assertFalse( $status->isOK() );
$this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
$this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
+ $this->overrideMwServices();
$status = $this->manager->checkAccountCreatePermissions( new \User );
$this->assertTrue( $status->isGood() );
}
--- /dev/null
+<?php
+
+use MediaWiki\Block\BlockManager;
+
+/**
+ * @group Blocking
+ * @group Database
+ * @coversDefaultClass \MediaWiki\Block\BlockManager
+ */
+class BlockManagerTest extends MediaWikiTestCase {
+
+ /** @var User */
+ protected $user;
+
+ /** @var int */
+ protected $sysopId;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->user = $this->getTestUser()->getUser();
+ $this->sysopId = $this->getTestSysop()->getUser()->getId();
+ }
+
+ private function getBlockManager( $overrideConfig ) {
+ $blockManagerConfig = array_merge( [
+ 'wgApplyIpBlocksToXff' => true,
+ 'wgCookieSetOnAutoblock' => true,
+ 'wgCookieSetOnIpBlock' => true,
+ 'wgDnsBlacklistUrls' => [],
+ 'wgEnableDnsBlacklist' => true,
+ 'wgProxyList' => [],
+ 'wgProxyWhitelist' => [],
+ 'wgSoftBlockRanges' => [],
+ ], $overrideConfig );
+ return new BlockManager(
+ $this->user,
+ $this->user->getRequest(),
+ ...array_values( $blockManagerConfig )
+ );
+ }
+
+ /**
+ * @dataProvider provideGetBlockFromCookieValue
+ * @covers ::getBlockFromCookieValue
+ */
+ public function testGetBlockFromCookieValue( $options, $expected ) {
+ $blockManager = $this->getBlockManager( [
+ 'wgCookieSetOnAutoblock' => true,
+ 'wgCookieSetOnIpBlock' => true,
+ ] );
+
+ $block = new Block( array_merge( [
+ 'address' => $options[ 'target' ] ?: $this->user,
+ 'by' => $this->sysopId,
+ ], $options[ 'blockOptions' ] ) );
+ $block->insert();
+
+ $class = new ReflectionClass( BlockManager::class );
+ $method = $class->getMethod( 'getBlockFromCookieValue' );
+ $method->setAccessible( true );
+
+ $user = $options[ 'loggedIn' ] ? $this->user : new User();
+ $user->getRequest()->setCookie( 'BlockID', $block->getCookieValue() );
+
+ $this->assertSame( $expected, (bool)$method->invoke(
+ $blockManager,
+ $user,
+ $user->getRequest()
+ ) );
+
+ $block->delete();
+ }
+
+ public static function provideGetBlockFromCookieValue() {
+ return [
+ 'Autoblocking user block' => [
+ [
+ 'target' => '',
+ 'loggedIn' => true,
+ 'blockOptions' => [
+ 'enableAutoblock' => true
+ ],
+ ],
+ true,
+ ],
+ 'Non-autoblocking user block' => [
+ [
+ 'target' => '',
+ 'loggedIn' => true,
+ 'blockOptions' => [],
+ ],
+ false,
+ ],
+ 'IP block for anonymous user' => [
+ [
+ 'target' => '127.0.0.1',
+ 'loggedIn' => false,
+ 'blockOptions' => [],
+ ],
+ true,
+ ],
+ 'IP block for logged in user' => [
+ [
+ 'target' => '127.0.0.1',
+ 'loggedIn' => true,
+ 'blockOptions' => [],
+ ],
+ false,
+ ],
+ 'IP range block for anonymous user' => [
+ [
+ 'target' => '127.0.0.0/8',
+ 'loggedIn' => false,
+ 'blockOptions' => [],
+ ],
+ true,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideIsLocallyBlockedProxy
+ * @covers ::isLocallyBlockedProxy
+ */
+ public function testIsLocallyBlockedProxy( $proxyList, $expected ) {
+ $class = new ReflectionClass( BlockManager::class );
+ $method = $class->getMethod( 'isLocallyBlockedProxy' );
+ $method->setAccessible( true );
+
+ $blockManager = $this->getBlockManager( [
+ 'wgProxyList' => $proxyList
+ ] );
+
+ $ip = '1.2.3.4';
+ $this->assertSame( $expected, $method->invoke( $blockManager, $ip ) );
+ }
+
+ public static function provideIsLocallyBlockedProxy() {
+ return [
+ 'Proxy list is empty' => [ [], false ],
+ 'Proxy list contains IP' => [ [ '1.2.3.4' ], true ],
+ 'Proxy list contains IP as value' => [ [ 'test' => '1.2.3.4' ], true ],
+ 'Proxy list contains range that covers IP' => [ [ '1.2.3.0/16' ], true ],
+ ];
+ }
+
+ /**
+ * @covers ::isLocallyBlockedProxy
+ */
+ public function testIsLocallyBlockedProxyDeprecated() {
+ $proxy = '1.2.3.4';
+
+ $this->hideDeprecated(
+ 'IP addresses in the keys of $wgProxyList (found the following IP ' .
+ 'addresses in keys: ' . $proxy . ', please move them to values)'
+ );
+
+ $class = new ReflectionClass( BlockManager::class );
+ $method = $class->getMethod( 'isLocallyBlockedProxy' );
+ $method->setAccessible( true );
+
+ $blockManager = $this->getBlockManager( [
+ 'wgProxyList' => [ $proxy => 'test' ]
+ ] );
+
+ $ip = '1.2.3.4';
+ $this->assertSame( true, $method->invoke( $blockManager, $ip ) );
+ }
+
+ /**
+ * @dataProvider provideIsDnsBlacklisted
+ * @covers ::isDnsBlacklisted
+ * @covers ::inDnsBlacklist
+ */
+ public function testIsDnsBlacklisted( $options, $expected ) {
+ $blockManager = $this->getBlockManager( [
+ 'wgEnableDnsBlacklist' => true,
+ 'wgDnsBlacklistUrls' => $options[ 'inBlacklist' ] ? [ 'local.wmftest.net' ] : [],
+ 'wgProxyWhitelist' => $options[ 'inWhitelist' ] ? [ '127.0.0.1' ] : [],
+ ] );
+
+ $ip = '127.0.0.1';
+ $this->assertSame(
+ $expected,
+ $blockManager->isDnsBlacklisted( $ip, $options[ 'check' ] )
+ );
+ }
+
+ public static function provideIsDnsBlacklisted() {
+ return [
+ 'IP is blacklisted' => [
+ [
+ 'inBlacklist' => true,
+ 'inWhitelist' => false,
+ 'check' => false,
+ ],
+ true,
+ ],
+ 'IP is not blacklisted' => [
+ [
+ 'inBlacklist' => false,
+ 'inWhitelist' => false,
+ 'check' => false,
+ ],
+ false,
+ ],
+ 'IP is blacklisted and whitelisted; whitelist is checked' => [
+ [
+ 'inBlacklist' => true,
+ 'inWhitelist' => true,
+ 'check' => false,
+ ],
+ true,
+ ],
+ 'IP is blacklisted and whitelisted; whitelist is not checked' => [
+ [
+ 'inBlacklist' => true,
+ 'inWhitelist' => true,
+ 'check' => true,
+ ],
+ false,
+ ],
+ ];
+ }
+}
$wrapper = TestingAccessWrapper::newFromObject( $this->testSubclass );
$this->assertSame( 1, $wrapper->privateNonDeprecated );
}, E_USER_ERROR, "Cannot access non-public property $fullName" );
+
+ $fullName = 'TestDeprecatedSubclass::$subclassPrivateNondeprecated';
+ $this->assertErrorTriggered( function () {
+ $this->assertSame( null, $this->testSubclass->subclassPrivateNondeprecated );
+ }, E_USER_ERROR, "Cannot access non-public property $fullName" );
+ $this->assertErrorTriggered( function () {
+ $this->testSubclass->subclassPrivateNondeprecated = 0;
+ $wrapper = TestingAccessWrapper::newFromObject( $this->testSubclass );
+ $this->assertSame( 1, $wrapper->subclassPrivateNondeprecated );
+ }, E_USER_ERROR, "Cannot access non-public property $fullName" );
}
protected function assertErrorTriggered( callable $callback, $level, $message ) {
class TestDeprecatedSubclass extends TestDeprecatedClass {
+ private $subclassPrivateNondeprecated = 1;
+
public function getDeprecatedPrivateParentProperty() {
return $this->privateDeprecated;
}
return [
[
'ar_minor_edit' => '0',
- 'ar_user' => '0',
+ 'ar_user' => null,
'ar_user_text' => $this->ipEditor,
- 'ar_actor' => null,
+ 'ar_actor' => (string)User::newFromName( $this->ipEditor, false )->getActorId( $this->db ),
'ar_len' => '11',
'ar_deleted' => '0',
'ar_rev_id' => strval( $this->ipRev->getId() ),
'ar_minor_edit' => '0',
'ar_user' => (string)$this->getTestUser()->getUser()->getId(),
'ar_user_text' => $this->getTestUser()->getUser()->getName(),
- 'ar_actor' => null,
+ 'ar_actor' => (string)$this->getTestUser()->getUser()->getActorId(),
'ar_len' => '7',
'ar_deleted' => '0',
'ar_rev_id' => strval( $this->firstRev->getId() ),
return [
[
'ar_minor_edit' => '0',
- 'ar_user' => '0',
+ 'ar_user' => null,
'ar_user_text' => $this->ipEditor,
- 'ar_actor' => null,
+ 'ar_actor' => (string)User::newFromName( $this->ipEditor, false )->getActorId( $this->db ),
'ar_len' => '11',
'ar_deleted' => '0',
'ar_rev_id' => strval( $this->ipRev->getId() ),
'ar_minor_edit' => '0',
'ar_user' => (string)$this->getTestUser()->getUser()->getId(),
'ar_user_text' => $this->getTestUser()->getUser()->getName(),
- 'ar_actor' => null,
+ 'ar_actor' => (string)$this->getTestUser()->getUser()->getActorId(),
'ar_len' => '7',
'ar_deleted' => '0',
'ar_rev_id' => strval( $this->firstRev->getId() ),
$this->tablesUsed += $this->getMcrTablesToReset();
- $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
$this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
$this->setMwGlobals(
'wgMultiContentRevisionSchemaMigrationStage',
}
public function testRcHidemyselfFilter() {
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
+ $this->overrideMwServices();
+
+ $user = $this->getTestUser()->getUser();
+ $user->getActorId( wfGetDB( DB_MASTER ) );
+ $this->assertConditions(
+ [ # expected
+ "NOT((rc_actor = '{$user->getActorId()}'))",
+ ],
+ [
+ 'hidemyself' => 1,
+ ],
+ "rc conditions: hidemyself=1 (logged in)",
+ $user
+ );
+
+ $user = User::newFromName( '10.11.12.13', false );
+ $id = $user->getActorId( wfGetDB( DB_MASTER ) );
+ $this->assertConditions(
+ [ # expected
+ "NOT((rc_actor = '{$user->getActorId()}'))",
+ ],
+ [
+ 'hidemyself' => 1,
+ ],
+ "rc conditions: hidemyself=1 (anon)",
+ $user
+ );
+ }
+
+ public function testRcHidemyselfFilter_old() {
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
}
public function testRcHidebyothersFilter() {
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
+ $this->overrideMwServices();
+
+ $user = $this->getTestUser()->getUser();
+ $user->getActorId( wfGetDB( DB_MASTER ) );
+ $this->assertConditions(
+ [ # expected
+ "(rc_actor = '{$user->getActorId()}')",
+ ],
+ [
+ 'hidebyothers' => 1,
+ ],
+ "rc conditions: hidebyothers=1 (logged in)",
+ $user
+ );
+
+ $user = User::newFromName( '10.11.12.13', false );
+ $id = $user->getActorId( wfGetDB( DB_MASTER ) );
+ $this->assertConditions(
+ [ # expected
+ "(rc_actor = '{$user->getActorId()}')",
+ ],
+ [
+ 'hidebyothers' => 1,
+ ],
+ "rc conditions: hidebyothers=1 (anon)",
+ $user
+ );
+ }
+
+ public function testRcHidebyothersFilter_old() {
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
}
public function testFilterUserExpLevelAllExperienceLevels() {
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
+ $this->overrideMwServices();
+
+ $this->assertConditions(
+ [
+ # expected
+ 'actor_rc_user.actor_user IS NOT NULL',
+ ],
+ [
+ 'userExpLevel' => 'newcomer;learner;experienced',
+ ],
+ "rc conditions: userExpLevel=newcomer;learner;experienced"
+ );
+ }
+
+ public function testFilterUserExpLevelAllExperienceLevels_old() {
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
}
public function testFilterUserExpLevelRegistrered() {
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
+ $this->overrideMwServices();
+
+ $this->assertConditions(
+ [
+ # expected
+ 'actor_rc_user.actor_user IS NOT NULL',
+ ],
+ [
+ 'userExpLevel' => 'registered',
+ ],
+ "rc conditions: userExpLevel=registered"
+ );
+ }
+
+ public function testFilterUserExpLevelRegistrered_old() {
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
}
public function testFilterUserExpLevelUnregistrered() {
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
+ $this->overrideMwServices();
+
+ $this->assertConditions(
+ [
+ # expected
+ 'actor_rc_user.actor_user IS NULL',
+ ],
+ [
+ 'userExpLevel' => 'unregistered',
+ ],
+ "rc conditions: userExpLevel=unregistered"
+ );
+ }
+
+ public function testFilterUserExpLevelUnregistrered_old() {
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
}
public function testFilterUserExpLevelRegistreredOrLearner() {
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
+ $this->overrideMwServices();
+
+ $this->assertConditions(
+ [
+ # expected
+ 'actor_rc_user.actor_user IS NOT NULL',
+ ],
+ [
+ 'userExpLevel' => 'registered;learner',
+ ],
+ "rc conditions: userExpLevel=registered;learner"
+ );
+ }
+
+ public function testFilterUserExpLevelRegistreredOrLearner_old() {
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
}
public function testFilterUserExpLevelUnregistreredOrExperienced() {
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
+ $this->overrideMwServices();
+
+ $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
+
+ $this->assertRegExp(
+ '/\(actor_rc_user\.actor_user IS NULL\) OR '
+ . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
+ reset( $conds ),
+ "rc conditions: userExpLevel=unregistered;experienced"
+ );
+ }
+
+ public function testFilterUserExpLevelUnregistreredOrExperienced_old() {
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
$this->assertContains( 'ip_changes', $queryInfo[0] );
$this->assertArrayHasKey( 'ip_changes', $queryInfo[5] );
- $this->assertSame( 'ipc_rev_timestamp', $queryInfo[1]['rev_timestamp'] );
- $this->assertSame( 'ipc_rev_id', $queryInfo[1]['rev_id'] );
- $this->assertSame( [ 'rev_timestamp DESC', 'rev_id DESC' ], $queryInfo[4]['ORDER BY'] );
+ $this->assertSame( [ 'ipc_rev_timestamp DESC', 'ipc_rev_id DESC' ], $queryInfo[4]['ORDER BY'] );
}
}
$this->setMwGlobals( [
'wgGroupPermissions' => [],
'wgRevokePermissions' => [],
- 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
] );
$this->overrideMwServices();
RequestContext::getMain()->setRequest( $request );
TestingAccessWrapper::newFromObject( $user )->mRequest = $request;
$request->getSession()->setUser( $user );
+ $this->overrideMwServices();
};
$this->setMwGlobals( 'wgSoftBlockRanges', [ '10.0.0.0/8' ] );
$this->assertFalse( $user->getExperienceLevel() );
}
- public static function provideIsLocallBlockedProxy() {
+ public static function provideIsLocallyBlockedProxy() {
return [
[ '1.2.3.4', '1.2.3.4' ],
[ '1.2.3.4', '1.2.3.0/16' ],
}
/**
- * @dataProvider provideIsLocallBlockedProxy
+ * @dataProvider provideIsLocallyBlockedProxy
* @covers User::isLocallyBlockedProxy
*/
public function testIsLocallyBlockedProxy( $ip, $blockListEntry ) {
+ $this->hideDeprecated( 'User::isLocallyBlockedProxy' );
+
$this->setMwGlobals(
'wgProxyList', []
);
$user = User::newFromId( $id );
$this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by ID' );
+ $user2 = User::newFromActorId( $user->getActorId() );
+ $this->assertEquals( $user->getId(), $user2->getId(),
+ 'User::newFromActorId works for an existing user' );
+
+ $row = $this->db->selectRow( 'user', User::selectFields(), [ 'user_id' => $id ], __METHOD__ );
+ $user = User::newFromRow( $row );
+ $this->assertTrue( $user->getActorId() > 0,
+ 'Actor ID can be retrieved for user loaded with User::selectFields()' );
+
+ $user = User::newFromId( $id );
+ $user->setName( 'UserTestActorId4-renamed' );
+ $user->saveSettings();
+ $this->assertEquals(
+ $user->getName(),
+ $this->db->selectField(
+ 'actor', 'actor_name', [ 'actor_id' => $user->getActorId() ], __METHOD__
+ ),
+ 'User::saveSettings updates actor table for name change'
+ );
+
+ // For sanity
+ $ip = '192.168.12.34';
+ $this->db->delete( 'actor', [ 'actor_name' => $ip ], __METHOD__ );
+
+ $user = User::newFromName( $ip, false );
+ $this->assertFalse( $user->getActorId() > 0, 'Anonymous user has no actor ID by default' );
+ $this->assertTrue( $user->getActorId( $this->db ) > 0,
+ 'Actor ID can be created for an anonymous user' );
+
+ $user = User::newFromName( $ip, false );
+ $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be loaded for an anonymous user' );
+ $user2 = User::newFromActorId( $user->getActorId() );
+ $this->assertEquals( $user->getName(), $user2->getName(),
+ 'User::newFromActorId works for an anonymous user' );
+ }
+
+ /**
+ * Actor tests with SCHEMA_COMPAT_READ_OLD
+ *
+ * The only thing different from testActorId() is the behavior if the actor
+ * row doesn't exist in the DB, since with SCHEMA_COMPAT_READ_NEW that
+ * situation can't happen. But we copy all the other tests too just for good measure.
+ *
+ * @covers User::newFromActorId
+ */
+ public function testActorId_old() {
+ $this->setMwGlobals( [
+ 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ ] );
+ $this->overrideMwServices();
+
+ $domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID();
+ $this->hideDeprecated( 'User::selectFields' );
+
+ // Newly-created user has an actor ID
+ $user = User::createNew( 'UserTestActorIdOld1' );
+ $id = $user->getId();
+ $this->assertTrue( $user->getActorId() > 0, 'User::createNew sets an actor ID' );
+
+ $user = User::newFromName( 'UserTestActorIdOld2' );
+ $user->addToDatabase();
+ $this->assertTrue( $user->getActorId() > 0, 'User::addToDatabase sets an actor ID' );
+
+ $user = User::newFromName( 'UserTestActorIdOld1' );
+ $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by name' );
+
+ $user = User::newFromId( $id );
+ $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by ID' );
+
$user2 = User::newFromActorId( $user->getActorId() );
$this->assertEquals( $user->getId(), $user2->getId(),
'User::newFromActorId works for an existing user' );
$this->assertFalse( $user->getActorId() > 0, 'No Actor ID by default if none in database' );
$this->assertTrue( $user->getActorId( $this->db ) > 0, 'Actor ID can be created if none in db' );
- $user->setName( 'UserTestActorId4-renamed' );
+ $user->setName( 'UserTestActorIdOld4-renamed' );
$user->saveSettings();
$this->assertEquals(
$user->getName(),