Merge "rdbms: cleanup visibility of Database::makeSelectOptions()"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 4 Sep 2019 20:40:59 +0000 (20:40 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 4 Sep 2019 20:40:59 +0000 (20:40 +0000)
103 files changed:
.phan/config.php
composer.json
includes/EditPage.php
includes/GlobalFunctions.php
includes/Revision/RevisionRenderer.php
includes/Revision/RevisionStore.php
includes/actions/HistoryAction.php
includes/api/ApiBase.php
includes/api/ApiExpandTemplates.php
includes/api/ApiMain.php
includes/api/ApiQueryBacklinks.php
includes/api/ApiQueryRevisionsBase.php
includes/api/i18n/es.json
includes/auth/AuthenticationRequest.php
includes/block/Restriction/AbstractRestriction.php
includes/changes/ChangesList.php
includes/config/EtcdConfig.php
includes/content/ContentHandler.php
includes/diff/DiffEngine.php
includes/export/ExportProgressFilter.php
includes/filerepo/LocalRepo.php
includes/filerepo/RepoGroup.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/LocalFileMoveBatch.php
includes/htmlform/fields/HTMLAutoCompleteSelectField.php
includes/http/MWHttpRequest.php
includes/import/ImportableOldRevision.php
includes/import/ImportableOldRevisionImporter.php
includes/import/ImportableUploadRevisionImporter.php
includes/import/WikiRevision.php
includes/installer/i18n/es.json
includes/installer/i18n/nl.json
includes/libs/MappedIterator.php
includes/libs/filebackend/FSFileBackend.php
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/FileBackendMultiWrite.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/MemoryFileBackend.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/filebackend/fileiteration/SwiftFileBackendList.php
includes/libs/filebackend/fileop/CopyFileOp.php
includes/libs/filebackend/fileop/CreateFileOp.php
includes/libs/filebackend/fileop/DeleteFileOp.php
includes/libs/filebackend/fileop/DescribeFileOp.php
includes/libs/filebackend/fileop/FileOp.php
includes/libs/filebackend/fileop/MoveFileOp.php
includes/libs/filebackend/fileop/StoreFileOp.php
includes/libs/http/MultiHttpClient.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/wancache/WANObjectCache.php
includes/libs/rdbms/loadmonitor/LoadMonitor.php
includes/media/ExifBitmapHandler.php
includes/parser/Parser.php
includes/parser/ParserDiffTest.php
includes/rcfeed/FormattedRCFeed.php
includes/resourceloader/ResourceLoaderClientHtml.php
includes/resourceloader/ResourceLoaderOOUIImageModule.php
includes/revisiondelete/RevDelFileList.php
includes/session/SessionManager.php
includes/shell/Shell.php
includes/skins/BaseTemplate.php
includes/specials/SpecialUncategorizedcategories.php
includes/specials/pagers/BlockListPager.php
includes/title/NamespaceInfo.php
includes/user/User.php
languages/Language.php
languages/i18n/ar.json
languages/i18n/az.json
languages/i18n/ban.json
languages/i18n/es-formal.json
languages/i18n/es.json
languages/i18n/exif/sh.json
languages/i18n/fi.json
languages/i18n/fit.json
languages/i18n/fr.json
languages/i18n/gom-deva.json
languages/i18n/gom-latn.json
languages/i18n/io.json
languages/i18n/ko.json
languages/i18n/lv.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/nl.json
languages/i18n/nqo.json
languages/i18n/pl.json
languages/i18n/pms.json
languages/i18n/sd.json
languages/i18n/sh.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/szl.json
maintenance/Maintenance.php
maintenance/benchmarks/benchmarkTidy.php
maintenance/convertExtensionToRegistration.php
maintenance/deleteArchivedFiles.php
maintenance/importImages.php
maintenance/storage/orphanStats.php
tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/filebackend/FileBackendTest.php
tests/phpunit/includes/import/ImportableOldRevisionImporterTest.php [new file with mode: 0644]

index 47f4512..fc775fe 100644 (file)
@@ -88,8 +88,13 @@ $cfg['exclude_analysis_directory_list'] = [
        'maintenance/language/',
        // External class
        'includes/libs/jsminplus.php',
+       // External class
+       'includes/libs/objectcache/utils/MemcachedClient.php',
 ];
 
+// NOTE: If you're facing an issue which you cannot easily fix, DO NOT add it here. Suppress it
+// either in-line with @phan-suppress-next-line and similar, at block-level (via @suppress), or at
+// file-level (with @phan-file-suppress), so that it stays enabled for the rest of the codebase.
 $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [
        // approximate error count: 19
        "PhanParamReqAfterOpt", // False positives with nullables (phan issue #3159). Use real nullables
@@ -97,12 +102,17 @@ $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [
        // approximate error count: 110
        "PhanParamTooMany", // False positives with variargs. Unsuppress after dropping HHVM
 
-       // approximate error count: 60
+       // approximate error count: 45
        "PhanTypeMismatchArgument",
-       // approximate error count: 752
+       // approximate error count: 693
        "PhanUndeclaredProperty",
 ] );
 
+// This helps a lot in discovering bad code, but unfortunately it will always fail for
+// hooks + pass by reference, see phan issue #2943.
+// @todo Enable when the issue above is resolved and we update our config!
+$cfg['redundant_condition_detection'] = false;
+
 $cfg['ignore_undeclared_variables_in_global_scope'] = true;
 // @todo It'd be great if we could just make phan read these from DefaultSettings, to avoid
 // duplicating the types.
@@ -125,6 +135,9 @@ $cfg['globals_type_map'] = array_merge( $cfg['globals_type_map'], [
        'wgWANObjectCaches' => 'array[]',
        'wgLocalInterwikis' => 'string[]',
        'wgDebugLogGroups' => 'string|false|array{destination:string,sample?:int,level:int}',
+       'wgCookiePrefix' => 'string|false',
+       'wgOut' => 'OutputPage',
+       'wgExtraNamespaces' => 'string[]',
 ] );
 
 return $cfg;
index dad3c5c..77dfc62 100644 (file)
@@ -42,7 +42,7 @@
                "wikimedia/cldr-plural-rule-parser": "1.0.0",
                "wikimedia/composer-merge-plugin": "1.4.1",
                "wikimedia/html-formatter": "1.0.2",
-               "wikimedia/ip-set": "2.0.1",
+               "wikimedia/ip-set": "2.1.0",
                "wikimedia/less.php": "1.8.0",
                "wikimedia/object-factory": "2.1.0",
                "wikimedia/password-blacklist": "0.1.4",
@@ -77,7 +77,7 @@
                "wikimedia/avro": "1.8.0",
                "wikimedia/testing-access-wrapper": "~1.0",
                "wmde/hamcrest-html-matchers": "^0.1.0",
-               "mediawiki/mediawiki-phan-config": "0.6.1",
+               "mediawiki/mediawiki-phan-config": "0.7.1",
                "symfony/yaml": "3.4.28",
                "johnkary/phpunit-speedtrap": "^1.0 | ^2.0"
        },
index 541541a..6ae4371 100644 (file)
@@ -1799,8 +1799,11 @@ class EditPage {
                } elseif ( !$status->isOK() ) {
                        # ...or the hook could be expecting us to produce an error
                        // FIXME this sucks, we should just use the Status object throughout
+                       if ( !$status->getErrors() ) {
+                               // Provide a fallback error message if none was set
+                               $status->fatal( 'hookaborted' );
+                       }
                        $this->hookError = $this->formatStatusErrors( $status );
-                       $status->fatal( 'hookaborted' );
                        $status->value = self::AS_HOOK_ERROR_EXPECTED;
                        return false;
                }
index 2cde173..38daab5 100644 (file)
@@ -2135,7 +2135,6 @@ function wfShellExec( $cmd, &$retval = null, $environ = [],
        }
 
        $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr'];
-       // @phan-suppress-next-line PhanTypeInvalidDimOffset
        $profileMethod = $options['profileMethod'] ?? wfGetCaller();
 
        try {
@@ -2200,7 +2199,6 @@ function wfShellWikiCmd( $script, array $parameters = [], array $options = [] )
        // Give site config file a chance to run the script in a wrapper.
        // The caller may likely want to call wfBasename() on $script.
        Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] );
-       // @phan-suppress-next-line PhanTypeInvalidDimOffset
        $cmd = [ $options['php'] ?? $wgPhpCli ];
        if ( isset( $options['wrapper'] ) ) {
                $cmd[] = $options['wrapper'];
@@ -2897,7 +2895,6 @@ function wfUnpack( $format, $data, $length = false ) {
        $result = unpack( $format, $data );
        Wikimedia\restoreWarnings();
 
-       // @phan-suppress-next-line PhanTypeComparisonFromArray Phan issue #3160
        if ( $result === false ) {
                // If it cannot extract the packed data.
                throw new MWException( "unpack could not unpack binary data" );
index ea90255..5d09e01 100644 (file)
@@ -109,7 +109,6 @@ class RevisionRenderer {
                        throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
                }
 
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $audience = $hints['audience']
                        ?? ( $forUser ? RevisionRecord::FOR_THIS_USER : RevisionRecord::FOR_PUBLIC );
 
@@ -123,7 +122,6 @@ class RevisionRenderer {
                        $options = ParserOptions::newCanonical( $forUser ?: 'canonical' );
                }
 
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $useMaster = $hints['use-master'] ?? false;
 
                $dbIndex = $useMaster
index 818a536..73f622a 100644 (file)
@@ -54,8 +54,10 @@ use Psr\Log\NullLogger;
 use RecentChange;
 use Revision;
 use RuntimeException;
+use StatusValue;
 use stdClass;
 use Title;
+use Traversable;
 use User;
 use WANObjectCache;
 use Wikimedia\Assert\Assert;
@@ -1876,6 +1878,125 @@ class RevisionStore
                return $rev;
        }
 
+       /**
+        * Construct a RevisionRecord instance for each row in $rows,
+        * and return them as an associative array indexed by revision ID.
+        * @param Traversable|array $rows the rows to construct revision records from
+        * @param array $options Supports the following options:
+        *               'slots' - whether metadata about revision slots should be
+        *               loaded immediately. Supports falsy or truthy value as well
+        *               as an explicit list of slot role names.
+        *               'content'- whether the actual content of the slots should be
+        *               preloaded. TODO: no supported yet.
+        * @param int $queryFlags
+        * @param Title|null $title
+        * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions
+        *                                         and an array of errors for the revisions failed to fetch.
+        */
+       public function newRevisionsFromBatch(
+               $rows,
+               array $options = [],
+               $queryFlags = 0,
+               Title $title = null
+       ) {
+               $result = new StatusValue();
+
+               $rowsByRevId = [];
+               $pageIds = [];
+               $titlesByPageId = [];
+               foreach ( $rows as $row ) {
+                       if ( isset( $rowsByRevId[$row->rev_id] ) ) {
+                               throw new InvalidArgumentException( "Duplicate rows in newRevisionsFromBatch {$row->rev_id}" );
+                       }
+                       if ( $title && $row->rev_page != $title->getArticleID() ) {
+                               throw new InvalidArgumentException(
+                                       "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
+                               );
+                       }
+                       $pageIds[] = $row->rev_page;
+                       $rowsByRevId[$row->rev_id] = $row;
+               }
+
+               if ( empty( $rowsByRevId ) ) {
+                       $result->setResult( true, [] );
+                       return $result;
+               }
+
+               // If the title is not supplied, batch-fetch Title objects.
+               if ( $title ) {
+                       $titlesByPageId[$title->getArticleID()] = $title;
+               } else {
+                       $pageIds = array_unique( $pageIds );
+                       foreach ( Title::newFromIDs( $pageIds ) as $t ) {
+                               $titlesByPageId[$t->getArticleID()] = $t;
+                       }
+               }
+
+               if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                       $result->setResult( true,
+                               array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
+                                       try {
+                                               return $this->newRevisionFromRow(
+                                                       $row,
+                                                       $queryFlags,
+                                                       $titlesByPageId[$row->rev_page]
+                                               );
+                                       } catch ( MWException $e ) {
+                                               $result->warning( 'internalerror', $e->getMessage() );
+                                               return null;
+                                       }
+                               }, $rowsByRevId )
+                       );
+                       return $result;
+               }
+
+               $slotQueryConds = [ 'slot_revision_id' => array_keys( $rowsByRevId ) ];
+               if ( is_array( $options['slots'] ) ) {
+                       $slotQueryConds['slot_role_id'] = array_map( function ( $slot_name ) {
+                               return $this->slotRoleStore->getId( $slot_name );
+                       }, $options['slots'] );
+               }
+
+               // TODO: Support optional fetching of the content
+               $queryInfo = self::getSlotsQueryInfo( [ 'content' ] );
+               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+               $slotRows = $db->select(
+                       $queryInfo['tables'],
+                       $queryInfo['fields'],
+                       $slotQueryConds,
+                       __METHOD__,
+                       [],
+                       $queryInfo['joins']
+               );
+
+               $slotRowsByRevId = [];
+               foreach ( $slotRows as $slotRow ) {
+                       $slotRowsByRevId[$slotRow->slot_revision_id][] = $slotRow;
+               }
+               $result->setResult( true, array_map( function ( $row ) use
+                       ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) {
+                               if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
+                                       $result->warning(
+                                               'internalerror',
+                                               "Couldn't find slots for rev {$row->rev_id}"
+                                       );
+                                       return null;
+                               }
+                               try {
+                                       return $this->newRevisionFromRowAndSlots(
+                                               $row,
+                                               $slotRowsByRevId[$row->rev_id],
+                                               $queryFlags,
+                                               $titlesByPageId[$row->rev_page]
+                                       );
+                               } catch ( MWException $e ) {
+                                       $result->warning( 'internalerror', $e->getMessage() );
+                                       return null;
+                               }
+               }, $rowsByRevId ) );
+               return $result;
+       }
+
        /**
         * Constructs a new MutableRevisionRecord based on the given associative array following
         * the MW1.29 convention for the Revision constructor.
index 385ccc9..db874f2 100644 (file)
@@ -95,6 +95,7 @@ class HistoryAction extends FormlessAction {
        private function preCacheMessages() {
                // Precache various messages
                if ( !isset( $this->message ) ) {
+                       $this->message = [];
                        $msgs = [ 'cur', 'last', 'pipe-separator' ];
                        foreach ( $msgs as $msg ) {
                                $this->message[$msg] = $this->msg( $msg )->escaped();
index d8134bb..0cd9806 100644 (file)
@@ -1308,8 +1308,15 @@ abstract class ApiBase extends ContextSource {
                                                }
                                                break;
                                        case 'limit':
+                                               // Must be a number or 'max'
+                                               if ( $value !== 'max' ) {
+                                                       $value = (int)$value;
+                                               }
+                                               if ( $multi ) {
+                                                       self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
+                                               }
                                                if ( !$parseLimit ) {
-                                                       // Don't do any validation whatsoever
+                                                       // Don't do min/max validation and don't parse 'max'
                                                        break;
                                                }
                                                if ( !isset( $paramSettings[self::PARAM_MAX] )
@@ -1320,21 +1327,16 @@ abstract class ApiBase extends ContextSource {
                                                                "MAX1 or MAX2 are not defined for the limit $encParamName"
                                                        );
                                                }
-                                               if ( $multi ) {
-                                                       self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
-                                               }
-                                               $min = $paramSettings[self::PARAM_MIN] ?? 0;
-                                               if ( $value == 'max' ) {
+                                               if ( $value === 'max' ) {
                                                        $value = $this->getMain()->canApiHighLimits()
                                                                ? $paramSettings[self::PARAM_MAX2]
                                                                : $paramSettings[self::PARAM_MAX];
                                                        $this->getResult()->addParsedLimit( $this->getModuleName(), $value );
                                                } else {
-                                                       $value = (int)$value;
                                                        $this->validateLimit(
                                                                $paramName,
                                                                $value,
-                                                               $min,
+                                                               $paramSettings[self::PARAM_MIN] ?? 0,
                                                                $paramSettings[self::PARAM_MAX],
                                                                $paramSettings[self::PARAM_MAX2]
                                                        );
index 1c7f63d..a5e7437 100644 (file)
@@ -103,6 +103,7 @@ class ApiExpandTemplates extends ApiBase {
                if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
                        $parser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS );
                        $dom = $parser->preprocessToDom( $params['text'] );
+                       // @phan-suppress-next-line PhanUndeclaredMethodInCallable
                        if ( is_callable( [ $dom, 'saveXML' ] ) ) {
                                // @phan-suppress-next-line PhanUndeclaredMethod
                                $xml = $dom->saveXML();
index efa4b04..a9fe258 100644 (file)
@@ -1708,7 +1708,7 @@ class ApiMain extends ApiBase {
         * @return string
         */
        protected function encodeRequestLogValue( $s ) {
-               static $table;
+               static $table = [];
                if ( !$table ) {
                        $chars = ';@$!*(),/:';
                        $numChars = strlen( $chars );
@@ -1914,6 +1914,10 @@ class ApiMain extends ApiBase {
                ];
        }
 
+       /**
+        * @inheritDoc
+        * @phan-param array{nolead?:bool,headerlevel?:int,tocnumber?:int[]} $options
+        */
        public function modifyHelp( array &$help, array $options, array &$tocData ) {
                // Wish PHP had an "array_insert_before". Instead, we have to manually
                // reindex the array to get 'permissions' in the right place.
index 6c1eb0f..d21f111 100644 (file)
@@ -326,7 +326,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
        /**
         * @param ApiPageSet $resultPageSet
         * @return void
-        * @suppress PhanTypeInvalidDimOffset
         */
        private function run( $resultPageSet = null ) {
                $this->params = $this->extractRequestParams( false );
index 7c92b35..90e5480 100644 (file)
@@ -512,6 +512,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                                        Parser::OT_PREPROCESS
                                );
                                $dom = $parser->preprocessToDom( $t );
+                               // @phan-suppress-next-line PhanUndeclaredMethodInCallable
                                if ( is_callable( [ $dom, 'saveXML' ] ) ) {
                                        // @phan-suppress-next-line PhanUndeclaredMethod
                                        $xml = $dom->saveXML();
index e9fd0a7..55fc061 100644 (file)
@@ -40,8 +40,8 @@
        "apihelp-main-param-action": "Qué acción se realizará.",
        "apihelp-main-param-format": "El formato de la salida.",
        "apihelp-main-param-maxlag": "Se puede usar el retardo máximo cuando se instala MediaWiki en un clúster replicado de base de datos. Para evitar acciones que causen más retardo en la replicación del sitio, este parámetro puede hacer que el cliente espere hasta que el retardo en la replicación sea menor que el valor especificado. En caso de retardo excesivo, se devuelve el código de error <samp>maxlag</samp> con un mensaje como <samp>Esperando a $host: $lag segundos de retardo</samp>.<br />Consulta [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: parámetro Maxlag]] para más información.",
-       "apihelp-main-param-smaxage": "Establece la cabecera HTTP <code>s-maxage</code> de control de caché a esta cantidad de segundos. Los errores nunca se almacenan en caché.",
-       "apihelp-main-param-maxage": "Establece la cabecera HTTP <code>max-age</code> de control de caché a esta cantidad de segundos. Los errores nunca se almacenan en la caché.",
+       "apihelp-main-param-smaxage": "Establece la cabecera HTTP <code>s-maxage</code> de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.",
+       "apihelp-main-param-maxage": "Establece la cabecera HTTP <code>max-age</code> de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.",
        "apihelp-main-param-assert": "Comprobar que el usuario haya iniciado sesión si el valor es <kbd>user</kbd> o si tiene el permiso de bot si es <kbd>bot</kbd>.",
        "apihelp-main-param-assertuser": "Verificar el usuario actual es el usuario nombrado.",
        "apihelp-main-param-requestid": "Cualquier valor dado aquí se incluirá en la respuesta. Se puede utilizar para distinguir solicitudes.",
        "apihelp-edit-param-text": "Contenido de la página.",
        "apihelp-edit-param-summary": "Editar resumen. Además de la sección del título cuando $1section=new y $1sectiontitle no están establecidos.",
        "apihelp-edit-param-tags": "Cambia las etiquetas para aplicarlas a la revisión.",
-       "apihelp-edit-param-minor": "Edición menor.",
+       "apihelp-edit-param-minor": "Marcar esta edición como menor.",
        "apihelp-edit-param-notminor": "Edición no menor.",
        "apihelp-edit-param-bot": "Marcar esta como una edición de bot.",
        "apihelp-edit-param-basetimestamp": "Marca de tiempo de la revisión base, usada para detectar conflictos de edición. Se puede obtener mediante [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]",
index 1a2442c..e7527d1 100644 (file)
@@ -376,7 +376,7 @@ abstract class AuthenticationRequest {
         * @return AuthenticationRequest
         */
        public static function __set_state( $data ) {
-               // @phan-suppress-next-line PhanTypeInstantiateAbstract
+               // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
                $ret = new static();
                foreach ( $data as $k => $v ) {
                        $ret->$k = $v;
index a010e83..e08b877 100644 (file)
@@ -99,7 +99,7 @@ abstract class AbstractRestriction implements Restriction {
         * @inheritDoc
         */
        public static function newFromRow( \stdClass $row ) {
-               // @phan-suppress-next-line PhanTypeInstantiateAbstract
+               // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
                return new static( $row->ir_ipb_id, $row->ir_value );
        }
 
index 0382d73..a48e191 100644 (file)
@@ -161,6 +161,7 @@ class ChangesList extends ContextSource {
         */
        private function preCacheMessages() {
                if ( !isset( $this->message ) ) {
+                       $this->message = [];
                        foreach ( [
                                'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history',
                                'semicolon-separator', 'pipe-separator' ] as $msg
index 09d0189..f3d3849 100644 (file)
@@ -161,6 +161,7 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                                                if ( is_array( $etcdResponse['config'] ) ) {
                                                        // Avoid having all servers expire cache keys at the same time
                                                        $expiry = microtime( true ) + $this->baseCacheTTL;
+                                                       // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                                                        $expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL;
                                                        $data = [
                                                                'config' => $etcdResponse['config'],
index f1df087..9fbb72c 100644 (file)
@@ -1102,7 +1102,7 @@ abstract class ContentHandler {
         * @param Revision|Content $undoafter Must be from an earlier revision than $undo
         * @param bool $undoIsLatest Set true if $undo is from the current revision (since 1.32)
         *
-        * @return mixed Content on success, false on failure
+        * @return Content|false Content on success, false on failure
         */
        public function getUndoContent( $current, $undo, $undoafter, $undoIsLatest = false ) {
                Assert::parameterType( Revision::class . '|' . Content::class, $current, '$current' );
index 6ebec1c..6fa40ea 100644 (file)
@@ -363,7 +363,6 @@ class DiffEngine {
                         */
                        $max = min( $this->m, $this->n );
                        for ( $forwardBound = 0; $forwardBound < $max
-                               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                && $this->from[$forwardBound] === $this->to[$forwardBound];
                                ++$forwardBound
                        ) {
index 9b1571f..5dd3e4c 100644 (file)
@@ -30,6 +30,10 @@ class ExportProgressFilter extends DumpFilter {
         */
        private $progress;
 
+       /**
+        * @param DumpOutput &$sink
+        * @param BackupDumper &$progress
+        */
        function __construct( &$sink, &$progress ) {
                parent::__construct( $sink );
                $this->progress = $progress;
index 8e3355c..84c0a61 100644 (file)
@@ -32,6 +32,7 @@ use Wikimedia\Rdbms\IDatabase;
  * in the wiki's own database. This is the most commonly used repository class.
  *
  * @ingroup FileRepo
+ * @method LocalFile|null newFile( $title, $time = false )
  */
 class LocalRepo extends FileRepo {
        /** @var callable */
index 96df29f..f61ca3b 100644 (file)
@@ -116,7 +116,6 @@ class RepoGroup {
         *                   be found.
         *   latest:         If true, load from the latest available data into File objects
         * @phan-param array{time?:mixed,ignoreRedirect?:bool,private?:bool,latest?:bool} $options
-        * @suppress PhanTypeInvalidDimOffset
         * @return File|bool False if title is not found
         */
        function findFile( $title, $options = [] ) {
index 4751a59..3090632 100644 (file)
@@ -1092,7 +1092,6 @@ class LocalFile extends File {
                array_shift( $urls ); // don't purge directory
 
                // Give media handler a chance to filter the file purge list
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                if ( !empty( $options['forThumbRefresh'] ) ) {
                        $handler = $this->getHandler();
                        if ( $handler ) {
index 137119d..21980b9 100644 (file)
@@ -126,10 +126,8 @@ class LocalFileMoveBatch {
        public function execute() {
                $repo = $this->file->repo;
                $status = $repo->newGood();
-               /** @var LocalFile $destFile */
                $destFile = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
                        ->newFile( $this->target );
-               '@phan-var LocalFile $destFile';
 
                $this->file->lock();
                $destFile->lock(); // quickly fail if destination is not available
index 41c0b3c..63e77ce 100644 (file)
@@ -29,8 +29,6 @@
  * The old name of autocomplete-data[-messages] was autocomplete[-messages] which is still
  * recognized but deprecated since MediaWiki 1.29 since it conflicts with how autocomplete is
  * used in HTMLTextField.
- *
- * @phan-file-suppress PhanTypeMismatchProperty This is doing weird things with mClass
  */
 class HTMLAutoCompleteSelectField extends HTMLTextField {
        protected $autocompleteData = [];
@@ -168,7 +166,6 @@ class HTMLAutoCompleteSelectField extends HTMLTextField {
 
                        $ret = $select->getHTML() . "<br />\n";
 
-                       // @phan-suppress-next-line PhanTypeMismatchDimEmpty
                        $this->mClass[] = 'mw-htmlform-hide-if';
                }
 
@@ -181,7 +178,6 @@ class HTMLAutoCompleteSelectField extends HTMLTextField {
                        }
                }
 
-               // @phan-suppress-next-line PhanTypeMismatchDimEmpty
                $this->mClass[] = 'mw-htmlform-autocomplete';
                $ret .= parent::getInputHTML( $valInSelect ? '' : $value );
                $this->mClass = $oldClass;
index 3885c03..d1c14ae 100644 (file)
@@ -103,7 +103,6 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
                $this->url = wfExpandUrl( $url, PROTO_HTTP );
                $this->parsedUrl = wfParseUrl( $this->url );
 
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $this->logger = $options['logger'] ?? new NullLogger();
 
                if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
index 6d1e242..8cfb605 100644 (file)
@@ -65,4 +65,10 @@ interface ImportableOldRevision {
         */
        public function getSha1Base36();
 
+       /**
+        * @since 1.34
+        * @return string[]
+        */
+       public function getTags();
+
 }
index ad62e16..821d6f6 100644 (file)
@@ -129,6 +129,11 @@ class ImportableOldRevisionImporter implements OldRevisionImporter {
                $revision->insertOn( $dbw );
                $changed = $page->updateIfNewerOn( $dbw, $revision );
 
+               $tags = $importableRevision->getTags();
+               if ( $tags !== [] ) {
+                       ChangeTags::addTags( $tags, null, $revision->getId() );
+               }
+
                if ( $changed !== false && $this->doUpdates ) {
                        $this->logger->debug( __METHOD__ . ": running updates\n" );
                        // countable/oldcountable stuff is handled in WikiImporter::finishImportPage
index e5f4b57..4be13b0 100644 (file)
@@ -114,7 +114,6 @@ class ImportableUploadRevisionImporter implements UploadRevisionImporter {
                                $user
                        );
                } else {
-                       '@phan-var LocalFile $file';
                        $flags = 0;
                        $status = $file->upload(
                                $source,
index e36d673..c29ba21 100644 (file)
@@ -144,6 +144,12 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
         */
        public $sha1base36 = false;
 
+       /**
+        * @since 1.34
+        * @var string[]
+        */
+       protected $tags = [];
+
        /**
         * @since 1.17
         * @var string
@@ -310,6 +316,14 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
                $this->sha1base36 = $sha1base36;
        }
 
+       /**
+        * @since 1.34
+        * @param string[] $tags
+        */
+       public function setTags( array $tags ) {
+               $this->tags = $tags;
+       }
+
        /**
         * @since 1.12.2
         * @param string $filename
@@ -509,6 +523,14 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
                return false;
        }
 
+       /**
+        * @since 1.34
+        * @return string[]
+        */
+       public function getTags() {
+               return $this->tags;
+       }
+
        /**
         * @since 1.17
         * @return string
index d349ff6..aa0d5ee 100644 (file)
        "config-apc": "[https://www.php.net/apc APC] está instalado",
        "config-apcu": "[https://www.php.net/apcu APCu] está instalado",
        "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] está instalado",
-       "config-no-cache-apcu": "<strong>Atención:</strong> no se pudo encontrar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nEl almacenamiento en caché de objetos no está activado.",
+       "config-no-cache-apcu": "<strong>Atención:</strong> no se pudo encontrar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nEl almacenamiento en antememoria de objetos no está activado.",
        "config-mod-security": "<strong>Advertencia:</strong> tu servidor web tiene activado [https://modsecurity.org/ mod_security]/mod_security2. Muchas de sus configuraciones comunes pueden causar problemas a MediaWiki u otro software que permita a los usuarios publicar contenido arbitrario. De ser posible, deberías desactivarlo. Si no, consulta la [https://modsecurity.org/documentation/ documentación de mod_security] o contacta con el administrador de tu servidor si encuentras errores aleatorios.",
        "config-diff3-bad": "GNU diff3 no se encuentra.",
        "config-git": "Se encontró el software de control de versiones Git: <code>$1</code>.",
        "config-cc-again": "Elegir otra vez...",
        "config-cc-not-chosen": "Elige la licencia Creative Commons que desees y haz clic en \"proceed\".",
        "config-advanced-settings": "Configuración avanzada",
-       "config-cache-options": "Configuración de la caché de objetos:",
+       "config-cache-options": "Configuración de la antememoria de objetos:",
        "config-cache-help": "El almacenamiento en caché de objetos se utiliza para mejorar la velocidad de MediaWiki mediante el almacenamiento en caché los datos usados más frecuentemente.\nA los sitios medianos y grandes se les recomienda que permitirlo. También es beneficioso para los sitios pequeños.",
        "config-cache-none": "Sin almacenamiento en caché (no se pierde ninguna funcionalidad, pero la velocidad puede resentirse en sitios grandes)",
-       "config-cache-accel": "Almacenamiento en caché de objetos PHP (APC, APCu o WinCache)",
+       "config-cache-accel": "Almacenamiento en antememoria de objetos PHP (APC, APCu o WinCache)",
        "config-cache-memcached": "Utilizar Memcached (necesita ser instalado y configurado aparte)",
        "config-memcached-servers": "Servidores Memcached:",
        "config-memcached-help": "Lista de direcciones IP que serán usadas por Memcached.\nDeben especificarse una por cada línea y especificar el puerto a utilizar. Por ejemplo:\n127.0.0.1:11211\n192.168.1.25:1234",
index 0c687f6..7a13bde 100644 (file)
@@ -62,7 +62,7 @@
        "config-page-existingwiki": "Bestaande wiki",
        "config-help-restart": "Wilt u alle opgeslagen gegevens die u hebt ingevoerd wissen en het installatieproces opnieuw starten?",
        "config-restart": "Ja, opnieuw starten",
-       "config-welcome": "=== Controle omgeving ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.",
+       "config-welcome": "=== Omgevingscontrole ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.",
        "config-welcome-section-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. U mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar uw keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoort u een [$2 exemplaar van de GNU General Public License] ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [https://www.gnu.org/copyleft/gpl.html lees de licentie online].",
        "config-sidebar": "* [https://www.mediawiki.org MediaWiki-thuispagina]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Gebruikershandleiding]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Beheerdershandleiding]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen]",
        "config-sidebar-readme": "Leesmij",
@@ -83,9 +83,9 @@
        "config-pcre-no-utf8": "<strong>Onherstelbare fout:</strong> de module PRCE van PHP lijkt te zijn gecompileerd zonder ondersteuning voor PCRE_UTF8.\nMediaWiki heeft ondersteuning voor UTF-8 nodig om correct te kunnen werken.",
        "config-memory-raised": "PHP's <code>memory_limit</code> is $1 en is verhoogd tot $2.",
        "config-memory-bad": "'''Waarschuwing:''' PHP's <code>memory_limit</code> is $1.\nDit is waarschijnlijk te laag.\nDe installatie kan mislukken!",
-       "config-apc": "[https://www.php.net/apc APC] is op dit moment geïnstalleerd",
+       "config-apc": "[https://www.php.net/apc APC] is geïnstalleerd",
        "config-apcu": "[https://www.php.net/apcu APCu] is geïnstalleerd",
-       "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is op dit moment geïnstalleerd",
+       "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is geïnstalleerd",
        "config-no-cache-apcu": "<strong>Waarschuwing:</strong> [https://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] of [https://www.iis.net/downloads/microsoft/wincache-extension WinCache] is niet aangetroffen.\nHet cachen van objecten is niet ingeschakeld.",
        "config-mod-security": "<strong>Waarschuwing:</strong> Uw webserver heeft de module [https://modsecurity.org/ mod_security]/mod_security2 ingeschakeld. Veel standaard instellingen hiervan zorgen voor problemen in combinatie met MediaWiki en andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nIndien mogelijk, zou deze moeten worden uitgeschakeld. Lees anders de [https://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.",
        "config-diff3-bad": "GNU diff3 niet aangetroffen. U kunt dit voorlopig negeren, maar bewerkingsconflicten kunnen vaker voorkomen.",
index 9d53a86..0e53038 100644 (file)
@@ -61,7 +61,6 @@ class MappedIterator extends FilterIterator {
                }
                parent::__construct( $baseIterator );
                $this->vCallback = $vCallback;
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $this->aCallback = $options['accept'] ?? null;
        }
 
index c1a796f..c333a5e 100644 (file)
@@ -137,7 +137,7 @@ class FSFileBackend extends FileBackendStore {
                        }
                }
 
-               return null;
+               return null; // invalid
        }
 
        /**
@@ -576,25 +576,23 @@ class FSFileBackend extends FileBackendStore {
        protected function doGetFileStat( array $params ) {
                $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
-                       return false; // invalid storage path
+                       return self::$RES_ERROR; // invalid storage path
                }
 
                $this->trapWarnings(); // don't trust 'false' if there were errors
                $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
                $hadError = $this->untrapWarnings();
 
-               if ( $stat ) {
+               if ( is_array( $stat ) ) {
                        $ct = new ConvertibleTimestamp( $stat['mtime'] );
 
                        return [
                                'mtime' => $ct->getTimestamp( TS_MW ),
                                'size' => $stat['size']
                        ];
-               } elseif ( !$hadError ) {
-                       return false; // file does not exist
-               } else {
-                       return self::UNKNOWN; // failure
                }
+
+               return $hadError ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        protected function doClearCache( array $paths = null ) {
@@ -610,7 +608,7 @@ class FSFileBackend extends FileBackendStore {
                $exists = is_dir( $dir );
                $hadError = $this->untrapWarnings();
 
-               return $hadError ? self::UNKNOWN : $exists;
+               return $hadError ? self::$RES_ERROR : $exists;
        }
 
        /**
@@ -624,18 +622,27 @@ class FSFileBackend extends FileBackendStore {
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+               $this->trapWarnings(); // don't trust 'false' if there were errors
                $exists = is_dir( $dir );
-               if ( !$exists ) {
-                       $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+               $isReadable = $exists ? is_readable( $dir ) : false;
+               $hadError = $this->untrapWarnings();
 
-                       return []; // nothing under this dir
-               } elseif ( !is_readable( $dir ) ) {
-                       $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+               if ( $isReadable ) {
+                       return new FSFileBackendDirList( $dir, $params );
+               } elseif ( $exists ) {
+                       $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'" );
 
-                       return self::UNKNOWN; // bad permissions?
-               }
+                       return self::$RES_ERROR; // bad permissions?
+               } elseif ( $hadError ) {
+                       $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'" );
+
+                       return self::$RES_ERROR;
+               } else {
+                       $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'" );
 
-               return new FSFileBackendDirList( $dir, $params );
+                       return []; // nothing under this dir
+               }
        }
 
        /**
@@ -649,18 +656,27 @@ class FSFileBackend extends FileBackendStore {
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+               $this->trapWarnings(); // don't trust 'false' if there were errors
                $exists = is_dir( $dir );
-               if ( !$exists ) {
-                       $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+               $isReadable = $exists ? is_readable( $dir ) : false;
+               $hadError = $this->untrapWarnings();
 
-                       return []; // nothing under this dir
-               } elseif ( !is_readable( $dir ) ) {
-                       $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+               if ( $exists && $isReadable ) {
+                       return new FSFileBackendFileList( $dir, $params );
+               } elseif ( $exists ) {
+                       $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'\n" );
 
-                       return self::UNKNOWN; // bad permissions?
-               }
+                       return self::$RES_ERROR; // bad permissions?
+               } elseif ( $hadError ) {
+                       $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'\n" );
 
-               return new FSFileBackendFileList( $dir, $params );
+                       return self::$RES_ERROR;
+               } else {
+                       $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'\n" );
+
+                       return []; // nothing under this dir
+               }
        }
 
        protected function doGetLocalReferenceMulti( array $params ) {
@@ -668,10 +684,21 @@ class FSFileBackend extends FileBackendStore {
 
                foreach ( $params['srcs'] as $src ) {
                        $source = $this->resolveToFSPath( $src );
-                       if ( $source === null || !is_file( $source ) ) {
-                               $fsFiles[$src] = null; // invalid path or file does not exist
-                       } else {
+                       if ( $source === null ) {
+                               $fsFiles[$src] = self::$RES_ERROR; // invalid path
+                               continue;
+                       }
+
+                       $this->trapWarnings(); // don't trust 'false' if there were errors
+                       $isFile = is_file( $source ); // regular files only
+                       $hadError = $this->untrapWarnings();
+
+                       if ( $isFile ) {
                                $fsFiles[$src] = new FSFile( $source );
+                       } elseif ( $hadError ) {
+                               $fsFiles[$src] = self::$RES_ERROR;
+                       } else {
+                               $fsFiles[$src] = self::$RES_ABSENT;
                        }
                }
 
@@ -684,26 +711,31 @@ class FSFileBackend extends FileBackendStore {
                foreach ( $params['srcs'] as $src ) {
                        $source = $this->resolveToFSPath( $src );
                        if ( $source === null ) {
-                               $tmpFiles[$src] = null; // invalid path
+                               $tmpFiles[$src] = self::$RES_ERROR; // invalid path
+                               continue;
+                       }
+                       // Create a new temporary file with the same extension...
+                       $ext = FileBackend::extensionFromPath( $src );
+                       $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
+                       if ( !$tmpFile ) {
+                               $tmpFiles[$src] = self::$RES_ERROR;
+                               continue;
+                       }
+
+                       $tmpPath = $tmpFile->getPath();
+                       // Copy the source file over the temp file
+                       $this->trapWarnings();
+                       $isFile = is_file( $source ); // regular files only
+                       $copySuccess = $isFile ? copy( $source, $tmpPath ) : false;
+                       $hadError = $this->untrapWarnings();
+
+                       if ( $copySuccess ) {
+                               $this->chmod( $tmpPath );
+                               $tmpFiles[$src] = $tmpFile;
+                       } elseif ( $hadError ) {
+                               $tmpFiles[$src] = self::$RES_ERROR; // copy failed
                        } else {
-                               // Create a new temporary file with the same extension...
-                               $ext = FileBackend::extensionFromPath( $src );
-                               $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
-                               if ( !$tmpFile ) {
-                                       $tmpFiles[$src] = null;
-                               } else {
-                                       $tmpPath = $tmpFile->getPath();
-                                       // Copy the source file over the temp file
-                                       $this->trapWarnings();
-                                       $ok = copy( $source, $tmpPath );
-                                       $this->untrapWarnings();
-                                       if ( !$ok ) {
-                                               $tmpFiles[$src] = null;
-                                       } else {
-                                               $this->chmod( $tmpPath );
-                                               $tmpFiles[$src] = $tmpFile;
-                                       }
-                               }
+                               $tmpFiles[$src] = self::$RES_ABSENT;
                        }
                }
 
index 428fec6..6ab1707 100644 (file)
@@ -131,8 +131,28 @@ abstract class FileBackend implements LoggerAwareInterface {
        const ATTR_METADATA = 2; // files can be stored with metadata key/values
        const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
 
-       /** @var null Idiom for "could not determine due to I/O errors" */
-       const UNKNOWN = null;
+       /** @var false Idiom for "no info; non-existant file" (since 1.34) */
+       const STAT_ABSENT = false;
+
+       /** @var null Idiom for "no info; I/O errors" (since 1.34) */
+       const STAT_ERROR = null;
+       /** @var null Idiom for "no file/directory list; I/O errors" (since 1.34) */
+       const LIST_ERROR = null;
+       /** @var null Idiom for "no temp URL; not supported or I/O errors" (since 1.34) */
+       const TEMPURL_ERROR = null;
+       /** @var null Idiom for "existence unknown; I/O errors" (since 1.34) */
+       const EXISTENCE_ERROR = null;
+
+       /** @var false Idiom for "no timestamp; missing file or I/O errors" (since 1.34) */
+       const TIMESTAMP_FAIL = false;
+       /** @var false Idiom for "no content; missing file or I/O errors" (since 1.34) */
+       const CONTENT_FAIL = false;
+       /** @var false Idiom for "no metadata; missing file or I/O errors" (since 1.34) */
+       const XATTRS_FAIL = false;
+       /** @var false Idiom for "no size; missing file or I/O errors" (since 1.34) */
+       const SIZE_FAIL = false;
+       /** @var false Idiom for "no SHA1 hash; missing file or I/O errors" (since 1.34) */
+       const SHA1_FAIL = false;
 
        /**
         * Create a new backend instance from configuration.
@@ -157,9 +177,9 @@ abstract class FileBackend implements LoggerAwareInterface {
         *      Allowed values are "implicit", "explicit" and "off".
         *   - concurrency : How many file operations can be done in parallel.
         *   - tmpDirectory : Directory to use for temporary files.
-        *   - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if tmpDirectory is
-        *      not set. If both are unset or null, then the backend will try to discover a usable
-        *      temporary directory.
+        *   - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if
+        *      tmpDirectory is not set. If both are unset or null, then the backend will
+        *      try to discover a usable temporary directory.
         *   - obResetFunc : alternative callback to clear the output buffer
         *   - streamMimeFunc : alternative method to determine the content type from the path
         *   - logger : Optional PSR logger object.
@@ -946,20 +966,29 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Check if a file exists at a storage path in the backend.
         * This returns false if only a directory exists at the path.
         *
+        * Callers that only care if a file is readily accessible can use non-strict
+        * comparisons on the result. If "does not exist" and "existence is unknown"
+        * must be distinguished, then strict comparisons to true/null should be used.
+        *
+        * @see FileBackend::EXISTENCE_ERROR
+        * @see FileBackend::directoryExists()
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return bool|null Returns null on failure
+        * @return bool|null Whether the file exists or null (I/O error)
         */
        abstract public function fileExists( array $params );
 
        /**
         * Get the last-modified timestamp of the file at a storage path.
         *
+        * @see FileBackend::TIMESTAMP_FAIL
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return string|bool TS_MW timestamp or false on failure
+        * @return string|false TS_MW timestamp or false (missing file or I/O error)
         */
        abstract public function getFileTimestamp( array $params );
 
@@ -967,22 +996,22 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Get the contents of a file at a storage path in the backend.
         * This should be avoided for potentially large files.
         *
+        * @see FileBackend::CONTENT_FAIL
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return string|bool Returns false on failure
+        * @return string|false Content string or false (missing file or I/O error)
         */
        final public function getFileContents( array $params ) {
-               $contents = $this->getFileContentsMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
+               $contents = $this->getFileContentsMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
 
                return $contents[$params['src']];
        }
 
        /**
         * Like getFileContents() except it takes an array of storage paths
-        * and returns a map of storage paths to strings (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
+        * and returns an order preserved map of storage paths to their content.
         *
         * @see FileBackend::getFileContents()
         *
@@ -990,7 +1019,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         *   - srcs        : list of source storage paths
         *   - latest      : use the latest available data
         *   - parallelize : try to do operations in parallel when possible
-        * @return array Map of (path name => string or false on failure)
+        * @return string[]|false[] Map of (path name => file content or false on failure)
         * @since 1.20
         */
        abstract public function getFileContentsMulti( array $params );
@@ -1006,11 +1035,13 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Use FileBackend::hasFeatures() to check how well this is supported.
         *
+        * @see FileBackend::XATTRS_FAIL
+        *
         * @param array $params
         * $params include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return array|bool Returns false on failure
+        * @return array|false File metadata array or false (missing file or I/O error)
         * @since 1.23
         */
        abstract public function getFileXAttributes( array $params );
@@ -1018,10 +1049,12 @@ abstract class FileBackend implements LoggerAwareInterface {
        /**
         * Get the size (bytes) of a file at a storage path in the backend.
         *
+        * @see FileBackend::SIZE_FAIL
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return int|bool Returns false on failure
+        * @return int|false File size in bytes or false (missing file or I/O error)
         */
        abstract public function getFileSize( array $params );
 
@@ -1033,36 +1066,41 @@ abstract class FileBackend implements LoggerAwareInterface {
         *   - size   : the file size (bytes)
         * Additional values may be included for internal use only.
         *
+        * @see FileBackend::STAT_ABSENT
+        * @see FileBackend::STAT_ERROR
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return array|bool|null Returns null on failure
+        * @return array|false|null Attribute map, false (missing file), or null (I/O error)
         */
        abstract public function getFileStat( array $params );
 
        /**
-        * Get a SHA-1 hash of the file at a storage path in the backend.
+        * Get a SHA-1 hash of the content of the file at a storage path in the backend.
+        *
+        * @see FileBackend::SHA1_FAIL
         *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return string|bool Hash string or false on failure
+        * @return string|false Hash string or false (missing file or I/O error)
         */
        abstract public function getFileSha1Base36( array $params );
 
        /**
-        * Get the properties of the file at a storage path in the backend.
+        * Get the properties of the content of the file at a storage path in the backend.
         * This gives the result of FSFile::getProps() on a local copy of the file.
         *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return array Returns FSFile::placeholderProps() on failure
+        * @return array Properties map; FSFile::placeholderProps() if file missing or on I/O error
         */
        abstract public function getFileProps( array $params );
 
        /**
-        * Stream the file at a storage path in the backend.
+        * Stream the content of the file at a storage path in the backend.
         *
         * If the file does not exists, an HTTP 404 error will be given.
         * Appropriate HTTP headers (Status, Content-Type, Content-Length)
@@ -1083,34 +1121,36 @@ abstract class FileBackend implements LoggerAwareInterface {
        abstract public function streamFile( array $params );
 
        /**
-        * Returns a file system file, identical to the file at a storage path.
+        * Returns a file system file, identical in content to the file at a storage path.
         * The file returned is either:
-        *   - a) A local copy of the file at a storage path in the backend.
+        *   - a) A TempFSFile local copy of the file at a storage path in the backend.
         *        The temporary copy will have the same extension as the source.
-        *   - b) An original of the file at a storage path in the backend.
-        * Temporary files may be purged when the file object falls out of scope.
+        *        Temporary files may be purged when the file object falls out of scope.
+        *   - b) An FSFile pointing to the original file at a storage path in the backend.
+        *        This is applicable for backends layered directly on top of file systems.
         *
-        * Write operations should *never* be done on this file as some backends
-        * may do internal tracking or may be instances of FileBackendMultiWrite.
-        * In that latter case, there are copies of the file that must stay in sync.
-        * Additionally, further calls to this function may return the same file.
+        * Never modify the returned file since it might be the original, it might be shared
+        * among multiple callers of this method, or the backend might internally keep FSFile
+        * references for deferred operations.
         *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return FSFile|null Returns null on failure
+        * @return FSFile|null Local file copy or null (missing file or I/O error)
         */
        final public function getLocalReference( array $params ) {
-               $fsFiles = $this->getLocalReferenceMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
+               $fsFiles = $this->getLocalReferenceMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
 
                return $fsFiles[$params['src']];
        }
 
        /**
-        * Like getLocalReference() except it takes an array of storage paths
-        * and returns a map of storage paths to FSFile objects (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
+        * Like getLocalReference() except it takes an array of storage paths and
+        * yields an order-preserved map of storage paths to temporary local file copies.
+        *
+        * Never modify the returned files since they might be originals, they might be shared
+        * among multiple callers of this method, or the backend might internally keep FSFile
+        * references for deferred operations.
         *
         * @see FileBackend::getLocalReference()
         *
@@ -1128,22 +1168,24 @@ abstract class FileBackend implements LoggerAwareInterface {
         * The temporary copy will have the same file extension as the source.
         * Temporary files may be purged when the file object falls out of scope.
         *
+        * Multiple calls to this method for the same path will create new copies.
+        *
         * @param array $params Parameters include:
         *   - src    : source storage path
         *   - latest : use the latest available data
-        * @return TempFSFile|null Returns null on failure
+        * @return TempFSFile|null Temporary local file copy or null (missing file or I/O error)
         */
        final public function getLocalCopy( array $params ) {
-               $tmpFiles = $this->getLocalCopyMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
+               $tmpFiles = $this->getLocalCopyMulti( [ 'srcs' => [ $params['src'] ] ] + $params );
 
                return $tmpFiles[$params['src']];
        }
 
        /**
-        * Like getLocalCopy() except it takes an array of storage paths and
-        * returns a map of storage paths to TempFSFile objects (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
+        * Like getLocalCopy() except it takes an array of storage paths and yields
+        * an order preserved-map of storage paths to temporary local file copies.
+        *
+        * Multiple calls to this method for the same path will create new copies.
         *
         * @see FileBackend::getLocalCopy()
         *
@@ -1166,10 +1208,12 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Otherwise, one would need to use getLocalReference(), which involves loading
         * the entire file on to local disk.
         *
+        * @see FileBackend::TEMPURL_ERROR
+        *
         * @param array $params Parameters include:
         *   - src : source storage path
         *   - ttl : lifetime (seconds) if pre-authenticated; default is 1 day
-        * @return string|null
+        * @return string|null URL or null (not supported or I/O error)
         * @since 1.21
         */
        abstract public function getFileHttpUrl( array $params );
@@ -1191,11 +1235,12 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Storage backends with eventual consistency might return stale data.
         *
+        * @see FileBackend::EXISTENCE_ERROR
         * @see FileBackend::clean()
         *
         * @param array $params Parameters include:
         *   - dir : storage directory
-        * @return bool|null Whether a directory exists or null on failure
+        * @return bool|null Whether a directory exists or null (I/O error)
         * @since 1.20
         */
        abstract public function directoryExists( array $params );
@@ -1213,12 +1258,13 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
         * @see FileBackend::directoryExists()
         *
         * @param array $params Parameters include:
         *   - dir     : storage directory
         *   - topOnly : only return direct child dirs of the directory
-        * @return Traversable|array|null Directory list enumerator null on failure
+        * @return Traversable|array|null Directory list enumerator or null (initial I/O error)
         * @since 1.20
         */
        abstract public function getDirectoryList( array $params );
@@ -1231,11 +1277,12 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
         * @see FileBackend::directoryExists()
         *
         * @param array $params Parameters include:
         *   - dir : storage directory
-        * @return Traversable|array|null Directory list enumerator or null on failure
+        * @return Traversable|array|null Directory list enumerator or null (initial I/O error)
         * @since 1.20
         */
        final public function getTopDirectoryList( array $params ) {
@@ -1254,11 +1301,13 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
+        *
         * @param array $params Parameters include:
         *   - dir        : storage directory
         *   - topOnly    : only return direct child files of the directory (since 1.20)
         *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
-        * @return Traversable|array|null File list enumerator or null on failure
+        * @return Traversable|array|null File list enumerator or null (initial I/O error)
         */
        abstract public function getFileList( array $params );
 
@@ -1270,6 +1319,8 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * Failures during iteration can result in FileBackendError exceptions (since 1.22).
         *
+        * @see FileBackend::LIST_ERROR
+        *
         * @param array $params Parameters include:
         *   - dir        : storage directory
         *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
@@ -1360,7 +1411,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * @param int|string $type LockManager::LOCK_* constant or "mixed"
         * @param StatusValue $status StatusValue to update on lock/unlock
         * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
-        * @return ScopedLock|null Returns null on failure
+        * @return ScopedLock|null RAII-style self-unlocking lock or null on failure
         */
        final public function getScopedFileLocks(
                array $paths, $type, StatusValue $status, $timeout = 0
@@ -1389,7 +1440,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * @param array $ops List of file operations to FileBackend::doOperations()
         * @param StatusValue $status StatusValue to update on lock/unlock
-        * @return ScopedLock|null
+        * @return ScopedLock|null RAII-style self-unlocking lock or null on failure
         * @since 1.20
         */
        abstract public function getScopedLocksForOps( array $ops, StatusValue $status );
@@ -1487,7 +1538,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * Returns null if the path is not of the format of a valid storage path.
         *
         * @param string $storagePath
-        * @return string|null
+        * @return string|null Normalized storage path or null on failure
         */
        final public static function normalizeStoragePath( $storagePath ) {
                list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
@@ -1509,7 +1560,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * "mwstore://backend/container/...", or null if there is no parent.
         *
         * @param string $storagePath
-        * @return string|null
+        * @return string|null Parent storage path or null on failure
         */
        final public static function parentStoragePath( $storagePath ) {
                $storagePath = dirname( $storagePath );
@@ -1582,7 +1633,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         * This uses the same traversal protection as Title::secureAndSplit().
         *
         * @param string $path Storage path relative to a container
-        * @return string|null
+        * @return string|null Normalized container path or null on failure
         */
        final protected static function normalizeContainerPath( $path ) {
                // Normalize directory separators
index 9bfb5c5..cbfd76e 100644 (file)
@@ -248,7 +248,7 @@ class FileBackendMultiWrite extends FileBackend {
                        $masterBackend = $this->backends[$this->masterIndex];
                        $masterParams = $this->substOpPaths( $params, $masterBackend );
                        $masterStat = $masterBackend->getFileStat( $masterParams );
-                       if ( $masterStat === self::UNKNOWN ) {
+                       if ( $masterStat === self::STAT_ERROR ) {
                                $status->fatal( 'backend-fail-stat', $path );
                                continue;
                        }
@@ -357,7 +357,7 @@ class FileBackendMultiWrite extends FileBackend {
                        $masterParams = $this->substOpPaths( $params, $masterBackend );
                        $masterPath = $masterParams['src'];
                        $masterStat = $masterBackend->getFileStat( $masterParams );
-                       if ( $masterStat === self::UNKNOWN ) {
+                       if ( $masterStat === self::STAT_ERROR ) {
                                $status->fatal( 'backend-fail-stat', $path );
                                $this->logger->error( "$fname: file '$masterPath' is not available" );
                                continue;
@@ -379,7 +379,7 @@ class FileBackendMultiWrite extends FileBackend {
                                $cloneParams = $this->substOpPaths( $params, $cloneBackend );
                                $clonePath = $cloneParams['src'];
                                $cloneStat = $cloneBackend->getFileStat( $cloneParams );
-                               if ( $cloneStat === self::UNKNOWN ) {
+                               if ( $cloneStat === self::STAT_ERROR ) {
                                        $status->fatal( 'backend-fail-stat', $path );
                                        $this->logger->error( "$fname: file '$clonePath' is not available" );
                                        continue;
@@ -501,7 +501,7 @@ class FileBackendMultiWrite extends FileBackend {
         *
         * @param array|string $paths List of paths or single string path
         * @param FileBackendStore $backend
-        * @return array|string
+        * @return string[]|string
         */
        protected function substPaths( $paths, FileBackendStore $backend ) {
                return preg_replace(
@@ -515,12 +515,13 @@ class FileBackendMultiWrite extends FileBackend {
         * Substitute the backend of internal storage paths with the proxy backend's name
         *
         * @param array|string $paths List of paths or single string path
-        * @return array|string
+        * @param FileBackendStore $backend internal storage backend
+        * @return string[]|string
         */
-       protected function unsubstPaths( $paths ) {
+       protected function unsubstPaths( $paths, FileBackendStore $backend ) {
                return preg_replace(
-                       '!^mwstore://([^/]+)!',
-                       StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
+                       '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!',
+                       StringUtils::escapeRegexReplacement( "mwstore://{$this->name}/" ),
                        $paths // string or array
                );
        }
@@ -674,7 +675,7 @@ class FileBackendMultiWrite extends FileBackend {
 
                $contents = []; // (path => FSFile) mapping using the proxy backend's name
                foreach ( $contentsM as $path => $data ) {
-                       $contents[$this->unsubstPaths( $path )] = $data;
+                       $contents[$this->unsubstPaths( $path, $this->backends[$index] )] = $data;
                }
 
                return $contents;
@@ -709,7 +710,7 @@ class FileBackendMultiWrite extends FileBackend {
 
                $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
                foreach ( $fsFilesM as $path => $fsFile ) {
-                       $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
+                       $fsFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $fsFile;
                }
 
                return $fsFiles;
@@ -723,7 +724,7 @@ class FileBackendMultiWrite extends FileBackend {
 
                $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
                foreach ( $tempFilesM as $path => $tempFile ) {
-                       $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
+                       $tempFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $tempFile;
                }
 
                return $tempFiles;
@@ -784,8 +785,14 @@ class FileBackendMultiWrite extends FileBackend {
                $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
                // Get the paths under the proxy backend's name
                $pbPaths = [
-                       LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
-                       LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
+                       LockManager::LOCK_UW => $this->unsubstPaths(
+                               $paths[LockManager::LOCK_UW],
+                               $this->backends[$this->masterIndex]
+                       ),
+                       LockManager::LOCK_EX => $this->unsubstPaths(
+                               $paths[LockManager::LOCK_EX],
+                               $this->backends[$this->masterIndex]
+                       )
                ];
 
                // Actually acquire the locks
index e637565..f2c07e8 100644 (file)
@@ -59,6 +59,16 @@ abstract class FileBackendStore extends FileBackend {
        const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
        const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
 
+       /** @var false Idiom for "no result due to missing file" (since 1.34) */
+       protected static $RES_ABSENT = false;
+       /** @var null Idiom for "no result due to I/O errors" (since 1.34) */
+       protected static $RES_ERROR = null;
+
+       /** @var string File does not exist according to a normal stat query */
+       protected static $ABSENT_NORMAL = 'FNE-N';
+       /** @var string File does not exist according to a "latest"-mode stat query */
+       protected static $ABSENT_LATEST = 'FNE-L';
+
        /**
         * @see FileBackend::__construct()
         * Additional $config params include:
@@ -91,9 +101,10 @@ abstract class FileBackendStore extends FileBackend {
        }
 
        /**
-        * Check if a file can be created or changed at a given storage path.
-        * FS backends should check if the parent directory exists, files can be
-        * written under it, and that any file already there is writable.
+        * Check if a file can be created or changed at a given storage path in the backend
+        *
+        * FS backends should check that the parent directory exists, files can be written
+        * under it, and that any file already there is both readable and writable.
         * Backends using key/value stores should check if the container exists.
         *
         * @param string $storagePath
@@ -122,6 +133,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function createInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
                        $status = $this->newStatus( 'backend-fail-maxsize',
                                $params['dst'], $this->maxFileSizeInternal() );
@@ -164,6 +176,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function storeInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
                        $status = $this->newStatus( 'backend-fail-maxsize',
                                $params['dst'], $this->maxFileSizeInternal() );
@@ -207,6 +220,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function copyInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $status = $this->doCopyInternal( $params );
                $this->clearCache( [ $params['dst'] ] );
                if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
@@ -240,6 +254,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function deleteInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $status = $this->doDeleteInternal( $params );
                $this->clearCache( [ $params['src'] ] );
                $this->deleteFileCache( $params['src'] ); // persistent cache
@@ -275,6 +290,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function moveInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $status = $this->doMoveInternal( $params );
                $this->clearCache( [ $params['src'], $params['dst'] ] );
                $this->deleteFileCache( $params['src'] ); // persistent cache
@@ -322,6 +338,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function describeInternal( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                if ( count( $params['headers'] ) ) {
                        $status = $this->doDescribeInternal( $params );
                        $this->clearCache( [ $params['src'] ] );
@@ -617,54 +634,71 @@ abstract class FileBackendStore extends FileBackend {
        final public function fileExists( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $stat = $this->getFileStat( $params );
+               if ( is_array( $stat ) ) {
+                       return true;
+               }
 
-               return ( $stat === self::UNKNOWN ) ? self::UNKNOWN : (bool)$stat;
+               return ( $stat === self::$RES_ABSENT ) ? false : self::EXISTENCE_ERROR;
        }
 
        final public function getFileTimestamp( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $stat = $this->getFileStat( $params );
+               if ( is_array( $stat ) ) {
+                       return $stat['mtime'];
+               }
 
-               return $stat ? $stat['mtime'] : false;
+               return self::TIMESTAMP_FAIL; // all failure cases
        }
 
        final public function getFileSize( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $stat = $this->getFileStat( $params );
+               if ( is_array( $stat ) ) {
+                       return $stat['size'];
+               }
 
-               return $stat ? $stat['size'] : false;
+               return self::SIZE_FAIL; // all failure cases
        }
 
        final public function getFileStat( array $params ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $path = self::normalizeStoragePath( $params['src'] );
                if ( $path === null ) {
-                       return false; // invalid storage path
+                       return self::STAT_ERROR; // invalid storage path
                }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
-               $latest = !empty( $params['latest'] ); // use latest data?
-               $requireSHA1 = !empty( $params['requireSHA1'] ); // require SHA-1 if file exists?
+               // Whether to bypass cache except for process cache entries loaded directly from
+               // high consistency backend queries (caller handles any cache flushing and locking)
+               $latest = !empty( $params['latest'] );
+               // Whether to ignore cache entries missing the SHA-1 field for existing files
+               $requireSHA1 = !empty( $params['requireSHA1'] );
 
+               $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
+               // Load the persistent stat cache into process cache if needed
                if ( !$latest ) {
-                       $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
-                       // Note that some backends, like SwiftFileBackend, sometimes set file stat process
-                       // cache entries from mass object listings that do not include the SHA-1. In that
-                       // case, loading the persistent stat cache will likely yield the SHA-1.
                        if (
-                               $stat === self::UNKNOWN ||
+                               // File stat is not in process cache
+                               $stat === null ||
+                               // Key/value store backends might opportunistically set file stat process
+                               // cache entries from object listings that do not include the SHA-1. In that
+                               // case, loading the persistent stat cache will likely yield the SHA-1.
                                ( $requireSHA1 && is_array( $stat ) && !isset( $stat['sha1'] ) )
                        ) {
-                               $this->primeFileCache( [ $path ] ); // check persistent cache
+                               $this->primeFileCache( [ $path ] );
+                               // Get any newly process-cached entry
+                               $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
                        }
                }
 
-               $stat = $this->cheapCache->getField( $path, 'stat', self::CACHE_TTL );
-               // If we want the latest data, check that this cached
-               // value was in fact fetched with the latest available data.
                if ( is_array( $stat ) ) {
                        if (
                                ( !$latest || $stat['latest'] ) &&
@@ -672,42 +706,90 @@ abstract class FileBackendStore extends FileBackend {
                        ) {
                                return $stat;
                        }
-               } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ], true ) ) {
-                       if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
-                               return false;
+               } elseif ( $stat === self::$ABSENT_LATEST ) {
+                       return self::STAT_ABSENT;
+               } elseif ( $stat === self::$ABSENT_NORMAL ) {
+                       if ( !$latest ) {
+                               return self::STAT_ABSENT;
                        }
                }
 
+               // Load the file stat from the backend and update caches
                $stat = $this->doGetFileStat( $params );
+               $this->ingestFreshFileStats( [ $path => $stat ], $latest );
 
-               if ( is_array( $stat ) ) { // file exists
-                       // Strongly consistent backends can automatically set "latest"
-                       $stat['latest'] = $stat['latest'] ?? $latest;
-                       $this->cheapCache->setField( $path, 'stat', $stat );
-                       $this->setFileCache( $path, $stat ); // update persistent cache
-                       if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                               $this->cheapCache->setField( $path, 'sha1',
-                                       [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
-                       }
-                       if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
-                               $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
-                               $this->cheapCache->setField( $path, 'xattr',
-                                       [ 'map' => $stat['xattr'], 'latest' => $latest ] );
+               if ( is_array( $stat ) ) {
+                       return $stat;
+               }
+
+               return ( $stat === self::$RES_ERROR ) ? self::STAT_ERROR : self::STAT_ABSENT;
+       }
+
+       /**
+        * Ingest file stat entries that just came from querying the backend (not cache)
+        *
+        * @param array[]|bool[]|null[] $stats Map of (path => doGetFileStat() stype result)
+        * @param bool $latest Whether doGetFileStat()/doGetFileStatMulti() had the 'latest' flag
+        * @return bool Whether all files have non-error stat replies
+        */
+       final protected function ingestFreshFileStats( array $stats, $latest ) {
+               $success = true;
+
+               foreach ( $stats as $path => $stat ) {
+                       if ( is_array( $stat ) ) {
+                               // Strongly consistent backends might automatically set this flag
+                               $stat['latest'] = $stat['latest'] ?? $latest;
+
+                               $this->cheapCache->setField( $path, 'stat', $stat );
+                               if ( isset( $stat['sha1'] ) ) {
+                                       // Some backends store the SHA-1 hash as metadata
+                                       $this->cheapCache->setField(
+                                               $path,
+                                               'sha1',
+                                               [ 'hash' => $stat['sha1'], 'latest' => $latest ]
+                                       );
+                               }
+                               if ( isset( $stat['xattr'] ) ) {
+                                       // Some backends store custom headers/metadata
+                                       $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
+                                       $this->cheapCache->setField(
+                                               $path,
+                                               'xattr',
+                                               [ 'map' => $stat['xattr'], 'latest' => $latest ]
+                                       );
+                               }
+                               // Update persistent cache (@TODO: set all entries in one batch)
+                               $this->setFileCache( $path, $stat );
+                       } elseif ( $stat === self::$RES_ABSENT ) {
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'stat',
+                                       $latest ? self::$ABSENT_LATEST : self::$ABSENT_NORMAL
+                               );
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'xattr',
+                                       [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
+                               );
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'sha1',
+                                       [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
+                               );
+                               $this->logger->debug(
+                                       __METHOD__ . ': File {path} does not exist',
+                                       [ 'path' => $path ]
+                               );
+                       } else {
+                               $success = false;
+                               $this->logger->error(
+                                       __METHOD__ . ': Could not stat file {path}',
+                                       [ 'path' => $path ]
+                               );
                        }
-               } elseif ( $stat === false ) { // file does not exist
-                       $this->cheapCache->setField( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
-                       $this->cheapCache->setField( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] );
-                       $this->cheapCache->setField( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] );
-                       $this->logger->debug( __METHOD__ . ': File {path} does not exist', [
-                               'path' => $path,
-                       ] );
-               } else { // an error occurred
-                       $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [
-                               'path' => $path,
-                       ] );
                }
 
-               return $stat;
+               return $success;
        }
 
        /**
@@ -722,6 +804,11 @@ abstract class FileBackendStore extends FileBackend {
 
                $params = $this->setConcurrencyFlags( $params );
                $contents = $this->doGetFileContentsMulti( $params );
+               foreach ( $contents as $path => $content ) {
+                       if ( !is_string( $content ) ) {
+                               $contents[$path] = self::CONTENT_FAIL; // used for all failure cases
+                       }
+               }
 
                return $contents;
        }
@@ -729,26 +816,34 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getFileContentsMulti()
         * @param array $params
-        * @return array
+        * @return string[]|bool[]|null[] Map of (path => string, false (missing), or null (error))
         */
        protected function doGetFileContentsMulti( array $params ) {
                $contents = [];
                foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       AtEase::suppressWarnings();
-                       $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
-                       AtEase::restoreWarnings();
+                       if ( $fsFile instanceof FSFile ) {
+                               AtEase::suppressWarnings();
+                               $content = file_get_contents( $fsFile->getPath() );
+                               AtEase::restoreWarnings();
+                               $contents[$path] = is_string( $content ) ? $content : self::$RES_ERROR;
+                       } elseif ( $fsFile === self::$RES_ABSENT ) {
+                               $contents[$path] = self::$RES_ABSENT;
+                       } else {
+                               $contents[$path] = self::$RES_ERROR;
+                       }
                }
 
                return $contents;
        }
 
        final public function getFileXAttributes( array $params ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $path = self::normalizeStoragePath( $params['src'] );
                if ( $path === null ) {
-                       return false; // invalid storage path
+                       return self::XATTRS_FAIL; // invalid storage path
                }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
                $latest = !empty( $params['latest'] ); // use latest data?
                if ( $this->cheapCache->hasField( $path, 'xattr', self::CACHE_TTL ) ) {
                        $stat = $this->cheapCache->getField( $path, 'xattr' );
@@ -759,8 +854,22 @@ abstract class FileBackendStore extends FileBackend {
                        }
                }
                $fields = $this->doGetFileXAttributes( $params );
-               $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
-               $this->cheapCache->setField( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
+               if ( is_array( $fields ) ) {
+                       $fields = self::normalizeXAttributes( $fields );
+                       $this->cheapCache->setField(
+                               $path,
+                               'xattr',
+                               [ 'map' => $fields, 'latest' => $latest ]
+                       );
+               } elseif ( $fields === self::$RES_ABSENT ) {
+                       $this->cheapCache->setField(
+                               $path,
+                               'xattr',
+                               [ 'map' => self::XATTRS_FAIL, 'latest' => $latest ]
+                       );
+               } else {
+                       $fields = self::XATTRS_FAIL; // used for all failure cases
+               }
 
                return $fields;
        }
@@ -768,19 +877,20 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getFileXAttributes()
         * @param array $params
-        * @return array[][]|false
+        * @return array[][]|false|null Attributes, false (missing file), or null (error)
         */
        protected function doGetFileXAttributes( array $params ) {
                return [ 'headers' => [], 'metadata' => [] ]; // not supported
        }
 
        final public function getFileSha1Base36( array $params ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $path = self::normalizeStoragePath( $params['src'] );
                if ( $path === null ) {
-                       return false; // invalid storage path
+                       return self::SHA1_FAIL; // invalid storage path
                }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
                $latest = !empty( $params['latest'] ); // use latest data?
                if ( $this->cheapCache->hasField( $path, 'sha1', self::CACHE_TTL ) ) {
                        $stat = $this->cheapCache->getField( $path, 'sha1' );
@@ -790,33 +900,49 @@ abstract class FileBackendStore extends FileBackend {
                                return $stat['hash'];
                        }
                }
-               $hash = $this->doGetFileSha1Base36( $params );
-               $this->cheapCache->setField( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
+               $sha1 = $this->doGetFileSha1Base36( $params );
+               if ( is_string( $sha1 ) ) {
+                       $this->cheapCache->setField(
+                               $path,
+                               'sha1',
+                               [ 'hash' => $sha1, 'latest' => $latest ]
+                       );
+               } elseif ( $sha1 === self::$RES_ABSENT ) {
+                       $this->cheapCache->setField(
+                               $path,
+                               'sha1',
+                               [ 'hash' => self::SHA1_FAIL, 'latest' => $latest ]
+                       );
+               } else {
+                       $sha1 = self::SHA1_FAIL; // used for all failure cases
+               }
 
-               return $hash;
+               return $sha1;
        }
 
        /**
         * @see FileBackendStore::getFileSha1Base36()
         * @param array $params
-        * @return bool|string
+        * @return bool|string|null SHA1, false (missing file), or null (error)
         */
        protected function doGetFileSha1Base36( array $params ) {
                $fsFile = $this->getLocalReference( $params );
-               if ( !$fsFile ) {
-                       return false;
-               } else {
-                       return $fsFile->getSha1Base36();
+               if ( $fsFile instanceof FSFile ) {
+                       $sha1 = $fsFile->getSha1Base36();
+
+                       return is_string( $sha1 ) ? $sha1 : self::$RES_ERROR;
                }
+
+               return ( $fsFile === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        final public function getFileProps( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
                $fsFile = $this->getLocalReference( $params );
-               $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
 
-               return $props;
+               return $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
        }
 
        final public function getLocalReferenceMulti( array $params ) {
@@ -844,10 +970,15 @@ abstract class FileBackendStore extends FileBackend {
                // Fetch local references of any remaning files...
                $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
                foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       $fsFiles[$path] = $fsFile;
-                       if ( $fsFile ) { // update the process cache...
-                               $this->expensiveCache->setField( $path, 'localRef',
-                                       [ 'object' => $fsFile, 'latest' => $latest ] );
+                       if ( $fsFile instanceof FSFile ) {
+                               $fsFiles[$path] = $fsFile;
+                               $this->expensiveCache->setField(
+                                       $path,
+                                       'localRef',
+                                       [ 'object' => $fsFile, 'latest' => $latest ]
+                               );
+                       } else {
+                               $fsFiles[$path] = null; // used for all failure cases
                        }
                }
 
@@ -857,7 +988,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getLocalReferenceMulti()
         * @param array $params
-        * @return array
+        * @return string[]|bool[]|null[] Map of (path => FSFile, false (missing), or null (error))
         */
        protected function doGetLocalReferenceMulti( array $params ) {
                return $this->doGetLocalCopyMulti( $params );
@@ -869,6 +1000,11 @@ abstract class FileBackendStore extends FileBackend {
 
                $params = $this->setConcurrencyFlags( $params );
                $tmpFiles = $this->doGetLocalCopyMulti( $params );
+               foreach ( $tmpFiles as $path => $tmpFile ) {
+                       if ( !$tmpFile ) {
+                               $tmpFiles[$path] = null; // used for all failure cases
+                       }
+               }
 
                return $tmpFiles;
        }
@@ -876,7 +1012,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getLocalCopyMulti()
         * @param array $params
-        * @return array
+        * @return string[]|bool[]|null[] Map of (path => TempFSFile, false (missing), or null (error))
         */
        abstract protected function doGetLocalCopyMulti( array $params );
 
@@ -886,7 +1022,7 @@ abstract class FileBackendStore extends FileBackend {
         * @return string|null
         */
        public function getFileHttpUrl( array $params ) {
-               return null; // not supported
+               return self::TEMPURL_ERROR; // not supported
        }
 
        final public function streamFile( array $params ) {
@@ -947,7 +1083,7 @@ abstract class FileBackendStore extends FileBackend {
        final public function directoryExists( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
-                       return false; // invalid storage path
+                       return self::EXISTENCE_ERROR; // invalid storage path
                }
                if ( $shard !== null ) { // confined to a single container/shard
                        return $this->doDirectoryExists( $fullCont, $dir, $params );
@@ -957,11 +1093,11 @@ abstract class FileBackendStore extends FileBackend {
                        $res = false; // response
                        foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
                                $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
-                               if ( $exists ) {
+                               if ( $exists === true ) {
                                        $res = true;
                                        break; // found one!
-                               } elseif ( $exists === null ) { // error?
-                                       $res = self::UNKNOWN; // if we don't find anything, it is indeterminate
+                               } elseif ( $exists === self::$RES_ERROR ) {
+                                       $res = self::EXISTENCE_ERROR;
                                }
                        }
 
@@ -981,8 +1117,8 @@ abstract class FileBackendStore extends FileBackend {
 
        final public function getDirectoryList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) { // invalid storage path
-                       return self::UNKNOWN;
+               if ( $dir === null ) {
+                       return self::EXISTENCE_ERROR; // invalid storage path
                }
                if ( $shard !== null ) {
                        // File listing is confined to a single container/shard
@@ -1005,14 +1141,14 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container Resolved container name
         * @param string $dir Resolved path relative to container
         * @param array $params
-        * @return Traversable|array|null Returns null on failure
+        * @return Traversable|array|null Iterable list or null (error)
         */
        abstract public function getDirectoryListInternal( $container, $dir, array $params );
 
        final public function getFileList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) { // invalid storage path
-                       return self::UNKNOWN;
+               if ( $dir === null ) {
+                       return self::LIST_ERROR; // invalid storage path
                }
                if ( $shard !== null ) {
                        // File listing is confined to a single container/shard
@@ -1035,7 +1171,7 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container Resolved container name
         * @param string $dir Resolved path relative to container
         * @param array $params
-        * @return Traversable|string[]|null Returns null on failure
+        * @return Traversable|string[]|null Iterable list or null (error)
         */
        abstract public function getFileListInternal( $container, $dir, array $params );
 
@@ -1356,7 +1492,6 @@ abstract class FileBackendStore extends FileBackend {
        final public function preloadFileStat( array $params ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
-               $success = true; // no network errors
 
                $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
                $stats = $this->doGetFileStatMulti( $params );
@@ -1364,45 +1499,10 @@ abstract class FileBackendStore extends FileBackend {
                        return true; // not supported
                }
 
-               $latest = !empty( $params['latest'] ); // use latest data?
-               foreach ( $stats as $path => $stat ) {
-                       $path = FileBackend::normalizeStoragePath( $path );
-                       if ( $path === null ) {
-                               continue; // this shouldn't happen
-                       }
-                       if ( is_array( $stat ) ) { // file exists
-                               // Strongly consistent backends can automatically set "latest"
-                               $stat['latest'] = $stat['latest'] ?? $latest;
-                               $this->cheapCache->setField( $path, 'stat', $stat );
-                               $this->setFileCache( $path, $stat ); // update persistent cache
-                               if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->cheapCache->setField( $path, 'sha1',
-                                               [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
-                               }
-                               if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
-                                       $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
-                                       $this->cheapCache->setField( $path, 'xattr',
-                                               [ 'map' => $stat['xattr'], 'latest' => $latest ] );
-                               }
-                       } elseif ( $stat === false ) { // file does not exist
-                               $this->cheapCache->setField( $path, 'stat',
-                                       $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
-                               $this->cheapCache->setField( $path, 'xattr',
-                                       [ 'map' => false, 'latest' => $latest ] );
-                               $this->cheapCache->setField( $path, 'sha1',
-                                       [ 'hash' => false, 'latest' => $latest ] );
-                               $this->logger->debug( __METHOD__ . ': File {path} does not exist', [
-                                       'path' => $path,
-                               ] );
-                       } else { // an error occurred
-                               $success = false;
-                               $this->logger->warning( __METHOD__ . ': Could not stat file {path}', [
-                                       'path' => $path,
-                               ] );
-                       }
-               }
+               // Whether this queried the backend in high consistency mode
+               $latest = !empty( $params['latest'] );
 
-               return $success;
+               return $this->ingestFreshFileStats( $stats, $latest );
        }
 
        /**
@@ -1810,7 +1910,7 @@ abstract class FileBackendStore extends FileBackend {
                                $paths[] = FileBackend::normalizeStoragePath( $item );
                        }
                }
-               // Get rid of any paths that failed normalization...
+               // Get rid of any paths that failed normalization
                $paths = array_filter( $paths, 'strlen' ); // remove nulls
                // Get all the corresponding cache keys for paths...
                foreach ( $paths as $path ) {
@@ -1819,22 +1919,33 @@ abstract class FileBackendStore extends FileBackend {
                                $pathNames[$this->fileCacheKey( $path )] = $path;
                        }
                }
-               // Get all cache entries for these file cache keys...
+               // Get all cache entries for these file cache keys.
+               // Note that negatives are not cached by getFileStat()/preloadFileStat().
                $values = $this->memCache->getMulti( array_keys( $pathNames ) );
-               foreach ( $values as $cacheKey => $val ) {
+               // Load all of the results into process cache...
+               foreach ( array_filter( $values, 'is_array' ) as $cacheKey => $stat ) {
                        $path = $pathNames[$cacheKey];
-                       if ( is_array( $val ) ) {
-                               $val['latest'] = false; // never completely trust cache
-                               $this->cheapCache->setField( $path, 'stat', $val );
-                               if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->cheapCache->setField( $path, 'sha1',
-                                               [ 'hash' => $val['sha1'], 'latest' => false ] );
-                               }
-                               if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
-                                       $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
-                                       $this->cheapCache->setField( $path, 'xattr',
-                                               [ 'map' => $val['xattr'], 'latest' => false ] );
-                               }
+                       // Sanity; this flag only applies to stat info loaded directly
+                       // from a high consistency backend query to the process cache
+                       unset( $stat['latest'] );
+
+                       $this->cheapCache->setField( $path, 'stat', $stat );
+                       if ( isset( $stat['sha1'] ) && strlen( $stat['sha1'] ) == 31 ) {
+                               // Some backends store SHA-1 as metadata
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'sha1',
+                                       [ 'hash' => $stat['sha1'], 'latest' => false ]
+                               );
+                       }
+                       if ( isset( $stat['xattr'] ) && is_array( $stat['xattr'] ) ) {
+                               // Some backends store custom headers/metadata
+                               $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'xattr',
+                                       [ 'map' => $stat['xattr'], 'latest' => false ]
+                               );
                        }
                }
        }
index 88b281e..f3bbecb 100644 (file)
@@ -41,7 +41,7 @@ class MemoryFileBackend extends FileBackendStore {
        }
 
        public function isPathUsableInternal( $storagePath ) {
-               return true;
+               return ( $this->resolveHashKey( $storagePath ) !== null );
        }
 
        protected function doCreateInternal( array $params ) {
@@ -148,7 +148,7 @@ class MemoryFileBackend extends FileBackendStore {
        protected function doGetFileStat( array $params ) {
                $src = $this->resolveHashKey( $params['src'] );
                if ( $src === null ) {
-                       return false; // invalid path
+                       return self::$RES_ERROR; // invalid path
                }
 
                if ( isset( $this->files[$src] ) ) {
@@ -158,15 +158,17 @@ class MemoryFileBackend extends FileBackendStore {
                        ];
                }
 
-               return false;
+               return self::$RES_ABSENT;
        }
 
        protected function doGetLocalCopyMulti( array $params ) {
                $tmpFiles = []; // (path => TempFSFile)
                foreach ( $params['srcs'] as $srcPath ) {
                        $src = $this->resolveHashKey( $srcPath );
-                       if ( $src === null || !isset( $this->files[$src] ) ) {
-                               $fsFile = null;
+                       if ( $src === null ) {
+                               $fsFile = self::$RES_ERROR;
+                       } elseif ( !isset( $this->files[$src] ) ) {
+                               $fsFile = self::$RES_ABSENT;
                        } else {
                                // Create a new temporary file with the same extension...
                                $ext = FileBackend::extensionFromPath( $src );
@@ -174,7 +176,7 @@ class MemoryFileBackend extends FileBackendStore {
                                if ( $fsFile ) {
                                        $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
                                        if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
-                                               $fsFile = null;
+                                               $fsFile = self::$RES_ERROR;
                                        }
                                }
                        }
index 6d6451e..56a2177 100644 (file)
@@ -145,8 +145,11 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        public function getFeatures() {
-               return ( FileBackend::ATTR_UNICODE_PATHS |
-                       FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
+               return (
+                       FileBackend::ATTR_UNICODE_PATHS |
+                       FileBackend::ATTR_HEADERS |
+                       FileBackend::ATTR_METADATA
+               );
        }
 
        protected function resolveContainerPath( $container, $relStoragePath ) {
@@ -593,7 +596,7 @@ class SwiftFileBackend extends FileBackendStore {
                $stat = $this->getContainerStat( $fullCont );
                if ( is_array( $stat ) ) {
                        return $status; // already there
-               } elseif ( $stat === self::UNKNOWN ) {
+               } elseif ( $stat === self::$RES_ERROR ) {
                        $status->fatal( 'backend-fail-internal', $this->name );
                        $this->logger->error( __METHOD__ . ': cannot get container stat' );
 
@@ -777,8 +780,6 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doGetFileContentsMulti( array $params ) {
-               $contents = [];
-
                $auth = $this->getAuthentication();
 
                $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
@@ -786,11 +787,12 @@ class SwiftFileBackend extends FileBackendStore {
                // if the file does not exist. Do not waste time doing file stats here.
                $reqs = []; // (path => op)
 
+               // Initial dummy values to preserve path order
+               $contents = array_fill_keys( $params['srcs'], self::$RES_ERROR );
                foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
                        if ( $srcRel === null || !$auth ) {
-                               $contents[$path] = false;
-                               continue;
+                               continue; // invalid storage path or auth error
                        }
                        // Create a new temporary memory file...
                        $handle = fopen( 'php://temp', 'wb' );
@@ -803,7 +805,6 @@ class SwiftFileBackend extends FileBackendStore {
                                        'stream'  => $handle,
                                ];
                        }
-                       $contents[$path] = false;
                }
 
                $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
@@ -812,10 +813,21 @@ class SwiftFileBackend extends FileBackendStore {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
                        if ( $rcode >= 200 && $rcode <= 299 ) {
                                rewind( $op['stream'] ); // start from the beginning
-                               $contents[$path] = stream_get_contents( $op['stream'] );
+                               $content = (string)stream_get_contents( $op['stream'] );
+                               $size = strlen( $content );
+                               // Make sure that stream finished
+                               if ( $size === (int)$rhdrs['content-length'] ) {
+                                       $contents[$path] = $content;
+                               } else {
+                                       $contents[$path] = self::$RES_ERROR;
+                                       $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
+                                       $this->onError( null, __METHOD__,
+                                               [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
+                               }
                        } elseif ( $rcode === 404 ) {
-                               $contents[$path] = false;
+                               $contents[$path] = self::$RES_ABSENT;
                        } else {
+                               $contents[$path] = self::$RES_ERROR;
                                $this->onError( null, __METHOD__,
                                        [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
                        }
@@ -832,7 +844,7 @@ class SwiftFileBackend extends FileBackendStore {
                        return ( count( $status->value ) ) > 0;
                }
 
-               return self::UNKNOWN; // error
+               return self::$RES_ERROR;
        }
 
        /**
@@ -874,6 +886,7 @@ class SwiftFileBackend extends FileBackendStore {
                        return $dirs; // nothing more
                }
 
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
                $prefix = ( $dir == '' ) ? null : "{$dir}/";
@@ -956,10 +969,11 @@ class SwiftFileBackend extends FileBackendStore {
                        return $files; // nothing more
                }
 
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
                $prefix = ( $dir == '' ) ? null : "{$dir}/";
-               // $objects will contain a list of unfiltered names or CF_Object items
+               // $objects will contain a list of unfiltered names or stdClass items
                // Non-recursive: only list files right under $dir
                if ( !empty( $params['topOnly'] ) ) {
                        if ( !empty( $params['adviseStat'] ) ) {
@@ -982,7 +996,7 @@ class SwiftFileBackend extends FileBackendStore {
                }
 
                $objects = $status->value;
-               $files = $this->buildFileObjectListing( $params, $dir, $objects );
+               $files = $this->buildFileObjectListing( $objects );
 
                // Page on the unfiltered object listing (what is returned may be filtered)
                if ( count( $objects ) < $limit ) {
@@ -997,14 +1011,12 @@ class SwiftFileBackend extends FileBackendStore {
 
        /**
         * Build a list of file objects, filtering out any directories
-        * and extracting any stat info if provided in $objects (for CF_Objects)
+        * and extracting any stat info if provided in $objects
         *
-        * @param array $params Parameters for getDirectoryList()
-        * @param string $dir Resolved container directory path
-        * @param array $objects List of CF_Object items or object names
+        * @param stdClass[]|string[] $objects List of stdClass items or object names
         * @return array List of (names,stat array or null) entries
         */
-       private function buildFileObjectListing( array $params, $dir, array $objects ) {
+       private function buildFileObjectListing( array $objects ) {
                $names = [];
                foreach ( $objects as $object ) {
                        if ( is_object( $object ) ) {
@@ -1042,18 +1054,17 @@ class SwiftFileBackend extends FileBackendStore {
 
        protected function doGetFileXAttributes( array $params ) {
                $stat = $this->getFileStat( $params );
-               if ( $stat ) {
-                       if ( !isset( $stat['xattr'] ) ) {
-                               // Stat entries filled by file listings don't include metadata/headers
-                               $this->clearCache( [ $params['src'] ] );
-                               $stat = $this->getFileStat( $params );
-                       }
+               // Stat entries filled by file listings don't include metadata/headers
+               if ( is_array( $stat ) && !isset( $stat['xattr'] ) ) {
+                       $this->clearCache( [ $params['src'] ] );
+                       $stat = $this->getFileStat( $params );
+               }
 
-                       // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
+               if ( is_array( $stat ) ) {
                        return $stat['xattr'];
-               } else {
-                       return false;
                }
+
+               return ( $stat === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        protected function doGetFileSha1base36( array $params ) {
@@ -1062,11 +1073,11 @@ class SwiftFileBackend extends FileBackendStore {
                $params['requireSHA1'] = true;
 
                $stat = $this->getFileStat( $params );
-               if ( $stat ) {
+               if ( is_array( $stat ) ) {
                        return $stat['sha1'];
-               } else {
-                       return false;
                }
+
+               return ( $stat === self::$RES_ERROR ) ? self::$RES_ERROR : self::$RES_ABSENT;
        }
 
        protected function doStreamFile( array $params ) {
@@ -1136,9 +1147,6 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doGetLocalCopyMulti( array $params ) {
-               /** @var TempFSFile[] $tmpFiles */
-               $tmpFiles = [];
-
                $auth = $this->getAuthentication();
 
                $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
@@ -1146,56 +1154,62 @@ class SwiftFileBackend extends FileBackendStore {
                // if the file does not exist. Do not waste time doing file stats here.
                $reqs = []; // (path => op)
 
+               // Initial dummy values to preserve path order
+               $tmpFiles = array_fill_keys( $params['srcs'], self::$RES_ERROR );
                foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
                        if ( $srcRel === null || !$auth ) {
-                               $tmpFiles[$path] = null;
-                               continue;
+                               continue; // invalid storage path or auth error
                        }
                        // Get source file extension
                        $ext = FileBackend::extensionFromPath( $path );
                        // Create a new temporary file...
                        $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
-                       if ( $tmpFile ) {
-                               $handle = fopen( $tmpFile->getPath(), 'wb' );
-                               if ( $handle ) {
-                                       $reqs[$path] = [
-                                               'method'  => 'GET',
-                                               'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                                               'headers' => $this->authTokenHeaders( $auth )
-                                                       + $this->headersFromParams( $params ),
-                                               'stream'  => $handle,
-                                       ];
-                               } else {
-                                       $tmpFile = null;
-                               }
+                       $handle = $tmpFile ? fopen( $tmpFile->getPath(), 'wb' ) : false;
+                       if ( $handle ) {
+                               $reqs[$path] = [
+                                       'method'  => 'GET',
+                                       'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                                       'headers' => $this->authTokenHeaders( $auth )
+                                               + $this->headersFromParams( $params ),
+                                       'stream'  => $handle,
+                               ];
+                               $tmpFiles[$path] = $tmpFile;
                        }
-                       $tmpFiles[$path] = $tmpFile;
                }
 
-               $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
+               // Ceph RADOS Gateway is in use (strong consistency) or X-Newest will be used
+               $latest = ( $this->isRGW || !empty( $params['latest'] ) );
+
                $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
                $reqs = $this->http->runMulti( $reqs, $opts );
                foreach ( $reqs as $path => $op ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
                        fclose( $op['stream'] ); // close open handle
                        if ( $rcode >= 200 && $rcode <= 299 ) {
-                               $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
-                               // Double check that the disk is not full/broken
-                               if ( $size != $rhdrs['content-length'] ) {
-                                       $tmpFiles[$path] = null;
+                               /** @var TempFSFile $tmpFile */
+                               $tmpFile = $tmpFiles[$path];
+                               // Make sure that the stream finished and fully wrote to disk
+                               $size = $tmpFile->getSize();
+                               if ( $size !== (int)$rhdrs['content-length'] ) {
+                                       $tmpFiles[$path] = self::$RES_ERROR;
                                        $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
                                        $this->onError( null, __METHOD__,
                                                [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
                                }
                                // Set the file stat process cache in passing
                                $stat = $this->getStatFromHeaders( $rhdrs );
-                               $stat['latest'] = $isLatest;
+                               $stat['latest'] = $latest;
                                $this->cheapCache->setField( $path, 'stat', $stat );
                        } elseif ( $rcode === 404 ) {
-                               $tmpFiles[$path] = false;
+                               $tmpFiles[$path] = self::$RES_ABSENT;
+                               $this->cheapCache->setField(
+                                       $path,
+                                       'stat',
+                                       $latest ? self::$ABSENT_LATEST : self::$ABSENT_NORMAL
+                               );
                        } else {
-                               $tmpFiles[$path] = null;
+                               $tmpFiles[$path] = self::$RES_ERROR;
                                $this->onError( null, __METHOD__,
                                        [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
                        }
@@ -1210,12 +1224,12 @@ class SwiftFileBackend extends FileBackendStore {
                ) {
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
                        if ( $srcRel === null ) {
-                               return null; // invalid path
+                               return self::TEMPURL_ERROR; // invalid path
                        }
 
                        $auth = $this->getAuthentication();
                        if ( !$auth ) {
-                               return null;
+                               return self::TEMPURL_ERROR;
                        }
 
                        $ttl = $params['ttl'] ?? 86400;
@@ -1255,7 +1269,7 @@ class SwiftFileBackend extends FileBackendStore {
                        }
                }
 
-               return null;
+               return self::TEMPURL_ERROR;
        }
 
        protected function directoriesAreVirtual() {
@@ -1394,6 +1408,7 @@ class SwiftFileBackend extends FileBackendStore {
         * @return array|bool|null False on 404, null on failure
         */
        protected function getContainerStat( $container, $bypassCache = false ) {
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
 
                if ( $bypassCache ) { // purge cache
@@ -1404,7 +1419,7 @@ class SwiftFileBackend extends FileBackendStore {
                if ( !$this->containerStatCache->hasField( $container, 'stat' ) ) {
                        $auth = $this->getAuthentication();
                        if ( !$auth ) {
-                               return self::UNKNOWN;
+                               return self::$RES_ERROR;
                        }
 
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
@@ -1425,12 +1440,12 @@ class SwiftFileBackend extends FileBackendStore {
                                        $this->setContainerCache( $container, $stat ); // update persistent cache
                                }
                        } elseif ( $rcode === 404 ) {
-                               return false;
+                               return self::$RES_ABSENT;
                        } else {
                                $this->onError( null, __METHOD__,
                                        [ 'cont' => $container ], $rerr, $rcode, $rdesc );
 
-                               return self::UNKNOWN;
+                               return self::$RES_ERROR;
                        }
                }
 
@@ -1595,24 +1610,21 @@ class SwiftFileBackend extends FileBackendStore {
 
                $auth = $this->getAuthentication();
 
-               $reqs = [];
+               $reqs = []; // (path => op)
+               // (a) Check the containers of the paths...
                foreach ( $params['srcs'] as $path ) {
                        list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                       if ( $srcRel === null ) {
-                               $stats[$path] = false;
-                               continue; // invalid storage path
-                       } elseif ( !$auth ) {
-                               $stats[$path] = self::UNKNOWN;
-                               continue;
+                       if ( $srcRel === null || !$auth ) {
+                               $stats[$path] = self::$RES_ERROR;
+                               continue; // invalid storage path or auth error
                        }
 
-                       // (a) Check the container
                        $cstat = $this->getContainerStat( $srcCont );
-                       if ( $cstat === false ) {
-                               $stats[$path] = false;
+                       if ( $cstat === self::$RES_ABSENT ) {
+                               $stats[$path] = self::$RES_ABSENT;
                                continue; // ok, nothing to do
                        } elseif ( !is_array( $cstat ) ) {
-                               $stats[$path] = self::UNKNOWN;
+                               $stats[$path] = self::$RES_ERROR;
                                continue;
                        }
 
@@ -1623,15 +1635,11 @@ class SwiftFileBackend extends FileBackendStore {
                        ];
                }
 
+               // (b) Check the files themselves...
                $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
                $reqs = $this->http->runMulti( $reqs, $opts );
-
-               foreach ( $params['srcs'] as $path ) {
-                       if ( array_key_exists( $path, $stats ) ) {
-                               continue; // some sort of failure above
-                       }
-                       // (b) Check the file
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
+               foreach ( $reqs as $path => $op ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
                        if ( $rcode === 200 || $rcode === 204 ) {
                                // Update the object if it is missing some headers
                                if ( !empty( $params['requireSHA1'] ) ) {
@@ -1643,9 +1651,9 @@ class SwiftFileBackend extends FileBackendStore {
                                        $stat['latest'] = true; // strong consistency
                                }
                        } elseif ( $rcode === 404 ) {
-                               $stat = false;
+                               $stat = self::$RES_ABSENT;
                        } else {
-                               $stat = self::UNKNOWN;
+                               $stat = self::$RES_ERROR;
                                $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
                        }
                        $stats[$path] = $stat;
index bcde8d9..92105c3 100644 (file)
@@ -33,7 +33,7 @@ abstract class SwiftFileBackendList implements Iterator {
        /** @var array List of path or (path,stat array) entries */
        protected $bufferIter = [];
 
-       /** @var string List items *after* this path */
+       /** @var string|null List items *after* this path */
        protected $bufferAfter = null;
 
        /** @var int */
@@ -108,6 +108,7 @@ abstract class SwiftFileBackendList implements Iterator {
                $this->pos = 0;
                $this->bufferAfter = null;
                $this->bufferIter = $this->pageFromList(
+                       // @phan-suppress-next-line PhanTypeMismatchArgumentPropertyReferenceReal
                        $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
                ); // updates $this->bufferAfter
        }
index 527de6a..59af944 100644 (file)
@@ -36,8 +36,10 @@ class CopyFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
                                $this->doOperation = false; // no-op
                                // Update file existence predicates (cache 404s)
@@ -50,10 +52,8 @@ class CopyFileOp extends FileOp {
 
                                return $status;
                        }
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
index f45b055..b68b98f 100644 (file)
@@ -34,21 +34,15 @@ class CreateFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source data is too big
-               if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
-                       $status->fatal( 'backend-fail-maxsize',
-                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
-                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
 
-                       return $status;
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
+               // Check if the source data is too big
+               $maxBytes = $this->backend->maxFileSizeInternal();
+               if ( strlen( $this->getParam( 'content' ) ) > $maxBytes ) {
+                       $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes );
 
                        return $status;
                }
-               // Check if destination file exists
+               // Check if an incompatible destination file exists
                $status->merge( $this->precheckDestExistence( $predicates ) );
                $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
                if ( $status->isOK() ) {
@@ -61,12 +55,14 @@ class CreateFileOp extends FileOp {
        }
 
        protected function doAttempt() {
-               if ( !$this->overwriteSameCase ) {
+               if ( $this->overwriteSameCase ) {
+                       $status = StatusValue::newGood(); // nothing to do
+               } else {
                        // Create the file at the destination
-                       return $this->backend->createInternal( $this->setFlags( $this->params ) );
+                       $status = $this->backend->createInternal( $this->setFlags( $this->params ) );
                }
 
-               return StatusValue::newGood();
+               return $status;
        }
 
        protected function getSourceSha1Base36() {
index 1047a98..3b48881 100644 (file)
@@ -32,8 +32,10 @@ class DeleteFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
                                $this->doOperation = false; // no-op
                                // Update file existence predicates (cache 404s)
@@ -46,10 +48,8 @@ class DeleteFileOp extends FileOp {
 
                                return $status;
                        }
-                       // Check if a file can be placed/changed at the source
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
-                       $status->fatal( 'backend-fail-delete', $this->params['src'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
index 0d1e553..3604b26 100644 (file)
@@ -32,21 +32,20 @@ class DescribeFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        $status->fatal( 'backend-fail-notexists', $this->params['src'] );
 
                        return $status;
-                       // Check if a file can be placed/changed at the source
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
-                       $status->fatal( 'backend-fail-describe', $this->params['src'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
                // Update file existence predicates
-               $predicates['exists'][$this->params['src']] =
-                       $this->fileExists( $this->params['src'], $predicates );
+               $predicates['exists'][$this->params['src']] = $srcExists;
                $predicates['sha1'][$this->params['src']] =
                        $this->fileSha1( $this->params['src'], $predicates );
 
index 961fdb9..a046588 100644 (file)
@@ -255,6 +255,18 @@ abstract class FileOp {
                        return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
                }
                $this->state = self::STATE_CHECKED;
+
+               $status = StatusValue::newGood();
+               $storagePaths = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
+               foreach ( array_unique( $storagePaths ) as $storagePath ) {
+                       if ( !$this->backend->isPathUsableInternal( $storagePath ) ) {
+                               $status->fatal( 'backend-fail-usable', $storagePath );
+                       }
+               }
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
                $status = $this->doPrecheck( $predicates );
                if ( !$status->isOK() ) {
                        $this->failed = true;
@@ -391,6 +403,8 @@ abstract class FileOp {
 
                                return $status;
                        }
+               } elseif ( $this->destExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['dst'] );
                }
 
                return $status;
@@ -409,9 +423,12 @@ abstract class FileOp {
        /**
         * Check if a file will exist in storage when this operation is attempted
         *
+        * Ideally, the file stat entry should already be preloaded via preloadFileStat().
+        * Otherwise, this will query the backend.
+        *
         * @param string $source Storage path
         * @param array $predicates
-        * @return bool
+        * @return bool|null Whether the file will exist or null on error
         */
        final protected function fileExists( $source, array $predicates ) {
                if ( isset( $predicates['exists'][$source] ) ) {
@@ -424,11 +441,14 @@ abstract class FileOp {
        }
 
        /**
-        * Get the SHA-1 of a file in storage when this operation is attempted
+        * Get the SHA-1 hash a file in storage will have when this operation is attempted
+        *
+        * Ideally, file the stat entry should already be preloaded via preloadFileStat() and
+        * the backend tracks hashes as extended attributes. Otherwise, this will query the backend.
         *
         * @param string $source Storage path
         * @param array $predicates
-        * @return string|bool False on failure
+        * @return string|bool The SHA-1 hash the file will have or false if non-existent or on error
         */
        final protected function fileSha1( $source, array $predicates ) {
                if ( isset( $predicates['sha1'][$source] ) ) {
index 55dca51..0a83370 100644 (file)
@@ -36,8 +36,10 @@ class MoveFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+
+               // Check source file existence
+               $srcExists = $this->fileExists( $this->params['src'], $predicates );
+               if ( $srcExists === false ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
                                $this->doOperation = false; // no-op
                                // Update file existence predicates (cache 404s)
@@ -50,14 +52,12 @@ class MoveFileOp extends FileOp {
 
                                return $status;
                        }
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
+               } elseif ( $srcExists === FileBackend::EXISTENCE_ERROR ) {
+                       $status->fatal( 'backend-fail-stat', $this->params['src'] );
 
                        return $status;
                }
-               // Check if destination file exists
+               // Check if an incompatible destination file exists
                $status->merge( $this->precheckDestExistence( $predicates ) );
                $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
                if ( $status->isOK() ) {
index 5783a82..b8cfbf6 100644 (file)
@@ -38,26 +38,21 @@ class StoreFileOp extends FileOp {
 
        protected function doPrecheck( array &$predicates ) {
                $status = StatusValue::newGood();
-               // Check if the source file exists on the file system
+
+               // Check if the source file exists in the file system and is not too big
                if ( !is_file( $this->params['src'] ) ) {
                        $status->fatal( 'backend-fail-notexists', $this->params['src'] );
 
                        return $status;
-                       // Check if the source file is too big
-               } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
-                       $status->fatal( 'backend-fail-maxsize',
-                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
-                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
-
-                       return $status;
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
+               }
+               // Check if the source file is too big
+               $maxBytes = $this->backend->maxFileSizeInternal();
+               if ( filesize( $this->params['src'] ) > $maxBytes ) {
+                       $status->fatal( 'backend-fail-maxsize', $this->params['dst'], $maxBytes );
 
                        return $status;
                }
-               // Check if destination file exists
+               // Check if an incompatible destination file exists
                $status->merge( $this->precheckDestExistence( $predicates ) );
                $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
                if ( $status->isOK() ) {
@@ -70,12 +65,14 @@ class StoreFileOp extends FileOp {
        }
 
        protected function doAttempt() {
-               if ( !$this->overwriteSameCase ) {
+               if ( $this->overwriteSameCase ) {
+                       $status = StatusValue::newGood(); // nothing to do
+               } else {
                        // Store the file at the destination
-                       return $this->backend->storeInternal( $this->setFlags( $this->params ) );
+                       $status = $this->backend->storeInternal( $this->setFlags( $this->params ) );
                }
 
-               return StatusValue::newGood();
+               return $status;
        }
 
        protected function getSourceSha1Base36() {
index 6b1ef89..2e3aa70 100644 (file)
@@ -353,9 +353,7 @@ class MultiHttpClient implements LoggerAwareInterface {
                        }
                        curl_setopt( $ch, CURLOPT_READFUNCTION,
                                function ( $ch, $fd, $length ) {
-                                       $data = fread( $fd, $length );
-                                       $len = strlen( $data );
-                                       return $data;
+                                       return (string)fread( $fd, $length );
                                }
                        );
                } elseif ( $req['method'] === 'POST' ) {
@@ -404,7 +402,6 @@ class MultiHttpClient implements LoggerAwareInterface {
                                $name = strtolower( $name );
                                $value = trim( $value );
                                if ( isset( $req['response']['headers'][$name] ) ) {
-                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        $req['response']['headers'][$name] .= ', ' . $value;
                                } else {
                                        $req['response']['headers'][$name] = $value;
index 0f7011d..348f300 100644 (file)
@@ -50,7 +50,6 @@ class HashBagOStuff extends MediumSpecificBagOStuff {
         * @codingStandardsIgnoreStart
         * @phan-param array{logger?:Psr\Log\LoggerInterface,asyncHandler?:callable,keyspace?:string,reportDupes?:bool,syncTimeout?:int,segmentationSize?:int,segmentedValueMaxSize?:int,maxKeys?:int} $params
         * @codingStandardsIgnoreEnd
-        * @suppress PhanTypeInvalidDimOffset
         */
        function __construct( $params = [] ) {
                $params['segmentationSize'] = $params['segmentationSize'] ?? INF;
index 2ce216d..a090e16 100644 (file)
@@ -585,7 +585,6 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @note Options added in 1.33: creating
         * @note Options added in 1.34: version, walltime
         * @return bool Success
-        * @suppress PhanTypeInvalidDimOffset
         */
        final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
                $now = $this->getCurrentTime();
@@ -1258,7 +1257,6 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @note Options added in 1.31: staleTTL, graceTTL
         * @note Options added in 1.33: touchedCallback
         * @note Callable type hints are not used to avoid class-autoloading
-        * @suppress PhanTypeInvalidDimOffset
         */
        final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
                $version = $opts['version'] ?? null;
@@ -1450,7 +1448,6 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                                $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
                        } else {
                                $finalSetOpts = [
-                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        'since' => $setOpts['since'] ?? $preCallbackTime,
                                        'version' => $version,
                                        'staleTTL' => $staleTTL,
@@ -2328,6 +2325,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
                $chance = ( 1 - $curTTL / $lowTTL );
 
+               // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
        }
 
@@ -2370,6 +2368,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
                $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
 
+               // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
        }
 
index 19550f4..c3f8879 100644 (file)
@@ -107,6 +107,7 @@ class LoadMonitor implements ILoadMonitor {
 
                $key = $this->getCacheKey( $serverIndexes );
                # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
+               // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
                # Keep keys around longer as fallbacks
                $staleTTL = 60;
index 874e872..9058340 100644 (file)
@@ -26,8 +26,6 @@
  * All metadata related, since both JPEG and TIFF support Exif.
  *
  * @ingroup Media
- * @phan-file-suppress PhanUndeclaredConstant Phan doesn't read constants in MediaHandler
- *   when accessed via self::
  */
 class ExifBitmapHandler extends BitmapHandler {
        const BROKEN_FILE = '-1'; // error extracting metadata
index 5c55124..b27338c 100644 (file)
@@ -4271,7 +4271,6 @@ class Parser {
         * @param bool $isMain
         * @return mixed|string
         * @private
-        * @suppress PhanTypeInvalidDimOffset
         */
        public function formatHeadings( $text, $origText, $isMain = true ) {
                # Inhibit editsection links if requested in the page
@@ -5578,7 +5577,6 @@ class Parser {
                Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
 
                # Linker does the rest
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $time = $options['time'] ?? false;
                $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
                        $time, $descQuery, $this->mOptions->getThumbSize() );
index 93f4246..bd610de 100644 (file)
@@ -40,6 +40,7 @@ class ParserDiffTest {
                if ( !is_null( $this->parsers ) ) {
                        return;
                }
+               $this->parsers = [];
 
                if ( isset( $this->conf['shortOutput'] ) ) {
                        $this->shortOutput = $this->conf['shortOutput'];
index 9b5b29e..d0b7ae3 100644 (file)
@@ -61,7 +61,6 @@ abstract class FormattedRCFeed extends RCFeed {
                        // @codeCoverageIgnoreStart
                        // T109544 - If a feed formatter returns null, this will otherwise cause an
                        // error in at least RedisPubSubFeedEngine. Not sure best to handle this.
-                       // @phan-suppress-next-line PhanTypeMismatchReturn
                        return;
                        // @codeCoverageIgnoreEnd
                }
index 8303896..151b5fd 100644 (file)
@@ -427,7 +427,6 @@ JAVASCRIPT;
                                $idx = -1;
                                foreach ( $grpModules as $name => $module ) {
                                        $shouldEmbed = $module->shouldEmbedModule( $context );
-                                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                                        if ( !$moduleSets || $moduleSets[$idx][0] !== $shouldEmbed ) {
                                                $moduleSets[++$idx] = [ $shouldEmbed, [] ];
                                        }
index 9c204fc..55e9e53 100644 (file)
@@ -137,7 +137,6 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
                                        $dataPath->getRemoteBasePath()
                                );
                        } else {
-                               // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
                                $path = dirname( $dataPath ) . '/' . $path;
                        }
                };
index 334dccf..ca7bc04 100644 (file)
@@ -110,10 +110,8 @@ class RevDelFileList extends RevDelList {
        }
 
        public function doPostCommitUpdates( array $visibilityChangeMap ) {
-               /** @var LocalFile $file */
                $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
                        ->newFile( $this->title );
-               '@phan-var LocalFile $file';
                $file->purgeCache();
                $file->purgeDescription();
 
index 1da0ceb..09cdf72 100644 (file)
@@ -286,7 +286,6 @@ final class SessionManager implements SessionManagerInterface {
                                        "$provider returned empty session info with id flagged unsafe"
                                );
                        }
-                       // @phan-suppress-next-line PhanTypeInvalidDimOffset
                        $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
                        if ( $compare > 0 ) {
                                continue;
index 74d77a8..478a615 100644 (file)
@@ -238,7 +238,6 @@ class Shell {
                // Give site config file a chance to run the script in a wrapper.
                // The caller may likely want to call wfBasename() on $script.
                Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] );
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $cmd = [ $options['php'] ?? $wgPhpCli ];
                if ( isset( $options['wrapper'] ) ) {
                        $cmd[] = $options['wrapper'];
index bee4aff..cd63796 100644 (file)
@@ -516,7 +516,6 @@ abstract class BaseTemplate extends QuickTemplate {
                if ( isset( $item['itemtitle'] ) ) {
                        $attrs['title'] = $item['itemtitle'];
                }
-               // @phan-suppress-next-line PhanTypeInvalidDimOffset
                return Html::rawElement( $options['tag'] ?? 'li', $attrs, $html );
        }
 
index 2dcb77f..60cbff1 100644 (file)
@@ -47,6 +47,7 @@ class UncategorizedCategoriesPage extends UncategorizedPagesPage {
         */
        private function getExceptionList() {
                if ( $this->exceptionList === null ) {
+                       $this->exceptionList = [];
                        $exList = $this->msg( 'uncategorized-categories-exceptionlist' )
                                ->inContentLanguage()->plain();
                        $proposedTitles = explode( "\n", $exList );
index 63cff94..4441a33 100644 (file)
@@ -74,7 +74,7 @@ class BlockListPager extends TablePager {
         * @param string $name
         * @param string $value
         * @return string
-        * @suppress PhanTypeArraySuspiciousNullable,PhanTypeArraySuspicious
+        * @suppress PhanTypeArraySuspicious
         */
        function formatValue( $name, $value ) {
                static $msg = null;
index 105eeaa..7234be2 100644 (file)
@@ -62,6 +62,7 @@ class NamespaceInfo {
        public static $canonicalNames = [
                NS_MEDIA            => 'Media',
                NS_SPECIAL          => 'Special',
+               NS_MAIN             => '',
                NS_TALK             => 'Talk',
                NS_USER             => 'User',
                NS_USER_TALK        => 'User_talk',
@@ -341,7 +342,7 @@ class NamespaceInfo {
         * Returns array of all defined namespaces with their canonical
         * (English) names.
         *
-        * @return array
+        * @return string[]
         */
        public function getCanonicalNamespaces() {
                if ( $this->canonicalNamespaces === null ) {
@@ -396,6 +397,7 @@ class NamespaceInfo {
         */
        public function getValidNamespaces() {
                if ( is_null( $this->validNamespaces ) ) {
+                       $this->validNamespaces = [];
                        foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) {
                                if ( $ns >= 0 ) {
                                        $this->validNamespaces[] = $ns;
index ad6b5b8..7068879 100644 (file)
@@ -249,7 +249,8 @@ class User implements IDBAccessObject, UserIdentity {
                        return $this->$name;
                } else {
                        wfLogWarning( 'tried to get non-visible property' );
-                       return null;
+                       $null = null;
+                       return $null;
                }
        }
 
index 16a6e1a..51ff8d5 100644 (file)
@@ -527,7 +527,6 @@ class Language {
                        }
 
                        # Sometimes a language will be localised but not actually exist on this wiki.
-                       // @phan-suppress-next-line PhanTypeMismatchForeach
                        foreach ( $this->namespaceNames as $key => $text ) {
                                if ( !isset( $validNamespaces[$key] ) ) {
                                        unset( $this->namespaceNames[$key] );
@@ -536,7 +535,6 @@ class Language {
 
                        # The above mixing may leave namespaces out of canonical order.
                        # Re-order by namespace ID number...
-                       // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
                        ksort( $this->namespaceNames );
 
                        Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
@@ -3237,7 +3235,6 @@ class Language {
                $fallbackChain = array_reverse( $fallbackChain );
                foreach ( $fallbackChain as $code ) {
                        if ( isset( $newWords[$code] ) ) {
-                               // @phan-suppress-next-line PhanTypeMismatchProperty
                                $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
                        }
                }
index d953eb7..3508483 100644 (file)
        "backend-fail-batchsize": "أعطت خلفية التخزين دفعة $1 ملف {{PLURAL:$1|عملية|عمليات}}; الحد الأقصى هو $2 {{PLURAL:$2|عملية|عمليات}}.",
        "backend-fail-usable": "تعذر قراءة أو كتابة الملف \"$1\" لنقص في التراخيص أو فقدان الدلائل/الحاويات.",
        "backend-fail-stat": "لا يمكن قراءة حالة الملف \"$1\".",
-       "backend-fail-hash": "يمكن تحديد دالة التشفير لملف \"$1\".",
+       "backend-fail-hash": "Ù\84ا Ù\8aÙ\85Ù\83Ù\86 ØªØ­Ø¯Ù\8aد Ø¯Ø§Ù\84Ø© Ø§Ù\84تشÙ\81Ù\8aر Ù\84Ù\85Ù\84Ù\81 \"$1\".",
        "filejournal-fail-dbconnect": "تعذر ربط الإتصال بقاعدة بيانات خلفية التخزين \"$1\".",
        "filejournal-fail-dbquery": "تعذر تحديث قاعدة بيانات خلفية تخزين \"$1\".",
        "lockmanager-notlocked": "تعذر فتح \"$1\"، الملف غير مغلق.",
index 7e41483..810c304 100644 (file)
        "deletereason-dropdown": "*Əsas silmə səbəbləri\n** Spam\n** Vandalizm\n** Müəllif hüququ pozuntusu\n** Müəllif istəyi\n** Səhv yönləndirmə",
        "delete-edit-reasonlist": "Silmə səbəblərinin redaktəsi",
        "delete-toobig": "Bu səhifə $1-dən artıq redaktə ilə çox böyük redaktə tarixçəsinə malikdir.\n\"{{SITENAME}}\" saytının fəaliyyətində problemlər yaratmamaq üçün bu cür səhifələrin silinməsi qadağandır.",
+       "deleting-backlinks-warning": "<strong>Xəbərdarlıq:</strong> Silmək istədiyiniz səhifəyə [[Xüsusi:WhatLinksHere/{{FULLPAGENAME}}|başqa səhifələr]]dən keçid verilib.",
        "rollback": "əvvəlki halına qaytar",
        "rollbacklink": "əvvəlki halına qaytar",
        "rollbacklinkcount": "$1 {{PLURAL:$1|dəyişikliyi|dəyişikliyi}} geri qaytar",
index 1ad2234..1b3950f 100644 (file)
        "filedelete-submit": "Usap",
        "filedelete-success": "<strong>$1</strong> sampun kausapin.",
        "filedelete-maintenance-title": "Nénten prasida ngusapin berkas",
-       "randompage": "Kaca punapi kémanten",
+       "randompage": "Kaca ulah-aluh",
        "statistics": "Statistik",
        "statistics-articles": "Kaca daging",
        "brokenredirects-edit": "uah",
        "tooltip-n-portal": "Indik proyék, sané prasida kalaksanayang, genah ngrereh wantuan",
        "tooltip-n-currentevents": "Rereh pidarta indik kawéntenan sané pinih anyar",
        "tooltip-n-recentchanges": "Bacakan uahan sané mangkin ring wiki",
-       "tooltip-n-randompage": "Cihnayang kaca napi kémanten",
+       "tooltip-n-randompage": "Cihnayang kaca ulah-aluh",
        "tooltip-n-help": "Genah ngrereh wantuan",
        "tooltip-t-whatlinkshere": "Bacakan makasami kaca ring wiki sané nuju iriki",
        "tooltip-t-recentchangeslinked": "Uahan sané mangkin saking kaca-kaca sané linked ring kaca puniki",
        "pageinfo-header-restrictions": "Saiban kaca",
        "pageinfo-header-properties": "Properti suratan",
        "pageinfo-display-title": "Edengang judul",
+       "pageinfo-length": "Dawannyané kaca (ring bita)",
        "pageinfo-namespace": "Genah wastan",
        "pageinfo-article-id": "ID kaca",
        "pageinfo-language": "Basa ring daging kaca",
index 6a31fae..10ddb23 100644 (file)
        "confirmemail_body_changed": "Alguien, probablemente usted,\nha modificado la dirección de correo electrónico asociado a la cuenta \"$2\" hacia esta en {{SITENAME}}, desde la dirección IP $1.\n\nPara confirmar que esta cuenta realmente le pertenece y reactivar las funciones de correo electrónico en {{SITENAME}}, abra este enlace en su navegador:\n\n$3\n\nSi la cuenta *no* le pertenece, sigua el siguiente enlace para cancelar la confirmación:\n\n$5\n\nEste código de confirmación expirará el $4.",
        "deletedwhileediting": "'''Aviso''': ¡Esta página fue borrada después de que usted empezara a editar!",
        "confirmrecreate": "{{GENDER:$1|El usuario|La usuaria}} [[User:$1|$1]] ([[User talk:$1|discusión]]) borró esta página después de que usted comenzara a editarla, por este motivo:\n: <em>$2</em>\nPor favor confirme que realmente quiere volver a crearla.",
+       "confirm-purge-top": "¿Quiere vaciar la antememoria de esta página?",
        "watchlistedit-normal-explain": "Los títulos de su lista de seguimiento se muestran debajo.\nPara eliminar un título, marque la casilla junto a él, y haga clic en ''{{int:Watchlistedit-normal-submit}}''.\nTambién puede [[Special:EditWatchlist/raw|editar la lista de en crudo]].",
        "watchlistedit-raw-explain": "Los títulos de su lista de seguimiento se muestran debajo. Esta lista puede ser editada añadiendo o eliminando líneas de la lista;\nun título por línea.\nCuando acabe, haga clic en \"{{int:Watchlistedit-raw-submit}}\".\nTambién puede [[Special:EditWatchlist|usar el editor estándar]].",
        "watchlistedit-raw-done": "Su lista de seguimiento ha sido actualizada.",
index 217d65e..484a448 100644 (file)
        "nocreate-loggedin": "No tienes permiso para crear páginas nuevas.",
        "sectioneditnotsupported-title": "Edición de sección no admitida",
        "sectioneditnotsupported-text": "No se admite la edición de secciones en esta página.",
+       "modeleditnotsupported-title": "No se admite la edición",
+       "modeleditnotsupported-text": "No se admite la edición en el modelo de contenidos $1.",
        "permissionserrors": "Error de permisos",
        "permissionserrorstext": "No tienes permiso para hacer eso, por {{PLURAL:$1|el siguiente motivo|los siguientes motivos}}:",
        "permissionserrorstext-withaction": "No tienes permiso para $2, por {{PLURAL:$1|el siguiente motivo|los siguientes motivos}}:",
        "content-model-json": "JSON",
        "content-json-empty-object": "Objeto vacío",
        "content-json-empty-array": "Matriz vacía",
+       "unsupported-content-model": "<strong>Atención:</strong> en este wiki no se admite el modelo de contenidos $1.</strong>",
+       "unsupported-content-diff": "No se admiten las diferencias en el modelo de contenidos $1.",
+       "unsupported-content-diff2": "En este wiki no se admiten las diferencias entre los modelos de contenidos $1 y $2.",
        "deprecated-self-close-category": "Páginas que utilizan etiquetas HTML autocerradas no válidas",
        "deprecated-self-close-category-desc": "Esta página contiene etiquetas HTML autocerradas no válidas, tales como <code>&lt;b/></code> o <code>&lt;span/></code>. El comportamiento de estas cambiará pronto para ser coherente con la especificación de HTML5, por lo que su utilización en el wikitexto está obsoleta.",
        "duplicate-args-warning": "<strong>Aviso:</strong> [[:$1]] llama a [[:$2]] con más de un valor para el parámetro «$3». Se usará solo el último valor proporcionado.",
        "right-reupload-own": "Subir una nueva versión de un archivo propio",
        "right-reupload-shared": "Sobrescribir localmente archivos presentes en el repositorio multimedia compartido",
        "right-upload_by_url": "Subir un archivo a traves de un URL",
-       "right-purge": "Purgar la caché de una página en el servidor",
+       "right-purge": "Purgar la antememoria de una página en el servidor",
        "right-autoconfirmed": "No resultar afectado por los límites de frecuencia de edición para las IP",
        "right-bot": "Ser tratado como un proceso automático",
        "right-nominornewtalk": "No accionar el aviso de mensajes nuevos al realizar ediciones menores en páginas de discusión",
        "delete_and_move_reason": "Borrada para permitir el traslado de \"[[$1]]\"",
        "selfmove": "El título es el mismo;\nno se puede trasladar una página sobre sí misma.",
        "immobile-source-namespace": "No se pueden trasladar páginas en el espacio de nombres «$1»",
+       "immobile-source-namespace-iw": "No es posible trasladar páginas de otros wikis desde este.",
        "immobile-target-namespace": "No se puede trasladar páginas al espacio de nombres «$1»",
        "immobile-target-namespace-iw": "Un enlace interwiki no es un destino válido para trasladar una página.",
        "immobile-source-page": "Esta página no se puede renombrar.",
        "immobile-target-page": "No se puede trasladar a ese título.",
+       "movepage-invalid-target-title": "El nombre solicitado no es válido.",
        "bad-target-model": "El destino deseado utiliza un modelo diferente de contenido. No se puede realizar la conversión de $1 a $2.",
        "imagenocrossnamespace": "No se puede trasladar el archivo a un espacio de nombres que no es para archivos.",
        "nonfile-cannot-move-to-file": "No es posible trasladar lo que no es un archivo al espacio de nombres de archivos.",
        "recreate": "Recrear",
        "confirm-purge-title": "Purgar esta página",
        "confirm_purge_button": "Aceptar",
-       "confirm-purge-top": "¿Quieres vaciar la caché de esta página?",
-       "confirm-purge-bottom": "Purgar una página limpia la caché y fuerza a que aparezca la versión más actual.",
+       "confirm-purge-top": "¿Quieres vaciar la antememoria de esta página?",
+       "confirm-purge-bottom": "Purgar una página limpia la antememoria y fuerza a que aparezca la versión más actual.",
        "confirm-watch-button": "Aceptar",
        "confirm-watch-top": "¿Añadir esta página a tu lista de seguimiento?",
        "confirm-unwatch-button": "Aceptar",
index aa5e61f..4f2310a 100644 (file)
        "exif-scenetype-1": "Direktno fotografisana slika",
        "exif-customrendered-0": "Normalni proces",
        "exif-customrendered-1": "Podešeni proces",
+       "exif-customrendered-2": "HDR (bez spremlenog originala)",
+       "exif-customrendered-3": "HDR (sa spremlenim originalom)",
+       "exif-customrendered-4": "Original (za HDR)",
        "exif-exposuremode-0": "Automatska ekpozicija",
        "exif-exposuremode-1": "Ručna ekspozicija",
        "exif-exposuremode-2": "Automatski određen raspon",
index 3ad35ac..33cddd8 100644 (file)
        "sessionfailure": "Näyttää siltä, että tämänhetkisessä istunnossasi on jokin ongelma; \ntämä toiminto on peruutettu varotoimena istunnon kaappaamisen estämiseksi.\nLähetä lomake uudelleen.",
        "changecontentmodel": "Muuta sivun sisältömallia",
        "changecontentmodel-legend": "Muuta sisältömallia",
-       "changecontentmodel-title-label": "Sivun otsikko",
-       "changecontentmodel-model-label": "Uusi sisältömalli",
+       "changecontentmodel-title-label": "Sivun otsikko:",
+       "changecontentmodel-model-label": "Uusi sisältömalli:",
        "changecontentmodel-reason-label": "Syy",
        "changecontentmodel-submit": "Tee muutos",
        "changecontentmodel-success-title": "Sisältömallia on muutettu",
index 05b6874..71e9e88 100644 (file)
        "fri": "pe",
        "sat": "la",
        "january": "janyaari",
-       "february": "helmikuu",
+       "february": "febryaari",
        "march": "maaliskuu",
        "april": "huhtikuu",
        "may_long": "toukokuu",
        "june": "kesäkuu",
-       "july": "jyyli",
+       "july": "heinäkuu",
        "august": "elokuu",
        "september": "septempäri",
        "october": "lokakuu",
        "november": "marraskuu",
        "december": "tesämperi",
        "january-gen": "janyaarin",
-       "february-gen": "helmikuun",
+       "february-gen": "febryaarin",
        "march-gen": "maaliskuun",
        "april-gen": "huhtikuun",
        "may-gen": "toukokuun",
        "june-gen": "kesäkuun",
-       "july-gen": "jyylin",
+       "july-gen": "heinäkuun",
        "august-gen": "elokuun",
        "september-gen": "septempäri",
        "october-gen": "lokakuun",
@@ -76,7 +76,7 @@
        "jun": "kesäkuu",
        "jul": "heinäkuu",
        "aug": "elokuu",
-       "sep": "syyskuu",
+       "sep": "septempäri",
        "oct": "lokakuu",
        "nov": "marraskuu",
        "dec": "joulukuu",
        "createaccount": "Luo käyttäjäkonttu",
        "createacct-emailrequired": "E-postin atressi",
        "createacct-another-submit": "Luo konttu",
+       "createacct-benefit-body1": "{{PLURAL:$1|mookkaus|mookhausta}}",
        "mailmypassword": "Uusi salasana",
        "loginlanguagelabel": "Kieli: $1",
        "pt-login": "Lokkaa sisäle",
        "loginreqlink": "lokkaa sisäle",
        "newarticle": "(Uusi)",
        "newarticletext": "Linkki vei sinun sivule, joka ei vielä ole.\nSaatat luoa sivun kirjottamalla alla olehvaan kenthään (katto [$1 apusivu] lisää tietoja).\nJos et halua luoa sivua, käytä browserin <strong>takashii</strong> knappia.",
-       "noarticletext": "Tällä hetkellä tällä sivulla ei ole tekstiä.\nTällä hetkelä tällä sivula ei ole tekstiä.\nSaatat [[Special:Search/{{PAGENAME}}|hakea sivun nimelä]] muilta sivuilta,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea aiheesheen liittyviä lokkia]\neli [{{fullurl:{{FULLPAGENAME}}|action=edit}} mookata tätä sivua]</span>.",
+       "noarticletext": "Tällä hetkelä tällä sivula ei ole tekstiä.\nSaatat [[Special:Search/{{PAGENAME}}|hakea sivun nimelä]] muilta sivuilta,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea aiheesheen liittyviä lokkia]\neli [{{fullurl:{{FULLPAGENAME}}|action=edit}} luoa tämä sivu]</span>.",
        "noarticletext-nopermission": "Tällä hetkelä tällä sivula ei ole tekstiä.\nSaatat [[Special:Search/{{PAGENAME}}|hakea sivun nimelä]] muilta sivuilta,\neli <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hakea relevantista lokista]\neli [{{fullurl:{{FULLPAGENAME}}|action=edit}} mookata tätä sivua]</span>.",
        "previewnote": "'''Tämä on vasta sivun etukattelu. Sivua ei ole vielä säästetty!'''",
        "editing": "Mookathaan sivua $1",
        "right-upload": "Lattaa ylös fiiliä",
        "newuserlogpage": "Uuitten käyttäjitten loki",
        "action-edit": "mookkaa tätä sivua",
+       "action-createaccount": "luoa tätä käyttäjäkontthua",
        "action-move-categorypages": "siirä katekuurisivuja",
        "nchanges": "$1 {{PLURAL:$1|muutos|muutosta}}",
        "enhancedrc-history": "histuuria",
        "rclistfrom": "Näytä uuet muutokset jälkhiin $3 $2",
        "rcshowhideminor": "$1 pienet muutokset",
        "rcshowhidebots": "$1 ropootit",
-       "rcshowhideliu": "\n$1 sisäle lokaattuja käyttäjiä",
+       "rcshowhideliu": "$1 rekisteröityneihtä käyttäjiä",
        "rcshowhideanons": "$1 anonyymit käyttäjät",
        "rcshowhidepatr": "$1 tarkistetut muutokset",
        "rcshowhidemine": "$1 omat muutokset",
        "filehist-dimensions": "Timensuunit",
        "filehist-comment": "Komentti",
        "imagelinks": "Fiilin käyttö",
-       "linkstoimage": "Seuraava {{PLURAL:$1|sivu |$1 sivut }} länkkaavat tähhään fiilhiin:",
-       "nolinkstoimage": "Ei ole yhtään sivua joka linkkaa tähhään fiilhiin.",
+       "linkstoimage": "{{PLURAL:$1|Seuraava sivu|Seuraavat $1 sivua}} käytthävät tätä fiilhiä:",
+       "nolinkstoimage": "Ei ole yhtään sivua joka käyttää tätä fiilhiä.",
        "sharedupload-desc-here": "Tämä fiili on jaettu kohtheesta $1 ja muut prujektit saattavat käyttää sitä.\nTiot [$2 fiilin kuvvaussivulta] näkyvät tässä alla.",
        "filedelete": "Ota poies $1",
        "filedelete-legend": "Ota poies fiili",
        "tooltip-t-permalink": "Ikunen linkki tämän sivun  versuunhiin",
        "tooltip-ca-nstab-main": "Näytä sisältösivu",
        "tooltip-ca-nstab-user": "Näytä käyttäjäsivu",
-       "tooltip-ca-nstab-special": "Tämä on spesiaalisivu; sie et saata mookata itteä sivua",
+       "tooltip-ca-nstab-special": "Tämä on spesiaalisivu, sitä ei saata mookata",
        "tooltip-ca-nstab-project": "Näytä prujektisivu",
        "tooltip-ca-nstab-image": "Näytä fiilisivu",
        "tooltip-ca-nstab-template": "Näytä mallia",
        "tooltip-undo": "\"Kumota\" palauttaa tämän muutoksen ja aukasee artikkelin mookkausruutun esitarkastuksen kansa. Antaa maholisuuen kirjottaa mutiveerinkin mookkaajan yhteenvethoon",
        "tooltip-preferences-save": "Säästä inställninkit",
        "tooltip-summary": "Kirjota lyhy yhteenveto",
+       "pageinfo-header-edits": "Mookkaushistuuria",
        "pageinfo-hidden-categories": "{{PLURAL:$1|Piilotettu katekuuri|Piilotetut katekuurit}} ($1)",
        "pageinfo-category-info": "Katekuuridetaljit",
        "previousdiff": "Vanheempi muutos",
        "tag-filter": "[[Special:Tags|Merkki]] filtteri:",
        "tags-delete": "ota poies",
        "restore-count-files": "{{PLURAL:$1|1 fiili|$1 fiilit}}",
+       "logentry-upload-upload": "$1 {{GENDER:$2|ylöslattasi}} $3",
        "searchsuggest-search": "Hae {{SITENAME}}",
        "mediastatistics-header-total": "Kaikki fiilit",
        "mw-widgets-categoryselector-add-category-placeholder": "Lissää katekuuri...",
index a4310e9..73a7bc4 100644 (file)
        "backend-fail-contenttype": "Impossible de déterminer le type de contenu du fichier à stocker en « $1 ».",
        "backend-fail-batchsize": "On a fourni au support de stockage un lot de $1 {{PLURAL:$1|opération|opérations}} de fichier; la limite est $2 {{PLURAL:$2|opération|opérations}}.",
        "backend-fail-usable": "Impossible de lire ou d’écrire le fichier « $1 » en raison de droits insuffisants ou de répertoires/conteneurs manquants.",
+       "backend-fail-stat": "Impossible de lire l’état du fichier « $1 ».",
+       "backend-fail-hash": "Impossible de déterminer le hachage cryptographique du fichier « $1 ».",
        "filejournal-fail-dbconnect": "Impossible de se connecter à la base de données du journal pour le terminal de stockage « $1 ».",
        "filejournal-fail-dbquery": "Impossible de mettre à jour la base de données du journal pour le terminal de stockage « $1 ».",
        "lockmanager-notlocked": "Impossible de déverrouiller « $1 » ; elle n'est pas verrouillée.",
index 76e0366..853ef7b 100644 (file)
        "contributions": "{{GENDER:$1|वापरपी}} योगदानां",
        "contributions-title": "$1 खातीर वापरप्याचीं योगदानां",
        "mycontris": "योगदान",
+       "anoncontribs": "योगदान",
        "uctop": "हालीचें",
        "month": "ह्या म्हयन्या सावन (आनी आदलें):",
        "year": "ह्या वर्सा सावन (आनी आदलें):",
        "whatlinkshere-links": "← दुवे",
        "whatlinkshere-hideredirs": "$1 पुनर्निर्देशन",
        "whatlinkshere-hidetrans": "$1 दूस्रात-समावेश",
-       "whatlinkshere-hidelinks": "$1 à¤\9cà¥\8bडणà¥\8dयà¥\8b",
+       "whatlinkshere-hidelinks": "$1 à¤¦à¥\81वà¥\87",
        "whatlinkshere-hideimages": "$1 फायल दुवे",
        "whatlinkshere-filters": "गाळणे",
        "ipboptions": "2 वरां: 2hours ,1 दीस:1 day,3 दीस:3 days,1 सुमान:1 week,2 सुमनां:2 weeks,1 म्हयनो:1 month,3 म्हयने:3 months,6 म्हयने:6 months,1 वर्स:1 year,अनिश्चीत:infinte",
        "thumbnail_error": "$1ः लघुप्रतिमा करतांनाची चूक",
        "tooltip-pt-userpage": "तुमचें वापरपाचें पान",
        "tooltip-pt-mytalk": "तुमचें चर्चेचें पान",
-       "tooltip-pt-preferences": "तुमची पसंती",
+       "tooltip-pt-preferences": "{{GENDER:|तुमची}} पसंती",
        "tooltip-pt-watchlist": "तुमी बदल करपा खातीर देखरेख करतात त्या पानांची वळेरी",
        "tooltip-pt-mycontris": "तुमच्या योगदानांची वळेरी",
        "tooltip-pt-login": "सत्रारंभ करप बरें, पूण तशी सक्ती ना.",
        "tooltip-summary": "आपरोसाची नोंदणी करात",
        "simpleantispam-label": "एन्टी-स्पैम तपासप.\nहे भरी<strong>नकाय</strong>!",
        "pageinfo-toolboxlink": "पानाची म्हायती",
+       "pageinfo-contentpage-yes": "हय",
        "previousdiff": "← आदलें संपादन",
        "nextdiff": "नवें संपादन →",
        "file-info-size": "$1 × $2 चित्रतत्व, फायलीचो आकार: $3, माइम प्रकार: $4",
        "watchlisttools-view": "प्रस्तूत बदल पळयात.",
        "watchlisttools-edit": "सादुरवळेरी पळय आनी संपादीत करात",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|उलयात]])",
+       "redirect-submit": "वचात",
+       "redirect-value": "मोल:",
        "specialpages": "विशेश पानां",
        "tag-filter": "[[Special:Tags|कुर्वेचीट]] गाळणो:",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|कुरवेचीट|कुरवेचीटी}}]]: $2",
+       "tags-active-yes": "हय",
        "htmlform-title-not-exists": "$1 अस्तित्वांत ना.",
        "logentry-delete-delete": "$1 {{GENDER:$2|काडून उडयल्ले पान}} $3",
        "logentry-move-move": "$1 हाणें $3 पानाक $4 {{GENDER:$2|हालयला}}",
        "logentry-newusers-create": "उपयोगकत्याचें $1 {{GENDER:$2|तयार केलें}}",
        "logentry-upload-upload": "$1 {{GENDER:$2|अपलोड केला}} $3",
-       "searchsuggest-search": "सोद",
+       "searchsuggest-search": "{{SITENAME}} सोद",
        "special-characters-group-latin": "रोमी",
        "special-characters-group-latinextended": "रोमी (आनिंक-उइ)",
        "special-characters-group-ipa": "IPA",
index b3dc4d0..83a633a 100644 (file)
        "yourrealname": "Khorem nanv:",
        "prefs-help-email": "Email potto sokticho na, pun tum gupitutor visroxi zalear gupitutor punorsthapon korunk email pottechi goroz podta.",
        "prefs-help-email-others": "Tujean dusreank tujea vapurpeacho panar vo bhasabhasache panar aslele eke email duve vorvim tuje xim sompork korunk diunk zata.\nDusre tuje xim sompork kortat tednam tuzo email potto tankam kollchenam.",
+       "userrights-user-editname": "Ek vapurpeachem nanv ghal:",
        "group-bot": "Robotam",
        "group-sysop": "Karbhari",
        "group-all": "(soglle)",
        "recentchanges-legend-heading": "<strong>Kunji:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|nove pananchi suchi]]-ui polloi)",
        "rcfilters-tag-remove": "'$1' kadd",
-       "rcfilters-activefilters": "Kriaxil gallnneo",
+       "rcfilters-legend-heading": "<strong>Sonkxepachi volleri:</strong>",
+       "rcfilters-activefilters": "Kriaxil challnneo",
        "rcfilters-activefilters-hide": "Lipoi",
        "rcfilters-activefilters-show": "Dakhoi",
-       "rcfilters-advancedfilters": "Sudarit gallnneo",
+       "rcfilters-advancedfilters": "Sudarit challnneo",
+       "rcfilters-limit-title": "Dakhovpache porinnam",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|bodol}}, $2",
        "rcfilters-days-title": "Halinche dis",
-       "rcfilters-hours-title": "Halinche voram",
+       "rcfilters-hours-title": "Halinchim voram",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|dis}}",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|vor|voram}}",
-       "rcfilters-quickfilters": "Samball’lleleo gallnneo",
-       "rcfilters-savedqueries-defaultlabel": "Samball’lleleo gallnneo",
+       "rcfilters-quickfilters": "Samball’lleleo challnneo",
+       "rcfilters-savedqueries-defaultlabel": "Samball’lleleo challnneo",
        "rcfilters-savedqueries-rename": "Nanv bodol",
+       "rcfilters-savedqueries-setdefault": "Default koxem bosoi",
+       "rcfilters-savedqueries-unsetdefault": "Default aslolem kaddun uddoi",
        "rcfilters-savedqueries-remove": "Kadun udoi",
        "rcfilters-savedqueries-new-name-label": "Nanv",
+       "rcfilters-savedqueries-apply-label": "Challnni roch",
        "rcfilters-savedqueries-cancel-label": "Rod'd kor",
        "rcfilters-show-new-changes": "$1 savn noveo bodol polloi",
-       "rcfilters-search-placeholder-mobile": "Gallnneo",
+       "rcfilters-search-placeholder-mobile": "Challnneo",
+       "rcfilters-invalid-filter": "Ovoid challnni",
+       "rcfilters-empty-filter": "Kriaxil challneo nant. Sogllem iogdan dakhoilam.",
+       "rcfilters-filterlist-whatsthis": "Heo koxeo kam kortat?",
+       "rcfilters-filterlist-feedbacklink": "Hea challnnechea avtam vixim tuka kitem dista tem amkam sang",
+       "rcfilters-highlightmenu-title": "Ek rong vinch",
+       "rcfilters-filterlist-noresults": "Kosleoch challnneo mellunk nant",
+       "rcfilters-filtergroup-authorship": "Iogdanachem borovp",
        "rcfilters-filter-editsbyself-label": "Tuven kel'leo bodol",
        "rcfilters-filter-editsbyself-description": "Tujeo svotacheo yogdanam.",
        "rcfilters-filter-editsbyother-label": "Dusreanim kel'le bodol",
+       "rcfilters-filter-editsbyother-description": "Tuje khas bhairavn, soglle bodol",
+       "rcfilters-filtergroup-user-experience-level": "Vaporpeachi nondnni ani onnbhov",
+       "rcfilters-filtergroup-automated": "Apoap zalolem iogdan",
        "rcfilters-filter-bots-label": "Robot",
+       "rcfilters-filter-bots-description": "Apoap avtamni kelolem sompadon",
+       "rcfilters-filter-humans-label": "Monxan kelolem (nhoi robotan)",
+       "rcfilters-filter-humans-description": "Monxani kelolem sompadon",
+       "rcfilters-filter-reviewstatus-unpatrolled-description": "Paro kela mhonn khunnavnk naslolem sompadon.",
+       "rcfilters-filtergroup-significance": "Mhotv",
        "rcfilters-filter-minor-label": "Dhakte bodol",
+       "rcfilters-filter-minor-description": "Borovpean dhaktem mhonn khunne chitt kelolem sompadon",
+       "rcfilters-filter-major-label": "Dhakte naslolem sompadon",
+       "rcfilters-filter-major-description": "Dhaktem mhonn khunne chitt korunk naslolem sompadon",
+       "rcfilters-filtergroup-watchlist": "Sadurvollerintlim panam",
+       "rcfilters-filter-watchlist-watchednew-description": "Bodol ghoddlea uprant tuvem bhett divnk na tea sadurvollerintlim panache bodol.",
+       "rcfilters-filtergroup-watchlistactivity": "Sadurvollerichem kario",
+       "rcfilters-filter-watchlistactivity-unseen-label": "Pollovnk naslole bodol",
+       "rcfilters-filter-watchlistactivity-unseen-description": "Bodol ghoddlea uprant tuvem bhett divnk na tea panache bodol.",
+       "rcfilters-filter-watchlistactivity-seen-label": "Polloilole bodol",
+       "rcfilters-filter-watchlistactivity-seen-description": "Bodol ghoddlea uprant tuvem bhett dilolea tea panache bodol.",
+       "rcfilters-filtergroup-changetype": "Bodolacho prokar",
        "rcfilters-filter-pageedits-label": "Panacheo sompadonam",
        "rcfilters-filter-categorization-label": "Vorgache bodol",
+       "rcfilters-filter-categorization-description": "Vorgant savn pana zoddloleachi vo kaddloleachi nond",
+       "rcfilters-filter-logactions-label": "Sotran nond zal’leo kario",
+       "rcfilters-filter-logactions-description": "Karbhari kario, khatem rochop, pana kaddun uddovp, uploads...",
        "rcfilters-filtergroup-lastrevision": "Akherchim uzollnnim",
        "rcfilters-filter-lastrevision-label": "Sogleanvon novi uzollnni",
+       "rcfilters-filter-lastrevision-description": "Ek panak fokot nimannem bodol",
+       "rcfilters-filter-previousrevision-description": "Soglle bodol je \"halinchi uzollnni\" nant.",
        "rcfilters-tag-prefix-namespace-inverted": "$1 <strong>:nhoi</strong>",
+       "rcfilters-view-tags": "Khunnechittichem sompadon",
+       "rcfilters-view-tags-help-icon-tooltip": "Khunnechittichem sompadona babtint odik xikun ghe",
+       "rcfilters-target-page-placeholder": "Ek panache nanv ( vo vorg) ghal",
        "rcnotefrom": "Sokoil <strong>$3, $4<strong> savn {{PLURAL:$5|zalelem bodol dilam|zalelem bodol dileant}} (<strong>$1<strong> meren {{PLURAL:$5|dakhoilam|dakhoileant}}).",
        "rclistfrom": "$3 $2 savn suru zatelim nove bodol dakhoi",
        "rcshowhideminor": "$1 dhakte bodol",
        "prefixindex": "Panam jenche nanvache survatek asa...",
        "shortpages": "Dhaktim panam",
        "longpages": "Lamb panam",
-       "protectedpages-filters": "Gallnneo:",
+       "protectedpages-filters": "Challnneo:",
        "listusers": "Vaporpeanchi volleri",
        "usercreated": "$3 hannem $1 disa $2 vaztam rochlelem",
        "newpages": "Novim panam",
        "allpagessubmit": "Voch",
        "allpages-hide-redirects": "Punornirdexonam lipoi",
        "categories": "Vorg",
+       "sp-deletedcontributions-contribs": "iogdan",
        "linksearch-ns": "Nanv-tholl:",
        "linksearch-ok": "Sod",
        "linksearch-line": "$1 $2 savn zoddlelem asa",
        "tag-filter": "[[Special:Tags|Kurvechit]] challni:",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Kurvechit|Kurvechiti}}]]: $2",
        "tags-title": "Kurvechitti",
+       "tags-hitcount-header": "Khunnechittichem bodol",
        "tags-active-yes": "Hoi",
        "tags-active-no": "Na",
        "tags-hitcount": "$1 {{PLURAL:$1|bodol}}",
index 4d8c538..81ade2a 100644 (file)
        "rcfilters-filter-editsbyself-label": "Vua modifikuri",
        "rcfilters-filter-editsbyself-description": "Vua propra kontributaji",
        "rcfilters-filter-editsbyother-label": "Modifikuri da altri",
-       "rcfilters-filter-editsbyother-description": "Omna modififuri, ecepte vua propra.",
+       "rcfilters-filter-editsbyother-description": "Omna modifikuri, ecepte vua propra.",
        "rcfilters-filtergroup-user-experience-level": "Registro e nivelo di konoco dil uzero",
        "rcfilters-filter-user-experience-level-registered-label": "Enrejistrita",
        "rcfilters-filter-user-experience-level-registered-description": "Enrejistrita redakteri.",
        "rcfilters-filter-watchlist-watched-label": "En mea surveyo-listo",
        "rcfilters-filter-watchlist-watched-description": "Modifikuri en pagini de vua surveyo-listo.",
        "rcfilters-filter-watchlist-watchednew-label": "Nova modifikuri en la surveyo-listo",
+       "rcfilters-filter-watchlist-watchednew-description": "Chanji en pagini quin vu surveyas, ma quin vu ne vizitis pos ke la modifikuri eventis.",
        "rcfilters-filter-watchlist-notwatched-label": "Ne en surveyo-listo",
        "rcfilters-filter-watchlist-notwatched-description": "Omni, ecepte modifikuri en la pagini de vua surveyo-listo.",
        "rcfilters-filter-watchlistactivity-unseen-description": "Modifikuri en la pagini quin vu ne vizitis pos ke la modifikuri facesis.",
index e0c6afd..52a713f 100644 (file)
        "move-page-legend": "문서 이동",
        "movepagetext": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n원래의 문서는 새 문서로 넘겨주는 링크로만 남게 되고,\n원래 이름을 가리키는 넘겨주기는 자동으로 갱신됩니다.\n만약 이 설정을 선택하지 않았다면 [[Special:DoubleRedirects|이중 넘겨주기]]와 [[Special:BrokenRedirects|끊긴 넘겨주기]]를 확인해주세요.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 이름을 새 이름으로 입력했을 때는 그 문서가 넘겨주기 문서이고 문서 역사가 없어야만 이동이 됩니다. 그렇지 않을 경우에는 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의!</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
        "movepagetext-noredirectfixer": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n원래의 문서는 새 문서로 넘겨주는 링크로만 남게 됩니다.\n[[Special:DoubleRedirects|이중 넘겨주기]]와 [[Special:BrokenRedirects|끊긴 넘겨주기]]를 확인해주세요.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 이름을 새 이름으로 입력했을 때는 그 문서가 넘겨주기 문서이고 문서 역사가 없어야만 이동이 됩니다. 그렇지 않을 경우에는 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의!</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
+       "movepagetext-noredirectsupport": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 제목을 새 제목으로 입력했을 때는 그 문서가 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의:</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
        "movepagetalktext": "이 칸에 체크하면, 딸린 토론 문서가 자동으로 이동됩니다. 다만 비어있지 않은 토론 문서가 있다면 이동되지 않습니다.\n\n이러한 경우에는 수동으로 이동하거나 합쳐야 합니다.",
        "moveuserpage-warning": "<strong>경고:</strong> 사용자 문서를 이동하려고 하고 있습니다. 사용자 문서만 이동되며 사용자 이름이 바뀌지 <strong>않는다</strong>는 점을 참고하세요.",
        "movecategorypage-warning": "<strong>경고:</strong> 분류 문서를 이동하려고 합니다. 해당 문서만 이동되고 옛 분류에 있는 문서는 새 분류 안에 다시 분류되지 <em>않음</em>을 참고하세요.",
index 820de6c..b50b012 100644 (file)
        "blockedtext": "'''Tavs lietotāja vārds vai IP adrese ir nobloķēta.'''\n\n$1 nobloķēja tavu lietotāja vārdu vai IP adresi.\nBloķējot norādītais iemesls bija: ''$2''.\n\n*Bloka sākums: $8\n*Bloka beigas: $6\n*Bija domāts nobloķēt: $7\n\nTu vari sazināties ar $1 vai kādu citu [[{{MediaWiki:Grouppage-sysop}}|administratoru]] lai apspriestu šo bloku.\n\nPievērs uzmanību, tam, ka ja tu neesi norādījis derīgu e-pasta adresi ''[[Special:Preferences|savās izvēlēs]]'', tev nedarbosies \"sūtīt e-pastu\" iespēja.\n\nTava IP adrese ir $3 un bloka identifikators ir #$5. Lūdzu iekļauj vienu no tiem, vai abus, visos turpmākajos pieprasījumos.",
        "autoblockedtext": "Tava IP adrese ir tikusi automātiski nobloķēta, tāpēc, ka to (nupat kā) ir lietojis cits dalībnieks, kuru nobloķēja $1.\nNorādītais bloķēšanas iemesls bija:\n\n:''$2''\n\n* Bloka sākums: $8\n* Bloka beigas: $6\n* Bija domāts nobloķēt: $7\n\nTu vari sazināties ar $1 vai kādu citu [[{{MediaWiki:Grouppage-sysop}}|adminu]] lai apspriestu šo bloku.\n\nAtceries, ka tu nevari lietot \"sūtīt e-pastu šim dalībniekam\" iespēju, ja tu neesi norādījis derīgu e-pasta adresi savās [[Special:Preferences|dalībnieka izvelēs]] un bloķējot tev nav aizbloķēta iespēja sūtīt e-pastu.\n\nTava pašreizējā IP adrese ir $3 un  bloka ID ir $5.\nLūdzu iekļauj šos visos ziņojumos, kurus sūti adminiem, apspriežot šo bloku.",
        "blockednoreason": "iemesls nav norādīts",
+       "blockedtext-composite-no-ids": "Tava IP adrese ir iekļauta vairākos melnajos sarakstos",
        "whitelistedittext": "Lūdzu $1, lai varētu labot lapas.",
        "confirmedittext": "Lai varētu izmainīt lapas, vispirms jāapstiprina savu e-pasta adresi.\nNorādi un apstiprini e-pasta adresi savos [[Special:Preferences|lietotāja uzstādījumos]].",
        "nosuchsectiontitle": "Nevaru atrast sadaļu",
        "uploadstash-bad-path-unknown-type": "Nezināms tips \"$1\".",
        "uploadstash-bad-path-unrecognized-thumb-name": "Neatpazīts sīktēla nosaukums.",
        "uploadstash-file-not-found-no-thumb": "Nevarēja iegūt sīkbildi.",
+       "uploadstash-no-extension": "Paplašinājums ir tukšs.",
        "uploadstash-zero-length": "Faila garums ir nulle.",
        "img-auth-accessdenied": "Pieeja liegta",
        "img-auth-nopathinfo": "Trūkst PATH_INFO.\nJūsu serveris nav konfigurēts nodot šo informāciju.\nTas var būt bāzēts uz CGI un neatbalstīt img_auth.\nSkatīt https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 355f391..7ccdd86 100644 (file)
        "nocreate-loggedin": "Du har ikke tillatelse til å opprette sider.",
        "sectioneditnotsupported-title": "Seksjonsredigering støttes ikke",
        "sectioneditnotsupported-text": "Seksjonsredigering støttes ikke på denne siden.",
+       "modeleditnotsupported-title": "Redigering støttes ikke",
+       "modeleditnotsupported-text": "Redigering støttes ikke for innholdsmodellen $1.",
        "permissionserrors": "Rettighetsfeil",
        "permissionserrorstext": "Du har ikke tillatelse til å utføre dette, av følgende {{PLURAL:$1|grunn|grunner}}:",
        "permissionserrorstext-withaction": "Du har ikke tillatelse til å $2 {{PLURAL:$1|fordi|av følgende grunner}}:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Tomt objekt",
        "content-json-empty-array": "Tom matrise",
+       "unsupported-content-model": "<strong>Advarsel:</strong> Innholdsmodellen $1 støttes ikke på denne wikien.",
+       "unsupported-content-diff": "Differ støttes ikke for innholdsmodellen $1.",
+       "unsupported-content-diff2": "Differ mellom innholdsmodellene $1 og $2 støttes ikke på denne wikien.",
        "deprecated-self-close-category": "Sider som bruker ugyldige, balanserte HTML-tagger.",
        "deprecated-self-close-category-desc": "Siden inneholder balanserte HTML-tagger slike som <code>&lt;b/></code> or <code>&lt;span/></code>. Oppførselen til disse vil snart endret for å være i samsvar med HTML-spesifikasjonen, og da vil de ikke kunne brukes som wikitekst.",
        "duplicate-args-warning": "<strong>Advarsel:</strong> [[:$1]] kaller [[:$2]] med flere enn en verdi for \"$3\"-parameteren. Bare den sist angitte verdien vil brukes.",
        "rcfilters-filter-showlinkedto-label": "Vis endringer på sider som lenker til",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Sider som lenker til</strong> den valgte siden",
        "rcfilters-target-page-placeholder": "Skriv inn et sidenavn (eller en kategori)",
+       "rcfilters-allcontents-label": "Alt innhold",
+       "rcfilters-alldiscussions-label": "Alle diskusjoner",
        "rcnotefrom": "Nedenfor er vist {{PLURAL:$5|endringen|endringene}} som er gjort siden <strong>$3, $4</strong> (frem til <strong>$1</strong>).",
        "rclistfromreset": "Nullstill datovalg",
        "rclistfrom": "Vis nye endringer fra og med $3 $2",
        "backend-fail-contenttype": "Kunne ikke avgjøre innholdstypen til filen som skal lagres på «$1».",
        "backend-fail-batchsize": "Bakgrunnsprosesseringen belastet med {{PLURAL:$1|en filoperasjon|en samling av $1 filoperasjoner}}; grensen er $2.",
        "backend-fail-usable": "Kunne ikke lese eller skrive fila «$1» på grunn av utilstrekkelige tillatelser eller manglende mapper/beholdere.",
+       "backend-fail-stat": "Kunne ikke lese statusen til fila «$1».",
+       "backend-fail-hash": "Kunne ikke bestemme den kryptografiske signaturen til fila «$1».",
        "filejournal-fail-dbconnect": "Kunne ikke koble til journaldatabasen for lagringssystemet «$1».",
        "filejournal-fail-dbquery": "Kunne ikke oppdatere journaldatabasen for lagringssystemet «$1».",
        "lockmanager-notlocked": "Kunne ikke låse opp «$1» fordi den er ikke låst.",
        "sessionfailure": "Det ser ut til å være et problem med innloggingen din, og handlingen ble avbrutt av sikkerhetshensyn. Vennlgist prøv å sende skjemaet en gang til.",
        "changecontentmodel": "Endre innholdsmodell for en side",
        "changecontentmodel-legend": "Endre innholdsmodell",
-       "changecontentmodel-title-label": "Sidetittel",
+       "changecontentmodel-title-label": "Sidetittel:",
        "changecontentmodel-current-label": "Nåværende innholdsmodell:",
-       "changecontentmodel-model-label": "Ny innholdsmodell",
+       "changecontentmodel-model-label": "Ny innholdsmodell:",
        "changecontentmodel-reason-label": "Begrunnelse:",
        "changecontentmodel-submit": "Endre",
        "changecontentmodel-success-title": "Innholdsmodellen ble endret",
        "move-subpages": "Flytt alle undersider (opp til $1)",
        "move-talk-subpages": "Flytt alle undersider av diskusjonssiden (opp til $1)",
        "movepage-page-exists": "Siden $1 finnes allerede og kan ikke overskrives automatisk.",
+       "movepage-source-doesnt-exist": "Sida $1 eksisterer ikke, og kan derfor ikke flyttes.",
        "movepage-page-moved": "Siden $1 har blitt flyttet til $2.",
        "movepage-page-unmoved": "Siden $1 kunne ikke flyttes til $2.",
        "movepage-max-pages": "Grensen på {{PLURAL:$1|én side|$1 sider}} er nådd; ingen flere sider vil bli flyttet automatisk.",
        "delete_and_move_reason": "Slettet for å muliggjøre flytting fra \"[[$1]]\"",
        "selfmove": "Tittelen er den samme; kan ikke flytte en side til seg selv.",
        "immobile-source-namespace": "Kan ikke flytte sider i navnerommet «$1»",
+       "immobile-source-namespace-iw": "Sider på andre wikier kan ikke flyttes fra denne wikien.",
        "immobile-target-namespace": "Kan ikke flytte sider til navnerommet «$1»",
        "immobile-target-namespace-iw": "Du kan ikke flytte en side til et navn som er en interwikilenke.",
        "immobile-source-page": "Denne siden kan ikke flyttes.",
        "immobile-target-page": "Kan ikke flytte til det navnet.",
+       "movepage-invalid-target-title": "Det forespurte navnet er ugyldig.",
        "bad-target-model": "Det ønskede målet bruker en annen innholdsmodell. Kan ikke konvertere fra $1 til $2.",
        "imagenocrossnamespace": "Kan ikke flytte filer til andre navnerom enn filnavnerommet",
        "nonfile-cannot-move-to-file": "Kan ikke flytte ikke-filer til filnavnerom",
index 1019520..c56b4df 100644 (file)
        "category-empty": "''In disse kategoria staon op t moment nog gien artikels of media.''",
        "hidden-categories": "Verbörgen {{PLURAL:$1|kategory|kategoryen}}",
        "hidden-category-category": "Verbörgen kategorieën",
-       "category-subcat-count": "{{PLURAL:$2|Disse kategorie hef de volgende subkategorie.|Disse kategorie hef de volgende {{PLURAL:$1|subkategorie|$1 subkategorieën}}, van in totaal $2.}}",
+       "category-subcat-count": "{{PLURAL:$2|Disse kategory hevt de volgende subkategory.|Disse kategory hevt de volgende {{PLURAL:$1|subkategory|$1 subkategoryen}}, van in totaal $2.}}",
        "category-subcat-count-limited": "Disse kategorie hef de volgende {{PLURAL:$1|subkategorie|$1 subkategorieën}}.",
-       "category-article-count": "{{PLURAL:$2|In disse kategorie steet allinnig de volgende zied.|De volgende {{PLURAL:$1|zied steet|$1 ziejen staon}} in disse kategorie, van in totaal $2.}}",
+       "category-article-count": "{{PLURAL:$2|In disse kategory steyt allinnig de volgende syde.|De volgende {{PLURAL:$1|syde steyt|$1 syden stån}} in disse kategory, van in totaal $2.}}",
        "category-article-count-limited": "In disse kategorie {{PLURAL:$1|steet de volgende zied|staon de volgende $1 ziejen}}.",
        "category-file-count": "In disse kategorie {{PLURAL:$2|steet t volgende bestaand|staon de volgende $1 bestaanden, van in totaal $2}}.",
        "category-file-count-limited": "In disse kategorie {{PLURAL:$1|steet t volgende bestaand|staon de volgende $1 bestaanden}}.",
        "searchbutton": "Söken",
        "go": "Artikel",
        "searcharticle": "Artikel",
-       "history": "Geschiedenisse",
+       "history": "Geskydenisse",
        "history_short": "Geskydenisse",
        "updatedmarker": "bie-ewörken sinds mien leste bezeuk",
        "printableversion": "Afdrükbåre versy",
        "subject": "Onderwarp:",
        "minoredit": "kleine wysiging",
        "watchthis": "volg disse syde",
-       "savearticle": "Zied opslaon",
+       "savearticle": "Syde upslån",
        "savechanges": "Wysigingen upslån",
        "publishpage": "Zied uutbrengen",
        "publishchanges": "Wysigingen üütbrengen",
        "nosuchsectiontitle": "Disse seksie besteet niet",
        "nosuchsectiontext": "Je proberen n seksie te bewarken dat niet besteet.\nt Kan ween dat t herneumd is of dat t vortedaon is to jie t an t bekieken waren.",
        "loginreqtitle": "Anmelden verplicht",
-       "loginreqlink": "Anmelden",
+       "loginreqlink": "anmelden",
        "loginreqpagetext": "Je mutten $1 um disse zied te bekieken.",
        "accmailtitle": "Wachtwoord is verstuurd.",
        "accmailtext": "Der is n willekeurig wachtwoord veur [[User talk:$1|$1]] verstuurd naor $2. t Kan ewiezigd wörden op de zied ''[[Special:ChangePassword|wachtwoord wiezigen]]'' naoda'j an-emeld bin.",
        "newarticle": "(Niej)",
        "newarticletext": "Disse zied besteet nog niet.\nIn t veld hieronder ku'j wat schrieven um disse zied an te maken (meer informasie vie'j op de [$1 hulpzied]).\nA'j hier per ongelok terechtekeumen bin gebruuk dan de knoppe '''veurige''' um weerumme te gaon.",
        "anontalkpagetext": "---- ''Disse overlegzied heurt bie n anonieme gebruker die nog gien gebrukersnaam hef, of t niet gebruukt. We gebruken daorumme t IP-adres um hum of heur te herkennen, mer t kan oek ween dat meerdere personen t zelfde IP-adres gebruken, en da'j hiermee berichten ontvangen die niet veur joe bedoeld bin. A'j dit veurkoemen willen, dan ku'j t best [[Special:CreateAccount|n gebrukersnaam anmaken]] of [[Special:UserLogin|anmelden]].''",
-       "noarticletext": "Der steet noen gien tekste op disse zied.\nJe kunnen [[Special:Search/{{PAGENAME}}|de titel opzeuken]] in aandere ziejen,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} zeuken in de logboeken],\nof [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse zied anmaken]</span>.",
+       "noarticletext": "Der steyt nun geen tekst up disse syde.\nJy künnet [[Special:Search/{{PAGENAME}}|de titel upsöken]] in andere syden,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} söken in de logboken],\nof [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse syde anmaken]</span>.",
        "noarticletext-nopermission": "Op disse zied steet gien tekste.\nJe kunnen [[Special:Search/{{PAGENAME}}|zeuken naor disse term]] in aandere ziejen of\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logboeken deurzeuken]</span>, mer je hebben gien rechten um disse zied an te maken.",
        "missing-revision": "De versie #$1 van de zied \"{{FULLPAGENAME}} besteet niet.\n\nDit kömp meestentieds deur t volgen van n verouwerde verwiezing naor n zied die vortedaon is.\nWaorschienlik ku'j der meer gegevens over vienen in t [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} vortdologboek].",
        "userpage-userdoesnotexist": "Je bewarken n gebrukerszied van n gebruker die niet besteet (gebruker \"<nowiki>$1</nowiki>\"). Kiek effen nao o'j disse zied wel anmaken/bewarken willen.",
        "previousrevision": "&larr; eyrere versy",
        "nextrevision": "niejere versie &rarr;",
        "currentrevisionlink": "versie zo as t noen is",
-       "cur": "noen",
+       "cur": "aktueel",
        "next": "Volgende",
-       "last": "leste",
+       "last": "lätste",
        "page_first": "eerste",
        "page_last": "leste",
        "histlegend": "Verklaoring aofkortingen: (noen) = verschil mit de op-esleugen versie, (veurige) = verschil mit de veurige versie, K = kleine wieziging",
        "titlematches": "Overeenkomst mit t onderwarp",
        "textmatches": "Overeenkomst mit teksten",
        "notextmatches": "Gien overeenstemming",
-       "prevn": "veurige {{PLURAL:$1|$1}}",
+       "prevn": "vöärige {{PLURAL:$1|$1}}",
        "nextn": "volgende {{PLURAL:$1|$1}}",
        "prevn-title": "{{PLURAL:$1|Veurig resultaot|Veurige $1 resultaoten}}",
        "nextn-title": "{{PLURAL:$1|Volgend resultaot|Volgende $1 resultaoten}}",
        "searchprofile-articles": "Artikels",
        "searchprofile-images": "Multimedia",
        "searchprofile-everything": "Alles",
-       "searchprofile-advanced": "Uutwyded",
+       "searchprofile-advanced": "Uutbreided",
        "searchprofile-articles-tooltip": "Söken in $1",
        "searchprofile-images-tooltip": "Söken nå bestanden",
        "searchprofile-everything-tooltip": "Alle inhold döärsöken (ouk oaverlegsyden)",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|sinds joew leste bezeuk}}",
        "enhancedrc-history": "geschiedenisse",
        "recentchanges": "Lätste wysigingen",
-       "recentchanges-legend": "Opsies veur leste wiezigingen",
+       "recentchanges-legend": "Optys vöär lätste wysigingen",
        "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-minor": "Dit is een kleine wysiging",
        "recentchanges-label-bot": "Disse bewarking is uutvoord döär een bot",
        "recentchanges-label-unpatrolled": "Disse bewarking is noch neet nåkeaken",
-       "recentchanges-label-plusminus": "Disse sydgroutte is mid dit antal bytes wysigd",
+       "recentchanges-label-plusminus": "Disse sydgroutde is mid dit antal bytes wysigd",
        "recentchanges-legend-heading": "<strong>Legenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}}<br />(see ouk de [[Special:NewPages|lyste mid nye syden]])",
        "recentchanges-submit": "Bekiek",
        "rcfilters-liveupdates-button": "Rechtstreakse aktualisering",
        "rcfilters-liveupdates-button-title-off": "Nye wysigingen voorddalik låten seen",
        "rcnotefrom": "Wysigingen sinds <strong>$3, $4</strong> (maximaal <strong>$1</strong> {{PLURAL:$1|wysiging|wysigingen}}).",
-       "rclistfrom": "Bekiek wiezigingen vanaof $3 $2",
-       "rcshowhideminor": "$1 kleine wiezigingen",
+       "rclistfrom": "Bekyk wysigingen vanaf $3 $2",
+       "rcshowhideminor": "$1 kleine wysigingen",
        "rcshowhideminor-show": "Bekiek",
        "rcshowhideminor-hide": "Verbarg",
-       "rcshowhidebots": "$1 botgebrukers",
+       "rcshowhidebots": "$1 botbrukers",
        "rcshowhidebots-show": "Bekiek",
        "rcshowhidebots-hide": "Verbarg",
-       "rcshowhideliu": "$1 eregistreerden gebrukers",
+       "rcshowhideliu": "$1 registreerde brukers",
        "rcshowhideliu-show": "Bekiek",
        "rcshowhideliu-hide": "Verbarg",
-       "rcshowhideanons": "$1 anonieme gebrukers",
+       "rcshowhideanons": "$1 anonime brukers",
        "rcshowhideanons-show": "Bekiek",
        "rcshowhideanons-hide": "Verbarg",
        "rcshowhidepatr": "$1 nao-ekeken bewarkingen",
        "rcshowhidepatr-show": "Bekiek",
        "rcshowhidepatr-hide": "Verbarg",
-       "rcshowhidemine": "$1 mien bewarkingen",
+       "rcshowhidemine": "$1 myn bewarkingen",
        "rcshowhidemine-show": "Bekiek",
        "rcshowhidemine-hide": "Verbarg",
        "rcshowhidecategorization": "$1 kategorisering van ziejen",
        "rcshowhidecategorization-show": "Bekiek",
        "rcshowhidecategorization-hide": "Verbarg",
-       "rclinks": "Bekiek de leste $1 wiezigingen van de aofgeleupen $2 dagen",
+       "rclinks": "Bekyk de lätste $1 wysigingen van de vöärbye $2 dagen",
        "diff": "verskil",
        "hist": "geskydenisse",
        "hide": "verbarg",
        "filehist-filesize": "Bestaandsgrootte",
        "filehist-comment": "Kommentaar",
        "imagelinks": "Bestandsbruuk",
-       "linkstoimage": "Disse holder wördt up de volgende {{PLURAL:$1|syde|$1 syden}} gebrüked:",
+       "linkstoimage": "Dit bestand wördt up de volgende {{PLURAL:$1|syde|$1 syden}} bruked:",
        "linkstoimage-more": "Der {{PLURAL:$2|is|bin}} meer as $1 {{PLURAL:$1|verwiezing|verwiezingen}} naor dit bestaand.\nDe volgende lieste gif allinnig de eerste {{PLURAL:$1|verwiezing|$1 verwiezingen}} naor dit bestaand weer.\nDe [[Special:WhatLinksHere/$2|hele lieste]] is oek beschikbaor.",
        "nolinkstoimage": "Geen enkelde syde gebrüükt disse holder.",
        "morelinkstoimage": "[[Special:WhatLinksHere/$1|Meer verwiezingen]] naor dit bestaand bekieken.",
        "undelete-show-file-submit": "Ja",
        "namespace": "Naamruumde:",
        "invert": "Selekty ümmekeyren",
-       "tooltip-invert": "Vink dit vakjen an um wiezigingen an ziejen binnen de ekeuzen naamruumte te verbargen (en de biebeheurende naamruumte as dat an-evinkt is)",
-       "namespace_association": "Naamruumte die hieran ekoppeld is",
-       "tooltip-namespace_association": "Vink dit vakjen an um oek de overlegnaamruumte, of in t ummekeren geval de naamruumte zelf, derbie te doon die bie disse naamruumte heurt.",
+       "tooltip-invert": "Vink dit vakjen an um wysigingen an syden binnen de eköäsen naamruumde te verbargen (en de bybehöyrende naamruumde as dat anvinked is)",
+       "namespace_association": "Bybehöyrende naamruumde",
+       "tooltip-namespace_association": "Vink dit vakjen an um ouk de oaverleg- en underwarpnaamruumde in te slüten dee by de selekteerde naamruumde höyret.",
        "blanknamespace": "(Höyvdnaamruumde)",
        "contributions": "{{GENDER:$1|Gebrukersbydragen}}",
        "contributions-title": "Biedragen van $1",
        "tooltip-pt-createaccount": "Skryv juw eigen vöäral in en meld juw eigen an. Dit is lykewels neet verplicht.",
        "tooltip-ca-talk": "Låt een oaverlegtekst oaver disse syde seen",
        "tooltip-ca-edit": "Bewark disse syde",
-       "tooltip-ca-addsection": "Niej oonderwaerp tovogen",
+       "tooltip-ca-addsection": "Ny underwarp tovogen",
        "tooltip-ca-viewsource": "Disse ziede is beveiligd taegen veraanderen. Iej könt wal kieken noar de ziede",
        "tooltip-ca-history": "Oldere versys van disse syde",
        "tooltip-ca-protect": "Beveilig disse ziede taegen veraanderen",
        "tooltip-ca-nstab-mediawiki": "Loat de systeemtekstbladziede zeen",
        "tooltip-ca-nstab-template": "Loat de malbladziede zeen",
        "tooltip-ca-nstab-help": "Loat de hölpbladziede zeen",
-       "tooltip-ca-nstab-category": "Loat de rubriekbladziede zeen",
+       "tooltip-ca-nstab-category": "Låt de kategorysyde seen",
        "tooltip-minoredit": "Markeer as n klaene wieziging",
        "tooltip-save": "Wiezigingen opsloan",
        "tooltip-preview": "Bekiek oew versie vuurda'j t opsloan (anbeveulen)!",
        "thumbsize": "Grootte van de miniatuuraofbeelding:",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|zied|ziejen}}",
        "file-info": "Bestaandsgrootte: $1, MIME-type: $2",
-       "file-info-size": "$1 × $2 beeldpunten, bestaandsgrootte: $3, MIME-type: $4",
+       "file-info-size": "$1 × $2 bealdpunten, bestandsgroutde: $3, MIME-type: $4",
        "file-info-size-pages": "$1 × $2 beeldpunten, bestaandsgrootte: $3, MIME-type: $4, $5 {{PLURAL:$5|zied|ziejen}}",
        "file-nohires": "Gien hogere resolusie beschikbaor.",
        "svg-long-desc": "SVG-bestaand, uutgangsgrootte $1 × $2 beeldpunten, bestaandsgrootte: $3",
        "svg-long-desc-animated": "Bewegend SVG-bestaand, uutgangsgrootte $1 × $2 beeldpunten, bestaandsgrootte: $3",
        "svg-long-error": "Ongeldig SVG-bestaand: $1",
-       "show-big-image": "Oorspronkelik bestaand",
-       "show-big-image-preview": "Grootte van disse weergave: $1.",
-       "show-big-image-other": "Aandere {{PLURAL:$2|resolusie|resolusies}}: $1.",
+       "show-big-image": "Oorsprungelik bestand",
+       "show-big-image-preview": "Groutde van disse weadergåve: $1.",
+       "show-big-image-other": "Andere {{PLURAL:$2|resoluty|resolutys}}: $1.",
        "show-big-image-size": "$1 × $2 bealdpunten",
        "file-info-gif-looped": "herhaolend",
        "file-info-gif-frames": "$1 {{PLURAL:$1|beeld|beelden}}",
index 811a9dd..eb7b6d5 100644 (file)
        "nocreate-loggedin": "U hebt geen rechten om nieuwe pagina's te maken.",
        "sectioneditnotsupported-title": "Het is niet mogelijk om paragrafen te bewerken",
        "sectioneditnotsupported-text": "Het is op deze pagina niet mogelijk om paragrafen te bewerken.",
+       "modeleditnotsupported-title": "Bewerken wordt niet ondersteunt",
+       "modeleditnotsupported-text": "Het inhoudsmodel $1 kan niet worden bewerkt.",
        "permissionserrors": "Fouten in rechten",
        "permissionserrorstext": "U hebt geen rechten om dit te doen om de volgende {{PLURAL:$1|reden|redenen}}:",
        "permissionserrorstext-withaction": "U hebt geen toestemming om $2, {{PLURAL:$1|want}}:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Leeg object",
        "content-json-empty-array": "Lege reeks",
+       "unsupported-content-model": "<strong>Let op:</strong> het inhoudsmodel $1 wordt niet ondersteunt op deze wiki.",
+       "unsupported-content-diff": "Wijzigingen worden niet ondersteund door het inhoudsmodel $1.",
+       "unsupported-content-diff2": "Wijzigingen tussen de inhoudsmodellen $1 en $2 worden niet ondersteund op deze wiki.",
        "deprecated-self-close-category": "Pagina's met ongeldige zelfsluitende HTML-tags",
        "deprecated-self-close-category-desc": "De pagina bevat ongeldige zelf-afgesloten HTML-tags, zoals <code>&lt;b/&gt;</code> of <code>&lt;span/&gt;</code>. Het gedrag van deze tags zal binnenkort veranderd worden zodat dit overeenkomt met de HTML5-specificatie, dus het gebruik hiervan is verouderd en wordt afgeraden.",
        "duplicate-args-warning": "<strong>Waarschuwing:</strong> [[:$1]] roept [[:$2]] aan met meer dan één waarde voor de parameter \"$3\". Alleen de laatste waarde wordt gebruikt.",
        "backend-fail-contenttype": "Het inhoudstype van het bestand om in de opslag \"$1\" op te slaan kon niet bepaald worden.",
        "backend-fail-batchsize": "Taak met $1 {{PLURAL:$1|bestandshandeling|bestandshandelingen}} in het opslagbackend; de limiet is $2 {{PLURAL:$2|handeling|handelingen}}.",
        "backend-fail-usable": "Het was niet mogelijk naar het bestand $1 te schrijven of eruit te lezen vanwege onvoldoende rechten of niet-aanwezige mappen of containers.",
+       "backend-fail-stat": "Kan de bestandsstatus van \"$1\" niet lezen.",
+       "backend-fail-hash": "Kan de cryptografische hash van het bestand \"$1\" niet bepalen.",
        "filejournal-fail-dbconnect": "Het was niet mogelijk een verbinding te maken met de journaldatabase voor het opslagbackend \"$1\".",
        "filejournal-fail-dbquery": "Het was niet mogelijk de journaldatabase bij te werken voor het opslagbackend \"$1\".",
        "lockmanager-notlocked": "Het was niet mogelijk \"$1\" vrij te geven; dit object is niet vergrendeld.",
        "sessionfailure": "Er lijkt een probleem te zijn met uw aanmeldsessie.\nUw handeling is gestopt uit voorzorg tegen een beveiligingsrisico (dat bestaat uit mogelijke \"hijacking\" van deze sessie).\nProbeer het formulier opnieuw te versturen.",
        "changecontentmodel": "Inhoudsmodel van pagina bewerken",
        "changecontentmodel-legend": "Inhoudsmodel wijzigen",
-       "changecontentmodel-title-label": "Paginanaam",
+       "changecontentmodel-title-label": "Paginanaam:",
        "changecontentmodel-current-label": "Huidige inhoudsmodel:",
-       "changecontentmodel-model-label": "Nieuw inhoudsmodel",
+       "changecontentmodel-model-label": "Nieuw inhoudsmodel:",
        "changecontentmodel-reason-label": "Reden:",
        "changecontentmodel-submit": "Wijzigen",
        "changecontentmodel-success-title": "Het inhoudsmodel is gewijzigd",
index bc13353..8b9debd 100644 (file)
        "nospecialpagetext": "<strong>ߊߟߎ߫ ߓߘߊ߫ ߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߘߏ߫ ߢߌߣߌ߲߫ ߡߍ߲ ߕߺߴߦߋ߲߬.</strong>\nߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߫ ߓߘߍ߬ߡߊ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫ ߢߌ߲߬ ߠߋ߫ ߞߊ߲߬ [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "ߝߎ߬ߕߎ߲߬ߕߌ",
        "databaseerror": "ߓߟߏߡߟߊ ߟߎ߬ ߝߊ߲ ߝߎ߬ߕߎ߲߬ߕߌ",
+       "databaseerror-textcl": "ߓߟߏߡߟߊߝߊ߲ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߝߎ߬ߕߎ߲߬ߕߌ ߘߏ߫ ߓߘߊ߫ ߓߌ߬ߟߵߊ߬ ߘߐ߫.",
        "databaseerror-query": "ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ $1",
        "databaseerror-function": "ߗߋߘߊ $1",
        "databaseerror-error": "ߝߎ߬ߕߎ߲߬ߕߌ: $1",
        "uploaddisabledtext": "ߞߐߕߐ߮ ߟߊߦߟߍ߬ߟߌ ߓߐ߫ ߣߴߊ߬ ߟߊ߫.",
        "php-uploaddisabledtext": "ߞߐߕߐ߮ ߟߊߦߟߍ߬ߟߌ ߓߐ߫ ߣߴߊ߬ ߟߊ߫ PHP ߘߐ߫.\nߞߐߕߐ߮ ߟߊߦߟߍ߬ߟߌ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߝߛߍ߬ߝߛߍ߫ ߖߊ߰ߣߌ߲߬.",
        "upload-scripted-pi-callback": "ߊ߬ ߕߍߣߊ߬ ߛߐ߲߬ ߠߊ߫ ߞߐߕߐ߯ ߟߊߦߟߍ߬ ߟߊ߫ XML-stylesheet ߟߐ߲ߣߌ߲ߦߊ ߘߊ߲ߘߊߟߌ.",
+       "uploadvirus": "ߓߊ߰ߙߋ߲ ߦߋ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊ߫߹\nߝߊߙߊ߲ߝߊ߯ߛߟߌ:$1",
        "upload-source": "ߞߐߕߐ߮ ߛߎ߲",
        "sourcefilename": "ߞߐߕߐ߮ ߕߐ߮ ߛߎ߲:",
        "sourceurl": "URL ߛߎ߲:",
index 50ce52f..384e7ae 100644 (file)
        "backend-fail-batchsize": "Wewnętrzne funkcje magazynowania otrzymały $1 {{PLURAL:$1|operację|operacje|operacji}} na pliku; limit to $2 {{PLURAL:$2|operacja|operacje|operacji}}.",
        "backend-fail-usable": "Nie można zapisać lub czytać z pliku \"$1\" ze względu na niewystarczające uprawnienia lub brak katalogów/kontenerów.",
        "backend-fail-stat": "Nie udało się odczytać statusu pliku „$1”.",
+       "backend-fail-hash": "Nie udało się zdefiniować hasha kryptograficznego pliku „$1”.",
        "filejournal-fail-dbconnect": "Nie można połączyć się z bazą danych dziennika dla backendu magazynowania \"$1\".",
        "filejournal-fail-dbquery": "Nie można zaktualizować bazy danych dziennika dla backendu magazynowania\"$1\".",
        "lockmanager-notlocked": "Nie można odblokować \"$1\", ponieważ nie jest on zablokowany.",
index f1622c5..ecc86bb 100644 (file)
        "history": "Version pì veje",
        "history_short": "Stòria",
        "history_small": "stòria",
-       "updatedmarker": "agiornà da l'ùltima vira che i son passà",
+       "updatedmarker": "agiornà da l'ùltima vira ch'it ses passà",
        "printableversion": "Version bon-a për stampé",
        "permalink": "Anliura fissa",
        "print": "Stampé",
        "ns-specialprotected": "As peulo nen modifichesse le pàgine speciaj.",
        "titleprotected": "La creassion ëd pàgine con ës tìtol-sì a l'é stàita proibìa da [[User:$1|$1]].\nComa rason a l'ha butà: <em>$2</em>.",
        "filereadonlyerror": "As peul pa modifichesse l'archivi «$1» përchè ël depòsit d'archivi «$2» a l'é an sola letura.\n\nL'aministrator ch'a l'ha blocalo a l'ha lassà sta spiegassion: «$3».",
+       "invalidtitle": "Cost tìtol a va nen bin!",
        "invalidtitle-knownnamespace": "Tìtol ch'a va nen bin con lë spassi nominal «$2» e ël test «$3»",
        "invalidtitle-unknownnamespace": "Tìtol pa bon con nùmer dë spassi nominal $1 e test «$2» sconossù",
        "exception-nologin": "Nen rintrà ant ël sistema",
        "password-change-forbidden": "A peul pa modifiché le ciav dzora a costa wiki.",
        "externaldberror": "Ò che a l'é rivaje n'eror con la base ëd dàit d'autenticassion esterna, ò pura a l'é chiel che a l'é nen autorisà a agiornesse sò cont estern.",
        "login": "Conession",
+       "login-security": "Verìfica toa identità!",
        "nav-login-createaccount": "Creé un cont o rintré ant ël sistema",
        "logout": "Seurte da 'nt ël sistema",
        "userlogout": "Dësconession",
index 894b29e..45da11d 100644 (file)
        "hidden-category-category": "لڪل زمرا",
        "category-subcat-count": "{{PLURAL:$2|ھن زمري ۾ رڳو ھيٺيون ذيلي زمرو آهي.|هن زمري ۾ ڪل $2 مان ھيٺيان {{PLURAL:$1|subcategory|$1 ذيلي زمرا}} آھن.}}",
        "category-subcat-count-limited": "هن زمري ۾ هيٺيان {{PLURAL:$1|ننڍا زمرا آهن|$1 subcategories}}.",
-       "category-article-count": "{{PLURAL:$2|Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ ØµØ±Ù\81 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 ØµÙ\81Ø­Ù\88 Ø¢Ù\87Ù\8a.|Ù\87Ù\8aÙºÙ\8aاÙ\86 {{PLURAL:$1|صÙ\81Ø­Ù\88 Ø¢Ù\87Ù\8a|$1 ØµÙ\81حا Ø¢Ù\87Ù\86}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾Ø\8c Ø³Ù\85Ù\88رÙ\86 $2 Ù\85اÙ\86.}}",
+       "category-article-count": "{{PLURAL:$2|Ù\87Ù\8a Ø²Ù\85رÙ\88 Ø±Ú³Ù\88 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 ØµÙ\81Ø­Ù\88 Ø±Ú©Ù\8a Ù¿Ù\88.|سÙ\85Ù\88رÙ\86 $2 Ù\85اÙ\86Ø\8c Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86\8aاÙ\86) {{PLURAL:$1|صÙ\81Ø­Ù\88|$1 ØµÙ\81حا}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ Ø¢Ú¾Ù\8a(Ø¢Ú¾Ù\86).}}",
        "category-article-count-limited": "هيٺِون {{PLURAL:$1|صفحو آهي|$1 صفحا آهن}} تازي زمري ۾.",
-       "category-file-count": "{{PLURAL:$2|Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ ØµØ±Ù\81 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\81ائÙ\8aÙ\84 Ø¢Ù\87Ù\8a.|Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\8aا Ù\87Ù\8aÙºÙ\8aاÙ\86 {{PLURAL:$1|Ù\81ائÙ\8aÙ\84 Ø¢Ù\87Ù\8a|$1 Ù\81ائÙ\8aÙ\84 Ø¢Ù\87Ù\86}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾Ø\8c Ø³Ù\85Ù\88رÙ\86 $2 Ù\85اÙ\86.}}",
+       "category-file-count": "{{PLURAL:$2|Ù\87Ù\8a Ø²Ù\85رÙ\88 Ø±Ú³Ù\88 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\81ائÙ\8aÙ\84 Ø±Ú©Ù\8a Ù¿Ù\88.|سÙ\85Ù\88رÙ\86 $2 Ù\85اÙ\86Ø\8c Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86\8aاÙ\86) {{PLURAL:$1|Ù\81ائÙ\8aÙ\84|$1 Ù\81ائÙ\8aÙ\84Ù\8e}} Ù\87Ù\86 Ø²Ù\85رÙ\8a Û¾ Ø¢Ú¾Ù\8a(Ø¢Ú¾Ù\86).}}",
        "category-file-count-limited": "هيٺيون يا هيٺيان {{PLURAL:$1|فائيل آهي|$1 فائيل آهن}} هن تازي زمري ۾.",
        "listingcontinuesabbrev": "جاري.",
        "index-category": "ڏسڻيل صفحا",
        "filerenameerror": "\"$1\" نالي فائيل تي نئون نالو \"$2\" رکجي نہ سگھجو.",
        "filedeleteerror": "\"$1\" فائيل ڊهي نہ سگھيو.",
        "directorycreateerror": "ڊائريڪٽري $1 جُڙي نہ سگھي.",
-       "directoryreadonlyerror": "ڊائريڪٽري $1 صرف پڙهي سگھجي ٿي.",
+       "directoryreadonlyerror": "ڊائريڪٽري \"$1\" رڳو-پڙھي-سگھجي-ٿي.",
        "directorynotreadableerror": "ڊائريڪٽري $1 پڙهي نہ ٿي سگھجي.",
        "filenotfound": "\"$1\" نالي فائيل لڀجي نہ سگھيو.",
        "unexpected": "غير متوقع قدر: \"$1\"=\"$2\".",
        "editingcomment": "(نئون ڀاڱو) $1 سنواريندي",
        "editconflict": "سنوار تڪرار: $1",
        "yourtext": "توهان جو متن",
-       "storedversion": "ساÙ\86Ú\8dÙ\8aÙ\84 Ù\85سÙ\88دÙ\88",
+       "storedversion": "ساÙ\86Ú\8dÙ\8aÙ\84 Ù\88رجاءÙ\8f",
        "yourdiff": "تفاوت",
        "copyrightwarning": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 ھيٺ ڏنل ڄاتيون وڃن ٿيون (تفصيلن لاءِ $1 ڏسندا).\nجيڪڏهن اوهان نٿا چاهيو تہ اوهان جي لکڻيءَ کي بي رحميءَ سان سنواريو وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ ان کي هتي اماڻيو.<br />\nتوهان اسان سان اھو بہ وچن ڪريو ٿا تہ ھي توهان پاڻ لکيو آھي يا وري ڪنھن مفت وسيلي يا عوامي ڊومين تان نقل ڪيو آهي.\n<strong>حق-۽-واسطا-رکندڙ ڪم کان اجازت سواءِ نہ اماڻيو.</strong>",
        "copyrightwarning2": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيدارين کي ٻيا ڀاڱيدار سنواري، بدلائي، يا ڊاهي سگھن ٿا. جيڪڏهن اوهان نہ ٿا چاهيو تہ اوهان جي لکڻين کي بي رحميءَ سان سنواريون وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ پنهنجي لکڻي هتي جمع نہ ڪرايو.</br>\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو تہ توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن اهڙي ئي مفت عوامي وسيلي تان ڪاپي ڪيو آهي. (تفصيلن لاءِ $1 ڏسندا).\n\n<strong>تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ بنان هتي جمع نہ ڪريو.</strong>",
        "nohistory": "هن صفحي جي ڪا بہ سوانح نہ آهي.",
        "currentrev": "تازو-ترين ورجاءُ",
        "currentrev-asof": "تازو-ترين ترين ورجاءُ بمطابق $1",
-       "revisionasof": "$1 Ù\88ارÙ\88 Ù¾Ø±Øª",
-       "revision-info": "$1 Ø¬Ù\88 {{GENDER:$6|$2}}$7 Ø¬Ù\8a Ø³Ù\86Ù\88ار Ø¨Ø¹Ø¯ Ù\85سÙ\88دÙ\88",
-       "previousrevision": "â\86\90اÚ\83ا Ù¾Ø±Ø§Ú»Ù\88 Ù¾Ø±Øª",
-       "nextrevision": "اÚ\83ا Ù\86ئÙ\88Ù\86 Ù¾Ø±Øªâ\86\92",
-       "currentrevisionlink": "هاڻوڪو پرت",
+       "revisionasof": "$1 Ù\88ارÙ\88 Ù\88رجاءÙ\8f",
+       "revision-info": "$1 Ø¬Ù\88 {{GENDER:$6|$2}}$7 Ø·Ø±Ù\81اÙ\86 Ù\88رجاءÙ\8f",
+       "previousrevision": "â\86\92 Ø§Ú\83ا Ù¾Ø±Ø§Ú»Ù\88 Ù\88رجاءÙ\8f",
+       "nextrevision": "اÚ\83ا Ù\86ئÙ\88Ù\86 Ù\88رجاءÙ\8f â\86\90",
+       "currentrevisionlink": "تازو-ترين ورجاءُ",
        "cur": "ھاڻوڪو",
        "next": "اڳيون",
        "last": "پويون",
        "page_first": "پھريون",
        "page_last": "آخري",
-       "history-fieldset-title": "Ù\85سÙ\88دا ڇاڻيو",
-       "history-show-deleted": "رڳو ڊاٺل مسودا",
+       "history-fieldset-title": "Ù\88رجاءÙ\8e ڇاڻيو",
+       "history-show-deleted": "رڳو ڊاھيل ورجاء",
        "histfirst": "اوائلي-ترين",
        "histlast": "نئون-ترين",
        "historysize": "({{PLURAL:$1|1 بائيٽ|$1 بائيٽون}})",
        "rev-deleted-user": "(واپرائيندڙ-نانءُ ڊاٿو ويو)",
        "rev-deleted-event": "(لاگ تفصيل هٽايا ويا)",
        "rev-deleted-user-contribs": "[واپرائيندڙ-نانءُ يا آءِپِي پتو مِٽايو ويو - ڀاڱيدارين مان سنوار لڪائي وئي]",
-       "rev-suppressed-no-diff": "تÙ\88Ù\87اÙ\86 Ø§Ù\87Ù\88 ØªÙ\81اÙ\88ت Ù\86ٿا Ú\8fسÙ\8a Ø³Ú¯Ú¾Ù\88Ø\8c Ú\87اڪاڻ ØªÛ\81 Ù\85سÙ\88دن مان ڪو ھڪ <strong>ڊاھيو ويو آھي</strong>.",
+       "rev-suppressed-no-diff": "تÙ\88Ù\87اÙ\86 Ø§Ù\87Ù\88 ØªÙ\81اÙ\88ت Ù\86ٿا Ú\8fسÙ\8a Ø³Ú¯Ú¾Ù\88Ø\8c Ú\87اڪاڻ ØªÛ\81 Ù\88رجائن مان ڪو ھڪ <strong>ڊاھيو ويو آھي</strong>.",
        "rev-delundel": "نمائش تبديل ڪريو",
        "rev-showdeleted": "ڏيکاريو",
        "revisiondelete": "ورجاءَ ڊاهيو/اڻ‌ڊاهيو",
        "diff-empty": "(ڪو بہ تفاوت ڪونھي)",
        "diff-multi-sameuser": "({{PLURAL:$1|هڪ وچولو ورجاءُ|$1 وچولا ورجاءَ}} ساڳي واپرائيندڙ طرفان ظاھر نہ ٿيندا)",
        "searchresults": "ڳولا نتيجا",
-       "search-filter-title-prefix": "صرÙ\81 انھن صفحن ۾ ڳوليندي جن جو عنوان \"$1\" سان شروع ٿي ٿو.",
+       "search-filter-title-prefix": "رڳÙ\88 انھن صفحن ۾ ڳوليندي جن جو عنوان \"$1\" سان شروع ٿي ٿو.",
        "search-filter-title-prefix-reset": "سڀ صفحا ڳوليو",
        "searchresults-title": "”$1“ لاءِ ڳولا نتيجا",
        "titlematches": "صفحي جو عنوان مشابھت رکي ٿو",
        "rcfilters-filter-logactions-label": "لاگڊ عمل",
        "rcfilters-filtergroup-lastrevision": "تازا-ترين ورجاءَ",
        "rcfilters-filter-lastrevision-label": "تازو-ترين ورجاءُ",
-       "rcfilters-filter-lastrevision-description": "ÚªÙ\86Ú¾Ù\86 ØµÙ\81Ø­Ù\8a Û¾ ØµØ±Ù\81 ØªØ§Ø²Ù\8a ترين تبديلي.",
+       "rcfilters-filter-lastrevision-description": "ÚªÙ\86Ú¾Ù\86 ØµÙ\81Ø­Ù\8a Û¾ Ø±Ú³Ù\88 ØªØ§Ø²Ù\8a-ترين تبديلي.",
        "rcfilters-filter-previousrevision-label": "تازو-ترين ورجاءُ نہ",
        "rcfilters-filter-previousrevision-description": "سڀ تبديليون جيڪي \"تازو-ترين ورجاءُ\" ناھن.",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:نہ</strong> $1",
        "nlinks": "$1 {{PLURAL:$1|ڳنڍڻو|ڳنڍڻا}}",
        "nmembers": "$1 {{PLURAL:$1|رڪن|رڪنَ}}",
        "nmemberschanged": "$1 → $2 {{PLURAL:$2|رڪن|رڪنَ}}",
-       "nrevisions": "$1 {{PLURAL:$1|Ù\85سÙ\88دÙ\88\85سÙ\88دا}}",
+       "nrevisions": "$1 {{PLURAL:$1|Ù\88رجاءÙ\8f\88رجاءÙ\8e}}",
        "nimagelinks": "$1 {{PLURAL:$1|صفحي|صفحن}} ۾ استعمال ٿيل",
        "ntransclusions": "$1 {{PLURAL:$1|صفحي|صفحن}} ۾ استعمال ٿيل",
        "specialpage-empty": "ھن رپورٽ لاءِ ڪي بہ نتيجا ناھن.",
        "protectedtitles": "تحفظيل عنوان",
        "protectedtitles-submit": "عنوان ڏيکاريو",
        "listusers": "واپرائيندڙن جي فهرست",
-       "listusers-editsonly": "صرÙ\81 سنوارن وارا واپرائيندڙ ڏيکاريو",
+       "listusers-editsonly": "رڳÙ\88 سنوارن وارا واپرائيندڙ ڏيکاريو",
        "listusers-temporarygroupsonly": "صرف عارضي واپرائيندڙ گروھن ۾ واپرائيندڙ ڏيکاريو",
        "listusers-creationsort": "سرجڻ جي تاريخ سان مرتب ڪريو",
        "listusers-desc": "گھٽجندڙ ترتيب ۾ مرتب ڪريو",
        "protect-default": "سڀ واپرائيندڙن کي اجازت ڏيو",
        "protect-fallback": "\"$1\" جي اجازت وارن واپرائيندڙن کي اجازت ڏيو",
        "protect-level-autoconfirmed": "خودڪار نموني پڪ ڪيل واپرائيندڙن کي اجازت ڏيو",
-       "protect-level-sysop": "صرÙ\81 Ù\85Ù\86تظÙ\85Ù\8aن کي اجازت ڏيو",
+       "protect-level-sysop": "رڳÙ\88 Ù\85Ù\86تظÙ\85ن کي اجازت ڏيو",
        "protect-summary-cascade": "تحفظ در تحفظ",
        "protect-expiry-indefinite": "لامحدود",
        "protect-cascade": "هن صفحي ۾ شامل صفحن کي تحفظيو (تحفظ در تحفظ)",
        "protect-existing-expiry": "موجوده پڄاڻي جو وقت: $3, $2",
        "protect-existing-expiry-infinity": "موجوده پڄاڻي جو وقت: لامحدود",
        "protect-otherreason-op": "ٻيو سبب",
-       "protect-expiry-options": "1 ڪلاڪ:1 hour,1 ڏينهن:1 day,1 هفتو:1 week,2 هفتو:2 weeks,1 مهينا:1 month,3 مهينا:3 months,6 مهينا:6 months,1 سال:1 year,اڻ کٽ:infinite",
+       "protect-expiry-options": "1 ڪلاڪ:1 hour,1 ڏينھن:1 day,1 هفتو:1 week,2 هفتو:2 weeks,1 مھينا:1 month,3 مھينا:3 months,6 مھينا:6 months,1 سال:1 year,اڻ-کٽ:infinite",
        "restriction-type": "اجازتنامو:",
        "restriction-level": "روڪ سطح:",
        "minimum-size": "ننڍي ماپ ۾",
        "restriction-level-autoconfirmed": "نيم تحفظيل",
        "undelete": "ڊاٺل صفحا ڏسو",
        "viewdeletedpage": "ڊاٺل صفحا ڏسو",
-       "undelete-nodiff": "ڪوبہ اڳيون مسودو نہ لڌو",
+       "undelete-nodiff": "ڪوبہ پويون ورجاءُ نہ لڌو.",
        "undeletebtn": "بحاليو",
        "undeleteviewlink": "ڏسو",
        "undeletecomment": "سبب:",
        "sp-contributions-userrights": "{{GENDER:$1|واپرائيندڙ}} حقن-جي سنڀال",
        "sp-contributions-search": "ڀاڱيدارين لاءِ ڳولا ڪريو",
        "sp-contributions-username": "آءِپي پتو يا واپرائيندڙ-نانءُ:",
-       "sp-contributions-toponly": "صرÙ\81 Ø§Ú¾Ù\8a Ø³Ù\86Ù\88ارÙ\88Ù\86 Ú\8fÙ\8aکارÙ\8aÙ\88 Ø¬Ù\8aÚªÙ\8a ØªØ§Ø²Ø§ ØªØ±Ù\8aÙ\86 Ù\85سÙ\88دا آھن",
+       "sp-contributions-toponly": "رڳÙ\88 Ø§Ú¾Ù\8a Ø³Ù\86Ù\88ارÙ\88Ù\86 Ú\8fÙ\8aکارÙ\8aÙ\88 Ø¬Ù\8aÚªÙ\8a ØªØ§Ø²Ø§-ترÙ\8aÙ\86 Ù\88رجاءÙ\8e آھن",
        "sp-contributions-newonly": "صرف اھي سنوارون ڏيکاريو جيڪي صفحي سرجايون آھن",
        "sp-contributions-hideminor": "معمولي سنوارون لڪايو",
        "sp-contributions-submit": "ڳوليو",
        "movepagetext": "هيٺيون فارم استعمال ڪندي ڪنھن صفحي کي نئون عنوان ڏئي سگھجي ٿو، جنھن سان سمورو صفحو نئين عنوان ڏانھن هليو ويندو. \nاڳوڻو عنوان نئين عنوان ڏانھن چورڻو بڻجي ويندو. \nتوهان  چورڻن کي سنواري سگھو ٿا جيڪي اصل عنوان ڏانهن خودبخود اشارو ڪن ٿا.\nانهي ڳالھ جي پڪ ڪري وٺو تہ [[Special:BrokenRedirects|ٽٽل چورڻا]] يا [[Special:DoubleRedirects|ٻٽا چورڻا]] نہ هجن.\nان ڳالھ جي پڪ ڪرڻ ذميواري توهان تي آهي تہ ڳنڍڻا اتي ئي وٺي وڃن ٿا جتي انھن کي وٺي وڃڻ گھرجي.\n\nياد رکندا تہ جيڪڏهن نئين عنوان سان اڳي ئي ڪو مضمون موجود آهي ته پوءِ صفحو '''نہ''' چوريو ويندو، سواءِ ان جي تہ موجوده صفحو محظ خالي آهي يا ڪا بہ سوانح نہ رکندڙ ڪو چورڻو آهي.\n\n<strong>نوٽ!</strong>\nاها هڪ مقبول صفحي لاءِ ڪا غير متوقع ۽ انتھائي اڻوڻندڙ تبديلي ثابت ٿي سگھي ٿي؛ براءِ مھرباني اڳتي وڌڻ کان اڳ پڪ ڪندا تہ توهان اها تبديلي آڻڻ جي نتيجن کان چڱيءَ ريت واقف آهيو.",
        "movepagetext-noredirectfixer": "هيٺيون فارم استعمال ڪندي ڪنھن صفحي کي نئون عنوان ڏئي سگھجي ٿو، جنھن سان سمورو صفحو نئين عنوان ڏانھن هليو ويندو. \nاڳوڻو عنوان نئين عنوان ڏانھن چورڻو بڻجي ويندو. \nتوهان  چورڻن کي سنواري سگھو ٿا جيڪي اصل عنوان ڏانھن خودبخود اشارو ڪن ٿا.\nانهي ڳالھ جي پڪ ڪري وٺو تہ [[Special:BrokenRedirects|ٽٽل چورڻا]] يا [[Special:DoubleRedirects|ٻٽا چورڻا]] نہ هجن.\nان ڳالھ جي پڪ ڪرڻ ذميواري توهان تي آهي تہ ڳنڍڻا اتي ئي وٺي وڃن ٿا جتي انھن کي وٺي وڃڻ گھرجي.\n\nياد رکندا تہ جيڪڏهن نئين عنوان سان اڳي ئي ڪو مضمون موجود آهي ته پوءِ صفحو '''نہ''' چوريو ويندو، سوا ان جي تہ موجوده صفحو محظ خالي آهي يا ڪا بہ سوانح نہ رکندڙ ڪو چورڻو آهي.\n\n<strong>نوٽ!</strong>\nاها هڪ مقبول صفحي لاءِ ڪا غير متوقع ۽ انتھائي اڻوڻندڙ تبديلي ثابت ٿي سگھي ٿي؛ مھرباني ڪري اڳتي وڌڻ کان اڳ پڪ ڪندا تہ توهان اها تبديلي آڻڻ جي نتيجن کان چڱيءَ ريت واقف آهيو.",
        "movepagetalktext": "جيڪڏهن توهان هن خاني کي نشان لڳائيندئو، واسطيدار مباحثي صفحو پاڻ ئي چوريو ويندو ماسواءِ اتي ڪو اڳ ئي ڪو غيرخالي مباحثي صفحو موجود هجي.\n\nان صورت ۾، جيڪڏهن توهان چاهيو ته صفحي کي پاڻ چوري يا ضم ڪري سگھو ٿا.",
-       "movecategorypage-warning": "<strong>Ú\86تاءÙ\8f:</strong> Ø§Ù\88Ù\87اÙ\86 Ø²Ù\85رÙ\8a Ù\88ارÙ\8a ØµÙ\81Ø­Ù\8a Ú©Ù\8a Ú\86Ù\88رڻ Ù\88Ú\83Ù\8a Ø±Ù\87Ù\8aا Ø¢Ù\87Ù\8aÙ\88. Ù\8aاد Ø±Ú©Ù\88 ØµØ±Ù\81 ØµÙ\81Ø­Ù\88 Ú\86Ù\88رÙ\86دÙ\88Ø\8c Ø¬Ù\8aÚªÚ\8fÙ\87Ù\86 ÚªÙ\8a Ø¨Ù\87 ØµÙ\81حا Ù¾Ø±Ø§Ú»Ù\8a Ø²Ù\85رÙ\8a Û¾ Ø´Ø§Ù\85Ù\84 Ø¢Ù\87Ù\86Ø\8c Ø§Ù\86Ù\87Ù\86 Ø¬Ù\8a Ù\86ئÙ\8aÙ\86 Ø²Ù\85رÙ\8a Û¾ Ø¯Ø±Ø¬Ø§Ø¨Ù\86دÙ\8a <em>Ù\86Ù\87</em> Ù¿يندي.",
+       "movecategorypage-warning": "<strong>Ú\86تاءÙ\8f:</strong> Ø§Ù\88Ù\87اÙ\86 Ø²Ù\85رÙ\8a Ù\88ارÙ\8a ØµÙ\81Ø­Ù\8a Ú©Ù\8a Ú\86Ù\88رڻ Ù\88Ú\83Ù\8a Ø±Ù\87Ù\8aا Ø¢Ù\87Ù\8aÙ\88. Ù\8aاد Ø±Ú©Ù\88 Ø±Ú³Ù\88 ØµÙ\81Ø­Ù\88 Ú\86رÙ\86دÙ\88Ø\8c Ù¾Ø±Ø§Ú»Ù\8a Ø²Ù\85رÙ\8a Û¾ ÚªÙ\86 Ø¨Û\81 ØµÙ\81Ø­Ù\86 Ø¬Ù\8a Ù\86ئÙ\8aÙ\86 ØµÙ\81Ø­Ù\8a Û¾ Ù»Ù\8aھر-زÙ\85راڪارÙ\8a <em>Ù\86Û\81</em> ÚªØ¦Ù\8a Ù\88يندي.",
        "movenotallowed": "توهان کي صفحا چورڻ جي اجازت حاصل ڪانهي.",
        "movenotallowedfile": "توهان کي فائيلس چورڻ جي اجازت حاصل ڪانهي.",
        "newtitle": "نئون عنوان:",
        "redirect-value": "قدر:",
        "redirect-user": "واپرائيندڙ آءِڊي",
        "redirect-page": "صفحي جي آءِڊي",
-       "redirect-revision": "صÙ\81Ø­Ù\8a Ø¬Ù\88 Ù\85سÙ\88دÙ\88",
+       "redirect-revision": "صÙ\81Ø­Ù\8a Ø¬Ù\88 Ù\88رجاءÙ\8f",
        "redirect-file": "فائيل‌نانءُ",
        "fileduplicatesearch-filename": "فائيل‌نانءُ:",
        "fileduplicatesearch-submit": "ڳوليو",
        "htmlform-title-not-exists": "$1 وجود نٿو رکي.",
        "logentry-delete-delete": "$1 {{GENDER:$2|ڊاٿو}} صفحو $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|بحاليو}} صفحو $3 ($4)",
-       "logentry-delete-revision": "$1 $3: $4 ØµÙ\81Ø­Ù\8a ØªÙ\8a {{PLURAL:$5|Ú¾Úª Ù\85سÙ\88دÙ\8a|$5 Ù\85سÙ\88دن}} جي ظاھريت {{GENDER:$2|تبديل ڪئي}}",
+       "logentry-delete-revision": "$1 $3: $4 ØµÙ\81Ø­Ù\8a ØªÙ\8a {{PLURAL:$5|Ú¾Úª Ù\88رجاءÙ\8e|$5 Ù\88رجائن}} جي ظاھريت {{GENDER:$2|تبديل ڪئي}}",
        "revdelete-content-hid": "مواد لڪيل",
        "revdelete-uname-hid": "واپرائيندڙ-نانءُ لڪل",
        "revdelete-unrestricted": "منتظمن تان پابنديون ھٽايون ويون",
index a66ead3..a4c30f8 100644 (file)
        "backend-fail-contenttype": "Ne mogu da utvrdim kakav sadržaj ima datoteka koju treba da smestim u „$1“.",
        "backend-fail-batchsize": "Skladišna osnova je dobila blokadu od $1 {{PLURAL:$1|operacije|operacije|operacija}}; ograničenje je $2 {{PLURAL:$2|operacija|operacije|operacija}}.",
        "backend-fail-usable": "Ne mogu pročitati ni snimiti datoteku $1 zbog nedovoljno dozvola ili nedostattnih direktorija/sadržaoca.",
+       "backend-fail-stat": "Nisam mogao pročitati stanje datoteke \"$1\".",
+       "backend-fail-hash": "Nisam mogao odrediti kriptografsku tarabu datoteke \"$1\".",
        "filejournal-fail-dbconnect": "Ne mogu da se povežem s novinarskom bazom za skladišnu osnovu „$1“.",
        "filejournal-fail-dbquery": "Ne mogu da ažuriram novinarsku bazu za skladišnu osnovu „$1“.",
        "lockmanager-notlocked": "Ne mogu da otključam „$1“ jer nije zaključan.",
        "sessionfailure": "Izgleda da postoji problem sa vašom sesijom;\nova radnja je otkazana kao prevencija protiv napadanja sesija.\nMolimo ponovno pošaljite obrazac.",
        "changecontentmodel": "Promijeni model sadržaja stranice",
        "changecontentmodel-legend": "Promijeni model sadržaja",
-       "changecontentmodel-title-label": "Naslov stranice",
+       "changecontentmodel-title-label": "Naslov stranice:",
        "changecontentmodel-current-label": "Trenutni sadržajni model:",
-       "changecontentmodel-model-label": "Novi model sadržaja",
+       "changecontentmodel-model-label": "Novi model sadržaja:",
        "changecontentmodel-reason-label": "Razlog:",
        "changecontentmodel-submit": "Smijeni",
        "changecontentmodel-success-title": "Model sadržaja je promijenjen",
index be0253c..7cd6220 100644 (file)
        "nocreate-loggedin": "Za ustvarjanje novih strani nimate dovoljenja.",
        "sectioneditnotsupported-title": "Urejanje razdelkov ni podprto",
        "sectioneditnotsupported-text": "Urejanje razdelkov ni podprto na tej strani.",
+       "modeleditnotsupported-title": "Urejanje ni podprto",
+       "modeleditnotsupported-text": "Urejanje ni podprto za model vsebine $1.",
        "permissionserrors": "Napaka dovoljenja",
        "permissionserrorstext": "Za izvedbo dejanja nimate dovoljenja zaradi {{PLURAL:$1|naslednjega razloga|naslednjih razlogov}}:",
        "permissionserrorstext-withaction": "Za $2 zaradi {{PLURAL:$1|naslednjega razloga|naslednjih razlogov}} nimate dovoljenja:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Prazen objekt",
        "content-json-empty-array": "Prazno polje",
+       "unsupported-content-model": "<strong>Opozorilo:</strong> Model vsebine $1 ni podprt na tem wikiju.",
+       "unsupported-content-diff": "Primerjave niso podprte za model vsebine $1.",
+       "unsupported-content-diff2": "Primerjave med modeloma vsebine $1 in $2 niso podprte na tem wikiju.",
        "deprecated-self-close-category": "Strani, ki uporabljajo neveljavne samozaključljive oznake HTML",
        "deprecated-self-close-category-desc": "Stran uporablja neveljavne samozaključljive oznake HTML, kot sta <code>&lt;b/></code> ali <code>&lt;span/></code>. Njihovo vedenje se bo kmalu spremenilo, da bo v skladu s specifikacijo HTML5, zato njihova uporaba v wikibesedilu ni zaželena.",
        "duplicate-args-warning": "<strong>Opozorilo:</strong> [[:$1]] kliče [[:$2]] z več kot eno vrednostjo za parameter »$3«. Uporabili bomo samo zadnjo navedeno vrednost.",
        "backend-fail-contenttype": "Ne morem določiti vrsto vsebine datoteke za shranjevanje pri »$1«.",
        "backend-fail-batchsize": "Skladiščnemu zaledju je dana vrsta $1 {{PLURAL:$1|datotečne operacije|datotečnih operacij}}; omejitev {{PLURAL:$2|je $2 operacija|sta $2 operaciji|so $2 operacije|je $2 operacij}}.",
        "backend-fail-usable": "Ne morem prebrati ali zapisati datoteke »$1« zaradi nezadostnih dovoljenj ali manjkajočega imenika/vsebnika.",
+       "backend-fail-stat": "Ne moremo prebrati stanja datoteke »$1«.",
+       "backend-fail-hash": "Ne moremo določiti kriptografske zgoščene vrednosti datoteke »$1«.",
        "filejournal-fail-dbconnect": "Ne morem se povezati z listovno zbirko podatkov za skladiščno zaledje »$1«.",
        "filejournal-fail-dbquery": "Ne morem posodobiti listovne zbirke podatkov za skladiščno zaledje »$1«.",
        "lockmanager-notlocked": "Ne morem odkleniti »$1«, saj ni zaklenjeno.",
        "sessionfailure": "Zdi se, da z vašo sejo prijave obstaja težava;\nto dejanje smo preklicali, da bi preprečili morebitno ugrabitev seje. Prosimo, ponovno potrdite obrazec.",
        "changecontentmodel": "Spremeni model vsebine strani",
        "changecontentmodel-legend": "Spremeni model vsebine",
-       "changecontentmodel-title-label": "Naslov strani",
+       "changecontentmodel-title-label": "Naslov strani:",
        "changecontentmodel-current-label": "Trenutni model vsebine:",
-       "changecontentmodel-model-label": "Novi model vsebine",
+       "changecontentmodel-model-label": "Novi model vsebine:",
        "changecontentmodel-reason-label": "Razlog:",
        "changecontentmodel-submit": "Spremeni",
        "changecontentmodel-success-title": "Spremenili smo model vsebine",
index 4c95a58..6f15977 100644 (file)
        "editpage-notsupportedcontentformat-title": "Формат садржаја није подржан",
        "editpage-notsupportedcontentformat-text": "Формат садржаја $1 није подржан за модел садржаја $2.",
        "slot-name-main": "Главни",
-       "content-model-wikitext": "викитекста",
+       "content-model-wikitext": "викитекст",
        "content-model-text": "чистог текста",
        "content-model-javascript": "јаваскрипта",
        "content-model-css": "Це-Ес-Еса",
index 9f4edad..d9e6449 100644 (file)
        "editpage-notsupportedcontentformat-title": "Format sadržaja nije podržan",
        "editpage-notsupportedcontentformat-text": "Format sadržaja $1 nije podržan za model sadržaja $2.",
        "slot-name-main": "Glavni",
-       "content-model-wikitext": "vikiteksta",
+       "content-model-wikitext": "vikitekst",
        "content-model-text": "čistog teksta",
        "content-model-javascript": "JavaScript-a",
        "content-model-css": "CSS-a",
index 9a5a4a3..d80d061 100644 (file)
        "talk": "Dyskusyjŏ",
        "views": "Widoki",
        "toolbox": "Nŏrzyńdzia",
+       "tool-link-emailuser": "Wyślij e-mail do {{GENDER:$1|tego używŏcza|tyj używŏczki|tego używŏcza}}",
        "imagepage": "Uobejrz zajta pliku",
        "mediawikipage": "Zajta komuńikata",
        "templatepage": "Zajta mustra",
        "logouttext": "'''Terozki jeżeś wylůgowany'''.\n\nDej pozůr, co na ńykerych zajtach przeglůndarka może dali pokozywać co jeżeś zalůgowany, a bydźe tak aże uodśwjyżysz jeij cache.",
        "welcomeuser": "Witej, $1",
        "welcomecreation-msg": "Uotwarli my sam lo Ćebje kůnto.\nPamjyntej coby posztalować [[Special:Preferences|preferencyji]]",
-       "yourname": "Mjano użytkowńika:",
+       "yourname": "Miano ôd używŏcza:",
        "userlogin-yourname": "Miano używŏcza",
        "userlogin-yourname-ph": "Wkludź swoje miano używŏcza",
-       "createacct-another-username-ph": "Wszkryflej mjano użytkowńika",
+       "createacct-another-username-ph": "Wkludź miano ôd używŏcza",
        "yourpassword": "Hasło:",
        "userlogin-yourpassword": "Hasło",
        "userlogin-yourpassword-ph": "Wkludź swoje hasło",
        "loginsuccess": "'''Terozki jeżeś zalogowany do {{SITENAME}} kej \"$1\".'''",
        "nosuchuser": "Niy ma używŏcza ô mianie \"$1\".\nBadnij szrajbōng, abo [[Special:CreateAccount|sprŏw nowe kōnto]].",
        "nosuchusershort": "Ńy mo sam użytkowńika uo mjańe \"$1\".",
-       "nouserspecified": "Podej mjano użytkowńika.",
+       "nouserspecified": "Musisz podać miano ôd używŏcza.",
        "login-userblocked": "Tyn sprowjorz mo zawarte sprowjyńa. Ńy idźe śe zalogować.",
        "wrongpassword": "Hasło kere żeś naszkryfloł je felerne. Poprůbůj naszkryflać je jeszcze roz.",
        "wrongpasswordempty": "Hasło kere żeś podou je uostawjůne blank. Naszkryflej je jeszcze roz.",
        "undo-success": "Sprowjyńy zostoło wycofane. Prosza pomjarkować ukozane půniżyj dyferencyje mjyndzy wersyjůma, coby zweryfikować jejich poprawność, potym zaś naszkryflać pomjyńańo coby zakończyć uoperacyjo.",
        "undo-failure": "Ta edycyjŏ niy może być cŏfniyntŏ skuli kōnfliktu ze wersyjami postrzednimi.",
        "undo-norev": "Edycyje niy idzie cŏfnōńć, bo ôna niy istniyje abo była wyciepniyntŏ.",
-       "undo-summary": "WycůfaÅ\84y wersyji $1 naszkryflanej bez [[Special:Contributions/$2|$2]] ([[User talk:$2|godka]])",
+       "undo-summary": "WycÅ\8ffanie wersyje $1 Ã´d [[Special:Contributions/$2|$2]] ([[User talk:$2|dyskusyjÅ\8f]])",
        "cantcreateaccount-text": "Tworzyńy kůnta s tygo adresu IP ('''$1''') uostoło zawarte bez użytkowńika [[User:$3|$3]].\n\nSkuli: ''$2''",
        "viewpagelogs": "Ôbejzdrz regesty dlŏ tyj strōny",
        "nohistory": "Ta zajta ńy mo swojij historyje sprowjyń.",
        "revdelete-modify-no-access": "Feler przy zmjyńe widoczności wersyji $2, $1. Ńy mosz uprawńeń lo ńygo.",
        "revdelete-modify-missing": "Feler. Ńy mo tajli $1 w baźe.",
        "revdelete-no-change": "''''Dej pozůr''': element $2, $1 mo już ustawjonům widoczność.",
-       "revdelete-concurrent-change": "Feler. Pomjyno już element $2, $1. Prosza uoboczyć to w rejerze.",
+       "revdelete-concurrent-change": "Feler przi modyfikacyji elymyntu ze $2 $1: Wyglōndŏ na to, że jego status bōł zmiyniōny ôd kogoś w czasie Twojij roboty.\nWejzdrzij do regestu.",
        "revdelete-only-restricted": "Ńy do śe ukryć tajli $2, $1 przed administracyjom. Wybjer jydnom ze uopcyji.",
        "revdelete-reason-dropdown": "* Kůmyntorze lo wyćepańa\n** NPA\n** Prywatność",
        "revdelete-otherreason": "Inkszy/dodatkowy powůd:",
        "yourrealname": "Prawdźiwe mjano",
        "yourlanguage": "Godka interfejsu",
        "yournick": "Twoja szrajbka:",
-       "badsig": "Felerno szrajbka, sprawdź znaczńiki HTML.",
+       "badsig": "Felerny podpis, wejzdrzij na znaczniki HTML.",
        "badsiglength": "Twojo szrajbka je za dugo. Ji maksymalno dugość to $1 {{PLURAL:$1|buchsztaby|buchsztabůw}}",
        "yourgender": "Płeć:",
        "gender-unknown": "ńyznano",
        "usereditcount": "$1 {{PLURAL:$1|sprowjyńe|sprowjyńa|sprowjyń}}",
        "usercreated": "{{GENDER:$3|Utworzono}} $1 uo $2",
        "newpages": "Nowe strōny",
-       "newpages-username": "Mjano użytkowńika:",
+       "newpages-username": "Miano ôd używŏcza:",
        "ancientpages": "Nojstarše artikle",
        "move": "Przeniyś",
        "movethispage": "Přećepej ta zajta",
        "mailnologin": "Brak adresu",
        "mailnologintext": "Muśyš śe [[Special:UserLogin|zalůgować]] i mjeć wpisany aktualny adres e-brif w swojich [[Special:Preferences|preferyncyjach]], coby můc wysuać e-brif do inkšygo užytkowńika.",
        "emailuser": "Poślij tymu używŏczowi e-mail",
+       "emailuser-title-target": "Wyślij e-mail do {{GENDER:$1|tego używŏcza|tyj używŏczki|tego używŏcza}}",
        "emailpagetext": "Możesz użyć půńiższygo formularza, coby wysłać wjadůmość e-brif do tygo użytkowńika.\nAdres e-brifa, kery zostoł bez Ćebje wkludzůny we [[Special:Preferences|Twojich sztalowańach]], pojawi śe we polu „Uod”, bez cůż uodbjorca bydźe můg Ći uodpedźeć.",
        "defemailsubject": "{{SITENAME}} - e-mail ôd używŏcza \"$1\"",
        "usermaildisabled": "E-mail ôd używŏcza je zastŏwiōny",
        "protect-expiry-options": "2 godźiny:2 hours,1 dźyń:1 day,3 dńi:3 days,1 tydźyń:1 week,2 tygodńy:2 weeks,1 mjeśůnc:1 month,3 mjeśůnce:3 months,6 mjeśency:6 months,1 rok:1 year,ńyskůńčůny:infińite",
        "restriction-type": "Pozwolyńy:",
        "restriction-level": "Poźům:",
-       "minimum-size": "Min. wjelgość",
-       "maximum-size": "Maksymalno wjelgość",
-       "pagesize": "(bajtůw)",
+       "minimum-size": "Minimalnŏ srogość",
+       "maximum-size": "Maksymalnŏ srogość:",
+       "pagesize": "(bajtÅ\8dw)",
        "restriction-edit": "Edytuj",
        "restriction-move": "Pōnknij",
        "restriction-create": "Stwůř",
        "tooltip-invert": "Ôznŏcz te pole, coby skryć zmiany na strōnach we ôbranyj przestrzyni mian (i swiōnzanōm z niōm inkszōm przestrzyniōm mian, jeźli je ôznaczōnŏ)",
        "namespace_association": "Swiōnzanŏ przestrzyń mian",
        "tooltip-namespace_association": "Ôznŏcz te pole, coby przidać strōnã dyskusyje i tymat swiōnzane ze ôbranōm przestrzyniōm mian",
-       "blanknamespace": "(przodńo)",
+       "blanknamespace": "(Przodniŏ)",
        "contributions": "Wkłŏd ôd {{GENDER:$1|używŏcza|używŏczki}}",
        "contributions-title": "Wkłŏd {{GENDER:$1|używŏcza|używŏczki}} $1",
        "mycontris": "Edycyje",
        "autosumm-blank": "POZŮR! Usůńjyńće treśći (zajta pozostoła pusto)!",
        "autosumm-replace": "POZŮR! Zastůmpjyńy treśći hasua bardzo krůtkym tekstym: „$1”",
        "autoredircomment": "Przekerowanie do [[$1]]",
-       "autosumm-new": "Wćepano nowo zajta: \"$1\"",
+       "autosumm-new": "Stworzōnŏ nowõ strōnã: \"$1\"",
        "lag-warn-normal": "Na tyj liśće zmjany nowsze jak {{PLURAL:$1|sekůnda|sekůnd}} můgům ńy być widoczne.",
        "lag-warn-high": "S kuli srogigo uobćůnżyńo serwerůw bazy danych, na tyj liśće zmjany nowše jak {{PLURAL:$1|sekůnda|sekůnd}} můgům ńy być widoczne.",
        "watchlistedit-normal-title": "Sprowjej lista zajtůw na kere dowom pozůr",
        "fileduplicatesearch-summary": "Šnupej za duplikatůma plika na podstawje wartośći fůnkcyji skrůtu.",
        "fileduplicatesearch-filename": "Mjano pliku:",
        "fileduplicatesearch-submit": "Šnupej",
-       "fileduplicatesearch-info": "$1 × $2 pikseli<br />Wjelgość plika: $3<br />Typ MIME: $4",
+       "fileduplicatesearch-info": "$1 × $2 pikselōw<br />Srogość zbioru: $3<br />Typ MIME: $4",
        "fileduplicatesearch-result-1": "Ńy ma duplikatu pliku „$1”.",
        "fileduplicatesearch-result-n": "We {{GRAMMAR:MS.lp|{{SITENAME}}}} {{PLURAL:$2|je dodatkowo kopia|sům $2 dodatkowe kopje|je $2 dodatkowych kopii}} plika „$1”.",
        "specialpages": "Ekstra strōny",
index d899590..6d6dbe5 100644 (file)
@@ -89,7 +89,11 @@ abstract class Maintenance {
        // Const for getStdin()
        const STDIN_ALL = 'all';
 
-       // Array of desired/allowed params
+       /**
+        * Array of desired/allowed params
+        * @var array[]
+        * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
+        */
        protected $mParams = [];
 
        // Array of mapping short parameters to long ones
@@ -128,9 +132,17 @@ abstract class Maintenance {
         */
        protected $mBatchSize = null;
 
-       // Generic options added by addDefaultParams()
+       /**
+        * Generic options added by addDefaultParams()
+        * @var array[]
+        * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
+        */
        private $mGenericParameters = [];
-       // Generic options which might or not be supported by the script
+       /**
+        * Generic options which might or not be supported by the script
+        * @var array[]
+        * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
+        */
        private $mDependantParameters = [];
 
        /**
index 558fec8..357bf1b 100644 (file)
@@ -59,6 +59,7 @@ class BenchmarkTidy extends Benchmarker {
                $min = $times[0];
                $max = end( $times );
                if ( $n % 2 ) {
+                       // @phan-suppress-next-line PhanTypeMismatchDimFetch
                        $median = $times[ ( $n - 1 ) / 2 ];
                } else {
                        $median = ( $times[$n / 2] + $times[$n / 2 - 1] ) / 2;
index 737e65f..409ecdf 100644 (file)
@@ -279,7 +279,6 @@ class ConvertExtensionToRegistration extends Maintenance {
        /**
         * @param string $realName
         * @param array[] $value
-        * @suppress PhanTypeInvalidDimOffset
         */
        protected function handleResourceModules( $realName, $value ) {
                $defaults = [];
index 7b2ef17..cb95b68 100644 (file)
@@ -66,9 +66,7 @@ class DeleteArchivedFiles extends Maintenance {
                                continue;
                        }
 
-                       /** @var LocalFile $file */
                        $file = $repo->newFile( $row->fa_name );
-                       '@phan-var LocalFile $file';
                        try {
                                $file->lock();
                        } catch ( LocalFileLockError $e ) {
index e4d1f09..4065978 100644 (file)
@@ -332,7 +332,6 @@ class ImportImages extends Maintenance {
 
                                if ( $this->hasOption( 'dry' ) ) {
                                        $this->output( "done.\n" );
-                                       // @phan-suppress-next-line PhanUndeclaredMethod
                                } elseif ( $image->recordUpload2(
                                        $archive->value,
                                        $summary,
@@ -358,7 +357,7 @@ class ImportImages extends Maintenance {
                                                # Protect the file
                                                $this->output( "\nWaiting for replica DBs...\n" );
                                                // Wait for replica DBs.
-                                               sleep( 2.0 ); # Why this sleep?
+                                               sleep( 2 ); # Why this sleep?
                                                wfWaitForSlaves();
 
                                                $this->output( "\nSetting image restrictions ... " );
index 60f88ba..6a04b98 100644 (file)
@@ -38,7 +38,7 @@ class OrphanStats extends Maintenance {
                        "Show some statistics on the blob_orphans table, created with trackBlobs.php" );
        }
 
-       protected function &getDB( $cluster, $groups = [], $wiki = false ) {
+       protected function getDB( $cluster, $groups = [], $wiki = false ) {
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $lb = $lbFactory->getExternalLB( $cluster );
 
index 7d301a9..c0ff44b 100644 (file)
@@ -197,4 +197,46 @@ class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
                return [ 'slot_revision_id' => $revId ];
        }
 
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+        * @throws \MWException
+        */
+       public function testNewRevisionsFromBatch_error() {
+               $page = $this->getTestPage();
+               $text = __METHOD__ . 'b-ä';
+               /** @var Revision $rev1 */
+               $rev1 = $page->doEditContent(
+                       new WikitextContent( $text . '1' ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+               $invalidRow = $this->revisionToRow( $rev1 );
+               $invalidRow->rev_id = 100500;
+               $result = MediaWikiServices::getInstance()->getRevisionStore()
+                       ->newRevisionsFromBatch(
+                               [ $this->revisionToRow( $rev1 ), $invalidRow ],
+                               [
+                                       'slots' => [ SlotRecord::MAIN ],
+                                       'content' => true
+                               ]
+                       );
+               $this->assertFalse( $result->isGood() );
+               $this->assertNotEmpty( $result->getErrors() );
+               $records = $result->getValue();
+               $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] );
+               $this->assertSame( $text . '1',
+                       $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+               $this->assertEquals( $page->getTitle()->getDBkey(),
+                       $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() );
+               $this->assertNull( $records[$invalidRow->rev_id] );
+               $this->assertSame( [ [
+                       'type' => 'warning',
+                       'message' => 'internalerror',
+                       'params' => [
+                               "Couldn't find slots for rev 100500"
+                       ]
+               ] ], $result->getErrors() );
+       }
 }
index 55bfab7..b0b9ddf 100644 (file)
@@ -102,23 +102,24 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        /**
+        * @param string|null $pageTitle whether to force-create a new page
         * @return WikiPage
         */
-       protected function getTestPage() {
-               if ( $this->testPage ) {
+       protected function getTestPage( $pageTitle = null ) {
+               if ( !is_null( $pageTitle ) && $this->testPage ) {
                        return $this->testPage;
                }
 
-               $title = $this->getTestPageTitle();
-               $this->testPage = WikiPage::factory( $title );
+               $title = is_null( $pageTitle ) ? $this->getTestPageTitle() : Title::newFromText( $pageTitle );
+               $page = WikiPage::factory( $title );
 
-               if ( !$this->testPage->exists() ) {
+               if ( !$page->exists() ) {
                        // Make sure we don't write to the live db.
                        $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
 
                        $user = static::getTestSysop()->getUser();
 
-                       $this->testPage->doEditContent(
+                       $page->doEditContent(
                                new WikitextContent( 'UTContent-' . __CLASS__ ),
                                'UTPageSummary-' . __CLASS__,
                                EDIT_NEW | EDIT_SUPPRESS_RC,
@@ -127,7 +128,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        );
                }
 
-               return $this->testPage;
+               if ( is_null( $pageTitle ) ) {
+                       $this->testPage = $page;
+               }
+               return $page;
        }
 
        /**
@@ -1959,4 +1963,96 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->assertSame( RevisionRecord::DELETED_TEXT, $deletedAfter );
        }
 
+       public function provideNewRevisionsFromBatchOptions() {
+               yield 'No preload slots or content, single page' => [
+                       null,
+                       []
+               ];
+               yield 'Preload slots and content, single page' => [
+                       null,
+                       [
+                               'slots' => [ SlotRecord::MAIN ],
+                               'content' => true
+                       ]
+               ];
+               yield 'No preload slots or content, multiple pages' => [
+                       'Other_Page',
+                       []
+               ];
+               yield 'Preload slots and content, multiple pages' => [
+                       'Other_Page',
+                       [
+                               'slots' => [ SlotRecord::MAIN ],
+                               'content' => true
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewRevisionsFromBatchOptions
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+        * @param string|null $otherPageTitle
+        * @param array|null $options
+        * @throws \MWException
+        */
+       public function testNewRevisionsFromBatch_preloadContent(
+               $otherPageTitle = null,
+               array $options = []
+       ) {
+               $page1 = $this->getTestPage();
+               $text = __METHOD__ . 'b-ä';
+               /** @var Revision $rev1 */
+               $rev1 = $page1->doEditContent(
+                       new WikitextContent( $text . '1' ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+               $page2 = $this->getTestPage( $otherPageTitle );
+               /** @var Revision $rev2 */
+               $rev2 = $page2->doEditContent(
+                       new WikitextContent( $text . '2' ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->newRevisionsFromBatch(
+                       [ $this->revisionToRow( $rev1 ), $this->revisionToRow( $rev2 ) ],
+                       $options
+               );
+               $this->assertTrue( $result->isGood() );
+               $this->assertEmpty( $result->getErrors() );
+               $records = $result->getValue();
+               $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] );
+               $this->assertRevisionRecordMatchesRevision( $rev2, $records[$rev2->getId()] );
+
+               $this->assertSame( $text . '1',
+                       $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+               $this->assertSame( $text . '2',
+                       $records[$rev2->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+               $this->assertEquals( $page1->getTitle()->getDBkey(),
+                       $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() );
+               $this->assertEquals( $page2->getTitle()->getDBkey(),
+                       $records[$rev2->getId()]->getPageAsLinkTarget()->getDBkey() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+        */
+       public function testNewRevisionsFromBatch_emptyBatch() {
+               $result = MediaWikiServices::getInstance()->getRevisionStore()
+                       ->newRevisionsFromBatch(
+                               [],
+                               [
+                                       'slots' => [ SlotRecord::MAIN ],
+                                       'content' => true
+                               ]
+                       );
+               $this->assertTrue( $result->isGood() );
+               $this->assertEmpty( $result->getValue() );
+               $this->assertEmpty( $result->getErrors() );
+       }
 }
index 3adf1b6..ea0cb8a 100644 (file)
@@ -889,10 +889,24 @@ class ApiBaseTest extends ApiTestCase {
                                [],
                                [ 'internalmode' => false ],
                        ],
-                       'Limit with parseLimits false' => [
+                       'Limit with parseLimits false (numeric)' => [
                                '100',
                                [ ApiBase::PARAM_TYPE => 'limit' ],
-                               '100',
+                               100,
+                               [],
+                               [ 'parseLimits' => false ],
+                       ],
+                       'Limit with parseLimits false (max)' => [
+                               'max',
+                               [ ApiBase::PARAM_TYPE => 'limit' ],
+                               'max',
+                               [],
+                               [ 'parseLimits' => false ],
+                       ],
+                       'Limit with parseLimits false (invalid)' => [
+                               'kitten',
+                               [ ApiBase::PARAM_TYPE => 'limit' ],
+                               0,
                                [],
                                [ 'parseLimits' => false ],
                        ],
@@ -901,7 +915,6 @@ class ApiBaseTest extends ApiTestCase {
                                [
                                        ApiBase::PARAM_TYPE => 'limit',
                                        ApiBase::PARAM_MAX2 => 10,
-                                       ApiBase::PARAM_ISMULTI => true,
                                ],
                                new MWException(
                                        'Internal error in ApiBase::getParameterFromSettings: ' .
@@ -913,7 +926,6 @@ class ApiBaseTest extends ApiTestCase {
                                [
                                        ApiBase::PARAM_TYPE => 'limit',
                                        ApiBase::PARAM_MAX => 10,
-                                       ApiBase::PARAM_ISMULTI => true,
                                ],
                                new MWException(
                                        'Internal error in ApiBase::getParameterFromSettings: ' .
index bc3696d..062087d 100644 (file)
@@ -293,7 +293,7 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
 
-               $this->assertBackendPathsConsistent( [ $dest ] );
+               $this->assertBackendPathsConsistent( [ $dest ], true );
        }
 
        public static function provider_testStore() {
@@ -318,19 +318,19 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testCopy
         */
-       public function testCopy( $op ) {
+       public function testCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestCopy( $op );
+               $this->doTestCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestCopy( $op );
+               $this->doTestCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
        }
 
-       private function doTestCopy( $op ) {
+       private function doTestCopy( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $backendName = $this->backendClass();
 
                $source = $op['src'];
@@ -338,99 +338,128 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->prepare( [ 'dir' => dirname( $source ) ] );
                $this->prepare( [ 'dir' => dirname( $dest ) ] );
 
-               if ( isset( $op['ignoreMissingSource'] ) ) {
-                       $status = $this->backend->doOperation( $op );
-                       $this->assertGoodStatus( $status,
-                               "Move from $source to $dest succeeded without warnings ($backendName)." );
-                       $this->assertEquals( [ 0 => true ], $status->success,
-                               "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
-                               "Source file $source does not exist ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ),
-                               "Destination file $dest does not exist ($backendName)." );
-
-                       return;
+               if ( is_string( $srcContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $srcContent, 'dst' => $source ] );
+                       $this->assertGoodStatus( $status, "Creation of $source succeeded ($backendName)." );
                }
-
-               $status = $this->backend->doOperation(
-                       [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
-               $this->assertGoodStatus( $status,
-                       "Creation of file at $source succeeded ($backendName)." );
-
-               if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
-                       $this->backend->copy( $op );
+               if ( is_string( $dstContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $dstContent, 'dst' => $dest ] );
+                       $this->assertGoodStatus( $status, "Creation of $dest succeeded ($backendName)." );
                }
 
                $status = $this->backend->doOperation( $op );
 
-               $this->assertGoodStatus( $status,
-                       "Copy from $source to $dest succeeded without warnings ($backendName)." );
-               $this->assertEquals( true, $status->isOK(),
-                       "Copy from $source to $dest succeeded ($backendName)." );
-               $this->assertEquals( [ 0 => true ], $status->success,
-                       "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
-               $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $source ] ),
-                       "Source file $source still exists ($backendName)." );
-               $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
-                       "Destination file $dest exists after copy ($backendName)." );
-
-               $this->assertEquals(
-                       $this->backend->getFileSize( [ 'src' => $source ] ),
-                       $this->backend->getFileSize( [ 'src' => $dest ] ),
-                       "Destination file $dest has correct size ($backendName)." );
+               if ( $okStatus ) {
+                       $this->assertGoodStatus(
+                               $status,
+                               "Copy from $source to $dest succeeded without warnings ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Copy from $source to $dest succeeded ($backendName)." );
+                       $this->assertEquals( [ 0 => true ], $status->success,
+                               "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
+                       if ( !is_string( $srcContent ) ) {
+                               $this->assertSame(
+                                       is_string( $dstContent ),
+                                       $this->backend->fileExists( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op copy ($backendName)." );
+                               $this->assertSame(
+                                       $dstContent,
+                                       $this->backend->getFileContents( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op copy ($backendName)." );
+                       } else {
+                               $this->assertEquals(
+                                       $this->backend->getFileSize( [ 'src' => $source ] ),
+                                       $this->backend->getFileSize( [ 'src' => $dest ] ),
+                                       "Destination file $dest has correct size ($backendName)." );
+                               $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
+                               $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
+                               $this->assertEquals(
+                                       $props1,
+                                       $props2,
+                                       "Source and destination have the same props ($backendName)." );
+                       }
+               } else {
+                       $this->assertBadStatus(
+                               $status,
+                               "Copy from $source to $dest fails ($backendName)." );
+                       $this->assertSame(
+                               is_string( $dstContent ),
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed copy ($backendName)." );
+                       $this->assertSame(
+                               $dstContent,
+                               $this->backend->getFileContents( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed copy ($backendName)." );
+               }
 
-               $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
-               $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
-               $this->assertEquals( $props1, $props2,
-                       "Source and destination have the same props ($backendName)." );
+               $this->assertSame(
+                       is_string( $srcContent ),
+                       (bool)$this->backend->fileExists( [ 'src' => $source ] ),
+                       "Source file $source unchanged after copy ($backendName)."
+               );
+               $this->assertSame(
+                       $srcContent,
+                       $this->backend->getFileContents( [ 'src' => $source ] ),
+                       "Source file $source unchanged after copy ($backendName)."
+               );
+               if ( is_string( $dstContent ) ) {
+                       $this->assertTrue(
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest exists after copy ($backendName)." );
+               }
 
-               $this->assertBackendPathsConsistent( [ $source, $dest ] );
+               $this->assertBackendPathsConsistent( [ $source, $dest ], $okSyncStatus );
        }
 
+       /**
+        * @return array (op, source exists, dest exists, op succeeds, sync check succeeds)
+        */
        public static function provider_testCopy() {
                $cases = [];
 
                $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
-               $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+               $dest = self::baseStorePath() . '/unittest-cont2/a/fileCopied.txt';
+               $opBase = [ 'op' => 'copy', 'src' => $source, 'dst' => $dest ];
 
-               $op = [ 'op' => 'copy', 'src' => $source, 'dst' => $dest ];
-               $cases[] = [
-                       $op, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwrite'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwriteSame'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', 'xxx', true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'yyy', true, true ];
+
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'zzz', false, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, 'xxx', false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, 'xxx', true, true ];
+
+               $op = $opBase;
+               $op['src'] = 'mwstore://wrongbackend/unittest-cont1/e/file.txt';
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, false, false ];
 
                return $cases;
        }
@@ -438,19 +467,19 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testMove
         */
-       public function testMove( $op ) {
+       public function testMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestMove( $op );
+               $this->doTestMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestMove( $op );
+               $this->doTestMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
        }
 
-       private function doTestMove( $op ) {
+       private function doTestMove( $op, $srcContent, $dstContent, $okStatus, $okSyncStatus ) {
                $backendName = $this->backendClass();
 
                $source = $op['src'];
@@ -458,100 +487,128 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->prepare( [ 'dir' => dirname( $source ) ] );
                $this->prepare( [ 'dir' => dirname( $dest ) ] );
 
-               if ( isset( $op['ignoreMissingSource'] ) ) {
-                       $status = $this->backend->doOperation( $op );
-                       $this->assertGoodStatus( $status,
-                               "Move from $source to $dest succeeded without warnings ($backendName)." );
-                       $this->assertEquals( [ 0 => true ], $status->success,
-                               "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
-                               "Source file $source does not exist ($backendName)." );
-                       $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ),
-                               "Destination file $dest does not exist ($backendName)." );
-
-                       return;
+               if ( is_string( $srcContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $srcContent, 'dst' => $source ] );
+                       $this->assertGoodStatus( $status, "Creation of $source succeeded ($backendName)." );
                }
-
-               $status = $this->backend->doOperation(
-                       [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
-               $this->assertGoodStatus( $status,
-                       "Creation of file at $source succeeded ($backendName)." );
-
-               if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
-                       $this->backend->copy( $op );
+               if ( is_string( $dstContent ) ) {
+                       $status = $this->backend->create( [ 'content' => $dstContent, 'dst' => $dest ] );
+                       $this->assertGoodStatus( $status, "Creation of $dest succeeded ($backendName)." );
                }
 
+               $oldSrcProps = $this->backend->getFileProps( [ 'src' => $source ] );
+
                $status = $this->backend->doOperation( $op );
-               $this->assertGoodStatus( $status,
-                       "Move from $source to $dest succeeded without warnings ($backendName)." );
-               $this->assertEquals( true, $status->isOK(),
-                       "Move from $source to $dest succeeded ($backendName)." );
-               $this->assertEquals( [ 0 => true ], $status->success,
-                       "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
-               $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
-                       "Source file $source does not still exists ($backendName)." );
-               $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
-                       "Destination file $dest exists after move ($backendName)." );
 
-               $this->assertNotEquals(
-                       $this->backend->getFileSize( [ 'src' => $source ] ),
-                       $this->backend->getFileSize( [ 'src' => $dest ] ),
-                       "Destination file $dest has correct size ($backendName)." );
+               if ( $okStatus ) {
+                       $this->assertGoodStatus(
+                               $status,
+                               "Move from $source to $dest succeeded without warnings ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Move from $source to $dest succeeded ($backendName)." );
+                       $this->assertEquals( [ 0 => true ], $status->success,
+                               "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+                       if ( !is_string( $srcContent ) ) {
+                               $this->assertSame(
+                                       is_string( $dstContent ),
+                                       $this->backend->fileExists( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op move ($backendName)." );
+                               $this->assertSame(
+                                       $dstContent,
+                                       $this->backend->getFileContents( [ 'src' => $dest ] ),
+                                       "Destination file $dest unchanged after no-op move ($backendName)." );
+                       } else {
+                               $this->assertEquals(
+                                       $this->backend->getFileSize( [ 'src' => $dest ] ),
+                                       strlen( $srcContent ),
+                                       "Destination file $dest has correct size ($backendName)." );
+                               $this->assertEquals(
+                                       $oldSrcProps,
+                                       $this->backend->getFileProps( [ 'src' => $dest ] ),
+                                       "Source and destination have the same props ($backendName)." );
+                       }
+               } else {
+                       $this->assertBadStatus(
+                               $status,
+                               "Move from $source to $dest fails ($backendName)." );
+                       $this->assertSame(
+                               is_string( $dstContent ),
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed move ($backendName)." );
+                       $this->assertSame(
+                               $dstContent,
+                               $this->backend->getFileContents( [ 'src' => $dest ] ),
+                               "Destination file $dest unchanged after failed move ($backendName)." );
+                       $this->assertSame(
+                               is_string( $srcContent ),
+                               (bool)$this->backend->fileExists( [ 'src' => $source ] ),
+                               "Source file $source unchanged after failed move ($backendName)."
+                       );
+                       $this->assertSame(
+                               $srcContent,
+                               $this->backend->getFileContents( [ 'src' => $source ] ),
+                               "Source file $source unchanged after failed move ($backendName)."
+                       );
+               }
 
-               $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
-               $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
-               $this->assertEquals( false, $props1['fileExists'],
-                       "Source file does not exist accourding to props ($backendName)." );
-               $this->assertEquals( true, $props2['fileExists'],
-                       "Destination file exists accourding to props ($backendName)." );
+               if ( is_string( $dstContent ) ) {
+                       $this->assertTrue(
+                               (bool)$this->backend->fileExists( [ 'src' => $dest ] ),
+                               "Destination file $dest exists after move ($backendName)." );
+               }
 
-               $this->assertBackendPathsConsistent( [ $source, $dest ] );
+               $this->assertBackendPathsConsistent( [ $source, $dest ], $okSyncStatus );
        }
 
+       /**
+        * @return array (op, source exists, dest exists, op succeeds, sync check succeeds)
+        */
        public static function provider_testMove() {
                $cases = [];
 
                $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
                $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+               $opBase = [ 'op' => 'move', 'src' => $source, 'dst' => $dest ];
 
-               $op = [ 'op' => 'move', 'src' => $source, 'dst' => $dest ];
-               $cases[] = [
-                       $op, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwrite'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['overwriteSame'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwrite'] = true;
+               $cases[] = [ $op, 'yyy', 'xxx', true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       $source, // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', false, true, true ];
 
-               $op2 = $op;
-               $op2['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op2, // operation
-                       self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
-                       $dest, // dest
-               ];
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'yyy', true, true ];
+
+               $op = $opBase;
+               $op['overwriteSame'] = true;
+               $cases[] = [ $op, 'yyy', 'zzz', false, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, 'xxx', false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, true, true ];
+
+               $op = $opBase;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, 'xxx', true, true ];
+
+               $op = $opBase;
+               $op['src'] = 'mwstore://wrongbackend/unittest-cont1/e/file.txt';
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, false, false, false, false ];
 
                return $cases;
        }
@@ -559,27 +616,27 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testDelete
         */
-       public function testDelete( $op, $withSource, $okStatus ) {
+       public function testDelete( $op, $srcContent, $okStatus, $okSyncStatus ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestDelete( $op, $withSource, $okStatus );
+               $this->doTestDelete( $op, $srcContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestDelete( $op, $withSource, $okStatus );
+               $this->doTestDelete( $op, $srcContent, $okStatus, $okSyncStatus );
                $this->tearDownFiles();
        }
 
-       private function doTestDelete( $op, $withSource, $okStatus ) {
+       private function doTestDelete( $op, $srcContent, $okStatus, $okSyncStatus ) {
                $backendName = $this->backendClass();
 
                $source = $op['src'];
                $this->prepare( [ 'dir' => dirname( $source ) ] );
 
-               if ( $withSource ) {
+               if ( is_string( $srcContent ) ) {
                        $status = $this->backend->doOperation(
-                               [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
+                               [ 'op' => 'create', 'content' => $srcContent, 'dst' => $source ] );
                        $this->assertGoodStatus( $status,
                                "Creation of file at $source succeeded ($backendName)." );
                }
@@ -597,7 +654,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Deletion of file at $source failed ($backendName)." );
                }
 
-               $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
+               $this->assertFalse(
+                       (bool)$this->backend->fileExists( [ 'src' => $source ] ),
                        "Source file $source does not exist after move ($backendName)." );
 
                $this->assertFalse(
@@ -605,44 +663,40 @@ class FileBackendTest extends MediaWikiTestCase {
                        "Source file $source has correct size (false) ($backendName)." );
 
                $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
-               $this->assertFalse( $props1['fileExists'],
+               $this->assertFalse(
+                       $props1['fileExists'],
                        "Source file $source does not exist according to props ($backendName)." );
 
-               $this->assertBackendPathsConsistent( [ $source ] );
+               $this->assertBackendPathsConsistent( [ $source ], $okSyncStatus );
        }
 
+       /**
+        * @return array (op, source content, op succeeds, sync check succeeds)
+        */
        public static function provider_testDelete() {
                $cases = [];
 
                $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
+               $baseOp = [ 'op' => 'delete', 'src' => $source ];
 
-               $op = [ 'op' => 'delete', 'src' => $source ];
-               $cases[] = [
-                       $op, // operation
-                       true, // with source
-                       true // succeeds
-               ];
+               $op = $baseOp;
+               $cases[] = [ $op, 'xxx', true, true ];
 
-               $cases[] = [
-                       $op, // operation
-                       false, // without source
-                       false // fails
-               ];
+               $op = $baseOp;
+               $op['ignoreMissingSource'] = true;
+               $cases[] = [ $op, 'xxx', true, true ];
+
+               $op = $baseOp;
+               $cases[] = [ $op, false, false, true ];
 
+               $op = $baseOp;
                $op['ignoreMissingSource'] = true;
-               $cases[] = [
-                       $op, // operation
-                       false, // without source
-                       true // succeeds
-               ];
+               $cases[] = [ $op, false, true, true ];
 
+               $op = $baseOp;
                $op['ignoreMissingSource'] = true;
-               $op['src'] = self::baseStorePath() . '/unittest-cont-bad/e/file.txt';
-               $cases[] = [
-                       $op, // operation
-                       false, // without source
-                       true // succeeds
-               ];
+               $op['src'] = 'mwstore://wrongbackend/unittest-cont1/e/file.txt';
+               $cases[] = [ $op, false, false, false ];
 
                return $cases;
        }
@@ -708,7 +762,7 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Describe of file at $source failed ($backendName)." );
                }
 
-               $this->assertBackendPathsConsistent( [ $source ] );
+               $this->assertBackendPathsConsistent( [ $source ], true );
        }
 
        private function assertHasHeaders( array $headers, array $attr ) {
@@ -809,7 +863,7 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Destination file $dest has original size according to props ($backendName)." );
                }
 
-               $this->assertBackendPathsConsistent( [ $dest ] );
+               $this->assertBackendPathsConsistent( [ $dest ], true );
        }
 
        /**
@@ -2608,7 +2662,7 @@ class FileBackendTest extends MediaWikiTestCase {
        }
 
        function tearDownFiles() {
-               $containers = [ 'unittest-cont1', 'unittest-cont2', 'unittest-cont-bad' ];
+               $containers = [ 'unittest-cont1', 'unittest-cont2' ];
                foreach ( $containers as $container ) {
                        $this->deleteFiles( $container );
                }
@@ -2627,14 +2681,24 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->backend->clean( [ 'dir' => "$base/$container", 'recursive' => 1 ] );
        }
 
-       function assertBackendPathsConsistent( array $paths ) {
-               if ( $this->backend instanceof FileBackendMultiWrite ) {
-                       $status = $this->backend->consistencyCheck( $paths );
+       private function assertBackendPathsConsistent( array $paths, $okSyncStatus ) {
+               if ( !$this->backend instanceof FileBackendMultiWrite ) {
+                       return;
+               }
+
+               $status = $this->backend->consistencyCheck( $paths );
+               if ( $okSyncStatus ) {
                        $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
+               } else {
+                       $this->assertBadStatus( $status, "Files not synced: " . implode( ',', $paths ) );
                }
        }
 
-       function assertGoodStatus( StatusValue $status, $msg ) {
+       private function assertGoodStatus( StatusValue $status, $msg ) {
                $this->assertEquals( print_r( [], 1 ), print_r( $status->getErrors(), 1 ), $msg );
        }
+
+       private function assertBadStatus( StatusValue $status, $msg ) {
+               $this->assertNotEquals( print_r( [], 1 ), print_r( $status->getErrors(), 1 ), $msg );
+       }
 }
diff --git a/tests/phpunit/includes/import/ImportableOldRevisionImporterTest.php b/tests/phpunit/includes/import/ImportableOldRevisionImporterTest.php
new file mode 100644 (file)
index 0000000..a68ac83
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+use Psr\Log\NullLogger;
+
+/**
+ * @group Database
+ * @coversDefaultClass ImportableOldRevisionImporter
+ */
+class ImportableOldRevisionImporterTest extends MediaWikiIntegrationTestCase {
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->tablesUsed[] = 'change_tag';
+               $this->tablesUsed[] = 'change_tag_def';
+
+               ChangeTags::defineTag( 'tag1' );
+       }
+
+       public function provideTestCases() {
+               yield [ [] ];
+               yield [ [ "tag1" ] ];
+       }
+
+       /**
+        * @covers ::import
+        * @param $expectedTags
+        * @dataProvider provideTestCases
+        */
+       public function testImport( $expectedTags ) {
+               $services = MediaWikiServices::getInstance();
+
+               $title = Title::newFromText( __CLASS__ . rand() );
+               $revision = new WikiRevision( $services->getMainConfig() );
+               $revision->setTitle( $title );
+               $revision->setTags( $expectedTags );
+               $revision->setText( "dummy edit" );
+
+               $importer = new ImportableOldRevisionImporter(
+                       true,
+                       new NullLogger(),
+                       $services->getDBLoadBalancer()
+               );
+               $result = $importer->import( $revision );
+               $this->assertTrue( $result );
+
+               $page = WikiPage::factory( $title );
+               $tags = ChangeTags::getTags(
+                       $services->getDBLoadBalancer()->getConnection( DB_MASTER ),
+                       null,
+                       $page->getLatest()
+               );
+               $this->assertSame( $expectedTags, $tags );
+       }
+}