merged from master
authordaniel <daniel.kinzler@wikimedia.de>
Sun, 29 Apr 2012 19:41:03 +0000 (21:41 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Sun, 29 Apr 2012 19:41:03 +0000 (21:41 +0200)
187 files changed:
RELEASE-NOTES-1.19
RELEASE-NOTES-1.20
docs/hooks.txt
includes/AutoLoader.php
includes/ChangesFeed.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Exception.php
includes/ExternalEdit.php
includes/ImagePage.php
includes/OutputPage.php
includes/Title.php
includes/WikiPage.php
includes/api/ApiBase.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiEmailUser.php
includes/api/ApiParse.php
includes/api/ApiProtect.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryCategoryMembers.php
includes/api/ApiUpload.php
includes/cache/GenderCache.php
includes/cache/LinkBatch.php
includes/db/CloneDatabase.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/debug/Debug.php
includes/filerepo/FileRepo.php
includes/filerepo/backend/FSFileBackend.php
includes/filerepo/backend/FileBackend.php
includes/filerepo/backend/FileBackendGroup.php
includes/filerepo/backend/FileBackendMultiWrite.php
includes/filerepo/backend/FileBackendStore.php
includes/filerepo/backend/SwiftFileBackend.php
includes/filerepo/backend/lockmanager/LockManager.php
includes/filerepo/backend/lockmanager/LockManagerGroup.php
includes/filerepo/file/ArchivedFile.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignAPIFile.php
includes/filerepo/file/LocalFile.php
includes/installer/DatabaseUpdater.php
includes/objectcache/BagOStuff.php
includes/objectcache/MemcachedClient.php
includes/parser/Preprocessor_DOM.php
includes/parser/Preprocessor_Hash.php
includes/parser/Preprocessor_HipHop.hphp
includes/profiler/Profiler.php
includes/profiler/ProfilerSimple.php
includes/profiler/ProfilerSimpleText.php
includes/profiler/ProfilerSimpleTrace.php
includes/profiler/ProfilerSimpleUDP.php
includes/profiler/ProfilerStub.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/revisiondelete/RevisionDelete.php
includes/revisiondelete/RevisionDeleteAbstracts.php
includes/revisiondelete/RevisionDeleteUser.php
includes/revisiondelete/RevisionDeleter.php
includes/search/SearchEngine.php
includes/search/SearchUpdate.php
includes/specials/SpecialCategories.php
includes/specials/SpecialEditWatchlist.php
includes/specials/SpecialEmailuser.php
includes/specials/SpecialJavaScriptTest.php
includes/specials/SpecialMergeHistory.php
includes/specials/SpecialSearch.php
includes/specials/SpecialUnblock.php
languages/messages/MessagesAr.php
languages/messages/MessagesArc.php
languages/messages/MessagesAs.php
languages/messages/MessagesAst.php
languages/messages/MessagesBn.php
languages/messages/MessagesCs.php
languages/messages/MessagesDa.php
languages/messages/MessagesDe.php
languages/messages/MessagesDsb.php
languages/messages/MessagesEn.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/MessagesHr.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/MessagesLus.php
languages/messages/MessagesMk.php
languages/messages/MessagesMl.php
languages/messages/MessagesMs.php
languages/messages/MessagesNl.php
languages/messages/MessagesOc.php
languages/messages/MessagesOs.php
languages/messages/MessagesPt.php
languages/messages/MessagesRo.php
languages/messages/MessagesRoa_tara.php
languages/messages/MessagesRu.php
languages/messages/MessagesTa.php
languages/messages/MessagesUk.php
languages/messages/MessagesVi.php
languages/messages/MessagesYi.php
languages/messages/MessagesYue.php
languages/messages/MessagesZh_hans.php
languages/messages/MessagesZh_hant.php
maintenance/Maintenance.php
maintenance/backupTextPass.inc
maintenance/copyFileBackend.php [new file with mode: 0644]
maintenance/language/messages.inc
maintenance/mctest.php
maintenance/populateParentId.php
maintenance/refreshLinks.php
resources/jquery.effects/jquery.effects.blind.js
resources/jquery.effects/jquery.effects.bounce.js
resources/jquery.effects/jquery.effects.clip.js
resources/jquery.effects/jquery.effects.core.js
resources/jquery.effects/jquery.effects.drop.js
resources/jquery.effects/jquery.effects.explode.js
resources/jquery.effects/jquery.effects.fade.js
resources/jquery.effects/jquery.effects.fold.js
resources/jquery.effects/jquery.effects.highlight.js
resources/jquery.effects/jquery.effects.pulsate.js
resources/jquery.effects/jquery.effects.scale.js
resources/jquery.effects/jquery.effects.shake.js
resources/jquery.effects/jquery.effects.slide.js
resources/jquery.effects/jquery.effects.transfer.js
resources/jquery.ui/i18n/jquery.ui.datepicker-de.js
resources/jquery.ui/i18n/jquery.ui.datepicker-et.js
resources/jquery.ui/i18n/jquery.ui.datepicker-fa.js
resources/jquery.ui/i18n/jquery.ui.datepicker-fi.js
resources/jquery.ui/i18n/jquery.ui.datepicker-mk.js
resources/jquery.ui/jquery.ui.accordion.js
resources/jquery.ui/jquery.ui.autocomplete.js
resources/jquery.ui/jquery.ui.button.js
resources/jquery.ui/jquery.ui.core.js
resources/jquery.ui/jquery.ui.datepicker.js
resources/jquery.ui/jquery.ui.dialog.js
resources/jquery.ui/jquery.ui.draggable.js
resources/jquery.ui/jquery.ui.droppable.js
resources/jquery.ui/jquery.ui.mouse.js
resources/jquery.ui/jquery.ui.position.js
resources/jquery.ui/jquery.ui.progressbar.js
resources/jquery.ui/jquery.ui.resizable.js
resources/jquery.ui/jquery.ui.selectable.js
resources/jquery.ui/jquery.ui.slider.js
resources/jquery.ui/jquery.ui.sortable.js
resources/jquery.ui/jquery.ui.tabs.js
resources/jquery.ui/jquery.ui.widget.js
resources/jquery.ui/themes/default/jquery.ui.accordion.css
resources/jquery.ui/themes/default/jquery.ui.autocomplete.css
resources/jquery.ui/themes/default/jquery.ui.button.css
resources/jquery.ui/themes/default/jquery.ui.core.css
resources/jquery.ui/themes/default/jquery.ui.datepicker.css
resources/jquery.ui/themes/default/jquery.ui.dialog.css
resources/jquery.ui/themes/default/jquery.ui.progressbar.css
resources/jquery.ui/themes/default/jquery.ui.resizable.css
resources/jquery.ui/themes/default/jquery.ui.selectable.css
resources/jquery.ui/themes/default/jquery.ui.slider.css
resources/jquery.ui/themes/default/jquery.ui.tabs.css
resources/jquery.ui/themes/default/jquery.ui.theme.css
resources/mediawiki/mediawiki.js
tests/phpunit/includes/cache/GenderCacheTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/maintenance/MaintenanceTest.php [new file with mode: 0644]
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 ebf30ca..bd8f775 100644 (file)
@@ -44,6 +44,8 @@ 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 22887) Add warning and tracking category for preprocessor errors
 
 === Bug fixes in 1.20 ===
 * (bug 30245) Use the correct way to construct a log page title.
@@ -78,7 +80,7 @@ production.
 * (bug 31236) "Next" and "Previous" buttons are shown incorrectly in
   an RTL environment.
 * (bug 35680) jQuery upgraded to 1.7.2.
-* (bug 35681) jQuery UI upgraded to 1.8.18.
+* jQuery UI upgraded to 1.8.19.
 * (bug 35705) QUnit upgraded from 1.2.0 to 1.5.0
 * (bug 35749) Updated maintenance/checkSyntax.php to use Git instead of
   Subversion when invoked with the --modified option.
@@ -88,6 +90,7 @@ production.
 * (bug 33564) transwiki import sometimes result in invalid title.
 * (bug 35572) Blocks appear to succeed even if query fails due to wrong DB structure
 * (bug 31757) Add a word-separator between help-messages in HTMLForm
+* (bug 30410) Removed deprecated $wgFilterCallback and the 'filtered' API error.
 
 === API changes in 1.20 ===
 * (bug 34316) Add ability to retrieve maximum upload size from MediaWiki API.
index 49e0801..79ba13a 100644 (file)
@@ -981,6 +981,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 77f69e3..0f163f6 100644 (file)
@@ -525,10 +525,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 fa2188e..ba3bfd6 100644 (file)
@@ -51,13 +51,13 @@ class ChangesFeed {
         * @param $rows ResultWrapper object with rows in recentchanges table
         * @param $lastmod Integer: timestamp of the last item in the recentchanges table (only used for the cache key)
         * @param $opts FormOptions as in SpecialRecentChanges::getDefaultOptions()
-        * @return null or true
+        * @return null|bool True or null
         */
        public function execute( $feed, $rows, $lastmod, $opts ) {
                global $wgLang, $wgRenderHashAppend;
 
                if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
-                       return;
+                       return null;
                }
 
                $optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend;
index ab811f4..92df370 100644 (file)
@@ -3825,21 +3825,6 @@ $wgSpamRegex = array();
 /** Same as the above except for edit summaries */
 $wgSummarySpamRegex = array();
 
-/**
- * Similarly you can get a function to do the job. The function will be given
- * the following args:
- *   - a Title object for the article the edit is made on
- *   - the text submitted in the textarea (wpTextbox1)
- *   - the section number.
- * The return should be boolean indicating whether the edit matched some evilness:
- *  - true : block it
- *  - false : let it through
- *
- * @deprecated since 1.17 Use hooks. See SpamBlacklist extension.
- * @var $wgFilterCallback bool|string|Closure
- */
-$wgFilterCallback = false;
-
 /**
  * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies
  * @since 1.16
index 1cceee0..105590f 100644 (file)
@@ -36,11 +36,6 @@ class EditPage {
         */
        const AS_HOOK_ERROR                = 210;
 
-       /**
-        * Status: The filter function set in $wgFilterCallback returned true (= block it)
-        */
-       const AS_FILTERING                 = 211;
-
        /**
         * Status: A hook function returned an error
         */
@@ -868,7 +863,7 @@ class EditPage {
 
                                                        # If we just undid one rev, use an autosummary
                                                        $firstrev = $oldrev->getNext();
-                                                       if ( $firstrev->getId() == $undo ) {
+                                                       if ( $firstrev && $firstrev->getId() == $undo ) {
                                                                $undoSummary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() );
                                                                if ( $this->summary === '' ) {
                                                                        $this->summary = $undoSummary;
@@ -1090,7 +1085,6 @@ class EditPage {
                                return true;
 
                        case self::AS_HOOK_ERROR:
-                       case self::AS_FILTERING:
                                return false;
 
                        case self::AS_PARSE_ERROR:
@@ -1167,8 +1161,7 @@ class EditPage {
         * AS_CONTENT_TOO_BIG and AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some time.
         */
        function internalAttemptSave( &$result, $bot = false ) {
-               global $wgFilterCallback, $wgUser, $wgRequest, $wgParser;
-               global $wgMaxArticleSize;
+               global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
 
                $status = Status::newGood();
 
@@ -1214,13 +1207,6 @@ class EditPage {
                        wfProfileOut( __METHOD__ );
                        return $status;
                }
-               if ( $wgFilterCallback && is_callable( $wgFilterCallback ) && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) {
-                       # Error messages or other handling should be performed by the filter function
-                       $status->setResult( false, self::AS_FILTERING );
-                       wfProfileOut( __METHOD__ . '-checks' );
-                       wfProfileOut( __METHOD__ );
-                       return $status;
-               }
                if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) {
                        # Error messages etc. could be handled within the hook...
                        $status->fatal( 'hookaborted' );
@@ -2073,7 +2059,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() );
                }
@@ -2082,7 +2068,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() );
                                }
@@ -2090,7 +2076,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 2d1772b..539d483 100644 (file)
@@ -53,11 +53,11 @@ class MWException extends Exception {
                global $wgExceptionHooks;
 
                if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) {
-                       return; // Just silently ignore
+                       return null; // Just silently ignore
                }
 
                if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) ) {
-                       return;
+                       return null;
                }
 
                $hooks = $wgExceptionHooks[ $name ];
@@ -74,6 +74,7 @@ class MWException extends Exception {
                                return $result;
                        }
                }
+               return null;
        }
 
        /**
index b870475..3468325 100644 (file)
@@ -80,10 +80,16 @@ class ExternalEdit extends ContextSource {
                } elseif ( $this->getRequest()->getVal( 'mode' ) == 'file' ) {
                        $type = "Edit file";
                        $image = wfLocalFile( $this->getTitle() );
-                       $urls = array( 'File' => array(
-                               'Extension' => $image->getExtension(),
-                               'URL' => $image->getCanonicalURL()
-                       ) );
+                       if ( $image ) {
+                               $urls = array(
+                                       'File' => array(
+                                               'Extension' => $image->getExtension(),
+                                               'URL' => $image->getCanonicalURL()
+                                       )
+                               );
+                       } else{
+                               $urls = array();
+                       }
                } else {
                        $type = "Edit text";
                        # *.wiki file extension is used by some editors for syntax
index 5755bdc..ad90f14 100644 (file)
@@ -87,7 +87,8 @@ class ImagePage extends Article {
                $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
 
                if ( $this->getTitle()->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) ) {
-                       return parent::view();
+                       parent::view();
+                       return;
                }
 
                $this->loadFile();
@@ -97,7 +98,8 @@ class ImagePage extends Article {
                                // mTitle is the same as the redirect target so ask Article
                                // to perform the redirect for us.
                                $wgRequest->setVal( 'diffonly', 'true' );
-                               return parent::view();
+                               parent::view();
+                               return;
                        } else {
                                // mTitle is not the same as the redirect target so it is
                                // probably the redirect page itself. Fake the redirect symbol
index beaca5f..bdf2223 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 disabled
+                       $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 552b6da..32ef76d 100644 (file)
@@ -740,17 +740,9 @@ class Title {
                        }
                }
 
-               // Strip off subpages
-               $pagename = $this->getText();
-               if ( strpos( $pagename, '/' ) !== false ) {
-                       list( $username , ) = explode( '/', $pagename, 2 );
-               } else {
-                       $username = $pagename;
-               }
-
                if ( $wgContLang->needsGenderDistinction() &&
                                MWNamespace::hasGenderDistinction( $this->mNamespace ) ) {
-                       $gender = GenderCache::singleton()->getGenderOf( $username, __METHOD__ );
+                       $gender = GenderCache::singleton()->getGenderOf( $this->getText(), __METHOD__ );
                        return $wgContLang->getGenderNsText( $this->mNamespace, $gender );
                }
 
index a71267b..c1af18f 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;
        }
 
        /**
@@ -271,6 +286,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 edd9aca..1103814 100644 (file)
@@ -698,6 +698,35 @@ abstract class ApiBase extends ContextSource {
                );
        }
 
+       /**
+        * @param $params array
+        * @return Title
+        */
+       public function getTitleOrPageId( $params ) {
+               $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
+
+               $titleObj = null;
+               if ( isset( $params['title'] ) ) {
+                       $titleObj = Title::newFromText( $params['title'] );
+                       if ( !$titleObj ) {
+                               $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
+                       }
+               } elseif ( isset( $params['pageid'] ) ) {
+                       $titleObj = Title::newFromID( $params['pageid'] );
+                       if ( !$titleObj ) {
+                               $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
+                       }
+               }
+               return $titleObj;
+       }
+
+       /**
+        * @return array
+        */
+       public function getTitleOrPageIdErrorMessage( ) {
+               return $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) );
+       }
+
        /**
         * Callback function used in requireOnlyOneParameter to check whether reequired parameters are set
         *
@@ -1215,7 +1244,6 @@ abstract class ApiBase extends ContextSource {
                'noimageredirect-anon' => array( 'code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects" ),
                'noimageredirect-logged' => array( 'code' => 'noimageredirect', 'info' => "You don't have permission to create image redirects" ),
                'spamdetected' => array( 'code' => 'spamdetected', 'info' => "Your edit was refused because it contained a spam fragment: \"\$1\"" ),
-               'filtered' => array( 'code' => 'filtered', 'info' => "The filter callback function refused your edit" ),
                'contenttoobig' => array( 'code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes" ),
                'noedit-anon' => array( 'code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages" ),
                'noedit' => array( 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ),
index eb83432..2a6e2e7 100644 (file)
@@ -46,24 +46,11 @@ class ApiDelete extends ApiBase {
        public function execute() {
                $params = $this->extractRequestParams();
 
-               $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
-
-               if ( isset( $params['title'] ) ) {
-                       $titleObj = Title::newFromText( $params['title'] );
-                       if ( !$titleObj ) {
-                               $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
-                       }
-                       $pageObj = WikiPage::factory( $titleObj );
-                       $pageObj->loadPageData( 'fromdbmaster' );
-                       if ( !$pageObj->exists() ) {
-                               $this->dieUsageMsg( 'notanarticle' );
-                       }
-               } elseif ( isset( $params['pageid'] ) ) {
-                       $pageObj = WikiPage::newFromID( $params['pageid'] );
-                       if ( !$pageObj ) {
-                               $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
-                       }
-                       $titleObj = $pageObj->getTitle();
+               $titleObj = $this->getTitleOrPageId( $params );
+               $pageObj = WikiPage::factory( $titleObj );
+               $pageObj->loadPageData( 'fromdbmaster' );
+               if ( !$pageObj->exists() ) {
+                       $this->dieUsageMsg( 'notanarticle' );
                }
 
                $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
@@ -241,7 +228,7 @@ class ApiDelete extends ApiBase {
 
        public function getPossibleErrors() {
                return array_merge( parent::getPossibleErrors(),
-                       $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ),
+                       $this->getTitleOrPageIdErrorMessage(),
                        array(
                                array( 'invalidtitle', 'title' ),
                                array( 'nosuchpageid', 'pageid' ),
index 133ca9c..970a4f7 100644 (file)
@@ -48,18 +48,9 @@ class ApiEditPage extends ApiBase {
                        $this->dieUsageMsg( 'missingtext' );
                }
 
-               $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
-
-               if ( isset( $params['title'] ) ) {
-                       $titleObj = Title::newFromText( $params['title'] );
-                       if ( !$titleObj || $titleObj->isExternal() ) {
-                               $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
-                       }
-               } elseif ( isset( $params['pageid'] ) ) {
-                       $titleObj = Title::newFromID( $params['pageid'] );
-                       if ( !$titleObj ) {
-                               $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
-                       }
+               $titleObj = $this->getTitleOrPageId( $params );
+               if ( $titleObj->isExternal() ) {
+                       $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                }
 
                $apiResult = $this->getResult();
@@ -295,9 +286,6 @@ class ApiEditPage extends ApiBase {
                        case EditPage::AS_SPAM_ERROR:
                                $this->dieUsageMsg( array( 'spamdetected', $result['spam'] ) );
 
-                       case EditPage::AS_FILTERING:
-                               $this->dieUsageMsg( 'filtered' );
-
                        case EditPage::AS_BLOCKED_PAGE_FOR_USER:
                                $this->dieUsageMsg( 'blockedtext' );
 
@@ -385,7 +373,7 @@ class ApiEditPage extends ApiBase {
                global $wgMaxArticleSize;
 
                return array_merge( parent::getPossibleErrors(),
-                       $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ),
+                       $this->getTitleOrPageIdErrorMessage(),
                        array(
                                array( 'nosuchpageid', 'pageid' ),
                                array( 'missingtext' ),
@@ -402,7 +390,6 @@ class ApiEditPage extends ApiBase {
                                array( 'noimageredirect-logged' ),
                                array( 'spamdetected', 'spam' ),
                                array( 'summaryrequired' ),
-                               array( 'filtered' ),
                                array( 'blockedtext' ),
                                array( 'contenttoobig', $wgMaxArticleSize ),
                                array( 'noedit-anon' ),
@@ -488,14 +475,14 @@ class ApiEditPage extends ApiBase {
                        'sectiontitle' => 'The title for a new section',
                        'text' => 'Page content',
                        'token' => array( 'Edit token. You can get one of these through prop=info.',
-                                               'The token should always be sent as the last parameter, or at least, after the text parameter'
+                                               "The token should always be sent as the last parameter, or at least, after the {$p}text parameter"
                        ),
-                       'summary' => 'Edit summary. Also section title when section=new',
+                       'summary' => "Edit summary. Also section title when {$p}section=new and {$p}sectiontitle is not set",
                        'minor' => 'Minor edit',
                        'notminor' => 'Non-minor edit',
                        'bot' => 'Mark this edit as bot',
                        'basetimestamp' => array( 'Timestamp of the base revision (obtained through prop=revisions&rvprop=timestamp).',
-                                               'Used to detect edit conflicts; leave unset to ignore conflicts.'
+                                               'Used to detect edit conflicts; leave unset to ignore conflicts'
                        ),
                        'starttimestamp' => array( 'Timestamp when you obtained the edit token.',
                                                'Used to detect edit conflicts; leave unset to ignore conflicts'
@@ -509,7 +496,8 @@ class ApiEditPage extends ApiBase {
                        'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.",
                                        'If set, the edit won\'t be done unless the hash is correct' ),
                        'prependtext' => "Add this text to the beginning of the page. Overrides {$p}text",
-                       'appendtext' => "Add this text to the end of the page. Overrides {$p}text",
+                       'appendtext' => array( "Add this text to the end of the page. Overrides {$p}text.",
+                                               "Use {$p}section=new to append a new section" ),
                        'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext",
                        'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision',
                        'redirect' => 'Automatically resolve redirects',
index d9eed60..0032bd8 100644 (file)
@@ -55,7 +55,7 @@ class ApiEmailUser extends ApiBase {
                        'Subject' => $params['subject'],
                        'CCMe' => $params['ccme'],
                );
-               $retval = SpecialEmailUser::submit( $data );
+               $retval = SpecialEmailUser::submit( $data, $this->getContext() );
 
                if ( $retval instanceof Status ) {
                        // SpecialEmailUser sometimes returns a status
index bb079dd..b85f0ff 100644 (file)
@@ -328,6 +328,9 @@ class ApiParse extends ApiBase {
                        // Try the parser cache first
                        // getParserOutput will save to Parser cache if able
                        $pout = $page->getParserOutput( $popts );
+                       if ( !$pout ) {
+                               $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
+                       }
                        if ( $getWikitext ) {
                 $this->content = $page->getContent( Revision::RAW ); #FIXME: use $this->content everywhere
                                $this->text = ContentHandler::getContentText( $this->content ); #FIXME: serialize, get format from params; or use object structure in result?
index ec7b560..57bf111 100644 (file)
@@ -37,19 +37,8 @@ class ApiProtect extends ApiBase {
                global $wgRestrictionLevels;
                $params = $this->extractRequestParams();
 
-               $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
-
-               if ( isset( $params['title'] ) ) {
-                       $titleObj = Title::newFromText( $params['title'] );
-                       if ( !$titleObj ) {
-                               $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
-                       }
-               } elseif ( isset( $params['pageid'] ) ) {
-                       $titleObj = Title::newFromID( $params['pageid'] );
-                       if ( !$titleObj ) {
-                               $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
-                       }
-               }
+               $titleObj = $this->getTitleOrPageId( $params );
+               $pageObj = WikiPage::factory( $titleObj );
 
                $errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() );
                if ( $errors ) {
@@ -115,7 +104,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() ) {
@@ -202,7 +190,7 @@ class ApiProtect extends ApiBase {
 
        public function getPossibleErrors() {
                return array_merge( parent::getPossibleErrors(),
-                       $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ),
+                       $this->getTitleOrPageIdErrorMessage(),
                        array(
                                array( 'invalidtitle', 'title' ),
                                array( 'nosuchpageid', 'pageid' ),
index ac112ef..e96676e 100644 (file)
@@ -34,6 +34,16 @@ class ApiQueryAllUsers extends ApiQueryBase {
                parent::__construct( $query, $moduleName, 'au' );
        }
 
+       /**
+        * This function converts the user name to a canonical form
+        * which is stored in the database.
+        * @param String $name
+        * @return String
+        */
+       private function getCanonicalUserName( $name ) {
+               return str_replace( '_', ' ', $name );
+       }
+
        public function execute() {
                $db = $this->getDB();
                $params = $this->extractRequestParams();
@@ -57,8 +67,8 @@ class ApiQueryAllUsers extends ApiQueryBase {
                $useIndex = true;
 
                $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
-               $from = is_null( $params['from'] ) ? null : $this->keyToTitle( $params['from'] );
-               $to = is_null( $params['to'] ) ? null : $this->keyToTitle( $params['to'] );
+               $from = is_null( $params['from'] ) ? null : $this->getCanonicalUserName( $params['from'] );
+               $to = is_null( $params['to'] ) ? null : $this->getCanonicalUserName( $params['to'] );
 
                # MySQL doesn't seem to use 'equality propagation' here, so like the
                # ActiveUsers special page, we have to use rc_user_text for some cases.
@@ -68,7 +78,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
 
                if ( !is_null( $params['prefix'] ) ) {
                        $this->addWhere( $userFieldToSort .
-                               $db->buildLike( $this->keyToTitle( $params['prefix'] ), $db->anyString() ) );
+                               $db->buildLike( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) );
                }
 
                if ( !is_null( $params['rights'] ) ) {
@@ -190,15 +200,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 +244,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 +269,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 +291,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 fb1237f..7f0b827 100644 (file)
@@ -54,22 +54,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
        private function run( $resultPageSet = null ) {
                $params = $this->extractRequestParams();
 
-               $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
-
-               if ( isset( $params['title'] ) ) {
-                       $categoryTitle = Title::newFromText( $params['title'] );
-
-                       if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY ) {
-                               $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' );
-                       }
-               } elseif( isset( $params['pageid'] ) ) {
-                       $categoryTitle = Title::newFromID( $params['pageid'] );
-
-                       if ( !$categoryTitle ) {
-                               $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
-                       } elseif ( $categoryTitle->getNamespace() != NS_CATEGORY ) {
-                               $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' );
-                       }
+               $categoryTitle = $this->getTitleOrPageId( $params );
+               if ( $categoryTitle->getNamespace() != NS_CATEGORY ) {
+                       $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' );
                }
 
                $prop = array_flip( $params['prop'] );
@@ -383,7 +370,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 
        public function getPossibleErrors() {
                return array_merge( parent::getPossibleErrors(),
-                       $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ),
+                       $this->getTitleOrPageIdErrorMessage(),
                        array(
                                array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ),
                                array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
index 5040c70..e58a1ca 100644 (file)
@@ -173,7 +173,7 @@ class ApiUpload extends ApiBase {
         */
        private function getChunkResult(){
                $result = array();
-               
+
                $result['result'] = 'Continue';
                $request = $this->getMain()->getRequest();
                $chunkPath = $request->getFileTempname( 'chunk' );
@@ -185,7 +185,7 @@ class ApiUpload extends ApiBase {
                                                                                $this->mParams['offset']);
                        if ( !$status->isGood() ) {
                                $this->dieUsage( $status->getWikiText(), 'stashfailed' );
-                               return ;
+                               return array();
                        }
 
                        // Check we added the last chunk: 
@@ -194,7 +194,7 @@ class ApiUpload extends ApiBase {
 
                                if ( !$status->isGood() ) {
                                        $this->dieUsage( $status->getWikiText(), 'stashfailed' );
-                                       return ;
+                                       return array();
                                }
 
                                // We have a new filekey for the fully concatenated file.
index 2cc1061..c27ab00 100644 (file)
@@ -48,7 +48,7 @@ class GenderCache {
                        $username = $username->getName();
                }
 
-               $username = strtr( $username, '_', ' ' );
+               $username = self::normalizeUsername( $username );
                if ( !isset( $this->cache[$username] ) ) {
 
                        if ( $this->misses >= $this->missLimit && $wgUser->getName() !== $username ) {
@@ -60,11 +60,7 @@ class GenderCache {
 
                        } else {
                                $this->misses++;
-                               if ( !User::isValidUserName( $username ) ) {
-                                       $this->cache[$username] = $this->getDefault();
-                               } else {
-                                       $this->doQuery( $username, $caller );
-                               }
+                               $this->doQuery( $username, $caller );
                        }
 
                }
@@ -86,7 +82,6 @@ class GenderCache {
                foreach ( $data as $ns => $pagenames ) {
                        if ( !MWNamespace::hasGenderDistinction( $ns ) ) continue;
                        foreach ( array_keys( $pagenames ) as $username ) {
-                               if ( isset( $this->cache[$username] ) ) continue;
                                $users[$username] = true;
                        }
                }
@@ -102,26 +97,28 @@ class GenderCache {
        public function doQuery( $users, $caller = '' ) {
                $default = $this->getDefault();
 
-               foreach ( (array) $users as $index => $value ) {
-                       $name = strtr( $value, '_', ' ' );
-                       if ( isset( $this->cache[$name] ) ) {
-                               // Skip users whose gender setting we already know
-                               unset( $users[$index] );
-                       } else {
-                               $users[$index] = $name;
+               $usersToCheck = array();
+               foreach ( (array) $users as $value ) {
+                       $name = self::normalizeUsername( $value );
+                       // Skip users whose gender setting we already know
+                       if ( !isset( $this->cache[$name] ) ) {
                                // For existing users, this value will be overwritten by the correct value
                                $this->cache[$name] = $default;
+                               // query only for valid names, which can be in the database
+                               if( User::isValidUserName( $name ) ) {
+                                       $usersToCheck[] = $name;
+                               }
                        }
                }
 
-               if ( count( $users ) === 0 ) {
+               if ( count( $usersToCheck ) === 0 ) {
                        return;
                }
 
                $dbr = wfGetDB( DB_SLAVE );
                $table = array( 'user', 'user_properties' );
                $fields = array( 'user_name', 'up_value' );
-               $conds = array( 'user_name' => $users );
+               $conds = array( 'user_name' => $usersToCheck );
                $joins = array( 'user_properties' =>
                        array( 'LEFT JOIN', array( 'user_id = up_user', 'up_property' => 'gender' ) ) );
 
@@ -129,11 +126,20 @@ class GenderCache {
                if ( strval( $caller ) !== '' ) {
                        $comment .= "/$caller";
                }
-               $res = $dbr->select( $table, $fields, $conds, $comment, $joins, $joins );
+               $res = $dbr->select( $table, $fields, $conds, $comment, array(), $joins );
 
                foreach ( $res as $row ) {
                        $this->cache[$row->user_name] = $row->up_value ? $row->up_value : $default;
                }
        }
 
+       private static function normalizeUsername( $username ) {
+               // Strip off subpages
+               $indexSlash = strpos( $username, '/' );
+               if ( $indexSlash !== false ) {
+                       $username = substr( $username, 0, $indexSlash );
+               }
+               // normalize underscore/spaces
+               return strtr( $username, '_', ' ' );
+       }
 }
index 29fde03..ab33dcf 100644 (file)
@@ -195,7 +195,7 @@ class LinkBatch {
                }
 
                $genderCache = GenderCache::singleton();
-               $genderCache->dolinkBatch( $this->data, $this->caller );
+               $genderCache->doLinkBatch( $this->data, $this->caller );
                return true;
        }
 
index bd0895c..4e43642 100644 (file)
@@ -20,6 +20,7 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  * http://www.gnu.org/copyleft/gpl.html
  *
+ * @file
  * @ingroup Database
  */
 
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 10905f2..ed73522 100644 (file)
@@ -1,4 +1,24 @@
 <?php
+/**
+ * Debug toolbar related code
+ *
+ * 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
+ */
 
 /**
  * New debugger system that outputs a toolbar on page view
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..9839fa1 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 );
        }
 
        /**
@@ -501,6 +534,14 @@ class FSFileBackend extends FileBackendStore {
                return $tmpFile;
        }
 
+       /**
+        * @see FileBackendStore::directoriesAreVirtual()
+        * @return bool
+        */
+       protected function directoriesAreVirtual() {
+               return false;
+       }
+
        /**
         * Chmod a file, suppressing the warnings
         *
@@ -543,53 +584,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 +651,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 +666,7 @@ class FSFileBackendFileList implements Iterator {
        public function next() {
                try {
                        $this->iter->next();
+                       $this->filterViaNext();
                } catch ( UnexpectedValueException $e ) {
                        $this->iter = null;
                }
@@ -619,6 +681,7 @@ class FSFileBackendFileList implements Iterator {
                $this->pos = 0;
                try {
                        $this->iter->rewind();
+                       $this->filterViaNext();
                } catch ( UnexpectedValueException $e ) {
                        $this->iter = null;
                }
@@ -631,4 +694,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..544c9c2 100644 (file)
@@ -176,8 +176,9 @@ abstract class FileBackend {
         *                         contents as the new contents to be written there.
         *
         * $opts is an associative of boolean flags, including:
-        * 'force'               : Errors that would normally cause a rollback do not.
-        *                         The remaining operations are still attempted if any fail.
+        * 'force'               : Operation precondition errors no longer trigger an abort.
+        *                         Any remaining operations are still attempted. Unexpected
+        *                         failures may still cause remaning operations to be aborted.
         * 'nonLocking'          : No locks are acquired for the operations.
         *                         This can increase performance for non-critical writes.
         *                         This has no effect unless the 'force' flag is set.
@@ -315,8 +316,8 @@ abstract class FileBackend {
         * otherwise safe from modification from other processes. Normally,
         * the file will be a new temp file, which should be adequate.
         * $params include:
-        *     srcs          : ordered source storage paths (e.g. chunk1, chunk2, ...)
-        *     dst           : file system path to 0-byte temp file
+        *     srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
+        *     dst  : file system path to 0-byte temp file
         *
         * @param $params Array Operation parameters
         * @return Status
@@ -383,7 +384,8 @@ abstract class FileBackend {
         * is that of an empty container, in which case it should be deleted.
         *
         * $params include:
-        *     dir : storage directory
+        *     dir       : storage directory
+        *     recursive : recursively delete empty subdirectories first (@since 1.20)
         *
         * @param $params Array
         * @return Status
@@ -546,22 +548,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 dirs 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 (@since 1.20)
         *
         * @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 +777,7 @@ abstract class FileBackend {
         *
         * @param $path string
         * @return bool
+        * @since 1.20
         */
        final public static function isPathTraversalFree( $path ) {
                return ( self::normalizeContainerPath( $path ) !== null );
index 73815cf..d705f76 100644 (file)
@@ -21,7 +21,6 @@ class FileBackendGroup {
        protected $backends = array();
 
        protected function __construct() {}
-       protected function __clone() {}
 
        /**
         * @return FileBackendGroup
index d68b975..208d34b 100644 (file)
@@ -313,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
@@ -401,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 6bbdb0a..fab62e5 100644 (file)
  * @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
+       protected $maxCacheSize = 300; // integer; max paths with entries
        /** @var Array Map of paths to large (RAM/disk) cache items */
        protected $expensiveCache = array(); // (storage path => key => value)
-       protected $maxExpensiveCacheSize = 10; // integer; max paths with entries
+       protected $maxExpensiveCacheSize = 5; // integer; max paths with entries
 
        /** @var Array Map of container names to sharding settings */
        protected $shardViaHashLevels = array(); // (container name => config array)
 
        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.
@@ -73,6 +86,7 @@ abstract class FileBackendStore extends FileBackend {
                } else {
                        $status = $this->doCreateInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -104,6 +118,7 @@ abstract class FileBackendStore extends FileBackend {
                } else {
                        $status = $this->doStoreInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -132,6 +147,7 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doCopyInternal( $params );
                $this->clearCache( array( $params['dst'] ) );
+               $this->deleteFileCache( $params['dst'] ); // persistent cache
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -158,6 +174,7 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doDeleteInternal( $params );
                $this->clearCache( array( $params['src'] ) );
+               $this->deleteFileCache( $params['src'] ); // persistent cache
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -185,6 +202,8 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doMoveInternal( $params );
                $this->clearCache( array( $params['src'], $params['dst'] ) );
+               $this->deleteFileCache( $params['src'] ); // persistent cache
+               $this->deleteFileCache( $params['dst'] ); // persistent cache
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -371,6 +390,17 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
+               // Recursive: first delete all empty subdirs recursively
+               if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
+                       $subDirsRel = $this->getTopDirectoryList( array( 'dir' => $params['dir'] ) );
+                       if ( $subDirsRel !== null ) { // no errors
+                               foreach ( $subDirsRel as $subDirRel ) {
+                                       $subDir = $params['dir'] . "/{$subDirRel}"; // full path
+                                       $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
+                               }
+                       }
+               }
+
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
@@ -390,11 +420,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
                        }
                }
 
@@ -463,7 +495,10 @@ abstract class FileBackendStore extends FileBackend {
                        wfProfileOut( __METHOD__ );
                        return false; // invalid storage path
                }
-               $latest = !empty( $params['latest'] );
+               $latest = !empty( $params['latest'] ); // use latest data?
+               if ( !isset( $this->cache[$path]['stat'] ) ) {
+                       $this->primeFileCache( array( $path ) ); // check persistent cache
+               }
                if ( isset( $this->cache[$path]['stat'] ) ) {
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
@@ -480,9 +515,10 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
                if ( is_array( $stat ) ) { // don't cache negatives
+                       $stat['latest'] = $latest;
                        $this->trimCache(); // limit memory
                        $this->cache[$path]['stat'] = $stat;
-                       $this->cache[$path]['stat']['latest'] = $latest;
+                       $this->setFileCache( $path, $stat ); // update persistent cache
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -646,8 +682,79 @@ abstract class FileBackendStore extends FileBackend {
        }
 
        /**
-        * @copydoc FileBackend::getFileList()
-        * @return Array|null|Traversable
+        * @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 Traversable|Array|null Returns null on failure
+        */
+       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 Returns null on failure
+        */
+       abstract public function getDirectoryListInternal( $container, $dir, array $params );
+
+       /**
+        * @see FileBackend::getFileList()
+        * @return Traversable|Array|null Returns null on failure
         */
        final public function getFileList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
@@ -661,7 +768,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 );
                }
        }
@@ -674,7 +781,7 @@ abstract class FileBackendStore extends FileBackend {
         * @param $container string Resolved container name
         * @param $dir string Resolved path relative to container
         * @param $params Array
-        * @return Traversable|Array|null
+        * @return Traversable|Array|null Returns null on failure
         */
        abstract public function getFileListInternal( $container, $dir, array $params );
 
@@ -775,9 +882,13 @@ 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 file and container caches
+               $this->primeFileCache( $performOps );
+               $this->primeContainerCache( $performOps );
+
                // Actually attempt the operation batch...
                $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
 
@@ -820,10 +931,20 @@ abstract class FileBackendStore extends FileBackend {
         */
        protected function doClearCache( array $paths = null ) {}
 
+       /**
+        * Is this a key/value store where directories are just virtual?
+        * Virtual directories exists in so much as files exists that are
+        * prefixed with the directory path followed by a forward slash.
+        *
+        * @return bool
+        */
+       abstract protected function directoriesAreVirtual();
+
        /**
         * Move a cache entry to the top (such as when accessed)
         *
         * @param $path string Storage path
+        * @return void
         */
        protected function pingCache( $path ) {
                if ( isset( $this->cache[$path] ) ) {
@@ -849,6 +970,7 @@ abstract class FileBackendStore extends FileBackend {
         * Move a cache entry to the top (such as when accessed)
         *
         * @param $path string Storage path
+        * @return void
         */
        protected function pingExpensiveCache( $path ) {
                if ( isset( $this->expensiveCache[$path] ) ) {
@@ -977,6 +1099,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
@@ -1056,29 +1191,198 @@ 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, 14*86400 );
+       }
+
+       /**
+        * Delete the cached info for a container
+        *
+        * @param $container Resolved container name
+        * @return void
+        */
+       final protected function deleteContainerCache( $container ) {
+               for ( $attempts=1; $attempts <= 3; $attempts++ ) {
+                       if ( $this->memCache->delete( $this->containerCacheKey( $container ) ) ) {
+                               return; // done!
+                       }
+               }
+               trigger_error( "Unable to delete stat cache for container $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
+        * @return void
+        */
+       final protected function primeContainerCache( array $items ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
+               $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' ) );
+               wfProfileOut( __METHOD__ . '-' . $this->name );
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * 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 ) {}
+
+       /**
+        * Get the cache key for a file path
+        *
+        * @param $path Storage path
+        * @return string
+        */
+       private function fileCacheKey( $path ) {
+               return wfMemcKey( 'backend', $this->getName(), 'file', sha1( $path ) );
+       }
+
+       /**
+        * Set the cached stat info for a file path
+        *
+        * @param $path Storage path
+        * @param $val mixed Information to cache
+        * @return void
+        */
+       final protected function setFileCache( $path, $val ) {
+               $this->memCache->set( $this->fileCacheKey( $path ), $val, 7*86400 );
+       }
+
+       /**
+        * Delete the cached stat info for a file path
+        *
+        * @param $path Storage path
+        * @return void
+        */
+       final protected function deleteFileCache( $path ) {
+               for ( $attempts=1; $attempts <= 3; $attempts++ ) {
+                       if ( $this->memCache->delete( $this->fileCacheKey( $path ) ) ) {
+                               return; // done!
+                       }
+               }
+               trigger_error( "Unable to delete stat cache for file $path." );
+       }
+
+       /**
+        * Do a batch lookup from cache for file stats for all paths
+        * used in a list of storage paths or FileOp objects.
+        *
+        * @param $items Array List of storage paths or FileOps
+        * @return void
+        */
+       final protected function primeFileCache( array $items ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
+               $paths = array(); // list of storage paths
+               $pathNames = array(); // (cache key => storage path)
+               // 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;
+                       }
+               }
+               // Get all the corresponding cache keys for paths...
+               foreach ( $paths as $path ) {
+                       list( $cont, $rel, $s ) = $this->resolveStoragePath( $path );
+                       if ( $rel !== null ) { // valid path for this backend
+                               $pathNames[$this->fileCacheKey( $path )] = $path;
+                       }
+               }
+               // Get all cache entries for these container cache keys...
+               $values = $this->memCache->getBatch( array_keys( $pathNames ) );
+               foreach ( $values as $cacheKey => $val ) {
+                       if ( is_array( $val ) ) {
+                               $this->trimCache(); // limit memory
+                               $this->cache[$pathNames[$cacheKey]]['stat'] = $val;
+                       }
+               }
+               wfProfileOut( __METHOD__ . '-' . $this->name );
+               wfProfileOut( __METHOD__ );
+       }
 }
 
 /**
- * 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
@@ -1127,6 +1431,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();
        }
@@ -1139,6 +1445,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();
        }
@@ -1148,7 +1456,7 @@ class FileBackendStoreShardListIterator implements Iterator {
         * @return bool
         */
        public function valid() {
-               if ( $this->iter == null ) {
+               if ( $this->iter === null ) {
                        return false; // some failure?
                } elseif ( is_array( $this->iter ) ) {
                        return ( current( $this->iter ) !== false ); // no paths can have this value
@@ -1157,6 +1465,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.
@@ -1176,7 +1503,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..41d30dd 100644 (file)
@@ -24,7 +24,7 @@ class SwiftFileBackend extends FileBackendStore {
        protected $auth; // Swift authentication handler
        protected $authTTL; // integer seconds
        protected $swiftAnonUser; // string; username to handle unauthenticated requests
-       protected $maxContCacheSize = 100; // integer; max containers with entries
+       protected $maxContCacheSize = 300; // integer; max containers with entries
 
        /** @var CF_Connection */
        protected $conn; // Swift connection handle
@@ -57,13 +57,15 @@ class SwiftFileBackend extends FileBackendStore {
                // Optional settings
                $this->authTTL = isset( $config['swiftAuthTTL'] )
                        ? $config['swiftAuthTTL']
-                       : 120; // some sane number
+                       : 5 * 60; // some sane number
                $this->swiftAnonUser = isset( $config['swiftAnonUser'] )
                        ? $config['swiftAnonUser']
                        : '';
                $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 getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
+       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, 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?
@@ -664,6 +772,14 @@ class SwiftFileBackend extends FileBackendStore {
                return $tmpFile;
        }
 
+       /**
+        * @see FileBackendStore::directoriesAreVirtual()
+        * @return bool
+        */
+       protected function directoriesAreVirtual() {
+               return true;
+       }
+
        /**
         * Get headers to send to Swift when reading a file based
         * on a FileBackend params array, e.g. that of getLocalCopy().
@@ -750,23 +866,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 +899,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 +913,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 +921,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 +963,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 +989,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 +1003,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 +1025,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 +1038,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
        }
 
        /**
@@ -906,6 +1048,64 @@ class SwiftFileBackendFileList implements Iterator {
         * @return bool
         */
        public function valid() {
-               return ( current( $this->bufferIter ) !== false ); // no paths can have this value
+               if ( $this->bufferIter === null ) {
+                       return false; // some failure?
+               } else {
+                       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 Returns null on failure
+        */
+       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|null
+        */
+       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|null
+        */
+       protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+               return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
        }
 }
index 506d850..9f29eb3 100644 (file)
@@ -139,8 +139,6 @@ class ScopedLock {
                $this->type = $type;
        }
 
-       protected function __clone() {}
-
        /**
         * Get a ScopedLock object representing a lock on resource paths.
         * Any locks are released once this object goes out of scope.
index 5e53366..8465d58 100644 (file)
@@ -17,8 +17,6 @@ class LockManagerGroup {
        protected $managers = array();
 
        protected function __construct() {}
-       protected function __clone() {}
-
        /**
         * @return LockManagerGroup
         */
index 9b0844b..05e1eb8 100644 (file)
@@ -143,7 +143,7 @@ class ArchivedFile {
                                array( 'ORDER BY' => 'fa_timestamp DESC' ) );
                        if ( $res == false || $dbr->numRows( $res ) == 0 ) {
                        // this revision does not exist?
-                               return;
+                               return null;
                        }
                        $ret = $dbr->resultObject( $res );
                        $row = $ret->fetchObject();
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 fee4b2a..923b994 100644 (file)
@@ -230,6 +230,7 @@ abstract class DatabaseUpdater {
         * @since 1.20
         *
         * @param $tableName string
+        * @return bool
         */
        public function tableExists( $tableName ) {
                return ( $this->db->tableExists( $tableName, __METHOD__ ) );
index 71469ab..cdb66c4 100644 (file)
@@ -56,8 +56,7 @@ abstract class BagOStuff {
        /**
         * Get an item with the given key. Returns false if it does not exist.
         * @param $key string
-        *
-        * @return bool|Object
+        * @return mixed Returns false on failure
         */
        abstract public function get( $key );
 
@@ -65,7 +64,6 @@ abstract class BagOStuff {
         * Get an associative array containing the item for each of the given keys.
         * Each item will be false if it does not exist.
         * @param $keys Array List of strings
-        *
         * @return Array
         */
        public function getBatch( array $keys ) {
@@ -81,6 +79,7 @@ abstract class BagOStuff {
         * @param $key string
         * @param $value mixed
         * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+        * @return bool success
         */
        abstract public function set( $key, $value, $exptime = 0 );
 
@@ -88,19 +87,33 @@ abstract class BagOStuff {
         * Delete an item.
         * @param $key string
         * @param $time int Amount of time to delay the operation (mostly memcached-specific)
+        * @return bool success
         */
        abstract public function delete( $key, $time = 0 );
 
+       /**
+        * @param $key string
+        * @param $timeout integer
+        * @return bool success
+        */
        public function lock( $key, $timeout = 0 ) {
                /* stub */
                return true;
        }
 
+       /**
+        * @param $key string
+        * @return bool success
+        */
        public function unlock( $key ) {
                /* stub */
                return true;
        }
 
+       /**
+        * @todo: what is this?
+        * @return Array
+        */
        public function keys() {
                /* stub */
                return array();
@@ -122,24 +135,36 @@ abstract class BagOStuff {
 
        /* *** Emulated functions *** */
 
+       /**
+        * @param $key string
+        * @param $value mixed
+        * @param $exptime integer
+        * @return bool success
+        */
        public function add( $key, $value, $exptime = 0 ) {
                if ( !$this->get( $key ) ) {
-                       $this->set( $key, $value, $exptime );
-
-                       return true;
+                       return $this->set( $key, $value, $exptime );
                }
+               return true;
        }
 
+       /**
+        * @param $key string
+        * @param $value mixed
+        * @return bool success
+        */
        public function replace( $key, $value, $exptime = 0 ) {
                if ( $this->get( $key ) !== false ) {
-                       $this->set( $key, $value, $exptime );
+                       return $this->set( $key, $value, $exptime );
                }
+               return true;
        }
 
        /**
         * @param $key String: Key to increase
         * @param $value Integer: Value to add to $key (Default 1)
         * @return null if lock is not possible else $key value increased by $value
+        * @return success
         */
        public function incr( $key, $value = 1 ) {
                if ( !$this->lock( $key ) ) {
@@ -157,10 +182,18 @@ abstract class BagOStuff {
                return $n;
        }
 
+       /**
+        * @param $key String
+        * @param $value Integer
+        * @return bool success
+        */
        public function decr( $key, $value = 1 ) {
                return $this->incr( $key, - $value );
        }
 
+       /**
+        * @param $text string
+        */
        public function debug( $text ) {
                if ( $this->debugMode ) {
                        $class = get_class( $this );
@@ -170,6 +203,7 @@ abstract class BagOStuff {
 
        /**
         * Convert an optionally relative time to an absolute time
+        * @param $exptime integer
         * @return int
         */
        protected function convertExpiry( $exptime ) {
@@ -180,5 +214,3 @@ abstract class BagOStuff {
                }
        }
 }
-
-
index 2815fee..1ddf3b9 100644 (file)
@@ -338,9 +338,10 @@ class MWMemcached {
                        $this->_debugprint( sprintf( "MemCache: delete %s (%s)\n", $key, $res ) );
                }
 
-               if ( $res == "DELETED" ) {
+               if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
                        return true;
                }
+
                return false;
        }
 
index f129f73..18bd01c 100644 (file)
@@ -958,10 +958,18 @@ class PPFrame_DOM implements PPFrame {
                }
 
                if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
+                       $this->parser->limitationWarn( 'node-count-exceeded',
+                               $this->parser->mPPNodeCount,
+                               $this->parser->mOptions->getMaxPPNodeCount()
+                       );
                        return '<span class="error">Node-count limit exceeded</span>';
                }
 
                if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
+                       $this->parser->limitationWarn( 'expansion-depth-exceeded',
+                               $expansionDepth,
+                               $this->parser->mOptions->getMaxPPExpandDepth()
+                       );
                        return '<span class="error">Expansion depth limit exceeded</span>';
                }
                wfProfileIn( __METHOD__ );
index 28283c7..4acb124 100644 (file)
@@ -915,9 +915,17 @@ class PPFrame_Hash implements PPFrame {
                }
 
                if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
+                       $this->parser->limitationWarn( 'node-count-exceeded',
+                                       $this->parser->mPPNodeCount,
+                                       $this->parser->mOptions->getMaxPPNodeCount()
+                       );
                        return '<span class="error">Node-count limit exceeded</span>';
                }
                if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
+                       $this->parser->limitationWarn( 'expansion-depth-exceeded',
+                                       $expansionDepth,
+                                       $this->parser->mOptions->getMaxPPExpandDepth()
+                       );
                        return '<span class="error">Expansion depth limit exceeded</span>';
                }
                ++$expansionDepth;
index aedcac2..a23168a 100644 (file)
@@ -1073,9 +1073,17 @@ class PPFrame_HipHop implements PPFrame {
                }
 
                if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
+                       $this->parser->limitationWarn( 'node-count-exceeded',
+                                       $this->parser->mPPNodeCount,
+                                       $this->parser->mOptions->getMaxPPNodeCount()
+                       );
                        return '<span class="error">Node-count limit exceeded</span>';
                }
                if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
+                       $this->parser->limitationWarn( 'expansion-depth-exceeded',
+                                       $expansionDepth,
+                                       $this->parser->mOptions->getMaxPPExpandDepth()
+                       );
                        return '<span class="error">Expansion depth limit exceeded</span>';
                }
                ++$expansionDepth;
index f884063..f2054b5 100644 (file)
@@ -1,12 +1,31 @@
 <?php
 /**
- * @defgroup Profiler Profiler
+ * Base class and functions for profiling.
+ *
+ * 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 Profiler
  * This file is only included if profiling is enabled
  */
 
+/**
+ * @defgroup Profiler Profiler
+ */
+
 /**
  * Begin profiling of a function
  * @param $functionname String: name of the function we will profile
index 1091495..82197fd 100644 (file)
@@ -1,5 +1,22 @@
 <?php
 /**
+ * Base class for simple profiling.
+ *
+ * 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 Profiler
  */
index 3621a41..3e7d6fa 100644 (file)
@@ -1,5 +1,22 @@
 <?php
 /**
+ * Profiler showing output in page source.
+ *
+ * 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 Profiler
  */
index 7602f06..f14139e 100644 (file)
@@ -1,5 +1,22 @@
 <?php
 /**
+ * Profiler showing execution trace.
+ *
+ * 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 Profiler
  */
index ae607aa..0de3a85 100644 (file)
@@ -1,5 +1,22 @@
 <?php
 /**
+ * Profiler sending messages over UDP.
+ *
+ * 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 Profiler
  */
index 1a0933c..a498860 100644 (file)
@@ -1,9 +1,31 @@
 <?php
 /**
- * Stub profiling functions
+ * Stub profiling functions.
+ *
+ * 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 Profiler
  */
+
+/**
+ * Stub profiler that does nothing
+ *
+ * @ingroup Profiler
+ */
 class ProfilerStub extends Profiler {
        public function isStub() {
                return true;
index 631ca64..6b7bffa 100644 (file)
@@ -70,8 +70,10 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
         */
        protected function getContent( $title ) {
                if ( $title->getNamespace() === NS_MEDIAWIKI ) {
-                       $message = wfMessage( $title->getDBkey() )->inContentLanguage();
-                       return $message->exists() ? $message->plain() : '';
+                       // The first "true" is to use the database, the second is to use the content langue
+                       // and the last one is to specify the message key already contains the language in it ("/de", etc.)
+                       $text = MessageCache::singleton()->get( $title->getDBkey(), true, true, true );
+                       return $text === false ? '' : $text;
                }
                if ( !$title->isCssJsSubpage() && !$title->isCssOrJsPage() ) {
                        return null;
index 9ca98e5..2c55980 100644 (file)
@@ -1,4 +1,26 @@
 <?php
+/**
+ * Base implementations for deletable items.
+ *
+ * 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 RevisionDelete
+ */
+
 /**
  * List for revision table items
  *
index e109e65..4f58099 100644 (file)
@@ -1,4 +1,25 @@
 <?php
+/**
+ * Interface definition for deletable items.
+ *
+ * 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 RevisionDelete
+ */
 
 /**
  * Abstract base class for a list of deletable items. The list class
index 0e965c8..c02e9c7 100644 (file)
@@ -1,9 +1,6 @@
 <?php
 /**
- * Backend functions for suppressing and unsuppressing all references to a given user,
- * used when blocking with HideUser enabled.  This was spun out of SpecialBlockip.php
- * in 1.18; at some point it needs to be rewritten to either use RevisionDelete abstraction,
- * or at least schema abstraction.
+ * Backend functions for suppressing and unsuppressing all references to a given user.
  *
  * 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
  * @file
  * @ingroup RevisionDelete
  */
+
+/**
+ * Backend functions for suppressing and unsuppressing all references to a given user,
+ * used when blocking with HideUser enabled.  This was spun out of SpecialBlockip.php
+ * in 1.18; at some point it needs to be rewritten to either use RevisionDelete abstraction,
+ * or at least schema abstraction.
+ *
+ * @ingroup RevisionDelete
+ */
 class RevisionDeleteUser {
 
        /**
index 8f85ae3..95d372c 100644 (file)
@@ -2,7 +2,23 @@
 /**
  * Revision/log/file deletion backend
  *
+ * 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 RevisionDelete
  */
 
 /**
index 0348ed1..678c530 100644 (file)
@@ -2,6 +2,21 @@
 /**
  * Basic search engine
  *
+ * 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 Search
  */
index a162d2b..40dd36c 100644 (file)
@@ -4,6 +4,21 @@
  *
  * See deferred.txt
  *
+ * 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 Search
  */
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 fa48317..b46ef0e 100644 (file)
@@ -3,10 +3,34 @@
  * @defgroup Watchlist Users watchlist handling
  */
 
+/**
+ * Implements Special:EditWatchlist
+ *
+ * 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 SpecialPage
+ * @ingroup Watchlist
+ */
+
 /**
  * Provides the UI through which users can perform editing
  * operations on their watchlist
  *
+ * @ingroup SpecialPage
  * @ingroup Watchlist
  * @author Rob Church <robchur@gmail.com>
  */
index 314da72..55b500d 100644 (file)
@@ -61,7 +61,8 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                        ),
                        'Subject' => array(
                                'type' => 'text',
-                               'default' => wfMsgExt( 'defemailsubject', array( 'content', 'parsemag' ), $this->getUser()->getName() ),
+                               'default' => $this->msg( 'defemailsubject',
+                                       $this->getUser()->getName() )->inContentLanguage()->text(),
                                'label-message' => 'emailsubject',
                                'maxlength' => 200,
                                'size' => 60,
@@ -124,11 +125,11 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                $this->mTargetObj = $ret;
 
                $form = new HTMLForm( $this->getFormFields(), $this->getContext() );
-               $form->addPreText( wfMsgExt( 'emailpagetext', 'parseinline' ) );
-               $form->setSubmitText( wfMsg( 'emailsend' ) );
+               $form->addPreText( $this->msg( 'emailpagetext' )->parse() );
+               $form->setSubmitTextMsg( 'emailsend' );
                $form->setTitle( $this->getTitle() );
-               $form->setSubmitCallback( array( __CLASS__, 'submit' ) );
-               $form->setWrapperLegend( wfMsgExt( 'email-legend', 'parsemag' ) );
+               $form->setSubmitCallback( array( __CLASS__, 'uiSubmit' ) );
+               $form->setWrapperLegendMsg( 'email-legend' );
                $form->loadData();
 
                if( !wfRunHooks( 'EmailUserForm', array( &$form ) ) ) {
@@ -224,14 +225,26 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                $string = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'askusername' ) ) .
                        Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
                        Xml::openElement( 'fieldset' ) .
-                       Html::rawElement( 'legend', null, wfMessage( 'emailtarget' )->parse() ) .
-                       Xml::inputLabel( wfMessage( 'emailusername' )->text(), 'target', 'emailusertarget', 30, $name ) . ' ' .
-                       Xml::submitButton( wfMessage( 'emailusernamesubmit' )->text() ) .
+                       Html::rawElement( 'legend', null, $this->msg( 'emailtarget' )->parse() ) .
+                       Xml::inputLabel( $this->msg( 'emailusername' )->text(), 'target', 'emailusertarget', 30, $name ) . ' ' .
+                       Xml::submitButton( $this->msg( 'emailusernamesubmit' )->text() ) .
                        Xml::closeElement( 'fieldset' ) .
                        Xml::closeElement( 'form' ) . "\n";
                return $string;
        }
 
+       /**
+        * Submit callback for an HTMLForm object, will simply call submit().
+        *
+        * @since 1.20
+        * @param $data array
+        * @param $form HTMLForm object
+        * @return Status|string|bool
+        */
+       public static function uiSubmit( array $data, HTMLForm $form ) {
+               return self::submit( $data, $form->getContext() );
+       }
+
        /**
         * Really send a mail. Permissions should have been checked using
         * getPermissionsError(). It is probably also a good
@@ -240,25 +253,22 @@ class SpecialEmailUser extends UnlistedSpecialPage {
         * @return Mixed: Status object, or potentially a String on error
         * or maybe even true on success if anything uses the EmailUser hook.
         */
-       public static function submit( $data ) {
+       public static function submit( array $data, IContextSource $context ) {
                global $wgUser, $wgUserEmailUseReplyTo;
 
                $target = self::getTarget( $data['Target'] );
                if( !$target instanceof User ) {
-                       return wfMsgExt( $target . 'text', 'parse' );
+                       return $context->msg( $target . 'text' )->parseAsBlock();
                }
                $to = new MailAddress( $target );
-               $from = new MailAddress( $wgUser );
+               $from = new MailAddress( $context->getUser() );
                $subject = $data['Subject'];
                $text = $data['Text'];
 
                // Add a standard footer and trim up trailing newlines
                $text = rtrim( $text ) . "\n\n-- \n";
-               $text .= wfMsgExt(
-                       'emailuserfooter',
-                       array( 'content', 'parsemag' ),
-                       array( $from->name, $to->name )
-               );
+               $text .= $context->msg( 'emailuserfooter',
+                       $from->name, $to->name )->inContentLanguage()->text();
 
                $error = '';
                if( !wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$text, &$error ) ) ) {
@@ -302,11 +312,8 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                        // unless they are emailing themselves, in which case one
                        // copy of the message is sufficient.
                        if ( $data['CCMe'] && $to != $from ) {
-                               $cc_subject = wfMsg(
-                                       'emailccsubject',
-                                       $target->getName(),
-                                       $subject
-                               );
+                               $cc_subject = $context->msg( 'emailccsubject' )->rawParams(
+                                       $target->getName(), $subject )->text();
                                wfRunHooks( 'EmailUserCC', array( &$from, &$from, &$cc_subject, &$text ) );
                                $ccStatus = UserMailer::send( $from, $from, $cc_subject, $text );
                                $status->merge( $ccStatus );
index f7e81ee..c217ecc 100644 (file)
@@ -1,7 +1,24 @@
 <?php
 /**
- * @ingroup SpecialPage
+ * Implements Special:JavaScriptTest
+ *
+ * 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 SpecialPage
  */
 
 /**
index f43b8f7..0aa8b30 100644 (file)
@@ -90,7 +90,8 @@ class SpecialMergeHistory extends SpecialPage {
                $this->outputHeader();
 
                if( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
-                       return $this->merge();
+                       $this->merge();
+                       return;
                }
 
                if ( !$this->mSubmitted ) {
index 3fa8687..a6d75bb 100644 (file)
@@ -174,7 +174,8 @@ class SpecialSearch extends SpecialPage {
                $t = Title::newFromText( $term );
                # If the string cannot be used to create a title
                if( is_null( $t ) ) {
-                       return $this->showResults( $term );
+                       $this->showResults( $term );
+                       return;
                }
                # If there's an exact or very near match, jump right there.
                $t = SearchEngine::getNearMatch( $term );
@@ -201,7 +202,7 @@ class SpecialSearch extends SpecialPage {
                                return;
                        }
                }
-               return $this->showResults( $term );
+               $this->showResults( $term );
        }
 
        /**
index 4ad31a0..fab21cd 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 /**
+ * Implements Special:Unblock
+ *
  * 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
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 7d48725..87825da 100644 (file)
@@ -801,7 +801,7 @@ $1ৰ দ্বাৰা এই অৱৰোধ কৰা হৈছে ।
 কোনো আসোঁৱাহপূৰ্ণ ৱেব-ভিত্তিক বেনামী প্ৰক্সী সেৱা ব্যৱহাৰ কৰিলে এনে হ’ব পাৰে ।",
 'edit_form_incomplete' => "'''এই সম্পাদনাৰ কিছু অংশ চাৰ্ভাৰলৈ নগ’ল; আপোনাৰ সম্পাদনা ঠিকে আছেনে পৰীক্ষা কৰি পুনৰ চেষ্টা কৰক ।'''",
 'editing' => '$1 সম্পাদনা',
-'creating' => 'সৃষ্টি কৰি থকা হৈছে $1',
+'creating' => '$1 পৃষ্ঠাখন আপুনি সৃষ্টি কৰি আছে',
 'editingsection' => '$1 (বিভাগ) সম্পাদনা কৰি থকা হৈছে',
 'editingcomment' => '$1 (নতুন বিভাগ) সম্পাদনা কৰি থকা হৈছে',
 'editconflict' => 'সম্পাদনা দ্বন্দ: $1',
@@ -870,6 +870,7 @@ $1ৰ দ্বাৰা এই অৱৰোধ কৰা হৈছে ।
 'edit-no-change' => 'আপোনাৰ সম্পাদনা আওকাণ কৰা হৈছে, কাৰণ লেখাত কোনো তফাৎ নাই',
 'edit-already-exists' => "নতুন পৃষ্ঠা সৃষ্টি কৰা নহ'ল ।
 পৃষ্ঠাখন ইতিমধ্যে আছেই ।",
+'defaultmessagetext' => 'সাধাৰণ বাৰ্তা পাঠ্য',
 
 # Parser/template warnings
 'expensive-parserfunction-warning' => "'''সতৰ্কবাণী:''' এই পৃষ্ঠাখনত অধিক এক্সপেনচিভ পাৰ্চাৰ ফাংচন কল আছে ।
@@ -986,7 +987,7 @@ $3 এ আগবঢ়োৱা ইয়াৰ কাৰণ হ’ল ''$2''",
 'revdelete-legend' => 'দৃষ্টিপাত সীমাবদ্ধ কৰক',
 'revdelete-hide-text' => 'সংশোধিত পাঠ গোপন কৰক',
 'revdelete-hide-image' => 'ফাইলৰ বিষয়বস্তু গোপন কৰক',
-'revdelete-hide-name' => 'কাৰ্য্য আৰু লক্ষ্য গোপন কৰক',
+'revdelete-hide-name' => 'কাৰ্য আৰু লক্ষ্য গোপন কৰক',
 'revdelete-hide-comment' => 'সম্পাদনা মন্তব্য আতৰাই থওক',
 'revdelete-hide-user' => 'সম্পাদকৰ সদস্যনাম/আই-পি ঠিকনা গোপন কৰক',
 'revdelete-hide-restricted' => 'প্ৰশাসকবৃন্দৰ লগতে আনৰ পৰাও তথ্য ৰোধ কৰক',
@@ -1007,7 +1008,7 @@ $1",
 'revdel-restore-deleted' => 'বাতিল কৰা সংশোধনসমূহ',
 'revdel-restore-visible' => 'দৃশ্যমান সংশোধনসমূহ',
 'pagehist' => 'পৃষ্ঠা ইতিহাস',
-'deletedhist' => 'মà¦\9aি à¦ªà§\87লà§\8bৱা ইতিহাস',
+'deletedhist' => 'বিলà§\8bপ à¦\95ৰাৰ ইতিহাস',
 'revdelete-hide-current' => ' $2, $1 তাৰিখৰ এই আইটেমটো গোপন কৰাত সমস্যা হৈছে: এইটো বৰ্তমানৰ সংশোধনী ।
 এইটোক গোপন কৰিব পৰা নাযাব ।',
 'revdelete-show-no-access' => '$2, $1 তাৰিখৰ এই আইটেমটো দেখুওৱাত সমস্যা হৈছে: এই আইটেমটো "সীমাবদ্ধ" হিছাপে চিহ্নিত ।
@@ -1073,7 +1074,7 @@ $1",
 'showhideselectedversions' => 'নিৰ্বাচিত সংশোধনসমূহ দেখুৱাওক/আঁৰ কৰক',
 'editundo' => 'পূৰ্ববত কৰক',
 'diff-multi' => '({{PLURAL:$2|এজন সদস্যৰ|$2জন সদস্যৰ}} দ্বাৰা {{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1-টা মধ্যৱৰ্তী সংশোধন}} দেখোৱা হোৱা নাই)',
-'diff-multi-manyusers' => '({{PLURAL:$2|à¦\8fà¦\9cনতà¦\95à§\88|$2-à¦\9cনতà¦\95à§\88}} à¦\85ধিà¦\95 à¦¸à¦¦à¦¸à§\8dযৰ à¦¦à§\8dবাৰা {{PLURAL:$1|à¦\8fà¦\9fা à¦®à¦§à§\8dযৱৰà§\8dতà§\80 à¦¸à¦\82শà§\8bধন|$1-à¦\9fা à¦®à¦§à§\8dযৱৰà§\8dতà§\80 à¦¸à¦\82শà§\8bধন}} à¦¦à§\87à¦\96à§\8bৱা হোৱা নাই)',
+'diff-multi-manyusers' => '({{PLURAL:$2|à¦\8fà¦\9cনতà¦\95à§\88|$2-à¦\9cনতà¦\95à§\88}} à¦\85ধিà¦\95 à¦¸à¦¦à¦¸à§\8dযৰ à¦¦à§\8dবাৰা {{PLURAL:$1|à¦\8fà¦\9fা à¦®à¦§à§\8dযৱৰà§\8dতà§\80 à¦¸à¦\82শà§\8bধন|$1-à¦\9fা à¦®à¦§à§\8dযৱৰà§\8dতà§\80 à¦¸à¦\82শà§\8bধন}} à¦¦à§\87à¦\96à§\81à¦\93ৱা হোৱা নাই)',
 
 # Search results
 'searchresults' => 'অনুসন্ধানৰ ফলাফল',
@@ -1591,7 +1592,7 @@ $1",
 'filename-bad-prefix' => "আপুনি আপলোড কৰা ফাইলৰ নামটো '''\"\$1\"''' দি আৰম্ভ হৈছে, যিটো ডিজিটেল কেমেৰাই স্বয়ংক্ৰিয়ভাৱে দিয়ে আৰু সি ব্যাখ্যামূলক নহয় ।
 অনুগ্ৰহ কৰি আপোনাৰ ফাইলটোৰ বাবে এটা ব্যাখ্যামূলক নাম বাছি লওক ।",
 'upload-success-subj' => "আপলোড সফল হ'ল",
-'upload-success-msg' => '[$2] à§° à¦ªà§°à¦¾ à¦\86পà§\8bনাৰ à¦\86পলà§\8bড à¦¸à¦«à¦² à¦¹à§\88à¦\9bà§\87 à¥¤ à¦\8fà¦\87à¦\9fà§\8b à¦\87য়াত à¦\89পলবà§\8dদà§\8dধ: [[:{{ns:file}}:$1]]',
+'upload-success-msg' => '[$2] ৰ পৰা আপোনাৰ আপলোড সফল হৈছে । এইটো ইয়াত উপলদ্ধ: [[:{{ns:file}}:$1]]',
 'upload-failure-subj' => 'আপল’ডত সমস্যা হৈছে',
 'upload-failure-msg' => '[$2] ৰ পৰা আপুনি কৰা আপল’ডত এটা সমস্যাই দেখা দিছে:
 
@@ -1980,6 +1981,9 @@ https://www.mediawiki.org/wiki/Manual:Image_Authorization চাওক।",
 'allpages-bad-ns' => '{{SITENAME}} ত কোনো "$1" নামস্থান নাই ।',
 'allpages-hide-redirects' => 'পুনঃনিৰ্দেশ লুকুৱাওক',
 
+# SpecialCachedPage
+'cachedspecial-refresh-now' => 'শেহতীয়া পাঠ্য',
+
 # Special:Categories
 'categories' => 'শ্ৰেণী',
 'categoriespagetext' => 'এই {{PLURAL:$1|বিষয়শ্ৰেণীত|বিষয়শ্ৰেণীসমূহত}}  পৃষ্ঠা বা মিডিয়া ফাইল আছে ।
@@ -3742,4 +3746,15 @@ You can also [[Special:EditWatchlist/raw|edit the raw list]].',
 '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 4414637..71aa803 100644 (file)
@@ -765,6 +765,7 @@ Les páxines personalizaes .css y .js usen un títulu en minúscules, p. ex. {{n
 'note' => "'''Nota:'''",
 'previewnote' => "'''Alcuerdate de qu'esto ye sólo una vista previa.'''
 ¡Los cambios entá nun se guardaron!",
+'continue-editing' => 'Siguir editando',
 'previewconflict' => "Esta vista previa amuesa'l testu del área d'edición d'arriba tal como apaecerá si escueyes guardar.",
 'session_fail_preview' => "'''¡Sentímoslo muncho! Nun se pudo procesar la to edición porque hebo una perda de datos de la sesión.
 Inténtalo otra vuelta. Si nun se t'arregla, intenta salir y volver a rexistrate.'''",
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 09d9778..a0b544a 100644 (file)
@@ -1410,7 +1410,7 @@ Stelle sicher, dass die Versionsgeschichte einer Seite historisch korrekt ist.',
 'datedefault' => 'Standard',
 'prefs-beta' => 'Beta-Funktionen',
 'prefs-datetime' => 'Datum und Zeit',
-'prefs-labs' => 'Alpha-Funktionen (experimentell)',
+'prefs-labs' => 'Alpha-Funktionen',
 'prefs-personal' => 'Benutzerdaten',
 'prefs-rc' => 'Letzte Änderungen',
 'prefs-watchlist' => 'Beobachtungsliste',
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 9a53e92..a21a7ac 100644 (file)
@@ -1479,6 +1479,10 @@ These arguments have been omitted.",
 'parser-template-loop-warning'            => 'Template loop detected: [[$1]]',
 'parser-template-recursion-depth-warning' => 'Template recursion depth limit exceeded ($1)',
 'language-converter-depth-warning'        => 'Language converter depth limit exceeded ($1)',
+'node-count-exceeded-category'            => 'Pages where node-count is exceeded',
+'node-count-exceeded-warning'             => 'Page exceeded the node-count',
+'expansion-depth-exceeded-category'       => 'Pages where expansion depth is exceeded',
+'expansion-depth-exceeded-warning'        => 'Page exceeded the expansion depth',
 
 # "Undo" feature
 'undo-success' => 'The edit can be undone.
index 7ac9383..9fceb11 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.
@@ -1096,7 +1098,7 @@ El registro de borrado y traslados para esta página están provistos aquí por
 El registro de borrados y traslados para la página están provistos debajo como referencia.',
 'log-fulllog' => 'Ver el registro completo',
 'edit-hook-aborted' => 'Edición cancelada por la extensión.
-No dió explicaciones.',
+No se aportaron explicaciones.',
 'edit-gone-missing' => 'No se pudo actualizar la página.
 Parece que ha sido borrada.',
 'edit-conflict' => 'Conflicto de edición.',
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 853c6ea..bb41921 100644 (file)
@@ -991,6 +991,7 @@ Stoga je uređivanje odbačeno da se spriječi uništavanje teksta stranice.
 To se ponekad događa kad rabite neispravan web-baziran anonimni posrednik (proxy).",
 'edit_form_incomplete' => "'''Neki dijelovi obrasca za uređivanja nisu dostigli do poslužitelja; provjerite jesu li izmjene netaknute i pokušajte ponovno.'''",
 'editing' => 'Uređujete $1',
+'creating' => 'Stvori $1',
 'editingsection' => 'Uređujete $1 (odlomak)',
 'editingcomment' => 'Uređujete $1 (novi odlomak)',
 'editconflict' => 'Istovremeno uređivanje: $1',
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..eeec5f1 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' => "'''申し訳ありません!セッションデータが消失したため編集を処理できませんでした。'''
 もう一度やり直してください。
@@ -1110,7 +1112,7 @@ IP アドレスは複数の利用者で共有されている場合がありま
 必要であれば文章をカットアンドペーストしてテキストファイルとして保存し、後ほど保存をやり直してください。
 
 データベースをロックした管理者による説明は以下の通りです:$1",
-'protectedpagewarning' => "'''è­¦å\91\8aï¼\9aã\81\93ã\81®ã\83\9aã\83¼ã\82¸ã\81¯ä¿\9dè­·ã\81\95ã\82\8cã\81¦ã\81\8aã\82\8a、管理者権限を持つ利用者のみが編集できます。'''
+'protectedpagewarning' => "'''è­¦å\91\8aï¼\9aã\81\93ã\81®ã\83\9aã\83¼ã\82¸ã\81¯ä¿\9dè­·ã\81\95ã\82\8cã\81¦ã\81\84ã\82\8bã\81\9fã\82\81、管理者権限を持つ利用者のみが編集できます。'''
 参考として以下に一番最後の記録を表示します:",
 'semiprotectedpagewarning' => "'''注意:'''このページは保護されているため、登録利用者のみが編集できます。
 参考として以下に一番最後の記録を表示します:",
@@ -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 0577803..583efb8 100644 (file)
@@ -739,7 +739,7 @@ Google hmangin i lo zawng hrih thei ang.
 'prefs-personal' => 'Hmangtu chanchin tawi',
 'prefs-rc' => 'Tihdanglam thar',
 'prefs-watchlist' => 'Ralvèn',
-'prefs-watchlist-days' => 'Ralvèna ni tihlang tùr chin:',
+'prefs-watchlist-days' => 'Ralvèna ni tihlan tùr chin:',
 'prefs-watchlist-days-max' => 'A rei berah ni $1 {{PLURAL:$1||}}',
 'prefs-watchlist-edits' => 'Ralvèn pawhseia tihdanglam zât tihlan tùr tam ber:',
 'prefs-watchlist-edits-max' => 'A tam ber: 1000',
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 b887d73..7a88814 100644 (file)
@@ -645,6 +645,7 @@ Informatz-ne un [[Special:ListUsers/sysop|administrator]] aprèp aver notada l
 'badarticleerror' => 'Aquesta accion pòt pas èsser efectuada sus aquesta pagina.',
 'cannotdelete' => 'Impossible de suprimir la pagina o lo fichièr « $1 ».
 Benlèu la supression ja es estada efectuada per qualqu’un mai.',
+'cannotdelete-title' => 'Impossible de suprimir la pagina "$1"',
 'badtitle' => 'Títol marrit',
 'badtitletext' => 'Lo títol de la pagina demandada es invalid, void o s’agís d’un títol interlenga o interprojècte mal ligat. Benlèu conten un o maites caractèrs que pòdon pas èsser utilizats dins los títols.',
 'perfcached' => 'Aquò es una version en amagatal e es benlèu pas a jorn. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.',
@@ -654,6 +655,7 @@ Benlèu la supression ja es estada efectuada per qualqu’un mai.',
 Foncion : $1<br />
 Requèsta : $2',
 'viewsource' => 'Vejatz lo tèxte font',
+'viewsource-title' => 'Veire la font de $1',
 'actionthrottled' => 'Accion limitada',
 'actionthrottledtext' => "Per luchar contra lo spam, l’utilizacion d'aquesta accion es limitada a un cèrt nombre de còps dins una sosta pro corta. S'avèra qu'avètz depassat aqueste limit. Ensajatz tornamai dins qualques minutas.",
 'protectedpagetext' => 'Aquesta pagina es estada protegida per empachar sa modificacion.',
@@ -913,6 +915,7 @@ La darrièra entrada del jornal dels blocatges es indicada çaijós a títol d
 'updated' => '(Mes a jorn)',
 'note' => "'''Nòta :'''",
 'previewnote' => "'''Atencion, aqueste tèxte es sonque una previsualizacion e es pas encara estat salvat !'''",
+'continue-editing' => "Contunhar l'edicion",
 'previewconflict' => 'Aquesta previsualizacion fa veire lo tèxte de la bóstia de modificacion superiora coma apareisserà se causissètz de lo salvar.',
 'session_fail_preview' => "'''Podèm pas enregistrar vòstra modificacion a causa d’una pèrda d’informacions concernent vòstra sesilha.
 Ensajatz tornarmai.
@@ -925,6 +928,7 @@ S'aquò fracassa encara, [[Special:UserLogout|desconnectatz-vos]], puèi connect
 S'aquò capita pas un còp de mai, [[Special:UserLogout|desconnectatz-vos]], puèi connectatz-vos tornamai.'''",
 'token_suffix_mismatch' => "'''Vòstra modificacion es pas estada acceptada perque vòstre navigador a mesclat los caractèrs de ponctuacion dins l’identificant d’edicion. La modificacion es estada regetada per empachar la corrupcion del tèxte de l’article. Aqueste problèma se produtz quand utilizatz un mandatari (proxy) anonim problematic.'''",
 'editing' => 'Modificacion de $1',
+'creating' => 'Creacion de $1',
 'editingsection' => 'Modificacion de $1 (seccion)',
 'editingcomment' => 'Modificacion de $1 (seccion novèla)',
 'editconflict' => 'Conflicte de modificacion : $1',
@@ -981,6 +985,7 @@ Sembla que siá estada suprimida.',
 'edit-no-change' => 'Vòstra modificacion es estada ignorada perque cap de cambiament es pas estat fach dins lo tèxte.',
 'edit-already-exists' => 'La pagina novèla a pogut èsser creada .
 Existís ja.',
+'defaultmessagetext' => 'Messatge per defaut',
 
 # Parser/template warnings
 'expensive-parserfunction-warning' => 'Atencion : Aquesta pagina conten tròp d’apèls dispendioses de foncions del parser.
@@ -1517,6 +1522,7 @@ Tanben podètz causir de permetre a d’autres de vos contactar per vòstra pagi
 'action-userrights' => 'modificar totes los dreches d’utilizaire',
 'action-userrights-interwiki' => 'modificar los dreches d’utilizaire e los sus d’autres wikis',
 'action-siteadmin' => 'varrolhar o desvarrolhar la banca de donadas',
+'action-sendemail' => 'mandar corrièrs electronics',
 
 # Recent changes
 'nchanges' => '$1 {{PLURAL:$1|cambiament|cambiaments}}',
@@ -1780,6 +1786,7 @@ La tièra seguenta aficha {{PLURAL:$1|lo primièr ligam de pagina|los $1 primiè
 Una [[Special:WhatLinksHere/$2|tièra completa]] es disponibla.',
 'nolinkstoimage' => 'Cap de pagina compòrta pas de ligam cap a aqueste imatge.',
 'morelinkstoimage' => 'Vejatz [[Special:WhatLinksHere/$1|mai de ligams]] cap a aqueste imatge.',
+'linkstoimage-redirect' => '$1 (redireccion de fichièr) $2',
 'duplicatesoffile' => "{{PLURAL:$1|Lo fichièr seguent es un duplicata|Los fichièrs seguents son de duplicatas}} d'aqueste fichièr ([[Special:FileDuplicateSearch/$2|mai de detalhs]]):",
 'sharedupload' => 'Aqueste fichièr proven de $1 e pòt èsser utilizat per d’autres projèctes.',
 'sharedupload-desc-there' => "Aqueste fichièr proven de $1 e pòt èsser utilizat per d'autres projèctes. Vejatz [$2 sa pagina de descripcion] per mai d'entresenhas.",
@@ -1876,6 +1883,7 @@ Una pagina es tractada coma una pagina d’omonimia s'utiliza un modèl qu'es li
 Cada entrada conten de ligams cap a la primièra e la segonda redireccions, e mai la primièra linha de tèxte de la segonda pagina, çò que provesís, de costuma, la « vertadièra » pagina cibla, cap a la quala la primièra redireccion deuriá redirigir.
 Las entradas <del>barradas</del> son estadas resolgudas.',
 'double-redirect-fixed-move' => '[[$1]] es estat renomenat, aquò es ara una redireccion cap a [[$2]]',
+'double-redirect-fixed-maintenance' => 'Correccion de  la doble redireccion de [[$1]] a [[$2]]',
 'double-redirect-fixer' => 'Corrector de redireccion',
 
 'brokenredirects' => 'Redireccions copadas',
@@ -2094,8 +2102,10 @@ L'adreça electronica qu'avètz indicada dins [[Special:Preferences|vòstras pre
 'watchnologin' => 'Vos sètz pas identificat(ada)',
 'watchnologintext' => 'Vos cal èsser [[Special:UserLogin|connectat(ada)]]
 per modificar vòstra lista de seguiment.',
+'addwatch' => 'Ajustar a la lista de seguiment',
 'addedwatchtext' => 'La pagina "[[:$1]]" es estada aponduda a vòstra [[Special:Watchlist|lista de seguiment]].
 Las modificacions venentas d\'aquesta pagina e de la pagina de discussion associada seràn repertoriadas aicí, e la pagina apareisserà <b>en gras</b> dins la [[Special:RecentChanges|tièra dels darrièrs cambiaments]] per èsser localizada mai aisidament.',
+'removewatch' => 'Suprimir de la lista de seguiment',
 'removedwatchtext' => 'La pagina « [[:$1]] » es estada levada de vòstra [[Special:Watchlist|lista de seguiment]].',
 'watch' => 'Seguir',
 'watchthispage' => 'Seguir aquesta pagina',
@@ -2419,6 +2429,7 @@ Podètz consultar la [[Special:BlockList|lista dels comptes e de las adreças IP
 a partir d'una adreça IP precedentament blocada.",
 'ipusubmit' => 'Suprimir aqueste blocatge',
 'unblocked' => '[[User:$1|$1]] es estat desblocat',
+'unblocked-range' => '$1 es estat desblocat',
 'unblocked-id' => 'Lo blocatge $1 es estat levat',
 'blocklist' => 'Utilizaires o adreças IP blocats',
 'ipblocklist' => 'Utilizaires o adreças IP blocats',
@@ -2787,6 +2798,13 @@ Aquò es probablament causat per un ligam sus lista negra que punta cap a un sit
 'spam_reverting' => 'Restabliment de la darrièra version que conten pas de ligam cap a $1',
 'spam_blanking' => 'Totas las versions que contenon de ligams cap a $1 son blanquidas',
 
+# Info page
+'pageinfo-subjectpage' => 'Pagina',
+'pageinfo-edits' => "Nombre d'edicions",
+'pageinfo-authors' => "Nombre d'autors distints",
+'pageinfo-views' => 'Nombre de vistas',
+'pageinfo-viewsperedit' => 'Visitas per modificacions',
+
 # Skin names
 'skinname-standard' => 'Estandard',
 'skinname-nostalgia' => 'Nostalgia',
@@ -3154,6 +3172,7 @@ Los autres ligams sus la meteissa linha son considerats coma d'excepcions, per e
 'exif-gpsdirection-m' => 'Nòrd magnetic',
 
 'exif-iimcategory-edu' => 'Educacion',
+'exif-iimcategory-evn' => 'Environament',
 'exif-iimcategory-hth' => 'Santat',
 'exif-iimcategory-lab' => 'Tribailh',
 'exif-iimcategory-pol' => 'Politic',
@@ -3264,6 +3283,7 @@ Confirmatz que desiratz tornar crear aqueste article.",
 'table_pager_first' => 'Primièra pagina',
 'table_pager_last' => 'Darrièra pagina',
 'table_pager_limit' => 'Far veire $1 elements per pagina',
+'table_pager_limit_label' => 'Elements per pagina:',
 'table_pager_limit_submit' => 'Accedir',
 'table_pager_empty' => 'Cap de resultat',
 
@@ -3405,6 +3425,7 @@ Picatz lo nom del fichièr sens lo prefix « {{ns:file}}: »",
 # Special:ComparePages
 'compare-page1' => 'Pagina 1',
 'compare-page2' => 'Pagina 2',
+'compare-submit' => 'Comparar',
 
 # Database error messages
 'dberr-header' => 'Aqueste wiki a un problèma',
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..79e037e 100644 (file)
@@ -695,6 +695,9 @@ $2',
 'ns-specialprotected' => 'Não é possível editar páginas especiais',
 'titleprotected' => 'Este título foi protegido contra criação por [[User:$1|$1]].
 A justificação foi "\'\'$2\'\'".',
+'filereadonlyerror' => 'Não é possível modificar o ficheiro "$1" porque o repositório de ficheiros "$2" está em modo de leitura.
+
+O administrador que efetuou o bloqueio deu a seguinte explicação: "$3".',
 
 # Virus scanner
 'virus-badscanner' => "Má configuração: antivírus desconhecido: ''$1''",
@@ -991,6 +994,7 @@ Este ainda não foi gravado!",
 'note' => "'''Nota:'''",
 'previewnote' => "'''Lembre-se que esta é apenas uma antevisão do resultado.'''
 As modificações ainda não foram gravadas!",
+'continue-editing' => 'Continuar a editar',
 'previewconflict' => 'Esta antevisão do resultado apresenta o texto da caixa de edição acima tal como este aparecerá se escolher gravá-lo.',
 'session_fail_preview' => "'''Não foi possível processar a edição devido à perda dos dados da sua sessão.
 Tente novamente, por favor.
@@ -1856,10 +1860,20 @@ Caso o problema persista, contacte um [[Special:ListUsers/sysop|administrador]].
 'backend-fail-internal' => 'Ocorreu um erro desconhecido no servidor de armazenamento "$1".',
 'backend-fail-contenttype' => 'Não foi possível determinar o tipo de conteúdo do ficheiro para armazenar em " $1 ".',
 
+# File journal errors
+'filejournal-fail-dbconnect' => 'Não foi possível ligar à base de dados de registos no "backend" de armazenamento "$1".',
+'filejournal-fail-dbquery' => 'Não foi possível atualizar a base de dados de registos do "backend" de armazenamento "$1".',
+
 # Lock manager
 'lockmanager-notlocked' => 'Não foi possível desbloquear " $1 "; não se encontra bloqueado.',
 'lockmanager-fail-closelock' => 'Não foi possível encerrar a referência de bloqueio para "$1".',
 'lockmanager-fail-deletelock' => 'Não foi possível eliminar a referência de bloqueio para "$1".',
+'lockmanager-fail-acquirelock' => 'Não foi possível adquirir bloqueio para "$1".',
+'lockmanager-fail-openlock' => 'Não foi possível abrir ficheiro de bloqueio para "$1".',
+'lockmanager-fail-releaselock' => 'Não foi possível libertar bloqueio para "$1".',
+'lockmanager-fail-db-bucket' => 'Não foi possível contactar bases de dados de bloqueio suficientes no "bucket" $1.',
+'lockmanager-fail-db-release' => 'Não foi possível libertar bloqueios na base de dados $1.',
+'lockmanager-fail-svr-release' => 'Não foi possível libertar bloqueios no servidor $1.',
 
 # ZipDirectoryReader
 'zip-file-open-error' => 'Foi encontrado um erro ao abrir o ficheiro ZIP para verificação.',
@@ -1877,6 +1891,7 @@ A sua segurança não pode ser devidamente verificada.',
 'uploadstash-badtoken' => 'Não foi possível executar essa operação, talvez porque as suas credenciais de edição expiraram. Tente novamente.',
 'uploadstash-errclear' => 'Não foi possível apagar os ficheiros.',
 'uploadstash-refresh' => 'Actualizar a lista de ficheiros',
+'invalid-chunk-offset' => 'Deslocamento de fragmento inválido',
 
 # img_auth script messages
 'img-auth-accessdenied' => 'Acesso negado',
@@ -1966,6 +1981,10 @@ Encontra-se disponível uma [[Special:WhatLinksHere/$2|lista completa]].',
 Consulte a [$2 página de descrição do ficheiro] para mais informações, por favor.',
 'sharedupload-desc-here' => 'Este ficheiro provém de $1 e pode ser usado por outros projectos.
 A descrição na [$2 página de descrição] é mostrada abaixo.',
+'sharedupload-desc-edit' => 'Este ficheiro provém de $1 e pode ser utilizado por outros projetos.
+Talvez você pretenda editar a descrição na sua [$2 página de descrição de ficheiro] lá.',
+'sharedupload-desc-create' => 'Este ficheiro provém de $1 e pode ser utilizado por outros projetos.
+Talvez você pretenda editar a descrição na sua [$2 página de descrição de ficheiro] lá.',
 'filepage-nofile' => 'Não existe nenhum ficheiro com este nome.',
 'filepage-nofile-link' => 'Não existe nenhum ficheiro com este nome, mas pode [$1 carregá-lo].',
 'uploadnewversion-linktext' => 'Carregar uma nova versão deste ficheiro',
@@ -2177,6 +2196,9 @@ Talvez contenha um ou mais caracteres que não podem ser usados nos títulos.',
 'allpages-bad-ns' => 'A {{SITENAME}} não possui o espaço nominal "$1".',
 'allpages-hide-redirects' => 'Ocultar redirecionamentos',
 
+# SpecialCachedPage
+'cachedspecial-refresh-now' => 'Ver mais recente.',
+
 # Special:Categories
 'categories' => 'Categorias',
 'categoriespagetext' => '{{PLURAL:$1|A seguinte categoria contém páginas ou ficheiros multimédia|As seguintes categorias contêm páginas ou ficheiros multimédia}}.
@@ -3775,6 +3797,9 @@ Em conjunto com este programa deve ter recebido [{{SERVER}}{{SCRIPTPATH}}/COPYIN
 'version-software' => 'Software instalado',
 'version-software-product' => 'Produto',
 'version-software-version' => 'Versão',
+'version-entrypoints' => 'URLs de ponto de entrada',
+'version-entrypoints-header-entrypoint' => 'Ponto de entrada',
+'version-entrypoints-header-url' => 'URL',
 
 # Special:FilePath
 'filepath' => 'Endereço de ficheiro',
@@ -3909,6 +3934,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 +3944,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..c1d4116 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.
@@ -993,7 +994,7 @@ Modificarea a fost respinsă pentru a preveni deformarea textului paginii.
 Acest fapt se poate întâmpla atunci când folosești un serviciu proxy anonim.'''",
 'edit_form_incomplete' => "'''Unele părți ale formularului de modificare nu au ajuns la server; verificați dacă modificările dumneavoastră sunt intacte și reîncercați.'''",
 'editing' => 'modificare $1',
-'creating' => 'Se creează $1',
+'creating' => 'Crearea paginii $1',
 'editingsection' => 'modificare $1 (secțiune)',
 'editingcomment' => 'Modificare $1 (secțiune nouă)',
 'editconflict' => 'Conflict de modificare: $1',
index 871aad0..46a196c 100644 (file)
@@ -1585,6 +1585,7 @@ Ce 'u probbleme angore jè presende, condatte 'n'[[Special:ListUsers/sysop|ammin
 'backend-fail-closetemp' => 'Non ge pozze achiudere file temboranèe.',
 'backend-fail-read' => "Non ge pozze leggere 'u file $1.",
 'backend-fail-create' => "Non ge pozze ccrejà 'u file $1.",
+'backend-fail-readonly' => 'L\'archivije de rete "$1" jè pe stu mumende in sole letture. \'U mutive ha state: "$2"',
 'backend-fail-connect' => 'Non ge pozze connettere \'a memorie de rrete "$1".',
 
 # Lock manager
@@ -2697,6 +2698,7 @@ Reggistrele sus a 'u combiuter tune e carechele aqquà.",
 'javascripttest-disabled' => "Sta funzione non g'à state abbilitate sus a sta Uicchi.",
 'javascripttest-title' => 'Stoche a esegue $1 test',
 'javascripttest-pagetext-noframework' => 'Sta pàgene jè riservate pe le esecuziune de le test de Javascript.',
+'javascripttest-pagetext-unknownframework' => 'Ambiende de teste scanusciute "$1".',
 'javascripttest-pagetext-frameworks' => 'Pe piacere scacchie une de le seguende ambiende de test: $1',
 
 # Tooltip help for the actions
@@ -3910,6 +3912,7 @@ Ce nò, tu puè ausà 'u module facile aqquà sotte. 'U commende tune avène agg
 'duration-seconds' => '{{PLURAL:$1|seconde|seconde}}',
 'duration-minutes' => '{{PLURAL:$1|minute|minute}}',
 'duration-hours' => '{{PLURAL: $1|ore|ore}}',
+'duration-days' => '$1 {{PLURAL:$1|sciurne|sciurne}}',
 'duration-weeks' => '{{PLURAL: $1|sumàne|sumàne}}',
 'duration-years' => '{{PLURAL: $1|anne|anne}}',
 'duration-decades' => '$1 {{PLURAL:$1|decade|decade}}',
index 13d71b8..95dc0d0 100644 (file)
@@ -986,6 +986,7 @@ $2
 'note' => "'''Примечание:'''",
 'previewnote' => "'''Помните, что это только предварительный просмотр.'''
 Ваши изменения ещё не были сохранены!",
+'continue-editing' => 'Продолжить редактирование',
 'previewconflict' => 'Этот предварительный просмотр отражает текст в верхнем окне редактирования так, как он будет выглядеть, если вы решите записать его.',
 'session_fail_preview' => "'''К сожалению, сервер не смог обработать вашу правку из-за потери идентификатора сессии.
 Пожалуйста, попробуйте ещё раз.
index dbdf9a3..ceefcc5 100644 (file)
@@ -215,7 +215,7 @@ $messages = array(
 'mytalk' => 'என் பேச்சு',
 'anontalk' => 'இந்த ஐ.பி. முகவரிக்கான பேச்சு',
 'navigation' => 'வழிசெலுத்தல்',
-'and' => '&#32;மற்றும்',
+'and' => ' மற்றும்',
 
 # Cologne Blue skin
 'qbfind' => 'கண்டுபிடி',
@@ -1859,6 +1859,9 @@ $1',
 'allpagesbadtitle' => 'கொடுக்கப்பட்ட தலைப்பு செல்லுபடியற்றது அல்லது பிழையான விக்கியிடை அல்லது மொழி முன்னொட்டைக் கொண்டுள்ளது. இது தலைப்புக்களில் பயன்படுத்த முடியாத எழுத்துக்களையும் கொண்டிருக்கலாம்.',
 'allpages-bad-ns' => '{{SITENAME}} தளத்தில் "$1" பெயர்வெளி கிடையாது.',
 
+# SpecialCachedPage
+'cachedspecial-refresh-now' => 'அண்மையான பதிப்பை காண்க',
+
 # Special:Categories
 'categories' => 'பகுப்புகள்',
 'categoriespagetext' => 'கீழே கொடுத்துள்ள பக்கங்கள் அல்லது ஊடகங்கள் இந்த {{PLURAL:$1|பகுப்பை|பகுப்புக்களை}} கொண்டுள்ளது.
@@ -2771,7 +2774,8 @@ $1',
 'metadata-help' => 'இந்தக் கோப்பு கூடுதலான தகவல்களைக் கொண்டுளது, இவை பெரும்பாலும் இக்கோப்பை உருவாக்கப் பயன்படுத்திய எண்ணிம ஒளிப்படக்கருவி அல்லது ஒளிவருடியால் சேர்க்கப்பட்டிருக்கலாம். இக்கோப்பு ஏதாவது வகையில் மாற்றியமைக்கப்பட்டிருந்தால் இத்தகவல்கள் அவற்றைச் சரிவர தராமல் இருக்கலாம்.',
 'metadata-expand' => 'மேலதிகத் தகவல்களைக் காட்டு',
 'metadata-collapse' => 'மேலதிகத் தகவல்களை மறை',
-'metadata-fields' => 'இங்கே காட்டப்பட்டுள்ள எக்சிப் மேல்நிலைத் தரவுகள் படிமவிளக்கப்பக்கத்தில் காட்டப்படும். ஏனைய தரவுகள் இயல்பிருப்பாக மறைக்கப்பட்டிருக்கும்.
+'metadata-fields' => 'இங்கே காட்டப்பட்டுள்ள எக்சிப் மேல்நிலைத் தரவுகள் படிமவிளக்கப்பக்கத்தில் மேல்நிலைத் தரவுகள் அட்டவணை மறைக்கப்பட்டிருக்கும் பொழுது
+ காட்டப்படும்.
 * make
 * model
 * datetimeoriginal
@@ -3337,7 +3341,7 @@ $5
 'version-variables' => 'மாறிகள்',
 'version-antispam' => ' குப்பை (spam) தடுப்பு',
 'version-skins' => 'தோல்கள்',
-'version-other' => 'மறà¯\8dறவà¯\88',
+'version-other' => 'பிறரà¯\8d',
 'version-mediahandlers' => 'ஊடக கையாளிகள்',
 'version-hooks' => 'கொக்கிகள்',
 'version-extension-functions' => 'நீட்சி செயற்பாடுகள்',
@@ -3348,13 +3352,14 @@ $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].',
 'version-software' => 'நிறுவப்பட்ட மென்பொருள்',
 'version-software-product' => 'உற்பத்திப்பொருள்',
 'version-software-version' => 'பதிப்பு',
+'version-entrypoints-header-url' => 'உரலி (URL)',
 
 # Special:FilePath
 'filepath' => 'கோப்பு வழி',
index cac83ff..695bd37 100644 (file)
@@ -955,6 +955,7 @@ $2
 Подібні проблеми можуть виникати при використанні анонімізуючих веб-проксі, що містять помилки.'''",
 'edit_form_incomplete' => "'''Частина даних із форми редагування не досягла сервера. Уважно перевірте чи не пошкоджено ваших правок і спробуйте ще раз.'''",
 'editing' => 'Редагування $1',
+'creating' => 'Створення $1',
 'editingsection' => 'Редагування $1 (розділ)',
 'editingcomment' => 'Редагування $1 (новий розділ)',
 'editconflict' => 'Конфлікт редагування: $1',
index b623ea3..1b5bf34 100644 (file)
@@ -415,7 +415,7 @@ $messages = array(
 'category-article-count-limited' => '{{PLURAL:$1|Trang|$1 trang}} sau nằm trong thể loại hiện hành.',
 'category-file-count' => '{{PLURAL:$2|Thể loại này có tập tin sau.|{{PLURAL:$1|Tập tin|$1 tập tin}} sau nằm trong thể loại này, trong tổng số $2 tập tin.}}',
 'category-file-count-limited' => '{{PLURAL:$1|Tập tin|$1 tập tin}} sau nằm trong thể loại hiện hành.',
-'listingcontinuesabbrev' => 'tiếp',
+'listingcontinuesabbrev' => '(tiếp theo)',
 'index-category' => 'Trang được ghi chỉ mục',
 'noindex-category' => 'Trang không hiển thị trong bộ máy tìm kiếm',
 'broken-file-category' => 'Trang nhúng tập tin không tồn tại',
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 77efaac..8a07a72 100644 (file)
@@ -11,6 +11,7 @@
  * @author Kaganer
  * @author KaiesTse
  * @author Mark85296341
+ * @author Simon Shek
  * @author Waihorace
  * @author William915
  * @author Wong128hk
@@ -638,6 +639,7 @@ $1',
 'createaccount' => '開戶口',
 'gotaccount' => '已經有戶口? $1。',
 'gotaccountlink' => '登入',
+'userlogin-resetlink' => '唔記得簽到資料?',
 'createaccountmail' => '用電郵',
 'createaccountreason' => '原因:',
 'badretype' => '你入嘅密碼唔一致。',
@@ -1698,7 +1700,7 @@ $1',
 'filehist-filesize' => '檔案大細',
 'filehist-comment' => '註解',
 'filehist-missing' => '檔案遺失',
-'imagelinks' => '檔案連結',
+'imagelinks' => '檔案用途',
 'linkstoimage' => '以下嘅$1個頁面連結到呢個檔案:',
 'linkstoimage-more' => '多過$1版連過去呢個檔案。
 下面嘅表只係列示咗連去呢個檔案嘅最頭$1版。
index 6e7f643..9cc1b18 100644 (file)
@@ -42,6 +42,7 @@
  * @author PhiLiP
  * @author Shinjiman
  * @author Shizhao
+ * @author Simon Shek
  * @author Supaiku
  * @author Tommyang
  * @author Waihorace
@@ -889,6 +890,7 @@ $2
 'updated' => '(已更新)',
 'note' => "'''注意:'''",
 'previewnote' => "'''请记住这仅为预览。'''您的更改还未保存!",
+'continue-editing' => '继续编辑',
 'previewconflict' => '这个预览显示了上面文字编辑区中的内容。它将在你选择保存后出现。',
 'session_fail_preview' => "'''抱歉!由于会话数据丢失,我们不能处理你的编辑。'''请重试。如果再次失败,请尝试[[Special:UserLogout|退出]]后重新登录。",
 'session_fail_preview_html' => "'''抱歉!我们不能处理你在进程数据丢失时的编辑。'''
@@ -1425,7 +1427,7 @@ $1",
 'right-editusercss' => '编辑其他用户的CSS文件',
 'right-edituserjs' => '编辑其他用户的JavaScript文件',
 'right-rollback' => '快速回退最后对特定页面作出的编辑的用户的所有编辑',
-'right-markbotedits' => '标示复原编辑作机械人编辑',
+'right-markbotedits' => '将回退编辑标记为机器人编辑动作',
 'right-noratelimit' => '没有使用频率限制',
 'right-import' => '由其它wiki中导入页面',
 'right-importupload' => '由文件上传中导入页面',
@@ -1657,7 +1659,7 @@ $1',
 'upload-too-many-redirects' => '在网址中有太多重新定向',
 'upload-unknown-size' => '未知大小',
 'upload-http-error' => '发生HTTP错误:$1',
-'upload-copy-upload-invalid-domain' => 'ä¸\8dè\83½ä»\8e该å\9f\9få\90\8dä¸\8bè½½æ\96\87件。',
+'upload-copy-upload-invalid-domain' => 'ä¸\8dè\83½ä»\8e该å\9f\9få\90\8dä¸\8aè½½æ\96\87件å\89¯æ\9c¬。',
 
 # File backend
 'backend-fail-stream' => '无法流传送文件$1。',
@@ -1801,7 +1803,8 @@ $1',
 请参阅在[$2 文件描述页面]以了解其相关信息。',
 'sharedupload-desc-here' => '该文件来自于$1,它可能在其它计划项目中被应用。
 它在[$2 文件描述页面]那边上的描述于下面显示。',
-'sharedupload-desc-edit' => '此文件是从 $1 和可能由其他维基项目使用。 !N !也许您想在其[ $2 文件描述页面]编辑说明。',
+'sharedupload-desc-edit' => '该文件来自$1,它可能在其它计划项目中被使用。
+或许您可以在其[$2 文件描述页面]上编辑说明。',
 'sharedupload-desc-create' => '此文件是从 $1 和可能由其他维基项目使用。 !N !也许您想在其[ $2 文件描述页面]编辑说明。',
 'filepage-nofile' => '不存在此名称的文件。',
 'filepage-nofile-link' => '不存在此名称的文件,但您可以[$1 上传它]。',
@@ -2014,6 +2017,7 @@ $1',
 'allpages-hide-redirects' => '隐藏重定向页',
 
 # SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => '你正在浏览本页的缓存版本,至多可能存在 $1 的延迟。',
 'cachedspecial-refresh-now' => '查看最新的。',
 
 # Special:Categories
@@ -3596,6 +3600,7 @@ MediaWiki是基于使用目的而加以发布,然而不负任何担保责任
 'version-software' => '已安装的软件',
 'version-software-product' => '产品',
 'version-software-version' => '版本',
+'version-entrypoints-header-url' => 'URL',
 
 # Special:FilePath
 'filepath' => '文件路径',
index 69167a0..fe7fb6a 100644 (file)
@@ -34,6 +34,7 @@
  * @author Philip
  * @author Shinjiman
  * @author Shizhao
+ * @author Simon Shek
  * @author Skjackey tse
  * @author Waihorace
  * @author Wmr89502270
@@ -591,6 +592,8 @@ $2',
 'customjsprotected' => '你並無權限去編輯此JavaScript頁面,因為他包含了另一位用戶的個人設定。',
 'ns-specialprotected' => '特殊頁面是不可以編輯的。',
 'titleprotected' => "這個標題已經被[[User:$1|$1]]保護以防止建立。理由是''$2''。",
+'filereadonlyerror' => '無法修改文件" $1 "因為文件庫" $2 "處於唯讀模式。 !
+管理員鎖定它的解釋是:" $3 "。',
 
 # Virus scanner
 'virus-badscanner' => "損壞設定: 未知的病毒掃瞄器: ''$1''",
@@ -675,6 +678,7 @@ $2',
 'emailconfirmlink' => '確認您的郵箱地址',
 'invalidemailaddress' => '郵箱地址格式不正確,請輸入正確的郵箱位址或清空該輸入框。',
 'cannotchangeemail' => '本wiki不允許對賬戶的電郵地址進行更改。',
+'emaildisabled' => '此網站不能發送電子郵件。',
 'accountcreated' => '已建立賬戶',
 'accountcreatedtext' => '$1的賬戶已經被建立。',
 'createaccount-title' => '在{{SITENAME}}中建立新賬戶',
@@ -865,6 +869,7 @@ $2
 'updated' => '(已更新)',
 'note' => "'''注意:'''",
 'previewnote' => "'''請記住這只是預覽,內容尚未儲存!'''",
+'continue-editing' => '繼續編輯',
 'previewconflict' => '這個預覽顯示了上面文字編輯區中的內容。它將在{{GENDER:|你|妳|你}}選擇保存後出現。',
 'session_fail_preview' => "'''很抱歉!由於部份資料遺失,我們無法處理您的編輯。'''
 請再試一次。
@@ -1642,6 +1647,7 @@ $1',
 'upload-too-many-redirects' => '在網址中有太多重新定向',
 'upload-unknown-size' => '未知的大小',
 'upload-http-error' => '已發生一個HTTP錯誤:$1',
+'upload-copy-upload-invalid-domain' => '不能從該域名上載檔𣗈副本。',
 
 # File backend
 'backend-fail-stream' => '無法流傳送文件$1。',
@@ -1660,12 +1666,17 @@ $1',
 'backend-fail-closetemp' => '無法創建臨時文件。',
 'backend-fail-read' => '找不到文件“$1”。',
 'backend-fail-create' => '找不到「$1」檔案。',
+'backend-fail-maxsize' => '無法創建檔𣗈$1​​,因為它大於$2字節。',
 'backend-fail-readonly' => '「$1」儲存後端目前是唯讀模式,因為:「$2」',
-'backend-fail-synced' => '文件"$1"在內部後端是不一致的區域。',
-'backend-fail-connect' => '無法連結至檔案後方“$1”。',
-'backend-fail-internal' => '檔案後方“$1”發生了一個未知錯誤。',
+'backend-fail-synced' => 'æ\96\87件"$1"å\9c¨å\85§é\83¨å­\98å\84²å¾\8c端æ\98¯ä¸\8dä¸\80è\87´ç\9a\84å\8d\80å\9f\9fã\80\82',
+'backend-fail-connect' => '無法連結至存儲後方“$1”。',
+'backend-fail-internal' => '存儲後方“$1”發生了一個未知錯誤。',
 'backend-fail-contenttype' => '無法確定檔案的內容類型以存儲於“$1”。',
-'backend-fail-batchsize' => '鑒於一批後端 $1 檔 {{PLURAL:$1| operation|operations}} ;限制是 $2   {{PLURAL:$2| operation|operations}}。',
+'backend-fail-batchsize' => '存儲後端被給予了$1次檔𣗈 {{PLURAL:$1|操作|操作}} ;限制是$2次{{PLURAL:$2|操作|操作}}。',
+
+# File journal errors
+'filejournal-fail-dbconnect' => '無法連接到後端存儲的日誌資料庫" $1 "。',
+'filejournal-fail-dbquery' => '無法更新後端存儲的日誌資料庫" $1 "。',
 
 # Lock manager
 'lockmanager-notlocked' => '無法解鎖「$1」;它沒有被鎖定。',
@@ -1698,7 +1709,10 @@ $1',
 
 # img_auth script messages
 'img-auth-accessdenied' => '拒絕存取',
-'img-auth-nopathinfo' => 'PATH_INFO缺失。您的服務器尚未設置傳送該信息。它可能是基於CGI的,因而不支持img_auth。[https://www.mediawiki.org/wiki/Manual:Image_Authorization 參見圖片認證。]',
+'img-auth-nopathinfo' => 'PATH_INFO缺失。
+您的服務器尚未設置傳送該信息。
+它可能是基於CGI的,因而不支持img_auth。
+請參見 https://www.mediawiki.org/wiki/Manual:Image_Authorization',
 'img-auth-notindir' => '所請求的路徑不在已經設定的上載目錄。',
 'img-auth-badtitle' => '不能夠由"$1"建立一個有效標題。',
 'img-auth-nologinnWL' => '您而家並未登入,"$1"不在白名單上。',
@@ -1780,6 +1794,10 @@ $1',
 請參閱在[$2 檔案描述頁面]以了解其相關資訊。',
 'sharedupload-desc-here' => '該檔案來自於$1,它可能在其它計劃項目中被應用。
 它在[$2 檔案描述頁面]那邊上的描述於下面顯示。',
+'sharedupload-desc-edit' => '該檔案來自$1,它可能在其它計劃項目中被使用。
+或許您可以在其[$2 檔𣗈描述頁面]上編輯說明。',
+'sharedupload-desc-create' => '該檔案來自$1,它可能在其它計劃項目中被使用。
+或許您可以在那邊的[$2 檔𣗈描述頁面]上編輯其說明。',
 'filepage-nofile' => '不存在此名稱的檔案。',
 'filepage-nofile-link' => '不存在此名稱的檔案,但您可以[$1 上傳它]。',
 'uploadnewversion-linktext' => '上傳該檔案的新版本',
@@ -1994,6 +2012,12 @@ Template:消除歧義',
 'allpagesprefix' => '顯示具有此前綴(名字空間)的頁面:',
 'allpagesbadtitle' => '給定的頁面標題是非法的,或者具有一個內部語言或內部 wiki 的前綴。它可能包含一個或更多的不能用於標題的字元。',
 'allpages-bad-ns' => '在{{SITENAME}}中沒有一個叫做"$1"的名字空間。',
+'allpages-hide-redirects' => '隱藏重定向頁',
+
+# SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => '你正在瀏覽本頁的緩存版本,至多可能存在$1的延遲。',
+'cachedspecial-viewing-cached-ts' => '您正在閱讀此頁的緩存版本,這可能不是完整的版本。',
+'cachedspecial-refresh-now' => '查看最新。',
 
 # Special:Categories
 'categories' => '頁面分類',
@@ -2430,8 +2454,8 @@ $1',
 'ipb-confirm' => '確認封禁',
 'badipaddress' => '無效IP地址',
 'blockipsuccesssub' => '查封成功',
-'blockipsuccesstext' => '[[Special:Contributions/$1|$1]]已經被查封。
-<br />參看[[Special:BlockList|被封IP地址列表]]以覆審查封。',
+'blockipsuccesstext' => '[[Special:Contributions/$1|$1]]已經被查封。<br />
+參看[[Special:BlockList|被封IP地址列表]]以覆審查封。',
 'ipb-blockingself' => '你要封禁自己!確認要這樣做嗎?',
 'ipb-confirmhideuser' => '你要封禁用戶並隱藏其用戶名,這會隱藏在所有列表及日誌中涉及此用戶之用戶名。你確定要這樣做嗎?',
 'ipb-edit-dropdown' => '編輯查封原因',
@@ -2834,6 +2858,11 @@ $1被封禁的理由是“$2”',
 'vector.css' => '/* 此處的 CSS 將影響使用 Vector 面板的用戶 */',
 'print.css' => '/* 此處的 CSS 將影響打印輸出 */',
 'handheld.css' => '/* 此處的 CSS 將影響在 $wgHandheldStyle 設定手提裝置面板 */',
+'noscript.css' => '/* 此處的 CSS 將影響沒有啓用 JavaScript 的用戶 */',
+'group-autoconfirmed.css' => '/* 此處的 CSS 將只會影響自動確認用戶 */',
+'group-bot.css' => '/* 此處的 CSS 將只會影響機器人 */',
+'group-sysop.css' => '/* 此處的 CSS 將只會影響管理員 */',
+'group-bureaucrat.css' => '/* 此處的 CSS 將只會影響行政員 */',
 
 # Scripts
 'common.js' => '/* 此處的JavaScript將載入於所有用戶每一個頁面。 */',
@@ -2975,14 +3004,20 @@ To disable showing a particular link, set it to 'disable', e.g.
 'variantname-zh-sg' => 'disable',
 Variants for Chinese language
 */
-'variantname-zh-hans' => '簡體',
-'variantname-zh-hant' => '繁體',
+'variantname-zh-hans' => '‪中文(简体)',
+'variantname-zh-hant' => '‪中文(繁體)',
 'variantname-zh-cn' => '大陸簡體',
 'variantname-zh-tw' => '台灣正體',
 'variantname-zh-hk' => '香港繁體',
+'variantname-zh-mo' => '澳門繁體',
 'variantname-zh-sg' => '新加坡簡體',
+'variantname-zh-my' => '马来西亚简体',
 'variantname-zh' => '不轉換',
 
+# Variants for Gan language
+'variantname-gan-hans' => '‪中文(简体)',
+'variantname-gan-hant' => '‪中文(繁體)',
+
 # Metadata
 'metadata' => '元數據',
 'metadata-help' => '此檔案中包含有擴展的訊息。這些訊息可能是由數位相機或掃描儀在創建或數字化過程中所添加的。
@@ -3591,6 +3626,8 @@ MediaWiki是基於使用目的而加以發佈,然而不負任何擔保責任
 'version-software' => '已經安裝的軟件',
 'version-software-product' => '產品',
 'version-software-version' => '版本',
+'version-entrypoints' => '入口點URL',
+'version-entrypoints-header-entrypoint' => '入口點',
 'version-entrypoints-header-url' => 'URL',
 
 # Special:FilePath
index 5d76cdf..ae1367b 100644 (file)
@@ -320,11 +320,7 @@ abstract class Maintenance {
                }
                if ( $channel === null ) {
                        $this->cleanupChanneled();
-                       if( php_sapi_name() == 'cli' ) {
-                               fwrite( STDOUT, $out );
-                       } else {
-                               print( $out );
-                       }
+                       print( $out );
                } else {
                        $out = preg_replace( '/\n\z/', '', $out );
                        $this->outputChanneled( $out, $channel );
@@ -358,11 +354,7 @@ abstract class Maintenance {
         */
        public function cleanupChanneled() {
                if ( !$this->atLineStart ) {
-                       if( php_sapi_name() == 'cli' ) {
-                               fwrite( STDOUT, "\n" );
-                       } else {
-                               print "\n";
-                       }
+                       print "\n";
                        $this->atLineStart = true;
                }
        }
@@ -381,31 +373,17 @@ abstract class Maintenance {
                        return;
                }
 
-               $cli = php_sapi_name() == 'cli';
-
                // End the current line if necessary
                if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
-                       if( $cli ) {
-                               fwrite( STDOUT, "\n" );
-                       } else {
-                               print "\n";
-                       }
+                       print "\n";
                }
 
-               if( $cli ) {
-                       fwrite( STDOUT, $msg );
-               } else {
-                       print $msg;
-               }
+               print $msg;
 
                $this->atLineStart = false;
                if ( $channel === null ) {
                        // For unchanneled messages, output trailing newline immediately
-                       if( $cli ) {
-                               fwrite( STDOUT, "\n" );
-                       } else {
-                               print "\n";
-                       }
+                       print "\n";
                        $this->atLineStart = true;
                }
                $this->lastChannel = $channel;
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" );
                        }
                }
 
diff --git a/maintenance/copyFileBackend.php b/maintenance/copyFileBackend.php
new file mode 100644 (file)
index 0000000..314318f
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Copy all files in one container of one backend to another.
+ *
+ * This can also be used to re-shard the files for one backend using the
+ * config of second backend. The second backend should have the same config
+ * as the first, except for it having a different name and different sharding
+ * configuration. The backend should be made read-only while this runs.
+ * After this script finishes, the old files in the containers can be deleted.
+ *
+ * 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
+ *
+ * @ingroup Maintenance
+ */
+
+require_once( dirname( __FILE__ ) . '/Maintenance.php' );
+
+class CopyFileBackend extends Maintenance {
+       public function __construct() {
+               parent::__construct();
+               $this->mDescription = "Copy all the files in one backend to another.";
+               $this->addOption( 'src', 'Backend containing the source files', true, true );
+               $this->addOption( 'dst', 'Backend where files should be copied to', true, true );
+               $this->addOption( 'containers', 'Pipe separated list of containers', true, true );
+               $this->addOption( 'fast', 'Skip SHA-1 checks on pre-existing files' );
+       }
+
+       public function execute() {
+               $src = FileBackendGroup::singleton()->get( $this->getOption( 'src' ) );
+               $dst = FileBackendGroup::singleton()->get( $this->getOption( 'dst' ) );
+
+               $containers = explode( '|', $this->getOption( 'containers' ) );
+               foreach ( $containers as $container ) {
+                       $this->output( "Doing container $container...\n" );
+
+                       $srcPathsRel = $src->getFileList(
+                               array( 'dir' => $src->getRootStoragePath() . "/$container" ) );
+                       if ( $srcPathsRel === null ) {
+                               $this->error( "Could not list files in $container.", 1 ); // die
+                       }
+                       foreach ( $srcPathsRel as $srcPathRel ) {
+                               $srcPath = $src->getRootStoragePath() . "/$container/$srcPathRel";
+                               $dstPath = $dst->getRootStoragePath() . "/$container/$srcPathRel";
+
+                               if ( $dst->fileExists( array( 'src' => $dstPath, 'latest' => 1 ) ) ) {
+                                       if ( $this->hasOption( 'fast' ) ) {
+                                               $this->output( "Already have $dstPath.\n" );
+                                               continue; // assume already copied...
+                                       }
+                                       $srcSha1 = $src->getFileSha1Base36( array( 'src' => $srcPath ) );
+                                       $dstSha1 = $dst->getFileSha1Base36( array( 'src' => $dstPath ) );
+                                       if ( $srcSha1 && $srcSha1 === $dstSha1 ) {
+                                               $this->output( "Already have $dstPath.\n" );
+                                               continue; // already copied...
+                                       }
+                               }
+
+                               $fsFile = $src->getLocalReference( array( 'src' => $srcPath, 'latest' => 1 ) );
+                               if ( !$fsFile ) {
+                                       $this->error( "Could not get local copy of $srcPath.", 1 ); // die
+                               }
+
+                               $status = $dst->prepare( array( 'dir' => dirname( $dstPath ) ) );
+                               $status->merge( $dst->store(
+                                       array( 'src' => $fsFile->getPath(), 'dst' => $dstPath ),
+                                       array( 'nonLocking' => 1, 'nonJournaled' => 1 )
+                               ) );
+                               if ( !$status->isOK() ) {
+                                       print_r( $status->getErrorsArray() );
+                                       $this->error( "Could not copy $srcPath to $dstPath.", 1 ); // die
+                               }
+
+                               $this->output( "Copied $srcPath to $dstPath.\n" );
+                       }
+               }
+       }
+}
+
+$maintClass = 'CopyFileBackend';
+require_once( RUN_MAINTENANCE_IF_MAIN );
index b656000..d22449b 100644 (file)
@@ -688,6 +688,10 @@ $wgMessageStructure = array(
                'parser-template-loop-warning',
                'parser-template-recursion-depth-warning',
                'language-converter-depth-warning',
+               'node-count-exceeded-category',
+               'node-count-exceeded-warning',
+               'expansion-depth-exceeded-category',
+               'expansion-depth-exceeded-warning',
        ),
        'undo' => array(
                'undo-success',
index 359cd24..088eaa3 100644 (file)
@@ -33,7 +33,7 @@ class mcTest extends Maintenance {
        }
 
        public function execute() {
-               global $wgMemCachedServers;
+               global $wgMemCachedServers, $wgMemCachedTimeout;
 
                $iterations = $this->getOption( 'i', 100 );
                if ( $this->hasArg() ) {
@@ -42,7 +42,10 @@ class mcTest extends Maintenance {
 
                foreach ( $wgMemCachedServers as $server ) {
                        $this->output( $server . " ", $server );
-                       $mcc = new MemCachedClientforWiki( array( 'persistant' => true ) );
+                       $mcc = new MemCachedClientforWiki( array(
+                               'persistant' => true,
+                               'timeout' => $wgMemCachedTimeout
+                       ) );
                        $mcc->set_servers( array( $server ) );
                        $set = 0;
                        $incr = 0;
index 14f158c..b6d2067 100644 (file)
@@ -61,7 +61,7 @@ class PopulateParentId extends LoggedUpdateMaintenance {
                        $cond = "rev_id BETWEEN $blockStart AND $blockEnd";
                        $res = $db->select( 'revision',
                                array( 'rev_id', 'rev_page', 'rev_timestamp', 'rev_parent_id' ),
-                               $cond, __METHOD__ );
+                               array( $cond, 'rev_parent_id' => null ), __METHOD__ );
                        # Go through and update rev_parent_id from these rows.
                        # Assume that the previous revision of the title was
                        # the original previous revision of the title when the
index 20a8043..2de851a 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__ );
@@ -203,32 +202,30 @@ class RefreshLinks extends Maintenance {
        public static function fixLinksFromArticle( $id ) {
                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 ) {
+               $content = $page->getContent( REVISION::RAW );
+               if ( null === false ) {
                        return;
                }
 
+               $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
 
                $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
-               $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
+               $context = RequestContext::getMain();
+
+               $parserOutput = $content->getParserOutput( $context, $page->getLatest(), $options, false );
 
-        $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
-        SecondaryDataUpdate::runUpdates( $updates );
+               $updates = $parserOutput->getSecondaryDataUpdates( $page->getTitle(), false );
+               SecondaryDataUpdate::runUpdates( $updates );
 
-        $dbw->commit();
-        // TODO: We don't know what happens here.
-               $update = new LinksUpdate( $title, $parserOutput, false );
-               $update->doUpdate();
                $dbw->commit( __METHOD__ );
        }
 
index 861bbfa..4a771fd 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Blind 1.8.18
+/*!
+ * jQuery UI Effects Blind 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index dc21f31..947c50f 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Bounce 1.8.18
+/*!
+ * jQuery UI Effects Bounce 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 1ac61bb..a92e061 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Clip 1.8.18
+/*!
+ * jQuery UI Effects Clip 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 19e2f70..430dd2e 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects 1.8.18
+/*!
+ * jQuery UI Effects 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -236,7 +236,7 @@ $.effects.animateClass = function(value, duration, easing, callback) {
                        originalStyleAttr = that.attr('style') || ' ',
                        originalStyle = filterStyles(getElementStyles.call(this)),
                        newStyle,
-                       className = that.attr('class');
+                       className = that.attr('class') || "";
 
                $.each(classAnimationActions, function(i, action) {
                        if (value[action]) {
@@ -306,7 +306,7 @@ $.fn.extend({
 /******************************************************************************/
 
 $.extend($.effects, {
-       version: "1.8.18",
+       version: "@VERSION",
 
        // Saves a set of properties in a data storage
        save: function(element, set) {
@@ -419,7 +419,7 @@ $.extend($.effects, {
        setTransition: function(element, list, factor, value) {
                value = value || {};
                $.each(list, function(i, x){
-                       unit = element.cssUnit(x);
+                       var unit = element.cssUnit(x);
                        if (unit[0] > 0) value[x] = unit[0] * factor + unit[1];
                });
                return value;
index c674cca..43e6420 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Drop 1.8.18
+/*!
+ * jQuery UI Effects Drop 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index b2af6da..1076e47 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Explode 1.8.18
+/*!
+ * jQuery UI Effects Explode 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index beb5cb4..2f2c4fa 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Fade 1.8.18
+/*!
+ * jQuery UI Effects Fade 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 762ec6c..1fd8e2a 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Fold 1.8.18
+/*!
+ * jQuery UI Effects Fold 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index f2d2333..2169411 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Highlight 1.8.18
+/*!
+ * jQuery UI Effects Highlight 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 418df4e..f68a110 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Pulsate 1.8.18
+/*!
+ * jQuery UI Effects Pulsate 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -15,8 +15,8 @@
 $.effects.pulsate = function(o) {
        return this.queue(function() {
                var elem = $(this),
-                       mode = $.effects.setMode(elem, o.options.mode || 'show');
-                       times = ((o.options.times || 5) * 2) - 1;
+                       mode = $.effects.setMode(elem, o.options.mode || 'show'),
+                       times = ((o.options.times || 5) * 2) - 1,
                        duration = o.duration ? o.duration / 2 : $.fx.speeds._default / 2,
                        isVisible = elem.is(':visible'),
                        animateTo = 0;
index e2d6482..11f1c25 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Scale 1.8.18
+/*!
+ * jQuery UI Effects Scale 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -140,7 +140,7 @@ $.effects.size = function(o) {
                        hProps = hProps.concat(['marginLeft','marginRight']); // Add margins
                        props2 = props.concat(vProps).concat(hProps); // Concat
                        el.find("*[width]").each(function(){
-                               child = $(this);
+                               var child = $(this);
                                if (restore) $.effects.save(child, props2);
                                var c_original = {height: child.height(), width: child.width()}; // Save original
                                child.from = {height: c_original.height * factor.from.y, width: c_original.width * factor.from.x};
index 91388ab..eca9bee 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Shake 1.8.18
+/*!
+ * jQuery UI Effects Shake 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index d481a2a..6a0e973 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Slide 1.8.18
+/*!
+ * jQuery UI Effects Slide 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 776fe46..f668859 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Effects Transfer 1.8.18
+/*!
+ * jQuery UI Effects Transfer 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 166f537..f3ef9e8 100644 (file)
@@ -13,7 +13,7 @@ jQuery(function($){
                dayNames: ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'],
                dayNamesShort: ['So','Mo','Di','Mi','Do','Fr','Sa'],
                dayNamesMin: ['So','Mo','Di','Mi','Do','Fr','Sa'],
-               weekHeader: 'Wo',
+               weekHeader: 'KW',
                dateFormat: 'dd.mm.yy',
                firstDay: 1,
                isRTL: false,
index 92f81f6..32702b2 100644 (file)
@@ -13,7 +13,7 @@ jQuery(function($){
                dayNames: ['Pühapäev', 'Esmaspäev', 'Teisipäev', 'Kolmapäev', 'Neljapäev', 'Reede', 'Laupäev'],
                dayNamesShort: ['Pühap', 'Esmasp', 'Teisip', 'Kolmap', 'Neljap', 'Reede', 'Laup'],
                dayNamesMin: ['P','E','T','K','N','R','L'],
-               weekHeader: 'Sm',
+               weekHeader: 'näd',
                dateFormat: 'dd.mm.yy',
                firstDay: 1,
                isRTL: false,
index 81de4da..be8acd2 100644 (file)
@@ -4,15 +4,51 @@
 jQuery(function($) {
        $.datepicker.regional['fa'] = {
                closeText: 'بستن',
-               prevText: '&#x3c;قبلي',
-               nextText: 'بعدي&#x3e;',
+               prevText: '&#x3C;قبلی',
+               nextText: 'بعدی&#x3E;',
                currentText: 'امروز',
-               monthNames: ['فروردين','ارديبهشت','خرداد','تير','مرداد','شهريور',
-               'مهر','آبان','آذر','دي','بهمن','اسفند'],
+               monthNames: [
+                       'فروردين',
+                       'ارديبهشت',
+                       'خرداد',
+                       'تير',
+                       'مرداد',
+                       'شهريور',
+                       'مهر',
+                       'آبان',
+                       'آذر',
+                       'دی',
+                       'بهمن',
+                       'اسفند'
+               ],
                monthNamesShort: ['1','2','3','4','5','6','7','8','9','10','11','12'],
-               dayNames: ['يکشنبه','دوشنبه','سه‌شنبه','چهارشنبه','پنجشنبه','جمعه','شنبه'],
-               dayNamesShort: ['ي','د','س','چ','پ','ج', 'ش'],
-               dayNamesMin: ['ي','د','س','چ','پ','ج', 'ش'],
+               dayNames: [
+                       'يکشنبه',
+                       'دوشنبه',
+                       'سه‌شنبه',
+                       'چهارشنبه',
+                       'پنجشنبه',
+                       'جمعه',
+                       'شنبه'
+               ],
+               dayNamesShort: [
+                       'ی',
+                       'د',
+                       'س',
+                       'چ',
+                       'پ',
+                       'ج', 
+                       'ش'
+               ],
+               dayNamesMin: [
+                       'ی',
+                       'د',
+                       'س',
+                       'چ',
+                       'پ',
+                       'ج', 
+                       'ش'
+               ],
                weekHeader: 'هف',
                dateFormat: 'yy/mm/dd',
                firstDay: 6,
index e1f25fd..4c5adda 100644 (file)
@@ -1,23 +1,23 @@
 /* Finnish initialisation for the jQuery UI date picker plugin. */
-/* Written by Harri Kilpi (harrikilpio@gmail.com). */
+/* Written by Harri Kilpiö (harrikilpio@gmail.com). */
 jQuery(function($){
-    $.datepicker.regional['fi'] = {
+       $.datepicker.regional['fi'] = {
                closeText: 'Sulje',
-               prevText: '&laquo;Edellinen',
-               nextText: 'Seuraava&raquo;',
-               currentText: 'T&auml;n&auml;&auml;n',
-        monthNames: ['Tammikuu','Helmikuu','Maaliskuu','Huhtikuu','Toukokuu','Kes&auml;kuu',
-        'Hein&auml;kuu','Elokuu','Syyskuu','Lokakuu','Marraskuu','Joulukuu'],
-        monthNamesShort: ['Tammi','Helmi','Maalis','Huhti','Touko','Kes&auml;',
-        'Hein&auml;','Elo','Syys','Loka','Marras','Joulu'],
-               dayNamesShort: ['Su','Ma','Ti','Ke','To','Pe','Su'],
+               prevText: '&#xAB;Edellinen',
+               nextText: 'Seuraava&#xBB;',
+               currentText: 'T&#xE4;n&#xE4;&#xE4;n',
+               monthNames: ['Tammikuu','Helmikuu','Maaliskuu','Huhtikuu','Toukokuu','Kes&#xE4;kuu',
+               'Hein&#xE4;kuu','Elokuu','Syyskuu','Lokakuu','Marraskuu','Joulukuu'],
+               monthNamesShort: ['Tammi','Helmi','Maalis','Huhti','Touko','Kes&#xE4;',
+               'Hein&#xE4;','Elo','Syys','Loka','Marras','Joulu'],
+               dayNamesShort: ['Su','Ma','Ti','Ke','To','Pe','La'],
                dayNames: ['Sunnuntai','Maanantai','Tiistai','Keskiviikko','Torstai','Perjantai','Lauantai'],
                dayNamesMin: ['Su','Ma','Ti','Ke','To','Pe','La'],
                weekHeader: 'Vk',
-        dateFormat: 'dd.mm.yy',
+               dateFormat: 'dd.mm.yy',
                firstDay: 1,
                isRTL: false,
                showMonthAfterYear: false,
                yearSuffix: ''};
-    $.datepicker.setDefaults($.datepicker.regional['fi']);
+       $.datepicker.setDefaults($.datepicker.regional['fi']);
 });
index 1e60242..0285325 100644 (file)
@@ -6,9 +6,9 @@ jQuery(function($){
                prevText: '&#x3C;',
                nextText: '&#x3E;',
                currentText: 'Денес',
-               monthNames: ['Ð\88анÑ\83аÑ\80и','Фебруари','Март','Април','Мај','Јуни',
+               monthNames: ['Ð\88анÑ\83аÑ\80и','Февруари','Март','Април','Мај','Јуни',
                'Јули','Август','Септември','Октомври','Ноември','Декември'],
-               monthNamesShort: ['Ð\88ан','Феб','Мар','Апр','Мај','Јун',
+               monthNamesShort: ['Ð\88ан','Фев','Мар','Апр','Мај','Јун',
                'Јул','Авг','Сеп','Окт','Ное','Дек'],
                dayNames: ['Недела','Понеделник','Вторник','Среда','Четврток','Петок','Сабота'],
                dayNamesShort: ['Нед','Пон','Вто','Сре','Чет','Пет','Саб'],
index 77afe55..c078963 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Accordion 1.8.18
+/*!
+ * jQuery UI Accordion 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -518,7 +518,7 @@ $.widget( "ui.accordion", {
 });
 
 $.extend( $.ui.accordion, {
-       version: "1.8.18",
+       version: "@VERSION",
        animations: {
                slide: function( options, additions ) {
                        options = $.extend({
index 8889ca9..70fb33b 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Autocomplete 1.8.18
+/*!
+ * jQuery UI Autocomplete 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -37,6 +37,7 @@ $.widget( "ui.autocomplete", {
                var self = this,
                        doc = this.element[ 0 ].ownerDocument,
                        suppressKeyPress;
+               this.isMultiLine = this.element.is( "textarea" );
 
                this.element
                        .addClass( "ui-autocomplete-input" )
@@ -62,14 +63,10 @@ $.widget( "ui.autocomplete", {
                                        self._move( "nextPage", event );
                                        break;
                                case keyCode.UP:
-                                       self._move( "previous", event );
-                                       // prevent moving cursor to beginning of text field in some browsers
-                                       event.preventDefault();
+                                       self._keyEvent( "previous", event );
                                        break;
                                case keyCode.DOWN:
-                                       self._move( "next", event );
-                                       // prevent moving cursor to end of text field in some browsers
-                                       event.preventDefault();
+                                       self._keyEvent( "next", event );
                                        break;
                                case keyCode.ENTER:
                                case keyCode.NUMPAD_ENTER:
@@ -131,9 +128,6 @@ $.widget( "ui.autocomplete", {
                                }, 150 );
                        });
                this._initSource();
-               this.response = function() {
-                       return self._response.apply( self, arguments );
-               };
                this.menu = $( "<ul></ul>" )
                        .addClass( "ui-autocomplete" )
                        .appendTo( $( this.options.appendTo || "body", doc )[0] )
@@ -268,18 +262,11 @@ $.widget( "ui.autocomplete", {
                                        url: url,
                                        data: request,
                                        dataType: "json",
-                                       context: {
-                                               autocompleteRequest: ++requestIndex
-                                       },
                                        success: function( data, status ) {
-                                               if ( this.autocompleteRequest === requestIndex ) {
-                                                       response( data );
-                                               }
+                                               response( data );
                                        },
                                        error: function() {
-                                               if ( this.autocompleteRequest === requestIndex ) {
-                                                       response( [] );
-                                               }
+                                               response( [] );
                                        }
                                });
                        };
@@ -310,10 +297,26 @@ $.widget( "ui.autocomplete", {
                this.pending++;
                this.element.addClass( "ui-autocomplete-loading" );
 
-               this.source( { term: value }, this.response );
+               this.source( { term: value }, this._response() );
        },
 
-       _response: function( content ) {
+       _response: function() {
+               var that = this,
+                       index = ++requestIndex;
+
+               return function( content ) {
+                       if ( index === requestIndex ) {
+                               that.__response( content );
+                       }
+
+                       that.pending--;
+                       if ( !that.pending ) {
+                               that.element.removeClass( "ui-autocomplete-loading" );
+                       }
+               };
+       },
+
+       __response: function( content ) {
                if ( !this.options.disabled && content && content.length ) {
                        content = this._normalize( content );
                        this._suggest( content );
@@ -321,10 +324,6 @@ $.widget( "ui.autocomplete", {
                } else {
                        this.close();
                }
-               this.pending--;
-               if ( !this.pending ) {
-                       this.element.removeClass( "ui-autocomplete-loading" );
-               }
        },
 
        close: function( event ) {
@@ -422,6 +421,14 @@ $.widget( "ui.autocomplete", {
 
        widget: function() {
                return this.menu.element;
+       },
+       _keyEvent: function( keyEvent, event ) {
+               if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
+                       this._move( keyEvent, event );
+
+                       // prevents moving cursor to beginning/end of the text field in some browsers
+                       event.preventDefault();
+               }
        }
 });
 
@@ -594,7 +601,7 @@ $.widget("ui.menu", {
                        }
 
                        var base = this.active.offset().top,
-                               height = this.element.height();
+                               height = this.element.height(),
                                result = this.element.children(".ui-menu-item").filter(function() {
                                        var close = $(this).offset().top - base + height - $(this).height();
                                        // TODO improve approximation
index 1ff2f7f..b1e3466 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Button 1.8.18
+/*!
+ * jQuery UI Button 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 98b4f9b..9feda3e 100644 (file)
@@ -1,7 +1,7 @@
 /*!
- * jQuery UI 1.8.18
+ * jQuery UI 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -18,7 +18,7 @@ if ( $.ui.version ) {
 }
 
 $.extend( $.ui, {
-       version: "1.8.18",
+       version: "@VERSION",
 
        keyCode: {
                ALT: 18,
index c3b08be..71820bb 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Datepicker 1.8.18
+/*!
+ * jQuery UI Datepicker 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -12,7 +12,7 @@
  */
 (function( $, undefined ) {
 
-$.extend($.ui, { datepicker: { version: "1.8.18" } });
+$.extend($.ui, { datepicker: { version: "@VERSION" } });
 
 var PROP_NAME = 'datepicker';
 var dpuuid = new Date().getTime();
@@ -620,8 +620,8 @@ $.extend(Datepicker.prototype, {
                                        $.datepicker._updateDatepicker(inst);
                                }
                        }
-                       catch (event) {
-                               $.datepicker.log(event);
+                       catch (err) {
+                               $.datepicker.log(err);
                        }
                }
                return true;
@@ -800,10 +800,8 @@ $.extend(Datepicker.prototype, {
                if (this._datepickerShowing) {
                        var showAnim = this._get(inst, 'showAnim');
                        var duration = this._get(inst, 'duration');
-                       var self = this;
                        var postProcess = function() {
                                $.datepicker._tidyDialog(inst);
-                               self._curInst = null;
                        };
                        if ($.effects && $.effects[showAnim])
                                inst.dpDiv.hide(showAnim, $.datepicker._get(inst, 'showOptions'), duration, postProcess);
@@ -1817,7 +1815,7 @@ $.fn.datepicker = function(options){
 $.datepicker = new Datepicker(); // singleton instance
 $.datepicker.initialized = false;
 $.datepicker.uuid = new Date().getTime();
-$.datepicker.version = "1.8.18";
+$.datepicker.version = "@VERSION";
 
 // Workaround for #4055
 // Add another global to avoid noConflict issues with inline event handlers
index 4340b45..1461016 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Dialog 1.8.18
+/*!
+ * jQuery UI Dialog 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -702,7 +702,7 @@ $.widget("ui.dialog", {
 });
 
 $.extend($.ui.dialog, {
-       version: "1.8.18",
+       version: "@VERSION",
 
        uuid: 0,
        maxZ: 0,
index 4141ff7..417b761 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Draggable 1.8.18
+/*!
+ * jQuery UI Draggable 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -505,7 +505,7 @@ $.widget("ui.draggable", $.ui.mouse, {
 });
 
 $.extend($.ui.draggable, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 $.ui.plugin.add("draggable", "connectToSortable", {
index afd421f..c551864 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Droppable 1.8.18
+/*!
+ * jQuery UI Droppable 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -147,7 +147,7 @@ $.widget("ui.droppable", {
 });
 
 $.extend($.ui.droppable, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 $.ui.intersect = function(draggable, droppable, toleranceMode) {
index 669d563..cb3acd8 100644 (file)
@@ -1,7 +1,7 @@
 /*!
- * jQuery UI Mouse 1.8.18
+ * jQuery UI Mouse 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -45,6 +45,9 @@ $.widget("ui.mouse", {
        // other instances of mouse
        _mouseDestroy: function() {
                this.element.unbind('.'+this.widgetName);
+               $(document)
+                       .unbind('mousemove.'+this.widgetName, this._mouseMoveDelegate)
+                       .unbind('mouseup.'+this.widgetName, this._mouseUpDelegate);
        },
 
        _mouseDown: function(event) {
index 1b90ea1..2f1975d 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Position 1.8.18
+/*!
+ * jQuery UI Position 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index c042b40..17d55cf 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Progressbar 1.8.18
+/*!
+ * jQuery UI Progressbar 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -103,7 +103,7 @@ $.widget( "ui.progressbar", {
 });
 
 $.extend( $.ui.progressbar, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 })( jQuery );
index b441435..ea5de1b 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Resizable 1.8.18
+/*!
+ * jQuery UI Resizable 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -541,7 +541,7 @@ $.widget("ui.resizable", $.ui.mouse, {
 });
 
 $.extend($.ui.resizable, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 /*
@@ -691,13 +691,13 @@ $.ui.plugin.add("resizable", "containment", {
 
                if (cp.left < (self._helper ? co.left : 0)) {
                        self.size.width = self.size.width + (self._helper ? (self.position.left - co.left) : (self.position.left - cop.left));
-                       if (pRatio) self.size.height = self.size.width / o.aspectRatio;
+                       if (pRatio) self.size.height = self.size.width / self.aspectRatio;
                        self.position.left = o.helper ? co.left : 0;
                }
 
                if (cp.top < (self._helper ? co.top : 0)) {
                        self.size.height = self.size.height + (self._helper ? (self.position.top - co.top) : self.position.top);
-                       if (pRatio) self.size.width = self.size.height * o.aspectRatio;
+                       if (pRatio) self.size.width = self.size.height * self.aspectRatio;
                        self.position.top = self._helper ? co.top : 0;
                }
 
index 275021d..852dc26 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Selectable 1.8.18
+/*!
+ * jQuery UI Selectable 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -261,7 +261,7 @@ $.widget("ui.selectable", $.ui.mouse, {
 });
 
 $.extend($.ui.selectable, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 })(jQuery);
index 23a56cc..d74ee25 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Slider 1.8.18
+/*!
+ * jQuery UI Slider 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -656,7 +656,7 @@ $.widget( "ui.slider", $.ui.mouse, {
 });
 
 $.extend( $.ui.slider, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 }(jQuery));
index c3fb719..13bcd7d 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Sortable 1.8.18
+/*!
+ * jQuery UI Sortable 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -661,7 +661,7 @@ $.widget("ui.sortable", $.ui.mouse, {
 
                                        var el = $(document.createElement(self.currentItem[0].nodeName))
                                                .addClass(className || self.currentItem[0].className+" ui-sortable-placeholder")
-                                               .removeClass("ui-sortable-helper")[0];
+                                               .removeClass("ui-sortable-helper").html("&nbsp;")[0];
 
                                        if(!className)
                                                el.style.visibility = "hidden";
@@ -1075,7 +1075,7 @@ $.widget("ui.sortable", $.ui.mouse, {
 });
 
 $.extend($.ui.sortable, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 })(jQuery);
index bd30ad5..88e8dd9 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Tabs 1.8.18
+/*!
+ * jQuery UI Tabs 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -427,7 +427,7 @@ $.widget( "ui.tabs", {
                // meta-function to give users option to provide a href string instead of a numerical index.
                // also sanitizes numerical indexes to valid values.
                if ( typeof index == "string" ) {
-                       index = this.anchors.index( this.anchors.filter( "[href$=" + index + "]" ) );
+                       index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) );
                }
 
                return index;
@@ -698,7 +698,7 @@ $.widget( "ui.tabs", {
 });
 
 $.extend( $.ui.tabs, {
-       version: "1.8.18"
+       version: "@VERSION"
 });
 
 /*
@@ -733,7 +733,6 @@ $.extend( $.ui.tabs.prototype, {
                                }
                        }
                        : function( e ) {
-                               t = o.selected;
                                rotate();
                        });
 
index 0c6f53f..f3b789a 100644 (file)
@@ -1,7 +1,7 @@
 /*!
- * jQuery UI Widget 1.8.18
+ * jQuery UI Widget 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 3e5df6c..b98034f 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Accordion 1.8.18
+/*!
+ * jQuery UI Accordion 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 955f121..978f2bb 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Autocomplete 1.8.18
+/*!
+ * jQuery UI Autocomplete 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
@@ -13,7 +13,7 @@
 * html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
 
 /*
- * jQuery UI Menu 1.8.18
+ * jQuery UI Menu @VERSION
  *
  * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
index 0074880..8b3de3d 100644 (file)
@@ -1,13 +1,13 @@
-/*
- * jQuery UI Button 1.8.18
+/*!
+ * jQuery UI Button 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
  * http://docs.jquery.com/UI/Button#theming
  */
-.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: hidden; *overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
 .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
 button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
 .ui-button-icons-only { width: 3.4em; } 
index 42fc9e7..38b5426 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI CSS Framework 1.8.18
+/*!
+ * jQuery UI CSS Framework 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index bf61459..bb532e0 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Datepicker 1.8.18
+/*!
+ * jQuery UI Datepicker 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index ba14475..6bac05c 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Dialog 1.8.18
+/*!
+ * jQuery UI Dialog 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 3850631..9e86586 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Progressbar 1.8.18
+/*!
+ * jQuery UI Progressbar 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index ecfa0d1..57b7158 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Resizable 1.8.18
+/*!
+ * jQuery UI Resizable 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index f06d70c..d3ef7c3 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Selectable 1.8.18
+/*!
+ * jQuery UI Selectable 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index df19d6f..1491d6b 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Slider 1.8.18
+/*!
+ * jQuery UI Slider 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 2e98e2e..20bc1ca 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI Tabs 1.8.18
+/*!
+ * jQuery UI Tabs 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 0e2a746..9e16251 100644 (file)
@@ -1,7 +1,7 @@
-/*
- * jQuery UI CSS Framework 1.8.18
+/*!
+ * jQuery UI CSS Framework 1.8.19
  *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
  * Dual licensed under the MIT or GPL Version 2 licenses.
  * http://jquery.org/license
  *
index 148b08b..4f345ab 100644 (file)
@@ -138,7 +138,7 @@ var mw = ( function ( $, undefined ) {
                /**
                 * Simple message parser, does $N replacement and nothing else.
                 * This may be overridden to provide a more complex message parser.
-                * 
+                *
                 * This function will not be called for nonexistent messages.
                 */
                parser: function() {
@@ -148,7 +148,7 @@ var mw = ( function ( $, undefined ) {
                                return parameters[index] !== undefined ? parameters[index] : '$' + match;
                        } );
                },
-               
+
                /**
                 * Appends (does not replace) parameters for replacement to the .parameters property.
                 *
@@ -192,7 +192,7 @@ var mw = ( function ( $, undefined ) {
                                text = this.parser();
                                text = mw.html.escape( text );
                        }
-                       
+
                        if ( this.format === 'parse' ) {
                                text = this.parser();
                        }
@@ -248,7 +248,7 @@ var mw = ( function ( $, undefined ) {
                 * emulates console.log in console-less environments.
                 */
                log: function() { },
-       
+
                /**
                 * @var constructor Make the Map constructor publicly available.
                 */
@@ -258,7 +258,7 @@ var mw = ( function ( $, undefined ) {
                 * @var constructor Make the Message constructor publicly available.
                 */
                Message: Message,
-       
+
                /**
                 * List of configuration values
                 *
@@ -267,25 +267,25 @@ var mw = ( function ( $, undefined ) {
                 * in the global window object.
                 */
                config: null,
-       
+
                /**
                 * @var object
                 *
                 * Empty object that plugins can be installed in.
                 */
                libs: {},
-       
+
                /* Extension points */
-       
+
                legacy: {},
-       
+
                /**
                 * Localization system
                 */
                messages: new Map(),
-       
+
                /* Public Methods */
-       
+
                /**
                 * Gets a message object, similar to wfMessage()
                 *
@@ -305,7 +305,7 @@ var mw = ( function ( $, undefined ) {
                        }
                        return new Message( mw.messages, key, parameters );
                },
-       
+
                /**
                 * Gets a message string, similar to wfMsg()
                 *
@@ -317,14 +317,14 @@ var mw = ( function ( $, undefined ) {
                msg: function ( /* key, parameter_1, parameter_2, .. */ ) {
                        return mw.message.apply( mw.message, arguments ).toString();
                },
-       
+
                /**
                 * Client-side module loader which integrates with the MediaWiki ResourceLoader
                 */
                loader: ( function () {
-       
+
                        /* Private Members */
-       
+
                        /**
                         * Mapping of registered modules
                         *
@@ -372,15 +372,15 @@ var mw = ( function ( $, undefined ) {
                                ready = false,
                                // Selector cache for the marker element. Use getMarker() to get/use the marker!
                                $marker = null;
-       
+
                        /* Cache document ready status */
-       
+
                        $(document).ready( function () {
                                ready = true;
                        } );
-       
+
                        /* Private methods */
-       
+
                        function getMarker() {
                                // Cached ?
                                if ( $marker ) {
@@ -464,7 +464,7 @@ var mw = ( function ( $, undefined ) {
                                }
                                return true;
                        }
-       
+
                        /**
                         * Generates an ISO8601 "basic" string from a UNIX timestamp
                         */
@@ -479,13 +479,13 @@ var mw = ( function ( $, undefined ) {
                                        pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
                                ].join( '' );
                        }
-       
+
                        /**
                         * Recursively resolves dependencies and detects circular references
                         */
                        function recurse( module, resolved, unresolved ) {
                                var n, deps, len;
-       
+
                                if ( registry[module] === undefined ) {
                                        throw new Error( 'Unknown dependency: ' + module );
                                }
@@ -518,7 +518,7 @@ var mw = ( function ( $, undefined ) {
                                }
                                resolved[resolved.length] = module;
                        }
-       
+
                        /**
                         * Gets a list of module names that a module depends on in their proper dependency order
                         *
@@ -528,7 +528,7 @@ var mw = ( function ( $, undefined ) {
                         */
                        function resolve( module ) {
                                var modules, m, deps, n, resolved;
-       
+
                                // Allow calling with an array of module names
                                if ( $.isArray( module ) ) {
                                        modules = [];
@@ -549,7 +549,7 @@ var mw = ( function ( $, undefined ) {
 
                                throw new Error( 'Invalid module argument: ' + module );
                        }
-       
+
                        /**
                         * Narrows a list of module names down to those matching a specific
                         * state (see comment on top of this scope for a list of valid states).
@@ -563,7 +563,7 @@ var mw = ( function ( $, undefined ) {
                         */
                        function filter( states, modules ) {
                                var list, module, s, m;
-       
+
                                // Allow states to be given as a string
                                if ( typeof states === 'string' ) {
                                        states = [states];
@@ -596,7 +596,7 @@ var mw = ( function ( $, undefined ) {
                                }
                                return list;
                        }
-       
+
                        /**
                         * Automatically executes jobs and modules which are pending with satistifed dependencies.
                         *
@@ -604,7 +604,7 @@ var mw = ( function ( $, undefined ) {
                         */
                        function handlePending( module ) {
                                var j, r;
-       
+
                                try {
                                        // Run jobs whose dependencies have just been met
                                        for ( j = 0; j < jobs.length; j += 1 ) {
@@ -645,7 +645,7 @@ var mw = ( function ( $, undefined ) {
                                        throw e;
                                }
                        }
-       
+
                        /**
                         * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
                         * depending on whether document-ready has occured yet and whether we are in async mode.
@@ -665,7 +665,7 @@ var mw = ( function ( $, undefined ) {
                                        if ( $.isFunction( callback ) ) {
                                                // Attach handlers for all browsers (based on jQuery.ajax)
                                                script.onload = script.onreadystatechange = function() {
-       
+
                                                        if (
                                                                !done
                                                                && (
@@ -673,11 +673,11 @@ var mw = ( function ( $, undefined ) {
                                                                        || /loaded|complete/.test( script.readyState )
                                                                )
                                                        ) {
-       
+
                                                                done = true;
-       
+
                                                                callback();
-       
+
                                                                // Handle memory leak in IE. This seems to fail in
                                                                // IE7 sometimes (Permission Denied error when
                                                                // accessing script.parentNode) so wrap it in
@@ -687,14 +687,14 @@ var mw = ( function ( $, undefined ) {
                                                                        if ( script.parentNode ) {
                                                                                script.parentNode.removeChild( script );
                                                                        }
-               
+
                                                                        // Dereference the script
                                                                        script = undefined;
                                                                } catch ( e ) { }
                                                        }
                                                };
                                        }
-                                       
+
                                        if ( window.opera ) {
                                                // Appending to the <head> blocks rendering completely in Opera,
                                                // so append to the <body> after document ready. This means the
@@ -719,7 +719,7 @@ var mw = ( function ( $, undefined ) {
                                        }
                                }
                        }
-       
+
                        /**
                         * Executes a loaded module, making it ready to use
                         *
@@ -727,7 +727,7 @@ var mw = ( function ( $, undefined ) {
                         */
                        function execute( module, callback ) {
                                var style, media, i, script, markModuleReady, nestedAddScript;
-       
+
                                if ( registry[module] === undefined ) {
                                        throw new Error( 'Module has not been registered yet: ' + module );
                                } else if ( registry[module].state === 'registered' ) {
@@ -737,7 +737,7 @@ var mw = ( function ( $, undefined ) {
                                } else if ( registry[module].state === 'ready' ) {
                                        throw new Error( 'Module has already been loaded: ' + module );
                                }
-       
+
                                // Add styles
                                if ( $.isPlainObject( registry[module].style ) ) {
                                        // 'media' type ignored, see documentation of mw.loader.implement
@@ -778,12 +778,12 @@ var mw = ( function ( $, undefined ) {
                                                        callback();
                                                        return;
                                                }
-       
+
                                                addScript( arr[i], function() {
                                                        nestedAddScript( arr, callback, async, i + 1 );
                                                }, async );
                                        };
-       
+
                                        if ( $.isArray( script ) ) {
                                                registry[module].state = 'loading';
                                                nestedAddScript( script, markModuleReady, registry[module].async, 0 );
@@ -801,7 +801,7 @@ var mw = ( function ( $, undefined ) {
                                        registry[module].state = 'error';
                                }
                        }
-       
+
                        /**
                         * Adds a dependencies to the queue with optional callbacks to be run
                         * when the dependencies are ready or fail
@@ -814,7 +814,7 @@ var mw = ( function ( $, undefined ) {
                         */
                        function request( dependencies, ready, error, async ) {
                                var regItemDeps, regItemDepLen, n;
-       
+
                                // Allow calling by single module name
                                if ( typeof dependencies === 'string' ) {
                                        dependencies = [dependencies];
@@ -856,7 +856,7 @@ var mw = ( function ( $, undefined ) {
                                // Work the queue
                                mw.loader.work();
                        }
-       
+
                        function sortQuery(o) {
                                var sorted = {}, key, a = [];
                                for ( key in o ) {
@@ -870,7 +870,7 @@ var mw = ( function ( $, undefined ) {
                                }
                                return sorted;
                        }
-       
+
                        /**
                         * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
                         * to a query string of the form foo.bar,baz|bar.baz,quux
@@ -883,7 +883,7 @@ var mw = ( function ( $, undefined ) {
                                }
                                return arr.join( '|' );
                        }
-       
+
                        /**
                         * Asynchronously append a script tag to the end of the body
                         * that invokes load.php
@@ -902,7 +902,7 @@ var mw = ( function ( $, undefined ) {
                                // Append &* to avoid triggering the IE6 extension check
                                addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
                        }
-       
+
                        /* Public Methods */
                        return {
                                addStyleTag: addStyleTag,
@@ -915,7 +915,7 @@ var mw = ( function ( $, undefined ) {
                                                source, group, g, i, modules, maxVersion, sourceLoadScript,
                                                currReqBase, currReqBaseLength, moduleMap, l,
                                                lastDotIndex, prefix, suffix, bytesAdded, async;
-               
+
                                        // Build a list of request parameters common to all requests.
                                        reqBase = {
                                                skin: mw.config.get( 'skin' ),
@@ -925,7 +925,7 @@ var mw = ( function ( $, undefined ) {
                                        // Split module batch by source and by group.
                                        splits = {};
                                        maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
-               
+
                                        // Appends a list of modules from the queue to the batch
                                        for ( q = 0; q < queue.length; q += 1 ) {
                                                // Only request modules which are registered
@@ -942,14 +942,14 @@ var mw = ( function ( $, undefined ) {
                                        if ( !batch.length ) {
                                                return;
                                        }
-               
+
                                        // The queue has been processed into the batch, clear up the queue.
                                        queue = [];
-               
+
                                        // Always order modules alphabetically to help reduce cache
                                        // misses for otherwise identical content.
                                        batch.sort();
-               
+
                                        // Split batch by source and by group.
                                        for ( b = 0; b < batch.length; b += 1 ) {
                                                bSource = registry[batch[b]].source;
@@ -963,24 +963,24 @@ var mw = ( function ( $, undefined ) {
                                                bSourceGroup = splits[bSource][bGroup];
                                                bSourceGroup[bSourceGroup.length] = batch[b];
                                        }
-               
+
                                        // Clear the batch - this MUST happen before we append any
                                        // script elements to the body or it's possible that a script
                                        // will be locally cached, instantly load, and work the batch
                                        // again, all before we've cleared it causing each request to
                                        // include modules which are already loaded.
                                        batch = [];
-               
+
                                        for ( source in splits ) {
-               
+
                                                sourceLoadScript = sources[source].loadScript;
-               
+
                                                for ( group in splits[source] ) {
-               
+
                                                        // Cache access to currently selected list of
                                                        // modules for this group from this source.
                                                        modules = splits[source][group];
-               
+
                                                        // Calculate the highest timestamp
                                                        maxVersion = 0;
                                                        for ( g = 0; g < modules.length; g += 1 ) {
@@ -988,16 +988,16 @@ var mw = ( function ( $, undefined ) {
                                                                        maxVersion = registry[modules[g]].version;
                                                                }
                                                        }
-               
+
                                                        currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase );
                                                        currReqBaseLength = $.param( currReqBase ).length;
                                                        async = true;
                                                        // We may need to split up the request to honor the query string length limit,
                                                        // so build it piece by piece.
                                                        l = currReqBaseLength + 9; // '&modules='.length == 9
-               
+
                                                        moduleMap = {}; // { prefix: [ suffixes ] }
-               
+
                                                        for ( i = 0; i < modules.length; i += 1 ) {
                                                                // Determine how many bytes this module would add to the query string
                                                                lastDotIndex = modules[i].lastIndexOf( '.' );
@@ -1007,7 +1007,7 @@ var mw = ( function ( $, undefined ) {
                                                                bytesAdded = moduleMap[prefix] !== undefined
                                                                        ? suffix.length + 3 // '%2C'.length == 3
                                                                        : modules[i].length + 3; // '%7C'.length == 3
-               
+
                                                                // If the request would become too long, create a new one,
                                                                // but don't create empty requests
                                                                if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
@@ -1037,7 +1037,7 @@ var mw = ( function ( $, undefined ) {
                                                }
                                        }
                                },
-               
+
                                /**
                                 * Register a source.
                                 *
@@ -1055,16 +1055,16 @@ var mw = ( function ( $, undefined ) {
                                                }
                                                return true;
                                        }
-               
+
                                        if ( sources[id] !== undefined ) {
                                                throw new Error( 'source already registered: ' + id );
                                        }
-               
+
                                        sources[id] = props;
-               
+
                                        return true;
                                },
-               
+
                                /**
                                 * Registers a module, letting the system know about it and its
                                 * properties. Startup modules contain calls to this function.
@@ -1115,7 +1115,7 @@ var mw = ( function ( $, undefined ) {
                                                registry[module].dependencies = dependencies;
                                        }
                                },
-               
+
                                /**
                                 * Implements a module, giving the system a course of action to take
                                 * upon loading. Results of a request for one or more modules contain
@@ -1171,7 +1171,7 @@ var mw = ( function ( $, undefined ) {
                                                execute( module );
                                        }
                                },
-               
+
                                /**
                                 * Executes a function as soon as one or more required modules are ready
                                 *
@@ -1210,7 +1210,7 @@ var mw = ( function ( $, undefined ) {
                                                request( dependencies, ready, error );
                                        }
                                },
-               
+
                                /**
                                 * Loads an external script or one or more modules for future use
                                 *
@@ -1283,7 +1283,7 @@ var mw = ( function ( $, undefined ) {
                                        request( filtered, null, null, async );
                                        return;
                                },
-               
+
                                /**
                                 * Changes the state of a module
                                 *
@@ -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,9 +1302,16 @@ 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;
+                                       }
                                },
-               
+
                                /**
                                 * Gets the version of a module
                                 *
@@ -1315,14 +1323,14 @@ var mw = ( function ( $, undefined ) {
                                        }
                                        return null;
                                },
-               
+
                                /**
                                 * @deprecated since 1.18 use mw.loader.getVersion() instead
                                 */
                                version: function () {
                                        return mw.loader.getVersion.apply( mw.loader, arguments );
                                },
-               
+
                                /**
                                 * Gets the state of a module
                                 *
@@ -1334,7 +1342,7 @@ var mw = ( function ( $, undefined ) {
                                        }
                                        return null;
                                },
-               
+
                                /**
                                 * Get names of all registered modules.
                                 *
@@ -1345,7 +1353,7 @@ var mw = ( function ( $, undefined ) {
                                                return key;
                                        } );
                                },
-               
+
                                /**
                                 * For backwards-compatibility with Squid-cached pages. Loads mw.user
                                 */
@@ -1354,7 +1362,7 @@ var mw = ( function ( $, undefined ) {
                                }
                        };
                }() ),
-       
+
                /** HTML construction helper functions */
                html: ( function () {
                        function escapeCallback( s ) {
@@ -1380,7 +1388,7 @@ var mw = ( function ( $, undefined ) {
                                escape: function ( s ) {
                                        return s.replace( /['"<>&]/g, escapeCallback );
                                },
-               
+
                                /**
                                 * Wrapper object for raw HTML passed to mw.html.element().
                                 * @constructor
@@ -1388,7 +1396,7 @@ var mw = ( function ( $, undefined ) {
                                Raw: function ( value ) {
                                        this.value = value;
                                },
-               
+
                                /**
                                 * Wrapper object for CDATA element contents passed to mw.html.element()
                                 * @constructor
@@ -1396,7 +1404,7 @@ var mw = ( function ( $, undefined ) {
                                Cdata: function ( value ) {
                                        this.value = value;
                                },
-               
+
                                /**
                                 * Create an HTML element string, with safe escaping.
                                 *
@@ -1418,7 +1426,7 @@ var mw = ( function ( $, undefined ) {
                                 */
                                element: function ( name, attrs, contents ) {
                                        var v, attrName, s = '<' + name;
-               
+
                                        for ( attrName in attrs ) {
                                                v = attrs[attrName];
                                                // Convert name=true, to name=name
@@ -1473,7 +1481,7 @@ var mw = ( function ( $, undefined ) {
                        tokens: new Map()
                }
        };
-       
+
 }( jQuery ) );
 
 // Alias $j to jQuery for backwards compatibility
diff --git a/tests/phpunit/includes/cache/GenderCacheTest.php b/tests/phpunit/includes/cache/GenderCacheTest.php
new file mode 100644 (file)
index 0000000..a8b987e
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @group Database
+ * @group Cache
+ */
+class GenderCacheTest extends MediaWikiLangTestCase {
+
+       function setUp() {
+               global $wgDefaultUserOptions;
+               parent::setUp();
+               //ensure the correct default gender
+               $wgDefaultUserOptions['gender'] = 'unknown';
+       }
+
+       function addDBData() {
+               $user = User::newFromName( 'UTMale' );
+               if( $user->getID() == 0 ) {
+                       $user->addToDatabase();
+                       $user->setPassword( 'UTMalePassword' );
+               }
+               //ensure the right gender
+               $user->setOption( 'gender', 'male' );
+               $user->saveSettings();
+
+               $user = User::newFromName( 'UTFemale' );
+               if( $user->getID() == 0 ) {
+                       $user->addToDatabase();
+                       $user->setPassword( 'UTFemalePassword' );
+               }
+               //ensure the right gender
+               $user->setOption( 'gender', 'female' );
+               $user->saveSettings();
+
+               $user = User::newFromName( 'UTDefaultGender' );
+               if( $user->getID() == 0 ) {
+                       $user->addToDatabase();
+                       $user->setPassword( 'UTDefaultGenderPassword' );
+               }
+               //ensure the default gender
+               $user->setOption( 'gender', null );
+               $user->saveSettings();
+       }
+
+       /**
+        * test usernames
+        *
+        * @dataProvider dataUserName
+        */
+       function testUserName( $username, $expectedGender ) {
+               $genderCache = GenderCache::singleton();
+               $gender = $genderCache->getGenderOf( $username );
+               $this->assertEquals( $gender, $expectedGender, "GenderCache normal" );
+       }
+
+       /**
+        * genderCache should work with user objects, too
+        *
+        * @dataProvider dataUserName
+        */
+       function testUserObjects( $username, $expectedGender ) {
+               $genderCache = GenderCache::singleton();
+               $user = User::newFromName( $username );
+               $gender = $genderCache->getGenderOf( $user );
+               $this->assertEquals( $gender, $expectedGender, "GenderCache normal" );
+       }
+
+       function dataUserName() {
+               return array(
+                       array( 'UTMale', 'male' ),
+                       array( 'UTFemale', 'female' ),
+                       array( 'UTDefaultGender', 'unknown' ),
+                       array( 'UTNotExist', 'unknown' ),
+                       //some not valid user
+                       array( '127.0.0.1', 'unknown' ),
+                       array( 'user@test', 'unknown' ),
+               );
+       }
+
+       /**
+        * test strip of subpages to avoid unnecessary queries
+        * against the never existing username
+        *
+        * @dataProvider dataStripSubpages
+        */
+       function testStripSubpages( $pageWithSubpage, $expectedGender ) {
+               $genderCache = GenderCache::singleton();
+               $gender = $genderCache->getGenderOf( $pageWithSubpage );
+               $this->assertEquals( $gender, $expectedGender, "GenderCache must strip of subpages" );
+       }
+
+       function dataStripSubpages() {
+               return array(
+                       array( 'UTMale/subpage', 'male' ),
+                       array( 'UTFemale/subpage', 'female' ),
+                       array( 'UTDefaultGender/subpage', 'unknown' ),
+                       array( 'UTNotExist/subpage', 'unknown' ),
+                       array( '127.0.0.1/subpage', 'unknown' ),
+               );
+       }
+}
index 2e95f55..af88bc8 100644 (file)
@@ -1022,6 +1022,58 @@ class FileBackendTest extends MediaWikiTestCase {
                }
        }
 
+       public function testRecursiveClean() {
+               $this->backend = $this->singleBackend;
+               $this->doTestRecursiveClean();
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->doTestRecursiveClean();
+               $this->tearDownFiles();
+       }
+
+       function doTestRecursiveClean() {
+               $backendName = $this->backendClass();
+
+               $base = $this->baseStorePath();
+               $dirs = array(
+                       "$base/unittest-cont1/a",
+                       "$base/unittest-cont1/a/b",
+                       "$base/unittest-cont1/a/b/c",
+                       "$base/unittest-cont1/a/b/c/d0",
+                       "$base/unittest-cont1/a/b/c/d1",
+                       "$base/unittest-cont1/a/b/c/d2",
+                       "$base/unittest-cont1/a/b/c/d0/1",
+                       "$base/unittest-cont1/a/b/c/d0/2",
+                       "$base/unittest-cont1/a/b/c/d1/3",
+                       "$base/unittest-cont1/a/b/c/d1/4",
+                       "$base/unittest-cont1/a/b/c/d2/5",
+                       "$base/unittest-cont1/a/b/c/d2/6"
+               );
+               foreach ( $dirs as $dir ) {
+                       $status = $this->prepare( array( 'dir' => $dir ) );
+                       $this->assertEquals( array(), $status->errors,
+                               "Preparing dir $dir succeeded without warnings ($backendName)." );
+               }
+
+               if ( $this->backend instanceof FSFileBackend ) {
+                       foreach ( $dirs as $dir ) {
+                               $this->assertEquals( true, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+                                       "Dir $dir exists ($backendName)." );
+                       }
+               }
+
+               $status = $this->backend->clean(
+                       array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) );
+               $this->assertEquals( array(), $status->errors,
+                       "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." );
+
+               foreach ( $dirs as $dir ) {
+                       $this->assertEquals( false, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+                               "Dir $dir no longer exists ($backendName)." );
+               }
+       }
+
        // @TODO: testSecure
 
        public function testDoOperations() {
@@ -1307,6 +1359,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 +1387,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'];
diff --git a/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php
new file mode 100644 (file)
index 0000000..4a6f08f
--- /dev/null
@@ -0,0 +1,812 @@
+<?php
+
+// It would be great if we were able to use PHPUnit's getMockForAbstractClass
+// instead of the MaintenanceFixup hack below. However, we cannot do
+// without changing the visibility and without working around hacks in
+// Maintenance.php
+//
+// For the same reason, we cannot just use FakeMaintenance.
+
+/**
+ * makes parts of the API of Maintenance that is hidden by protected visibily
+ * visible for testing, and makes up for a stream closing hack in Maintenance.php.
+ *
+ * This class is solely used for being able to test Maintenance right now
+ * without having to apply major refactorings to fix some design issues in
+ * Maintenance.php. Before adding more functions here, please consider whether
+ * this approach is correct, or a refactoring Maintenance to separate concers
+ * is more appropriate.
+ *
+ * Upon refactoring, keep in mind that besides the maintenance scrits themselves
+ * and tests right here, also at least Extension:Maintenance make use of
+ * Maintenance.
+ *
+ * Due to a hack in Maintenance.php using register_shutdown_function, be sure to
+ * finally call simulateShutdown on MaintenanceFixup instance before a test
+ * ends.
+ *
+ */
+class MaintenanceFixup extends Maintenance {
+
+       // --- Making up for the register_shutdown_function hack in Maintenance.php
+
+       /**
+        * The test case that generated this instance.
+        *
+        * This member is motivated by allowing the destructor to check whether or not
+        * the test failed, in order to avoid unnecessary nags about omitted shutdown
+        * simulation.
+        * But as it is already available, we also usi it to flagging tests as failed
+        *
+        * @var MediaWikiTestCase
+        */
+       private $testCase;
+
+       /**
+        * shutdownSimulated === true iff simulateShutdown has done it's work
+        *
+        * @var bool
+        */
+       private $shutdownSimulated = false;
+
+       /**
+        * Simulates what Maintenance wants to happen at script's end.
+        */
+       public function simulateShutdown() {
+
+               if ( $this->shutdownSimulated ) {
+                       $this->testCase->fail( __METHOD__ . " called more than once" );
+               }
+
+               // The cleanup action.
+               $this->outputChanneled( false );
+
+               // Bookkeeping that we simulated the clean up.
+               $this->shutdownSimulated = true;
+       }
+
+       // Note that the "public" here does not change visibility
+       public function outputChanneled( $msg, $channel = null ) {
+               if ( $this->shutdownSimulated ) {
+                       if ( $msg !== false ) {
+                               $this->testCase->fail( "Already past simulated shutdown, but msg is "
+                                       . "not false. Did the hack in Maintenance.php change? Please "
+                                       . "adapt the test case or Maintenance.php" );
+                       }
+
+                       // The current call is the one registered via register_shutdown_function.
+                       // We can safely ignore it, as we simulated this one via simulateShutdown
+                       // before (if we did not, the destructor of this instance will warn about
+                       // it)
+                       return;
+               }
+
+               return call_user_func_array ( array( "parent", __FUNCTION__ ), func_get_args() );
+       }
+
+       /**
+        * Safety net around register_shutdown_function of Maintenance.php
+        */
+       public function __destruct() {
+               if ( ( ! $this->shutdownSimulated ) && ( ! $this->testCase->hasFailed() ) ) {
+                       // Someone generated a MaintenanceFixup instance without calling
+                       // simulateShutdown. We'd have to raise a PHPUnit exception to correctly
+                       // flag this illegal usage. However, we are already in a destruktor, which
+                       // would trigger undefined behaviour. Hence, we can only report to the
+                       // error output :( Hopefully people read the PHPUnit output.
+                       fwrite( STDERR, "ERROR! Instance of " . __CLASS__ . " destructed without "
+                               . "calling simulateShutdown method. Call simulateShutdown on the "
+                               . "instance before it gets destructed." );
+               }
+
+               // The following guard is required, as PHP does not offer default destructors :(
+               if ( is_callable( "parent::__destruct" ) ) {
+                       parent::__destruct();
+               }
+       }
+
+       public function __construct( MediaWikiTestCase $testCase ) {
+               parent::__construct();
+               $this->testCase = $testCase;
+       }
+
+
+
+       // --- Making protected functions visible for test
+
+       public function output( $out, $channel = null ) {
+               // Just to make PHP not nag about signature mismatches, we copied
+               // Maintenance::output signature. However, we do not use (or rely on)
+               // those variables. Instead we pass to Maintenance::output whatever we
+               // receive at runtime.
+               return call_user_func_array ( array( "parent", __FUNCTION__ ), func_get_args() );
+       }
+
+
+
+       // --- Requirements for getting instance of abstract class
+
+       public function execute() {
+               $this->testCase->fail( __METHOD__ . " called unexpectedly" );
+       }
+
+}
+
+class MaintenanceTest extends MediaWikiTestCase {
+
+
+       /**
+        * The main Maintenance instance that is used for testing.
+        *
+        * @var MaintenanceFixup
+        */
+       private $m;
+
+
+       protected function setUp() {
+               parent::setUp();
+               $this->m = new MaintenanceFixup( $this );
+       }
+
+
+       /**
+        * asserts the output before and after simulating shutdown
+        *
+        * This function simulates shutdown of self::m.
+        *
+        * @param $preShutdownOutput string: expected output before simulating shutdown
+        * @param $expectNLAppending bool: Whether or not shutdown simulation is expected
+        *            to add a newline to the output. If false, $preShutdownOutput is the
+        *            expected output after shutdown simulation. Otherwise,
+        *            $preShutdownOutput with an appended newline is the expected output
+        *            after shutdown simulation.
+        */
+       private function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) {
+
+               $this->assertEquals( $preShutdownOutput, $this->getActualOutput(),
+                               "Output before shutdown simulation" );
+
+               $this->m->simulateShutdown();
+
+               $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" );
+               $this->expectOutputString( $postShutdownOutput );
+       }
+
+
+       // Although the following tests do not seem to be too consistent (compare for
+       // example the newlines within the test.*StringString tests, or the
+       // test.*Intermittent.* tests), the objective of these tests is not to describe
+       // consistent behaviour, but rather currently existing behaviour.
+
+
+       function testOutputEmpty() {
+               $this->m->output( "" );
+               $this->assertOutputPrePostShutdown( "", False );
+       }
+
+       function testOutputString() {
+               $this->m->output( "foo" );
+               $this->assertOutputPrePostShutdown( "foo", False );
+       }
+
+       function testOutputStringString() {
+               $this->m->output( "foo" );
+               $this->m->output( "bar" );
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testOutputStringNL() {
+               $this->m->output( "foo\n" );
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testOutputStringNLNL() {
+               $this->m->output( "foo\n\n" );
+               $this->assertOutputPrePostShutdown( "foo\n\n", False );
+       }
+
+       function testOutputStringNLString() {
+               $this->m->output( "foo\nbar" );
+               $this->assertOutputPrePostShutdown( "foo\nbar", False );
+       }
+
+       function testOutputStringNLStringNL() {
+               $this->m->output( "foo\nbar\n" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputStringNLStringNLLinewise() {
+               $this->m->output( "foo\n" );
+               $this->m->output( "bar\n" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputStringNLStringNLArbitrary() {
+               $this->m->output( "" );
+               $this->m->output( "foo" );
+               $this->m->output( "" );
+               $this->m->output( "\n" );
+               $this->m->output( "ba" );
+               $this->m->output( "" );
+               $this->m->output( "r\n" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputStringNLStringNLArbitraryAgain() {
+               $this->m->output( "" );
+               $this->m->output( "foo" );
+               $this->m->output( "" );
+               $this->m->output( "\nb" );
+               $this->m->output( "a" );
+               $this->m->output( "" );
+               $this->m->output( "r\n" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputWNullChannelEmpty() {
+               $this->m->output( "", null );
+               $this->assertOutputPrePostShutdown( "", False );
+       }
+
+       function testOutputWNullChannelString() {
+               $this->m->output( "foo", null );
+               $this->assertOutputPrePostShutdown( "foo", False );
+       }
+
+       function testOutputWNullChannelStringString() {
+               $this->m->output( "foo", null );
+               $this->m->output( "bar", null );
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testOutputWNullChannelStringNL() {
+               $this->m->output( "foo\n", null );
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testOutputWNullChannelStringNLNL() {
+               $this->m->output( "foo\n\n", null );
+               $this->assertOutputPrePostShutdown( "foo\n\n", False );
+       }
+
+       function testOutputWNullChannelStringNLString() {
+               $this->m->output( "foo\nbar", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar", False );
+       }
+
+       function testOutputWNullChannelStringNLStringNL() {
+               $this->m->output( "foo\nbar\n", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputWNullChannelStringNLStringNLLinewise() {
+               $this->m->output( "foo\n", null );
+               $this->m->output( "bar\n", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputWNullChannelStringNLStringNLArbitrary() {
+               $this->m->output( "", null );
+               $this->m->output( "foo", null );
+               $this->m->output( "", null );
+               $this->m->output( "\n", null );
+               $this->m->output( "ba", null );
+               $this->m->output( "", null );
+               $this->m->output( "r\n", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputWNullChannelStringNLStringNLArbitraryAgain() {
+               $this->m->output( "", null );
+               $this->m->output( "foo", null );
+               $this->m->output( "", null );
+               $this->m->output( "\nb", null );
+               $this->m->output( "a", null );
+               $this->m->output( "", null );
+               $this->m->output( "r\n", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputWChannelString() {
+               $this->m->output( "foo", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo", True );
+       }
+
+       function testOutputWChannelStringNL() {
+               $this->m->output( "foo\n", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo", True );
+       }
+
+       function testOutputWChannelStringNLNL() {
+               // If this test fails, note that output takes strings with double line
+               // endings (although output's implementation in this situation calls
+               // outputChanneled with a string ending in a nl ... which is not allowed
+               // according to the documentation of outputChanneled)
+               $this->m->output( "foo\n\n", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\n", True );
+       }
+
+       function testOutputWChannelStringNLString() {
+               $this->m->output( "foo\nbar", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testOutputWChannelStringNLStringNL() {
+               $this->m->output( "foo\nbar\n", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testOutputWChannelStringNLStringNLLinewise() {
+               $this->m->output( "foo\n", "bazChannel" );
+               $this->m->output( "bar\n", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar", True );
+       }
+
+       function testOutputWChannelStringNLStringNLArbitrary() {
+               $this->m->output( "", "bazChannel" );
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->output( "", "bazChannel" );
+               $this->m->output( "\n", "bazChannel" );
+               $this->m->output( "ba", "bazChannel" );
+               $this->m->output( "", "bazChannel" );
+               $this->m->output( "r\n", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar", True );
+       }
+
+       function testOutputWChannelStringNLStringNLArbitraryAgain() {
+               $this->m->output( "", "bazChannel" );
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->output( "", "bazChannel" );
+               $this->m->output( "\nb", "bazChannel" );
+               $this->m->output( "a", "bazChannel" );
+               $this->m->output( "", "bazChannel" );
+               $this->m->output( "r\n", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testOutputWMultipleChannelsChannelChange() {
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->output( "bar", "bazChannel" );
+               $this->m->output( "qux", "quuxChannel" );
+               $this->m->output( "corge", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", True );
+       }
+
+       function testOutputWMultipleChannelsChannelChangeNL() {
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->output( "bar\n", "bazChannel" );
+               $this->m->output( "qux\n", "quuxChannel" );
+               $this->m->output( "corge", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", True );
+       }
+
+       function testOutputWAndWOChannelStringStartWO() {
+               $this->m->output( "foo" );
+               $this->m->output( "bar", "bazChannel" );
+               $this->m->output( "qux" );
+               $this->m->output( "quux", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar\nquxquux", True );
+       }
+
+       function testOutputWAndWOChannelStringStartW() {
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->output( "bar" );
+               $this->m->output( "qux", "bazChannel" );
+               $this->m->output( "quux" );
+               $this->assertOutputPrePostShutdown( "foo\nbarqux\nquux", False );
+       }
+
+       function testOutputWChannelTypeSwitch() {
+               $this->m->output( "foo", 1 );
+               $this->m->output( "bar", 1.0 );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testOutputIntermittentEmpty() {
+               $this->m->output( "foo" );
+               $this->m->output( "" );
+               $this->m->output( "bar" );
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testOutputIntermittentFalse() {
+               $this->m->output( "foo" );
+               $this->m->output( false );
+               $this->m->output( "bar" );
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testOutputIntermittentFalseAfterOtherChannel() {
+               $this->m->output( "qux", "quuxChannel" );
+               $this->m->output( "foo" );
+               $this->m->output( false );
+               $this->m->output( "bar" );
+               $this->assertOutputPrePostShutdown( "qux\nfoobar", False );
+       }
+
+       function testOutputWNullChannelIntermittentEmpty() {
+               $this->m->output( "foo", null );
+               $this->m->output( "", null );
+               $this->m->output( "bar", null );
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testOutputWNullChannelIntermittentFalse() {
+               $this->m->output( "foo", null );
+               $this->m->output( false, null );
+               $this->m->output( "bar", null );
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testOutputWChannelIntermittentEmpty() {
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->output( "", "bazChannel" );
+               $this->m->output( "bar", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar", True );
+       }
+
+       function testOutputWChannelIntermittentFalse() {
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->output( false, "bazChannel" );
+               $this->m->output( "bar", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar", True );
+       }
+
+       // Note that (per documentation) outputChanneled does take strings that end
+       // in \n, hence we do not test such strings.
+
+       function testOutputChanneledEmpty() {
+               $this->m->outputChanneled( "" );
+               $this->assertOutputPrePostShutdown( "\n", False );
+       }
+
+       function testOutputChanneledString() {
+               $this->m->outputChanneled( "foo" );
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testOutputChanneledStringString() {
+               $this->m->outputChanneled( "foo" );
+               $this->m->outputChanneled( "bar" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputChanneledStringNLString() {
+               $this->m->outputChanneled( "foo\nbar" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputChanneledStringNLStringNLArbitraryAgain() {
+               $this->m->outputChanneled( "" );
+               $this->m->outputChanneled( "foo" );
+               $this->m->outputChanneled( "" );
+               $this->m->outputChanneled( "\nb" );
+               $this->m->outputChanneled( "a" );
+               $this->m->outputChanneled( "" );
+               $this->m->outputChanneled( "r" );
+               $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", False );
+       }
+
+       function testOutputChanneledWNullChannelEmpty() {
+               $this->m->outputChanneled( "", null );
+               $this->assertOutputPrePostShutdown( "\n", False );
+       }
+
+       function testOutputChanneledWNullChannelString() {
+               $this->m->outputChanneled( "foo", null );
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testOutputChanneledWNullChannelStringString() {
+               $this->m->outputChanneled( "foo", null );
+               $this->m->outputChanneled( "bar", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputChanneledWNullChannelStringNLString() {
+               $this->m->outputChanneled( "foo\nbar", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputChanneledWNullChannelStringNLStringNLArbitraryAgain() {
+               $this->m->outputChanneled( "", null );
+               $this->m->outputChanneled( "foo", null );
+               $this->m->outputChanneled( "", null );
+               $this->m->outputChanneled( "\nb", null );
+               $this->m->outputChanneled( "a", null );
+               $this->m->outputChanneled( "", null );
+               $this->m->outputChanneled( "r", null );
+               $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", False );
+       }
+
+       function testOutputChanneledWChannelString() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo", True );
+       }
+
+       function testOutputChanneledWChannelStringNLString() {
+               $this->m->outputChanneled( "foo\nbar", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testOutputChanneledWChannelStringString() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( "bar", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar", True );
+       }
+
+       function testOutputChanneledWChannelStringNLStringNLArbitraryAgain() {
+               $this->m->outputChanneled( "", "bazChannel" );
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( "", "bazChannel" );
+               $this->m->outputChanneled( "\nb", "bazChannel" );
+               $this->m->outputChanneled( "a", "bazChannel" );
+               $this->m->outputChanneled( "", "bazChannel" );
+               $this->m->outputChanneled( "r", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testOutputChanneledWMultipleChannelsChannelChange() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( "bar", "bazChannel" );
+               $this->m->outputChanneled( "qux", "quuxChannel" );
+               $this->m->outputChanneled( "corge", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", True );
+       }
+
+       function testOutputChanneledWMultipleChannelsChannelChangeEnclosedNull() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( "bar", null );
+               $this->m->outputChanneled( "qux", null );
+               $this->m->outputChanneled( "corge", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", True );
+       }
+
+       function testOutputChanneledWMultipleChannelsChannelAfterNullChange() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( "bar", null );
+               $this->m->outputChanneled( "qux", null );
+               $this->m->outputChanneled( "corge", "quuxChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", True );
+       }
+
+       function testOutputChanneledWAndWOChannelStringStartWO() {
+               $this->m->outputChanneled( "foo" );
+               $this->m->outputChanneled( "bar", "bazChannel" );
+               $this->m->outputChanneled( "qux" );
+               $this->m->outputChanneled( "quux", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux", True );
+       }
+
+       function testOutputChanneledWAndWOChannelStringStartW() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( "bar" );
+               $this->m->outputChanneled( "qux", "bazChannel" );
+               $this->m->outputChanneled( "quux" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux\n", False );
+       }
+
+       function testOutputChanneledWChannelTypeSwitch() {
+               $this->m->outputChanneled( "foo", 1 );
+               $this->m->outputChanneled( "bar", 1.0 );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testOutputChanneledWOChannelIntermittentEmpty() {
+               $this->m->outputChanneled( "foo" );
+               $this->m->outputChanneled( "" );
+               $this->m->outputChanneled( "bar" );
+               $this->assertOutputPrePostShutdown( "foo\n\nbar\n", False );
+       }
+
+       function testOutputChanneledWOChannelIntermittentFalse() {
+               $this->m->outputChanneled( "foo" );
+               $this->m->outputChanneled( false );
+               $this->m->outputChanneled( "bar" );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputChanneledWNullChannelIntermittentEmpty() {
+               $this->m->outputChanneled( "foo", null );
+               $this->m->outputChanneled( "", null );
+               $this->m->outputChanneled( "bar", null );
+               $this->assertOutputPrePostShutdown( "foo\n\nbar\n", False );
+       }
+
+       function testOutputChanneledWNullChannelIntermittentFalse() {
+               $this->m->outputChanneled( "foo", null );
+               $this->m->outputChanneled( false, null );
+               $this->m->outputChanneled( "bar", null );
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testOutputChanneledWChannelIntermittentEmpty() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( "", "bazChannel" );
+               $this->m->outputChanneled( "bar", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foobar", True );
+       }
+
+       function testOutputChanneledWChannelIntermittentFalse() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->outputChanneled( false, "bazChannel" );
+               $this->m->outputChanneled( "bar", "bazChannel" );
+               $this->assertOutputPrePostShutdown( "foo\nbar", True );
+       }
+
+       function testCleanupChanneledClean() {
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "", False );
+       }
+
+       function testCleanupChanneledAfterOutput() {
+               $this->m->output( "foo" );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo", False );
+       }
+
+       function testCleanupChanneledAfterOutputWNullChannel() {
+               $this->m->output( "foo", null );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo", False );
+       }
+
+       function testCleanupChanneledAfterOutputWChannel() {
+               $this->m->output( "foo", "bazChannel" );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testCleanupChanneledAfterNLOutput() {
+               $this->m->output( "foo\n" );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testCleanupChanneledAfterNLOutputWNullChannel() {
+               $this->m->output( "foo\n", null );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testCleanupChanneledAfterNLOutputWChannel() {
+               $this->m->output( "foo\n", "bazChannel" );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testCleanupChanneledAfterOutputChanneledWOChannel() {
+               $this->m->outputChanneled( "foo" );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testCleanupChanneledAfterOutputChanneledWNullChannel() {
+               $this->m->outputChanneled( "foo", null );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testCleanupChanneledAfterOutputChanneledWChannel() {
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $this->m->cleanupChanneled();
+               $this->assertOutputPrePostShutdown( "foo\n", False );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutput() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->output( "foo" );
+               $m2->output( "bar" );
+
+               $this->assertEquals( "foobar", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->output( "foo", null );
+               $m2->output( "bar", null );
+
+               $this->assertEquals( "foobar", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foobar", False );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutputWChannel() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->output( "foo", "bazChannel" );
+               $m2->output( "bar", "bazChannel" );
+
+               $this->assertEquals( "foobar", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foobar\n", True );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->output( "foo\n", null );
+               $m2->output( "bar\n", null );
+
+               $this->assertEquals( "foo\nbar\n", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->output( "foo\n", "bazChannel" );
+               $m2->output( "bar\n", "bazChannel" );
+
+               $this->assertEquals( "foobar", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foobar\n", True );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutputChanneled() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->outputChanneled( "foo" );
+               $m2->outputChanneled( "bar" );
+
+               $this->assertEquals( "foo\nbar\n", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->outputChanneled( "foo", null );
+               $m2->outputChanneled( "bar", null );
+
+               $this->assertEquals( "foo\nbar\n", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foo\nbar\n", False );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $m2->outputChanneled( "bar", "bazChannel" );
+
+               $this->assertEquals( "foobar", $this->getActualOutput(),
+                               "Output before shutdown simulation (m2)" );
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foobar\n", True );
+       }
+
+       function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() {
+               $m2 = new MaintenanceFixup( $this );
+
+               $this->m->outputChanneled( "foo", "bazChannel" );
+               $m2->outputChanneled( "bar", "bazChannel" );
+
+               $this->assertEquals( "foobar", $this->getActualOutput(),
+                               "Output before first cleanup" );
+               $this->m->cleanupChanneled();
+               $this->assertEquals( "foobar\n", $this->getActualOutput(),
+                               "Output after first cleanup" );
+               $m2->cleanupChanneled();
+               $this->assertEquals( "foobar\n\n", $this->getActualOutput(),
+                               "Output after second cleanup" );
+
+               $m2->simulateShutdown();
+               $this->assertOutputPrePostShutdown( "foobar\n\n", False );
+       }
+
+
+}
\ No newline at end of file
index d18b33b..e3f780b 100644 (file)
@@ -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();
-