Merge "(bug 30410) Remove deprecated $wgFilterCallback. The 'filtered' API error...
authorBrion VIBBER <brion@wikimedia.org>
Thu, 26 Apr 2012 23:15:16 +0000 (23:15 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 26 Apr 2012 23:15:16 +0000 (23:15 +0000)
82 files changed:
RELEASE-NOTES-1.19
RELEASE-NOTES-1.20
docs/hooks.txt
includes/AutoLoader.php
includes/EditPage.php
includes/OutputPage.php
includes/PathRouter.php
includes/WebRequest.php
includes/WikiPage.php
includes/api/ApiProtect.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiTokens.php
includes/db/Database.php
includes/db/DatabaseError.php
includes/db/DatabaseIbm_db2.php
includes/db/DatabaseMssql.php
includes/db/DatabaseMysql.php
includes/db/DatabaseOracle.php
includes/db/DatabasePostgres.php
includes/db/DatabaseSqlite.php
includes/db/DatabaseUtility.php
includes/db/LBFactory.php
includes/db/LBFactory_Multi.php
includes/db/LBFactory_Single.php
includes/db/LoadBalancer.php
includes/db/LoadMonitor.php
includes/db/ORMResult.php
includes/db/ORMRow.php
includes/db/ORMTable.php
includes/filerepo/FileRepo.php
includes/filerepo/backend/FSFileBackend.php
includes/filerepo/backend/FileBackend.php
includes/filerepo/backend/FileBackendMultiWrite.php
includes/filerepo/backend/FileBackendStore.php
includes/filerepo/backend/SwiftFileBackend.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignAPIFile.php
includes/filerepo/file/LocalFile.php
includes/parser/Parser.php
includes/specials/SpecialCategories.php
languages/messages/MessagesAr.php
languages/messages/MessagesArc.php
languages/messages/MessagesBn.php
languages/messages/MessagesCs.php
languages/messages/MessagesDa.php
languages/messages/MessagesDsb.php
languages/messages/MessagesEs.php
languages/messages/MessagesEt.php
languages/messages/MessagesFa.php
languages/messages/MessagesFi.php
languages/messages/MessagesFr.php
languages/messages/MessagesGl.php
languages/messages/MessagesHe.php
languages/messages/MessagesHsb.php
languages/messages/MessagesHu.php
languages/messages/MessagesHy.php
languages/messages/MessagesIa.php
languages/messages/MessagesIlo.php
languages/messages/MessagesIt.php
languages/messages/MessagesJa.php
languages/messages/MessagesKa.php
languages/messages/MessagesLb.php
languages/messages/MessagesMk.php
languages/messages/MessagesMl.php
languages/messages/MessagesMs.php
languages/messages/MessagesNl.php
languages/messages/MessagesOs.php
languages/messages/MessagesPt.php
languages/messages/MessagesRo.php
languages/messages/MessagesRu.php
languages/messages/MessagesTa.php
languages/messages/MessagesUk.php
languages/messages/MessagesYi.php
languages/messages/MessagesZh_hans.php
maintenance/backupTextPass.inc
maintenance/refreshLinks.php
resources/Resources.php
resources/mediawiki/mediawiki.js
skins/vector/screen-hd.css [new file with mode: 0644]
skins/vector/screen.css
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/phpunit.php

index 94f034c..c15ebff 100644 (file)
@@ -291,8 +291,6 @@ production.
   array.
 * (bug 29753) mw.util.tooltipAccessKeyPrefix should be alt-shift for Chrome
    on Windows
-* (bug 25095) Special:Categories should also include the first relevant item
-   when "from" is filled.
 * (bug 12262) Indents and lists are now aligned
 * (bug 34972) An error occurred while changing your watchlist settings for 
   [[Special:WhatLinksHere/Example]]
index 84e4417..318768c 100644 (file)
@@ -44,6 +44,7 @@ production.
   $wgDebugLogFile['memcached'] to some filepath.
 * (bug 35685) api.php URL and other entry point URLs are now listed on
   Special:Version
+* Edit notices can now be translated.
 
 === Bug fixes in 1.20 ===
 * (bug 30245) Use the correct way to construct a log page title.
index d87bd3d..33db1c5 100644 (file)
@@ -395,6 +395,11 @@ In this data array, the key-value-pair identified by the apiLink key is
 required.
 &$apis: array of services
 
+'ApiTokensGetTokenTypes': use this hook to extend action=tokens with new
+token types.
+&$tokenTypes: supported token types in format 'type' => callback function
+used to retrieve this type of tokens.
+
 'ArticleAfterFetchContent': after fetching content of an article from
 the database
 $article: the article (object) being loaded from the database
@@ -899,6 +904,12 @@ $article: in case all revisions of the file are deleted a reference to the
 $user: user who performed the deletion
 $reason: reason
 
+'FileTransformed': When a file is transformed and moved into storage
+$file: reference to the File object
+$thumb: the MediaTransformOutput object
+$tmpThumbPath: The temporary file system path of the transformed file
+$thumbPath: The permanent storage path of the transformed file
+
 'FileUpload': When a file upload occurs
 $file : Image object representing the file that was uploaded
 $reupload : Boolean indicating if there was a previously another image there or not (since 1.17)
index 60cbbaa..937f09c 100644 (file)
@@ -510,10 +510,16 @@ $wgAutoloadLocalClasses = array(
        'FileBackend' => 'includes/filerepo/backend/FileBackend.php',
        'FileBackendStore' => 'includes/filerepo/backend/FileBackendStore.php',
        'FileBackendStoreShardListIterator' => 'includes/filerepo/backend/FileBackendStore.php',
+       'FileBackendStoreShardDirIterator' => 'includes/filerepo/backend/FileBackendStore.php',
+       'FileBackendStoreShardFileIterator' => 'includes/filerepo/backend/FileBackendStore.php',
        'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
        'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
+       'FSFileBackendList' => 'includes/filerepo/backend/FSFileBackend.php',
+       'FSFileBackendDirList' => 'includes/filerepo/backend/FSFileBackend.php',
        'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
        'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',
+       'SwiftFileBackendList' => 'includes/filerepo/backend/SwiftFileBackend.php',
+       'SwiftFileBackendDirList' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'FileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
        'DBFileJournal' => 'includes/filerepo/backend/filejournal/DBFileJournal.php',
index cf17f76..dce8d91 100644 (file)
@@ -1904,7 +1904,7 @@ class EditPage {
 
                # Optional notices on a per-namespace and per-page basis
                $editnotice_ns = 'editnotice-' . $this->mTitle->getNamespace();
-               $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage();
+               $editnotice_ns_message = wfMessage( $editnotice_ns );
                if ( $editnotice_ns_message->exists() ) {
                        $wgOut->addWikiText( $editnotice_ns_message->plain() );
                }
@@ -1913,7 +1913,7 @@ class EditPage {
                        $editnotice_base = $editnotice_ns;
                        while ( count( $parts ) > 0 ) {
                                $editnotice_base .= '-' . array_shift( $parts );
-                               $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage();
+                               $editnotice_base_msg = wfMessage( $editnotice_base );
                                if ( $editnotice_base_msg->exists() ) {
                                        $wgOut->addWikiText( $editnotice_base_msg->plain() );
                                }
@@ -1921,7 +1921,7 @@ class EditPage {
                } else {
                        # Even if there are no subpages in namespace, we still don't want / in MW ns.
                        $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->mTitle->getDBkey() );
-                       $editnoticeMsg = wfMessage( $editnoticeText )->inContentLanguage();
+                       $editnoticeMsg = wfMessage( $editnoticeText );
                        if ( $editnoticeMsg->exists() ) {
                                $wgOut->addWikiText( $editnoticeMsg->plain() );
                        }
index beaca5f..3ef7ceb 100644 (file)
@@ -2573,12 +2573,24 @@ $templates
                                $extraQuery
                        );
                        $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
-                       // Drop modules that know they're empty
+                       // Extract modules that know they're empty
+                       $emptyModules = array ();
                        foreach ( $modules as $key => $module ) {
                                if ( $module->isKnownEmpty( $context ) ) {
+                                       $emptyModules[$key] = 'ready';
                                        unset( $modules[$key] );
                                }
                        }
+                       // Inline empty modules: since they're empty, just mark them as 'ready'
+                       if ( count( $emptyModules ) > 0 && $only !== ResourceLoaderModule::TYPE_STYLES ) {
+                               // If we're only getting the styles, we don't need to do anything for empty modules.
+                               $links .= Html::inlineScript(\r
+                                               ResourceLoader::makeLoaderConditionalScript(\r
+                                                               ResourceLoader::makeLoaderStateScript( $emptyModules )\r
+                                               )\r
+                               ) . "\n";
+                       }
+
                        // If there are no modules left, skip this group
                        if ( $modules === array() ) {
                                continue;
@@ -2642,7 +2654,7 @@ $templates
                                // Automatically select style/script elements
                                if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
                                        $link = Html::linkedStyle( $url );
-                               } else if ( $loadCall ) { 
+                               } else if ( $loadCall ) {
                                        $link = Html::inlineScript(
                                                ResourceLoader::makeLoaderConditionalScript(
                                                        Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
@@ -2670,7 +2682,7 @@ $templates
         */
        function getHeadScripts() {
                global $wgResourceLoaderExperimentalAsyncLoading;
-               
+
                // Startup - this will immediately load jquery and mediawiki modules
                $scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
 
@@ -2702,7 +2714,7 @@ $templates
                                )
                        );
                }
-               
+
                if ( $wgResourceLoaderExperimentalAsyncLoading ) {
                        $scripts .= $this->getScriptsForBottomQueue( true );
                }
@@ -2748,43 +2760,87 @@ $templates
                // Legacy Scripts
                $scripts .= "\n" . $this->mScripts;
 
-               $userScripts = array();
+               $defaultModules = array();
 
                // Add site JS if enabled
                if ( $wgUseSiteJs ) {
                        $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS,
                                /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
                        );
-                       if( $this->getUser()->isLoggedIn() ){
-                               $userScripts[] = 'user.groups';
-                       }
+                       $defaultModules['site'] = 'loading';
+               } else {
+                       // The wiki is configured to not allow a site module.
+                       $defaultModules['site'] = 'missing';
                }
 
                // Add user JS if enabled
-               if ( $wgAllowUserJs && $this->getUser()->isLoggedIn() ) {
-                       if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) {
-                               # XXX: additional security check/prompt?
-                               // We're on a preview of a JS subpage
-                               // Exclude this page from the user module in case it's in there (bug 26283)
-                               $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
-                                       array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
-                               );
-                               // Load the previewed JS
-                               $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
+               if ( $wgAllowUserJs ) {
+                       if ( $this->getUser()->isLoggedIn() ) {
+                               if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) {
+                                       # XXX: additional security check/prompt?
+                                       // We're on a preview of a JS subpage
+                                       // Exclude this page from the user module in case it's in there (bug 26283)
+                                       $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
+                                               array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
+                                       );
+                                       // Load the previewed JS
+                                       $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
+                                       // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded
+                                       // asynchronously and may arrive *after* the inline script here. So the previewed code
+                                       // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js...
+                               } else {
+                                       // Include the user module normally, i.e., raw to avoid it being wrapped in a closure.
+                                       $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+                                               /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+                                       );
+                               }
+                               $defaultModules['user'] = 'loading';
                        } else {
-                               // Include the user module normally
-                               // We can't do $userScripts[] = 'user'; because the user module would end up
-                               // being wrapped in a closure, so load it raw like 'site'
-                               $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+                               // Non-logged-in users have no user module. Treat it as empty and 'ready' to avoid
+                               // blocking default gadgets that might depend on it. Although arguably default-enabled
+                               // gadgets should not depend on the user module, it's harmless and less error-prone to
+                               // handle this case.
+                               $defaultModules['user'] = 'ready';
+                       }
+               } else {
+                       // User JS diabled
+                       $defaultModules['user'] = 'missing';
+               }
+
+               // Group JS is only enabled if site JS is enabled.
+               if ( $wgUseSiteJs ) {
+                       if ( $this->getUser()->isLoggedIn() ) {
+                               $scripts .= $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED,
                                        /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
                                );
+                               $defaultModules['user.groups'] = 'loading';
+                       } else {
+                               // Non-logged-in users have no user.groups module. Treat it as empty and 'ready' to
+                               // avoid blocking gadgets that might depend upon the module.
+                               $defaultModules['user.groups'] = 'ready';
                        }
+               } else {
+                       // Site (and group JS) disabled
+                       $defaultModules['user.groups'] = 'missing';
                }
-               $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED,
-                       /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
-               );
 
-               return $scripts;
+               $loaderInit = '';
+               if ( $inHead ) {
+                       // We generate loader calls anyway, so no need to fix the client-side loader's state to 'loading'.
+                       foreach ( $defaultModules as $m => $state ) {
+                               if ( $state == 'loading' ) {
+                                       unset( $defaultModules[$m] );
+                               }
+                       }
+               }
+               if ( count( $defaultModules ) > 0 ) {
+                       $loaderInit = Html::inlineScript(
+                               ResourceLoader::makeLoaderConditionalScript(
+                                       ResourceLoader::makeLoaderStateScript( $defaultModules )
+                               )
+                       ) . "\n";
+               }
+               return $loaderInit . $scripts;
        }
 
        /**
@@ -2939,12 +2995,11 @@ $templates
        }
 
        /**
-        * @param $unused
-        * @param $addContentType bool
+        * @param $addContentType bool: Whether <meta> specifying content type should be returned
         *
-        * @return string HTML tag links to be put in the header.
+        * @return array in format "link name or number => 'link html'".
         */
-       public function getHeadLinks( $unused = null, $addContentType = false ) {
+       public function getHeadLinksArray( $addContentType = false ) {
                global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI,
                        $wgSitename, $wgVersion, $wgHtml5, $wgMimeType,
                        $wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes,
@@ -2957,20 +3012,20 @@ $templates
                        if ( $wgHtml5 ) {
                                # More succinct than <meta http-equiv=Content-Type>, has the
                                # same effect
-                               $tags[] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
+                               $tags['meta-charset'] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
                        } else {
-                               $tags[] = Html::element( 'meta', array(
+                               $tags['meta-content-type'] = Html::element( 'meta', array(
                                        'http-equiv' => 'Content-Type',
                                        'content' => "$wgMimeType; charset=UTF-8"
                                ) );
-                               $tags[] = Html::element( 'meta', array(  // bug 15835
+                               $tags['meta-content-style-type'] = Html::element( 'meta', array(  // bug 15835
                                        'http-equiv' => 'Content-Style-Type',
                                        'content' => 'text/css'
                                ) );
                        }
                }
 
-               $tags[] = Html::element( 'meta', array(
+               $tags['meta-generator'] = Html::element( 'meta', array(
                        'name' => 'generator',
                        'content' => "MediaWiki $wgVersion",
                ) );
@@ -2979,7 +3034,7 @@ $templates
                if( $p !== 'index,follow' ) {
                        // http://www.robotstxt.org/wc/meta-user.html
                        // Only show if it's different from the default robots policy
-                       $tags[] = Html::element( 'meta', array(
+                       $tags['meta-robots'] = Html::element( 'meta', array(
                                'name' => 'robots',
                                'content' => $p,
                        ) );
@@ -2990,7 +3045,7 @@ $templates
                                "/<.*?" . ">/" => '',
                                "/_/" => ' '
                        );
-                       $tags[] = Html::element( 'meta', array(
+                       $tags['meta-keywords'] = Html::element( 'meta', array(
                                'name' => 'keywords',
                                'content' =>  preg_replace(
                                        array_keys( $strip ),
@@ -3007,7 +3062,11 @@ $templates
                        } else {
                                $a = 'name';
                        }
-                       $tags[] = Html::element( 'meta',
+                       $tagName = "meta-{$tag[0]}";
+                       if ( isset( $tags[$tagName] ) ) {
+                               $tagName .= $tag[1];
+                       }
+                       $tags[$tagName] = Html::element( 'meta',
                                array(
                                        $a => $tag[0],
                                        'content' => $tag[1]
@@ -3026,14 +3085,14 @@ $templates
                                && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) {
                                // Original UniversalEditButton
                                $msg = $this->msg( 'edit' )->text();
-                               $tags[] = Html::element( 'link', array(
+                               $tags['universal-edit-button'] = Html::element( 'link', array(
                                        'rel' => 'alternate',
                                        'type' => 'application/x-wiki',
                                        'title' => $msg,
                                        'href' => $this->getTitle()->getLocalURL( 'action=edit' )
                                ) );
                                // Alternate edit link
-                               $tags[] = Html::element( 'link', array(
+                               $tags['alternative-edit'] = Html::element( 'link', array(
                                        'rel' => 'edit',
                                        'title' => $msg,
                                        'href' => $this->getTitle()->getLocalURL( 'action=edit' )
@@ -3046,15 +3105,15 @@ $templates
                # uses whichever one appears later in the HTML source. Make sure
                # apple-touch-icon is specified first to avoid this.
                if ( $wgAppleTouchIcon !== false ) {
-                       $tags[] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
+                       $tags['apple-touch-icon'] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
                }
 
                if ( $wgFavicon !== false ) {
-                       $tags[] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
+                       $tags['favicon'] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
                }
 
                # OpenSearch description link
-               $tags[] = Html::element( 'link', array(
+               $tags['opensearch'] = Html::element( 'link', array(
                        'rel' => 'search',
                        'type' => 'application/opensearchdescription+xml',
                        'href' => wfScript( 'opensearch_desc' ),
@@ -3066,7 +3125,7 @@ $templates
                        # for the MediaWiki API (and potentially additional custom API
                        # support such as WordPress or Twitter-compatible APIs for a
                        # blogging extension, etc)
-                       $tags[] = Html::element( 'link', array(
+                       $tags['rsd'] = Html::element( 'link', array(
                                'rel' => 'EditURI',
                                'type' => 'application/rsd+xml',
                                // Output a protocol-relative URL here if $wgServer is protocol-relative
@@ -3086,14 +3145,14 @@ $templates
                                if ( !$urlvar ) {
                                        $variants = $lang->getVariants();
                                        foreach ( $variants as $_v ) {
-                                               $tags[] = Html::element( 'link', array(
+                                               $tags["variant-$_v"] = Html::element( 'link', array(
                                                        'rel' => 'alternate',
                                                        'hreflang' => $_v,
                                                        'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) )
                                                );
                                        }
                                } else {
-                                       $tags[] = Html::element( 'link', array(
+                                       $tags['canonical'] = Html::element( 'link', array(
                                                'rel' => 'canonical',
                                                'href' => $this->getTitle()->getCanonicalUrl()
                                        ) );
@@ -3116,7 +3175,7 @@ $templates
                }
 
                if ( $copyright ) {
-                       $tags[] = Html::element( 'link', array(
+                       $tags['copyright'] = Html::element( 'link', array(
                                'rel' => 'copyright',
                                'href' => $copyright )
                        );
@@ -3165,7 +3224,17 @@ $templates
                                }
                        }
                }
-               return implode( "\n", $tags );
+               return $tags;
+       }
+
+       /**
+        * @param $unused
+        * @param $addContentType bool: Whether <meta> specifying content type should be returned
+        *
+        * @return string HTML tag links to be put in the header.
+        */
+       public function getHeadLinks( $unused = null, $addContentType = false ) {
+               return implode( "\n", $this->getHeadLinksArray( $addContentType ) );
        }
 
        /**
@@ -3260,7 +3329,7 @@ $templates
                                $otherTags .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false,
                                        array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
                                );
-                               
+
                                // Load the previewed CSS
                                // If needed, Janus it first. This is user-supplied CSS, so it's
                                // assumed to be right for the content language directionality.
index 9700b90..a80e04b 100644 (file)
@@ -34,7 +34,7 @@
  *     the relevant contents
  *   - The default behavior is equivalent to `array( 'title' => '$1' )`,
  *     if you don't want the title parameter you can explicitly use `array( 'title' => false )`
- *   - You can specify a value that won't have replacements in it 
+ *   - You can specify a value that won't have replacements in it
  *     using `'foo' => array( 'value' => 'bar' );`
  *
  * Options:
  */
 class PathRouter {
 
+       /**
+        * @var array
+        */
+       private $patterns = array();
+
        /**
         * Protected helper to do the actual bulk work of adding a single pattern.
         * This is in a separate method so that add() can handle the difference between
         * a single string $path and an array() $path that contains multiple path
         * patterns each with an associated $key to pass on.
+        * @param $path string
+        * @param $params array
+        * @param $options array
+        * @param $key null|string
         */
        protected function doAdd( $path, $params, $options, $key = null ) {
                // Make sure all paths start with a /
@@ -140,6 +149,9 @@ class PathRouter {
        /**
         * Add a new path pattern to the path router with the strict option on
         * @see self::add
+        * @param $path string|array
+        * @param $params array
+        * @param $options array
         */
        public function addStrict( $path, $params = array(), $options = array() ) {
                $options['strict'] = true;
@@ -158,6 +170,10 @@ class PathRouter {
                array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
        }
 
+       /**
+        * @param $pattern object
+        * @return float|int
+        */
        protected static function makeWeight( $pattern ) {
                # Start with a weight of 0
                $weight = 0;
@@ -202,7 +218,7 @@ class PathRouter {
                // Make sure our patterns are sorted by weight so the most specific
                // matches are tested first
                $this->sortByWeight();
-               
+
                $matches = null;
 
                foreach ( $this->patterns as $pattern ) {
@@ -219,6 +235,11 @@ class PathRouter {
                return is_null( $matches ) ? array() : $matches;
        }
 
+       /**
+        * @param $path string
+        * @param $pattern string
+        * @return array|null
+        */
        protected static function extractTitle( $path, $pattern ) {
                // Convert the path pattern into a regexp we can match with
                $regexp = preg_quote( $pattern->path, '#' );
@@ -321,6 +342,8 @@ class PathRouterPatternReplacer {
         * We do this inside of a replacement callback because after replacement we can't tell the
         * difference between a $1 that was not replaced and a $1 that was part of
         * the content a $1 was replaced with.
+        * @param $value string
+        * @return string
         */
        public function replace( $value ) {
                $this->error = false;
@@ -331,6 +354,10 @@ class PathRouterPatternReplacer {
                return $value;
        }
 
+       /**
+        * @param $m array
+        * @return string
+        */
        protected function callback( $m ) {
                if ( $m[1] == "key" ) {
                        if ( is_null( $this->key ) ) {
index 9d0e579..39c1b1b 100644 (file)
@@ -671,6 +671,7 @@ class WebRequest {
 
        /**
         * HTML-safe version of appendQuery().
+        * @deprecated: Deprecated in 1.20, warnings in 1.21, remove in 1.22.
         *
         * @param $query String: query string fragment; do not include initial '?'
         * @return String
index 6cad466..2678f54 100644 (file)
@@ -121,11 +121,26 @@ class WikiPage extends Page {
         * @return WikiPage|null
         */
        public static function newFromID( $id ) {
-               $t = Title::newFromID( $id );
-               if ( $t ) {
-                       return self::factory( $t );
+               $dbr = wfGetDB( DB_SLAVE );
+               $row = $dbr->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
+               if ( !$row ) {
+                       return null;
                }
-               return null;
+               return self::newFromRow( $row );
+       }
+
+       /**
+        * Constructor from a database row
+        *
+        * @since 1.20
+        * @param $row object: database row containing at least fields returned
+        *        by selectFields().
+        * @return WikiPage
+        */
+       public static function newFromRow( $row ) {
+               $page = self::factory( Title::newFromRow( $row ) );
+               $page->loadFromRow( $row );
+               return $page;
        }
 
        /**
@@ -256,6 +271,17 @@ class WikiPage extends Page {
                        }
                }
 
+               $this->loadFromRow( $data );
+       }
+
+       /**
+        * Load the object from a database row
+        *
+        * @since 1.20
+        * @param $data object: database row containing at least fields returned
+        *        by selectFields()
+        */
+       public function loadFromRow( $data ) {
                $lc = LinkCache::singleton();
 
                if ( $data ) {
index ec7b560..596bc5c 100644 (file)
@@ -44,11 +44,14 @@ class ApiProtect extends ApiBase {
                        if ( !$titleObj ) {
                                $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                        }
+                       $pageObj = WikiPage::factory( $titleObj );
+                       $pageObj->loadPageData( 'fromdbmaster' );
                } elseif ( isset( $params['pageid'] ) ) {
-                       $titleObj = Title::newFromID( $params['pageid'] );
-                       if ( !$titleObj ) {
+                       $pageObj = WikiPage::newFromID( $params['pageid'] );
+                       if ( !$pageObj ) {
                                $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
                        }
+                       $titleObj = $pageObj->getTitle();
                }
 
                $errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() );
@@ -115,7 +118,6 @@ class ApiProtect extends ApiBase {
                $watch = $params['watch'] ? 'watch' : $params['watchlist'];
                $this->setWatch( $watch, $titleObj );
 
-               $pageObj = WikiPage::factory( $titleObj );
                $status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() );
 
                if ( !$status->isOK() ) {
index ac112ef..3c19c18 100644 (file)
@@ -190,15 +190,14 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                        $lastUserData = null;
 
                                        if ( !$fit ) {
-                                               $this->setContinueEnumParameter( 'from',
-                                                               $this->keyToTitle( $lastUserData['name'] ) );
+                                               $this->setContinueEnumParameter( 'from', $lastUserData['name'] );
                                                break;
                                        }
                                }
 
                                if ( $count > $limit ) {
                                        // We've reached the one extra which shows that there are additional pages to be had. Stop here...
-                                       $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->user_name ) );
+                                       $this->setContinueEnumParameter( 'from', $row->user_name );
                                        break;
                                }
 
@@ -235,17 +234,23 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                        'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function' );
                        }
 
-                       $lastUserObj = User::newFromName( $lastUser );
+                       $lastUserObj = User::newFromId( $row->user_id );
 
                        // Add user's group info
                        if ( $fld_groups ) {
-                               if ( !isset( $lastUserData['groups'] ) && $lastUserObj ) {
-                                       $lastUserData['groups'] = ApiQueryUsers::getAutoGroups( $lastUserObj );
+                               if ( !isset( $lastUserData['groups'] ) ) {
+                                       if ( $lastUserObj ) {
+                                               $lastUserData['groups'] = ApiQueryUsers::getAutoGroups( $lastUserObj );
+                                       } else {
+                                               // This should not normally happen
+                                               $lastUserData['groups'] = array();
+                                       }
                                }
 
                                if ( !is_null( $row->ug_group2 ) ) {
                                        $lastUserData['groups'][] = $row->ug_group2;
                                }
+
                                $result->setIndexedTagName( $lastUserData['groups'], 'g' );
                        }
 
@@ -254,13 +259,20 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                $result->setIndexedTagName( $lastUserData['implicitgroups'], 'g' );
                        }
                        if ( $fld_rights ) {
-                               if ( !isset( $lastUserData['rights'] ) && $lastUserObj ) {
-                                       $lastUserData['rights'] =  User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
+                               if ( !isset( $lastUserData['rights'] ) ) {
+                                       if ( $lastUserObj ) {
+                                               $lastUserData['rights'] =  User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
+                                       } else {
+                                               // This should not normally happen
+                                               $lastUserData['rights'] = array();
+                                       }
                                }
+
                                if ( !is_null( $row->ug_group2 ) ) {
                                        $lastUserData['rights'] = array_unique( array_merge( $lastUserData['rights'],
                                                User::getGroupPermissions( array( $row->ug_group2 ) ) ) );
                                }
+
                                $result->setIndexedTagName( $lastUserData['rights'], 'r' );
                        }
                }
@@ -269,8 +281,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
                        $fit = $result->addValue( array( 'query', $this->getModuleName() ),
                                null, $lastUserData );
                        if ( !$fit ) {
-                               $this->setContinueEnumParameter( 'from',
-                                       $this->keyToTitle( $lastUserData['name'] ) );
+                               $this->setContinueEnumParameter( 'from', $lastUserData['name'] );
                        }
                }
 
index 7964095..4a68826 100644 (file)
@@ -35,19 +35,16 @@ class ApiTokens extends ApiBase {
        }
 
        public function execute() {
+               wfProfileIn( __METHOD__ );
                $params = $this->extractRequestParams();
                $res = array();
 
+               $types = $this->getTokenTypes();
                foreach ( $params['type'] as $type ) {
                        $type = strtolower( $type );
-                       $func = 'get' .
-                                       ucfirst( $type ) .
-                                       'Token';
-                       if ( $type === 'patrol' ) {
-                               $val = call_user_func( array( 'ApiQueryRecentChanges', $func ), null, null );
-                       } else {
-                               $val = call_user_func( array( 'ApiQueryInfo', $func ), null, null );
-                       }
+
+                       $val = call_user_func( $types[$type], null, null );
+
                        if ( $val === false ) {
                                $this->setWarning( "Action '$type' is not allowed for the current user" );
                        } else {
@@ -56,6 +53,25 @@ class ApiTokens extends ApiBase {
                }
 
                $this->getResult()->addValue( null, $this->getModuleName(), $res );
+               wfProfileOut( __METHOD__ );
+       }
+
+       private function getTokenTypes() {
+               static $types = null;
+               if ( $types ) {
+                       return $types;
+               }
+               wfProfileIn( __METHOD__ );
+               $types = array( 'patrol' => 'ApiQueryRecentChanges::getPatrolToken' );
+               $names = array( 'edit', 'delete', 'protect', 'move', 'block', 'unblock',
+                       'email', 'import', 'watch' );
+               foreach ( $names as $name ) {
+                       $types[$name] = 'ApiQUeryInfo::get' . ucfirst( $name ) . 'Token';
+               }
+               wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) );
+               ksort( $types );
+               wfProfileOut( __METHOD__ );
+               return $types;
        }
 
        public function getAllowedParams() {
@@ -63,11 +79,8 @@ class ApiTokens extends ApiBase {
                        'type' => array(
                                ApiBase::PARAM_DFLT => 'edit',
                                ApiBase::PARAM_ISMULTI => true,
-                               ApiBase::PARAM_TYPE => array(
-                                       'edit', 'delete', 'protect', 'move', 'block', 'unblock',
-                                       'email', 'import', 'watch', 'patrol'
-                               )
-                       )
+                               ApiBase::PARAM_TYPE => array_keys( $this->getTokenTypes() ),
+                       ),
                );
        }
 
index 5c03617..84ed9f4 100644 (file)
@@ -2,10 +2,26 @@
 /**
  * @defgroup Database Database
  *
+ * This file deals with database interface functions
+ * and query specifics/optimisations.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Database
- * This file deals with database interface functions
- * and query specifics/optimisations
  */
 
 /** Number of times to re-try an operation in case of deadlock */
index 836d781..6aed05c 100644 (file)
@@ -1,4 +1,25 @@
 <?php
+/**
+ * This file contains database error classes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
 
 /**
  * Database error base class
index c9c311d..5df6456 100644 (file)
@@ -2,7 +2,22 @@
 /**
  * This is the IBM DB2 database abstraction layer.
  * See maintenance/ibm_db2/README for development notes
- * and other specific information
+ * and other specific information.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
  * @ingroup Database
index 61963b6..7a75e1e 100644 (file)
@@ -2,6 +2,21 @@
 /**
  * This is the MS SQL Server Native database abstraction layer.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Database
  * @author Joel Penner <a-joelpe at microsoft dot com>
index 4fce0a5..c334c38 100644 (file)
@@ -2,6 +2,21 @@
 /**
  * This is the MySQL database abstraction layer.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Database
  */
index 58cb28b..8ce6e70 100644 (file)
@@ -2,6 +2,21 @@
 /**
  * This is the Oracle database abstraction layer.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Database
  */
index a11b173..d058769 100644 (file)
@@ -2,6 +2,21 @@
 /**
  * This is the Postgres database abstraction layer.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Database
  */
index dc086b4..15d1ad0 100644 (file)
@@ -3,6 +3,21 @@
  * This is the SQLite database abstraction layer.
  * See maintenance/sqlite/README for development notes and other specific information
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Database
  */
index 0ea713c..eacebcf 100644 (file)
@@ -1,4 +1,26 @@
 <?php
+/**
+ * This file contains database-related utiliy classes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
 /**
  * Utility class.
  * @ingroup Database
index dec6ae1..aaca12c 100644 (file)
@@ -1,6 +1,21 @@
 <?php
 /**
- * Generator of database load balancing objects
+ * Generator of database load balancing objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
  * @ingroup Database
index b7977a2..6008813 100644 (file)
@@ -1,6 +1,21 @@
 <?php
 /**
- * Advanced generator of database load balancing objects for wiki farms
+ * Advanced generator of database load balancing objects for wiki farms.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
  * @ingroup Database
index f80aa4b..4b165b2 100644 (file)
@@ -1,4 +1,25 @@
 <?php
+/**
+ * Simple generator of database connections that always returns the same object.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
 
 /**
  * An LBFactory class that always returns a single database object.
index db348e8..c40ac0c 100644 (file)
@@ -1,6 +1,21 @@
 <?php
 /**
- * Database load balancing
+ * Database load balancing.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
  * @ingroup Database
index 16a0343..146ac61 100644 (file)
@@ -1,6 +1,21 @@
 <?php
 /**
- * Database load monitoring
+ * Database load monitoring.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
  * @ingroup Database
index bed8809..31e0c19 100644 (file)
@@ -1,8 +1,22 @@
 <?php
-
 /**
  * Result of a ORMTable::select, which returns ORMRow objects.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @since 1.20
  *
  * @file ORMResult.php
@@ -10,6 +24,7 @@
  * @licence GNU GPL v2 or later
  * @author Jeroen De Dauw < jeroendedauw@gmail.com >
  */
+
 class ORMResult implements Iterator {
 
        /**
index 4ac41cc..d3a97db 100644 (file)
@@ -1,11 +1,25 @@
 <?php
-
 /**
  * Abstract base class for representing objects that are stored in some DB table.
  * This is basically an ORM-like wrapper around rows in database tables that
  * aims to be both simple and very flexible. It is centered around an associative
  * array of fields and various methods to do common interaction with the database.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * These methods are likely candidates for overriding:
  * * getDefaults
  * * remove
@@ -37,6 +51,7 @@
  * @licence GNU GPL v2 or later
  * @author Jeroen De Dauw < jeroendedauw@gmail.com >
  */
+
 abstract class ORMRow {
 
        /**
index 651eadd..2f02c6b 100644 (file)
@@ -1,8 +1,22 @@
 <?php
-
 /**
  * Abstract base class for representing a single database table.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @since 1.20
  *
  * @file ORMTable.php
@@ -10,6 +24,7 @@
  * @licence GNU GPL v2 or later
  * @author Jeroen De Dauw < jeroendedauw@gmail.com >
  */
+
 abstract class ORMTable {
 
        /**
index d38f5ca..196c9de 100644 (file)
@@ -782,6 +782,7 @@ class FileRepo {
        /**
         * Import a file from the local file system into the repo.
         * This does no locking nor journaling and overrides existing files.
+        * This function can be used to write to otherwise read-only foreign repos.
         * This is intended for copying generated thumbnails into the repo.
         *
         * @param $src string File system path
@@ -794,7 +795,8 @@ class FileRepo {
 
        /**
         * Purge a file from the repo. This does no locking nor journaling.
-        * This is intended for purging thumbnail.
+        * This function can be used to write to otherwise read-only foreign repos.
+        * This is intended for purging thumbnails.
         *
         * @param $path string Virtual URL or storage path
         * @return FileRepoStatus
@@ -803,17 +805,31 @@ class FileRepo {
                return $this->quickPurgeBatch( array( $path ) );
        }
 
+       /**
+        * Deletes a directory if empty.
+        * This function can be used to write to otherwise read-only foreign repos.
+        *
+        * @param $dir string Virtual URL (or storage path) of directory to clean
+        * @return Status
+        */
+       public function quickCleanDir( $dir ) {
+               $status = $this->newGood();
+               $status->merge( $this->backend->clean(
+                       array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
+
+               return $status;
+       }
+
        /**
         * Import a batch of files from the local file system into the repo.
         * This does no locking nor journaling and overrides existing files.
+        * This function can be used to write to otherwise read-only foreign repos.
         * This is intended for copying generated thumbnails into the repo.
         *
         * @param $src Array List of tuples (file system path, virtual URL or storage path)
         * @return FileRepoStatus
         */
        public function quickImportBatch( array $pairs ) {
-               $this->assertWritableRepo(); // fail out if read-only
-
                $status = $this->newGood();
                $operations = array();
                foreach ( $pairs as $pair ) {
@@ -834,15 +850,14 @@ class FileRepo {
        }
 
        /**
-        * Purge a batch of files from the repo. This does no locking nor journaling.
-        * This is intended for purging thumbnails.
+        * Purge a batch of files from the repo.
+        * This function can be used to write to otherwise read-only foreign repos.
+        * This does no locking nor journaling and is intended for purging thumbnails.
         *
         * @param $path Array List of virtual URLs or storage paths
         * @return FileRepoStatus
         */
        public function quickPurgeBatch( array $paths ) {
-               $this->assertWritableRepo(); // fail out if read-only
-
                $status = $this->newGood();
                $operations = array();
                foreach ( $paths as $path ) {
@@ -1087,7 +1102,7 @@ class FileRepo {
        }
 
        /**
-        * Deletes a directory if empty
+        * Deletes a directory if empty.
         *
         * @param $dir string Virtual URL (or storage path) of directory to clean
         * @return Status
index 4a27ca1..df822fd 100644 (file)
@@ -7,11 +7,11 @@
 
 /**
  * @brief Class for a file system (FS) based file backend.
- * 
+ *
  * All "containers" each map to a directory under the backend's base directory.
  * For backwards-compatibility, some container paths can be set to custom paths.
  * The wiki ID will not be used in any custom paths, so this should be avoided.
- * 
+ *
  * Having directories with thousands of files will diminish performance.
  * Sharding can be accomplished by using FileRepo-style hash paths.
  *
@@ -76,7 +76,7 @@ class FSFileBackend extends FileBackendStore {
 
        /**
         * Sanity check a relative file system path for validity
-        * 
+        *
         * @param $path string Normalized relative path
         * @return bool
         */
@@ -95,14 +95,14 @@ class FSFileBackend extends FileBackendStore {
        /**
         * Given the short (unresolved) and full (resolved) name of
         * a container, return the file system path of the container.
-        * 
+        *
         * @param $shortCont string
         * @param $fullCont string
-        * @return string|null 
+        * @return string|null
         */
        protected function containerFSRoot( $shortCont, $fullCont ) {
                if ( isset( $this->containerPaths[$shortCont] ) ) {
-                       return $this->containerPaths[$shortCont]; 
+                       return $this->containerPaths[$shortCont];
                } elseif ( isset( $this->basePath ) ) {
                        return "{$this->basePath}/{$fullCont}";
                }
@@ -111,7 +111,7 @@ class FSFileBackend extends FileBackendStore {
 
        /**
         * Get the absolute file system path for a storage path
-        * 
+        *
         * @param $storagePath string Storage path
         * @return string|null
         */
@@ -439,6 +439,41 @@ class FSFileBackend extends FileBackendStore {
                clearstatcache(); // clear the PHP file stat cache
        }
 
+       /**
+        * @see FileBackendStore::doDirectoryExists()
+        * @return bool|null
+        */
+       protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
+               list( $b, $shortCont, $r ) = 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 );
+               $hadError = $this->untrapWarnings();
+
+               return $hadError ? null : $exists;
+       }
+
+       /**
+        * @see FileBackendStore::getDirectoryListInternal()
+        * @return Array|null
+        */
+       public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
+               list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               $exists = is_dir( $dir );
+               if ( !$exists ) {
+                       wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+                       return array(); // nothing under this dir
+               } elseif ( !is_readable( $dir ) ) {
+                       wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+                       return null; // bad permissions?
+               }
+               return new FSFileBackendDirList( $dir, $params );
+       }
+
        /**
         * @see FileBackendStore::getFileListInternal()
         * @return array|FSFileBackendFileList|null
@@ -451,13 +486,11 @@ class FSFileBackend extends FileBackendStore {
                if ( !$exists ) {
                        wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
                        return array(); // nothing under this dir
-               }
-               $readable = is_readable( $dir );
-               if ( !$readable ) {
+               } elseif ( !is_readable( $dir ) ) {
                        wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
                        return null; // bad permissions?
                }
-               return new FSFileBackendFileList( $dir );
+               return new FSFileBackendFileList( $dir, $params );
        }
 
        /**
@@ -543,53 +576,65 @@ class FSFileBackend extends FileBackendStore {
 }
 
 /**
- * Wrapper around RecursiveDirectoryIterator that catches
- * exception or does any custom behavoir that we may want.
+ * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that
+ * catches exception or does any custom behavoir that we may want.
  * Do not use this class from places outside FSFileBackend.
  *
  * @ingroup FileBackend
  */
-class FSFileBackendFileList implements Iterator {
-       /** @var RecursiveIteratorIterator */
+abstract class FSFileBackendList implements Iterator {
+       /** @var Iterator */
        protected $iter;
        protected $suffixStart; // integer
        protected $pos = 0; // integer
+       /** @var Array */
+       protected $params = array();
 
        /**
         * @param $dir string file system directory
         */
-       public function __construct( $dir ) {
+       public function __construct( $dir, array $params ) {
                $dir = realpath( $dir ); // normalize
                $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
+               $this->params = $params;
+
                try {
+                       $this->iter = $this->initIterator( $dir );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->iter = null; // bad permissions? deleted?
+               }
+       }
+
+       /**
+        * Return an appropriate iterator object to wrap
+        *
+        * @param $dir string file system directory
+        * @return Iterator
+        */
+       protected function initIterator( $dir ) {
+               if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
+                       # Get an iterator that will get direct sub-nodes
+                       return new DirectoryIterator( $dir );
+               } else { // recursive
                        # Get an iterator that will return leaf nodes (non-directories)
                        if ( MWInit::classExists( 'FilesystemIterator' ) ) { // PHP >= 5.3
                                # RecursiveDirectoryIterator extends FilesystemIterator.
                                # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
-                               $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
-                               $this->iter = new RecursiveIteratorIterator( 
-                                       new RecursiveDirectoryIterator( $dir, $flags ) );
+                               $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
+                               return new RecursiveIteratorIterator(
+                                       new RecursiveDirectoryIterator( $dir, $flags ),
+                                       RecursiveIteratorIterator::CHILD_FIRST // include dirs
+                               );
                        } else { // PHP < 5.3
                                # RecursiveDirectoryIterator extends DirectoryIterator
-                               $this->iter = new RecursiveIteratorIterator( 
-                                       new RecursiveDirectoryIterator( $dir ) );
+                               return new RecursiveIteratorIterator(
+                                       new RecursiveDirectoryIterator( $dir ),
+                                       RecursiveIteratorIterator::CHILD_FIRST // include dirs
+                               );
                        }
-               } catch ( UnexpectedValueException $e ) {
-                       $this->iter = null; // bad permissions? deleted?
                }
        }
 
-       /**
-        * @see Iterator::current()
-        * @return string|bool String or false
-        */
-       public function current() {
-               // Return only the relative path and normalize slashes to FileBackend-style
-               // Make sure to use the realpath since the suffix is based upon that
-               return str_replace( '\\', '/',
-                       substr( realpath( $this->iter->current() ), $this->suffixStart ) );
-       }
-
        /**
         * @see Iterator::key()
         * @return integer
@@ -598,6 +643,14 @@ class FSFileBackendFileList implements Iterator {
                return $this->pos;
        }
 
+       /**
+        * @see Iterator::current()
+        * @return string|bool String or false
+        */
+       public function current() {
+               return $this->getRelPath( $this->iter->current()->getPathname() );
+       }
+
        /**
         * @see Iterator::next()
         * @return void
@@ -605,6 +658,7 @@ class FSFileBackendFileList implements Iterator {
        public function next() {
                try {
                        $this->iter->next();
+                       $this->filterViaNext();
                } catch ( UnexpectedValueException $e ) {
                        $this->iter = null;
                }
@@ -619,6 +673,7 @@ class FSFileBackendFileList implements Iterator {
                $this->pos = 0;
                try {
                        $this->iter->rewind();
+                       $this->filterViaNext();
                } catch ( UnexpectedValueException $e ) {
                        $this->iter = null;
                }
@@ -631,4 +686,44 @@ class FSFileBackendFileList implements Iterator {
        public function valid() {
                return $this->iter && $this->iter->valid();
        }
+
+       /**
+        * Filter out items by advancing to the next ones
+        */
+       protected function filterViaNext() {}
+
+       /**
+        * Return only the relative path and normalize slashes to FileBackend-style.
+        * Uses the "real path" since the suffix is based upon that.
+        *
+        * @param $path string
+        * @return string
+        */
+       protected function getRelPath( $path ) {
+               return strtr( substr( realpath( $path ), $this->suffixStart ), '\\', '/' );
+       }
+}
+
+class FSFileBackendDirList extends FSFileBackendList {
+       protected function filterViaNext() {
+               while ( $this->iter->valid() ) {
+                       if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
+                               $this->iter->next(); // skip non-directories and dot files
+                       } else {
+                               break;
+                       }
+               }
+       }
+}
+
+class FSFileBackendFileList extends FSFileBackendList {
+       protected function filterViaNext() {
+               while ( $this->iter->valid() ) {
+                       if ( !$this->iter->current()->isFile() ) {
+                               $this->iter->next(); // skip non-files and dot files
+                       } else {
+                               break;
+                       }
+               }
+       }
 }
index b9821bf..f3b879a 100644 (file)
@@ -546,22 +546,89 @@ abstract class FileBackend {
        abstract public function getLocalCopy( array $params );
 
        /**
-        * Get an iterator to list out all stored files under a storage directory.
+        * Check if a directory exists at a given storage path.
+        * Backends using key/value stores will check if the path is a
+        * virtual directory, meaning there are files under the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * $params include:
+        *     dir : storage directory
+        *
+        * @return bool|null Returns null on failure
+        * @since 1.20
+        */
+       abstract public function directoryExists( array $params );
+
+       /**
+        * Get an iterator to list *all* directories under a storage directory.
+        * If the directory is of the form "mwstore://backend/container",
+        * then all directories in the container should be listed.
+        * If the directory is of form "mwstore://backend/container/dir",
+        * then all directories directly under that directory should be listed.
+        * Results should be storage directories relative to the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * $params include:
+        *     dir     : storage directory
+        *     topOnly : only return direct child directories of the directory
+        *
+        * @return Traversable|Array|null Returns null on failure
+        * @since 1.20
+        */
+       abstract public function getDirectoryList( array $params );
+
+       /**
+        * Same as FileBackend::getDirectoryList() except only lists
+        * directories that are immediately under the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * $params include:
+        *     dir : storage directory
+        *
+        * @return Traversable|Array|null Returns null on failure
+        * @since 1.20
+        */
+       final public function getTopDirectoryList( array $params ) {
+               return $this->getDirectoryList( array( 'topOnly' => true ) + $params );
+       }
+
+       /**
+        * Get an iterator to list *all* stored files under a storage directory.
         * If the directory is of the form "mwstore://backend/container",
         * then all files in the container should be listed.
         * If the directory is of form "mwstore://backend/container/dir",
-        * then all files under that container directory should be listed.
+        * then all files under that directory should be listed.
         * Results should be storage paths relative to the given directory.
         *
         * Storage backends with eventual consistency might return stale data.
         *
         * $params include:
-        *     dir : storage path directory
+        *     dir     : storage directory
+        *     topOnly : only return direct child files of the directory
         *
         * @return Traversable|Array|null Returns null on failure
         */
        abstract public function getFileList( array $params );
 
+       /**
+        * Same as FileBackend::getFileList() except only lists
+        * files that are immediately under the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * $params include:
+        *     dir : storage directory
+        *
+        * @return Traversable|Array|null Returns null on failure
+        * @since 1.20
+        */
+       final public function getTopFileList( array $params ) {
+               return $this->getFileList( array( 'topOnly' => true ) + $params );
+       }
+
        /**
         * Invalidate any in-process file existence and property cache.
         * If $paths is given, then only the cache for those files will be cleared.
@@ -708,6 +775,7 @@ abstract class FileBackend {
         *
         * @param $path string
         * @return bool
+        * @since 1.20
         */
        final public static function isPathTraversalFree( $path ) {
                return ( self::normalizeContainerPath( $path ) !== null );
index 1f084d1..208d34b 100644 (file)
@@ -84,8 +84,7 @@ class FileBackendMultiWrite extends FileBackend {
                $status = Status::newGood();
 
                $performOps = array(); // list of FileOp objects
-               $filesRead = array(); // storage paths read from
-               $filesChanged = array(); // storage paths written to
+               $paths = array(); // storage paths read from or written to
                // Build up a list of FileOps. The list will have all the ops
                // for one backend, then all the ops for the next, and so on.
                // These batches of ops are all part of a continuous array.
@@ -98,25 +97,18 @@ class FileBackendMultiWrite extends FileBackend {
                        if ( $index == 0 ) { // first batch
                                // Get the files used for these operations. Each backend has a batch of
                                // the same operations, so we only need to get them from the first batch.
-                               foreach ( $performOps as $fileOp ) {
-                                       $filesRead = array_merge( $filesRead, $fileOp->storagePathsRead() );
-                                       $filesChanged = array_merge( $filesChanged, $fileOp->storagePathsChanged() );
-                               }
+                               $paths = $backend->getPathsToLockForOpsInternal( $performOps );
                                // Get the paths under the proxy backend's name
-                               $filesRead = $this->unsubstPaths( $filesRead );
-                               $filesChanged = $this->unsubstPaths( $filesChanged );
+                               $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
+                               $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
                        }
                }
 
                // Try to lock those files for the scope of this function...
                if ( empty( $opts['nonLocking'] ) ) {
-                       $filesLockSh = array_diff( $filesRead, $filesChanged ); // optimization
-                       $filesLockEx = $filesChanged;
-                       // Get a shared lock on the parent directory of each path changed
-                       $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
                        // Try to lock those files for the scope of this function...
-                       $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
-                       $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+                       $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
+                       $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
                        if ( !$status->isOK() ) {
                                return $status; // abort
                        }
@@ -127,7 +119,7 @@ class FileBackendMultiWrite extends FileBackend {
 
                // Do a consistency check to see if the backends agree
                if ( count( $this->backends ) > 1 ) {
-                       $status->merge( $this->consistencyCheck( array_merge( $filesRead, $filesChanged ) ) );
+                       $status->merge( $this->consistencyCheck( array_merge( $paths['sh'], $paths['ex'] ) ) );
                        if ( !$status->isOK() ) {
                                return $status; // abort
                        }
@@ -321,7 +313,7 @@ class FileBackendMultiWrite extends FileBackend {
        }
 
        /**
-        * @see FileBackend::getFileList()
+        * @see FileBackend::concatenate()
         */
        public function concatenate( array $params ) {
                // We are writing to an FS file, so we don't need to do this per-backend
@@ -409,6 +401,22 @@ class FileBackendMultiWrite extends FileBackend {
                return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
        }
 
+       /**
+        * @see FileBackend::directoryExists()
+        */
+       public function directoryExists( array $params ) {
+               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+               return $this->backends[$this->masterIndex]->directoryExists( $realParams );
+       }
+
+       /**
+        * @see FileBackend::getSubdirectoryList()
+        */
+       public function getDirectoryList( array $params ) {
+               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+               return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
+       }
+
        /**
         * @see FileBackend::getFileList()
         */
index 57654e7..be9357b 100644 (file)
@@ -19,6 +19,9 @@
  * @since 1.19
  */
 abstract class FileBackendStore extends FileBackend {
+       /** @var BagOStuff */
+       protected $memCache;
+
        /** @var Array Map of paths to small (RAM/disk) cache items */
        protected $cache = array(); // (storage path => key => value)
        protected $maxCacheSize = 100; // integer; max paths with entries
@@ -31,6 +34,16 @@ abstract class FileBackendStore extends FileBackend {
 
        protected $maxFileSize = 4294967296; // integer bytes (4GiB)
 
+       /**
+        * @see FileBackend::__construct()
+        *
+        * @param $config Array
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+               $this->memCache = new EmptyBagOStuff(); // disabled by default
+       }
+
        /**
         * Get the maximum allowable file size given backend
         * medium restrictions and basic performance constraints.
@@ -390,11 +403,13 @@ abstract class FileBackendStore extends FileBackend {
 
                if ( $shard !== null ) { // confined to a single container/shard
                        $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
+                       $this->deleteContainerCache( $fullCont ); // purge cache
                } else { // directory is on several shards
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
                        foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
                                $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+                               $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
                        }
                }
 
@@ -646,7 +661,78 @@ abstract class FileBackendStore extends FileBackend {
        }
 
        /**
-        * @copydoc FileBackend::getFileList()
+        * @see FileBackend::directoryExists()
+        * @return bool|null
+        */
+       final public function directoryExists( array $params ) {
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) {
+                       return false; // invalid storage path
+               }
+               if ( $shard !== null ) { // confined to a single container/shard
+                       return $this->doDirectoryExists( $fullCont, $dir, $params );
+               } else { // directory is on several shards
+                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+                       list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+                       $res = false; // response
+                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+                               $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
+                               if ( $exists ) {
+                                       $res = true;
+                                       break; // found one!
+                               } elseif ( $exists === null ) { // error?
+                                       $res = null; // if we don't find anything, it is indeterminate
+                               }
+                       }
+                       return $res;
+               }
+       }
+
+       /**
+        * @see FileBackendStore::directoryExists()
+        *
+        * @param $container string Resolved container name
+        * @param $dir string Resolved path relative to container
+        * @param $params Array
+        * @return bool|null
+        */
+       abstract protected function doDirectoryExists( $container, $dir, array $params );
+
+       /**
+        * @see FileBackend::getDirectoryList()
+        * @return Array|null|Traversable
+        */
+       final public function getDirectoryList( array $params ) {
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) { // invalid storage path
+                       return null;
+               }
+               if ( $shard !== null ) {
+                       // File listing is confined to a single container/shard
+                       return $this->getDirectoryListInternal( $fullCont, $dir, $params );
+               } else {
+                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+                       // File listing spans multiple containers/shards
+                       list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+                       return new FileBackendStoreShardDirIterator( $this,
+                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
+               }
+       }
+
+       /**
+        * Do not call this function from places outside FileBackend
+        *
+        * @see FileBackendStore::getDirectoryList()
+        *
+        * @param $container string Resolved container name
+        * @param $dir string Resolved path relative to container
+        * @param $params Array
+        * @return Traversable|Array|null
+        */
+       abstract public function getDirectoryListInternal( $container, $dir, array $params );
+
+       /**
+        * @see FileBackend::getFileList()
         * @return Array|null|Traversable
         */
        final public function getFileList( array $params ) {
@@ -661,7 +747,7 @@ abstract class FileBackendStore extends FileBackend {
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        // File listing spans multiple containers/shards
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
-                       return new FileBackendStoreShardListIterator( $this,
+                       return new FileBackendStoreShardFileIterator( $this,
                                $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
                }
        }
@@ -726,6 +812,29 @@ abstract class FileBackendStore extends FileBackend {
                return $performOps;
        }
 
+       /**
+        * Get a list of storage paths to lock for a list of operations
+        * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys,
+        * each corresponding to a list of storage paths to be locked.
+        *
+        * @param $performOps Array List of FileOp objects
+        * @return Array ('sh' => list of paths, 'ex' => list of paths)
+        */
+       final public function getPathsToLockForOpsInternal( array $performOps ) {
+               // Build up a list of files to lock...
+               $paths = array( 'sh' => array(), 'ex' => array() );
+               foreach ( $performOps as $fileOp ) {
+                       $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
+                       $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
+               }
+               // Optimization: if doing an EX lock anyway, don't also set an SH one
+               $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
+               // Get a shared lock on the parent directory of each path changed
+               $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
+
+               return $paths;
+       }
+
        /**
         * @see FileBackend::doOperationsInternal()
         * @return Status
@@ -741,18 +850,10 @@ abstract class FileBackendStore extends FileBackend {
                // Acquire any locks as needed...
                if ( empty( $opts['nonLocking'] ) ) {
                        // Build up a list of files to lock...
-                       $filesLockEx = $filesLockSh = array();
-                       foreach ( $performOps as $fileOp ) {
-                               $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
-                               $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
-                       }
-                       // Optimization: if doing an EX lock anyway, don't also set an SH one
-                       $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
-                       // Get a shared lock on the parent directory of each path changed
-                       $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
+                       $paths = $this->getPathsToLockForOpsInternal( $performOps );
                        // Try to lock those files for the scope of this function...
-                       $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
-                       $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+                       $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
+                       $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
                        if ( !$status->isOK() ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
@@ -760,9 +861,12 @@ abstract class FileBackendStore extends FileBackend {
                        }
                }
 
-               // Clear any cache entries (after locks acquired)
+               // Clear any file cache entries (after locks acquired)
                $this->clearCache();
 
+               // Load from the persistent container cache
+               $this->primeContainerCache( $performOps );
+
                // Actually attempt the operation batch...
                $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
 
@@ -962,6 +1066,19 @@ abstract class FileBackendStore extends FileBackend {
                return ''; // no sharding
        }
 
+       /**
+        * Check if a storage path maps to a single shard.
+        * Container dirs like "a", where the container shards on "x/xy",
+        * can reside on several shards. Such paths are tricky to handle.
+        *
+        * @param $storagePath string Storage path
+        * @return bool
+        */
+       final public function isSingleShardPathInternal( $storagePath ) {
+               list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
+               return ( $shard !== null );
+       }
+
        /**
         * Get the sharding config for a container.
         * If greater than 0, then all file storage paths within
@@ -1041,29 +1158,113 @@ abstract class FileBackendStore extends FileBackend {
        protected function resolveContainerPath( $container, $relStoragePath ) {
                return $relStoragePath;
        }
+
+       /**
+        * Get the cache key for a container
+        *
+        * @param $container Resolved container name
+        * @return string
+        */
+       private function containerCacheKey( $container ) {
+               return wfMemcKey( 'backend', $this->getName(), 'container', $container );
+       }
+
+       /**
+        * Set the cached info for a container
+        *
+        * @param $container Resolved container name
+        * @param $val mixed Information to cache
+        * @return void
+        */
+       final protected function setContainerCache( $container, $val ) {
+               $this->memCache->set( $this->containerCacheKey( $container ), $val, 7*86400 );
+       }
+
+       /**
+        * Delete the cached info for a container
+        *
+        * @param $container Resolved container name
+        * @return void
+        */
+       final protected function deleteContainerCache( $container ) {
+               $this->memCache->delete( $this->containerCacheKey( $container ) );
+       }
+
+       /**
+        * Do a batch lookup from cache for container stats for all containers
+        * used in a list of container names, storage paths, or FileOp objects.
+        *
+        * @param $items Array List of storage paths or FileOps
+        * @return void
+        */
+       final protected function primeContainerCache( array $items ) {
+               $paths = array(); // list of storage paths
+               $contNames = array(); // (cache key => resolved container name)
+               // Get all the paths/containers from the items...
+               foreach ( $items as $item ) {
+                       if ( $item instanceof FileOp ) {
+                               $paths = array_merge( $paths, $item->storagePathsRead() );
+                               $paths = array_merge( $paths, $item->storagePathsChanged() );
+                       } elseif ( self::isStoragePath( $item ) ) {
+                               $paths[] = $item;
+                       } elseif ( is_string( $item ) ) { // full container name
+                               $contNames[$this->containerCacheKey( $item )] = $item;
+                       }
+               }
+               // Get all the corresponding cache keys for paths...
+               foreach ( $paths as $path ) {
+                       list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+                       if ( $fullCont !== null ) { // valid path for this backend
+                               $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
+                       }
+               }
+
+               $contInfo = array(); // (resolved container name => cache value)
+               // Get all cache entries for these container cache keys...
+               $values = $this->memCache->getBatch( array_keys( $contNames ) );
+               foreach ( $values as $cacheKey => $val ) {
+                       $contInfo[$contNames[$cacheKey]] = $val;
+               }
+
+               // Populate the container process cache for the backend...
+               $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
+       }
+
+       /**
+        * Fill the backend-specific process cache given an array of
+        * resolved container names and their corresponding cached info.
+        * Only containers that actually exist should appear in the map.
+        *
+        * @param $containerInfo Array Map of resolved container names to cached info
+        * @return void
+        */
+       protected function doPrimeContainerCache( array $containerInfo ) {}
 }
 
 /**
- * FileBackendStore helper function to handle file listings that span container shards.
+ * FileBackendStore helper function to handle listings that span container shards.
  * Do not use this class from places outside of FileBackendStore.
  *
  * @ingroup FileBackend
  */
-class FileBackendStoreShardListIterator implements Iterator {
-       /* @var FileBackendStore */
+abstract class FileBackendStoreShardListIterator implements Iterator {
+       /** @var FileBackendStore */
        protected $backend;
-       /* @var Array */
+       /** @var Array */
        protected $params;
-       /* @var Array */
+       /** @var Array */
        protected $shardSuffixes;
-       protected $container; // string
-       protected $directory; // string
+       protected $container; // string; full container name
+       protected $directory; // string; resolved relative path
 
-       /* @var Traversable */
+       /** @var Traversable */
        protected $iter;
        protected $curShard = 0; // integer
        protected $pos = 0; // integer
 
+       /** @var Array */
+       protected $multiShardPaths = array(); // (rel path => 1)
+
        /**
         * @param $backend FileBackendStore
         * @param $container string Full storage container name
@@ -1112,6 +1313,8 @@ class FileBackendStoreShardListIterator implements Iterator {
                } else {
                        $this->iter->next();
                }
+               // Filter out items that we already listed
+               $this->filterViaNext();
                // Find the next non-empty shard if no elements are left
                $this->nextShardIteratorIfNotValid();
        }
@@ -1124,6 +1327,8 @@ class FileBackendStoreShardListIterator implements Iterator {
                $this->pos = 0;
                $this->curShard = 0;
                $this->setIteratorFromCurrentShard();
+               // Filter out items that we already listed
+               $this->filterViaNext();
                // Find the next non-empty shard if this one has no elements
                $this->nextShardIteratorIfNotValid();
        }
@@ -1142,6 +1347,25 @@ class FileBackendStoreShardListIterator implements Iterator {
                }
        }
 
+       /**
+        * Filter out duplicate items by advancing to the next ones
+        */
+       protected function filterViaNext() {
+               while ( $this->iter->valid() ) {
+                       $rel = $this->iter->current(); // path relative to given directory
+                       $path = $this->params['dir'] . "/{$rel}"; // full storage path
+                       if ( !$this->backend->isSingleShardPathInternal( $path ) ) {
+                               // Don't keep listing paths that are on multiple shards
+                               if ( isset( $this->multiShardPaths[$rel] ) ) {
+                                       $this->iter->next(); // we already listed this path
+                               } else {
+                                       $this->multiShardPaths[$rel] = 1;
+                                       break;
+                               }
+                       }
+               }
+       }
+
        /**
         * If the list iterator for this container shard is out of items,
         * then move on to the next container that has items.
@@ -1161,7 +1385,35 @@ class FileBackendStoreShardListIterator implements Iterator {
         */
        protected function setIteratorFromCurrentShard() {
                $suffix = $this->shardSuffixes[$this->curShard];
-               $this->iter = $this->backend->getFileListInternal(
+               $this->iter = $this->listFromShard(
                        "{$this->container}{$suffix}", $this->directory, $this->params );
        }
+
+       /**
+        * Get the list for a given container shard
+        *
+        * @param $container string Resolved container name
+        * @param $dir string Resolved path relative to container
+        * @param $params Array
+        * @return Traversable|Array|null
+        */
+       abstract protected function listFromShard( $container, $dir, array $params );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
+       protected function listFromShard( $container, $dir, array $params ) {
+               return $this->backend->getDirectoryListInternal( $container, $dir, $params );
+       }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
+       protected function listFromShard( $container, $dir, array $params ) {
+               return $this->backend->getFileListInternal( $container, $dir, $params );
+       }
 }
index c7e40e8..66720fc 100644 (file)
@@ -64,6 +64,8 @@ class SwiftFileBackend extends FileBackendStore {
                $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
                        ? $config['shardViaHashLevels']
                        : '';
+               // Cache container info to mask latency
+               $this->memCache = wfGetMainCache();
        }
 
        /**
@@ -535,12 +537,39 @@ class SwiftFileBackend extends FileBackendStore {
                return $data;
        }
 
+       /**
+        * @see FileBackendStore::doDirectoryExists()
+        * @return bool|null
+        */
+       protected function doDirectoryExists( $fullCont, $dir, array $params ) {
+               try {
+                       $container = $this->getContainer( $fullCont );
+                       $prefix = ( $dir == '' ) ? null : "{$dir}/";
+                       return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 );
+               } catch ( NoSuchContainerException $e ) {
+                       return false;
+               } catch ( InvalidResponseException $e ) {
+               } catch ( Exception $e ) { // some other exception?
+                       $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
+               }
+
+               return null; // error
+       }
+
+       /**
+        * @see FileBackendStore::getDirectoryListInternal()
+        * @return SwiftFileBackendDirList
+        */
+       public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
+               return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
+       }
+
        /**
         * @see FileBackendStore::getFileListInternal()
         * @return SwiftFileBackendFileList
         */
        public function getFileListInternal( $fullCont, $dir, array $params ) {
-               return new SwiftFileBackendFileList( $this, $fullCont, $dir );
+               return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
        }
 
        /**
@@ -548,17 +577,96 @@ class SwiftFileBackend extends FileBackendStore {
         *
         * @param $fullCont string Resolved container name
         * @param $dir string Resolved storage directory with no trailing slash
-        * @param $after string Storage path of file to list items after
+        * @param $after string|null Storage path of file to list items after
         * @param $limit integer Max number of items to list
-        * @return Array
+        * @param $params Array Includes flag for 'topOnly'
+        * @return Array List of relative paths of dirs directly under $dir
+        */
+       public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
+               $dirs = array();
+
+               try {
+                       $container = $this->getContainer( $fullCont );
+                       $prefix = ( $dir == '' ) ? null : "{$dir}/";
+                       // Non-recursive: only list dirs right under $dir
+                       if ( !empty( $params['topOnly'] ) ) {
+                               $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
+                               foreach ( $objects as $object ) { // files and dirs
+                                       if ( substr( $object, -1 ) === '/' ) {
+                                               $dirs[] = $object; // directories end in '/'
+                                       }
+                                       $after = $object; // update last item
+                               }
+                       // Recursive: list all dirs under $dir and its subdirs
+                       } else {
+                               // Get directory from last item of prior page
+                               $lastDir = $this->getParentDir( $after ); // must be first page
+                               $objects = $container->list_objects( $limit, $after, $prefix );
+                               foreach ( $objects as $object ) { // files
+                                       $objectDir = $this->getParentDir( $object ); // directory of object
+                                       if ( $objectDir !== false ) { // file has a parent dir
+                                               // Swift stores paths in UTF-8, using binary sorting.
+                                               // See function "create_container_table" in common/db.py.
+                                               // If a directory is not "greater" than the last one,
+                                               // then it was already listed by the calling iterator.
+                                               if ( $objectDir > $lastDir ) {
+                                                       $pDir = $objectDir;
+                                                       do { // add dir and all its parent dirs
+                                                               $dirs[] = "{$pDir}/";
+                                                               $pDir = $this->getParentDir( $pDir );
+                                                       } while ( $pDir !== false // sanity
+                                                               && $pDir > $lastDir // not done already
+                                                               && strlen( $pDir ) > strlen( $dir ) // within $dir
+                                                       );
+                                               }
+                                               $lastDir = $objectDir;
+                                       }
+                                       $after = $object; // update last item
+                               }
+                       }
+               } catch ( NoSuchContainerException $e ) {
+               } catch ( InvalidResponseException $e ) {
+               } catch ( Exception $e ) { // some other exception?
+                       $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
+               }
+
+               return $dirs;
+       }
+
+       protected function getParentDir( $path ) {
+               return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
+       }
+
+       /**
+        * Do not call this function outside of SwiftFileBackendFileList
+        *
+        * @param $fullCont string Resolved container name
+        * @param $dir string Resolved storage directory with no trailing slash
+        * @param $after string|null Storage path of file to list items after
+        * @param $limit integer Max number of items to list
+        * @param $params Array Includes flag for 'topOnly'
+        * @return Array List of relative paths of files under $dir
         */
-       public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
+       public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
                $files = array();
 
                try {
                        $container = $this->getContainer( $fullCont );
                        $prefix = ( $dir == '' ) ? null : "{$dir}/";
-                       $files = $container->list_objects( $limit, $after, $prefix );
+                       // Non-recursive: only list files right under $dir
+                       if ( !empty( $params['topOnly'] ) ) { // files and dirs
+                               $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
+                               foreach ( $objects as $object ) {
+                                       if ( substr( $object, -1 ) !== '/' ) {
+                                               $files[] = $object; // directories end in '/'
+                                       }
+                               }
+                       // Recursive: list all files under $dir and its subdirs
+                       } else { // files
+                               $files = $container->list_objects( $limit, $after, $prefix );
+                       }
+                       $after = end( $files ); // update last item
+                       reset( $files ); // reset pointer
                } catch ( NoSuchContainerException $e ) {
                } catch ( InvalidResponseException $e ) {
                } catch ( Exception $e ) { // some other exception?
@@ -750,23 +858,30 @@ class SwiftFileBackend extends FileBackendStore {
         * Use $reCache if the file count or byte count is needed.
         *
         * @param $container string Container name
-        * @param $reCache bool Refresh the process cache
+        * @param $bypassCache bool Bypass all caches and load from Swift
         * @return CF_Container
+        * @throws InvalidResponseException
         */
-       protected function getContainer( $container, $reCache = false ) {
+       protected function getContainer( $container, $bypassCache = false ) {
                $conn = $this->getConnection(); // Swift proxy connection
-               if ( $reCache ) {
-                       unset( $this->connContainers[$container] ); // purge cache
+               if ( $bypassCache ) { // purge cache
+                       unset( $this->connContainers[$container] );
+               } elseif ( !isset( $this->connContainers[$container] ) ) {
+                       $this->primeContainerCache( array( $container ) ); // check persistent cache
                }
                if ( !isset( $this->connContainers[$container] ) ) {
                        $contObj = $conn->get_container( $container );
                        // NoSuchContainerException not thrown: container must exist
                        if ( count( $this->connContainers ) >= $this->maxContCacheSize ) { // trim cache?
                                reset( $this->connContainers );
-                               $key = key( $this->connContainers );
-                               unset( $this->connContainers[$key] );
+                               unset( $this->connContainers[key( $this->connContainers )] );
                        }
                        $this->connContainers[$container] = $contObj; // cache it
+                       if ( !$bypassCache ) {
+                               $this->setContainerCache( $container, // update persistent cache
+                                       array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count )
+                               );
+                       }
                }
                return $this->connContainers[$container];
        }
@@ -776,6 +891,7 @@ class SwiftFileBackend extends FileBackendStore {
         *
         * @param $container string Container name
         * @return CF_Container
+        * @throws InvalidResponseException
         */
        protected function createContainer( $container ) {
                $conn = $this->getConnection(); // Swift proxy connection
@@ -789,6 +905,7 @@ class SwiftFileBackend extends FileBackendStore {
         *
         * @param $container string Container name
         * @return void
+        * @throws InvalidResponseException
         */
        protected function deleteContainer( $container ) {
                $conn = $this->getConnection(); // Swift proxy connection
@@ -796,6 +913,28 @@ class SwiftFileBackend extends FileBackendStore {
                unset( $this->connContainers[$container] ); // purge cache
        }
 
+       /**
+        * @see FileBackendStore::doPrimeContainerCache()
+        * @return void
+        */
+       protected function doPrimeContainerCache( array $containerInfo ) {
+               try {
+                       $conn = $this->getConnection(); // Swift proxy connection
+                       foreach ( $containerInfo as $container => $info ) {
+                               $this->connContainers[$container] = new CF_Container(
+                                       $conn->cfs_auth,
+                                       $conn->cfs_http,
+                                       $container,
+                                       $info['count'],
+                                       $info['bytes']
+                               );
+                       }
+               } catch ( InvalidResponseException $e ) {
+               } catch ( Exception $e ) { // some other exception?
+                       $this->logException( $e, __METHOD__, array() );
+               }
+       }
+
        /**
         * Log an unexpected exception for this backend
         *
@@ -816,22 +955,24 @@ class SwiftFileBackend extends FileBackendStore {
 }
 
 /**
- * SwiftFileBackend helper class to page through object listings.
+ * SwiftFileBackend helper class to page through listings.
  * Swift also has a listing limit of 10,000 objects for sanity.
  * Do not use this class from places outside SwiftFileBackend.
  *
  * @ingroup FileBackend
  */
-class SwiftFileBackendFileList implements Iterator {
+abstract class SwiftFileBackendList implements Iterator {
        /** @var Array */
        protected $bufferIter = array();
        protected $bufferAfter = null; // string; list items *after* this path
        protected $pos = 0; // integer
+       /** @var Array */
+       protected $params = array();
 
        /** @var SwiftFileBackend */
        protected $backend;
-       protected $container; //
-       protected $dir; // string storage directory
+       protected $container; // string; container name
+       protected $dir; // string; storage directory
        protected $suffixStart; // integer
 
        const PAGE_SIZE = 5000; // file listing buffer size
@@ -840,8 +981,9 @@ class SwiftFileBackendFileList implements Iterator {
         * @param $backend SwiftFileBackend
         * @param $fullCont string Resolved container name
         * @param $dir string Resolved directory relative to container
+        * @param $params Array
         */
-       public function __construct( SwiftFileBackend $backend, $fullCont, $dir ) {
+       public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
                $this->backend = $backend;
                $this->container = $fullCont;
                $this->dir = $dir;
@@ -853,14 +995,7 @@ class SwiftFileBackendFileList implements Iterator {
                } else { // dir within container
                        $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
                }
-       }
-
-       /**
-        * @see Iterator::current()
-        * @return string|bool String or false
-        */
-       public function current() {
-               return substr( current( $this->bufferIter ), $this->suffixStart );
+               $this->params = $params;
        }
 
        /**
@@ -882,10 +1017,9 @@ class SwiftFileBackendFileList implements Iterator {
                // Check if there are no files left in this page and
                // advance to the next page if this page was not empty.
                if ( !$this->valid() && count( $this->bufferIter ) ) {
-                       $this->bufferAfter = end( $this->bufferIter );
-                       $this->bufferIter = $this->backend->getFileListPageInternal(
-                               $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
-                       );
+                       $this->bufferIter = $this->pageFromList(
+                               $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+                       ); // updates $this->bufferAfter
                }
        }
 
@@ -896,9 +1030,9 @@ class SwiftFileBackendFileList implements Iterator {
        public function rewind() {
                $this->pos = 0;
                $this->bufferAfter = null;
-               $this->bufferIter = $this->backend->getFileListPageInternal(
-                       $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
-               );
+               $this->bufferIter = $this->pageFromList(
+                       $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+               ); // updates $this->bufferAfter
        }
 
        /**
@@ -908,4 +1042,58 @@ class SwiftFileBackendFileList implements Iterator {
        public function valid() {
                return ( current( $this->bufferIter ) !== false ); // no paths can have this value
        }
+
+       /**
+        * Get the given list portion (page)
+        *
+        * @param $container string Resolved container name
+        * @param $dir string Resolved path relative to container
+        * @param $after string|null
+        * @param $limit integer
+        * @param $params Array
+        * @return Traversable|Array|null
+        */
+       abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class SwiftFileBackendDirList extends SwiftFileBackendList {
+       /**
+        * @see Iterator::current()
+        * @return string|bool String (relative path) or false
+        */
+       public function current() {
+               return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
+       }
+
+       /**
+        * @see SwiftFileBackendList::pageFromList()
+        * @return Array
+        */
+       protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+               return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
+       }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class SwiftFileBackendFileList extends SwiftFileBackendList {
+       /**
+        * @see Iterator::current()
+        * @return string|bool String (relative path) or false
+        */
+       public function current() {
+               return substr( current( $this->bufferIter ), $this->suffixStart );
+       }
+
+       /**
+        * @see SwiftFileBackendList::pageFromList()
+        * @return Array
+        */
+       protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+               return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
+       }
 }
index ae11e67..d32a0e3 100644 (file)
@@ -842,6 +842,13 @@ abstract class File {
                                }
                        }
 
+                       // If the backend is ready-only, don't keep generating thumbnails
+                       // only to return transformation errors, just return the error now.
+                       if ( $this->repo->getReadOnlyReason() !== false ) {
+                               $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
+                               break;
+                       }
+
                        // Create a temp FS file with the same extension and the thumbnail
                        $thumbExt = FileBackend::extensionFromPath( $thumbPath );
                        $tmpFile = TempFSFile::factory( 'transform_', $thumbExt );
@@ -871,6 +878,8 @@ abstract class File {
                                } else {
                                        $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
                                }
+                               // Give extensions a chance to do something with this thumbnail...
+                               wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) );
                        }
 
                        // Purge. Useful in the event of Core -> Squid connection failure or squid
index 9ae501a..8014413 100644 (file)
@@ -249,6 +249,6 @@ class ForeignAPIFile extends File {
                # Delete the thumbnails
                $this->repo->quickPurgeBatch( $purgeList );
                # Clear out the thumbnail directory if empty
-               $this->repo->cleanDir( $dir );
+               $this->repo->quickCleanDir( $dir );
        }
 }
index ffadca9..701a1ec 100644 (file)
@@ -779,7 +779,7 @@ class LocalFile extends File {
                # Delete the thumbnails
                $this->repo->quickPurgeBatch( $purgeList );
                # Clear out the thumbnail directory if empty
-               $this->repo->cleanDir( $dir );
+               $this->repo->quickCleanDir( $dir );
        }
 
        /** purgeDescription inherited */
index 20c8b95..56197ea 100644 (file)
@@ -188,7 +188,7 @@ class Parser {
        public function __construct( $conf = array() ) {
                $this->mConf = $conf;
                $this->mUrlProtocols = wfUrlProtocols();
-               $this->mExtLinkBracketedRegex = '/\[((' . wfUrlProtocols() . ')'.
+               $this->mExtLinkBracketedRegex = '/\[((' . $this->mUrlProtocols . ')'.
                        self::EXT_LINK_URL_CLASS.'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
                if ( isset( $conf['preprocessorClass'] ) ) {
                        $this->mPreprocessorClass = $conf['preprocessorClass'];
@@ -1814,7 +1814,7 @@ class Parser {
                        # Don't allow internal links to pages containing
                        # PROTO: where PROTO is a valid URL protocol; these
                        # should be external links.
-                       if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
+                       if ( preg_match( '/^(?:' . $this->mUrlProtocols . ')/', $m[1] ) ) {
                                $s .= $prefix . '[[' . $line ;
                                wfProfileOut( __METHOD__."-misc" );
                                continue;
@@ -2051,7 +2051,7 @@ class Parser {
         * @return String: less-or-more HTML with NOPARSE bits
         */
        function armorLinks( $text ) {
-               return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
+               return preg_replace( '/\b(' . $this->mUrlProtocols . ')/',
                        "{$this->mUniqPrefix}NOPARSE$1", $text );
        }
 
@@ -5573,7 +5573,7 @@ class Parser {
                # @todo FIXME: Not tolerant to blank link text
                # I.E. [http://www.mediawiki.org] will render as [1] or something depending
                # on how many empty links there are on the page - need to figure that out.
-               $text = preg_replace( '/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
+               $text = preg_replace( '/\[(?:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
 
                # Parse wikitext quotes (italics & bold)
                $text = $this->doQuotes( $text );
index 338cd70..6d2831c 100644 (file)
@@ -59,16 +59,12 @@ class SpecialCategories extends SpecialPage {
  * @ingroup SpecialPage Pager
  */
 class CategoryPager extends AlphabeticPager {
-       private $conds = array( 'cat_pages > 0' );
-
        function __construct( IContextSource $context, $from ) {
                parent::__construct( $context );
                $from = str_replace( ' ', '_', $from );
                if( $from !== '' ) {
                        $from = Title::capitalize( $from, NS_CATEGORY );
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $this->conds[] = 'cat_title >= ' . $dbr->addQuotes( $from );
-                       $this->setOffset( '' );
+                       $this->mOffset = $from;
                }
        }
 
@@ -76,7 +72,7 @@ class CategoryPager extends AlphabeticPager {
                return array(
                        'tables' => array( 'category' ),
                        'fields' => array( 'cat_title','cat_pages' ),
-                       'conds' => $this->conds,
+                       'conds' => array( 'cat_pages > 0' ),
                        'options' => array( 'USE INDEX' => 'cat_title' ),
                );
        }
index 7d3ddc4..665121f 100644 (file)
@@ -1159,6 +1159,7 @@ $2
 هذا يحدث أحيانا عندما تستخدم خدمة بروكسي مجهول معيبة مبنية على الوب.'''",
 'edit_form_incomplete' => "'''بعض أجزاء من نموذج التعديل لم تصل إلى الخادم؛ تأكد من أن تعديلاتك لم تمس وحاول مجددا.'''",
 'editing' => 'تحرير $1',
+'creating' => 'إنشاء $1',
 'editingsection' => 'تحرير $1 (قسم)',
 'editingcomment' => 'تعديل $1 (قسم جديد)',
 'editconflict' => 'تضارب في التحرير: $1',
index 50d44a8..f0a812d 100644 (file)
@@ -351,10 +351,13 @@ $1',
 'internalerror' => 'ܦܘܕܐ ܓܘܝܐ',
 'internalerror_info' => 'ܦܘܕܐ ܓܘܝܐ: $1',
 'badtitle' => 'ܟܘܢܝܐ ܠܐ ܛܒܐ',
+'perfcached' => 'ܓܠܝܬ̈ܐ ܗܠܝܢ ܐܣܢܝܢ ܐܢܘܢ ܘܡܬܡܨܝܢܬܐ ܐܝܬܝܗܝ ܕܠܐ ܢܗܘܢ ܚܘ̈ܕܬܐ. ܡܬܚܐ ܥܠܝܐ ܕ {{PLURAL:$1|ܚܕ ܦܠܛܐ|$1 ܦܠܛ̈ܐ}} ܐܝܬ ܒܐܣܢܐ.',
+'perfcachedts' => 'ܓܠܝܬ̈ܐ ܗܠܝܢ ܐܣܢܝܢ ܐܢܘܢ ܘܚܘܕܬܐ ܐܚܪܝܐ ܗܘܐ ܒ $1. ܡܬܚܐ ܥܠܝܐ ܕ {{PLURAL:$4|ܚܕ ܦܠܛܐ|$4 ܦܠܛ̈ܐ}} ܐܝܬ ܒܐܣܢܐ.',
 'viewsource' => 'ܚܙܝ ܡܒܘܥܐ',
-'actionthrottled' => 'ܠܐ ܘܪܕ ܠܡܥܒܕ ܝܬܝܪ ܡܢ ܐܗܐ ܥܒܕܐ',
-'viewsourcetext' => 'ܡܨܐ ܐܢܬ ܠܚܙܝܐ ܘܢܣܚܐ ܠܡܒܘܥ̈ܐ ܕܐܗܐ ܦܐܬܐ:',
-'protectedinterface' => 'ܐܗܐ ܦܐܬܐ ܡܘܬܪܐ ܟܬܝܒܬܐ ܕܦܐܬܐ ܠܚܘܪܙܐ, ܘܐܝܠܗ ܢܛܪܬܐ ܠܡܘܢܥܐ ܚܪܒܐ.',
+'viewsource-title' => 'ܚܙܝ ܡܒܘܥܐ ܕ $1',
+'actionthrottled' => 'ܠܐ ܡܬܡܨܝܢܬܐ ܐܝܬܝܗܝ ܠܡܥܒܕ ܝܬܝܪ ܡܢ ܗܢܐ ܥܒܕܐ',
+'viewsourcetext' => 'ܡܨܐ ܐܢܬ ܕܢܚܙܐ ܘܢܣܚܐ ܠܡܒܘ̈ܥܐ ܕܗܕܐ ܦܐܬܐ:',
+'protectedinterface' => 'ܗܕܐ ܦܐܬܐ ܡܘܬܪܐ ܟܬܝܒܬܐ ܕܦܐܬܐ ܠܚܘܪܙܐ, ܘܐܝܬܝܗܝ ܢܛܪܬܐ ܠܡܘܢܥ ܚܘܒܠܐ.',
 'editinginterface' => "''ܙܘܗܪܐ:''' ܐܢܬ ܥܒܕܬ ܫܚܠܦܬܐ ܒܦܐܬܐ ܡܬܦܠܚܬ ܠܡܘܬܘܪ̈ܐ ܦܐܬܘܬ̈ܐ ܟܬܝܒ̈ܐ ܠܚܘܪܙܐ.
 ܟܠ ܫܘܚܠܦܐ ܒܐܗܐ ܦܐܬܐ ܒܕ ܥܒܕ ܟܪ ܥܠ ܡܚܙܝܬܐ ܦܐܬܐ ܕܡܦܠܚܢܐ ܠܡܦܠܚܢ̈ܐ ܐܚܪ̈ܝܢܐ.
 ܠܬܘܪ̈ܓܡܐ، ܡܦܠܚ ܬܪܡܝܬܐ ܬܘܪܓܡܐ ܕܡܝܕܝܐܘܝܩܝ [//translatewiki.net/wiki/Main_Page?setlang=ar translatewiki.net].",
@@ -926,7 +929,7 @@ $1',
 ܗܫܐ ܐܝܬܝܗܝ  ܨܘܝܒܐ ܠ [[$2]].',
 
 'brokenredirects' => 'ܨܘܝܒ̈ܐ ܬܒܝܪ̈ܐ',
-'brokenredirectstext' => 'ܨܘ̈ܝܒܐ ܗܠܝܢ ܡܛܝܢ ܠܕ̈ܦܐ ܕܠܝܬ:',
+'brokenredirectstext' => 'ܨܘ̈ܝܒܐ ܗܠܝܢ ܡܛܝܢ ܠܕ̈ܦܐ ܕܠܝܬܠܗܘܢ ܐܝܬܘܬܐ:',
 'brokenredirects-edit' => 'ܫܚܠܦ',
 'brokenredirects-delete' => 'ܫܘܦ',
 
index 0307e7d..cbb5f01 100644 (file)
@@ -3169,6 +3169,7 @@ $4-এ নিশ্চিতকরণ কোডটি মেয়াদোত
 'feedback-error1' => 'ত্রুটি: এপিআই থেকে অজানা ফলাফল এসেছে',
 'feedback-error2' => 'ত্রুটি: সম্পাদনা ব্যর্থ',
 'feedback-error3' => 'ত্রুটি: এপিআই থেকে কোন সাড়া নেই',
+'feedback-close' => 'সম্পন্ন',
 
 # API errors
 'api-error-badaccess-groups' => 'আপনার এই উইকিতে ফাইল আপলোডের অনুমতি নেই।',
@@ -3206,4 +3207,15 @@ $4-এ নিশ্চিতকরণ কোডটি মেয়াদোত
 'api-error-uploaddisabled' => 'এই উইকির জন্য আপলোড সুবিধা নিস্ক্রিয় রয়েছে।',
 'api-error-verification-error' => 'সম্ভবত এই ফাইলটি ত্রুটিপূর্ণ অথবা এর এক্সটেনশনটি ভুল।',
 
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|সেকেন্ড|সেকেন্ড}}',
+'duration-minutes' => '$1 {{PLURAL:$1|মিনিট|মিনিট}}',
+'duration-hours' => '$1 {{PLURAL:$1|ঘন্টা|ঘন্টা}}',
+'duration-days' => '$1 {{PLURAL:$1|দিন|দিন}}',
+'duration-weeks' => '{{PLURAL: $1|সপ্তাহ|সপ্তাহ}}',
+'duration-years' => '$1 {{PLURAL:$1|বছর|বছর}}',
+'duration-decades' => '$1 {{PLURAL:$1|দশক|দশক}}',
+'duration-centuries' => '$1 {{PLURAL:$1|শতাব্দী|শতাব্দী}}',
+'duration-millennia' => '$1 {{PLURAL:$1|সহস্রাব্দ|সহস্রাব্দ}}',
+
 );
index c9b245b..f55e957 100644 (file)
@@ -1023,6 +1023,7 @@ Zde je pro přehled zobrazen nejnovější záznam z knihy zablokování:',
 'note' => "'''Poznámka:'''&nbsp;",
 'previewnote' => "'''Pamatujte, že toto je pouze náhled.'''
 Změny zatím nebyly uloženy!",
+'continue-editing' => 'Pokračovat v editaci',
 'previewconflict' => 'Tento náhled ukazuje text tak, jak bude vypadat po uložení stránky.',
 'session_fail_preview' => "'''Váš požadavek se nepodařilo zpracovat kvůli ztrátě dat z relace.
 Zkuste to prosím znovu.
@@ -3823,6 +3824,9 @@ MediaWiki je distribuována v naději, že bude užitečná, avšak BEZ JAKÉKOL
 'version-software' => 'Nainstalovaný software',
 'version-software-product' => 'Název',
 'version-software-version' => 'Verze',
+'version-entrypoints' => 'URL vstupních bodů',
+'version-entrypoints-header-entrypoint' => 'Vstupní bod',
+'version-entrypoints-header-url' => 'URL',
 
 # Special:FilePath
 'filepath' => 'Cesta k souboru',
index 8cd0dc0..b330567 100644 (file)
@@ -859,6 +859,7 @@ Loggen over den seneste blokering ses nedenfor:',
 'note' => "'''Bemærk:'''",
 'previewnote' => "'''Husk at dette er kun en forhåndsvisning.'''
 Dine ændringer er endnu ikke blevet gemt!",
+'continue-editing' => 'Fortsæt med at redigere',
 'previewconflict' => 'Denne forhåndsvisning er resultatet af den redigérbare tekst ovenfor, sådan vil det komme til at se ud hvis du vælger at gemme teksten.',
 'session_fail_preview' => "'''Din ændring kunne ikke gemmes, da dine sessionsdata er gået tabt.
 Prøv venligst igen. Hvis problemet fortsætter, log af og log på igen.'''",
index a1cb4bd..b9e10e6 100644 (file)
@@ -811,6 +811,7 @@ Nejnowšy zapisk blokěrowańskego protokola pódawa se dołojce ako referenca:'
 'note' => "'''Pokazka:'''",
 'previewnote' => "'''Wobmysli, až to jo jano pśeglěd.'''
 Twóje změny hyšći njejsu składowane!",
+'continue-editing' => 'Dalej wobźěłaś',
 'previewconflict' => 'Toś ten pśeglěd wótbłyšćujo tekst górjejcnego póla. Bok buźo tak wuglědaś, jolic jen něnto składujoš.',
 'session_fail_preview' => "'''Wódaj! Twójo wobźěłanje njejo se mógało składowaś, dokulaž su daty twójogo pósejźenja se zgubili. Pšosym wopytaj hyšći raz. Jolic až to pón pśecej hyšći njejźo, wopytaj se wótzjawiś a zasej pśizjawiś.'''",
 'session_fail_preview_html' => "'''Wódaj! Twójo wobźěłanje njejo se mógało składowaś, dokulaž su daty twójogo pósejźenja se zgubili.'''
index 7ac9383..305ee57 100644 (file)
@@ -41,6 +41,7 @@
  * @author Hercule
  * @author Icvav
  * @author Imre
+ * @author Invadinado
  * @author Jatrobat
  * @author Jens Liebenau
  * @author Jurock
@@ -365,15 +366,15 @@ $messages = array(
 'tog-previewontop' => 'Mostrar previsualización antes del cuadro de edición',
 'tog-previewonfirst' => 'Mostrar previsualización en la primera edición',
 'tog-nocache' => 'Desactivar la caché de páginas del navegador',
-'tog-enotifwatchlistpages' => 'Enviarme un correo electrónico cuando una página en mi lista de seguimiento sea modificada',
-'tog-enotifusertalkpages' => 'Enviarme un correo electrónico cuando mi página de discusión sea modificada',
+'tog-enotifwatchlistpages' => 'Enviarme un correo electrónico cuando se modifique una página en mi lista de seguimiento',
+'tog-enotifusertalkpages' => 'Enviarme un correo electrónico cuando se modifique mi página de discusión',
 'tog-enotifminoredits' => 'Notificarme también los cambios menores de páginas',
 'tog-enotifrevealaddr' => 'Revelar mi dirección de correo electrónico en los correos de notificación',
 'tog-shownumberswatching' => 'Mostrar el número de usuarios que la vigilan',
 'tog-oldsig' => 'Firma actual:',
 'tog-fancysig' => 'Tratar firma como wikitexto (sin un enlace automático)',
-'tog-externaleditor' => 'Utilizar editor externo por defecto (sólo para expertos pues necesitas ajustes especiales en tu ordenador. [//www.mediawiki.org/wiki/Manual:External_editors Más información.])',
-'tog-externaldiff' => 'Utilizar diff externo por defecto (sólo para expertos pues necesitas ajustes especiales en tu ordenador. [//www.mediawiki.org/wiki/Manual:External_editors Más información.])',
+'tog-externaleditor' => 'Utilizar editor externo por defecto (sólo para expertos, pues necesitas ajustes especiales en tu ordenador; [//www.mediawiki.org/wiki/Manual:External_editors más información])',
+'tog-externaldiff' => 'Utilizar diff externo por defecto (sólo para expertos, pues necesitas ajustes especiales en tu ordenador; [//www.mediawiki.org/wiki/Manual:External_editors más información])',
 'tog-showjumplinks' => 'Habilitar enlaces de accesibilidad «saltar a»',
 'tog-uselivepreview' => 'Usar live preview (JavaScript) (Experimental)',
 'tog-forceeditsummary' => 'Alertar al grabar sin resumen de edición.',
@@ -461,7 +462,7 @@ $messages = array(
 'category-empty' => "''La categoría no contiene actualmente ningún artículo o archivo multimedia.''",
 'hidden-categories' => '{{PLURAL:$1|Categoría escondida|Categorías escondidas}}',
 'hidden-category-category' => 'Categorías ocultas',
-'category-subcat-count' => '{{PLURAL:$2|Esta categoría comprende solamente la siguiente categoría.|Esta categoría incluye {{PLURAL:$1|la siguiente categorías|las siguientes $1 subcategorías}}, de un total de $2.}}',
+'category-subcat-count' => '{{PLURAL:$2|Esta categoría solo contiene la siguiente subcategoría.|Esta categoría contiene {{PLURAL:$1|la siguiente subcategoría|las siguientes $1 subcategorías}}, de un total de $2.}}',
 'category-subcat-count-limited' => 'Esta categoría contiene {{PLURAL:$1|la siguiente subcategoría|las siguientes $1 subcategorías}}.',
 'category-article-count' => '{{PLURAL:$2|Esta categoría incluye solamente la siguiente página.|{{PLURAL:$1|La siguiente página página pertenece|Las siguientes $1 páginas pertenecen}} a esta categoría, de un total de $2.}}',
 'category-article-count-limited' => '{{PLURAL:$1|La siguiente página pertenece|Las siguientes $1 páginas pertenecen}} a esta categoría.',
@@ -469,12 +470,12 @@ $messages = array(
 'category-file-count-limited' => '{{PLURAL:$1|El siguiente fichero pertenece|Los siguientes $1 ficheros pertenecen}} a esta categoría.',
 'listingcontinuesabbrev' => 'cont.',
 'index-category' => 'Páginas indexadas',
-'noindex-category' => 'Páginas no indizadas',
+'noindex-category' => 'Páginas no indexadas',
 'broken-file-category' => 'Páginas con enlaces rotos a archivos',
 
 'about' => 'Acerca de',
 'article' => 'Artículo',
-'newwindow' => '(Se abre en una ventana nueva)',
+'newwindow' => '(se abre en una ventana nueva)',
 'cancel' => 'Cancelar',
 'moredotdotdot' => 'Más...',
 'mypage' => 'Mi página',
@@ -834,7 +835,7 @@ Puedes ignorar este mensaje si esta cuenta fue creada por error.',
 'login-throttled' => 'Has intentado demasiadas veces iniciar sesión. Por favor espera antes de intentarlo nuevamente.',
 'login-abort-generic' => 'Tu inicio de sesión no fue exitoso - Cancelado',
 'loginlanguagelabel' => 'Idioma: $1',
-'suspicious-userlogout' => 'Tu solicitud de desconexión ha sido denegada debido a que parece que ésta ha sido enviada desde un navegador defectuoso o un proxy caché.',
+'suspicious-userlogout' => 'Tu solicitud de desconexión ha sido denegada, pues parece haber sido enviada desde un navegador defectuoso o un proxy caché.',
 
 # E-mail sending
 'php-mail-error-unknown' => 'Error desconocido en la función mail() de PHP',
@@ -942,7 +943,7 @@ Contraseña temporal: $2',
 Tu dirección IP se almacenará en el historial de ediciones de la página.",
 'anonpreviewwarning' => "''No has iniciado sesión con una cuenta de usuario. Al guardar los cambios se almacenará tu dirección IP en el historial de edición de la página.''",
 'missingsummary' => "'''Atención:''' No has escrito un resumen de edición. Si haces clic nuevamente en «{{int:savearticle}}» tu edición se grabará sin él.",
-'missingcommenttext' => 'Por favor introduce texto debajo.',
+'missingcommenttext' => 'Por favor, introduce un texto debajo.',
 'missingcommentheader' => "'''Recordatorio:''' No has escrito un título para este comentario. Si haces clic nuevamente en \"{{int:savearticle}}\" tu edición se grabará sin él.",
 'summary-preview' => 'Previsualización del resumen:',
 'subject-preview' => 'Previsualización del tema/título:',
@@ -1026,6 +1027,7 @@ La última entrada del registro de bloqueos se proporciona debajo para mayor ref
 'note' => "'''Nota:'''",
 'previewnote' => "'''¡Recuerda que esto es solo una previsualización.'''
 ¡Tus cambios aún no se ha guardado!",
+'continue-editing' => 'Continuar editando',
 'previewconflict' => 'Esta previsualización refleja el texto en el área de edición superior como aparecerá una vez guardados los cambios.',
 'session_fail_preview' => "'''Lo sentimos, no pudimos procesar la edición debido a una pérdida de los datos de sesión.'''
 Por favor, inténtalo de nuevo.
index 770d785..e27db56 100644 (file)
@@ -953,6 +953,7 @@ Allpool on toodud viimane blokeerimislogi sissekanne:',
 'note' => "'''Meeldetuletus:'''",
 'previewnote' => "'''Ära unusta, et see on kõigest eelvaade!'''
 Sinu muudatused pole veel salvestatud!",
+'continue-editing' => 'Jätka redigeerimist',
 'previewconflict' => 'See eelvaade näitab, kuidas ülemises toimetuskastis olev tekst hakkab välja nägema, kui otsustate salvestada.',
 'session_fail_preview' => "'''Vabandust! Meil ei õnnestunud seansiandmete kaotuse tõttu sinu muudatust töödelda.'''
 Palun proovi uuesti.
index f3dd6b6..0f84990 100644 (file)
@@ -1129,6 +1129,7 @@ $2
 'note' => "'''نکته:'''",
 'previewnote' => "'''به یاد داشته باشید که این فقط پیش‌نمایش است.'''
 تغییرات شما هنوز ذخیره نشده‌است!",
+'continue-editing' => 'ادامهٔ ویرایش',
 'previewconflict' => 'این پیش‌نمایش منعکس‌کنندهٔ متن ناحیهٔ ویرایش متن بالایی است، به شکلی که اگر متن را ذخیره کنید نمایش خواهد یافت.',
 'session_fail_preview' => "'''شرمنده! به علت از دست رفتن اطلاعات نشست کاربری نمی‌توانیم ویرایش شما را پردازش کنیم.'''
 لطفاً دوباره سعی کنید.
@@ -4091,7 +4092,7 @@ $5
 'logentry-newusers-newusers' => '$1 یک حساب کاربری ایجاد کرد',
 'logentry-newusers-create' => '$1 یک حساب کاربری ایجاد کرد',
 'logentry-newusers-create2' => '$1 یک حساب کاربری ایجاد کرد $3',
-'logentry-newusers-autocreate' => 'کاروری حساب $1  بساتن به شکل خودکار',
+'logentry-newusers-autocreate' => 'حساب $1  به شکل خودکار ساخته شد',
 'newuserlog-byemail' => 'گذرواژه با پست الکترونیکی ارسال شد',
 
 # Feedback
index c7e1b03..aba7318 100644 (file)
@@ -689,6 +689,9 @@ $2',
 'customjsprotected' => 'Sinulla ei ole oikeutta muuttaa tätä JavaScript-sivua, koska se sisältää toisen käyttäjän henkilökohtaisia asetuksia.',
 'ns-specialprotected' => 'Toimintosivuja ei voi muokata.',
 'titleprotected' => "Käyttäjä [[User:$1|$1]] on asettanut tämän sivun luontikieltoon: ''$2''.",
+'filereadonlyerror' => 'Tiedostoa "$1" ei voi muuttaa, koska jaettu mediavarasto "$2" on "vain luku" -tilassa.
+
+Lukituksen asettanut ylläpitäjä on antanut seuraavan syyn toimenpiteelle: "$3".',
 
 # Virus scanner
 'virus-badscanner' => "Virheellinen asetus: Tuntematon virustutka: ''$1''",
@@ -765,6 +768,7 @@ Tästä johtuen tästä IP-osoitteesta ei voi tällä hetkellä luoda uusia tunn
 'emailconfirmlink' => 'Varmenna sähköpostiosoite',
 'invalidemailaddress' => 'Sähköpostiosoitetta ei voida hyväksyä, koska se ei ole oikeassa muodossa. Ole hyvä ja anna oikea sähköpostiosoite tai jätä kenttä tyhjäksi.',
 'cannotchangeemail' => 'Tunnuksien sähköpostiosoitteita ei voi muuttaa tässä wikissä.',
+'emaildisabled' => 'Tältä sivustolta ei voi lähettää sähköpostia.',
 'accountcreated' => 'Käyttäjätunnus luotiin',
 'accountcreatedtext' => 'Käyttäjän $1 käyttäjätunnus luotiin.',
 'createaccount-title' => 'Tunnuksen luominen {{GRAMMAR:illative|{{SITENAME}}}}',
@@ -961,7 +965,8 @@ Alla on viimeisin estolokin tapahtuma:',
 'userinvalidcssjstitle' => "'''Varoitus:''' Tyyliä nimeltä ”$1” ei ole olemassa. Muista, että käyttäjän määrittelemät .css- ja .js-sivut alkavat pienellä alkukirjaimella, esim. {{ns:user}}:Matti Meikäläinen/vector.css eikä {{ns:user}}:Matti Meikäläinen/Vector.css.",
 'updated' => '(Päivitetty)',
 'note' => "'''Huomautus:'''",
-'previewnote' => "'''Tämä on vasta sivun esikatselu. Sivua ei ole vielä tallennettu!'''",
+'previewnote' => "'''Tämä on vasta sivun esikatselu. Tekemiäsi muokkauksia ei ole vielä tallennettu!'''",
+'continue-editing' => 'Jatka muokkaamista',
 'previewconflict' => 'Tämä esikatselu näyttää miltä muokkausalueella oleva teksti näyttää tallennettuna.',
 'session_fail_preview' => "'''Muokkaustasi ei voitu tallentaa, koska istuntosi tiedot ovat kadonneet.''' Yritä uudelleen. Jos ongelma ei katoa, yritä [[Special:UserLogout|kirjautua ulos]] ja takaisin sisään.",
 'session_fail_preview_html' => "'''Muokkaustasi ei voitu tallentaa, koska istuntosi tiedot ovat kadonneet.'''
@@ -972,6 +977,7 @@ Yritä uudelleen. Jos ongelma ei katoa, yritä [[Special:UserLogout|kirjautua ul
 'token_suffix_mismatch' => "'''Muokkauksesi on hylätty, koska asiakasohjelmasi ei osaa käsitellä välimerkkejä muokkaustarkisteessa. Syynä voi olla viallinen välityspalvelin.'''",
 'edit_form_incomplete' => "'''Osa muokkauslomakkeesta ei saavuttanut palvelinta. Tarkista, että muokkauksesi ovat vahingoittumattomia ja yritä uudelleen.'''",
 'editing' => 'Muokataan sivua $1',
+'creating' => 'Sivun $1 luonti',
 'editingsection' => 'Muokataan osiota sivusta $1',
 'editingcomment' => 'Muokataan uutta osiota sivulla $1',
 'editconflict' => 'Päällekkäinen muokkaus: $1',
@@ -987,7 +993,7 @@ Sinun täytyy yhdistää muutoksesi olemassa olevaan tekstiin.
 'yourdiff' => 'Eroavaisuudet',
 'copyrightwarning' => "'''Muutoksesi astuvat voimaan välittömästi.''' Kaikki {{GRAMMAR:illative|{{SITENAME}}}} tehtävät tuotokset katsotaan julkaistuksi $2 -lisenssin mukaisesti ($1). Jos et halua, että kirjoitustasi muokataan armottomasti ja uudelleenkäytetään vapaasti, älä tallenna kirjoitustasi. Tallentamalla muutoksesi lupaat, että kirjoitit tekstisi itse, tai kopioit sen jostain vapaasta lähteestä. '''ÄLÄ KÄYTÄ TEKIJÄNOIKEUDEN ALAISTA MATERIAALIA ILMAN LUPAA!'''",
 'copyrightwarning2' => "Huomaa, että kuka tahansa voi muokata, muuttaa ja poistaa kaikkia sivustolle tekemiäsi lisäyksiä ja muutoksia. Muokkaamalla sivustoa luovutat sivuston käyttäjille tämän oikeuden ja takaat, että lisäämäsi aineisto on joko itse kirjoittamaasi tai peräisin jostain vapaasta lähteestä. Lisätietoja sivulla $1. '''TEKIJÄNOIKEUDEN ALAISEN MATERIAALIN KÄYTTÄMINEN ILMAN LUPAA ON EHDOTTOMASTI KIELLETTYÄ!'''",
-'longpageerror' => "'''Sivun koko on $1 binäärikilotavua. Sivua ei voida tallentaa, koska enimmäiskoko on $2 binäärikilotavua.'''",
+'longpageerror' => "'''Virhe: Lähettämäsi tekstin pituus on {{PLURAL:$1|kilotavu|$1 kilotavua}}. Tekstiä ei voida tallentaa, koska se on pitempi kuin sallittu enimmäispituus {{PLURAL:$2|yksi kilotavu|$2 kilotavua}}.'''",
 'readonlywarning' => "'''Varoitus: Tietokanta on lukittu huoltoa varten, joten et pysty tallentamaan muokkauksiasi juuri nyt.'''
 Saattaa olla paras leikata ja liimata tekstisi omaan tekstitiedostoosi ja tallentaa se tänne myöhemmin.
 
@@ -1026,6 +1032,7 @@ Se on ilmeisesti poistettu.',
 'edit-no-change' => 'Muokkauksesi sivuutettiin, koska tekstiin ei tehty mitään muutoksia.',
 'edit-already-exists' => 'Uuden sivun luominen ei onnistunut.
 Se on jo olemassa.',
+'defaultmessagetext' => 'Viestin oletusteksti',
 
 # Parser/template warnings
 'expensive-parserfunction-warning' => 'Tällä sivulla on liian monta hitaiden laajennusfunktioiden kutsua.
@@ -1177,8 +1184,8 @@ Sinulla ei ole oikeutta siihen.',
 
 # Suppression log
 'suppressionlog' => 'Häivytysloki',
-'suppressionlogtext' => 'Alla on lista uusimmista poistoista ja muokkausestoista, jotka sisältävät ylläpitäjiltä piilotettua materiaalia.
-[[Special:BlockList|Muokkausestolistassa]] on tämänhetkiset muokkausestot.',
+'suppressionlogtext' => 'Alla on luettelo poistoista ja muokkausestoista, jotka sisältävät ylläpitäjiltä piilotettua materiaalia.
+[[Special:BlockList|Estolistassa]] on lueteltu voimassa olevat muokkauskiellot ja muokkausestot.',
 
 # History merging
 'mergehistory' => 'Yhdistä muutoshistoriat',
@@ -1240,7 +1247,7 @@ Uuden ja vanhan sivun muutoksien pitää muodostaa jatkumo – ne eivät saa men
 
 $1 {{int:pipe-separator}} $2',
 'searchmenu-legend' => 'Hakuasetukset',
-'searchmenu-exists' => "'''Sivu [[:$1]] löytyy tästä wikistä.'''",
+'searchmenu-exists' => "'''Tässä wikissä on sivu nimellä [[:$1]].'''",
 'searchmenu-new' => "'''Luo sivu ''[[:$1]]'' tähän wikiin.'''",
 'searchhelp-url' => 'Help:Sisällys',
 'searchmenu-prefix' => '[[Special:PrefixIndex/$1|Selaa sivuja tällä etuliitteellä]]',
@@ -1598,6 +1605,7 @@ Tässä satunnaisesti tuotettu arvo, jota voit käyttää: $1',
 'newsectionsummary' => '/* $1 */ uusi osio',
 'rc-enhanced-expand' => 'Näytä yksityiskohdat (JavaScript)',
 'rc-enhanced-hide' => 'Piilota yksityiskohdat',
+'rc-old-title' => 'alun perin luotu nimellä "$1"',
 
 # Recent changes linked
 'recentchangeslinked' => 'Linkitettyjen sivujen muutokset',
@@ -1762,6 +1770,7 @@ $1',
 'backend-fail-closetemp' => 'Väliaikaista tiedostoa ei voitu sulkea.',
 'backend-fail-read' => 'Tiedostoa $1 ei voitu lukea.',
 'backend-fail-create' => 'Tiedostoa $1 ei voitu luoda.',
+'backend-fail-connect' => 'Varastojärjestelmään "$1" ei saada yhteyttä.',
 
 # Lock manager
 'lockmanager-notlocked' => 'Kohteen $1 lukitusta ei voitu poistaa, koska se ei ole lukittu.',
@@ -1879,6 +1888,10 @@ Seuraava lista näyttää {{PLURAL:$1|ensimmäisen linkittävän sivun|$1 ensimm
 Katso [$2 tiedoston kuvaussivulta] lisätietoja.',
 'sharedupload-desc-here' => 'Tämä tiedosto on jaettu kohteesta $1 ja muut projektit saattavat käyttää sitä.
 Tiedot [$2 tiedoston kuvaussivulta] näkyvät alla.',
+'sharedupload-desc-edit' => 'Tämä tiedosto on jaettu kohteesta $1 ja muut projektit saattavat käyttää sitä. 
+Voit tarvittaessa muokata [$2 tiedoston kuvaussivua] kohteessa.',
+'sharedupload-desc-create' => 'Tämä tiedosto on jaettu kohteesta $1 ja muut projektit saattavat käyttää sitä. 
+Voit tarvittaessa muokata [$2 tiedoston kuvaussivua] kohteessa.',
 'filepage-nofile' => 'Tämän nimistä tiedostoa ei ole olemassa.',
 'filepage-nofile-link' => 'Tämän nimistä tiedostoa ei ole olemassa, mutta voit [$1 tallentaa sen].',
 'uploadnewversion-linktext' => 'Tallenna uusi versio tästä tiedostosta',
@@ -2008,6 +2021,8 @@ Jokaisella rivillä on linkit ensimmäiseen ja toiseen ohjaukseen sekä toisen o
 'wantedpages' => 'Halutut sivut',
 'wantedpages-badtitle' => 'Virheellinen otsikko tuloksissa: $1',
 'wantedfiles' => 'Halutut tiedostot',
+'wantedfiletext-cat' => 'Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del>. Lisäksi sellaiset sivut, joihin on sisällytetty tiedostoja, jotka eivät ole olemassa, on luetteloitu [[:$1|täällä]].',
+'wantedfiletext-nocat' => 'Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del.>',
 'wantedtemplates' => 'Halutut mallineet',
 'mostlinked' => 'Viitatuimmat sivut',
 'mostlinkedcategories' => 'Viitatuimmat luokat',
@@ -2085,6 +2100,12 @@ Voit rajoittaa listaa valitsemalla lokityypin, käyttäjän tai sivun johon muut
 'allpagesprefix' => 'Katkaisuhaku',
 'allpagesbadtitle' => 'Annettu otsikko oli kelvoton tai siinä oli wikien välinen etuliite.',
 'allpages-bad-ns' => '{{GRAMMAR:inessive|{{SITENAME}}}} ei ole nimiavaruutta ”$1”.',
+'allpages-hide-redirects' => 'Piilota ohjaussivut',
+
+# SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => 'Katselet arkistoitua versiota tästä sivusta, joka voi olla jopa $1 vanha.',
+'cachedspecial-viewing-cached-ts' => 'Katselet arkistoitua versiota tästä sivusta, joka ei välttämättä ole sivun viimeisin versio.',
+'cachedspecial-refresh-now' => 'Näytä uusin versio.',
 
 # Special:Categories
 'categories' => 'Luokat',
@@ -2392,7 +2413,7 @@ Voit palauttaa versiota valikoivasti valitsemalla vain niiden versioiden valinta
 'undeletedrevisions' => '{{PLURAL:$1|Yksi versio|$1 versiota}} palautettiin',
 'undeletedrevisions-files' => '{{PLURAL:$1|Yksi versio|$1 versiota}} ja {{PLURAL:$2|yksi tiedosto|$2 tiedostoa}} palautettiin',
 'undeletedfiles' => '{{PLURAL:$1|1 tiedosto|$1 tiedostoa}} palautettiin',
-'cannotundelete' => 'Palauttaminen epäonnistui.',
+'cannotundelete' => 'Palauttaminen epäonnistui; joku muu on voinut jo palauttaa sivun.',
 'undeletedpage' => "'''$1 on palautettu.'''
 
 [[Special:Log/delete|Poistolokista]] löydät listan viimeisimmistä poistoista ja palautuksista.",
@@ -2505,7 +2526,7 @@ Alla on viimeisin estolokin tapahtuma:',
 'badipaddress' => 'IP-osoite on väärin muotoiltu.',
 'blockipsuccesssub' => 'Esto onnistui',
 'blockipsuccesstext' => 'Käyttäjä tai IP-osoite [[Special:Contributions/$1|$1]] on estetty.<br />
-Nykyiset estot löytyvät [[Special:BlockList|estolistalta]].',
+Voimassa olevat estot näkyvät [[Special:BlockList|estolistasta]].',
 'ipb-blockingself' => 'Olet estämässä itseäsi. Oletko varma, että haluat tehdä niin?',
 'ipb-confirmhideuser' => 'Olet estämässä käyttäjää ”piilota käyttäjä” -toiminnon kanssa.  Tämä piilottaa käyttäjän nimen kaikissa luetteloissa ja lokitapahtumissa.  Oletko varma, että haluat tehdä näin?',
 'ipb-edit-dropdown' => 'Muokkaa estosyitä',
@@ -2558,7 +2579,7 @@ Alla on ote estolokista.',
 Alla on ote häivytyslokista.',
 'blocklogentry' => 'esti käyttäjän tai IP-osoitteen [[$1]]. Eston kesto $2 $3',
 'reblock-logentry' => 'muutti käyttäjän tai IP-osoitteen [[$1]] eston asetuksia. Eston kesto $2 $3',
-'blocklogtext' => 'Tämä on loki muokkausestoista ja niiden purkamisista. Automaattisesti estettyjä IP-osoitteita ei kirjata. Tutustu [[Special:BlockList|estolistaan]] nähdäksesi listan tällä hetkellä voimassa olevista estoista.',
+'blocklogtext' => 'Tämä on loki muokkausestoista ja niiden purkamisista. Automaattisesti estettyjä IP-osoitteita ei kirjata. Tutustu [[Special:BlockList|estolistaan]] nähdäksesi luettelon tällä hetkellä voimassa olevista estoista.',
 'unblocklogentry' => 'poisti käyttäjältä $1 muokkauseston',
 'block-log-flags-anononly' => 'vain kirjautumattomat käyttäjät estetty',
 'block-log-flags-nocreate' => 'tunnusten luonti estetty',
@@ -2797,7 +2818,7 @@ Tallenna tiedot koneellesi ja tuo ne tällä sivulla.',
 
 # JavaScriptTest
 'javascripttest' => 'JavaScriptin testaus',
-'javascripttest-disabled' => 'Tämä toiminto ei ole käytössä.',
+'javascripttest-disabled' => 'Tämä toiminto ei ole käytössä tässä wikissä.',
 'javascripttest-title' => 'Suoritetaan $1-testejä.',
 'javascripttest-pagetext-noframework' => 'Tämä sivu on varattu JavaScript-testien suorittamiseen.',
 'javascripttest-pagetext-unknownframework' => 'Tuntematon testausalusta $1.',
@@ -3637,6 +3658,9 @@ Sinun olisi pitänyt saada [{{SERVER}}{{SCRIPTPATH}}/COPYING kopio GNU General P
 'version-software' => 'Asennettu ohjelmisto',
 'version-software-product' => 'Tuote',
 'version-software-version' => 'Versio',
+'version-entrypoints' => 'Aloituskohtien URL-osoitteet',
+'version-entrypoints-header-entrypoint' => 'Aloituskohta',
+'version-entrypoints-header-url' => 'URL',
 
 # Special:FilePath
 'filepath' => 'Tiedoston osoite',
@@ -3795,6 +3819,7 @@ Muussa tapauksessa voit käyttää alla olevaa helpompaa lomaketta. Kommenttisi
 'api-error-duplicate-archive-popup-title' => 'Tiedostolla on {{PLURAL:$1|poistettu kaksoiskappale|poistettuja kaksoiskappaleita}}',
 'api-error-duplicate-popup-title' => 'Tiedoston {{PLURAL:$1|kaksoiskappale|kaksoiskappaleet}}',
 'api-error-empty-file' => 'Määrittämäsi tiedosto on tyhjä.',
+'api-error-emptypage' => 'Ei ole sallittua luoda uutta, tyhjää sivua.',
 'api-error-fetchfileerror' => 'Sisäinen virhe: jotakin meni pieleen tiedoston haussa.',
 'api-error-file-too-large' => 'Määrittämäsi tiedosto on liian iso.',
 'api-error-filename-tooshort' => 'Tiedoston nimi on liian lyhyt.',
@@ -3823,4 +3848,15 @@ Muussa tapauksessa voit käyttää alla olevaa helpompaa lomaketta. Kommenttisi
 'api-error-uploaddisabled' => 'Tiedostojen tallentaminen ei ole käytössä.',
 'api-error-verification-error' => 'Tiedosto voi olla vioittunut, tai sillä saattaa olla väärä tiedostopääte.',
 
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|sekunti|sekuntia}}',
+'duration-minutes' => '$1 {{PLURAL:$1|minuutti|minuuttia}}',
+'duration-hours' => '$1 {{PLURAL:$1|tunti|tuntia}}',
+'duration-days' => '$1 {{PLURAL:$1|päivä|päivää}}',
+'duration-weeks' => '$1 {{PLURAL:$1|viikko|viikkoa}}',
+'duration-years' => '$1 {{PLURAL:$1|vuosi|vuotta}}',
+'duration-decades' => '$1 {{PLURAL:$1|vuosikymmen|vuosikymmentä}}',
+'duration-centuries' => '$1 {{PLURAL:$1|vuosisata|vuosisataa}}',
+'duration-millennia' => '$1 {{PLURAL:$1|vuosituhat|vuosituhatta}}',
+
 );
index 7a99e56..e9d6334 100644 (file)
@@ -1031,6 +1031,7 @@ La dernière entrée du registre des blocages est indiquée ci-dessous à titre
 'note' => "'''Note :'''",
 'previewnote' => "'''Rappelez-vous que ce n’est qu’une prévisualisation.'''
 Vos modifications n’ont pas encore été enregistrées !",
+'continue-editing' => 'Continuer la modification',
 'previewconflict' => 'Cette prévisualisation montre le texte de la boîte supérieure de modification tel qu’il apparaîtra si vous choisissez de le publier.',
 'session_fail_preview' => "'''Nous ne pouvons enregistrer votre modification à cause d’une perte d’informations concernant votre session.'''
 Veuillez réessayer.
index ba3ccca..6a184c9 100644 (file)
@@ -915,6 +915,7 @@ Lembre que as páxinas .css e .js personalizadas utilizan un título en minúscu
 'updated' => '(Actualizado)',
 'note' => "'''Nota:'''",
 'previewnote' => "'''Lembre que esta é só unha vista previa e que aínda non gardou os seus cambios!'''",
+'continue-editing' => 'Continuar editando',
 'previewconflict' => 'Esta vista previa mostra o texto na área superior tal e como aparecerá se escolle gardar.',
 'session_fail_preview' => "'''O sistema non pode procesar a súa edición porque se perderon os datos de inicio da sesión.
 Por favor, inténteo de novo.
index 5aae82b..9ce0b51 100644 (file)
@@ -1042,6 +1042,7 @@ $2
 'note' => "'''הערה:'''",
 'previewnote' => "'''זכרו שזו רק תצוגה מקדימה.'''
 השינויים שלכם טרם נשמרו!",
+'continue-editing' => 'להמשך העריכה',
 'previewconflict' => 'תצוגה מקדימה זו מציגה כיצד ייראה הטקסט בחלון העריכה העליון, אם תבחרו לשמור אותו.',
 'session_fail_preview' => "'''לא ניתן לבצע את עריכתכם עקב אובדן מידע הכניסה.'''
 אנא נסו שוב.
index 2f42f37..8bc8040 100644 (file)
@@ -804,6 +804,7 @@ Móžeš [[Special:Search/{{PAGENAME}}|tutón titul strony]] na druhich stronach
 'note' => "'''Kedźbu:'''",
 'previewnote' => "'''Wobmysl, zo to je jenož přehlad.'''
 Twoje změny hišće njejsu składowane!",
+'continue-editing' => 'Dale wobdźěłać',
 'previewconflict' => 'Tutón přehlad zwobraznja tekst w hornim tekstowym polu,  kaž so zjewi, jeli jón składuješ.',
 'session_fail_preview' => "'''Njemóžachmy twoju změnu předźěłać, dokelž su so posedźenske daty zhubili.'''
 Spytaj prošu hišće raz.
index 00e1fc5..f5ee6bd 100644 (file)
@@ -991,6 +991,7 @@ a szerkesztési tokenben. A szerkesztés azért lett visszautasítva, hogy megel
 Ez a probléma akkor fordulhat elő, ha hibás, web-alapú proxyszolgáltatást használsz.'''",
 'edit_form_incomplete' => "'''A szerkesztési űrlap egyes részei nem érkeztek meg a szerverre; ellenőrizd újra, hogy a szerkesztés sértetlen-e, majd próbáld újra.'''",
 'editing' => '$1 szerkesztése',
+'creating' => '$1 létrehozása',
 'editingsection' => '$1 szerkesztése (szakasz)',
 'editingcomment' => '$1 szerkesztése (új szakasz)',
 'editconflict' => 'Szerkesztési ütközés: $1',
index 369d443..bd10818 100644 (file)
@@ -887,6 +887,7 @@ $2',
 '''Եթե սա բարեխիղճ խմբագրման փորձ է, խնդրում ենք փորձել կրկին։ Սխալի կրկնման դեպքում՝ փորձեք [[Special:UserLogout|դուրս գալ]], ապա կրկին մտնել համակարգ։'''",
 'token_suffix_mismatch' => "'''Ձեր խմբագրումը մերժվել է, քանի որ ձեր օգտագործած ծրագիրը աղավաղել է կետադրության նշանները խմբագրման դաշտում։ Խմբագրումը մերժվել է էջի տեքստի խաթարումը կանխելու նպատակով։ Սա երբեմն պայմանավորված է սխալներ պարունակող անանվանեցնող վեբ-փոխարինորդ (proxy) ծառայության օգտագործմամբ։'''",
 'editing' => 'Խմբագրում. $1',
+'creating' => 'Ստեղծում $1',
 'editingsection' => 'Խմբագրում. $1 (բաժին)',
 'editingcomment' => 'Խմբագրում $1 (նոր բաժին)',
 'editconflict' => 'Խմբագրման ընդհարում. $1',
index f83362d..a0ff026 100644 (file)
@@ -844,6 +844,7 @@ Memora que le paginas .css and .js personalisate usa un titulo in minusculas, p.
 'note' => "'''Nota:'''",
 'previewnote' => "'''Isto es solmente un previsualisation.'''
 Le modificationes non ha ancora essite publicate!",
+'continue-editing' => 'Continuar a modificar',
 'previewconflict' => 'Iste previsualisation reflecte le apparentia final del texto in le area de modification superior
 si tu opta pro publicar lo.',
 'session_fail_preview' => "'''Nos non poteva processar tu modification proque nos perdeva le datos del session.
index 2b5502c..0a85879 100644 (file)
@@ -405,7 +405,7 @@ $2',
 Ti naited a rason ket ''$2''.",
 'filereadonlyerror' => 'Di nabaliwan ti papales "$1" gapu ket ti repositorio ti papeles "$2" ket basaen laeng a moda.
 
-Ti rason a naited ket "\'\'$3\'\'".',
+Ti administrador a nagserra ket nagited iti daytoy a panagilawlawag "\'\'$3\'\'".',
 
 # Virus scanner
 'virus-badscanner' => 'Madi di panaka-aramidna: Di am-ammo a birus a panagskan: "$1"',
@@ -501,6 +501,7 @@ Awan ti e-surat nga ipatulod para dagitoy a langa.',
 'invalidemailaddress' => 'Ti e-surat a pagtaengam ket saan a maawat, ket kasla addaan ti saan a napudno a nakabuklan.
 Pangngaasi ta ikkam ti nasayaat  a  nakabuklan a pagtaengan wenno ikkatem amin dagiti naikabil mo.',
 'cannotchangeemail' => 'Dagiti pakabilangan nga e-surat a pagtaengan ket saan a mabaliwan ditoy a wiki.',
+'emaildisabled' => 'Daytoy a pagsaaadan ket saan a makaipatuod kadagiti e-surat.',
 'accountcreated' => 'Naaramiden ti pakabilangan',
 'accountcreatedtext' => 'Naaramiden ti pakabilangan a pagaramat ni $1.',
 'createaccount-title' => 'Panagaramid iti pakabilangan para iti {{SITENAME}}',
@@ -708,7 +709,9 @@ Ti naudi a listaan ti panaka-serra ket adda dita baba tapno mausar a reperensia:
 Annawid a .css ken .js dagiti titulo ket agususar ti napababa a letra, a kas dagiti {{ns:user}}:Foo/vector.css saan ket a {{ns:user}}:Foo/Vector.css.",
 'updated' => '(Napabaro)',
 'note' => "'''Paammo:'''",
-'previewnote' => "'''Maysa laeng a pagpadas daytoy; dagiti sinukatam ket saan pay a naidulin!'''",
+'previewnote' => "'''Laglagipem a daytoy ket panagipadas laeng.'''
+Dagiti sinukatam ket saan pay a naidulin!",
+'continue-editing' => 'Agtultuloy nga agurnos',
 'previewconflict' => 'Daytoy a panagpadas ket agiparang ti testo dita ngato a panagurnos a lugar a kasla agparang no kayatmo nga idulin.',
 'session_fail_preview' => "'''Pasensian a! Saan mi a maaramid ti panag-urnos gapu ngamin ta naawanan ti gimong ti data.'''
 Pangngaasi ta padasem manen.
@@ -724,6 +727,7 @@ Ti panag-urnos ket saan a naawat tapno mapawilan ti panakadadael ti testo ti pan
 Mapasamak daytoy no agus-usar ka ti saan a nasayaat a naibasta ti sapot a diamammo a proxy a panagserbi.",
 'edit_form_incomplete' => "'''Adda dagiti paset ti panag-urnos a kabuklan a saan a nakadanon dita server; kitkitaen nga dagiti pianag-urnos mo ket saan a naikkatan ken padasem manen.'''",
 'editing' => 'Ururnosen ti $1',
+'creating' => 'Agparpartuat ti $1',
 'editingsection' => 'Ururnosen ti $1 (paset)',
 'editingcomment' => 'Ururnosen ti $1 (baro a paset)',
 'editconflict' => 'Adda kasinnungat ti panag-urnos: $1',
@@ -791,6 +795,7 @@ Kasla met naikkaten.',
 'edit-no-change' => 'Ti inurnos mo ket saan a naikaskaso, ngamin ket awan ti nasukatan a testo.',
 'edit-already-exists' => 'Saan a makaaramid ti baro a panid.
 Adda met daytoyen.',
+'defaultmessagetext' => 'Naisigud a testo ti mensahe',
 
 # Parser/template warnings
 'expensive-parserfunction-warning' => "'''Ballaag:''' Daytoy a panid ket adu unay kadagiti nangina a parser nga opisio a pinagtawtawag.
@@ -950,8 +955,8 @@ Saan mo a mabalin a serrekan.',
 
 # Suppression log
 'suppressionlog' => 'Listaan ti nadepdepan',
-'suppressionlogtext' => 'Dita baba ket addaan dagiti listaan ti pinagikkat ken naserraan nga adda nagyanna a nailemmeng kadagiti administrador.
-Kitaen ti [[Special:BlockList|Listaan ti naserraan nga IP]] iti listaan ti agdama a kadagiti operasional a pinagparit ken panagserra',
+'suppressionlogtext' => 'Dita baba ket addaan dagiti listaan ti pinagikkat ken npanagserra a nairaman dagiti linaon a nailemmeng manipud kadagiti administrador.
+Kitaen ti [[Special:BlockList|Listaan ti lapden nga IP]] para iti listaan kadagiti agdama nga operasional a pinagparit ken panagserra.',
 
 # History merging
 'mergehistory' => 'Pagtiponen dagiti pakasaritaan ti pampanid',
@@ -1895,6 +1900,10 @@ Mapabassit mo ti pinagpakita no piliam ti kita ti listaan, ti nagan ti gar-arama
 'allpages-bad-ns' => 'Awan ti {{SITENAME}} iti nagan ti lugar a "$1".',
 'allpages-hide-redirects' => 'Ilemmeng dagiti baw-ing',
 
+# SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => 'Kitkitaenm ti naidulin a bersion iti daytoy a panid, nga addan ti kadaanan a $1.',
+'cachedspecial-refresh-now' => 'Kitaen ti kinaudian.',
+
 # Special:Categories
 'categories' => 'Dagiti kategoria',
 'categoriespagetext' => 'Dagiti sumaganad a {{PLURAL:$1|nagyan ti kategoria|dagiti nagyan ti kategoria}} pampanid wenno midia.
@@ -2342,8 +2351,8 @@ Ikkan ti nainaganan a rason dita baba (kas pagarigan, dakamaten ti maysa a panid
 'ipb-confirm' => 'Pasingkedan ti serra',
 'badipaddress' => 'Imbalido nga IP a pagtaengan',
 'blockipsuccesssub' => 'Balligi ti panangserra',
-'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] ket naserraan.<br />
-Kitaen ti [[Special:BlockList|listaan ti IP a naserraan]] ta kitaen dagiti serra.',
+'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] ket naserraanen.<br />
+Kitaen ti [[Special:BlockList|listaan ti lapden nga IP ]] tapno marepaso dagiti serra.',
 'ipb-blockingself' => 'Mangrugrugi ka nga agserra kenka! Sigurado nga kayatmo nga aramiden daytoy?',
 'ipb-confirmhideuser' => 'Mangrugrugi ka ti mangserra ti agar-aramat nga addaan ti napabalin na nga "ilemmeng ti agar-aramat". Iddeppen na ti nagan daytoy nga agar-aramat kadagiti amin a listaan ken dagiti naikabkabil ti listaan. Sigurado ka a kasta ti kayatmo?',
 'ipb-edit-dropdown' => 'Urnosen dagiti rason ti panagserra',
@@ -2396,9 +2405,9 @@ Ti listaan ti serra ket naikabil dita baba tapno mausar a reperensia:',
 Ti listaan ti napasardeng ket naikabil dita baba tapno mausar a reperensia:',
 'blocklogentry' => 'naserraan ni [[$1]] nga addaan ti oras ti panagpaso nga $2 $3',
 'reblock-logentry' => 'sinukatan ti panakaserra ni [[$1]] ti agtapos nga oras nga $2 $3',
-'blocklogtext' => 'Daytoy ket listaan ti gar-aramat kadagiti pinagserraken panaglukat ti serra
-Dagiti na-automatiko a panakaserra ti IP a pagtaengan ket saan a nailista.
-Kitaen ti [[Special:BlockList|Listaan ti serra ti IP]] ti listaan kadagiti agdama a naiparit a pagpataray ken dagiti serra.',
+'blocklogtext' => 'Daytoy ket listaan ti agar-aramat kadagiti pinagserra ken panaglukat ti serra
+Dagiti na-atomatiko a panakaserra ti IP a pagtaengan ket saan a nailista.
+Kitaen ti [[Special:BlockList|Listaan ti lapden nga IP]] para iti listaan kadagiti agdama a naiparit a pagpataray ken dagiti serra.',
 'unblocklogentry' => 'lukatan ti serra ni $1',
 'block-log-flags-anononly' => 'dagiti di am-ammo nga agar-aramat laeng',
 'block-log-flags-nocreate' => 'naisardeng ti pinagaramid iti pakabilangan',
@@ -3073,6 +3082,8 @@ Dagiti dadduma ket mailemmeng a kinasigud.
 'exif-planarconfiguration-1' => 'chunky format',
 'exif-planarconfiguration-2' => 'planar format',
 
+'exif-colorspace-65535' => 'Di-nakalibrar',
+
 'exif-componentsconfiguration-0' => 'awan',
 
 'exif-exposureprogram-0' => 'Saan a naipalpalawag',
@@ -3455,6 +3466,9 @@ Naka-awat ka kuman ti [{{SERVER}}{{SCRIPTPATH}}/COPYING kopia iti GNU Sapasap a
 'version-software' => 'Naikabil a software',
 'version-software-product' => 'Produkto',
 'version-software-version' => 'Bersion',
+'version-entrypoints' => 'Paserrekan a puntos dagiti URL',
+'version-entrypoints-header-entrypoint' => 'Pagserrekan a puntos',
+'version-entrypoints-header-url' => 'URL',
 
 # Special:FilePath
 'filepath' => 'Dalanan ti papeles',
@@ -3644,4 +3658,15 @@ Nupay kasta, mau-sar mo ti nakabuklan dita baba. Ti komentario nga ited mo ket m
 'api-error-uploaddisabled' => 'Nabaldado ti mangipapan iti daytoy a wiki.',
 'api-error-verification-error' => 'Dakes ngata daytoy a papeles, wenno addaan ti madi a pagpa-atiddog.',
 
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|segundo|seg-segundo}}',
+'duration-minutes' => '$1 {{PLURAL:$1|minuto|min-minuto}}',
+'duration-hours' => '$1 {{PLURAL:$1|oras|or-oras}}',
+'duration-days' => '$1 {{PLURAL:$1|aldaw|al-aldaw}}',
+'duration-weeks' => '$1 {{PLURAL:$1|lawas|law-lawas}}',
+'duration-years' => '$1 {{PLURAL:$1|tawen|taw-tawen}}',
+'duration-decades' => '$1 {{PLURAL:$1|dekada|dek-dekada}}',
+'duration-centuries' => '$1 {{PLURAL:$1|siglo|sig-siglo}}',
+'duration-millennia' => '$1 {{PLURAL:$1|milenio|mil-milenio}}',
+
 );
index 2cb6ac6..8e00feb 100644 (file)
@@ -920,6 +920,7 @@ L'ultimo elemento del registro dei blocchi è riportato di seguito per informazi
 'note' => "'''NOTA:'''",
 'previewnote' => "'''Ricorda che questa è solo un'anteprima.'''
 Le tue modifiche NON sono ancora state salvate!",
+'continue-editing' => 'Continua a modificare',
 'previewconflict' => 'L\'anteprima corrisponde al testo presente nella casella di modifica superiore e rappresenta la pagina come apparirà se si sceglie di premere "Salva la pagina" in questo momento.',
 'session_fail_preview' => "'''Non è stato possibile elaborare la modifica perché sono andati persi i dati relativi alla sessione.
 Riprovare.
index dcbbe03..d75dcce 100644 (file)
@@ -9,6 +9,7 @@
  *
  * @author Akaniji
  * @author Alexsh
+ * @author Ant176
  * @author Aotake
  * @author Aphaia
  * @author Broad-Sky
@@ -480,12 +481,12 @@ $messages = array(
 'category-empty' => "''このカテゴリには、ページまたはメディアがひとつもありません。''",
 'hidden-categories' => '{{PLURAL:$1|隠しカテゴリ}}',
 'hidden-category-category' => '隠しカテゴリ',
-'category-subcat-count' => '{{PLURAL:$2|このカテゴリには以下の下位カテゴリのみが含まれています。|このカテゴリには $2 下位カテゴリが含まれており、そのうち以下の{{PLURAL:$1|下位カテゴリ| $1 下位カテゴリ}}を表示しています。}}',
-'category-subcat-count-limited' => 'このカテゴリには以下の{{PLURAL:$1|下位カテゴリ| $1 下位カテゴリ}}が含まれています。',
-'category-article-count' => '{{PLURAL:$2|このカテゴリには以下のページのみ含まれています。|このカテゴリには $2 ページが含まれており、そのうち以下の {{PLURAL:$1|$1 ページ}}を表示しています。}}',
-'category-article-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ページ| $1 ページ}}が含まれています。',
+'category-subcat-count' => '{{PLURAL:$2|このカテゴリには以下の下位カテゴリのみが含まれています。|このカテゴリには $2 下位カテゴリが含まれており、そのうち以下の{{PLURAL:$1|下位カテゴリ|&#32;$1 下位カテゴリ}}を表示しています。}}',
+'category-subcat-count-limited' => 'このカテゴリには以下の{{PLURAL:$1|下位カテゴリ|&#32;$1 下位カテゴリ}}が含まれています。',
+'category-article-count' => '{{PLURAL:$2|このカテゴリには以下のページのみ含まれています。|このカテゴリには $2 ページが含まれており、そのうち以下の {{PLURAL:$1|$1 ページ}}を表示しています。}}',
+'category-article-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ページ|&#32;$1 ページ}}が含まれています。',
 'category-file-count' => '{{PLURAL:$2|このカテゴリには以下のファイルのみが含まれています。|このカテゴリには $2 ファイルが含まれており、そのうち以下の {{PLURAL:$1|$1 ファイル}}を表示しています。}}',
-'category-file-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ファイル| $1 ファイル}}が含まれています。',
+'category-file-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ファイル|&#32;$1 ファイル}}が含まれています。',
 'listingcontinuesabbrev' => 'の続き',
 'index-category' => '検索エンジンに収集されるページ',
 'noindex-category' => '検索エンジンに収集されないページ',
@@ -640,7 +641,7 @@ $1',
 'collapsible-collapse' => '折り畳む',
 'collapsible-expand' => '展開する',
 'thisisdeleted' => '$1を閲覧または復帰しますか?',
-'viewdeleted' => '$1を表示しますか?',
+'viewdeleted' => '$1を閲覧しますか?',
 'restorelink' => '削除された$1編集',
 'feedlinks' => 'フィード:',
 'feed-invalid' => 'フィード形式の指定が間違っています。',
@@ -771,7 +772,7 @@ $2',
 'yourname' => '利用者名:',
 'yourpassword' => 'パスワード:',
 'yourpasswordagain' => 'パスワード再入力:',
-'remembermypassword' => 'このブラウザーにログイン情報を保存 (最長{{PLURAL:$1|日|日間}})',
+'remembermypassword' => 'このブラウザーにログイン情報を保存 (最長 $1 {{PLURAL:$1|日|日間}})',
 'securelogin-stick-https' => 'ログイン後にHTTPS接続を維持',
 'yourdomainname' => 'ドメイン:',
 'externaldberror' => '外部の認証データベースでエラーが発生したか、または外部アカウント情報の更新が許可されていません。',
@@ -1064,7 +1065,8 @@ IP アドレスは複数の利用者で共有されている場合がありま
 'updated' => '(更新)',
 'note' => "'''お知らせ:'''",
 'previewnote' => "'''これはプレビューです。'''
-変更箇所はまだ保存されていません",
+変更箇所はまだ保存されていません!",
+'continue-editing' => '編集を続行',
 'previewconflict' => 'このプレビューは、上の文章編集エリアの文章を保存した場合にどう見えるようになるかを示すものです。',
 'session_fail_preview' => "'''申し訳ありません!セッションデータが消失したため編集を処理できませんでした。'''
 もう一度やり直してください。
@@ -1139,7 +1141,7 @@ IP アドレスは複数の利用者で共有されている場合がありま
 参考のため以下にこのページの削除と移動の記録を表示します:",
 'moveddeleted-notice' => 'このページは削除されています。
 参考のため、このページの削除と移動の記録を以下に表示します。',
-'log-fulllog' => '完全な記録を見る',
+'log-fulllog' => '完全な記録を閲覧',
 'edit-hook-aborted' => 'フックによって編集が破棄されました。
 理由は不明です。',
 'edit-gone-missing' => 'ページを更新できませんでした。
@@ -1179,7 +1181,7 @@ IP アドレスは複数の利用者で共有されている場合がありま
 $3が示した理由は ''$2'' です。",
 
 # History pages
-'viewpagelogs' => 'このページに関する記録を表示',
+'viewpagelogs' => 'このページに関する記録を閲覧',
 'nohistory' => 'このページには編集履歴がありません。',
 'currentrev' => '最新版',
 'currentrev-asof' => '$1時点における最新版',
@@ -1559,7 +1561,7 @@ HTMLタグを見直してください。',
 'saveusergroups' => '利用者グループを保存',
 'userrights-groupsmember' => '所属グループ:',
 'userrights-groupsmember-auto' => '自動的に付与される権限:',
-'userrights-groups-help' => 'ã\81\93ã\81®å\88©ç\94¨è\80\85ã\81\8cå±\9eã\81\99ã\82\8bã\82°ã\83«ã\83¼ã\83\97ã\82\92å¤\89æ\9b´ã\81\99ã\82\8bã\81\93ã\81¨ã\81\8cã\81§ã\81\8dã\81¾ã\81\99ã\80\82
+'userrights-groups-help' => 'この利用者が属するグループを変更できます。
 * チェックが入っているボックスは、この利用者がそのグループに属していることを意味します。
 * チェックが入っていないボックスは、この利用者がそのグループに属していないことを意味します。
 * 「*」はグループに一旦追加した場合に除去(あるいはその逆)ができないことを示しています。',
@@ -1598,7 +1600,7 @@ HTMLタグを見直してください。',
 # Rights
 'right-read' => 'ページを閲覧',
 'right-edit' => 'ページを編集',
-'right-createpage' => '(議論ページではない)ページを作成',
+'right-createpage' => '(議論ページ以外の)ページを作成',
 'right-createtalk' => '議論ページを作成',
 'right-createaccount' => '新しい利用者アカウントを作成',
 'right-minoredit' => '細部の編集の印を付ける',
@@ -1825,13 +1827,13 @@ HTMLタグを見直してください。',
 * アップロード中のファイルの名前:'''<tt>[[:$1]]</tt>'''
 * 既存ファイルの名前:'''<tt>[[:$2]]</tt>'''
 違う名前を選択してください。",
-'fileexists-thumbnail-yes' => "このファイルは元の画像から縮小されたもの(サムネイル)のようです。
+'fileexists-thumbnail-yes' => "このファイルは元の画像から縮小されたもの''(サムネイル)''のようです。
 [[$1|thumb]]
 ファイル'''<tt>[[:$1]]</tt>'''を確認してください。
-確認したファイルが同じ画像のもとのサイズの版である場合、サムネイルを個別にアップロードする必要はありません。",
+確認したファイルが同じ画像の元のサイズの版の場合は、サムネイルを別途アップロードする必要はありません。",
 'file-thumbnail-no' => "ファイル名が'''<tt>$1</tt>'''から始まっています。
-他の画像から縮小されたもの(サムネイル)のようです。
\82\88ã\82\8aé«\98精細ã\81ªç\94»å\83\8fã\82\92ã\81\8aæ\8c\81ã\81¡ã\81®å ´å\90\88ã\81¯ã\80\81ã\81\9dã\81¡ã\82\89ã\82\92ã\82¢ã\83\83ã\83\97ã\83­ã\83¼ã\83\89ã\81\97ã\81¦ã\81\8fã\81 ã\81\95ã\81\84ã\80\82ã\81\9dã\81\86ã\81§ない場合はファイル名を変更してください。",
+他の画像から縮小されたもの''(サムネイル)''のようです。
\82\88ã\82\8aé«\98精細ã\81ªç\94»å\83\8fã\82\92ã\81\8aæ\8c\81ã\81¡ã\81®å ´å\90\88ã\81¯ã\81\9dã\82\8cã\82\92ã\82¢ã\83\83ã\83\97ã\83­ã\83¼ã\83\89ã\81\97ã\81¦ã\81\8fã\81 ã\81\95ã\81\84ã\80\82ã\81\8aæ\8c\81ã\81¡ã\81§ã\81¯ない場合はファイル名を変更してください。",
 'fileexists-forbidden' => 'この名前のファイルは既に存在しており、上書きできません。
 アップロードを継続したい場合は、前のページに戻り、別のファイル名を使用してください。
 [[File:$1|thumb|center|$1]]',
@@ -2039,7 +2041,7 @@ URLが正しいものであり、ウェブサイトが稼働していること
 'filehist-dimensions' => '解像度',
 'filehist-filesize' => 'ファイルサイズ',
 'filehist-comment' => 'コメント',
-'filehist-missing' => 'ã\83\95ã\82¡ã\82¤ã\83«ã\81\8cã\81¿ã\81¤ã\81\8bりません',
+'filehist-missing' => 'ã\83\95ã\82¡ã\82¤ã\83«ã\81\8cã\81\82りません',
 'imagelinks' => 'ファイルの使用状況',
 'linkstoimage' => 'このファイルへは以下の {{PLURAL:$1|ページ| $1 ページ}}からリンクしています:',
 'linkstoimage-more' => 'このファイルへは $1 を超える数のページからリンクがあります。
@@ -2711,7 +2713,7 @@ $1',
 ** 複数アカウントの不正利用
 ** 不適切な利用者名',
 'ipb-hardblock' => 'ログインしている利用者によるこのIPアドレスからの編集を不許可',
-'ipbcreateaccount' => 'アカウント作成を禁止する',
+'ipbcreateaccount' => 'アカウント作成を禁止',
 'ipbemailban' => 'メール送信を防止',
 'ipbenableautoblock' => 'この利用者が最後に使用したIPアドレスと、後に編集しようとしたIPアドレスを自動的にブロック',
 'ipbsubmit' => 'この利用者をブロック',
@@ -3064,7 +3066,7 @@ MediaWiki 全般のローカライズ(地域化)に貢献したい場合は
 'tooltip-pt-login' => 'ログインすることが推奨されます。ただし、必須ではありません。',
 'tooltip-pt-anonlogin' => 'ログインすることが推奨されます。ただし、必須ではありません。',
 'tooltip-pt-logout' => 'ログアウト',
-'tooltip-ca-talk' => '記事についての議論',
+'tooltip-ca-talk' => '本文ページについての議論',
 'tooltip-ca-edit' => 'このページを編集できます。保存する前にプレビューボタンを使用してください。',
 'tooltip-ca-addsection' => '新しい節を開始する',
 'tooltip-ca-viewsource' => 'このページは保護されています。
@@ -3098,7 +3100,7 @@ MediaWiki 全般のローカライズ(地域化)に貢献したい場合は
 'tooltip-t-specialpages' => '特別ページの一覧',
 'tooltip-t-print' => 'このページの印刷用ページ',
 'tooltip-t-permalink' => 'このページのこの版への固定リンク',
-'tooltip-ca-nstab-main' => '本文を表示',
+'tooltip-ca-nstab-main' => '本文を閲覧',
 'tooltip-ca-nstab-user' => '利用者ページを表示',
 'tooltip-ca-nstab-media' => 'メディアページを表示',
 'tooltip-ca-nstab-special' => 'これは特別ページです。編集することはできません。',
@@ -3107,7 +3109,7 @@ MediaWiki 全般のローカライズ(地域化)に貢献したい場合は
 'tooltip-ca-nstab-mediawiki' => 'システムメッセージを表示',
 'tooltip-ca-nstab-template' => 'テンプレートを表示',
 'tooltip-ca-nstab-help' => 'ヘルプページを表示',
-'tooltip-ca-nstab-category' => 'カテゴリページを表示',
+'tooltip-ca-nstab-category' => 'カテゴリページを閲覧',
 'tooltip-minoredit' => 'この編集を細部の変更とマーク',
 'tooltip-save' => '変更を保存',
 'tooltip-preview' => '変更をプレビューで確認できます。保存前に使用してください!',
@@ -4044,7 +4046,7 @@ MediaWikiは、有用であることを期待して配布されていますが
 'fileduplicatesearch-info' => '$1×$2 ピクセル<br />ファイルサイズ:$3<br />MIMEタイプ:$4',
 'fileduplicatesearch-result-1' => 'ファイル「$1」と重複するファイルはありません。',
 'fileduplicatesearch-result-n' => 'ファイル「$1」は$2件のファイルと重複しています。',
-'fileduplicatesearch-noresults' => 'ã\80\8c$1ã\80\8dã\81¨ã\81\84ã\81\86å\90\8då\89\8dã\81®ã\83\95ã\82¡ã\82¤ã\83«ã\81\8cã\81¿ã\81¤ã\81\8bりません。',
+'fileduplicatesearch-noresults' => 'ã\80\8c$1ã\80\8dã\81¨ã\81\84ã\81\86å\90\8då\89\8dã\81®ã\83\95ã\82¡ã\82¤ã\83«ã\81¯ã\81\82りません。',
 
 # Special:SpecialPages
 'specialpages' => '特別ページ',
index e816497..0a317eb 100644 (file)
@@ -839,6 +839,7 @@ $2
 'note' => "'''შენიშვნა:'''",
 'previewnote' => "'''დაიმახსოვრეთ, ეს მხოლოდ წინასწარი გადახედვაა.'''
 თქვენი ცვლილებები ჯერ არ შენახულა!",
+'continue-editing' => 'რედაქტირების გაგრძელება',
 'previewconflict' => 'შავი ნიმუში უჩვენებს ტექსტს ზედა რედაქტირების ფანჯარაში, როგორც ის გამოჩნდება თუ თქვენ მას შეინახავთ.',
 'session_fail_preview' => "'''უკაცრავად! ვერ შევძელით თქვენი რედაქტირების შენახვა სესიის მონაცემთა დაკარგვის გამო.
 გთხოვთ ისევ სცადოთ.
index 7a264ca..7b13bfd 100644 (file)
@@ -835,6 +835,7 @@ Denkt drun datt eegen .css an .js Säiten e kleng geschriwwenen Titel benotzen,
 'note' => "'''Notiz:'''",
 'previewnote' => "'''Denkt drun datt dëst nëmmen eng net gespäichert Versioun ass.'''
 Är Ännerunge sinn nach net gespäichert!",
+'continue-editing' => 'Weider änneren',
 'previewconflict' => 'Dir gesitt an dem ieweschten Textfeld wéi den Text ausgesi wäert, wann Dir späichert.',
 'session_fail_preview' => "'''Är Ännerung konnt net gespäichert gi well d'Date vun Ärer Sessioun verluergaange sinn.
 Versicht et w.e.g. nach eng Kéier.
@@ -1903,6 +1904,7 @@ An all Rei sti Linken zur éischter an zweeter Viruleedung, souwéi d\'Zil vun d
 'wantedpages' => 'Gewënschte Säiten',
 'wantedpages-badtitle' => 'Net valabelen Titel am Resultat: $1',
 'wantedfiles' => 'Gewënschte Fichieren',
+'wantedfiletext-cat' => 'Dës Fichiere gi benotzt awer et gëtt se net. Fichiere aus frieme Repositorie kënnen hei gewise ginn och wann et se gëtt. All esou falsch Positiver ginn <del>duerchgestrach</del>. Zousätzlech gi Säiten an deene Fichieren dra sinn déi et net gëtt op [[:$1]] gewisen.',
 'wantedtemplates' => 'Gewënschte Schablounen',
 'mostlinked' => 'Dacks verlinkte Säiten',
 'mostlinkedcategories' => 'Dacks benotzte Kategorien',
index 3573202..9acbc1b 100644 (file)
@@ -1025,6 +1025,7 @@ $2
 'note' => "'''Напомена:'''",
 'previewnote' => "'''Имајте предвид дека ова е само преглед.'''
 Вашите промени сè уште не се зачувани!",
+'continue-editing' => 'Продолжете со уредување',
 'previewconflict' => 'Овој преглед прикажува како ќе изгледа текстот внесен во горниот дел откако ќе се зачува страницата.',
 'session_fail_preview' => "'''Жалиме! Не можевме да го обработиме вашето уредување поради загуба на сесиски податоци.'''
 Обидете се повторно.
@@ -1428,7 +1429,7 @@ $1",
 Секој што го знае клучот во полево ќе може да го чита вашиот список на набљудувања, па затоа изберете некоја безбедна вредност.
 Еве една случајно-создадена вредност што можете да ја користите: $1',
 'savedprefs' => 'Вашите нагодувања се зачувани.',
-'timezonelegend' => 'ЧаÑ\81овна Ð·Ð¾Ð½Ð°:',
+'timezonelegend' => 'ЧаÑ\81овен Ð¿Ð¾Ñ\98аÑ\81:',
 'localtime' => 'Локално време:',
 'timezoneuseserverdefault' => 'Од викито ($1)',
 'timezoneuseoffset' => 'Друго (посочете отстапување)',
@@ -1676,7 +1677,7 @@ $1",
 'rcshowhidemine' => '$1 мои уредувања',
 'rclinks' => 'Прикажи скорешни $1 промени во последните $2 дена<br />$3',
 'diff' => 'разл',
-'hist' => 'ист',
+'hist' => 'истор',
 'hide' => 'Скриј',
 'show' => 'Прикажи',
 'minoreditletter' => 'с',
index e9caaf1..6a988b5 100644 (file)
@@ -980,6 +980,7 @@ $1 ആണ് ഈ തടയൽ നടത്തിയത്. ''$2'' എന്ന
 'note' => "'''പ്രത്യേക ശ്രദ്ധയ്ക്ക്:'''",
 'previewnote' => "'''ഇതൊരു പ്രിവ്യൂ മാത്രമാണെന്ന് ഓർക്കുക.'''
 താങ്കൾ വരുത്തിയ മാറ്റങ്ങൾ ഇതുവരെ സേവ് ചെയ്തിട്ടില്ല!",
+'continue-editing' => 'തിരുത്തൽ തുടരുക',
 'previewconflict' => 'ഈ പ്രിവ്യൂവിൽ മുകളിലെ ടെക്സ്റ്റ് ഏരിയയിലുള്ള എഴുത്ത് മാത്രമാണ് കാട്ടുന്നത്, സേവ്‌ ചെയ്യാൻ താങ്കൾ തീരുമാനിച്ചാൽ അത് സേവ് ആകുന്നതാണ്.',
 'session_fail_preview' => "'''ക്ഷമിക്കണം! സെഷൻ ഡാറ്റ നഷ്ടപ്പെട്ടതിനാൽ താങ്കളുടെ തിരുത്തലിന്റെ തുടർപ്രക്രിയ നടത്തുവാൻ സാധിച്ചില്ല.''' 
 ദയവായി വീണ്ടും ശ്രമിക്കൂ.
@@ -2252,7 +2253,7 @@ https://www.mediawiki.org/wiki/Manual:Image_Authorization കാണുക.',
 'notanarticle' => 'ലേഖന താൾ അല്ല',
 'notvisiblerev' => 'മറ്റൊരു ഉപയോക്താവ് സൃഷ്ടിച്ച അവസാനത്തെ നാൾപ്പതിപ്പ് മായ്ച്ചിരിക്കുന്നു',
 'watchnochange' => 'താങ്കൾ ശ്രദ്ധിക്കുന്ന താളുകൾ ഒന്നും തന്നെ ഇക്കാലയളവിൽ തിരുത്തപ്പെട്ടിട്ടില്ല.',
-'watchlist-details' => 'à´¸à´\82â\80\8cവാദà´\82 à´¤à´¾à´³àµ\81à´\95ൾ à´\85à´²àµ\8dലാതàµ\8dà´¤ {{PLURAL:$1|ഒരു താൾ|$1 താളുകൾ}} താങ്കൾ ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിലുണ്ട്.',
+'watchlist-details' => 'à´¸à´\82â\80\8cവാദà´\82 à´¤à´¾à´³àµ\81à´\95ൾ à´\89ൾപàµ\8dà´ªàµ\86à´\9fàµ\81à´¤àµ\8dതാതàµ\86 {{PLURAL:$1|ഒരു താൾ|$1 താളുകൾ}} താങ്കൾ ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിലുണ്ട്.',
 'wlheader-enotif' => '* ഇമെയിൽ വിജ്ഞാപനം സാധ്യമാക്കിയിരിക്കുന്നു.',
 'wlheader-showupdated' => "* താങ്കളുടെ അവസാന സന്ദർശനത്തിനു ശേഷം തിരുത്തപ്പെട്ട താളുകൾ  '''കടുപ്പിച്ച്''' കാണിച്ചിരിക്കുന്നു",
 'watchmethod-recent' => 'ശ്രദ്ധിക്കുന്ന താളുകൾക്കുവേണ്ടി പുതിയ മാറ്റങ്ങൾ പരിശോധിക്കുന്നു',
@@ -2750,7 +2751,7 @@ $1',
 'delete_and_move' => 'മായ്ക്കുകയും മാറ്റുകയും ചെയ്യുക',
 'delete_and_move_text' => '==താൾ മായ്ക്കേണ്ടിയിരിക്കുന്നു==
 
-താà´\99àµ\8dà´\95ൾ à´¸àµ\83à´·àµ\8dà´\9fà´¿à´\95àµ\8dà´\95ാൻ à´¶àµ\8dരമിà´\9aàµ\8dà´\9a "[[:$1]]" à´\8eà´¨àµ\8dà´¨ à´¤à´¾àµ¾ à´¨à´¿à´²à´µà´¿à´²àµ\81à´£àµ\8dà´\9fàµ\8d. à´\86 à´¤à´¾àµ¾ à´®à´¾à´¯àµ\8dà´\9aàµ\8dà´\9aàµ\8d à´ªàµ\81തിയ à´¤à´²à´\95àµ\8dà´\95àµ\86à´\9fàµ\8dà´\9fàµ\8d à´¨àµ½കേണ്ടതുണ്ടോ?',
+മാറàµ\8dറാനായി à´¨àµ½à´\95à´¿à´¯ "[[:$1]]" à´\8eà´¨àµ\8dà´¨ à´¤à´¾àµ¾ à´¨à´¿à´²à´µà´¿à´²àµ\81à´£àµ\8dà´\9fàµ\8d. à´\88 à´®à´¾à´±àµ\8dà´±à´\82 à´¨à´\9fà´¤àµ\8dà´¤àµ\81à´¨àµ\8dനതിനàµ\81à´µàµ\87à´£àµ\8dà´\9fà´¿ à´\86 à´¤à´¾àµ¾ à´®à´¾à´¯àµ\8dà´\95àµ\8dകേണ്ടതുണ്ടോ?',
 'delete_and_move_confirm' => 'ശരി, താൾ നീക്കം ചെയ്യുക',
 'delete_and_move_reason' => '"[[$1]]" എന്നതിൽ നിന്നും മാറ്റാനുള്ള സൗകര്യത്തിനായി മായ്ച്ചു',
 'selfmove' => 'പഴയ തലക്കെട്ടു തന്നെയാണ് മാറ്റാനായി നൽകിയിരിക്കുന്നത്; അതിനാൽ തലക്കെട്ടുമാറ്റം സാദ്ധ്യമല്ല.',
index 6c762c3..9035749 100644 (file)
@@ -859,6 +859,7 @@ Masukan log sekatan terakhir disediakan di bawah sebagai rujukan:',
 'note' => "'''Catatan:'''",
 'previewnote' => "'''Ingatlah bahawa ini hanya pralihat.'''
 Perubahan anda belum disimpan!",
+'continue-editing' => 'Teruskan menyunting',
 'previewconflict' => 'Paparan ini merupakan teks di bahagian atas dalam kotak sunting teks. Teks ini akan disimpan sekiranya anda memilih berbuat demikian.',
 'session_fail_preview' => "'''Kami tidak dapat memproses suntingan anda kerana kehilangan data sesi. Sila cuba lagi. Jika masalah ini berlanjutan, [[Special:UserLogout|log keluar]] dahulu, kemudian log masuk sekali lagi.'''",
 'session_fail_preview_html' => "'''Kami tidak dapat memproses suntingan anda kerana kehilangan data sesi.'''
index 62f921d..43de418 100644 (file)
@@ -1050,6 +1050,7 @@ Uw eigen .css- en .js-pagina's beginnen met een kleine letter, bijvoorbeeld {{ns
 'note' => "'''Opmerking:'''",
 'previewnote' => "'''Let op: dit is een controlepagina.'''
 Uw tekst is niet opgeslagen!",
+'continue-editing' => 'Doorgaan met bewerken',
 'previewconflict' => 'Deze voorvertoning geeft aan hoe de tekst in het bovenste veld eruit ziet als u deze opslaat.',
 'session_fail_preview' => "'''Uw bewerking is niet verwerkt, omdat de sessiegegevens verloren zijn gegaan.
 Probeer het opnieuw.
index 0cb4671..2d4e482 100644 (file)
@@ -910,7 +910,7 @@ $3',
 'uploaderror' => 'Файл сæвæрыны рæдыд',
 'uploadlogpage' => 'Æвгæндты лог',
 'filename' => 'Файлы ном',
-'filedesc' => 'Ð\98вдÑ\82Ñ\8bÑ\82Ñ\8b Ð°фыст:',
+'filedesc' => 'Ð\90фыст:',
 'minlength1' => 'Файлы номы хъуамæ æппынкъаддæр иу дамгъæ уа.',
 'badfilename' => 'Нывы ном ивд æрцыдис. Ныр хуины «$1».',
 'savefile' => 'Бавæр æй',
index faa9510..b5cf2ac 100644 (file)
@@ -3909,6 +3909,8 @@ Imagens serão apresentadas pelo browser na resolução máxima; ficheiros de ou
 'newuserlog-byemail' => 'palavra-chave enviada por correio-electrónico',
 
 # Feedback
+'feedback-bugornote' => 'Se está pronto para descrever um problema técnico em detalhe, por favor, [$1 denuncie um defeito].
+Caso contrário, pode facilmente usar o formulário abaixo. O seu comentário será adicionado à página "[$3 $2]", junto com o seu nome de utilizador e o navegador que está a usar.',
 'feedback-subject' => 'Assunto:',
 'feedback-message' => 'Mensagem:',
 'feedback-cancel' => 'Cancelar',
@@ -3917,9 +3919,10 @@ Imagens serão apresentadas pelo browser na resolução máxima; ficheiros de ou
 'feedback-error1' => 'Erro: O resultado da API não foi reconhecido',
 'feedback-error2' => 'Erro: A edição falhou',
 'feedback-error3' => 'Erro: A API não responde',
+'feedback-thanks' => 'Obrigado! O seu comentário foi adicionado à página "[ $2  $1 ]".',
 'feedback-close' => 'Feito',
 'feedback-bugcheck' => 'Perfeito! Verifique apenas que não é já um dos [$1 defeitos conhecidos].',
-'feedback-bugnew' => 'Eu verifiquei. Reportar um novo bug.',
+'feedback-bugnew' => 'Eu verifiquei. Denunciar um novo defeito.',
 
 # API errors
 'api-error-badaccess-groups' => 'Não tem permissão para enviar ficheiros para esta wiki.',
index e3a6b6f..7889fd0 100644 (file)
@@ -979,6 +979,7 @@ Paginile .css și .js specifice utilizatorilor au titluri care încep cu literă
 'note' => "'''Notă:'''",
 'previewnote' => "'''Țineți cont că aceasta este doar o previzualizare.'''
 Modificările dumneavoastră nu au fost încă salvate!",
+'continue-editing' => 'Continuare editare',
 'previewconflict' => 'Această pre-vizualizare reflectă textul din caseta de sus, respectiv felul în care va arăta articolul dacă alegeți să-l salvați acum.',
 'session_fail_preview' => "'''Ne pare rău! Nu am putut procesa modificarea dumneavoastră din cauza pierderii datelor sesiunii.
 Vă rugăm să încercați din nou.
index 13d71b8..95dc0d0 100644 (file)
@@ -986,6 +986,7 @@ $2
 'note' => "'''Примечание:'''",
 'previewnote' => "'''Помните, что это только предварительный просмотр.'''
 Ваши изменения ещё не были сохранены!",
+'continue-editing' => 'Продолжить редактирование',
 'previewconflict' => 'Этот предварительный просмотр отражает текст в верхнем окне редактирования так, как он будет выглядеть, если вы решите записать его.',
 'session_fail_preview' => "'''К сожалению, сервер не смог обработать вашу правку из-за потери идентификатора сессии.
 Пожалуйста, попробуйте ещё раз.
index dbdf9a3..593c305 100644 (file)
@@ -215,7 +215,7 @@ $messages = array(
 'mytalk' => 'என் பேச்சு',
 'anontalk' => 'இந்த ஐ.பி. முகவரிக்கான பேச்சு',
 'navigation' => 'வழிசெலுத்தல்',
-'and' => '&#32;மற்றும்',
+'and' => ' மற்றும்',
 
 # Cologne Blue skin
 'qbfind' => 'கண்டுபிடி',
@@ -3337,7 +3337,7 @@ $5
 'version-variables' => 'மாறிகள்',
 'version-antispam' => ' குப்பை (spam) தடுப்பு',
 'version-skins' => 'தோல்கள்',
-'version-other' => 'மறà¯\8dறவà¯\88',
+'version-other' => 'பிறரà¯\8d',
 'version-mediahandlers' => 'ஊடக கையாளிகள்',
 'version-hooks' => 'கொக்கிகள்',
 'version-extension-functions' => 'நீட்சி செயற்பாடுகள்',
@@ -3348,7 +3348,7 @@ $5
 'version-version' => '(பதிப்பு $1)',
 'version-license' => 'அனுமதி',
 'version-poweredby-credits' => "இந்த் விக்கி '''[//www.mediawiki.org/ MediaWiki]''' இதன் மூலம் வழங்கப்படுகிறது, காப்புரிமை © 2001-$1 $2.",
-'version-poweredby-others' => 'மறà¯\8dறவà¯\88à®\95ள்',
+'version-poweredby-others' => 'பிறர்',
 'version-license-info' => 'மீடியாவிக்கியானது இலவச மென்பொருள்.இதை நீங்கள் மற்றவர்களுக்கு கொடுப்பது அல்லது திருத்தம் செய்வது இலவச மென்பொருள் அறக்கட்டளை வழங்கிய   GNUவின் பொது உரிம விதிகளுக்குட்பட்டது;உரிமத்தின் இரண்டாவது பதிப்பு அல்லது அதற்கு மேற்பட்ட பதிப்பு (உங்கள் விருப்பத்திற்க்கேற்றவாறு).
 மீடியா உபயோகப்படக்கூடியது என்ற நம்பிக்கையில் வெளியிடப்பட்டுள்ளது, ஆனால் இதற்க்கு உத்தரவாதம் கிடையாது.மேலும் வணிகத்தன்மைக்கான அல்லது ஒரு குறிப்பிட்ட செயலுக்காகவும் உத்தரவாதம் கிடையாது.மேலும் விவரங்களுக்கு GNU பொது உரிமத்தை பார்க்கவும்.
 நீங்கள் இந்த  மென்பொருளுடன் [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License] பெற்றீருப்பிர்கள்;இல்லையெனில் , Free Software Foundation, Inc.,51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA க்கு எழுதவும்.அல்லது [//www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].',
index cac83ff..695bd37 100644 (file)
@@ -955,6 +955,7 @@ $2
 Подібні проблеми можуть виникати при використанні анонімізуючих веб-проксі, що містять помилки.'''",
 'edit_form_incomplete' => "'''Частина даних із форми редагування не досягла сервера. Уважно перевірте чи не пошкоджено ваших правок і спробуйте ще раз.'''",
 'editing' => 'Редагування $1',
+'creating' => 'Створення $1',
 'editingsection' => 'Редагування $1 (розділ)',
 'editingcomment' => 'Редагування $1 (новий розділ)',
 'editconflict' => 'Конфлікт редагування: $1',
index 6be77b9..fc2e86c 100644 (file)
@@ -600,8 +600,8 @@ $2',
 'nologin' => "איר האט נישט קיין קאנטע? '''$1'''.",
 'nologinlink' => 'באשאפֿט א קאנטע',
 'createaccount' => 'באשאפֿט א נייע קאנטע',
-'gotaccount' => "האסט שוין א קאנטע? '''$1'''.",
-'gotaccountlink' => 'אריינלאגירן',
+'gotaccount' => "האסטו שוין א קאנטע? '''$1'''.",
+'gotaccountlink' => 'אַרײַנלאגירן',
 'userlogin-resetlink' => 'פארגעסן אײַערע אַרײַנלאָגירן פרטים?',
 'createaccountmail' => 'דורך ע-פאסט',
 'createaccountreason' => 'אורזאַך:',
@@ -3472,4 +3472,14 @@ $5
 'api-error-uploaddisabled' => 'ארויפֿלאָדן איז אומאַקטיווירט אויף דער וויקי',
 'api-error-verification-error' => 'די טעקע איז מעגלעך פֿארדארבן, אדער האט א פֿאַלשע ענדונג.',
 
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|סעקונדע|סעקונדעס}}',
+'duration-minutes' => '$1 {{PLURAL:$1|מינוט|מינוט}}',
+'duration-hours' => "$1 {{PLURAL:$1|שעה|שעה'ן}}",
+'duration-days' => '$1 {{PLURAL:$1|טאג|טעג}}',
+'duration-weeks' => '$1 {{PLURAL:$1|וואך|וואכן}}',
+'duration-years' => '$1 {{PLURAL:$1|יאר|יאר}}',
+'duration-decades' => '$1 {{PLURAL:$1|צענדליקער|צענדליקערס}}',
+'duration-centuries' => '$1 {{PLURAL:$1|יארהונדערט|יארהונדערטער}}',
+
 );
index 6e7f643..8da71aa 100644 (file)
@@ -889,6 +889,7 @@ $2
 'updated' => '(已更新)',
 'note' => "'''注意:'''",
 'previewnote' => "'''请记住这仅为预览。'''您的更改还未保存!",
+'continue-editing' => '继续编辑',
 'previewconflict' => '这个预览显示了上面文字编辑区中的内容。它将在你选择保存后出现。',
 'session_fail_preview' => "'''抱歉!由于会话数据丢失,我们不能处理你的编辑。'''请重试。如果再次失败,请尝试[[Special:UserLogout|退出]]后重新登录。",
 'session_fail_preview_html' => "'''抱歉!我们不能处理你在进程数据丢失时的编辑。'''
@@ -1425,7 +1426,7 @@ $1",
 'right-editusercss' => '编辑其他用户的CSS文件',
 'right-edituserjs' => '编辑其他用户的JavaScript文件',
 'right-rollback' => '快速回退最后对特定页面作出的编辑的用户的所有编辑',
-'right-markbotedits' => '标示复原编辑作机械人编辑',
+'right-markbotedits' => '将回退编辑标记为机器人编辑动作',
 'right-noratelimit' => '没有使用频率限制',
 'right-import' => '由其它wiki中导入页面',
 'right-importupload' => '由文件上传中导入页面',
@@ -2014,6 +2015,7 @@ $1',
 'allpages-hide-redirects' => '隐藏重定向页',
 
 # SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => '你正在浏览本页的缓存版本,至多可能存在 $1 的延迟。',
 'cachedspecial-refresh-now' => '查看最新的。',
 
 # Special:Categories
@@ -3596,6 +3598,7 @@ MediaWiki是基于使用目的而加以发布,然而不负任何担保责任
 'version-software' => '已安装的软件',
 'version-software-product' => '产品',
 'version-software-version' => '版本',
+'version-entrypoints-header-url' => 'URL',
 
 # Special:FilePath
 'filepath' => '文件路径',
index 6752166..11f0cd5 100644 (file)
@@ -444,6 +444,13 @@ class TextPassDumper extends BackupDumper {
                                        } else {
                                                $text = $this->getTextDb( $id );
                                        }
+
+                                       // No more checks for texts from DB for now.
+                                       // If we received something that is not false,
+                                       // We treat it as good text, regardless of whether it actually is or is not
+                                       if ( $text !== false ) {
+                                               return $text;
+                                       }
                                }
 
                                if ( $text === false ) {
@@ -480,20 +487,22 @@ class TextPassDumper extends BackupDumper {
                        // Something went wrong; we did not a text that was plausible :(
                        $failures++;
 
-
-                       // After backing off for some time, we try to reboot the whole process as
-                       // much as possible to not carry over failures from one part to the other
-                       // parts
-                       sleep( $this->failureTimeout );
-                       try {
-                               $this->rotateDb();
-                               if ( $this->spawn ) {
-                                       $this->closeSpawn();
-                                       $this->openSpawn();
+                       // A failure in a prefetch hit does not warrant resetting db connection etc.
+                       if ( ! $tryIsPrefetch ) {
+                               // After backing off for some time, we try to reboot the whole process as
+                               // much as possible to not carry over failures from one part to the other
+                               // parts
+                               sleep( $this->failureTimeout );
+                               try {
+                                       $this->rotateDb();
+                                       if ( $this->spawn ) {
+                                               $this->closeSpawn();
+                                               $this->openSpawn();
+                                       }
+                               } catch ( Exception $e ) {
+                                       $this->progress( "Rebooting getText infrastructure failed (" . $e->getMessage() . ")" .
+                                               " Trying to continue anyways" );
                                }
-                       } catch ( Exception $e ) {
-                               $this->progress( "Rebooting getText infrastructure failed (" . $e->getMessage() . ")" .
-                                       " Trying to continue anyways" );
                        }
                }
 
index 7abbc90..26d7e29 100644 (file)
@@ -174,10 +174,10 @@ class RefreshLinks extends Maintenance {
         * @param $id int The page_id of the redirect
         */
        private function fixRedirect( $id ) {
-               $title = Title::newFromID( $id );
+               $page = WikiPage::newFromID( $id );
                $dbw = wfGetDB( DB_MASTER );
 
-               if ( is_null( $title ) ) {
+               if ( $page === null ) {
                        // This page doesn't exist (any more)
                        // Delete any redirect table entry for it
                        $dbw->delete( 'redirect', array( 'rd_from' => $id ),
@@ -185,11 +185,10 @@ class RefreshLinks extends Maintenance {
                        return;
                }
 
-               $page = WikiPage::factory( $title );
                $rt = $page->getRedirectTarget();
 
                if ( $rt === null ) {
-                       // $title is not a redirect
+                       // The page is not a redirect
                        // Delete any redirect table entry for it
                        $dbw->delete( 'redirect', array( 'rd_from' => $id ),
                                __METHOD__ );
@@ -201,28 +200,29 @@ class RefreshLinks extends Maintenance {
         * @param $id int The page_id
         */
        public static function fixLinksFromArticle( $id ) {
-               global $wgParser;
+               global $wgParser, $wgContLang;
 
-               $title = Title::newFromID( $id );
-               $dbw = wfGetDB( DB_MASTER );
+               $page = WikiPage::newFromID( $id );
 
                LinkCache::singleton()->clear();
 
-               if ( is_null( $title ) ) {
+               if ( $page === null ) {
                        return;
                }
 
-               $revision = Revision::newFromTitle( $title );
-               if ( !$revision ) {
+               $text = $page->getRawText();
+               if ( $text === false ) {
                        return;
                }
 
+               $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
 
-               $options = new ParserOptions;
-               $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
-               $update = new LinksUpdate( $title, $parserOutput, false );
+               $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+               $parserOutput = $wgParser->parse( $text, $page->getTitle(), $options, true, true, $page->getLatest() );
+               $update = new LinksUpdate( $page->getTitle(), $parserOutput, false );
                $update->doUpdate();
+
                $dbw->commit( __METHOD__ );
        }
 
index 2855f9a..909cb6e 100644 (file)
@@ -75,6 +75,7 @@ return array(
                        'common/commonContent.css' => array( 'media' => 'screen' ),
                        'common/commonInterface.css' => array( 'media' => 'screen' ),
                        'vector/screen.css' => array( 'media' => 'screen' ),
+                       'vector/screen-hd.css' => array( 'media' => 'screen and (min-width: 900px)' ),
                ),
                'scripts' => 'vector/vector.js',
                'remoteBasePath' => $GLOBALS['wgStylePath'],
index 148b08b..eb281f8 100644 (file)
@@ -1292,6 +1292,7 @@ var mw = ( function ( $, undefined ) {
                                 */
                                state: function ( module, state ) {
                                        var m;
+
                                        if ( typeof module === 'object' ) {
                                                for ( m in module ) {
                                                        mw.loader.state( m, module[m] );
@@ -1301,7 +1302,14 @@ var mw = ( function ( $, undefined ) {
                                        if ( registry[module] === undefined ) {
                                                mw.loader.register( module );
                                        }
-                                       registry[module].state = state;
+                                       if ( state === 'ready' && registry[module].state !== state) {
+                                               // Make sure pending modules depending on this one get executed if their
+                                               // dependencies are now fulfilled!
+                                               registry[module].state = state;
+                                               handlePending( module );
+                                       } else {
+                                               registry[module].state = state;
+                                       }
                                },
                
                                /**
diff --git a/skins/vector/screen-hd.css b/skins/vector/screen-hd.css
new file mode 100644 (file)
index 0000000..ebb639b
--- /dev/null
@@ -0,0 +1,31 @@
+/* Vector screen styles for high definition displays */
+
+div#content {
+       margin-left: 11em;
+       padding: 1.25em 1.5em 1.5em 1.5em;
+}
+#p-logo {
+       left: 0.5em;
+}
+div#footer {
+       margin-left: 11em;
+       padding: 1.25em;
+}
+div#mw-panel div.portal div.body {
+       padding-left: 0.5em;
+}
+div#mw-panel div.portal h5 {
+       padding-left: 2.25em;
+}
+#p-search {
+       margin-right: 1em;
+}
+#left-navigation {
+       left: 11em;
+}
+#p-personal {
+       right: 1em;
+}
+#mw-head-base {
+       margin-left: 11em;
+}
index d48b5f8..b309d15 100644 (file)
@@ -72,7 +72,7 @@ div.emptyPortlet {
 /* Personal */
 #p-personal {
        position: absolute;
-       top: 0;
+       top: 0.33em;
        right: 0.75em;
 }
 #p-personal h5 {
@@ -844,3 +844,45 @@ div.vectorTabs ul {
 .tipsy {
        font-size: 0.8em;
 }
+
+/* Animate between standard and high definition layouts */
+
+div#content,
+div#footer {
+       transition: margin-left 250ms, padding 250ms;
+       -moz-transition: margin-left 250ms, padding 250ms;
+       -webkit-transition: margin-left 250ms, padding 250ms;
+       -o-transition: margin-left 250ms, padding 250ms;
+}
+#p-logo,
+#left-navigation {
+       transition: left 250ms;
+       -moz-transition: left 250ms;
+       -webkit-transition: left 250ms;
+       -o-transition: left 250ms;
+}
+div#mw-panel div.portal div.body,
+div#mw-panel div.portal h5 {
+       transition: padding-left 250ms;
+       -moz-transition: padding-left 250ms;
+       -webkit-transition: padding-left 250ms;
+       -o-transition: padding-left 250ms;
+}
+#p-search {
+       transition: margin-right 250ms;
+       -moz-transition: margin-right 250ms;
+       -webkit-transition: margin-right 250ms;
+       -o-transition: margin-right 250ms;
+}
+#p-personal {
+       transition: right 250ms;
+       -moz-transition: right 250ms;
+       -webkit-transition: right 250ms;
+       -o-transition: right 250ms;
+}
+#mw-head-base {
+       transition: margin-left 250ms;
+       -moz-transition: margin-left 250ms;
+       -webkit-transition: margin-left 250ms;
+       -o-transition: margin-left 250ms;
+}
index 2e95f55..612c368 100644 (file)
@@ -1307,6 +1307,26 @@ class FileBackendTest extends MediaWikiTestCase {
 
                $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." );
 
+               // Expected listing (top files only)
+               $expected = array(
+                       "test1.txt",
+                       "test2.txt",
+                       "test3.txt",
+                       "test4.txt",
+                       "test5.txt"
+               );
+               sort( $expected );
+
+               // Actual listing (top files only)
+               $list = array();
+               $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
+
                foreach ( $files as $file ) { // clean up
                        $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
                }
@@ -1315,6 +1335,182 @@ class FileBackendTest extends MediaWikiTestCase {
                foreach ( $iter as $iter ) {} // no errors
        }
 
+       public function testGetDirectoryList() {
+               $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
+               $this->doTestGetDirectoryList();
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestGetDirectoryList();
+               $this->tearDownFiles();
+       }
+
+       private function doTestGetDirectoryList() {
+               $backendName = $this->backendClass();
+
+               $base = $this->baseStorePath();
+               $files = array(
+                       "$base/unittest-cont1/test1.txt",
+                       "$base/unittest-cont1/test2.txt",
+                       "$base/unittest-cont1/test3.txt",
+                       "$base/unittest-cont1/subdir1/test1.txt",
+                       "$base/unittest-cont1/subdir1/test2.txt",
+                       "$base/unittest-cont1/subdir2/test3.txt",
+                       "$base/unittest-cont1/subdir2/test4.txt",
+                       "$base/unittest-cont1/subdir2/subdir/test1.txt",
+                       "$base/unittest-cont1/subdir3/subdir/test2.txt",
+                       "$base/unittest-cont1/subdir4/subdir/test3.txt",
+                       "$base/unittest-cont1/subdir4/subdir/test4.txt",
+                       "$base/unittest-cont1/subdir4/subdir/test5.txt",
+                       "$base/unittest-cont1/subdir4/subdir/sub/test0.txt",
+                       "$base/unittest-cont1/subdir4/subdir/sub/120-px-file.txt",
+               );
+
+               // Add the files
+               $ops = array();
+               foreach ( $files as $file ) {
+                       $this->prepare( array( 'dir' => dirname( $file ) ) );
+                       $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
+               }
+               $status = $this->backend->doOperations( $ops );
+               $this->assertEquals( array(), $status->errors,
+                       "Creation of files succeeded ($backendName)." );
+               $this->assertEquals( true, $status->isOK(),
+                       "Creation of files succeeded with OK status ($backendName)." );
+
+               // Expected listing
+               $expected = array(
+                       "subdir1",
+                       "subdir2",
+                       "subdir3",
+                       "subdir4",
+               );
+               sort( $expected );
+
+               $this->assertEquals( true,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir1" ) ),
+                       "Directory exists in ($backendName)." );
+               $this->assertEquals( true,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) ),
+                       "Directory exists in ($backendName)." );
+               $this->assertEquals( false,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/test1.txt" ) ),
+                       "Directory does not exists in ($backendName)." );
+
+               // Actual listing (no trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Actual listing (with trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Expected listing
+               $expected = array(
+                       "subdir",
+               );
+               sort( $expected );
+
+               // Actual listing (no trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Actual listing (with trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Actual listing (using iterator second time)
+               $list = array();
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName), second iteration." );
+
+               // Expected listing (recursive)
+               $expected = array(
+                       "subdir1",
+                       "subdir2",
+                       "subdir3",
+                       "subdir4",
+                       "subdir2/subdir",
+                       "subdir3/subdir",
+                       "subdir4/subdir",
+                       "subdir4/subdir/sub",
+               );
+               sort( $expected );
+
+               // Actual listing (recursive)
+               $list = array();
+               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+               // Expected listing (recursive)
+               $expected = array(
+                       "subdir",
+                       "subdir/sub",
+               );
+               sort( $expected );
+
+               // Actual listing (recursive)
+               $list = array();
+               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir4" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+               // Actual listing (recursive, second time)
+               $list = array();
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+               foreach ( $files as $file ) { // clean up
+                       $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+               }
+
+               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
+               foreach ( $iter as $iter ) {} // no errors
+       }
+
        // test helper wrapper for backend prepare() function
        private function prepare( array $params ) {
                $this->dirsToPrune[] = $params['dir'];
index d18b33b..e3f780b 100755 (executable)
@@ -18,6 +18,16 @@ define( 'MW_PHPUNIT_TEST', true );
 require_once( "$IP/maintenance/Maintenance.php" );
 
 class PHPUnitMaintClass extends Maintenance {
+
+       function __construct() {
+               parent::__construct();
+               $this->addOption( 'with-phpunitdir'
+                       , 'Directory to include PHPUnit from, for example when using a git fetchout from upstream. Path will be prepended to PHP `include_path`.'
+                       , false # not required
+                       , true  # need arg
+               );
+       }
+
        public function finalSetup() {
                parent::finalSetup();
 
@@ -37,7 +47,42 @@ class PHPUnitMaintClass extends Maintenance {
 
                $wgLocalisationCacheConf['storeClass'] =  'LCStore_Null';
        }
-       public function execute() { }
+
+       public function execute() {
+               global $IP;
+
+               # Make sure we have --configuration or PHPUnit might complain
+               if( !in_array( '--configuration', $_SERVER['argv'] ) ) {
+                       //Hack to eliminate the need to use the Makefile (which sucks ATM)
+                       array_splice( $_SERVER['argv'], 1, 0,
+                               array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) );
+               }
+
+               # --with-phpunitdir let us override the default PHPUnit version
+               if( $phpunitDir = $this->getOption( 'with-phpunitdir' ) ) {
+                       # Sanity checks
+                       if( !is_dir($phpunitDir) ) {
+                               $this->error( "--with-phpunitdir should be set to an existing directory", 1 );
+                       }
+                       if( !is_readable( $phpunitDir."/PHPUnit/Runner/Version.php" ) ) {
+                               $this->error( "No usable PHPUnit installation in $phpunitDir.\nAborting.\n", 1 );
+                       }
+
+                       # Now prepends provided PHPUnit directory
+                       $this->output( "Will attempt loading PHPUnit from `$phpunitDir`\n" );
+                       set_include_path( $phpunitDir
+                               . PATH_SEPARATOR . get_include_path() );
+
+                       # Cleanup $args array so the option and its value do not
+                       # pollute PHPUnit
+                       $key = array_search( '--with-phpunitdir', $_SERVER['argv'] );
+                       unset( $_SERVER['argv'][$key]   ); // the option
+                       unset( $_SERVER['argv'][$key+1] ); // its value
+                       $_SERVER['argv'] = array_values( $_SERVER['argv'] );
+
+               }
+       }
+
        public function getDbType() {
                return Maintenance::DB_ADMIN;
        }
@@ -46,18 +91,13 @@ class PHPUnitMaintClass extends Maintenance {
 $maintClass = 'PHPUnitMaintClass';
 require( RUN_MAINTENANCE_IF_MAIN );
 
-if( !in_array( '--configuration', $_SERVER['argv'] ) ) {
-       //Hack to eliminate the need to use the Makefile (which sucks ATM)
-       array_splice( $_SERVER['argv'], 1, 0,
-               array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) );
-}
-
 require_once( 'PHPUnit/Runner/Version.php' );
-if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) {
+
+if( PHPUnit_Runner_Version::id() !== '@package_version@'
+   && version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) {
        die( 'PHPUnit 3.5 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" );
 }
 require_once( 'PHPUnit/Autoload.php' );
 
 require_once( "$IP/tests/TestsAutoLoader.php" );
 MediaWikiPHPUnitCommand::main();
-