Merge "API: Catch Errors as well as Exceptions"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 23 Aug 2018 17:05:19 +0000 (17:05 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 23 Aug 2018 17:05:19 +0000 (17:05 +0000)
91 files changed:
.phpcs.xml
RELEASE-NOTES-1.32
autoload.php
includes/ServiceWiring.php
includes/Storage/BlobStoreFactory.php
includes/Storage/RevisionStore.php
includes/Title.php
includes/api/ApiComparePages.php
includes/api/ApiStashEdit.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/api/i18n/zh-hant.json
includes/changetags/ChangeTags.php
includes/db/DatabaseOracle.php
includes/diff/DifferenceEngine.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/libs/filebackend/fsfile/TempFSFile.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/skins/Skin.php
includes/specials/pagers/UsersPager.php
includes/title/MediaWikiTitleCodec.php
includes/title/TitleValue.php
includes/widget/CheckMatrixWidget.php [new file with mode: 0644]
languages/i18n/ce.json
languages/i18n/cv.json
languages/i18n/da.json
languages/i18n/de-formal.json
languages/i18n/el.json
languages/i18n/es.json
languages/i18n/fr.json
languages/i18n/ja.json
languages/i18n/kn.json
languages/i18n/lt.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/pl.json
languages/i18n/qqq.json
languages/i18n/sah.json
languages/i18n/shi.json
languages/i18n/sr-ec.json
languages/i18n/tr.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/addChangeTag.php [new file with mode: 0644]
maintenance/backup.inc [deleted file]
maintenance/benchmarks/bench_strtr_str_replace.php [deleted file]
maintenance/benchmarks/benchmarkStringReplacement.php [new file with mode: 0644]
maintenance/dumpBackup.php
maintenance/dumpTextPass.php
maintenance/includes/BackupDumper.php [new file with mode: 0644]
maintenance/resources/foreign-resources.yaml [new file with mode: 0644]
maintenance/resources/manageForeignResources.php [new file with mode: 0644]
maintenance/resources/update-oojs.sh [deleted file]
maintenance/resources/update-ooui.sh [deleted file]
resources/Resources.php
resources/lib/jquery.async.js [new file with mode: 0644]
resources/lib/jquery.ba-throttle-debounce.js [new file with mode: 0644]
resources/lib/jquery.cookie.js [new file with mode: 0644]
resources/lib/jquery.form.js [new file with mode: 0644]
resources/lib/jquery.fullscreen.js [new file with mode: 0644]
resources/lib/jquery.hoverIntent.js [new file with mode: 0644]
resources/lib/jquery.jStorage.js [new file with mode: 0644]
resources/lib/jquery.mockjax.js [new file with mode: 0644]
resources/lib/jquery.xmldom.js [new file with mode: 0644]
resources/lib/jquery/jquery.async.js [deleted file]
resources/lib/jquery/jquery.ba-throttle-debounce.js [deleted file]
resources/lib/jquery/jquery.cookie.js [deleted file]
resources/lib/jquery/jquery.form.js [deleted file]
resources/lib/jquery/jquery.fullscreen.js [deleted file]
resources/lib/jquery/jquery.hoverIntent.js [deleted file]
resources/lib/jquery/jquery.jStorage.js [deleted file]
resources/lib/jquery/jquery.mockjax.js [deleted file]
resources/lib/jquery/jquery.xmldom.js [deleted file]
resources/src/jquery/jquery.accessKeyLabel.js
resources/src/mediawiki.inspect.js
resources/src/mediawiki.user.js
resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js [new file with mode: 0644]
tests/parser/ParserTestMockParser.php
tests/phan/config.php
tests/phpunit/includes/api/ApiComparePagesTest.php
tests/phpunit/includes/api/ApiStashEditTest.php
tests/phpunit/includes/title/MediaWikiTitleCodecTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js

index 944c3e2..10394d3 100644 (file)
                <exclude-pattern>*/maintenance/7zip.inc</exclude-pattern>
                <exclude-pattern>*/maintenance/CodeCleanerGlobalsPass.inc</exclude-pattern>
                <exclude-pattern>*/maintenance/archives/upgradeLogging\.php</exclude-pattern>
-               <exclude-pattern>*/maintenance/backup.inc</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_HTTP_HTTPS\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_Wikimedia_base_convert\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_delete_truncate\.php</exclude-pattern>
index 3a1cc88..a7fc232 100644 (file)
@@ -136,6 +136,18 @@ production.
 * action=query&prop=deletedrevisions, action=query&list=allrevisions, and
   action=query&list=alldeletedrevisions are changed similarly to
   &prop=revisions (see the three previous items).
+* (T174032) action=compare now supports multi-content revisions.
+  * It has a 'slots' parameter to select diffing of individual slots. The
+    default behavior is to return one combined diff.
+  * The 'fromtext', 'fromsection', 'fromcontentmodel', 'fromcontentformat',
+    'totext', 'tosection', 'tocontentmodel', and 'tocontentformat' parameters
+    are deprecated. Specify the new 'fromslots' and 'toslots' to identify which
+    slots have text supplied and the corresponding templated parameters for
+    each slot.
+  * The behavior of 'fromsection' and 'tosection' of extracting one section's
+    content is not being preserved. 'fromsection-{slot}' and 'tosection-{slot}'
+    instead expand the given text as if for a section edit. This effectively
+    declines T183823 in favor of T185723.
 
 === Action API internal changes in 1.32 ===
 * Added 'ApiParseMakeOutputPage' hook.
@@ -375,6 +387,8 @@ because of Phabricator reports.
 * All SpecialPageFactory static methods are deprecated. Instead, call the
   methods on a SpecialPageFactory instance, which may be obtained from
   MediaWikiServices.
+* mw.user.stickyRandomId was renamed to the more explicit
+  mw.user.getPageviewToken to better capture its function.
 
 === Other changes in 1.32 ===
 * (T198811) The following tables have had their UNIQUE indexes turned into
index 8679827..cef68b0 100644 (file)
@@ -12,6 +12,7 @@ $wgAutoloadLocalClasses = [
        'ActiveUsersPager' => __DIR__ . '/includes/specials/pagers/ActiveUsersPager.php',
        'ActivityUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/ActivityUpdateJob.php',
        'ActorMigration' => __DIR__ . '/includes/ActorMigration.php',
+       'AddChangeTag' => __DIR__ . '/maintenance/addChangeTag.php',
        'AddRFCandPMIDInterwiki' => __DIR__ . '/maintenance/addRFCandPMIDInterwiki.php',
        'AddSite' => __DIR__ . '/maintenance/addSite.php',
        'AjaxDispatcher' => __DIR__ . '/includes/AjaxDispatcher.php',
@@ -173,7 +174,7 @@ $wgAutoloadLocalClasses = [
        'AvroValidator' => __DIR__ . '/includes/utils/AvroValidator.php',
        'BacklinkCache' => __DIR__ . '/includes/cache/BacklinkCache.php',
        'BacklinkJobUtils' => __DIR__ . '/includes/jobqueue/utils/BacklinkJobUtils.php',
-       'BackupDumper' => __DIR__ . '/maintenance/backup.inc',
+       'BackupDumper' => __DIR__ . '/maintenance/includes/BackupDumper.php',
        'BackupReader' => __DIR__ . '/maintenance/importDump.php',
        'BadRequestError' => __DIR__ . '/includes/exception/BadRequestError.php',
        'BadTitleError' => __DIR__ . '/includes/exception/BadTitleError.php',
@@ -187,7 +188,6 @@ $wgAutoloadLocalClasses = [
        'BcryptPassword' => __DIR__ . '/includes/password/BcryptPassword.php',
        'BenchHttpHttps' => __DIR__ . '/maintenance/benchmarks/bench_HTTP_HTTPS.php',
        'BenchIfSwitch' => __DIR__ . '/maintenance/benchmarks/bench_if_switch.php',
-       'BenchStrtrStrReplace' => __DIR__ . '/maintenance/benchmarks/bench_strtr_str_replace.php',
        'BenchUtf8TitleCheck' => __DIR__ . '/maintenance/benchmarks/bench_utf8_title_check.php',
        'BenchWfIsWindows' => __DIR__ . '/maintenance/benchmarks/bench_wfIsWindows.php',
        'BenchWikimediaBaseConvert' => __DIR__ . '/maintenance/benchmarks/bench_Wikimedia_base_convert.php',
@@ -200,6 +200,7 @@ $wgAutoloadLocalClasses = [
        'BenchmarkParse' => __DIR__ . '/maintenance/benchmarks/benchmarkParse.php',
        'BenchmarkPurge' => __DIR__ . '/maintenance/benchmarks/benchmarkPurge.php',
        'BenchmarkSanitizer' => __DIR__ . '/maintenance/benchmarks/benchmarkSanitizer.php',
+       'BenchmarkStringReplacement' => __DIR__ . '/maintenance/benchmarks/benchmarkStringReplacement.php',
        'BenchmarkTidy' => __DIR__ . '/maintenance/benchmarks/benchmarkTidy.php',
        'BenchmarkTitleValue' => __DIR__ . '/maintenance/benchmarks/benchmarkTitleValue.php',
        'Benchmarker' => __DIR__ . '/maintenance/benchmarks/Benchmarker.php',
@@ -843,6 +844,7 @@ $wgAutoloadLocalClasses = [
        'Maintenance' => __DIR__ . '/maintenance/Maintenance.php',
        'MakeTestEdits' => __DIR__ . '/maintenance/makeTestEdits.php',
        'MalformedTitleException' => __DIR__ . '/includes/title/MalformedTitleException.php',
+       'ManageForeignResources' => __DIR__ . '/maintenance/resources/manageForeignResources.php',
        'ManageJobs' => __DIR__ . '/maintenance/manageJobs.php',
        'ManualLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
        'MapCacheLRU' => __DIR__ . '/includes/libs/MapCacheLRU.php',
@@ -934,6 +936,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Special\\SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php',
        'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php',
        'MediaWiki\\User\\UserIdentityValue' => __DIR__ . '/includes/user/UserIdentityValue.php',
+       'MediaWiki\\Widget\\CheckMatrixWidget' => __DIR__ . '/includes/widget/CheckMatrixWidget.php',
        'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
        'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
        'MediaWiki\\Widget\\DateInputWidget' => __DIR__ . '/includes/widget/DateInputWidget.php',
index 1a19465..99b2942 100644 (file)
@@ -72,7 +72,7 @@ return [
 
        'BlobStoreFactory' => function ( MediaWikiServices $services ) : BlobStoreFactory {
                return new BlobStoreFactory(
-                       $services->getDBLoadBalancer(),
+                       $services->getDBLoadBalancerFactory(),
                        $services->getMainWANObjectCache(),
                        $services->getMainConfig(),
                        $services->getContentLanguage()
index 63ca74d..4e1f97f 100644 (file)
@@ -23,7 +23,7 @@ namespace MediaWiki\Storage;
 use Config;
 use Language;
 use WANObjectCache;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\LBFactory;
 
 /**
  * Service for instantiating BlobStores
@@ -35,9 +35,9 @@ use Wikimedia\Rdbms\LoadBalancer;
 class BlobStoreFactory {
 
        /**
-        * @var LoadBalancer
+        * @var LBFactory
         */
-       private $loadBalancer;
+       private $lbFactory;
 
        /**
         * @var WANObjectCache
@@ -55,12 +55,12 @@ class BlobStoreFactory {
        private $contLang;
 
        public function __construct(
-               LoadBalancer $loadBalancer,
+               LBFactory $lbFactory,
                WANObjectCache $cache,
                Config $mainConfig,
                Language $contLang
        ) {
-               $this->loadBalancer = $loadBalancer;
+               $this->lbFactory = $lbFactory;
                $this->cache = $cache;
                $this->config = $mainConfig;
                $this->contLang = $contLang;
@@ -85,8 +85,9 @@ class BlobStoreFactory {
         * @return SqlBlobStore
         */
        public function newSqlBlobStore( $wikiId = false ) {
+               $lb = $this->lbFactory->getMainLB( $wikiId );
                $store = new SqlBlobStore(
-                       $this->loadBalancer,
+                       $lb,
                        $this->cache,
                        $wikiId
                );
index 5769527..14b1820 100644 (file)
@@ -2174,7 +2174,9 @@ class RevisionStore
                                // NOTE: even when this class is set to not read from the old schema, callers
                                // should still be able to join against the text table, as long as we are still
                                // writing the old schema for compatibility.
-                               wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
+                               // TODO: This should trigger a deprecation warning eventually (T200918), but not
+                               // before all known usages are removed (see T198341 and T201164).
+                               // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
                        }
 
                        $ret['tables'][] = 'text';
index e74824c..c919b18 100644 (file)
@@ -134,8 +134,15 @@ class Title implements LinkTarget {
        /** @var bool Boolean for initialisation on demand */
        public $mRestrictionsLoaded = false;
 
-       /** @var string Text form including namespace/interwiki, initialised on demand */
-       protected $mPrefixedText = null;
+       /**
+        * Text form including namespace/interwiki, initialised on demand
+        *
+        * Only public to share cache with TitleFormatter
+        *
+        * @private
+        * @var string
+        */
+       public $prefixedText = null;
 
        /** @var mixed Cached value for getTitleProtection (create protection) */
        public $mTitleProtection;
@@ -1669,12 +1676,12 @@ class Title implements LinkTarget {
         * @return string The prefixed title, with spaces
         */
        public function getPrefixedText() {
-               if ( $this->mPrefixedText === null ) {
+               if ( $this->prefixedText === null ) {
                        $s = $this->prefix( $this->mTextform );
                        $s = strtr( $s, '_', ' ' );
-                       $this->mPrefixedText = $s;
+                       $this->prefixedText = $s;
                }
-               return $this->mPrefixedText;
+               return $this->prefixedText;
        }
 
        /**
index 93c35d3..6bfa35d 100644 (file)
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+
 class ApiComparePages extends ApiBase {
 
-       private $guessed = false, $guessedTitle, $guessedModel, $props;
+       /** @var RevisionStore */
+       private $revisionStore;
+
+       private $guessedTitle = false, $props;
+
+       public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
+               parent::__construct( $mainModule, $moduleName, $modulePrefix );
+               $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+       }
 
        public function execute() {
                $params = $this->extractRequestParams();
 
                // Parameter validation
-               $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
-               $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
+               $this->requireAtLeastOneParameter(
+                       $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
+               );
+               $this->requireAtLeastOneParameter(
+                       $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots'
+               );
 
                $this->props = array_flip( $params['prop'] );
 
                // Cache responses publicly by default. This may be overridden later.
                $this->getMain()->setCacheMode( 'public' );
 
-               // Get the 'from' Revision and Content
-               list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
+               // Get the 'from' RevisionRecord
+               list( $fromRev, $fromRelRev, $fromValsRev ) = $this->getDiffRevision( 'from', $params );
 
-               // Get the 'to' Revision and Content
+               // Get the 'to' RevisionRecord
                if ( $params['torelative'] !== null ) {
-                       if ( !$relRev ) {
+                       if ( !$fromRelRev ) {
                                $this->dieWithError( 'apierror-compare-relative-to-nothing' );
                        }
                        switch ( $params['torelative'] ) {
                                case 'prev':
                                        // Swap 'from' and 'to'
-                                       $toRev = $fromRev;
-                                       $toContent = $fromContent;
-                                       $fromRev = $relRev->getPrevious();
-                                       $fromContent = $fromRev
-                                               ? $fromRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
-                                               : $toContent->getContentHandler()->makeEmptyContent();
-                                       if ( !$fromContent ) {
-                                               $this->dieWithError(
-                                                       [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
-                                               );
-                                       }
+                                       list( $toRev, $toRelRev2, $toValsRev ) = [ $fromRev, $fromRelRev, $fromValsRev ];
+                                       $fromRev = $this->revisionStore->getPreviousRevision( $fromRelRev );
+                                       $fromRelRev = $fromRev;
+                                       $fromValsRev = $fromRev;
                                        break;
 
                                case 'next':
-                                       $toRev = $relRev->getNext();
-                                       $toContent = $toRev
-                                               ? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
-                                               : $fromContent;
-                                       if ( !$toContent ) {
-                                               $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
-                                       }
+                                       $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
+                                       $toRelRev = $toRev;
+                                       $toValsRev = $toRev;
                                        break;
 
                                case 'cur':
-                                       $title = $relRev->getTitle();
-                                       $id = $title->getLatestRevID();
-                                       $toRev = $id ? Revision::newFromId( $id ) : null;
+                                       $title = $fromRelRev->getPageAsLinkTarget();
+                                       $toRev = $this->revisionStore->getRevisionByTitle( $title );
                                        if ( !$toRev ) {
+                                               $title = Title::newFromLinkTarget( $title );
                                                $this->dieWithError(
                                                        [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
                                                );
                                        }
-                                       $toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
-                                       if ( !$toContent ) {
-                                               $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
-                                       }
+                                       $toRelRev = $toRev;
+                                       $toValsRev = $toRev;
                                        break;
                        }
-                       $relRev2 = null;
                } else {
-                       list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
+                       list( $toRev, $toRelRev, $toValsRev ) = $this->getDiffRevision( 'to', $params );
                }
 
-               // Should never happen, but just in case...
-               if ( !$fromContent || !$toContent ) {
+               // Handle missing from or to revisions
+               if ( !$fromRev || !$toRev ) {
                        $this->dieWithError( 'apierror-baddiff' );
                }
 
-               // Extract sections, if told to
-               if ( isset( $params['fromsection'] ) ) {
-                       $fromContent = $fromContent->getSection( $params['fromsection'] );
-                       if ( !$fromContent ) {
-                               $this->dieWithError(
-                                       [ 'apierror-compare-nosuchfromsection', wfEscapeWikiText( $params['fromsection'] ) ],
-                                       'nosuchfromsection'
-                               );
-                       }
+               // Handle revdel
+               if ( !$fromRev->audienceCan(
+                       RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser()
+               ) ) {
+                       $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' );
                }
-               if ( isset( $params['tosection'] ) ) {
-                       $toContent = $toContent->getSection( $params['tosection'] );
-                       if ( !$toContent ) {
-                               $this->dieWithError(
-                                       [ 'apierror-compare-nosuchtosection', wfEscapeWikiText( $params['tosection'] ) ],
-                                       'nosuchtosection'
-                               );
-                       }
+               if ( !$toRev->audienceCan(
+                       RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser()
+               ) ) {
+                       $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
                }
 
                // Get the diff
                $context = new DerivativeContext( $this->getContext() );
-               if ( $relRev && $relRev->getTitle() ) {
-                       $context->setTitle( $relRev->getTitle() );
-               } elseif ( $relRev2 && $relRev2->getTitle() ) {
-                       $context->setTitle( $relRev2->getTitle() );
+               if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
+                       $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
+               } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
+                       $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
                } else {
-                       $this->guessTitleAndModel();
-                       if ( $this->guessedTitle ) {
-                               $context->setTitle( $this->guessedTitle );
+                       $guessedTitle = $this->guessTitle();
+                       if ( $guessedTitle ) {
+                               $context->setTitle( $guessedTitle );
                        }
                }
-               $de = $fromContent->getContentHandler()->createDifferenceEngine(
-                       $context,
-                       $fromRev ? $fromRev->getId() : 0,
-                       $toRev ? $toRev->getId() : 0,
-                       /* $rcid = */ null,
-                       /* $refreshCache = */ false,
-                       /* $unhide = */ true
-               );
-               $de->setContent( $fromContent, $toContent );
-               $difftext = $de->getDiffBody();
-               if ( $difftext === false ) {
-                       $this->dieWithError( 'apierror-baddiff' );
+               $de = new DifferenceEngine( $context );
+               $de->setRevisions( $fromRev, $toRev );
+               if ( $params['slots'] === null ) {
+                       $difftext = $de->getDiffBody();
+                       if ( $difftext === false ) {
+                               $this->dieWithError( 'apierror-baddiff' );
+                       }
+               } else {
+                       $difftext = [];
+                       foreach ( $params['slots'] as $role ) {
+                               $difftext[$role] = $de->getDiffBodyForRole( $role );
+                       }
                }
 
                // Fill in the response
                $vals = [];
-               $this->setVals( $vals, 'from', $fromRev );
-               $this->setVals( $vals, 'to', $toRev );
+               $this->setVals( $vals, 'from', $fromValsRev );
+               $this->setVals( $vals, 'to', $toValsRev );
 
                if ( isset( $this->props['rel'] ) ) {
-                       if ( $fromRev ) {
-                               $rev = $fromRev->getPrevious();
+                       if ( !$fromRev instanceof MutableRevisionRecord ) {
+                               $rev = $this->revisionStore->getPreviousRevision( $fromRev );
                                if ( $rev ) {
                                        $vals['prev'] = $rev->getId();
                                }
                        }
-                       if ( $toRev ) {
-                               $rev = $toRev->getNext();
+                       if ( !$toRev instanceof MutableRevisionRecord ) {
+                               $rev = $this->revisionStore->getNextRevision( $toRev );
                                if ( $rev ) {
                                        $vals['next'] = $rev->getId();
                                }
@@ -161,10 +156,18 @@ class ApiComparePages extends ApiBase {
                }
 
                if ( isset( $this->props['diffsize'] ) ) {
-                       $vals['diffsize'] = strlen( $difftext );
+                       $vals['diffsize'] = 0;
+                       foreach ( (array)$difftext as $text ) {
+                               $vals['diffsize'] += strlen( $text );
+                       }
                }
                if ( isset( $this->props['diff'] ) ) {
-                       ApiResult::setContentValue( $vals, 'body', $difftext );
+                       if ( is_array( $difftext ) ) {
+                               ApiResult::setArrayType( $difftext, 'kvp', 'diff' );
+                               $vals['bodies'] = $difftext;
+                       } else {
+                               ApiResult::setContentValue( $vals, 'body', $difftext );
+                       }
                }
 
                // Diffs can be really big and there's little point in having
@@ -174,49 +177,55 @@ class ApiComparePages extends ApiBase {
        }
 
        /**
-        * Guess an appropriate default Title and content model for this request
+        * Load a revision by ID
         *
-        * Fills in $this->guessedTitle based on the first of 'fromrev',
-        * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
-        * valid.
+        * Falls back to checking the archive table if appropriate.
+        *
+        * @param int $id
+        * @return RevisionRecord|null
+        */
+       private function getRevisionById( $id ) {
+               $rev = $this->revisionStore->getRevisionById( $id );
+               if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
+                       // Try the 'archive' table
+                       $arQuery = $this->revisionStore->getArchiveQueryInfo();
+                       $row = $this->getDB()->selectRow(
+                               $arQuery['tables'],
+                               array_merge(
+                                       $arQuery['fields'],
+                                       [ 'ar_namespace', 'ar_title' ]
+                               ),
+                               [ 'ar_rev_id' => $id ],
+                               __METHOD__,
+                               [],
+                               $arQuery['joins']
+                       );
+                       if ( $row ) {
+                               $rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
+                               $rev->isArchive = true;
+                       }
+               }
+               return $rev;
+       }
+
+       /**
+        * Guess an appropriate default Title for this request
         *
-        * Fills in $this->guessedModel based on the Revision or Title used to
-        * determine $this->guessedTitle, or the 'fromcontentmodel' or
-        * 'tocontentmodel' parameters if no title was guessed.
+        * @return Title|null
         */
-       private function guessTitleAndModel() {
-               if ( $this->guessed ) {
-                       return;
+       private function guessTitle() {
+               if ( $this->guessedTitle !== false ) {
+                       return $this->guessedTitle;
                }
 
-               $this->guessed = true;
+               $this->guessedTitle = null;
                $params = $this->extractRequestParams();
 
                foreach ( [ 'from', 'to' ] as $prefix ) {
                        if ( $params["{$prefix}rev"] !== null ) {
-                               $revId = $params["{$prefix}rev"];
-                               $rev = Revision::newFromId( $revId );
-                               if ( !$rev ) {
-                                       // Titles of deleted revisions aren't secret, per T51088
-                                       $arQuery = Revision::getArchiveQueryInfo();
-                                       $row = $this->getDB()->selectRow(
-                                               $arQuery['tables'],
-                                               array_merge(
-                                                       $arQuery['fields'],
-                                                       [ 'ar_namespace', 'ar_title' ]
-                                               ),
-                                               [ 'ar_rev_id' => $revId ],
-                                               __METHOD__,
-                                               [],
-                                               $arQuery['joins']
-                                       );
-                                       if ( $row ) {
-                                               $rev = Revision::newFromArchiveRow( $row );
-                                       }
-                               }
+                               $rev = $this->getRevisionById( $params["{$prefix}rev"] );
                                if ( $rev ) {
-                                       $this->guessedTitle = $rev->getTitle();
-                                       $this->guessedModel = $rev->getContentModel();
+                                       $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
                                        break;
                                }
                        }
@@ -238,38 +247,84 @@ class ApiComparePages extends ApiBase {
                        }
                }
 
-               if ( !$this->guessedModel ) {
-                       if ( $this->guessedTitle ) {
-                               $this->guessedModel = $this->guessedTitle->getContentModel();
-                       } elseif ( $params['fromcontentmodel'] !== null ) {
-                               $this->guessedModel = $params['fromcontentmodel'];
-                       } elseif ( $params['tocontentmodel'] !== null ) {
-                               $this->guessedModel = $params['tocontentmodel'];
+               return $this->guessedTitle;
+       }
+
+       /**
+        * Guess an appropriate default content model for this request
+        * @param string $role Slot for which to guess the model
+        * @return string|null Guessed content model
+        */
+       private function guessModel( $role ) {
+               $params = $this->extractRequestParams();
+
+               $title = null;
+               foreach ( [ 'from', 'to' ] as $prefix ) {
+                       if ( $params["{$prefix}rev"] !== null ) {
+                               $rev = $this->getRevisionById( $params["{$prefix}rev"] );
+                               if ( $rev ) {
+                                       if ( $rev->hasSlot( $role ) ) {
+                                               return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
+                                       }
+                               }
+                       }
+               }
+
+               $guessedTitle = $this->guessTitle();
+               if ( $guessedTitle && $role === 'main' ) {
+                       // @todo: Use SlotRoleRegistry and do this for all slots
+                       return $guessedTitle->getContentModel();
+               }
+
+               if ( isset( $params["fromcontentmodel-$role"] ) ) {
+                       return $params["fromcontentmodel-$role"];
+               }
+               if ( isset( $params["tocontentmodel-$role"] ) ) {
+                       return $params["tocontentmodel-$role"];
+               }
+
+               if ( $role === 'main' ) {
+                       if ( isset( $params['fromcontentmodel'] ) ) {
+                               return $params['fromcontentmodel'];
+                       }
+                       if ( isset( $params['tocontentmodel'] ) ) {
+                               return $params['tocontentmodel'];
                        }
                }
+
+               return null;
        }
 
        /**
-        * Get the Revision and Content for one side of the diff
+        * Get the RevisionRecord for one side of the diff
         *
-        * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
-        * 'contentmodel', and 'contentformat' parameters to determine what content
+        * This uses the appropriate set of parameters to determine what content
         * should be diffed.
         *
         * Returns three values:
-        * - The revision used to retrieve the content, if any
-        * - The content to be diffed
-        * - The revision specified, if any, even if not used to retrieve the
-        *   Content
+        * - A RevisionRecord holding the content
+        * - The revision specified, if any, even if content was supplied
+        * - The revision to pass to setVals(), if any
         *
         * @param string $prefix 'from' or 'to'
         * @param array $params
-        * @return array [ Revision|null, Content, Revision|null ]
+        * @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ]
         */
-       private function getDiffContent( $prefix, array $params ) {
+       private function getDiffRevision( $prefix, array $params ) {
+               // Back compat params
+               $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" );
+               $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" );
+               if ( $params["{$prefix}text"] !== null ) {
+                       $params["{$prefix}slots"] = [ 'main' ];
+                       $params["{$prefix}text-main"] = $params["{$prefix}text"];
+                       $params["{$prefix}section-main"] = null;
+                       $params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"];
+                       $params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"];
+               }
+
                $title = null;
                $rev = null;
-               $suppliedContent = $params["{$prefix}text"] !== null;
+               $suppliedContent = $params["{$prefix}slots"] !== null;
 
                // Get the revision and title, if applicable
                $revId = null;
@@ -308,94 +363,146 @@ class ApiComparePages extends ApiBase {
                        }
                }
                if ( $revId !== null ) {
-                       $rev = Revision::newFromId( $revId );
-                       if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
-                               // Try the 'archive' table
-                               $arQuery = Revision::getArchiveQueryInfo();
-                               $row = $this->getDB()->selectRow(
-                                       $arQuery['tables'],
-                                       array_merge(
-                                               $arQuery['fields'],
-                                               [ 'ar_namespace', 'ar_title' ]
-                                       ),
-                                       [ 'ar_rev_id' => $revId ],
-                                       __METHOD__,
-                                       [],
-                                       $arQuery['joins']
-                               );
-                               if ( $row ) {
-                                       $rev = Revision::newFromArchiveRow( $row );
-                                       $rev->isArchive = true;
-                               }
-                       }
+                       $rev = $this->getRevisionById( $revId );
                        if ( !$rev ) {
                                $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
                        }
-                       $title = $rev->getTitle();
+                       $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
 
                        // If we don't have supplied content, return here. Otherwise,
                        // continue on below with the supplied content.
                        if ( !$suppliedContent ) {
-                               $content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
-                               if ( !$content ) {
-                                       $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
+                               $newRev = $rev;
+
+                               // Deprecated 'fromsection'/'tosection'
+                               if ( isset( $params["{$prefix}section"] ) ) {
+                                       $section = $params["{$prefix}section"];
+                                       $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
+                                       $content = $rev->getContent( 'main', RevisionRecord::FOR_THIS_USER, $this->getUser() );
+                                       if ( !$content ) {
+                                               $this->dieWithError(
+                                                       [ 'apierror-missingcontent-revid-role', $rev->getId(), 'main' ], 'missingcontent'
+                                               );
+                                       }
+                                       $content = $content ? $content->getSection( $section ) : null;
+                                       if ( !$content ) {
+                                               $this->dieWithError(
+                                                       [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
+                                                       "nosuch{$prefix}section"
+                                               );
+                                       }
+                                       $newRev->setContent( 'main', $content );
                                }
-                               return [ $rev, $content, $rev ];
+
+                               return [ $newRev, $rev, $rev ];
                        }
                }
 
                // Override $content based on supplied text
-               $model = $params["{$prefix}contentmodel"];
-               $format = $params["{$prefix}contentformat"];
-
-               if ( !$model && $rev ) {
-                       $model = $rev->getContentModel();
-               }
-               if ( !$model && $title ) {
-                       $model = $title->getContentModel();
-               }
-               if ( !$model ) {
-                       $this->guessTitleAndModel();
-                       $model = $this->guessedModel;
-               }
-               if ( !$model ) {
-                       $model = CONTENT_MODEL_WIKITEXT;
-                       $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
-               }
-
                if ( !$title ) {
-                       $this->guessTitleAndModel();
-                       $title = $this->guessedTitle;
+                       $title = $this->guessTitle();
                }
-
-               try {
-                       $content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format );
-               } catch ( MWContentSerializationException $ex ) {
-                       $this->dieWithException( $ex, [
-                               'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
+               if ( $rev ) {
+                       $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
+               } else {
+                       $newRev = $this->revisionStore->newMutableRevisionFromArray( [
+                               'title' => $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __METHOD__ )
                        ] );
                }
+               foreach ( $params["{$prefix}slots"] as $role ) {
+                       $text = $params["{$prefix}text-{$role}"];
+                       if ( $text === null ) {
+                               $newRev->removeSlot( $role );
+                               continue;
+                       }
+
+                       $model = $params["{$prefix}contentmodel-{$role}"];
+                       $format = $params["{$prefix}contentformat-{$role}"];
 
-               if ( $params["{$prefix}pst"] ) {
-                       if ( !$title ) {
-                               $this->dieWithError( 'apierror-compare-no-title' );
+                       if ( !$model && $rev && $rev->hasSlot( $role ) ) {
+                               $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
+                       }
+                       if ( !$model && $title && $role === 'main' ) {
+                               // @todo: Use SlotRoleRegistry and do this for all slots
+                               $model = $title->getContentModel();
+                       }
+                       if ( !$model ) {
+                               $model = $this->guessModel( $role );
+                       }
+                       if ( !$model ) {
+                               $model = CONTENT_MODEL_WIKITEXT;
+                               $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
+                       }
+
+                       try {
+                               $content = ContentHandler::makeContent( $text, $title, $model, $format );
+                       } catch ( MWContentSerializationException $ex ) {
+                               $this->dieWithException( $ex, [
+                                       'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
+                               ] );
+                       }
+
+                       if ( $params["{$prefix}pst"] ) {
+                               if ( !$title ) {
+                                       $this->dieWithError( 'apierror-compare-no-title' );
+                               }
+                               $popts = ParserOptions::newFromContext( $this->getContext() );
+                               $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
+                       }
+
+                       $section = $params["{$prefix}section-{$role}"];
+                       if ( $section !== null && $section !== '' ) {
+                               if ( !$rev ) {
+                                       $this->dieWithError( "apierror-compare-no{$prefix}revision" );
+                               }
+                               $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getUser() );
+                               if ( !$oldContent ) {
+                                       $this->dieWithError(
+                                               [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
+                                               'missingcontent'
+                                       );
+                               }
+                               if ( !$oldContent->getContentHandler()->supportsSections() ) {
+                                       $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
+                               }
+                               try {
+                                       $content = $oldContent->replaceSection( $section, $content, '' );
+                               } catch ( Exception $ex ) {
+                                       // Probably a content model mismatch.
+                                       $content = null;
+                               }
+                               if ( !$content ) {
+                                       $this->dieWithError( [ 'apierror-sectionreplacefailed' ] );
+                               }
+                       }
+
+                       // Deprecated 'fromsection'/'tosection'
+                       if ( $role === 'main' && isset( $params["{$prefix}section"] ) ) {
+                               $section = $params["{$prefix}section"];
+                               $content = $content->getSection( $section );
+                               if ( !$content ) {
+                                       $this->dieWithError(
+                                               [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
+                                               "nosuch{$prefix}section"
+                                       );
+                               }
                        }
-                       $popts = ParserOptions::newFromContext( $this->getContext() );
-                       $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
-               }
 
-               return [ null, $content, $rev ];
+                       $newRev->setContent( $role, $content );
+               }
+               return [ $newRev, $rev, null ];
        }
 
        /**
-        * Set value fields from a Revision object
+        * Set value fields from a RevisionRecord object
+        *
         * @param array &$vals Result array to set data into
         * @param string $prefix 'from' or 'to'
-        * @param Revision|null $rev
+        * @param RevisionRecord|null $rev
         */
        private function setVals( &$vals, $prefix, $rev ) {
                if ( $rev ) {
-                       $title = $rev->getTitle();
+                       $title = $rev->getPageAsLinkTarget();
                        if ( isset( $this->props['ids'] ) ) {
                                $vals["{$prefix}id"] = $title->getArticleID();
                                $vals["{$prefix}revid"] = $rev->getId();
@@ -408,41 +515,42 @@ class ApiComparePages extends ApiBase {
                        }
 
                        $anyHidden = false;
-                       if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+                       if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
                                $vals["{$prefix}texthidden"] = true;
                                $anyHidden = true;
                        }
 
-                       if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
+                       if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
                                $vals["{$prefix}userhidden"] = true;
                                $anyHidden = true;
                        }
-                       if ( isset( $this->props['user'] ) &&
-                               $rev->userCan( Revision::DELETED_USER, $this->getUser() )
-                       ) {
-                               $vals["{$prefix}user"] = $rev->getUserText( Revision::RAW );
-                               $vals["{$prefix}userid"] = $rev->getUser( Revision::RAW );
+                       if ( isset( $this->props['user'] ) ) {
+                               $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getUser() );
+                               if ( $user ) {
+                                       $vals["{$prefix}user"] = $user->getName();
+                                       $vals["{$prefix}userid"] = $user->getId();
+                               }
                        }
 
-                       if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
+                       if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
                                $vals["{$prefix}commenthidden"] = true;
                                $anyHidden = true;
                        }
-                       if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) {
-                               if ( isset( $this->props['comment'] ) ) {
-                                       $vals["{$prefix}comment"] = $rev->getComment( Revision::RAW );
-                               }
-                               if ( isset( $this->props['parsedcomment'] ) ) {
+                       if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) {
+                               $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getUser() );
+                               if ( $comment !== null ) {
+                                       if ( isset( $this->props['comment'] ) ) {
+                                               $vals["{$prefix}comment"] = $comment->text;
+                                       }
                                        $vals["{$prefix}parsedcomment"] = Linker::formatComment(
-                                               $rev->getComment( Revision::RAW ),
-                                               $rev->getTitle()
+                                               $comment->text, Title::newFromLinkTarget( $title )
                                        );
                                }
                        }
 
                        if ( $anyHidden ) {
                                $this->getMain()->setCacheMode( 'private' );
-                               if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
+                               if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
                                        $vals["{$prefix}suppressed"] = true;
                                }
                        }
@@ -455,6 +563,12 @@ class ApiComparePages extends ApiBase {
        }
 
        public function getAllowedParams() {
+               $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap();
+               if ( !in_array( 'main', $slotRoles, true ) ) {
+                       $slotRoles[] = 'main';
+               }
+               sort( $slotRoles, SORT_STRING );
+
                // Parameters for the 'from' and 'to' content
                $fromToParams = [
                        'title' => null,
@@ -464,24 +578,58 @@ class ApiComparePages extends ApiBase {
                        'rev' => [
                                ApiBase::PARAM_TYPE => 'integer'
                        ],
-                       'text' => [
-                               ApiBase::PARAM_TYPE => 'text'
+
+                       'slots' => [
+                               ApiBase::PARAM_TYPE => $slotRoles,
+                               ApiBase::PARAM_ISMULTI => true,
+                       ],
+                       'text-{slot}' => [
+                               ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
+                               ApiBase::PARAM_TYPE => 'text',
+                       ],
+                       'section-{slot}' => [
+                               ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
+                               ApiBase::PARAM_TYPE => 'string',
+                       ],
+                       'contentformat-{slot}' => [
+                               ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                       ],
+                       'contentmodel-{slot}' => [
+                               ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
+                               ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
                        ],
-                       'section' => null,
                        'pst' => false,
+
+                       'text' => [
+                               ApiBase::PARAM_TYPE => 'text',
+                               ApiBase::PARAM_DEPRECATED => true,
+                       ],
                        'contentformat' => [
                                ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                               ApiBase::PARAM_DEPRECATED => true,
                        ],
                        'contentmodel' => [
                                ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
-                       ]
+                               ApiBase::PARAM_DEPRECATED => true,
+                       ],
+                       'section' => [
+                               ApiBase::PARAM_DFLT => null,
+                               ApiBase::PARAM_DEPRECATED => true,
+                       ],
                ];
 
                $ret = [];
                foreach ( $fromToParams as $k => $v ) {
+                       if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
+                               $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots';
+                       }
                        $ret["from$k"] = $v;
                }
                foreach ( $fromToParams as $k => $v ) {
+                       if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
+                               $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots';
+                       }
                        $ret["to$k"] = $v;
                }
 
@@ -508,6 +656,12 @@ class ApiComparePages extends ApiBase {
                        ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
                ];
 
+               $ret['slots'] = [
+                       ApiBase::PARAM_TYPE => $slotRoles,
+                       ApiBase::PARAM_ISMULTI => true,
+                       ApiBase::PARAM_ALL => true,
+               ];
+
                return $ret;
        }
 
index a3e3e57..ab9ae8e 100644 (file)
@@ -234,6 +234,7 @@ class ApiStashEdit extends ApiBase {
                                        return self::ERROR_CACHE;
                                }
                        } else {
+                               // @todo Doesn't seem reachable, see @todo in buildStashValue
                                $logger->info( "Uncacheable parser output for key '{cachekey}' ('{title}') [{code}].",
                                        [ 'cachekey' => $key, 'title' => $titleStr, 'code' => $code ] );
                                return self::ERROR_UNCACHEABLE;
@@ -410,6 +411,9 @@ class ApiStashEdit extends ApiBase {
                }
 
                if ( $ttl <= 0 ) {
+                       // @todo It doesn't seem like this can occur, because it would mean an entry older than
+                       // getCacheExpiry() seconds, which is much longer than PRESUME_FRESH_TTL_SEC, and
+                       // anything older than PRESUME_FRESH_TTL_SEC will have been thrown out already.
                        return [ null, 0, 'no_ttl' ];
                }
 
index 3c74f25..e29ddf9 100644 (file)
        "apihelp-compare-param-fromtitle": "First title to compare.",
        "apihelp-compare-param-fromid": "First page ID to compare.",
        "apihelp-compare-param-fromrev": "First revision to compare.",
-       "apihelp-compare-param-fromtext": "Use this text instead of the content of the revision specified by <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>.",
+       "apihelp-compare-param-frompst": "Do a pre-save transform on <var>fromtext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-fromslots": "Override content of the revision specified by <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>.\n\nThis parameter specifies the slots that are to be modified. Use <var>fromtext-&#x7B;slot}</var>, <var>fromcontentmodel-&#x7B;slot}</var>, and <var>fromcontentformat-&#x7B;slot}</var> to specify content for each slot.",
+       "apihelp-compare-param-fromtext-{slot}": "Text of the specified slot. If omitted, the slot is removed from the revision.",
+       "apihelp-compare-param-fromsection-{slot}": "When <var>fromtext-&#x7B;slot}</var> is the content of a single section, this is the section number. It will be merged into the revision specified by <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var> as if for a section edit.",
+       "apihelp-compare-param-fromcontentmodel-{slot}": "Content model of <var>fromtext-&#x7B;slot}</var>. If not supplied, it will be guessed based on the other parameters.",
+       "apihelp-compare-param-fromcontentformat-{slot}": "Content serialization format of <var>fromtext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-fromtext": "Specify <kbd>fromslots=main</kbd> and use <var>fromtext-main</var> instead.",
+       "apihelp-compare-param-fromcontentmodel": "Specify <kbd>fromslots=main</kbd> and use <var>fromcontentmodel-main</var> instead.",
+       "apihelp-compare-param-fromcontentformat": "Specify <kbd>fromslots=main</kbd> and use <var>fromcontentformat-main</var> instead.",
        "apihelp-compare-param-fromsection": "Only use the specified section of the specified 'from' content.",
-       "apihelp-compare-param-frompst": "Do a pre-save transform on <var>fromtext</var>.",
-       "apihelp-compare-param-fromcontentmodel": "Content model of <var>fromtext</var>. If not supplied, it will be guessed based on the other parameters.",
-       "apihelp-compare-param-fromcontentformat": "Content serialization format of <var>fromtext</var>.",
        "apihelp-compare-param-totitle": "Second title to compare.",
        "apihelp-compare-param-toid": "Second page ID to compare.",
        "apihelp-compare-param-torev": "Second revision to compare.",
        "apihelp-compare-param-torelative": "Use a revision relative to the revision determined from <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>. All of the other 'to' options will be ignored.",
-       "apihelp-compare-param-totext": "Use this text instead of the content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.",
-       "apihelp-compare-param-tosection": "Only use the specified section of the specified 'to' content.",
        "apihelp-compare-param-topst": "Do a pre-save transform on <var>totext</var>.",
-       "apihelp-compare-param-tocontentmodel": "Content model of <var>totext</var>. If not supplied, it will be guessed based on the other parameters.",
-       "apihelp-compare-param-tocontentformat": "Content serialization format of <var>totext</var>.",
+       "apihelp-compare-param-toslots": "Override content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.\n\nThis parameter specifies the slots that are to be modified. Use <var>totext-&#x7B;slot}</var>, <var>tocontentmodel-&#x7B;slot}</var>, and <var>tocontentformat-&#x7B;slot}</var> to specify content for each slot.",
+       "apihelp-compare-param-totext-{slot}": "Text of the specified slot. If omitted, the slot is removed from the revision.",
+       "apihelp-compare-param-tosection-{slot}": "When <var>totext-&#x7B;slot}</var> is the content of a single section, this is the section number. It will be merged into the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var> as if for a section edit.",
+       "apihelp-compare-param-toslots": "Specify content to use instead of the content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.\n\nThis parameter specifies the slots that have content. Use <var>totext-&#x7B;slot}</var>, <var>tocontentmodel-&#x7B;slot}</var>, and <var>tocontentformat-&#x7B;slot}</var> to specify content for each slot.",
+       "apihelp-compare-param-totext-{slot}": "Text of the specified slot.",
+       "apihelp-compare-param-tocontentmodel-{slot}": "Content model of <var>totext-&#x7B;slot}</var>. If not supplied, it will be guessed based on the other parameters.",
+       "apihelp-compare-param-tocontentformat-{slot}": "Content serialization format of <var>totext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-totext": "Specify <kbd>toslots=main</kbd> and use <var>totext-main</var> instead.",
+       "apihelp-compare-param-tocontentmodel": "Specify <kbd>toslots=main</kbd> and use <var>tocontentmodel-main</var> instead.",
+       "apihelp-compare-param-tocontentformat": "Specify <kbd>toslots=main</kbd> and use <var>tocontentformat-main</var> instead.",
+       "apihelp-compare-param-tosection": "Only use the specified section of the specified 'to' content.",
        "apihelp-compare-param-prop": "Which pieces of information to get.",
        "apihelp-compare-paramvalue-prop-diff": "The diff HTML.",
        "apihelp-compare-paramvalue-prop-diffsize": "The size of the diff HTML, in bytes.",
        "apihelp-compare-paramvalue-prop-comment": "The comment on the 'from' and 'to' revisions.",
        "apihelp-compare-paramvalue-prop-parsedcomment": "The parsed comment on the 'from' and 'to' revisions.",
        "apihelp-compare-paramvalue-prop-size": "The size of the 'from' and 'to' revisions.",
+       "apihelp-compare-param-slots": "Return individual diffs for these slots, rather than one combined diff for all slots.",
        "apihelp-compare-example-1": "Create a diff between revision 1 and 2.",
 
        "apihelp-createaccount-summary": "Create a new user account.",
        "apierror-compare-no-title": "Cannot pre-save transform without a title. Try specifying <var>fromtitle</var> or <var>totitle</var>.",
        "apierror-compare-nosuchfromsection": "There is no section $1 in the 'from' content.",
        "apierror-compare-nosuchtosection": "There is no section $1 in the 'to' content.",
+       "apierror-compare-nofromrevision": "No 'from' revision. Specify <var>fromrev</var>, <var>fromtitle</var>, or <var>fromid</var>.",
+       "apierror-compare-notorevision": "No 'to' revision. Specify <var>torev</var>, <var>totitle</var>, or <var>toid</var>.",
        "apierror-compare-relative-to-nothing": "No 'from' revision for <var>torelative</var> to be relative to.",
        "apierror-contentserializationexception": "Content serialization failed: $1",
        "apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
        "apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.",
        "apierror-missingcontent-pageid": "Missing content for page ID $1.",
        "apierror-missingcontent-revid": "Missing content for revision ID $1.",
+       "apierror-missingcontent-revid-role": "Missing content for revision ID $1 for role $2.",
        "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|The parameter|At least one of the parameters}} $1 is required.",
        "apierror-missingparam-one-of": "{{PLURAL:$2|The parameter|One of the parameters}} $1 is required.",
        "apierror-missingparam": "The <var>$1</var> parameter must be set.",
index f158f27..e58683a 100644 (file)
        "apihelp-clientlogin-example-login2": "{{doc-apihelp-example|clientlogin}}",
        "apihelp-compare-summary": "{{doc-apihelp-summary|compare}}",
        "apihelp-compare-extended-description": "{{doc-apihelp-extended-description|compare}}",
-       "apihelp-compare-param-fromtitle": "{{doc-apihelp-param|compare|fromtitle}}",
+       "apihelp-compare-param-fromcontentformat": "{{doc-apihelp-param|compare|fromcontentformat}}",
+       "apihelp-compare-param-fromcontentformat-{slot}": "{{doc-apihelp-param|compare|fromcontentformat-&#x7B;slot} }}",
+       "apihelp-compare-param-fromcontentmodel": "{{doc-apihelp-param|compare|fromcontentmodel}}",
+       "apihelp-compare-param-fromcontentmodel-{slot}": "{{doc-apihelp-param|compare|fromcontentmodel-&#x7B;slot} }}",
        "apihelp-compare-param-fromid": "{{doc-apihelp-param|compare|fromid}}",
+       "apihelp-compare-param-frompst": "{{doc-apihelp-param|compare|frompst}}",
        "apihelp-compare-param-fromrev": "{{doc-apihelp-param|compare|fromrev}}",
-       "apihelp-compare-param-fromtext": "{{doc-apihelp-param|compare|fromtext}}",
        "apihelp-compare-param-fromsection": "{{doc-apihelp-param|compare|fromsection}}",
-       "apihelp-compare-param-frompst": "{{doc-apihelp-param|compare|frompst}}",
-       "apihelp-compare-param-fromcontentmodel": "{{doc-apihelp-param|compare|fromcontentmodel}}",
-       "apihelp-compare-param-fromcontentformat": "{{doc-apihelp-param|compare|fromcontentformat}}",
-       "apihelp-compare-param-totitle": "{{doc-apihelp-param|compare|totitle}}",
+       "apihelp-compare-param-fromsection-{slot}": "{{doc-apihelp-param|compare|fromsection-&#x7B;slot} }}",
+       "apihelp-compare-param-fromslots": "{{doc-apihelp-param|compare|fromslots}}",
+       "apihelp-compare-param-fromtext": "{{doc-apihelp-param|compare|fromtext}}",
+       "apihelp-compare-param-fromtext-{slot}": "{{doc-apihelp-param|compare|fromtext-&#x7B;slot} }}",
+       "apihelp-compare-param-fromtitle": "{{doc-apihelp-param|compare|fromtitle}}",
+       "apihelp-compare-param-tocontentformat": "{{doc-apihelp-param|compare|tocontentformat}}",
+       "apihelp-compare-param-tocontentformat-{slot}": "{{doc-apihelp-param|compare|tocontentformat-&#x7B;slot} }}",
+       "apihelp-compare-param-tocontentmodel": "{{doc-apihelp-param|compare|tocontentmodel}}",
+       "apihelp-compare-param-tocontentmodel-{slot}": "{{doc-apihelp-param|compare|tocontentmodel-&#x7B;slot} }}",
        "apihelp-compare-param-toid": "{{doc-apihelp-param|compare|toid}}",
-       "apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
+       "apihelp-compare-param-topst": "{{doc-apihelp-param|compare|topst}}",
        "apihelp-compare-param-torelative": "{{doc-apihelp-param|compare|torelative}}",
-       "apihelp-compare-param-totext": "{{doc-apihelp-param|compare|totext}}",
+       "apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
        "apihelp-compare-param-tosection": "{{doc-apihelp-param|compare|tosection}}",
-       "apihelp-compare-param-topst": "{{doc-apihelp-param|compare|topst}}",
-       "apihelp-compare-param-tocontentmodel": "{{doc-apihelp-param|compare|tocontentmodel}}",
-       "apihelp-compare-param-tocontentformat": "{{doc-apihelp-param|compare|tocontentformat}}",
+       "apihelp-compare-param-tosection-{slot}": "{{doc-apihelp-param|compare|tosection-&#x7B;slot} }}",
+       "apihelp-compare-param-toslots": "{{doc-apihelp-param|compare|toslots}}",
+       "apihelp-compare-param-totext": "{{doc-apihelp-param|compare|totext}}",
+       "apihelp-compare-param-totext-{slot}": "{{doc-apihelp-param|compare|totext-&#x7B;slot} }}",
+       "apihelp-compare-param-totitle": "{{doc-apihelp-param|compare|totitle}}",
        "apihelp-compare-param-prop": "{{doc-apihelp-param|compare|prop}}",
        "apihelp-compare-paramvalue-prop-diff": "{{doc-apihelp-paramvalue|compare|prop|diff}}",
        "apihelp-compare-paramvalue-prop-diffsize": "{{doc-apihelp-paramvalue|compare|prop|diffsize}}",
        "apihelp-compare-paramvalue-prop-comment": "{{doc-apihelp-paramvalue|compare|prop|comment}}",
        "apihelp-compare-paramvalue-prop-parsedcomment": "{{doc-apihelp-paramvalue|compare|prop|parsedcomment}}",
        "apihelp-compare-paramvalue-prop-size": "{{doc-apihelp-paramvalue|compare|prop|size}}",
+       "apihelp-compare-param-slots": "{{doc-apihelp-param|compare|slots}}",
        "apihelp-compare-example-1": "{{doc-apihelp-example|compare}}",
        "apihelp-createaccount-summary": "{{doc-apihelp-summary|createaccount}}",
        "apihelp-createaccount-param-preservestate": "{{doc-apihelp-param|createaccount|preservestate|info=This message is displayed in addition to {{msg-mw|api-help-authmanagerhelper-preservestate}}.}}",
        "apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
        "apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",
        "apierror-compare-no-title": "{{doc-apierror}}",
+       "apierror-compare-nofromrevision": "{{doc-apierror}}",
        "apierror-compare-nosuchfromsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
        "apierror-compare-nosuchtosection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
+       "apierror-compare-notorevision": "{{doc-apierror}}",
        "apierror-compare-relative-to-nothing": "{{doc-apierror}}",
        "apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
        "apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.",
        "apierror-mimesearchdisabled": "{{doc-apierror}}",
        "apierror-missingcontent-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
        "apierror-missingcontent-revid": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number",
+       "apierror-missingcontent-revid-role": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number\n* $2 - Role name",
        "apierror-missingparam-at-least-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
        "apierror-missingparam-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
        "apierror-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
index 1cced41..82b3ea8 100644 (file)
        "apihelp-block-param-allowusertalk": "允許使用者編輯自己的對話頁面 (依據 <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var> 的設定)。",
        "apihelp-block-param-reblock": "若使用者已被封鎖,覆寫既有的封鎖設定值。",
        "apihelp-block-param-watchuser": "監視使用者或 IP 位址的使用者頁面與對話頁面。",
+       "apihelp-block-param-tags": "在封鎖日誌裡更改套用到項目的標籤。",
        "apihelp-block-example-ip-simple": "封鎖 IP 位址 <kbd>192.0.2.5</kbd> 三天,原因為 <kbd>First strike</kbd>。",
        "apihelp-block-example-user-complex": "永久封鎖 IP 位址 <kbd>Vandal</kbd>,原因為 <kbd>Vandalism</kbd>。",
        "apihelp-changeauthenticationdata-summary": "為目前使用者變更身分核對資料。",
+       "apihelp-changeauthenticationdata-example-password": "嘗試更改目前使用者的密碼至 <kbd>ExamplePassword</kbd>。",
        "apihelp-checktoken-summary": "檢查來自 <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> 的密鑰有效性。",
        "apihelp-checktoken-param-type": "要測試的密鑰類型。",
        "apihelp-checktoken-param-token": "要測試的密鑰。",
        "apihelp-feedrecentchanges-param-feedformat": "摘要格式。",
        "apihelp-feedrecentchanges-param-namespace": "用於限制結果的命名空間。",
        "apihelp-feedrecentchanges-param-invert": "除所選定者外的所有命名空間。",
+       "apihelp-feedrecentchanges-param-days": "用於限制結果的天數。",
        "apihelp-feedrecentchanges-param-limit": "回傳的結果數量上限。",
+       "apihelp-feedrecentchanges-param-from": "顯示自那時以來的更改。",
        "apihelp-feedrecentchanges-param-hideminor": "隱藏小編輯。",
        "apihelp-feedrecentchanges-param-hidebots": "隱藏由機器人做的變更。",
        "apihelp-feedrecentchanges-param-hideanons": "隱藏匿名使用者做的變更。",
        "apihelp-login-example-login": "登入",
        "apihelp-logout-summary": "登出並清除 session 資料。",
        "apihelp-logout-example-logout": "登出當前使用者",
+       "apihelp-managetags-summary": "執行相關到更改標籤的管理任務。",
        "apihelp-managetags-param-tags": "在標籤管理日誌裡更改套用到項目的標籤。",
        "apihelp-mergehistory-summary": "合併頁面歷史",
        "apihelp-mergehistory-param-reason": "合併歷史的原因。",
        "apihelp-paraminfo-param-helpformat": "說明字串的格式。",
        "apihelp-parse-param-summary": "解析摘要。",
        "apihelp-parse-param-pageid": "解析此頁面的內容。覆蓋 <var>$1page</var>。",
+       "apihelp-parse-param-redirects": "若 <var>$1page</var> 或者 <var>$1pageid</var> 被設定成重新導向,則解析它。",
        "apihelp-parse-param-prop": "要取得的資訊部份:",
        "apihelp-parse-paramvalue-prop-headhtml": "取得頁面已解析的 <code>&lt;head&gt;</code>。",
        "apihelp-parse-param-disablepp": "請改用<var>$1disablelimitreport</var>。",
        "apihelp-purge-param-forcelinkupdate": "更新連結表格。",
        "apihelp-purge-example-generator": "重新整理主要命名空間的前10個頁面。",
        "apihelp-query-summary": "擷取來自及有關MediaWiki的數據。",
+       "apihelp-query-param-prop": "替已查詢頁面所要取得的屬性。",
        "apihelp-query-param-list": "要取得的清單。",
        "apihelp-query-param-meta": "要取得的詮釋資料。",
        "apihelp-query+allcategories-summary": "列舉所有分類。",
        "apihelp-query+alldeletedrevisions-param-user": "此列出由該使用者作出的修訂。",
        "apihelp-query+alldeletedrevisions-param-excludeuser": "不要列出由該使用者作出的修訂。",
        "apihelp-query+alldeletedrevisions-param-namespace": "僅列出此命名空間的頁面。",
+       "apihelp-query+allfileusages-param-prefix": "搜尋以此值為開頭的所有檔案標題。",
+       "apihelp-query+allfileusages-param-prop": "要包含到的資訊部份:",
        "apihelp-query+allfileusages-paramvalue-prop-title": "添加檔案標題。",
        "apihelp-query+allfileusages-param-limit": "要回傳的項目總數。",
        "apihelp-query+allfileusages-param-dir": "列出時所採用的方向。",
        "apihelp-query+allfileusages-example-unique": "列出唯一的檔案標題。",
        "apihelp-query+allfileusages-example-unique-generator": "取得所有檔案標題,標記為遺失。",
        "apihelp-query+allfileusages-example-generator": "取得包含檔案的頁面。",
+       "apihelp-query+allimages-summary": "按順序列舉所有圖片。",
        "apihelp-query+allimages-param-sort": "作為排序順序的屬性。",
        "apihelp-query+allimages-param-dir": "列出時所採用的方向。",
        "apihelp-query+allimages-param-minsize": "限制圖片至少要有這樣多的位元組。",
        "apihelp-query+allimages-param-maxsize": "限制圖片最多只能這樣多的位元組。",
+       "apihelp-query+allimages-param-sha1": "圖片的 SHA1 雜湊值。覆蓋 $1sha1base36。",
+       "apihelp-query+allimages-param-sha1base36": "以 base 36 的圖片 SHA1 雜湊值(使用在 MediaWiki)。",
        "apihelp-query+allimages-param-mime": "所要搜尋的 MIME 類型,例如:<kbd>image/jpeg</kbd>。",
        "apihelp-query+allimages-param-limit": "要回傳的圖片總數。",
+       "apihelp-query+allimages-example-B": "搜尋以字母 <kbd>B</kbd> 為開頭的所有檔案清單。",
+       "apihelp-query+allimages-example-recent": "顯示近期已上傳檔案的清單,類似於 [[Special:NewFiles]]。",
+       "apihelp-query+alllinks-param-from": "要起始列舉的連結標題。",
+       "apihelp-query+alllinks-param-to": "要終止列舉的連結標題。",
+       "apihelp-query+alllinks-param-prop": "要包含的資訊部份:",
        "apihelp-query+alllinks-paramvalue-prop-title": "添加連結標題。",
        "apihelp-query+alllinks-param-namespace": "要列舉的命名空間。",
        "apihelp-query+alllinks-param-limit": "要回傳的項目總數。",
        "apihelp-query+alllinks-param-dir": "列出時所採用的方向。",
+       "apihelp-query+alllinks-example-unique": "列出唯一的連結標題。",
+       "apihelp-query+alllinks-example-unique-generator": "取得所有已連結標題,標記為遺失。",
+       "apihelp-query+alllinks-example-generator": "取得包含連結的頁面。",
        "apihelp-query+allmessages-summary": "返回來自該網站的訊息。",
        "apihelp-query+allmessages-param-prop": "要取得的屬性。",
        "apihelp-query+allmessages-param-lang": "以此語言來回傳訊息。",
        "apihelp-query+allmessages-param-from": "以此訊息來回傳訊息開頭。",
        "apihelp-query+allmessages-param-to": "以此訊息來回傳訊息結尾。",
+       "apihelp-query+allmessages-param-prefix": "回傳帶有前綴的訊息。",
+       "apihelp-query+allmessages-example-ipb": "顯示以 <kbd>ipb-</kbd> 起始的訊息。",
+       "apihelp-query+allmessages-example-de": "顯示在德語裡的 <kbd>august</kbd> 與 <kbd>mainpage</kbd> 訊息。",
        "apihelp-query+allpages-param-from": "起始列舉的頁面標題。",
        "apihelp-query+allpages-param-to": "終止列舉的頁面標題。",
        "apihelp-query+allpages-param-prefix": "搜尋以此值為開頭的所有頁面標題。",
        "apihelp-query+allredirects-param-namespace": "要列舉的命名空間。",
        "apihelp-query+allredirects-param-limit": "要回傳的項目總數。",
        "apihelp-query+allredirects-param-dir": "列出時所採用的方向。",
+       "apihelp-query+allredirects-example-unique-generator": "取得所有目標頁面,標記為遺失。",
        "apihelp-query+allredirects-example-generator": "取得包含重新導向的頁面。",
        "apihelp-query+allrevisions-summary": "列出所有修訂版本。",
        "apihelp-query+allrevisions-param-start": "起始列舉的時間戳記。",
        "apihelp-query+allusers-param-prop": "要包含的資訊部份:",
        "apihelp-query+allusers-paramvalue-prop-rights": "列出使用者所擁有的權限。",
        "apihelp-query+allusers-paramvalue-prop-editcount": "添加使用者的編輯次數。",
+       "apihelp-query+allusers-param-witheditsonly": "僅列出有做過編輯的使用者。",
        "apihelp-query+allusers-example-Y": "列出以<kbd>Y</kbd>開頭的使用者。",
        "apihelp-query+authmanagerinfo-summary": "取得目前身分核對狀態的資訊。",
        "apihelp-query+backlinks-summary": "找出連結至指定頁面的所有頁面。",
        "apihelp-query+backlinks-param-namespace": "要列舉的命名空間。",
        "apihelp-query+backlinks-param-dir": "列出時所採用的方向。",
+       "apihelp-query+backlinks-example-simple": "顯示至 <kbd>Main page</kbd> 的連結。",
        "apihelp-query+blocks-summary": "列出所有被封鎖使用者與 IP 位址。",
        "apihelp-query+blocks-param-start": "起始列舉的時間戳記。",
        "apihelp-query+blocks-param-end": "終止列舉的時間戳記。",
        "apihelp-query+blocks-paramvalue-prop-userid": "添加已封鎖使用者的使用者 ID。",
        "apihelp-query+blocks-paramvalue-prop-by": "添加進行封鎖中的使用者之使用者名稱。",
        "apihelp-query+blocks-paramvalue-prop-byid": "添加進行封鎖中的使用者之使用者 ID。",
+       "apihelp-query+blocks-paramvalue-prop-reason": "添加封鎖的原因。",
        "apihelp-query+blocks-example-simple": "列出封鎖。",
        "apihelp-query+blocks-example-users": "列出使用者 <kbd>Alice</kbd> 與 <kbd>Bob</kbd> 的封鎖。",
+       "apihelp-query+categories-summary": "列出頁面隸屬的所有分類。",
+       "apihelp-query+categories-param-show": "要顯示出的分類種類。",
        "apihelp-query+categories-param-limit": "要回傳的分類數量。",
        "apihelp-query+categoryinfo-summary": "回傳有關指定分類的資訊。",
        "apihelp-query+categorymembers-summary": "在指定的分類中列出所有頁面。",
+       "apihelp-query+categorymembers-param-prop": "要包含的資訊部份:",
        "apihelp-query+categorymembers-paramvalue-prop-ids": "添加頁面 ID。",
+       "apihelp-query+categorymembers-paramvalue-prop-title": "添加標題與頁面的命名空間 ID。",
        "apihelp-query+categorymembers-param-limit": "回傳的頁面數量上限。",
+       "apihelp-query+categorymembers-param-sort": "作為排序順序的屬性。",
        "apihelp-query+categorymembers-param-startsortkey": "請改用 $1starthexsortkey。",
        "apihelp-query+categorymembers-param-endsortkey": "請改用 $1endhexsortkey。",
        "apihelp-query+categorymembers-example-simple": "取得在 <kbd>Category:Physics</kbd> 裡前 10 項的頁面。",
        "apihelp-query+contributors-param-limit": "要回傳的貢獻人員數量。",
        "apihelp-query+deletedrevisions-summary": "取得已刪除修訂的資訊。",
+       "apihelp-query+deletedrevisions-param-tag": "僅列出以此標籤所標記的修訂。",
        "apihelp-query+deletedrevisions-param-user": "此列出由該使用者作出的修訂。",
        "apihelp-query+deletedrevisions-param-excludeuser": "不要列出由該使用者作出的修訂。",
        "apihelp-query+deletedrevs-summary": "列出已刪除的修訂。",
        "apihelp-query+deletedrevs-param-end": "終止列舉的時間戳記。",
        "apihelp-query+deletedrevs-param-from": "在此標題開始列出。",
        "apihelp-query+deletedrevs-param-to": "在此標題停止列出。",
+       "apihelp-query+deletedrevs-param-tag": "僅列出以此標籤所標記的修訂。",
        "apihelp-query+deletedrevs-param-user": "此列出由該使用者作出的修訂。",
        "apihelp-query+deletedrevs-param-excludeuser": "不要列出由該使用者作出的修訂。",
        "apihelp-query+deletedrevs-param-namespace": "僅列出此命名空間的頁面。",
        "apihelp-query+embeddedin-param-limit": "要回傳的頁面總數。",
        "apihelp-query+extlinks-summary": "回傳所有指定頁面的外部 URL (非 interwiki)。",
        "apihelp-query+extlinks-param-limit": "要回傳的連結數量。",
+       "apihelp-query+exturlusage-param-prop": "要包含的資訊部份:",
        "apihelp-query+exturlusage-paramvalue-prop-ids": "添加頁面 ID。",
+       "apihelp-query+exturlusage-paramvalue-prop-title": "添加標題與頁面的命名空間 ID。",
        "apihelp-query+exturlusage-paramvalue-prop-url": "添加用於頁面的 URL。",
        "apihelp-query+exturlusage-param-namespace": "要列舉的頁面命名空間。",
        "apihelp-query+exturlusage-param-limit": "要回傳的頁面數量。",
+       "apihelp-query+filearchive-param-from": "起始列舉的圖片標題。",
+       "apihelp-query+filearchive-param-to": "終止列舉的圖片標題。",
        "apihelp-query+filearchive-param-limit": "要回傳的圖片總數。",
        "apihelp-query+filearchive-param-dir": "列出時所採用的方向。",
        "apihelp-query+filearchive-param-sha1": "圖片的 SHA1 雜湊值。覆蓋 $1sha1base36。",
        "apihelp-query+filearchive-param-prop": "要取得的圖片資訊:",
        "apihelp-query+filearchive-paramvalue-prop-sha1": "替圖片添加 SHA-1 雜湊值。",
+       "apihelp-query+filearchive-paramvalue-prop-description": "添加圖片版本的描述。",
+       "apihelp-query+filearchive-paramvalue-prop-parseddescription": "解析版本的描述。",
        "apihelp-query+filearchive-paramvalue-prop-mime": "添加圖片的 MIME。",
        "apihelp-query+filearchive-paramvalue-prop-mediatype": "添加圖片的媒體類型。",
+       "apihelp-query+filearchive-paramvalue-prop-metadata": "列出圖片版本的 Exif 詮釋資料。",
+       "apihelp-query+filearchive-paramvalue-prop-bitdepth": "添加版本的位元深度。",
+       "apihelp-query+filearchive-example-simple": "顯示所有已刪除檔案的清單。",
+       "apihelp-query+filerepoinfo-paramvalue-prop-url": "公共區域 URL 路徑。",
        "apihelp-query+fileusage-param-prop": "要取得的屬性。",
        "apihelp-query+fileusage-paramvalue-prop-pageid": "各頁面的頁面 ID。",
        "apihelp-query+fileusage-paramvalue-prop-title": "各頁面的標題。",
        "apihelp-query+imageusage-param-dir": "列出時所採用的方向。",
        "apihelp-query+info-summary": "取得基本頁面訊息。",
        "apihelp-query+info-param-prop": "要取得的額外屬性:",
+       "apihelp-query+info-paramvalue-prop-protection": "列出各頁面的保護層級。",
        "apihelp-query+info-paramvalue-prop-readable": "使用者是否可閱讀此頁面。",
        "apihelp-query+iwbacklinks-param-prop": "要取得的屬性。",
        "apihelp-query+iwlinks-summary": "回傳指定頁面的所有 interwiki 連結。",
        "apihelp-query+iwlinks-paramvalue-prop-url": "添加完整的 URL。",
        "apihelp-query+iwlinks-param-limit": "要回傳的跨 Wiki 連結數量。",
+       "apihelp-query+iwlinks-param-dir": "列出時所採用的方向。",
        "apihelp-query+langbacklinks-param-limit": "要回傳的頁面總數。",
        "apihelp-query+langbacklinks-param-prop": "要取得的屬性。",
+       "apihelp-query+langbacklinks-param-dir": "列出時所採用的方向。",
        "apihelp-query+langlinks-summary": "回傳指定頁面的所有跨語言連結。",
        "apihelp-query+langlinks-param-limit": "要回傳的 langlinks 數量。",
        "apihelp-query+langlinks-paramvalue-prop-url": "添加完整的 URL。",
        "apihelp-query+links-summary": "回傳指定頁面的所有連結。",
        "apihelp-query+links-param-limit": "要回傳的連結數量。",
        "apihelp-query+linkshere-param-prop": "要取得的屬性。",
+       "apihelp-query+linkshere-paramvalue-prop-pageid": "各頁面的頁面 ID。",
        "apihelp-query+linkshere-paramvalue-prop-title": "各頁面的標題。",
        "apihelp-query+linkshere-paramvalue-prop-redirect": "若頁面為重新導向,則做出標記。",
+       "apihelp-query+linkshere-param-namespace": "僅包含這些命名空間的頁面。",
        "apihelp-query+linkshere-param-limit": "要回傳的數量。",
        "apihelp-query+logevents-summary": "從日誌中獲取事件。",
        "apihelp-query+logevents-param-prop": "要取得的屬性。",
        "apihelp-query+logevents-param-end": "結束列舉的時間戳記。",
        "apihelp-query+logevents-param-limit": "要回傳的事件項目總數。",
        "apihelp-query+pagepropnames-param-limit": "回傳的名稱數量上限。",
+       "apihelp-query+pagepropnames-example-simple": "取得前 10 個屬性名稱。",
        "apihelp-query+pageswithprop-paramvalue-prop-ids": "添加頁面 ID。",
        "apihelp-query+pageswithprop-param-limit": "回傳的頁面數量上限。",
        "apihelp-query+prefixsearch-param-search": "搜尋字串。",
        "apihelp-query+prefixsearch-param-namespace": "搜尋的命名空間。若 <var>$1search</var> 以有效的命名空間前綴為開頭則會被忽略。",
        "apihelp-query+prefixsearch-param-limit": "回傳的結果數量上限。",
        "apihelp-query+prefixsearch-param-offset": "要略過的結果數量。",
+       "apihelp-query+protectedtitles-param-namespace": "僅列出這些命名空間的標題。",
+       "apihelp-query+protectedtitles-param-level": "僅列出具有這些保護層級的標題。",
        "apihelp-query+protectedtitles-param-limit": "要回傳的頁面總數。",
        "apihelp-query+protectedtitles-param-prop": "要取得的屬性。",
+       "apihelp-query+protectedtitles-paramvalue-prop-level": "添加保護層級。",
        "apihelp-query+protectedtitles-example-simple": "列出已保護的標題。",
        "apihelp-query+querypage-param-limit": "回傳的結果數量。",
        "apihelp-query+random-summary": "取得隨機頁面集合",
        "apihelp-query+redirects-param-prop": "要取得的屬性。",
        "apihelp-query+redirects-paramvalue-prop-pageid": "各重新導向的頁面 ID。",
        "apihelp-query+redirects-paramvalue-prop-title": "各重新導向的標題。",
+       "apihelp-query+redirects-param-namespace": "僅包含這些命名空間的頁面。",
        "apihelp-query+redirects-param-limit": "要回傳的重新導向數量。",
        "apihelp-query+revisions-summary": "取得修訂的資訊。",
        "apihelp-query+revisions-example-content": "取得用於標題 <kbd>API</kbd> 與 <kbd>Main Page</kbd> 最新修訂內容的資料。",
        "apihelp-query+search-paramvalue-prop-hasrelated": "已忽略",
        "apihelp-query+search-param-limit": "要回傳的頁面總數。",
        "apihelp-query+siteinfo-paramvalue-prop-general": "全面系統資訊。",
+       "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "特殊頁面別名清單。",
        "apihelp-query+siteinfo-param-numberingroup": "列出在使用者群組裡的使用者數目。",
        "apihelp-query+siteinfo-example-simple": "索取站台資訊。",
        "apihelp-query+siteinfo-example-interwiki": "索取本地端跨 wiki 前綴的清單。",
        "apihelp-query+usercontribs-paramvalue-prop-comment": "添加編輯的註釋。",
        "apihelp-query+usercontribs-paramvalue-prop-parsedcomment": "添加編輯的已解析註解。",
        "apihelp-query+usercontribs-paramvalue-prop-size": "添加編輯的新大小。",
+       "apihelp-query+userinfo-summary": "取得目前使用者的資訊。",
+       "apihelp-query+userinfo-param-prop": "要包含的資訊部份:",
        "apihelp-query+userinfo-paramvalue-prop-realname": "添加使用者的真實姓名。",
        "apihelp-query+userinfo-paramvalue-prop-email": "添加使用者的電子郵件地址與電子郵件驗證日期。",
        "apihelp-query+userinfo-paramvalue-prop-registrationdate": "添加使用者的註冊日期。",
index 9a015f4..dd29c10 100644 (file)
@@ -1183,7 +1183,7 @@ class ChangeTags {
         * Extensions should NOT use this function; they can use the ListDefinedTags
         * hook instead.
         *
-        * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
+        * Includes a call to ChangeTag::canCreateTag(), so your code doesn't need to
         * do that.
         *
         * @param string $tag
index 4977762..876b9bb 100644 (file)
@@ -81,15 +81,6 @@ class DatabaseOracle extends Database {
                return false;
        }
 
-       /**
-        * Usually aborts on failure
-        * @param string $server
-        * @param string $user
-        * @param string $password
-        * @param string $dbName
-        * @throws DBConnectionError
-        * @return resource|null
-        */
        function open( $server, $user, $password, $dbName ) {
                global $wgDBOracleDRCP;
                if ( !function_exists( 'oci_connect' ) ) {
@@ -173,7 +164,7 @@ class DatabaseOracle extends Database {
                $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
                $this->doQuery( 'ALTER SESSION SET NLS_NUMERIC_CHARACTERS=\'.,\'' );
 
-               return $this->conn;
+               return (bool)$this->conn;
        }
 
        /**
index 2ceda21..891f0fe 100644 (file)
@@ -1014,6 +1014,34 @@ class DifferenceEngine extends ContextSource {
                return $difftext;
        }
 
+       /**
+        * Get the diff table body for one slot, without header
+        *
+        * @param string $role
+        * @return string|false
+        */
+       public function getDiffBodyForRole( $role ) {
+               $diffRenderers = $this->getSlotDiffRenderers();
+               if ( !isset( $diffRenderers[$role] ) ) {
+                       return false;
+               }
+
+               $slotContents = $this->getSlotContents();
+               $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
+                       $slotContents[$role]['new'] );
+               if ( !$slotDiff ) {
+                       return false;
+               }
+
+               if ( $role !== 'main' ) {
+                       // TODO use human-readable role name at least
+                       $slotTitle = $role;
+                       $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
+               }
+
+               return $this->localiseDiff( $slotDiff );
+       }
+
        /**
         * Get a slot header for inclusion in a diff body (as a table row).
         *
index da68a62..a679e45 100644 (file)
@@ -129,7 +129,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
                                        $thisAttribs['class'] = 'checkmatrix-forced checkmatrix-forced-on';
                                }
 
-                               $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs );
+                               $checkbox = $this->getOneCheckboxHTML( $checked, $attribs + $thisAttribs );
 
                                $rowContents .= Html::rawElement(
                                        'td',
@@ -148,24 +148,35 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
                return $html;
        }
 
-       protected function getOneCheckbox( $checked, $attribs ) {
-               if ( $this->mParent instanceof OOUIHTMLForm ) {
-                       return new OOUI\CheckboxInputWidget( [
-                               'name' => "{$this->mName}[]",
-                               'selected' => $checked,
-                       ] + OOUI\Element::configFromHtmlAttributes(
-                               $attribs
-                       ) );
-               } else {
-                       $checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs );
-                       if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
-                               $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
-                                       $checkbox .
-                                       Html::element( 'label', [ 'for' => $attribs['id'] ] ) .
-                                       Html::closeElement( 'div' );
-                       }
-                       return $checkbox;
+       public function getInputOOUI( $value ) {
+               $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
+
+               return new MediaWiki\Widget\CheckMatrixWidget(
+                       [
+                               'name' => $this->mName,
+                               'infusable' => true,
+                               'id' => $this->mID,
+                               'rows' => $this->mParams['rows'],
+                               'columns' => $this->mParams['columns'],
+                               'tooltips' => $this->mParams['tooltips'],
+                               'forcedOff' => isset( $this->mParams['force-options-off'] ) ?
+                                       $this->mParams['force-options-off'] : [],
+                               'forcedOn' => isset( $this->mParams['force-options-on'] ) ?
+                                       $this->mParams['force-options-on'] : [],
+                               'values' => $value
+                       ] + OOUI\Element::configFromHtmlAttributes( $attribs )
+               );
+       }
+
+       protected function getOneCheckboxHTML( $checked, $attribs ) {
+               $checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs );
+               if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
+                       $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
+                               $checkbox .
+                               Html::element( 'label', [ 'for' => $attribs['id'] ] ) .
+                               Html::closeElement( 'div' );
                }
+               return $checkbox;
        }
 
        protected function isTagForcedOff( $tag ) {
@@ -262,4 +273,12 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
 
                return $res;
        }
+
+       protected function getOOUIModules() {
+               return [ 'mediawiki.widgets.CheckMatrixWidget' ];
+       }
+
+       protected function shouldInfuseOOUI() {
+               return true;
+       }
 }
index 00d2028..321424f 100644 (file)
@@ -61,7 +61,7 @@ class TempFSFile extends FSFile {
                        if ( !is_string( $tmpDirectory ) ) {
                                $tmpDirectory = self::getUsableTempDirectory();
                        }
-                       $path = wfTempDir() . '/' . $prefix . $hex . $ext;
+                       $path = $tmpDirectory . '/' . $prefix . $hex . $ext;
                        Wikimedia\suppressWarnings();
                        $newFileHandle = fopen( $path, 'x' );
                        Wikimedia\restoreWarnings();
index eba1657..0de90c9 100644 (file)
@@ -178,10 +178,6 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function open( $server, $user, $password, $dbName ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
        public function fetchObject( $res ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
index e35e082..e276d09 100644 (file)
@@ -266,7 +266,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var int[] Prior flags member variable values */
        private $priorFlags = [];
 
-       /** @var object|string Class name or object With profileIn/profileOut methods */
+       /** @var mixed Class name or object With profileIn/profileOut methods */
        protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
@@ -373,6 +373,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
+       /**
+        * Open a new connection to the database (closing any existing one)
+        *
+        * @param string $server Database server host
+        * @param string $user Database user name
+        * @param string $password Database user password
+        * @param string $dbName Database name
+        * @return bool
+        * @throws DBConnectionError
+        */
+       abstract protected function open( $server, $user, $password, $dbName );
+
        /**
         * Construct a Database subclass instance given a database type and parameters
         *
@@ -3496,7 +3508,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                        list( $phpCallback ) = $callback;
                                        $phpCallback( $this );
                                } catch ( Exception $ex ) {
-                                       $this->errorLogger( $ex );
+                                       ( $this->errorLogger )( $ex );
                                        $e = $e ?: $ex;
                                }
                        }
@@ -4018,7 +4030,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * a wrapper. Nowadays, raw database objects are never exposed to external
         * callers, so this is unnecessary in external code.
         *
-        * @param bool|ResultWrapper|resource|object $result
+        * @param bool|ResultWrapper|resource $result
         * @return bool|ResultWrapper
         */
        protected function resultObject( $result ) {
index fed6f14..1246e44 100644 (file)
@@ -77,16 +77,7 @@ class DatabaseMssql extends Database {
                parent::__construct( $params );
        }
 
-       /**
-        * Usually aborts on failure
-        * @param string $server
-        * @param string $user
-        * @param string $password
-        * @param string $dbName
-        * @throws DBConnectionError
-        * @return bool|resource|null
-        */
-       public function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName ) {
                # Test for driver support, to avoid suppressed fatal error
                if ( !function_exists( 'sqlsrv_connect' ) ) {
                        throw new DBConnectionError(
@@ -130,7 +121,7 @@ class DatabaseMssql extends Database {
 
                $this->opened = true;
 
-               return $this->conn;
+               return (bool)$this->conn;
        }
 
        /**
@@ -243,7 +234,7 @@ class DatabaseMssql extends Database {
        }
 
        /**
-        * @param MssqlResultWrapper $res
+        * @param IResultWrapper $res
         * @return stdClass
         */
        public function fetchObject( $res ) {
@@ -252,7 +243,7 @@ class DatabaseMssql extends Database {
        }
 
        /**
-        * @param MssqlResultWrapper $res
+        * @param IResultWrapper $res
         * @return array
         */
        public function fetchRow( $res ) {
index 57fab54..0f57551 100644 (file)
@@ -120,15 +120,7 @@ abstract class DatabaseMysqlBase extends Database {
                return 'mysql';
        }
 
-       /**
-        * @param string $server
-        * @param string $user
-        * @param string $password
-        * @param string $dbName
-        * @throws Exception|DBConnectionError
-        * @return bool
-        */
-       public function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName ) {
                # Close/unset connection handle
                $this->close();
 
index a959d72..3c2f145 100644 (file)
@@ -86,7 +86,7 @@ class DatabasePostgres extends Database {
                return false;
        }
 
-       public function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName ) {
                # Test for Postgres support, to avoid suppressed fatal error
                if ( !function_exists( 'pg_connect' ) ) {
                        throw new DBConnectionError(
index 25fbba0..1b9675a 100644 (file)
@@ -155,24 +155,14 @@ class DatabaseSqlite extends Database {
                return false;
        }
 
-       /** Open an SQLite database and return a resource handle to it
-        *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
-        *
-        * @param string $server
-        * @param string $user Unused
-        * @param string $pass
-        * @param string $dbName
-        *
-        * @throws DBConnectionError
-        * @return bool
-        */
-       function open( $server, $user, $pass, $dbName ) {
+       protected function open( $server, $user, $pass, $dbName ) {
                $this->close();
                $fileName = self::generateFileName( $this->dbDir, $dbName );
                if ( !is_readable( $fileName ) ) {
                        $this->conn = false;
                        throw new DBConnectionError( $this, "SQLite database not accessible" );
                }
+               // Only $dbName is used, the other parameters are irrelevant for SQLite databases
                $this->openFile( $fileName, $dbName );
 
                return (bool)$this->conn;
index 7da259d..f97db3a 100644 (file)
@@ -370,18 +370,6 @@ interface IDatabase {
         */
        public function getType();
 
-       /**
-        * Open a new connection to the database (closing any existing one)
-        *
-        * @param string $server Database server host
-        * @param string $user Database user name
-        * @param string $password Database user password
-        * @param string $dbName Database name
-        * @return bool
-        * @throws DBConnectionError
-        */
-       public function open( $server, $user, $password, $dbName );
-
        /**
         * Fetch the next row from the given result object, in object form.
         * Fields can be retrieved with $row->fieldname, with fields acting like
index fbc3be9..00b4130 100644 (file)
@@ -938,10 +938,6 @@ class LoadBalancer implements ILoadBalancer {
                        $server = $this->servers[$i];
                        $server['serverIndex'] = $i;
                        $server['autoCommitOnly'] = $autoCommit;
-                       if ( $this->localDomain->getDatabase() !== null ) {
-                               // Use the local domain table prefix if the local domain is specified
-                               $server['tablePrefix'] = $this->localDomain->getTablePrefix();
-                       }
                        $conn = $this->reallyOpenConnection( $server, $this->localDomain );
                        $host = $this->getServerName( $i );
                        if ( $conn->isOpen() ) {
@@ -1037,7 +1033,6 @@ class LoadBalancer implements ILoadBalancer {
                                $this->errorConnection = $conn;
                                $conn = false;
                        } else {
-                               $conn->tablePrefix( $prefix ); // as specified
                                // Note that if $domain is an empty string, getDomainID() might not match it
                                $this->conns[$connInUseKey][$i][$conn->getDomainID()] = $conn;
                                $this->connLogger->debug( __METHOD__ . ": opened new connection for $i/$domain" );
@@ -1081,20 +1076,20 @@ class LoadBalancer implements ILoadBalancer {
         * Returns a Database object whether or not the connection was successful.
         *
         * @param array $server
-        * @param DatabaseDomain $domainOverride Use an unspecified domain to not select any database
+        * @param DatabaseDomain $domain Domain the connection is for, possibly unspecified
         * @return Database
         * @throws DBAccessError
         * @throws InvalidArgumentException
         */
-       protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) {
+       protected function reallyOpenConnection( array $server, DatabaseDomain $domain ) {
                if ( $this->disabled ) {
                        throw new DBAccessError();
                }
 
-               // Handle $domainOverride being a specified or an unspecified domain
-               if ( $domainOverride->getDatabase() === null ) {
-                       // Normally, an RDBMS requires a DB name specified on connection and the $server
-                       // configuration array is assumed to already specify an appropriate DB name.
+               if ( $domain->getDatabase() === null ) {
+                       // The database domain does not specify a DB name and some database systems require a
+                       // valid DB specified on connection. The $server configuration array contains a default
+                       // DB name to use for connections in such cases.
                        if ( $server['type'] === 'mysql' ) {
                                // For MySQL, DATABASE and SCHEMA are synonyms, connections need not specify a DB,
                                // and the DB name in $server might not exist due to legacy reasons (the default
@@ -1102,10 +1097,16 @@ class LoadBalancer implements ILoadBalancer {
                                $server['dbname'] = null;
                        }
                } else {
-                       $server['dbname'] = $domainOverride->getDatabase();
-                       $server['schema'] = $domainOverride->getSchema();
+                       $server['dbname'] = $domain->getDatabase();
+               }
+
+               if ( $domain->getSchema() !== null ) {
+                       $server['schema'] = $domain->getSchema();
                }
 
+               // It is always possible to connect with any prefix, even the empty string
+               $server['tablePrefix'] = $domain->getTablePrefix();
+
                // Let the handle know what the cluster master is (e.g. "db1052")
                $masterName = $this->getServerName( $this->getWriterIndex() );
                $server['clusterMasterHost'] = $masterName;
index 43b3ec1..c603f2f 100644 (file)
@@ -1472,7 +1472,7 @@ abstract class Skin extends ContextSource {
                                $uTalkTitle,
                                $this->msg( 'newmessageslinkplural' )->params( $plural )->escaped(),
                                [],
-                               [ 'redirect' => 'no' ]
+                               $uTalkTitle->isRedirect() ? [ 'redirect' => 'no' ] : []
                        );
 
                        $newMessagesDiffLink = Linker::linkKnown(
index aa757e6..bc24d26 100644 (file)
@@ -333,7 +333,7 @@ class UsersPager extends AlphabeticPager {
                Hooks::run( 'SpecialListusersHeaderForm', [ $this, &$beforeSubmitButtonHookOut ] );
 
                if ( $beforeSubmitButtonHookOut !== '' ) {
-                       $formDescriptior[ 'beforeSubmitButtonHookOut' ] = [
+                       $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
                                'class' => HTMLInfoField::class,
                                'raw' => true,
                                'default' => $beforeSubmitButtonHookOut
@@ -349,7 +349,7 @@ class UsersPager extends AlphabeticPager {
                Hooks::run( 'SpecialListusersHeader', [ $this, &$beforeClosingFieldsetHookOut ] );
 
                if ( $beforeClosingFieldsetHookOut !== '' ) {
-                       $formDescriptior[ 'beforeClosingFieldsetHookOut' ] = [
+                       $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
                                'class' => HTMLInfoField::class,
                                'raw' => true,
                                'default' => $beforeClosingFieldsetHookOut
index 15f8ff0..f6a4c06 100644 (file)
@@ -79,7 +79,7 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @param string $text
         *
         * @throws InvalidArgumentException If the namespace is invalid
-        * @return string
+        * @return string Namespace name with underscores (not spaces)
         */
        public function getNamespaceName( $namespace, $text ) {
                if ( $this->language->needsGenderDistinction() &&
@@ -112,29 +112,30 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @return string
         */
        public function formatTitle( $namespace, $text, $fragment = '', $interwiki = '' ) {
-               if ( $namespace !== 0 && $namespace !== false ) {
-                       // Try to get a namespace name, but fallback
-                       // to empty string if it doesn't exist. And
-                       // assume that ns 0 is the empty string.
+               $out = '';
+               if ( $interwiki !== '' ) {
+                       $out = $interwiki . ':';
+               }
+
+               if ( $namespace != 0 ) {
                        try {
                                $nsName = $this->getNamespaceName( $namespace, $text );
                        } catch ( InvalidArgumentException $e ) {
-                               $nsName = '';
+                               // See T165149. Awkward, but better than erroneously linking to the main namespace.
+                               $nsName = $this->language->getNsText( NS_SPECIAL ) . ":Badtitle/NS{$namespace}";
                        }
-                       $text = $nsName . ':' . $text;
-               }
 
-               if ( $fragment !== '' ) {
-                       $text = $text . '#' . $fragment;
+                       $out .= $nsName . ':';
                }
+               $out .= $text;
 
-               if ( $interwiki !== '' ) {
-                       $text = $interwiki . ':' . $text;
+               if ( $fragment !== '' ) {
+                       $out .= '#' . $fragment;
                }
 
-               $text = str_replace( '_', ' ', $text );
+               $out = str_replace( '_', ' ', $out );
 
-               return $text;
+               return $out;
        }
 
        /**
@@ -185,12 +186,16 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @return string
         */
        public function getPrefixedText( LinkTarget $title ) {
-               return $this->formatTitle(
-                       $title->getNamespace(),
-                       $title->getText(),
-                       '',
-                       $title->getInterwiki()
-               );
+               if ( !isset( $title->prefixedText ) ) {
+                       $title->prefixedText = $this->formatTitle(
+                               $title->getNamespace(),
+                               $title->getText(),
+                               '',
+                               $title->getInterwiki()
+                       );
+               }
+
+               return $title->prefixedText;
        }
 
        /**
@@ -200,28 +205,12 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @return string
         */
        public function getPrefixedDBkey( LinkTarget $target ) {
-               $key = '';
-               if ( $target->isExternal() ) {
-                       $key .= $target->getInterwiki() . ':';
-               }
-               // Try to get a namespace name, but fallback
-               // to empty string if it doesn't exist
-               try {
-                       $nsName = $this->getNamespaceName(
-                               $target->getNamespace(),
-                               $target->getText()
-                       );
-               } catch ( InvalidArgumentException $e ) {
-                       $nsName = '';
-               }
-
-               if ( $target->getNamespace() !== 0 ) {
-                       $key .= $nsName . ':';
-               }
-
-               $key .= $target->getText();
-
-               return strtr( $key, ' ', '_' );
+               return strtr( $this->formatTitle(
+                       $target->getNamespace(),
+                       $target->getDBkey(),
+                       '',
+                       $target->getInterwiki()
+               ), ' ', '_' );
        }
 
        /**
index 43a399a..698bc4f 100644 (file)
@@ -59,6 +59,16 @@ class TitleValue implements LinkTarget {
         */
        protected $interwiki;
 
+       /**
+        * Text form including namespace/interwiki, initialised on demand
+        *
+        * Only public to share cache with TitleFormatter
+        *
+        * @private
+        * @var string
+        */
+       public $prefixedText = null;
+
        /**
         * Constructs a TitleValue.
         *
diff --git a/includes/widget/CheckMatrixWidget.php b/includes/widget/CheckMatrixWidget.php
new file mode 100644 (file)
index 0000000..7783f31
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * Check matrix widget. Displays a matrix of checkboxes for given options
+ *
+ * @copyright 2018 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class CheckMatrixWidget extends \OOUI\Widget {
+
+       protected $name = '';
+       protected $columns = [];
+       protected $rows = [];
+       protected $tooltips = [];
+       protected $values = [];
+       protected $forcedOn = [];
+       protected $forcedOff = [];
+
+       /**
+        * CheckMatrixWidget constructor
+        *
+        * Operates similarly to MultiSelectWidget, but instead of using an array of
+        * options, uses an array of rows and an array of columns to dynamically
+        * construct a matrix of options. The tags used to identify a particular cell
+        * are of the form "columnName-rowName"
+        *
+        * @param array $config Configuration array with the following options:
+        *   - columns
+        *     - Required list of columns in the matrix.
+        *   - rows
+        *     - Required list of rows in the matrix.
+        *   - force-options-on
+        *     - Accepts array of column-row tags to be displayed as enabled but unavailable to change
+        *   - force-options-off
+        *     - Accepts array of column-row tags to be displayed as disabled but unavailable to change.
+        *   - tooltips
+        *     - Optional array mapping row label to tooltip content
+        *   - tooltip-class
+        *     - Optional CSS class used on tooltip container span. Defaults to mw-icon-question.
+        */
+       public function __construct( array $config = [] ) {
+               // Configuration initialization
+
+               parent::__construct( $config );
+
+               $this->name = isset( $config['name'] ) ?
+                       $config[ 'name' ] : null;
+               $this->id = isset( $config['id'] ) ?
+                       $config['id'] : null;
+
+               // Properties
+               $this->rows = isset( $config['rows'] ) ?
+                       $config['rows'] : [];
+               $this->columns = isset( $config['columns'] ) ?
+                       $config['columns'] : [];
+               $this->tooltips = isset( $config['tooltips'] ) ?
+                       $config['tooltips'] : [];
+
+               $this->values = isset( $config['values'] ) ?
+                       $config['values'] : [];
+
+               $this->forcedOn = isset( $config['forcedOn'] ) ?
+                       $config['forcedOn'] : [];
+               $this->forcedOff = isset( $config['forcedOff'] ) ?
+                       $config['forcedOff'] : [];
+
+               // Build the table
+               $table = new \OOUI\Tag( 'table' );
+               $tr = new \OOUI\Tag( 'tr' );
+               // Build the header
+               $tr->appendContent( $this->getCellTag( "\u{00A0}" ) );
+               foreach ( $this->columns as $columnLabel => $columnTag ) {
+                       $tr->appendContent(
+                               $this->getCellTag( $columnLabel )
+                       );
+               }
+               $table->appendContent( $tr );
+
+               // Build the options matrix
+               foreach ( $this->rows as $rowLabel => $rowTag ) {
+                       $table->appendContent(
+                               $this->getTableRow( $rowLabel, $rowTag )
+                       );
+               }
+
+               // Initialization
+               $this->addClasses( [ 'mw-widget-checkMatrixWidget' ] );
+               $this->appendContent( $table );
+       }
+
+       /**
+        * Get a formatted table row for the option, with
+        * a checkbox widget.
+        *
+        * @param  string $label Row label
+        * @param  string $tag   Row tag name
+        * @return \OOUI\Tag The resulting table row
+        */
+       private function getTableRow( $label, $tag ) {
+               $row = new \OOUI\Tag( 'tr' );
+               $tooltip = $this->getTooltip( $label );
+               $labelFieldConfig = $tooltip ? [ 'help' => $tooltip ] : [];
+               // Build label cell
+               $labelField = new \OOUI\FieldLayout(
+                       new \OOUI\Widget(), // Empty widget, since we don't have the checkboxes here
+                       [
+                               'label' => $label,
+                               'align' => 'inline',
+                       ] + $labelFieldConfig
+               );
+               $row->appendContent( $this->getCellTag( $labelField ) );
+
+               // Build checkbox column cells
+               foreach ( $this->columns as $columnTag ) {
+                       $thisTag = "$columnTag-$tag";
+
+                       // Construct a checkbox
+                       $checkbox = new \OOUI\CheckboxInputWidget( [
+                               'value' => $thisTag,
+                               'name' => $this->name ? "{$this->name}[]" : null,
+                               'id' => $this->id ? "{$this->id}-$thisTag" : null,
+                               'selected' => $this->isTagChecked( $thisTag ),
+                               'disabled' => $this->isTagDisabled( $thisTag ),
+                       ] );
+
+                       $row->appendContent( $this->getCellTag( $checkbox ) );
+               }
+               return $row;
+       }
+
+       /**
+        * Get an individual cell tag with requested content
+        *
+        * @param  string $content Content for the <td> cell
+        * @return \OOUI\Tag Resulting cell
+        */
+       private function getCellTag( $content ) {
+               $cell = new \OOUI\Tag( 'td' );
+               $cell->appendContent( $content );
+               return $cell;
+       }
+
+       /**
+        * Check whether the given tag's checkbox should
+        * be checked
+        *
+        * @param  string $tagName Tag name
+        * @return boolean Tag should be checked
+        */
+       private function isTagChecked( $tagName ) {
+               // If the tag is in the value list
+               return in_array( $tagName, (array)$this->values, true ) ||
+                       // Or if the tag is forced on
+                       in_array( $tagName, (array)$this->forcedOn, true );
+       }
+
+       /**
+        * Check whether the given tag's checkbox should
+        * be disabled
+        *
+        * @param  string $tagName Tag name
+        * @return boolean Tag should be disabled
+        */
+       private function isTagDisabled( $tagName ) {
+               return (
+                       // If the entire widget is disabled
+                       $this->isDisabled() ||
+                       // If the tag is 'forced on' or 'forced off'
+                       in_array( $tagName, (array)$this->forcedOn, true ) ||
+                       in_array( $tagName, (array)$this->forcedOff, true )
+               );
+       }
+
+       /**
+        * Get the tooltip help associated with this row
+        *
+        * @param  string $label Label name
+        * @return string Tooltip. Null if none is available.
+        */
+       private function getTooltip( $label ) {
+               return isset( $this->tooltips[ $label ] ) ?
+                       $this->tooltips[ $label ] : null;
+       }
+
+       protected function getJavaScriptClassName() {
+               return 'mw.widgets.CheckMatrixWidget';
+       }
+
+       public function getConfig( &$config ) {
+               $config += [
+                       'name' => $this->name,
+                       'id' => $this->id,
+                       'rows' => $this->rows,
+                       'columns' => $this->columns,
+                       'tooltips' => $this->tooltips,
+                       'forcedOff' => $this->forcedOff,
+                       'forcedOn' => $this->forcedOn,
+                       'values' => $this->values,
+               ];
+               return parent::getConfig( $config );
+       }
+}
index a74dd08..f95f33a 100644 (file)
        "anonymous": "{{PLURAL:$1|1=ЦӀе хьулйина декъашхо|ЦӀе хьулйина декъашхой}} {{grammar:genitive|{{SITENAME}}}}",
        "siteuser": "декъашхо {{grammar:genitive|{{SITENAME}}}} $1",
        "anonuser": "цӀе хьулйина декъашхо {{grammar:genitive|{{SITENAME}}}} $1",
-       "lastmodifiedatby": "Ð¥Ó\80аÑ\80а Ð°Ð³Ó\80о Ñ\82Ó\80аÑ\8cÑ\85Ñ\85Ñ\8cаÑ\80а Ñ\85ийÑ\86ина: $1 $2, Ñ\85ийÑ\86ам Ð±Ð¸Ð½Ð° — $3",
+       "lastmodifiedatby": "Ð¥Ó\80аÑ\80а Ð°Ð³Ó\80о Ñ\82Ó\80аÑ\8cÑ\85Ñ\85Ñ\8cаÑ\80а Ñ\85ийÑ\86ам Ð±Ð¸Ð½Ð°: $1 $2, Ñ\85ийÑ\86аман Ð°Ð²Ñ\82оÑ\80 — $3",
        "othercontribs": "Кхуллуш дакъалецира декъашхоша: $1.",
        "others": "кхин",
        "siteusers": "{{PLURAL:$2|1=декъашхо|декъашхой}} {{grammar:genitive|{{SITENAME}}}} $1",
        "logentry-managetags-create": "$1 {{GENDER:$2|Кхоьллина}} билгало «$4»",
        "log-name-tag": "Билгалонийн тептар",
        "rightsnone": "(яц)",
+       "rightslogentry-temporary-group": "$1 (ханна, $2 кхаччалца)",
        "feedback-adding": "АгӀона хетарг тӀетохар...",
        "feedback-back": "ЮхагӀо",
        "feedback-bugornote": "Хьайн техникин халонах лаьцна яздан хӀума делахь, дехар до, [$1 хаам бе тхоьга].\nДацахь хьан йиш ю хӀокху атта кепаца «[$3 $2]» агӀонг коммент тӀетоха хьан декъашхочун цӀарца, кхин лелош йолу браузер билгал еш.",
index fc87478..9af37c1 100644 (file)
        "about": "Ăнлантарни",
        "article": "Статья",
        "newwindow": "(çĕнĕ чӳречере)",
-       "cancel": "Пăрахăçла",
+       "cancel": "Çырмасăр хăвар",
        "moredotdotdot": "Малалла…",
        "mypage": "Страница",
        "mytalk": "Сӳтсе явни",
        "nosuchuser": "$1 ятлă хутшăнакан çук.\nÇырнă ята тепĕр хут тĕрĕслĕр, е аяларах вырнаçнă формăна усă курса çĕнĕ хутшăнакана регистрацилĕр.",
        "nosuchusershort": "$1 ятлă хутшăнакан çук. Ятне епле çырнине тĕрĕслĕр.",
        "nouserspecified": "Сирĕн хутшăнаканăн ятне каламалла.",
-       "wrongpassword": "ЭÑ\81иÑ\80 ÐºÄ\83Ñ\82аÑ\80Ñ\82нÄ\83 Ð²Ä\83Ñ\80Ñ\82Ñ\82Ä\83н Ñ\81Ä\83маÑ\85 Ñ\82Ä\95Ñ\80Ä\95Ñ\81 Ð¼Ð°Ñ\80. Ð£Ñ\80Ä\83Ñ\85Ñ\85ине ÐºÄ\83Ñ\82аÑ\80Ñ\82Ä\83р.",
+       "wrongpassword": "ЯÑ\82Ä\83 Ðµ Ð¿Ð°Ñ\80олÑ\8cÄ\95 Ñ\82Ä\95Ñ\80Ä\95Ñ\81 Ð¼Ð°Ñ\80.\nТепÄ\95Ñ\80 Ñ\85Ñ\83Ñ\82 ÐºÄ\95Ñ\80Ñ\82Ä\95р.",
        "wrongpasswordempty": "Пушă мар пароль çырăр тархасшăн.",
        "mailmypassword": "Çĕнĕ вăрттăн сăмаха ярса ил",
        "passwordremindertitle": "{{grammar:genitive|{{SITENAME}}}} хутшăнаканăн вăрттăн сăмахне асаилтересси",
        "last": "малт.",
        "page_first": "пĕрремĕш",
        "page_last": "юлашки",
-       "history-fieldset-title": "Ð\98Ñ\81Ñ\82оÑ\80ине Ð¿Ä\83Ñ\85",
+       "history-fieldset-title": "УлÄ\83Ñ\88Ä\83нниÑ\81ене Ñ\88Ñ\8bÑ\80а",
        "histfirst": "киввисем",
        "histlast": "çĕннисем",
        "historysize": "({{PLURAL:$1|1 байт|$1 байт}})",
        "recentchanges-submit": "Кăтарт",
        "rcfilters-legend-heading": "<strong>Кĕскетнисем:</strong>",
        "rcfilters-other-review-tools": "Урăх пăхмаллисем",
+       "rcfilters-activefilters-hide": "Кăтартмалла мар",
+       "rcfilters-activefilters-show": "Кăтартмалла",
        "rcfilters-show-new-changes": "Çĕнĕ улăшăннисем",
        "rcfilters-filterlist-title": "Фильтрсем",
        "rcfilters-filter-editsbyself-label": "Хăвăр улăштарнисем",
        "apihelp-no-such-module": "\"$1\" модульĕ тупăнмарĕ.",
        "booksources": "Кĕнекесен çăлкуçĕсем",
        "booksources-search": "Туп",
-       "specialloguserlabel": "Ð¥Ñ\83Ñ\82Ñ\88Ä\83накан:",
+       "specialloguserlabel": "ТÄ\83вакан:",
        "log": "Логсем",
        "logeventslist-submit": "Кăтарт",
        "all-logs-page": "Пĕтĕм логсем",
        "unwatch": "ан сăна",
        "unwatchthispage": "Сăнама пăрах",
        "notanarticle": "Ку статья мар",
+       "watchlist-hide": "Кăтартмалла мар",
        "watchlist-submit": "Кăтарт",
        "watching": "Сăнамаллисем шутне хушасси…",
        "unwatching": "Сăнав ят-йышĕнчен кăларса пăрахасси…",
        "import-noarticle": "Импортламалли страница çук!",
        "importlogpage": "Импорт журналĕ",
        "tooltip-pt-userpage": "Сирĕн хутшăнакан страници",
-       "tooltip-pt-mytalk": "Сирĕн канашлу страници",
+       "tooltip-pt-mytalk": "{{GENDER:|Сирĕн}} сӳтсе явакан страницу",
        "tooltip-pt-anontalk": "IP адресне сӳтсе явни",
        "tooltip-pt-preferences": "Сирĕн ĕнерлевсем",
        "tooltip-pt-watchlist": "Эсир пăхакан страницисем",
        "tag-filter": "[[Special:Tags|Тегсен]] фильтрĕ:",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Тег|Тегсем}}]]: $2)",
        "tags-title": "Тегсем",
+       "tags-tag": "Тегĕн ячĕ",
        "compare-submit": "Танлаштар",
        "htmlform-selectorother-other": "Урăххи",
        "htmlform-no": "Çук",
index 43af650..21cc02a 100644 (file)
        "resetpass-submit-loggedin": "Skift adgangskode",
        "resetpass-submit-cancel": "Annuller",
        "resetpass-wrong-oldpass": "Ugyldig midlertidig eller gældende adgangskode.\nDu har muligvis allerede ændret din adgangskode eller bedt om en ny midlertidig kode.",
-       "resetpass-recycled": "Vær venlig at ændre din adgangskode til noget andet end din nuværende adgangskode.",
+       "resetpass-recycled": "Ændr venligst din adgangskode til noget andet end din nuværende adgangskode.",
        "resetpass-temp-emailed": "Du loggede på med en midlertidig kode tilsendt på e-mail.\nFor at afslutte indlogning skal du angive en ny adgangskode her:",
        "resetpass-temp-password": "Midlertidig adgangskode",
        "resetpass-abort-generic": "Ændring af adgangskode er blevet afbrudt af en udvidelse",
        "resetpass-expired": "Din adgangskode er udløbet. Angiv en ny adgangskode for at logge på.",
-       "resetpass-expired-soft": "Din adgangskode er udløbet og skal ændres. Vær venlig at ændre den nu, eller tryk \"{{int:authprovider-resetpass-skip-label}}\" for at ændre den senere.",
-       "resetpass-validity-soft": "Din adgangskode er ikke gyldig:  $1 \n\nVær venlig at ændre den nu, eller tryk \"{{int:authprovider-resetpass-skip-label}}\" for at ændre den senere.",
+       "resetpass-expired-soft": "Din adgangskode er udløbet og skal ændres. Ændr den venligst nu, eller tryk \"{{int:authprovider-resetpass-skip-label}}\" for at ændre den senere.",
+       "resetpass-validity-soft": "Din adgangskode er ikke gyldig:  $1 \n\nVælg venligst en ny adgangskode nu, eller tryk \"{{int:authprovider-resetpass-skip-label}}\" for at ændre den senere.",
        "passwordreset": "Nulstil adgangskode",
        "passwordreset-text-one": "Udfyld denne formular for at nulstille din adgangskode.",
        "passwordreset-text-many": "{{PLURAL:$1|Udfyld et af felterne for at modtage en midlertidig adgangskode via e-mail.}}",
        "previewerrortext": "Der opstod en fejl under forsøget på at lave en forhåndsvisning af dine ændringer.",
        "blockedtitle": "Du eller din IP-adresse er blokeret",
        "blockedtext": "<strong>Dit brugernavn eller din IP-adresse er blevet blokeret.</strong>\n\nBlokeringen er foretaget af $1.\nDen anførte grund er <em>$2</em>.\n\nBlokeringen starter: $8\nBlokeringen udløber: $6\nBlokeringen er rettet mod: $7\n\nDu kan kontakte $1 eller en af de andre [[{{MediaWiki:Grouppage-sysop}}|administratorer]] for at diskutere blokeringen.\nDu kan ikke bruge funktionen \"{{int:emailuser}}\" medmindre der er angivet en gyldig e-mailadresse i dine [[Special:Preferences|kontoindstillinger]], og du ikke er blevet blokeret fra at bruge den.\n\nDin nuværende IP-adresse er $3, og blokerings-id er #$5.\nAngiv venligst alle ovenstående detaljer ved henvendelser om blokeringen.",
-       "autoblockedtext": "Din IP-adresse er blevet blokeret automatisk fordi den blev brugt af en anden bruger som er blevet blokeret af $1.\nBegrundelsen for det er:\n\n:''$2''\n\n* Blokeringsperiodens start: $8\n* Blokeringen udløber: $6\n* Blokeringen er ment for: $7\n\nDu kan kontakte $1 eller en af de andre [[{{MediaWiki:Grouppage-sysop}}|administratorer]] for at diskutere blokeringen.\n\nBemærk at du ikke kan bruge funktionen \"e-mail til denne bruger\" medmindre du har en gyldig e-mailadresse registreret i din [[Special:Preferences|brugerindstilling]], og du ikke er blevet blokeret fra at bruge den.\n\nDin nuværende IP-adresse er $3, og blokerings-id'et er #$5.\nAngiv venligst alle de ovenstående detaljer ved eventuelle henvendelser.",
+       "autoblockedtext": "Din IP-adresse er blevet blokeret automatisk fordi den blev brugt af en anden bruger som er blevet blokeret af $1.\nDen givne begrundelse er:\n\n:<em>$2</em>\n\n* Blokeringsperiodens start: $8\n* Blokeringen udløber: $6\n* Blokeringen er rettet mod: $7\n\nDu kan kontakte $1 eller en af de andre [[{{MediaWiki:Grouppage-sysop}}|administratorer]] for at diskutere blokeringen.\n\nBemærk at du ikke kan bruge funktionen \"{{int:emailuser}}\" medmindre du har en gyldig e-mailadresse registreret i dine [[Special:Preferences|brugerindstillinger]] og du ikke er blevet blokeret fra at bruge den.\n\nDin nuværende IP-adresse er $3, og blokerings-id'et er #$5.\nAngiv venligst alle de ovenstående detaljer ved eventuelle henvendelser.",
        "systemblockedtext": "Dit brugernavn eller din IP-adresse er automatisk blokeret af MediaWiki.\nBegrundelsen for det er:\n\n:<em>$2</em>\n\n* Blokeringsperiodens start: $8\n* Blokeringen udløber: $6\n* Blokeringen er ment for: $7\n\nDin nuværende IP-adresse er $3.\nAngiv venligst alle de ovenstående detaljer ved eventuelle henvendelser.",
        "blockednoreason": "ingen begrundelse givet",
        "whitelistedittext": "Du skal $1 for at kunne redigere sider.",
        "readonlywarning": "<strong>Advarsel: Databasen er låst på grund af vedligeholdelse, så du kan ikke gemme dine ændringer lige nu.</strong>\nDet kan være en god idé at kopiere din tekst over i en tekstfil og gemme den til senere.\n\nAdministratoren, som låste databasen, gav denne forklaring: $1",
        "protectedpagewarning": "'''ADVARSEL: Denne side er skrivebeskyttet, så kun administratorer kan redigere den.'''<br />\nDen seneste logpost vises nedenfor:",
        "semiprotectedpagewarning": "'''Bemærk: Siden er låst, så kun registrerede brugere kan ændre den.'''\n<br />Den seneste logpost vises nedenfor:",
-       "cascadeprotectedwarning": "<strong>Advarsel:</strong> Denne side er blevet beskyttet, så den kun kan ændres af brugere med administratorrettigheder, fordi indholdet er inkluderet i følgende {{PLURAL:$1|side|sider}} med nedarvet sidebeskyttelse:",
+       "cascadeprotectedwarning": "<strong>Advarsel:</strong> Denne side er blevet beskyttet, så kun brugere med [[Special:ListGroupRights|bestemte rettigheder]] kan ændre den, fordi indholdet er inkluderet i følgende {{PLURAL:$1|side|sider}} med nedarvet sidebeskyttelse:",
        "titleprotectedwarning": "ADVARSEL:  Den side er låst så kun [[Special:ListGroupRights|visse brugere]] kan oprette den.'''\n<br />Den seneste logpost vises nedenfor:",
        "templatesused": "{{PLURAL:$1|Skabelon|Skabeloner}} der er brugt på denne side:",
        "templatesusedpreview": "Følgende {{PLURAL:$1|skabelon|skabeloner}} bruges i denne forhåndsvisning:",
        "recentchangesdays": "Antal dage som skal vises i seneste ændringer:",
        "recentchangesdays-max": "(maks. $1 {{PLURAL:$1|dag|dage}})",
        "recentchangescount": "Antal redigeringer som skal vises som standard i sidste ændringer, sidehistorikker og logger:",
-       "prefs-help-recentchangescount": "Det gælder for seneste ændringer, historikker og logger.",
+       "prefs-help-recentchangescount": "Maksimalt antal: 1000",
        "prefs-help-watchlist-token2": "Dette er den hemmelige nøgle til web-feed af din overvågningsliste.\nHvis andre kender den, vil man være i stand til at læse din overvågningsliste, så del den ikke.\n[[Special:ResetTokens|Klik her]] hvis du har brug at nulstille den.",
        "savedprefs": "Dine indstillinger er blevet gemt.",
        "savedrights": "Brugergrupperne for {{GENDER:$1|$1}} er blevet gemt.",
        "grant-createaccount": "Oprette af konti",
        "grant-createeditmovepage": "Oprette, redigere og flytte sider",
        "grant-delete": "Slette sider, revisioner og logposter",
-       "grant-editinterface": "Redigere MediaWiki-navnerummet og bruger/side JSON",
+       "grant-editinterface": "Redigere MediaWiki-navnerummet og JSON for hele webstedet og brugere",
        "grant-editmycssjs": "Redigere din bruger-CSS/JSON/JavaScript",
        "grant-editmyoptions": "Redigere dine brugerindstillinger",
        "grant-editmywatchlist": "Redigere din overvågningsliste",
        "uploadstash-summary": "Denne side giver adgang til filer, de er uploadet (eller er i gang med at blive det), men som endnu ikke er offentliggjort på wikien. Disse filer er kun synlige for brugeren, der har uploadet dem.",
        "uploadstash-clear": "Ryd stashede filer",
        "uploadstash-nofiles": "Du har ingen stashede filer.",
-       "uploadstash-badtoken": "Udførelse af handlingen mislykkedes, måske fordi dine redigerings legitimationsoplysninger udløbet. Prøv igen.",
+       "uploadstash-badtoken": "Udførelsen af handlingen mislykkedes, måske fordi dine legitimationsoplysninger for redigering er udløbet. Prøv venligst igen.",
        "uploadstash-errclear": "Rydning af filerne mislykkedes.",
        "uploadstash-refresh": "Opdatér filoversigten",
        "uploadstash-thumbnail": "vis miniature",
        "filehist-filesize": "Filstørrelse",
        "filehist-comment": "Kommentar",
        "imagelinks": "Filanvendelse",
-       "linkstoimage": "{{PLURAL:$1|Den følgende side|De følgende $1 sider}} henviser til denne fil:",
+       "linkstoimage": "{{PLURAL:$1|Den følgende side|De følgende $1 sider}} bruger denne fil:",
        "linkstoimage-more": "Flere end $1 {{PLURAL:$1|side|sider}} henviser til denne fil.\nDen følgende liste viser kun {{PLURAL:$1|den første henvisning|de $1 første henvisninger}}.\nEn [[Special:WhatLinksHere/$2|komplet liste]] er tilgængelig.",
-       "nolinkstoimage": "Der er ingen sider der henviser til denne fil.",
+       "nolinkstoimage": "Der er ingen sider der bruger denne fil.",
        "morelinkstoimage": "Se [[Special:WhatLinksHere/$1|flere henvisninger]] til denne fil.",
        "linkstoimage-redirect": "$1 (filomdirigering) $2",
        "duplicatesoffile": "Følgende {{PLURAL:$1|fil er en dublet|filer er dubletter}} af denne fil ([[Special:FileDuplicateSearch/$2|flere detaljer]]):",
        "editcomment": "Redigeringsbeskrivelsen var: <em>$1</em>.",
        "revertpage": "Gendannet til seneste version af [[User:$1|$1]], fjerner ændringer fra [[Special:Contributions/$2|$2]] ([[User talk:$2|diskussion]])",
        "revertpage-nouser": "Gendannet til seneste version af {{GENDER:$1|[[User:$1|$1]]}}, fjerner ændringer fra en skjult bruger",
-       "rollback-success": "Ændringerne fra $1 er fjernet,\nog den seneste version af $2 er gendannet.",
+       "rollback-success": "Ændringerne foretaget af {{GENDER:$3|$1}} er blevet tilbagestillet, og den seneste version af {{GENDER:$4|$2}} er gendannet.",
        "sessionfailure-title": "Sessionsfejl",
-       "sessionfailure": "Der lader til at være et problem med din loginsession; denne handling blev annulleret som en sikkerhedsforanstaltning mod kapring af sessionen. Tryk på \"tilbage\"-knappen og genindlæs den side du kom fra, og prøv dernæst igen.",
+       "sessionfailure": "Der lader til at være et problem med din loginsession; denne handling blev annulleret som en sikkerhedsforanstaltning mod kapring af sessionen. Genindsend venligst formularen.",
        "changecontentmodel-legend": "Ændr indholdsmodel",
        "changecontentmodel-title-label": "Sidetitel",
        "changecontentmodel-model-label": "Ny indholdsmodel",
        "sp-contributions-newbies-sub": "Fra nye kontoer",
        "sp-contributions-newbies-title": "Brugerbidrag fra nye konti",
        "sp-contributions-blocklog": "blokeringslog",
-       "sp-contributions-suppresslog": "undertrykte brugerbidrag",
+       "sp-contributions-suppresslog": "undertrykte {{GENDER:$1|brugerbidrag}}",
        "sp-contributions-deleted": "slettede {{GENDER:$1|brugerbidrag}}",
        "sp-contributions-uploads": "uploads",
        "sp-contributions-logs": "loglister",
        "sp-contributions-talk": "diskussion",
-       "sp-contributions-userrights": "håndtering af brugerrettigheder",
+       "sp-contributions-userrights": "håndtering af {{GENDER:$1|brugerrettigheder}}",
        "sp-contributions-blocked-notice": "Denne bruger er i øjeblikket blokeret. Loggen over den seneste blokering kan ses nedenfor:",
        "sp-contributions-blocked-notice-anon": "Denne IP-adresse er i øjeblikket blokeret.\nDen seneste post i blokeringsloggen vises nedenfor:",
        "sp-contributions-search": "Søg efter bidrag",
        "lockedbyandtime": "(af $1 den $2 kl. $3)",
        "move-page": "Flyt $1",
        "move-page-legend": "Flyt side",
-       "movepagetext": "Når du bruger formularen herunder, vil du få omdøbt en side og flyttet hele sidens historie til det nye navn.\nDen gamle titel vil blive en omdirigeringsside til den nye titel.\nDu kan opdatere omdirigeringer, der peger på den oprindelige titel, automatisk.\nHvis du vælger ikke at opdatere dem automatisk, så sørg for at tjekke efter [[Special:DoubleRedirects|dobbelte]] eller [[Special:BrokenRedirects|dårlige omdirigeringer]].\nDu er ansvarlig for, at alle henvisninger stadig peger derhen, hvor det er meningen de skal pege.\n\nBemærk at siden '''ikke''' kan flyttes, hvis der allerede er en side med den nye titel, medmindre den side er en omdirigering uden nogen redigeringshistorik.\nDet betyder, at du kan flytte en side tilbage hvor den kom fra, hvis du kommer til at lave en fejl, og det betyder, at du ikke kan overskrive en eksisterende side.\n\n'''ADVARSEL!'''\nDette kan være en drastisk og uventet ændring for en populær side; vær sikker på, at du forstår konsekvenserne af dette før du fortsætter.",
-       "movepagetext-noredirectfixer": "Brug formularen herunder du vil omdøbe en side og flyttet hele sidens historie til det nye navn.\nDen gamle titel vil blive en omdirigeringsside til den nye titel.\nVær sikker på at tjekke for [[Special:DoubleRedirects|dobbelte]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for at sikre, at alle henvisninger stadig peger på et sted hvor det giver meningen at gå.\n\nBemærk, at siden '''ikke''' kan flyttes hvis der allerede er en side med den nye titel, medmindre den er tom eller er en omdirigering, og har ingen historie.\nDet betyder at du kan omdøbe en side tilbage hvor den kom fra, hvis du laver en fejl, og du kan ikke overskrive en eksisterende side.\n\n'''Advarsel!'''\nDette kan være en drastisk og uventet ændring for en populær side;\ndu skal være sikker på at du forstår konsekvenserne af dette før du fortsætter.",
+       "movepagetext": "Når du bruger formularen herunder, vil du få omdøbt en side og flyttet hele sidens historie til det nye navn.\nDen gamle titel vil blive en omdirigeringsside til den nye titel.\nDu kan opdatere omdirigeringer, der peger på den oprindelige titel, automatisk.\nHvis du vælger ikke at opdatere dem automatisk, så sørg for at tjekke efter [[Special:DoubleRedirects|dobbelte]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for, at alle henvisninger stadig peger derhen, hvor det er meningen de skal pege.\n\nBemærk at siden <strong>ikke</strong> kan flyttes, hvis der allerede er en side med den nye titel, medmindre den side er en omdirigering uden nogen redigeringshistorik.\nDet betyder, at du kan flytte en side tilbage hvor den kom fra, hvis du kommer til at lave en fejl, og det betyder, at du ikke kan overskrive en eksisterende side.\n\n<strong>Bemærk:</strong>\nDette kan være en drastisk og uventet ændring for en populær side; vær sikker på, at du forstår konsekvenserne af dette før du fortsætter.",
+       "movepagetext-noredirectfixer": "Brug af formularen herunder vil omdøbe en side og flytte hele sidens historie til det nye navn.\nDen gamle titel vil blive en omdirigeringsside til den nye titel.\nVær sikker på at tjekke for [[Special:DoubleRedirects|dobbelte]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for at sikre, at alle henvisninger stadig peger på det, som det er meningen, de skal pege på.\n\nBemærk, at siden <strong>ikke</strong> kan flyttes, hvis der allerede er en side med den nye titel, medmindre det er en omdirigeringsside uden historie.\nDet betyder, at du kan omdøbe en side tilbage hvor den kom fra, hvis du laver en fejl, og at du ikke kan overskrive en eksisterende side.\n\n<strong>Bemærk:</strong>\nDette kan være en drastisk og uventet ændring for en populær side; vær sikker på, at du forstår konsekvenserne af dette før du fortsætter.",
        "movepagetalktext": "Den tilhørende diskussionsside, hvis der er en, vil automatisk blive flyttet med siden '''medmindre:''' *Du flytter siden til et andet navnerum,\n*En ikke-tom diskussionsside allerede eksisterer under det nye navn, eller\n*Du fjerner markeringen i boksen nedenunder.\n\nI disse tilfælde er du nødt til at flytte eller sammenflette siden manuelt.",
        "moveuserpage-warning": "'''Advarsel:''' Du er ved at flytte en brugerside. Bemærk at det kun er siden, der vil blive flyttet – brugeren bliver ''ikke'' omdøbt.",
        "movenologintext": "Du skal være registreret bruger og [[Special:UserLogin|logget på]] for at flytte en side.",
        "delete_and_move_text": "==Sletning nødvendig==\n\nArtiklen \"[[:$1]]\" eksisterer allerede. Vil du slette den for at gøre plads til flytningen?",
        "delete_and_move_confirm": "Ja, slet siden",
        "delete_and_move_reason": "Slettet for at gøre plads til flytning fra \"[[$1]]\"",
-       "selfmove": "Begge sider har samme navn. Man kan ikke flytte en side oven i sig selv.",
+       "selfmove": "Titlen er den samme; man kan ikke flytte en side til samme side.",
        "immobile-source-namespace": "Kan ikke flytte sider i navnerummet \"$1\"",
        "immobile-target-namespace": "Kan ikke flytte sider til navnerummet \"$1\"",
        "immobile-target-namespace-iw": "En side kan ikke flyttes til en interwiki-henvisning.",
        "fix-double-redirects": "Opdater henvisninger til det oprindelige navn",
        "move-leave-redirect": "Efterlad en omdirigering",
        "protectedpagemovewarning": "'''Bemærk:''' Denne side er låst så kun administratorer kan flytte den.<br />\nDen seneste logpost vises nedenfor:",
-       "semiprotectedpagemovewarning": "'''Bemærk:''' Denne side er låst så kun registrerede brugere kan flytte den.<br />\nDen seneste logpost vises nedenfor:",
+       "semiprotectedpagemovewarning": "<strong>Bemærk:</strong> Denne side er låst, så kun automatisk bekræftede brugere kan flytte den.\nDen seneste logpost vises nedenfor som reference:",
        "move-over-sharedrepo": "== Fil findes ==\n[[:$1]] findes på en delt kilde. Ved at flytte en fil til denne titel vil overskrive den eksisterende delte fil.",
        "file-exists-sharedrepo": "Det valgte filnavn er allerede i brug på en delt kilde.\nVælg venligst et andet navn.",
        "export": "Eksportér sider",
index 9c8a5d5..9773a6a 100644 (file)
@@ -25,7 +25,8 @@
                        "Florian",
                        "Tiin",
                        "Rhirsch",
-                       "Metalhead64"
+                       "Metalhead64",
+                       "Vogone"
                ]
        },
        "tog-hideminor": "Kleine Änderungen in den „Letzten Änderungen“ ausblenden",
@@ -39,6 +40,7 @@
        "tog-editsectiononrightclick": "Einzelne Abschnitte per Rechtsklick bearbeiten",
        "tog-enotifrevealaddr": "Ihre E-Mail-Adresse in Benachrichtigungs-E-Mails anzeigen",
        "cancel": "Abbrechen",
+       "search-ignored-headings": " #<!-- diese Zeile nicht verändern --> <pre>\n# Überschriften, die von der Suche ignoriert werden.\n# Diese Änderungen werden wirksam, sobald die Seite mit der Überschrift indexiert wurde.\n# Sie können die Seitenindexierung erzwingen, indem Sie einen Nulledit durchführen.\n# Syntax:\n#   * Alles, was einer Raute („#“) bis zum Zeilenende folgt, ist ein Kommentar.\n#   * Jede nicht-leere Zeile ist der exakte zu ignorierende Titel.\nEinzelnachweise\nWeblinks\nSiehe auch\n #</pre> <!-- diese Zeile nicht verändern -->",
        "view-pool-error": "Entschuldigen Sie bitte, dass die Server im Moment überlastet sind.\nZu viele Benutzer versuchen, diese Seite zu besuchen.\nBitte warten Sie einige Minuten, bevor Sie es noch einmal versuchen.\n\n$1",
        "generic-pool-error": "Entschuldigen Sie bitte, dass die Server im Moment überlastet sind.\nZu viele Benutzer wollen diese Ressource ansehen.\nBitte warten Sie einen Moment, bevor Sie sie erneut aufrufen.",
        "badaccess-group0": "Sie haben nicht die erforderlichen Benutzerrechte für diese Aktion.",
        "youhavenewmessagesmanyusers": "Sie haben $1 von vielen Benutzern ($2).",
        "youhavenewmessagesmulti": "Sie haben neue Nachrichten: $1",
        "confirmable-confirm": "Sind {{GENDER:$1|Sie}} sicher?",
+       "transaction-duration-limit-exceeded": "Um eine große Verzögerung in der Datenreplikation zu vermeiden, wurde diese Transaktion abgebrochen. Die Schreibdauer ($1) hat die Grenze von $2 Sekunden überschritten. Falls Sie viele Objekte auf einmal ändern, versuchen Sie stattdessen, die Änderungen auf mehrere Operationen aufzuteilen.",
        "enterlockreason": "Bitte geben Sie einen Grund ein, warum die Datenbank gesperrt werden soll und eine Abschätzung über die Dauer der Sperrung",
        "readonlytext": "Die Datenbank ist vorübergehend für Neueinträge und Änderungen gesperrt. Bitte versuchen Sie es später noch einmal.\n\nGrund der Sperrung: $1",
        "missing-article": "Der Text von „$1“ $2 wurde nicht in der Datenbank gefunden.\n\nDie Seite ist möglicherweise gelöscht oder verschoben worden.\n\nFalls dies nicht der Fall ist, haben Sie eventuell einen Fehler in der Software gefunden. Bitte melden Sie dies einem [[Special:ListUsers/sysop|Administrator]] unter Nennung der URL.",
        "actionthrottledtext": "Im Rahmen einer Anti-Spam-Maßnahme kann diese Aktion in einem kurzen Zeitabstand nur begrenzt oft ausgeführt werden. Diese Grenze haben Sie überschritten.\nBitte versuchen Sie es in ein paar Minuten erneut.",
-       "viewsourcetext": "Sie können den Quelltext dieser Seite betrachten und kopieren:",
-       "viewyourtext": "Sie können den Quelltext '''Ihrer Bearbeitung''' dieser Seite betrachten und kopieren:",
+       "viewsourcetext": "Sie können den Quelltext dieser Seite betrachten und kopieren.",
+       "viewyourtext": "Sie können den Quelltext <strong>Ihrer Bearbeitung</strong> dieser Seite betrachten und kopieren.",
        "protectedinterface": "Diese Seite enthält Text für die Benutzeroberfläche der Software auf diesem Wiki und ist geschützt, um Missbrauch vorzubeugen.\nNutzen Sie bitte [https://translatewiki.net/ translatewiki.net], das Lokalisierungsprojekt von MediaWiki, um Übersetzungen für alle Wikis hinzuzufügen oder zu ändern.",
        "editinginterface": "'''Warnung:''' Diese Seite enthält von der MediaWiki-Software genutzten Text.\nÄnderungen auf dieser Seite wirken sich auf die Benutzeroberfläche dieses Wikis aus.\nNutzen Sie bitte [https://translatewiki.net/ translatewiki.net], das Lokalisierungsprojekt von MediaWiki, um Übersetzungen für alle Wikis hinzuzufügen oder zu ändern.",
        "translateinterface": "Um Übersetzungen für alle Wikis hinzuzufügen oder zu ändern, verwenden Sie bitte [//translatewiki.net/ translatewiki.net], das MediaWiki-Lokalisierungsprojekt.",
        "namespaceprotected": "Sie haben nicht die erforderliche Berechtigung, um Seiten im Namensraum '''$1''' bearbeiten zu können.",
        "customcssprotected": "Sie haben nicht die Berechtigung, diese CSS enthaltende Seite zu bearbeiten, da sie die persönlichen Einstellungen eines anderen Benutzers enthält.",
+       "customjsonprotected": "Sie haben nicht die Berechtigung, diese CSS enthaltende Seite zu bearbeiten, da sie die persönlichen Einstellungen eines anderen Benutzers enthält.",
        "customjsprotected": "Sie haben nicht die Berechtigung, diese JavaScript enthaltende Seite zu bearbeiten, da es sich hierbei um die persönlichen Einstellungen eines anderen Benutzers handelt.",
+       "sitejsonprotected": "Sie haben nicht die Berechtigung, diese JSON-Seite zu bearbeiten, da sie alle Besucher betreffen könnte.",
        "mycustomcssprotected": "Sie haben keine Berechtigung, diese CSS-Seite zu bearbeiten.",
        "mycustomjsprotected": "Sie haben keine Berechtigung, diese JavaScript-Seite zu bearbeiten.",
        "myprivateinfoprotected": "Sie haben keine Berechtigung, Ihre privaten Informationen zu bearbeiten.",
index b0ea878..20bb78a 100644 (file)
        "logentry-delete-delete": "{{GENDER:$2|Ο|Η}} $1 διέγραψε τη σελίδα $3",
        "logentry-delete-delete_redir": "{{GENDER:$2|Ο|Η}} $1 διέγραψε την ανακατεύθυνση $3 με αντικατάσταση",
        "logentry-delete-restore": "{{GENDER:$1|Ο|Η}} $1 αποκατέστησε τη σελίδα $3 ($4)",
+       "logentry-delete-restore-nocount": "{{GENDER:$2|Ο|Η}} $1 αποκατέστησε τη σελίδα $3",
+       "restore-count-revisions": "{{PLURAL:$1|1 τροποποίηση|$1 τροποποιήσεις}}",
+       "restore-count-files": "{{PLURAL:$1|1 αρχείο|$1 αρχεία}}",
        "logentry-delete-event": "{{GENDER:$2|Ο|Η}} $1 άλλαξε την ορατότητα {{PLURAL:$5|ενός καταγραφόμενου συμβάντος|$5 καταγραφόμενων συμβάντων}} στο $3: $4",
        "logentry-delete-revision": "{{GENDER:$2|Ο|Η}} $1 άλλαξε την ορατότητα {{PLURAL:$5|μίας αναθεώρησης|$5 αναθεωρήσεων}} στη σελίδα $3: $4",
        "logentry-delete-event-legacy": "{{GENDER:$2|Ο|Η}} $1 άλλαξε την ορατότητα των καταγραφόμενων συμβάντων στη σελίδα $3",
index b06bcc7..2fd9783 100644 (file)
                        "Amaia",
                        "Tiberius1701",
                        "Astroemi",
-                       "Jelou"
+                       "Jelou",
+                       "Ktranz"
                ]
        },
        "tog-underline": "Subrayar los enlaces:",
        "ns-specialprotected": "No se pueden editar las páginas especiales.",
        "titleprotected": "Este título ha sido protegido contra creación por [[User:$1|$1]].\nEl motivo proporcionado es <em>$2</em>.",
        "filereadonlyerror": "No se puede modificar el archivo \"$1\" porque el repositorio de archivos \"$2\" es de solo lectura.\n\nEl administrador del sistema que lo ha bloqueado ofrece esta explicación: \"$3\".",
+       "invalidtitle": "Título inválido",
        "invalidtitle-knownnamespace": "El título con el espacio de nombres «$2» y el texto «$3» no es válido",
        "invalidtitle-unknownnamespace": "El título con el espacio de nombres desconocido (n.º $1) y el texto «$2» no es válido",
        "exception-nologin": "No has accedido",
        "uploadstash-zero-length": "El archivo está vacío.",
        "invalid-chunk-offset": "Desplazamiento inválido del fragmento",
        "img-auth-accessdenied": "Acceso denegado",
-       "img-auth-nopathinfo": "Falta PATH_INFO.\nEl servidor no está configurado para proporcionar esta información.\nEs posible que esté basado en CGI y que no sea compatible con img_auth.\nConsulte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
+       "img-auth-nopathinfo": "Falta la información de ruta.\nEl servidor tiene que estar configurado para proporcionar las variables REQUEST_URI y/o PATH_INFO.\nSi lo está, intentá habilitar $wgUsePathInfo.\nConsulte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "img-auth-notindir": "La ruta solicitada no figura en la carpeta de subidas configurada.",
        "img-auth-badtitle": "Incapaz de construir un título válido de «$1».",
        "img-auth-nologinnWL": "No has iniciado sesión y «$1» no está en la lista blanca.",
        "filehist-filesize": "Tamaño del archivo",
        "filehist-comment": "Comentario",
        "imagelinks": "Usos del archivo",
-       "linkstoimage": "{{PLURAL:$1|La siguiente página enlaza|Las siguientes páginas enlazan}} a este archivo:",
-       "linkstoimage-more": "Hay más de {{PLURAL:$1|una página que enlaza|$1 páginas que enlazan}} con este archivo.\nLa lista siguiente sólo muestra {{PLURAL:$1|la primera página que enlaza|las primeras $1 páginas que enlazan}} con este archivo.\nTambién puedes consultar la [[Special:WhatLinksHere/$2|lista completa]].",
-       "nolinkstoimage": "No hay páginas que enlacen a esta imagen.",
+       "linkstoimage": "{{PLURAL:$1|La siguiente página usa|Las siguientes páginas usan}} a este archivo:",
+       "linkstoimage-more": "Hay más de {{PLURAL:$1|una página que usa|$1 páginas que usan}} este archivo.\nLa lista siguiente sólo muestra {{PLURAL:$1|la primera página que usa|las primeras $1 páginas que usan}} este archivo.\nTambién puedes consultar la [[Special:WhatLinksHere/$2|lista completa]].",
+       "nolinkstoimage": "No hay páginas que enlacen a este archivo.",
        "morelinkstoimage": "Mira [[Special:WhatLinksHere/$1|más enlaces]] a este archivo.",
        "linkstoimage-redirect": "$1 (archivo de redirección) $2",
        "duplicatesoffile": "{{PLURAL:$1|El siguiente archivo es un duplicado|Los siguientes $1 archivos son duplicados}} de éste ([[Special:FileDuplicateSearch/$2|más detalles]]):",
        "limitreport-expansiondepth": "Profundidad máxima de expansión",
        "limitreport-expensivefunctioncount": "Cuenta de la función expansiva del analizador",
        "limitreport-unstrip-depth": "Profundidad de recursión de función «unstrip»",
+       "limitreport-unstrip-size": "Unstrip tamaño post-expandido",
        "limitreport-unstrip-size-value": "$1/$2 {{PLURAL:$2|byte|bytes}}",
        "expandtemplates": "Expandir plantillas",
        "expand_templates_intro": "Esta página especial toma un texto wiki y expande todas sus plantillas recursivamente.\nTambién expande las funciones sintácticas como <code><nowiki>{{</nowiki>#language:…}}</code>, y variables como\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>. De hecho, expande casi cualquier cosa que esté entre llaves dobles.",
        "passwordpolicies-policy-passwordcannotmatchusername": "La contraseña no puede ser la misma que el nombre de usuario",
        "passwordpolicies-policy-passwordcannotmatchblacklist": "La contraseña no puede coincidir con la lista de contraseñas específicamente prohibidas",
        "passwordpolicies-policy-maximalpasswordlength": "La contraseña no puede tener más de $1 {{PLURAL:$1|caracter|caracteres}}",
-       "passwordpolicies-policy-passwordcannotbepopular": "La contraseña no puede {{PLURAL:$1|ser la contraseña más popular|encontrarse en la lista de $1 contraseñas populares}}"
+       "passwordpolicies-policy-passwordcannotbepopular": "La contraseña no puede {{PLURAL:$1|ser la contraseña más popular|encontrarse en la lista de $1 contraseñas populares}}",
+       "easydeflate-invaliddeflate": "El contenido proporcionado no esta comprimido correctamente"
 }
index cdc118c..abfc0e4 100644 (file)
        "subject-preview": "Aperçu du sujet :",
        "previewerrortext": "Une erreur s’est produite lors de la tentative de prévisualisation de vos modifications.",
        "blockedtitle": "L’utilisateur est bloqué.",
-       "blockedtext": "<strong>Votre compte utilisateur ou votre adresse IP a été bloqué.</strong>\n\nLe blocage a été effectué par $1.\nLa raison invoquée est la suivante : <em>$2</em>.\n\n* Début du blocage : $8\n* Expiration du blocage : $6\n* Compte bloqué : $7.\n\nVous pouvez contacter $1 ou un autre [[{{MediaWiki:Grouppage-sysop}}|administrateur]] pour en discuter.\nVous ne pouvez utiliser la fonction « {{int:emailuser}} » que si une adresse de courriel valide est spécifiée dans vos [[Special:Preferences|préférences]] et que si cette fonctionnalité na pas été bloquée.\nVotre adresse IP actuelle est $3 et votre identifiant de blocage est $5.\nVeuillez inclure tous les détails ci-dessus dans chacune des requêtes que vous ferez.",
-       "autoblockedtext": "Votre adresse IP a été bloquée automatiquement car elle a été utilisée par un autre utilisateur, lui-même bloqué par $1.\nLa raison invoquée est :\n\n: <em>$2</em>.\n\n* Début du blocage : $8\n* Expiration du blocage : $6\n* Compte bloqué : $7\n\nVous pouvez contacter $1 ou l’un des autres [[{{MediaWiki:Grouppage-sysop}}|administrateurs]] pour discuter de ce blocage.\n\nNotez que vous ne pourrez utiliser la fonctionnalité « {{int:emailuser}} » que si vous avez une adresse de courriel validée dans vos [[Special:Preferences|préférences]] et que cette fonctionnalité na pas été désactivée.\n\nVotre adresse IP actuelle est $3, et le numéro de blocage est $5.\nVeuillez inclure tous les détails ci-dessus dans chacune des requêtes que vous ferez.",
+       "blockedtext": "<strong>Votre compte utilisateur ou votre adresse IP a été bloqué.</strong>\n\nLe blocage a été effectué par $1.\nLa raison invoquée est la suivante : <em>$2</em>.\n\n* Début du blocage : $8\n* Expiration du blocage : $6\n* Compte bloqué : $7.\n\nVous pouvez contacter $1 ou un autre [[{{MediaWiki:Grouppage-sysop}}|administrateur]] pour en discuter.\nVous ne pouvez utiliser la fonction « {{int:emailuser}} » que si une adresse de courriel valide est spécifiée dans vos [[Special:Preferences|préférences]] et que si cette fonctionnalité ne vous a pas été bloquée.\nVotre adresse IP actuelle est $3 et votre identifiant de blocage est $5.\nVeuillez inclure tous les détails ci-dessus dans chacune des requêtes que vous ferez.",
+       "autoblockedtext": "Votre adresse IP a été bloquée automatiquement car elle a été utilisée par un autre utilisateur, lui-même bloqué par $1.\nLa raison invoquée est :\n\n: <em>$2</em>.\n\n* Début du blocage : $8\n* Expiration du blocage : $6\n* Compte bloqué : $7\n\nVous pouvez contacter $1 ou l’un des autres [[{{MediaWiki:Grouppage-sysop}}|administrateurs]] pour discuter de ce blocage.\n\nNotez que vous ne pourrez utiliser la fonctionnalité « {{int:emailuser}} » que si vous avez une adresse de courriel validée dans vos [[Special:Preferences|préférences]] et que cette fonctionnalité ne vous a pas été désactivée.\n\nVotre adresse IP actuelle est $3, et le numéro de blocage est $5.\nVeuillez inclure tous les détails ci-dessus dans chacune des requêtes que vous ferez.",
        "systemblockedtext": "Votre nom d'utilisateur ou votre adresse IP ont été bloqués automatiquement par MediaWiki.\nLa raison donnée est la suivante:\n\n: <em>$2</em>.\n\n* Le début du blocage: $8\n* Expiration du délai de blocage: $6\n* Elément concerné: $7\n\nVotre adresse IP actuelle est $3.\nVeuillez inclure tous les détails ci-dessus dans chacune des requêtes que vous ferez.",
        "blockednoreason": "aucune raison donnée",
        "whitelistedittext": "Vous devez vous $1 pour avoir la permission de modifier le contenu.",
index b955139..297cf75 100644 (file)
        "botpasswords-restriction-failed": "ボットパスワード制限によりログインできません。",
        "botpasswords-invalid-name": "指定された利用者名には、ボット用パスワードの区切りである「$1」 が含まれていません。",
        "botpasswords-not-exist": "利用者「$1」はボット「$2」のパスワードを所持していません。",
+       "botpasswords-needs-reset": "{{GENDER:$1|利用者}}「$1」のボット名「$2」のためのパスワードはリセットする必要があります。",
        "resetpass_forbidden": "パスワードは変更できません",
        "resetpass_forbidden-reason": "パスワードは変更できません: $1",
        "resetpass-no-info": "このページに直接アクセスするためにはログインしている必要があります。",
        "grouppage-autoconfirmed": "{{ns:project}}:自動承認された利用者",
        "grouppage-bot": "{{ns:project}}:ボット",
        "grouppage-sysop": "{{ns:project}}:管理者",
+       "grouppage-interface-admin": "{{ns:project}}:インターフェース管理者",
        "grouppage-bureaucrat": "{{ns:project}}:ビューロクラット",
        "grouppage-suppress": "{{ns:project}}:秘匿者",
        "right-read": "ページを閲覧",
index 5038969..e751f0c 100644 (file)
@@ -32,7 +32,8 @@
                        "Yogesh",
                        "Lokesha kunchadka",
                        "Anoop rao",
-                       "Rakshika"
+                       "Rakshika",
+                       "Gopala Krishna A"
                ]
        },
        "tog-underline": "ಕೊಂಡಿಗಳ ಕೆಳಗೆ ಗೆರೆ ತೋರಿಸಿ",
        "botpasswords-existing": "ಆಸ್ಥಿತ್ವದಲ್ಲಿರುವ ಬಾಟ್ ಪ್ರವೇಶಪದ",
        "botpasswords-createnew": "ಹೊಸ ಬಾಟ್ ಪ್ರವೇಶಪದ ರಚಿಸಿ",
        "botpasswords-editexisting": "ಆಸ್ಥಿತ್ವದಲ್ಲಿರುವ ಬಾಟ್ ಪ್ರವೇಶಪದ ಸ೦ಪಾದಿಸಿ",
+       "botpasswords-label-create": "ಸೃಷ್ಟಿಸು",
        "resetpass_forbidden": "ಪ್ರವೇಶಪದಗಳನ್ನು ಬದಲಾಯಿಸುವಂತಿಲ್ಲ.",
        "resetpass-no-info": "ನೀವು ಈ ಪುಟವನ್ನು ನೇರತಲುಪಲು ಲಾಗಿನ್ ಆಗಿರುವುದು ಆವಶ್ಯಕ.",
        "resetpass-submit-loggedin": "ಪ್ರವೇಶಪದ ಬದಲಾಯಿಸು",
index 78a2748..cfd23ac 100644 (file)
        "resetpass-submit-loggedin": "Keisti slaptažodį",
        "resetpass-submit-cancel": "Atšaukti",
        "resetpass-wrong-oldpass": "Klaidingas laikinas ar esamas slaptažodis.\nJūs galbūt jau sėkmingai pakeitėte savo slaptažodį ar jau prašėte naujo laikino slaptažodžio.",
-       "resetpass-recycled": "Atkurkite savo slaptažodį kitokiu, nei buvo prieš tai.",
+       "resetpass-recycled": "Pakeiskite savo slaptažodį kitokiu, nei buvo prieš tai.",
        "resetpass-temp-emailed": "Jūs prisijungęs laikinu slaptažodžiu, gautu per elektroninį paštą. Kad baigtumėte jungtis, čia turite nustatyti naują slaptažodį:",
        "resetpass-temp-password": "Laikinas slaptažodis:",
        "resetpass-abort-generic": "Slaptažodžio keitimas buvo nutrauktas nuo ekstenzijos.",
        "resetpass-expired": "Jūsų slaptažodžio galiojimas baigėsi. Prašome nustatyti naują prisijungimo slaptažodį.",
-       "resetpass-expired-soft": "Jūsų slaptažodžio galiojimas baigėsi ir jį reikia atkurti iš naujo. Pasirinkite naują slaptažodį dabar arba spauskite \"{{int:authprovider-resetpass-skip-label}}\", kad būtų atstatytas vėliau.",
-       "resetpass-validity-soft": "Jūsų slaptažodis netinkamas: $1\n\nPasirinkite naują slaptažodį dabar arba spauskite \"{{int:authprovider-resetpass-skip-label}}\", kad būtų atkurtas vėliau.",
+       "resetpass-expired-soft": "Jūsų slaptažodžio galiojimas baigėsi ir jį reikia pakeisti. Pasirinkite naują slaptažodį dabar arba spauskite \"{{int:authprovider-resetpass-skip-label}}\", kad būtų pakeistas vėliau.",
+       "resetpass-validity-soft": "Jūsų slaptažodis netinkamas: $1\n\nPasirinkite naują slaptažodį dabar arba spauskite \"{{int:authprovider-resetpass-skip-label}}\", kad būtų pakeistas vėliau.",
        "passwordreset": "Atkurti slaptažodį",
        "passwordreset-text-one": "Užpildykite šią formą, norėdami atkurti savo slaptažodį.",
        "passwordreset-text-many": "{{PLURAL:$1|Užpildykite vieną iš laukelių, kad el. paštu gautumėte laikinąjį slaptažodį.}}",
        "filehist-filesize": "Rinkmenos dydis",
        "filehist-comment": "Paaiškinimas",
        "imagelinks": "Rinkmenos naudojimas",
-       "linkstoimage": "{{PLURAL:$1|Šis puslapis|Šie puslapiai}} nurodo į šią rinkmeną:",
-       "linkstoimage-more": "Daugiau nei $1 {{PLURAL:$1|puslapis|puslapiai|puslapių}} rodo į šį failą.\nŠis sąrašas rodo tik {{PLURAL:$1|puslapio|pirmų $1 puslapių}} nuorodas į šį failą.\nYra pasiekiamas ir [[Special:WhatLinksHere/$2|visas sąrašas]].",
-       "nolinkstoimage": "Į rinkmeną nenurodo joks puslapis.",
+       "linkstoimage": "{{PLURAL:$1|Šis puslapis|Šie puslapiai}} naudoja šią rinkmeną:",
+       "linkstoimage-more": "Daugiau nei $1 {{PLURAL:$1|puslapis|puslapiai|puslapių}} naudoja šią rinkmeną.\nŠis sąrašas rodo tik {{PLURAL:$1|puslapį, naudojantį|pirmus $1 puslapius, naudojančius|pirmus $1 puslapių, naudojančių}} šį failą.\nYra pasiekiamas ir [[Special:WhatLinksHere/$2|visas sąrašas]].",
+       "nolinkstoimage": "Rinkmena nėra naudojama jokiame puslapyje.",
        "morelinkstoimage": "Žiūrėti [[Special:WhatLinksHere/$1|daugiau nuorodų]] į šį failą.",
        "linkstoimage-redirect": "$1 (failo peradresavimas) $2",
        "duplicatesoffile": "Šis failas turi {{PLURAL:$1|$1 dublikatą|$1 dublikatus|$1 dublikatų}} ([[Special:FileDuplicateSearch/$2|daugiau informacijos]]):",
index 23b3244..1918083 100644 (file)
@@ -54,6 +54,7 @@
        "tog-watchlisthideminor": "Annascunne 'e cagnamiente piccerille d' 'a lista 'e cuntrollo mia",
        "tog-watchlisthideliu": "Annascunne 'e cagnamiénte 'e l'utente riggistrate 'a l'elenco 'e cuntrollo",
        "tog-watchlistreloadautomatically": "Recarreca 'a lista 'e paggene cuntrullate automaticamente quanno nu filtro se fosse cagnato (ce buò 'o JavaScript)",
+       "tog-watchlistunwatchlinks": "Azzecca marcature dirette ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) pe' secutà/nun secutà 'e cagnamente a 'e paggene (ce buò 'o JavaScript pe' puté ausà sta funziunalità).",
        "tog-watchlisthideanons": "Annascunne 'e cagnamiente fatte d'anonime 'a l'elenco 'e cuntrollo",
        "tog-watchlisthidepatrolled": "Annascunne 'e modifiche cuntrullate 'a l'elenco 'e cuntrollo",
        "tog-watchlisthidecategorization": "Annascunne 'a categorizzazione d' 'e paggene",
        "customcssprotected": "Nun v'è permesso 'a cagnà sta paggena CSS, pecché cuntene 'e mpustaziune perzunale 'e n'at'utente.",
        "customjsonprotected": "Nun v'è permesso 'a cagnà sta paggena JSON, pecché cuntene 'e mpustaziune perzunale 'e n'at'utente.",
        "customjsprotected": "Nun v'è permesso 'a cagnà sta paggena JavaScript, pecché cuntene 'e mpustaziune perzunale 'e n'at'utente.",
+       "sitecssprotected": "Nun téne premmesse pe' puté cagnà sta paggena CSS pecché putesse dà prubbleme a 'e vvisite",
+       "sitejsonprotected": "Nun téne premmesse pe' puté cagnà sta paggena JSON pecché putesse dà prubbleme a 'e vvisite",
        "mycustomcssprotected": "Nun v'è permesso 'a cagnà sta paggena CSS.",
        "mycustomjsonprotected": "Nun v'è permesso 'a cagnà sta paggena JSON.",
        "mycustomjsprotected": "Nun v'è licenzia pe cagnà sta paggena JavaScript.",
        "filehist-filesize": "Dimenziune d\"o file",
        "filehist-comment": "Commento",
        "imagelinks": "Jonte ê ffiure",
-       "linkstoimage": "{{PLURAL:$1|Sta paggena cullega|$1 'e sti paggene cullegano}} a stu file:",
-       "linkstoimage-more": "Cchiù 'e $1 {{PLURAL:$1|paggena cullega|paggene cullegano}} a stu file.<br />\nL'elenco ccà abbascio fà vedé {{PLURAL:$1|'a primma paggena ca cullega|'e primme $1 paggene ca cullegano}} sulamente a stu file.<br />\nNa [[Special:WhatLinksHere/$2|lista completa]] è a disposizione.",
-       "nolinkstoimage": "Nisciuna paggena cullega a stu file.",
+       "linkstoimage": "{{PLURAL:$1|Pe sta paggena ce buò|Pe $1 'e sti paggene ce buò}} stu file:",
+       "linkstoimage-more": "Cchiù 'e $1 {{PLURAL:$1|paggena buò|paggene vonno}} stu file.<br />\nL'elenco ccà abbascio fà vedé {{PLURAL:$1|'a primma paggena ca buò|'e primme $1 paggene ca vonno}} sulamente a stu file.<br />\nNa [[Special:WhatLinksHere/$2|lista completa]] è a disposizione.",
+       "nolinkstoimage": "Pe' nisciuna paggena ce buò stu file.",
        "morelinkstoimage": "Vide [[Special:WhatLinksHere/$1|cchiù cullegamiente]] a stu file.",
        "linkstoimage-redirect": "$1 (redirezionamiente d' 'o file) $2",
        "duplicatesoffile": "{{PLURAL:$1|'O file ccà abbascio è nu duplicato|'E $1 file ccà abbascio songo duplicate}} 'e stu file ([[Special:FileDuplicateSearch/$2|cchiù nfurmaziune]]):",
index cffe621..dee448f 100644 (file)
        "recentchangeslinked-page": "Sidenavn:",
        "recentchangeslinked-to": "Vis endringer på sider som lenker til den gitte siden istedet",
        "recentchanges-page-added-to-category": "[[:$1]] ble lagt til i kategorien",
-       "recentchanges-page-added-to-category-bundled": "[[:$1]] lagt til i kategori, [[Special:WhatLinksHere/$1|denne siden er inkludert i andre sider]]",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] lagt til i kategorien; [[Special:WhatLinksHere/$1|denne siden er inkludert i andre sider]]",
        "recentchanges-page-removed-from-category": "[[:$1]] fjernet fra kategori",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] fjernet fra kategori, [[Special:WhatLinksHere/$1|denne siden er inkludert i andre sider]]",
        "autochange-username": "Automatisk MediaWiki-endring",
index c3c8b24..e8d6c7a 100644 (file)
        "logout": "Ofmelden",
        "userlogout": "Aofmelden",
        "notloggedin": "Neet an-emelded",
-       "userlogin-noaccount": "Heb jy noch gyn gebrukersname?",
+       "userlogin-noaccount": "Heb jy noch geen gebrukersname?",
        "userlogin-joinproject": "Wörd lid van {{SITENAME}}",
        "createaccount": "Inskryven",
        "userlogin-resetpassword-link": "Juuw wachtwoord vergeaten?",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|dag|dagen}}",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|uur|uren}}",
        "rcfilters-quickfilters": "Up-eslöägen filters",
-       "rcfilters-quickfilters-placeholder-title": "Noch gyn filters up-eslöägen",
+       "rcfilters-quickfilters-placeholder-title": "Noch geen filters up-eslöägen",
        "rcfilters-quickfilters-placeholder-description": "Üm juuw filterinstellingen up te slån en et låter te gebruken, klik up et bladwyserikoon underan by \"Aktive filters\".",
        "rcfilters-savedqueries-apply-label": "Instellingen opslaon",
        "rcfilters-savedqueries-cancel-label": "Aofbreken",
        "rcfilters-restore-default-filters": "Standardfilters weerummezetten",
        "rcfilters-clear-all-filters": "Alle filters vortdoon",
        "rcfilters-show-new-changes": "Låt nyste wysigingen seen",
-       "rcfilters-search-placeholder": "Filter wysigingen (gebruuk et menü of söök up filtername)",
+       "rcfilters-search-placeholder": "Filter wysigingen (gebruuk et menu of söök up filtername)",
        "rcfilters-filterlist-feedbacklink": "Låt uns weaten wat jy van disse (nye) filterhülpmiddels vinden",
        "rcfilters-highlightbutton-title": "Resultåten markeren",
        "rcfilters-highlightmenu-title": "Kies n kleur",
index 1e8ddfd..3faa6ba 100644 (file)
        "watchnologin": "Nie jesteś zalogowany",
        "addwatch": "Dodaj do listy obserwowanych",
        "addedwatchtext": "Strona „[[:$1|$1]]” wraz ze swoją stroną dyskusji została dodana do Twojej [[Special:Watchlist|listy obserwowanych]].",
-       "addedwatchtext-talk": "Strona „[[:$1]]” i strony z nią związane zostały dodane do Twojej [[Special:Watchlist|listy obserwowanych]].",
+       "addedwatchtext-talk": "Strona „[[:$1]]” i strona z nią powiązana zostały dodane do Twojej [[Special:Watchlist|listy obserwowanych]].",
        "addedwatchtext-short": "Strona „$1” została dodana do twojej listy obserwowanych.",
        "removewatch": "Usuń z listy obserwowanych",
        "removedwatchtext": "Strona „[[:$1|$1]]” wraz ze swoją stroną dyskusji została usunięta z Twojej [[Special:Watchlist|listy obserwowanych]].",
-       "removedwatchtext-talk": "Strona „[[:$1]]” i strony z nią związane zostały usunięte z Twojej [[Special:Watchlist|listy obserwowanych]].",
+       "removedwatchtext-talk": "Strona „[[:$1]]” i strona z nią powiązana zostały usunięte z Twojej [[Special:Watchlist|listy obserwowanych]].",
        "removedwatchtext-short": "Strona „$1” została usunięta z twojej listy obserwowanych.",
        "watch": "Obserwuj",
        "watchthispage": "Obserwuj",
index 4b1717d..b6558b6 100644 (file)
        "colon-separator": "{{optional}}\nChange it only if your language uses another character for ':' or it needs an extra space before the colon.",
        "autocomment-prefix": "{{notranslate}}",
        "pipe-separator": "{{optional}}",
-       "word-separator": "{{optional}}\nThis is a string which is (usually) put between words of the language. It is used, e.g. when messages are concatenated (appended to each other). Note that you must express a space as html entity &amp;#32; because the editing and updating process strips leading and trailing spaces from messages.\n\nMost languages use a space, but some Asian languages, such as Thai and Chinese, do not.",
+       "word-separator": "{{optional}}\nThis is a string which is (usually) put between words of the language. It is used, e.g. when messages are concatenated (appended to each other). Note that you must express a space as html entity &amp;#32; because the editing and updating process strips leading and trailing spaces from messages.\n\nMost languages use a space, but some Asian languages, such as Thai and Chinese, do not.\n{{Format|plain}}",
        "ellipsis": "{{optional}}",
        "percent": "{{optional}}",
        "parentheses": "{{optional}}",
-       "brackets": "{{Optional}}",
+       "brackets": "{{Optional}}\n{{Format|plain}}",
        "quotation-marks": "Quotation marks, for quoting, sometimes titles etc., depending on the language.\n\nSee: [[w:Non-English usage of quotation marks|Non-English usage of quotation marks on Wikipedia]].\n\nParameters:\n* $1 - text to be wrapped in quotation marks",
        "imgmultipageprev": "{{Identical|Previous page}}",
        "imgmultipagenext": "{{Identical|Next page}}",
index 0bad36e..b7218fa 100644 (file)
        "blanknamespace": "(Сүрүн)",
        "contributions": "{{GENDER:$1|Кыттааччы}} суруйуута (кылаата)",
        "contributions-title": "$1 кыттааччы киллэрбит уларытыылара",
-       "mycontris": "Суруйуу тиһигэ",
+       "mycontris": "Суруйуу испииһэгэ",
        "anoncontribs": "Суруйуу тиһилигэ",
        "contribsub2": "$1 ($2) суруйуута",
        "contributions-userdoesnotexist": "Маннык \"$1\" кыттааччы аата бэлиэтэниллибэтэх.",
index 1e285b5..8ab9ee4 100644 (file)
        "sunday": "ⴰⵙⴰⵎⴰⵙ",
        "monday": "ⴰⵢⵏⴰⵙ",
        "tuesday": "ⴰⵙⵉⵏⴰⵙ",
-       "wednesday": "Akras",
-       "thursday": "ⴰⴽⵡⴰⵙ",
-       "friday": "â´°âµ\99âµ\89âµ\8eⵡⴰâµ\99",
-       "saturday": "asidyas",
-       "sun": "asamas",
-       "mon": "Aynas",
-       "tue": "Asinas",
-       "wed": "Akras",
-       "thu": "Akwas",
-       "fri": "Asimwas",
-       "sat": "Asidyas",
+       "wednesday": "ⵍⴰⵔⴱⵄ",
+       "thursday": "âµ\8dâµ\85âµ\8eâµ\89ⵙ",
+       "friday": "âµ\8dâµ\8aâ´°âµ\8eâµ\84",
+       "saturday": "ⵙⵙⴱⵜ",
+       "sun": "ⵍⵃⴷⴷ",
+       "mon": "ⵍⵜⵏⵉⵏ",
+       "tue": "ⵟⵟⵍⴰⵜⴰ",
+       "wed": "ⵍⴰⵔⴱⵄ",
+       "thu": "ⵍⵅⵎⵉⵙ",
+       "fri": "ⵍⵊⴰⵎⵄ",
+       "sat": "ⵙⵙⴱⵜ",
        "january": "ⵉⵏⵏⴰⵢⵔ",
        "february": "ⴼⴱⵔⴰⵢⵔ",
        "march": "ⵎⴰⵔⵚ",
        "june": "ⵢⵓⵏⵢⵓ",
        "july": "ⵢⵓⵍⵢⵓⵣ",
        "august": "ⵖⵓⵛⵜ",
-       "september": "âµ\9bâµ\93âµ\9câ´°âµ\8fⴱⵉⵔ",
+       "september": "âµ\9bâµ\93âµ\9câ´°âµ\8eⴱⵉⵔ",
        "october": "ⴽⵜⵓⴱⵔ",
-       "november": "âµ\8fâµ\93ⵡⴰâµ\8fⴱⵉⵔ",
-       "december": "â´·âµ\93âµ\8aâ´°âµ\8fⴱⵉⵔ",
+       "november": "âµ\8fâµ\93ⵡⴰâµ\8eⴱⵉⵔ",
+       "december": "â´·âµ\93âµ\8aâ´°âµ\8eⴱⵉⵔ",
        "january-gen": "ⵉⵏⵏⴰⵢⵔ",
        "february-gen": "ⴼⴱⵔⴰⵢⵔ",
        "march-gen": "ⵎⴰⵔⵚ",
        "june-gen": "ⵢⵓⵏⵢⵓ",
        "july-gen": "ⵢⵓⵍⵢⵓⵣ",
        "august-gen": "ⵖⵓⵛⵜ",
-       "september-gen": "âµ\9bâµ\93âµ\9câ´°âµ\8fⴱⵉⵔ",
+       "september-gen": "âµ\9bâµ\93âµ\9câ´°âµ\8eⴱⵉⵔ",
        "october-gen": "ⴽⵜⵓⴱⵔ",
        "november-gen": "ⵏⵓⵡⴰⵎⴱⵉⵔ",
-       "december-gen": "â´·âµ\93âµ\8aâ´°âµ\8fⴱⵉⵔ",
+       "december-gen": "â´·âµ\93âµ\8aâ´°âµ\8eⴱⵉⵔ",
        "jan": "ⵉⵏⵏ",
        "feb": "brayr",
-       "mar": "âµ\8eâ´°âµ\94",
+       "mar": "âµ\8eâ´°âµ\95",
        "apr": "ⴰⴱⵔ",
        "may": "ⵎⴰⵢ",
        "jun": "ⵢⵓⵏ",
        "june-date": "$1 ⵢⵓⵏⵢⵓ",
        "july-date": "$1 ⵢⵓⵍⵢⵓⵣ",
        "august-date": "$1 ⵖⵓⵛⵜ",
-       "september-date": "$1 âµ\9bâµ\93âµ\9câ´°âµ\8fⴱⵉⵔ",
+       "september-date": "$1 âµ\9bâµ\93âµ\9câ´°âµ\8eⴱⵉⵔ",
        "october-date": "$1 ⴽⵜⵓⴱⵔ",
-       "november-date": "$1 âµ\8fâµ\93ⵡⴰâµ\8fⴱⵉⵔ",
-       "december-date": "$1 â´·âµ\93âµ\8aâ´°âµ\8fⴱⵉⵔ",
+       "november-date": "$1 âµ\8fâµ\93ⵡⴰâµ\8eⴱⵉⵔ",
+       "december-date": "$1 â´·âµ\93âµ\8aâ´°âµ\8eⴱⵉⵔ",
        "pagecategories": "{{PLURAL:$1|ⵜⴰⴳⴳⴰⵢⵜ|ⵜⴰⴳⴳⴰⵢⵉⵏ}}",
        "category_header": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⵏ ⵜⴰⴳⴳⴰⵢⵜ \"$1\"",
        "subcategories": "ⵜⵉⴷⵓⴳⴳⴰⵢⵉⵏ",
        "updatedmarker": "Tuybddal z tizrink li iğuran",
        "printableversion": "ⴰⵎⴱⵔⵉⵎⵉ ⵜⴰⵙⵏⴰ ⴰⴷ",
        "permalink": "Azday Bdda illan",
-       "print": "Siggz",
+       "print": "ⴰⵎⴱⵔⵉⵎⵉ",
        "edit": "ⵙⵏⴼⵍ",
        "create": "ⵙⵏⵓⵍⴼⵓ",
        "delete": "ⴽⴽⵙ",
index ef4d5d3..ae7317d 100644 (file)
        "tog-hidecategorization": "Сакриј категоризацију страница",
        "tog-extendwatchlist": "Прошири списак надгледања за поглед свих промена, не само скорашњих",
        "tog-usenewrc": "Групиши измене по страници у скорашњим изменама и списку надгледања",
-       "tog-numberheadings": "Ð\90Ñ\83Ñ\82омаÑ\82Ñ\81ки Ð½Ñ\83меÑ\80иÑ\88и Ð¿Ð¾Ð´Ð½Ð°Ñ\81лове",
+       "tog-numberheadings": "Аутоматски нумериши наслове",
        "tog-showtoolbar": "Прикажи траку са алаткама за уређивање",
        "tog-editondblclick": "Уреди странице двоструким кликом",
        "tog-editsectiononrightclick": "Омогући уређивање одељака десним кликом на њихове наслове",
        "tog-watchcreations": "Додај странице које направим и датотеке које отпремим на мој списак надгледања",
        "tog-watchdefault": "Додај странице и датотеке које уредим на мој списак надгледања",
        "tog-watchmoves": "Додај странице и датотеке које преместим на мој списак надгледања",
-       "tog-watchdeletion": "Ð\94одаÑ\98 Ñ\81Ñ\82Ñ\80аниÑ\86е Ð¸ Ð´Ð°Ñ\82оÑ\82еке ÐºÐ¾Ñ\98е Ð¾бришем на мој списак надгледања",
+       "tog-watchdeletion": "Ð\94одаÑ\98 Ñ\81Ñ\82Ñ\80аниÑ\86е Ð¸ Ð´Ð°Ñ\82оÑ\82еке ÐºÐ¾Ñ\98е Ð¸Ð·бришем на мој списак надгледања",
        "tog-watchuploads": "Додај датотеке које отпремим на мој списак надгледања",
        "tog-watchrollback": "Додај странице на којима сам извршио враћање измена на мој списак надгледања",
        "tog-minordefault": "Означавај све измене као мање",
@@ -85,7 +85,7 @@
        "tog-showhiddencats": "Прикажи скривене категорије",
        "tog-norollbackdiff": "Не приказуј разлику након извршеног враћања",
        "tog-useeditwarning": "Упозори ме када напуштам страницу за уређивање са несачуваним променама",
-       "tog-prefershttps": "Увек користи сигурну везу док сам пријављен/а.",
+       "tog-prefershttps": "Увек ÐºÐ¾Ñ\80иÑ\81Ñ\82и Ñ\81игÑ\83Ñ\80нÑ\83 Ð²ÐµÐ·Ñ\83 Ð´Ð¾Ðº Ñ\81ам Ð¿Ñ\80иÑ\98авÑ\99ен/на.",
        "underline-always": "увек",
        "underline-never": "никад",
        "underline-default": "према теми или прегледачу",
        "edit-local": "Уреди локални опис",
        "create": "Направи",
        "create-local": "Додај локални опис",
-       "delete": "Ð\9eбриши",
-       "undelete_short": "Ð\92Ñ\80аÑ\82и {{PLURAL:$1|обÑ\80иÑ\81анÑ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83|$1 Ð¾Ð±Ñ\80иÑ\81ане Ð¸Ð·Ð¼ÐµÐ½Ðµ|$1 Ð¾брисаних измена}}",
-       "viewdeleted_short": "Ð\9fогледаÑ\98 {{PLURAL:$1|Ñ\98еднÑ\83 Ð¾Ð±Ñ\80иÑ\81анÑ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83|$1 Ð¾Ð±Ñ\80иÑ\81ане Ð¸Ð·Ð¼ÐµÐ½Ðµ|$1 Ð¾брисаних измена}}",
+       "delete": "Ð\98збриши",
+       "undelete_short": "Ð\92Ñ\80аÑ\82и {{PLURAL:$1|избÑ\80иÑ\81анÑ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83|$1 Ð¸Ð·Ð±Ñ\80иÑ\81ане Ð¸Ð·Ð¼ÐµÐ½Ðµ|$1 Ð¸Ð·брисаних измена}}",
+       "viewdeleted_short": "Ð\9fогледаÑ\98 {{PLURAL:$1|Ñ\98еднÑ\83 Ð¸Ð·Ð±Ñ\80иÑ\81анÑ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83|$1 Ð¸Ð·Ð±Ñ\80иÑ\81ане Ð¸Ð·Ð¼ÐµÐ½Ðµ|$1 Ð¸Ð·брисаних измена}}",
        "protect": "Заштити",
        "protect_change": "промени",
        "unprotect": "Промени заштиту",
        "confirmable-no": "Не",
        "thisisdeleted": "Погледај или врати $1?",
        "viewdeleted": "Погледај $1?",
-       "restorelink": "{{PLURAL:$1|Ñ\98еднÑ\83 Ð¾Ð±Ñ\80иÑ\81анÑ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83|$1 Ð¾Ð±Ñ\80иÑ\81ане Ð¸Ð·Ð¼ÐµÐ½Ðµ|$1 Ð¾брисаних измена}}",
+       "restorelink": "{{PLURAL:$1|Ñ\98еднÑ\83 Ð¸Ð·Ð±Ñ\80иÑ\81анÑ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83|$1 Ð¸Ð·Ð±Ñ\80иÑ\81ане Ð¸Ð·Ð¼ÐµÐ½Ðµ|$1 Ð¸Ð·брисаних измена}}",
        "feedlinks": "Фид:",
        "feed-invalid": "Неважећи тип пријаве на фид.",
        "feed-unavailable": "Фидови синдикације нису доступни",
        "nstab-category": "Категорија",
        "mainpage-nstab": "Главна страна",
        "nosuchaction": "Нема такве радње",
-       "nosuchactiontext": "РадÑ\9aа Ð½Ð°Ð²ÐµÐ´ÐµÐ½Ð° Ñ\83 URL-Ñ\83 Ð½Ð¸Ñ\98е Ð²Ð°Ð»Ð¸Ð´Ð½а.\nМожда сте откуцали погрешан URL-а или сте пратили покварен линк.\nОво такође може да указује на грешку у софтверу који користи {{SITENAME}}.",
+       "nosuchactiontext": "РадÑ\9aа ÐºÐ¾Ñ\98а Ñ\98е Ð½Ð°Ð²ÐµÐ´ÐµÐ½Ð° Ñ\83 URL-Ñ\83 Ð½Ð¸Ñ\98е Ð²Ð°Ð¶ÐµÑ\9bа.\nМожда сте откуцали погрешан URL-а или сте пратили покварен линк.\nОво такође може да указује на грешку у софтверу који користи {{SITENAME}}.",
        "nosuchspecialpage": "Нема такве посебне странице",
        "nospecialpagetext": "<strong>Захтевали сте невалидну посебну страницу.</strong>\n\nСписак валидних посебних страница може да се пронађе на „[[Special:SpecialPages|{{int:specialpages}}]]”.",
        "error": "Грешка",
        "readonly": "База података је закључана",
        "enterlockreason": "Унесите разлог за закључавање, укључујући и време откључавања",
        "readonlytext": "База података је тренутно закључана, што значи да је није могуће мењати.\n\nСистемски администратор је навео следеће објашњење: $1",
-       "missing-article": "ТекÑ\81Ñ\82 Ñ\81Ñ\82Ñ\80аниÑ\86е Ð¿Ð¾Ð´ Ð½Ð°Ð·Ð¸Ð²Ð¾Ð¼ â\80\9e$1â\80\9c ($2) Ð½Ð¸Ñ\98е Ð¿Ñ\80онаÑ\92ен.\n\nУзÑ\80ок Ð¾Ð²Ðµ Ð³Ñ\80еÑ\88ке Ñ\98е Ð¾Ð±Ð¸Ñ\87но Ð·Ð°Ñ\81Ñ\82аÑ\80ела Ð¸Ð·Ð¼ÐµÐ½Ð° Ð¸Ð»Ð¸ Ð»Ð¸Ð½Ðº Ð´Ð¾ Ð¾брисане странице.\n\nАко се не ради о томе, онда сте вероватно пронашли грешку у софтверу.\nПријавите је [[Special:ListUsers/sysop|администратору]] уз одговарајући линк.",
+       "missing-article": "ТекÑ\81Ñ\82 Ñ\81Ñ\82Ñ\80аниÑ\86е Ð¿Ð¾Ð´ Ð½Ð°Ð·Ð¸Ð²Ð¾Ð¼ â\80\9e$1â\80\9c ($2) Ð½Ð¸Ñ\98е Ð¿Ñ\80онаÑ\92ен.\n\nУзÑ\80ок Ð¾Ð²Ðµ Ð³Ñ\80еÑ\88ке Ñ\98е Ð¾Ð±Ð¸Ñ\87но Ð·Ð°Ñ\81Ñ\82аÑ\80ела Ð¸Ð·Ð¼ÐµÐ½Ð° Ð¸Ð»Ð¸ Ð»Ð¸Ð½Ðº Ð´Ð¾ Ð¸Ð·брисане странице.\n\nАко се не ради о томе, онда сте вероватно пронашли грешку у софтверу.\nПријавите је [[Special:ListUsers/sysop|администратору]] уз одговарајући линк.",
        "missingarticle-rev": "(ревизија#: $1)",
        "missingarticle-diff": "(разлика: $1, $2)",
        "readonly_lag": "База података је аутоматски закључана да би се секундарни сервери базе података ускладили с главним.",
        "internalerror-fatal-exception": "Грешка необрађеног изузетка типа „$1“",
        "filecopyerror": "Не могу да копирам датотеку „$1“ у „$2“.",
        "filerenameerror": "Не могу да преименујем датотеку „$1“ у „$2“.",
-       "filedeleteerror": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¾бришем датотеку „$1“.",
+       "filedeleteerror": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¸Ð·бришем датотеку „$1“.",
        "directorycreateerror": "Не могу да направим директоријум „$1“.",
        "directoryreadonlyerror": "Директоријум „$1“ је само за читање.",
        "directorynotreadableerror": "Директоријум „$1“ није читљив.",
        "unexpected": "Неочекивана вредност: „$1“=„$2“.",
        "formerror": "Грешка: не могу да пошаљем образац.",
        "badarticleerror": "Ова радња се не може извршити на овој страници.",
-       "cannotdelete": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¾Ð±Ñ\80иÑ\88ем Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ð¸Ð»Ð¸ Ð´Ð°Ñ\82оÑ\82екÑ\83 â\80\9e$1â\80\9c.\nÐ\92еÑ\80оваÑ\82но Ñ\98Ñ\83 Ñ\98е Ð½ÐµÐºÐ¾ Ð´Ñ\80Ñ\83ги Ð¾брисао.",
-       "cannotdelete-title": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¾бришем страницу „$1“",
+       "cannotdelete": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¸Ð·Ð±Ñ\80иÑ\88ем Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ð¸Ð»Ð¸ Ð´Ð°Ñ\82оÑ\82екÑ\83 â\80\9e$1â\80\9c.\nÐ\9cогÑ\83Ñ\9bе Ñ\98е Ð´Ð° Ñ\98Ñ\83 Ñ\98е Ð½ÐµÐºÐ¾ Ð²ÐµÑ\9b Ð¸Ð·брисао.",
+       "cannotdelete-title": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¸Ð·бришем страницу „$1“",
        "delete-hook-aborted": "Брисање је прекинула кука.\nНије дато никакво образложење.",
        "no-null-revision": "Не могу да направим нову ништавну ревизију странице „$1“",
        "badtitle": "Лош наслов",
-       "badtitletext": "Ð\97аÑ\85Ñ\82евани наслов странице је неважећи, празан или је погрешно повезан међујезички или међувики наслов.\nМожда садржи један или више знакова који се не могу користити у насловима.",
+       "badtitletext": "ТÑ\80ажени наслов странице је неважећи, празан или је погрешно повезан међујезички или међувики наслов.\nМожда садржи један или више знакова који се не могу користити у насловима.",
        "title-invalid-empty": "Тражено име странице је празно или садржи само назив именског простора.",
        "title-invalid-utf8": "Тражени назив странице садржи неважећи UTF-8 знак.",
-       "title-invalid-interwiki": "Тражени наслов странице садржи унутрашњу вики везу која не може бити кориштена у насловима.",
+       "title-invalid-interwiki": "Тражени наслов странице садржи унутрашњи вики линк који не може да се користи у насловима.",
        "title-invalid-talk-namespace": "Тражени наслов странице се односи на страницу за разговор која не може постојати.",
        "title-invalid-characters": "Тражени наслов има неважеће знакове: „$1“.",
        "title-invalid-relative": "Наслов има релативну путању. Релативни наслови страница (./, ../) нису важећи јер ће често бити недоступни у корисничком прегледачу.",
        "title-invalid-magic-tilde": "Тражени наслов странице садржи неважећи след магичног знака тилда (<nowiki>~~~</nowiki>).",
        "title-invalid-too-long": "Тражени назив странице је предугачак. Не сме бити дужи од $1 {{PLURAL:$1|бајта|бајтова}} у UTF-8 кодирању.",
-       "title-invalid-leading-colon": "Ð\97аÑ\85Ñ\82евани наслов странице садржи неважећу двотачку на почетку.",
+       "title-invalid-leading-colon": "ТÑ\80ажени наслов странице садржи неважећу двотачку на почетку.",
        "perfcached": "Следећи подаци су кеширани и можда нису ажурирани. У кешу {{PLURAL:$1|је доступан највише један резултат|су доступна највише $1 резултата|је доступно највише $1 резултата}}.",
        "perfcachedts": "Следећи подаци су кеширани и последњи пут ажурирани на датум $2 у $3 ч. У кешу {{PLURAL:$4|је доступан највише један резултат|су доступна највише $4 резултата|је доступно највише $4 резултата}}.",
        "querypage-no-updates": "Ажурирање ове странице је тренутно онемогућено.\nПодаци који се овде налазе могу бити застарели.",
        "botpasswords-label-create": "Направи",
        "botpasswords-label-update": "Ажурирај",
        "botpasswords-label-cancel": "Откажи",
-       "botpasswords-label-delete": "Ð\9eбриши",
+       "botpasswords-label-delete": "Ð\98збриши",
        "botpasswords-label-resetpassword": "Ресетуј лозинку",
        "botpasswords-label-grants": "Применљиве дозволе:",
        "botpasswords-label-grants-column": "Одобрено",
        "botpasswords-bad-appid": "Име бота „$1” није валидно.",
        "botpasswords-insert-failed": "Неуспело додавање бота под именом „$1”. Можда је већ додат?",
-       "botpasswords-update-failed": "Ð\9dеÑ\83Ñ\81пело Ð°Ð¶Ñ\83Ñ\80иÑ\80аÑ\9aе Ð±Ð¾Ñ\82а Ð¿Ð¾Ð´ Ð½Ð°Ð·Ð¸Ð²Ð¾Ð¼ â\80\9e$1â\80\9d. Ð\94а Ð»Ð¸ Ñ\98е Ð¾брисан?",
+       "botpasswords-update-failed": "Ð\9dеÑ\83Ñ\81пело Ð°Ð¶Ñ\83Ñ\80иÑ\80аÑ\9aе Ð±Ð¾Ñ\82а Ð¿Ð¾Ð´ Ð½Ð°Ð·Ð¸Ð²Ð¾Ð¼ â\80\9e$1â\80\9d. Ð\94а Ð½Ð¸Ñ\98е Ð¸Ð·брисан?",
        "botpasswords-created-title": "Направљена лозинка бота",
        "botpasswords-created-body": "Лозинка за бота „$1” корисника „$2” је направљена.",
        "botpasswords-updated-title": "Лозинка бота промењена",
        "botpasswords-updated-body": "Лозинка за бота „$1” корисника „$2” је ажурирана.",
-       "botpasswords-deleted-title": "Ð\9eбрисана лозинка бота",
-       "botpasswords-deleted-body": "Лозинка за бота „$1” корисника „$2” је обрисана.",
+       "botpasswords-deleted-title": "Ð\98збрисана лозинка бота",
+       "botpasswords-deleted-body": "Лозинка за бота „$1” {{GENDER:$2|корисника|кориснице}} „$2” је избрисана.",
        "botpasswords-no-provider": "BotPasswordsSessionProvider није доступан.",
        "botpasswords-restriction-failed": "Не можете се пријавити због ограничења лозинки за ботове.",
        "botpasswords-not-exist": "Корисник „$1“ нема лозинку бота „$2“.",
-       "resetpass_forbidden": "Ð\9bозинка Ð½Ðµ Ð¼Ð¾Ð¶Ðµ Ð±Ð¸Ñ\82и Ð¿Ñ\80омеÑ\9aена",
-       "resetpass_forbidden-reason": "Ð\9bозинке Ð½Ð¸Ñ\98е Ð¼Ð¾Ð³Ñ\83Ñ\9bе Ð¿Ñ\80омениÑ\82и: $1",
+       "resetpass_forbidden": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¿Ñ\80оменим Ð»Ð¾Ð·Ð¸Ð½ÐºÐµ",
+       "resetpass_forbidden-reason": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¿Ñ\80оменим Ð»Ð¾Ð·Ð¸Ð½ÐºÐµ: $1",
        "resetpass-no-info": "Морате бити пријављени да бисте приступили овој страници.",
        "resetpass-submit-loggedin": "Промени лозинку",
        "resetpass-submit-cancel": "Откажи",
        "changeemail-header": "Попуните овај образац да би сте променили вашу имејл-адресу. Ако бисте желели да уклоните повезаност било које имејл-адресе са вашег налога, оставите празно поље за нову имејл-адресу када шаљете образац.",
        "changeemail-no-info": "Морате бити пријављени да бисте приступили овој страници.",
        "changeemail-oldemail": "Актуелна имејл-адреса:",
-       "changeemail-newemail": "Нова имејл адреса:",
+       "changeemail-newemail": "Нова имејл-адреса:",
        "changeemail-none": "(ништа)",
        "changeemail-password": "Ваша лозинка за пројекат {{SITENAME}}:",
        "changeemail-submit": "Промени имејл",
        "changeemail-throttled": "Превише пута сте покушали да се пријавите.\nМолимо вас да сачекате $1 пре него што покушате поново.",
-       "changeemail-nochange": "Унесите другу имејл адресу.",
+       "changeemail-nochange": "Унесите другу имејл-адресу.",
        "resettokens": "Ресетовање токена",
        "resettokens-text": "Можете поново поставити жетоне који ће вам омогућити приступ одређеним приватним подацима повезаним са вашим налогом овде.\n\nТребали бисте то да урадите ако их мимо воље поделите са неким или ако је ваш налог угрожен.",
        "resettokens-no-tokens": "Нема жетона за ресетовање.",
        "link_sample": "Наслов линка",
        "link_tip": "Унутрашњи линк",
        "extlink_sample": "http://www.example.com/ наслов линка",
-       "extlink_tip": "СпоÑ\99аÑ\88Ñ\9aа Ð²ÐµÐ·Ð° (Ñ\81 префиксом http://)",
+       "extlink_tip": "СпоÑ\99аÑ\88Ñ\9aи Ð»Ð¸Ð½Ðº (Ñ\81а префиксом http://)",
        "headline_sample": "Текст наслова",
        "headline_tip": "Поднаслов (ниво 2)",
        "nowiki_sample": "Овде уметните необликован текст",
        "subject-preview": "Преглед теме:",
        "previewerrortext": "Дошло је до грешке при покушају прегледа промена.",
        "blockedtitle": "Корисник је блокиран",
-       "blockedtext": "<strong>Ð\92аÑ\88е ÐºÐ¾Ñ\80иÑ\81ниÑ\87ко Ð¸Ð¼Ðµ Ð¸Ð»Ð¸ IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ана.</strong>\n\nÐ\91локиÑ\80аÑ\9aе Ñ\98е {{GENDER:$4|извÑ\80Ñ\88ио|извÑ\80Ñ\88ила}} $1.\nРазлог Ñ\98е <em>$2</em>.\n\n* Ð\9fоÑ\87еÑ\82ак Ð±Ð»Ð¾ÐºÐ°Ð´Ðµ: $8\n* Ð\9aÑ\80аÑ\98 Ð±Ð»Ð¾ÐºÐ°Ð´Ðµ: $6\n* Ð\91локиÑ\80ани ÐºÐ¾Ñ\80иÑ\81ник: $7\n\nÐ\9cожеÑ\82е Ð´Ð° ÐºÐ¾Ð½Ñ\82акÑ\82иÑ\80аÑ\82е {{GENDER:$4|коÑ\80иÑ\81ника|коÑ\80иÑ\81ниÑ\86Ñ\83}} $1 Ð¸Ð»Ð¸ Ð´Ñ\80Ñ\83гог [[{{MediaWiki:Grouppage-sysop}}|админиÑ\81Ñ\82Ñ\80аÑ\82оÑ\80а]] Ð´Ð° Ð±Ð¸Ñ\81Ñ\82е Ñ\80азговаÑ\80али Ð¾ Ð±Ð»Ð¾ÐºÐ°Ð´Ð¸.\nÐ\9dе Ð¼Ð¾Ð¶ÐµÑ\82е Ð´Ð° ÐºÐ¾Ñ\80иÑ\81Ñ\82иÑ\82е Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82 â\80\9e{{int:emailuser}}â\80\9d Ð¾Ñ\81им Ð°ÐºÐ¾ Ñ\81Ñ\82е Ð½Ð°Ð²ÐµÐ»Ð¸ Ð²Ð°Ñ\99анÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ñ\83 Ñ\81воÑ\98им [[Special:Preferences|подеÑ\88аваÑ\9aима Ð½Ð°Ð»Ð¾Ð³Ð°]] Ð¸ Ð½Ð¸Ñ\81Ñ\82е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ани Ð¾Ð´ ÐºÐ¾Ñ\80иÑ\88Ñ\9bеÑ\9aа Ð¸Ñ\81Ñ\82е.\nÐ\92аÑ\88а Ð°ÐºÑ\82Ñ\83елна IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е $3, Ð° ID Ð±Ð»Ð¾ÐºÐ°Ð´Ðµ #$5.\nУкÑ\99Ñ\83Ñ\87ите све горње детаље при прављењу било каквих упита.",
+       "blockedtext": "<strong>Ð\92аÑ\88е ÐºÐ¾Ñ\80иÑ\81ниÑ\87ко Ð¸Ð¼Ðµ Ð¸Ð»Ð¸ IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ана.</strong>\n\nÐ\91локиÑ\80аÑ\9aе Ñ\98е {{GENDER:$4|извÑ\80Ñ\88ио|извÑ\80Ñ\88ила}} $1.\nРазлог Ñ\98е <em>$2</em>.\n\n* Ð\9fоÑ\87еÑ\82ак Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aа: $8\n* Ð\98Ñ\81Ñ\82ек Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aа: $6\n* Ð\91локиÑ\80ани: $7\n\nÐ\9cожеÑ\82е Ð´Ð° ÐºÐ¾Ð½Ñ\82акÑ\82иÑ\80аÑ\82е {{GENDER:$4|коÑ\80иÑ\81ника|коÑ\80иÑ\81ниÑ\86Ñ\83}} $1 Ð¸Ð»Ð¸ Ð´Ñ\80Ñ\83гог [[{{MediaWiki:Grouppage-sysop}}|админиÑ\81Ñ\82Ñ\80аÑ\82оÑ\80а]] Ð´Ð° Ð±Ð¸Ñ\81Ñ\82е Ñ\80азговаÑ\80али Ð¾ Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aÑ\83.\nÐ\9dе Ð¼Ð¾Ð¶ÐµÑ\82е Ð´Ð° ÐºÐ¾Ñ\80иÑ\81Ñ\82иÑ\82е Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82 â\80\9e{{int:emailuser}}â\80\9d Ð¾Ñ\81им Ð°ÐºÐ¾ Ñ\81Ñ\82е Ð½Ð°Ð²ÐµÐ»Ð¸ Ð²Ð°Ð»Ð¸Ð´Ð½Ñ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ñ\83 Ñ\81воÑ\98им [[Special:Preferences|подеÑ\88аваÑ\9aима Ð½Ð°Ð»Ð¾Ð³Ð°]] Ð¸ Ð½Ð¸Ñ\81Ñ\82е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ани Ð¾Ð´ ÐºÐ¾Ñ\80иÑ\88Ñ\9bеÑ\9aа Ð¸Ñ\81Ñ\82е.\nÐ\92аÑ\88а Ð°ÐºÑ\82Ñ\83елна IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е $3, Ð° ID Ð±Ð»Ð¾ÐºÐ°Ð´Ðµ #$5.\nÐ\9dаведите све горње детаље при прављењу било каквих упита.",
        "autoblockedtext": "Ваша IP адреса је аутоматски блокирана јер ју је користио други корисник, кога је {{GENDER:$4|блокирао|блокирала}} $1.\nРазлог:\n\n:<em>$2</em>\n\n* Почетак блокаде: $8\n* Крај блокаде: $6\n* Име корисника: $7\n\nМожете да контактирате {{GENDER:$4|корисника|корисницу}} $1 или другог [[{{MediaWiki:Grouppage-sysop}}|администратора]] да бисте расправљали о блокади.\n\nЗапамтите да не можете да користите могућност „{{int:emailuser}}“ осим ако сте навели ваљану имејл адресу у својим [[Special:Preferences|подешавањима]].\n\nВаша актуелна IP адреса је $3, а ID блокаде $5.\nУкључите све горње детаље при прављењу било каквих упита.",
        "blockednoreason": "разлог није наведен",
        "whitelistedittext": "За уређивање странице је потребно да будете $1.",
        "confirmedittext": "Морате да потврдите своју имејл адресу пре уређивања страница.\nПоставите и потврдите имејл адресу преко [[Special:Preferences|подешавања]].",
        "nosuchsectiontitle": "Не могу да пронађем одељак.",
-       "nosuchsectiontext": "Ð\9fокÑ\83Ñ\88али Ñ\81Ñ\82е Ð´Ð° Ñ\83Ñ\80едиÑ\82е Ð¾Ð´ÐµÑ\99ак ÐºÐ¾Ñ\98и Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и.\nÐ\9cожда Ñ\98е Ð¿Ñ\80емеÑ\88Ñ\82ен Ð¸Ð»Ð¸ Ð¾брисан док сте прегледали страницу.",
+       "nosuchsectiontext": "Ð\9fокÑ\83Ñ\88али Ñ\81Ñ\82е Ð´Ð° Ñ\83Ñ\80едиÑ\82е Ð¾Ð´ÐµÑ\99ак ÐºÐ¾Ñ\98и Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и.\nÐ\9cожда Ñ\98е Ð¿Ñ\80емеÑ\88Ñ\82ен Ð¸Ð»Ð¸ Ð¸Ð·брисан док сте прегледали страницу.",
        "loginreqtitle": "Потребна је пријава",
        "loginreqlink": "пријављени",
        "loginreqpagetext": "Морате бити $1 да бисте видели друге странице.",
        "anontalkpagetext": "----\n<em>Ово је страница за разговор с анонимним корисником који још нема налог или га не користи.</em>\nЗбог тога морамо да користимо бројчану IP адресу како бисмо га препознали.\nТакву адресу може делити више корисника.\nАко сте анонимни корисник и мислите да су вам упућене примедбе, [[Special:CreateAccount|отворите налог]] или се [[Special:UserLogin|пријавите]] да бисте избегли будућу забуну с осталим анонимним корисницима.",
        "noarticletext": "На овој страници тренутно нема текста.\nМожете [[Special:Search/{{PAGENAME}}|потражити овај наслов]] на другим страницама,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} претражити сродне извештаје] или [{{fullurl:{{FULLPAGENAME}}|action=edit}} направити ову страницу]</span>.",
        "noarticletext-nopermission": "Тренутно нема текста на овој страници.\nМожете да [[Special:Search/{{PAGENAME}}|потражите овај наслов странице]] на другим страницама или <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} претражите сродне евиденције]</span>, али немате дозволу да направите ову страницу.",
-       "missing-revision": "РевизиÑ\98а Ð±Ñ\80. $1 Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86и Ð¿Ð¾Ð´ Ð¸Ð¼ÐµÐ½Ð¾Ð¼ â\80\9e{{FULLPAGENAME}}â\80\9c Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и.\n\nÐ\9eво Ñ\81е Ð¾Ð±Ð¸Ñ\87но Ð´ÐµÑ\88ава ÐºÐ°Ð´Ð° Ð¿Ñ\80аÑ\82иÑ\82е Ð·Ð°Ñ\81Ñ\82аÑ\80ели Ð»Ð¸Ð½Ðº Ð´Ð¾ Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\98а Ñ\98е Ð¾брисана.\nВише информација можете да пронађете у [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} евиденцији брисања].",
+       "missing-revision": "РевизиÑ\98а Ð±Ñ\80. $1 Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86и Ð¿Ð¾Ð´ Ð¸Ð¼ÐµÐ½Ð¾Ð¼ â\80\9e{{FULLPAGENAME}}â\80\9c Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и.\n\nÐ\9eво Ñ\81е Ð¾Ð±Ð¸Ñ\87но Ð´ÐµÑ\88ава ÐºÐ°Ð´Ð° Ð¿Ñ\80аÑ\82иÑ\82е Ð·Ð°Ñ\81Ñ\82аÑ\80ели Ð»Ð¸Ð½Ðº Ð´Ð¾ Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\98а Ñ\98е Ð¸Ð·брисана.\nВише информација можете да пронађете у [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} евиденцији брисања].",
        "userpage-userdoesnotexist": "Кориснички налог „<nowiki>$1</nowiki>“ није отворен.\nРазмислите да ли заиста желите да направите/уредите ову страницу.",
        "userpage-userdoesnotexist-view": "Кориснички налог „$1“ није отворен.",
        "blocked-notice-logextract": "Овај корисник је тренутно блокиран.\nПоследњи унос у евиденцији блокирања је наведен испод као референца:",
        "readonlywarning": "<strong>Упозорење: база података је закључана ради одржавања, тако да тренутно нећете моћи да сачувате измене.</strong>\nМожда бисте желели сачувати текст за касније у некој текстуалној датотеци.\n\nСистемски администратор је навео следеће објашњење: $1",
        "protectedpagewarning": "<strong>Упозорење: Ова страница је заштићена, тако да само корисници са администраторским овлашћењима могу да је уређују.</strong>\nНајновији унос у евиденцији је наведен испод као референца:",
        "semiprotectedpagewarning": "<strong>Напомена:</strong> Ова страница је заштићена, тако да само аутоматски потврђени корисници могу да је уређују.\nНајновији унос у евиденцији је наведен испод као референца:",
-       "cascadeprotectedwarning": "<strong>Упозорење:</strong> Ова страница је заштићена, тако да само корисници са [[Special:ListGroupRights|одређеним правима]] могу да је уређују, јер је она укључена у {{PLURAL:$1|следећу страницу која је заштићена|следеће странице које су заштићене}} преносивом заштитом:",
+       "cascadeprotectedwarning": "<strong>Упозорење:</strong> Ова страница је заштићена тако да само корисници са [[Special:ListGroupRights|одређеним правима]] могу да је уређују, јер је укључена у {{PLURAL:$1|следећу страницу која је заштићена|следеће странице које су заштићене}} преносивом заштитом:",
        "titleprotectedwarning": "<strong>Упозорење: Ова страница је заштићена, тако да су потребна [[Special:ListGroupRights|посебна права]] да се она направи.</strong>\nНајновији унос у евиденцији је наведен испод као референца:",
        "templatesused": "{{PLURAL:$1|Шаблон који се користи|Шаблони који се користе}} на овој страници:",
        "templatesusedpreview": "{{PLURAL:$1|Шаблон|Шаблони}} у овом претпрегледу:",
        "permissionserrorstext": "Немате дозволу за ову радњу из {{PLURAL:$1|следећег|следећих}} разлога:",
        "permissionserrorstext-withaction": "Немате дозволу да $2 из {{PLURAL:$1|следећег|следећих}} разлога:",
        "contentmodelediterror": "Не можете уредити ову ревизију јер је њен модел садржаја <code>$1</code>, што се разликује од актуелног модела садржаја странице <code>$2</code>.",
-       "recreate-moveddeleted-warn": "<strong>УпозоÑ\80еÑ\9aе: Ð¿Ð¾Ð½Ð¾Ð²Ð¾ Ð¿Ñ\80авиÑ\82е Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 ÐºÐ¾Ñ\98а Ñ\98е Ð¿Ñ\80еÑ\82Ñ\85одно Ð¾Ð±Ñ\80иÑ\81ана.</strong>\n\nРазмоÑ\82Ñ\80иÑ\82е Ð´Ð° Ð»Ð¸ Ñ\98е Ð¿Ñ\80икладно Ð´Ð° Ð½Ð°Ñ\81Ñ\82авиÑ\82е Ñ\81 Ñ\83Ñ\80еÑ\92иваÑ\9aем Ð¾Ð²Ðµ Ñ\81Ñ\82Ñ\80аниÑ\86е.\nÐ\9eвде Ñ\98е Ð½Ð°Ð²ÐµÐ´ÐµÐ½Ð° ÐµÐ²Ð¸Ð´ÐµÐ½Ñ\86иÑ\98а Ð±Ñ\80иÑ\81аÑ\9aа Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82аÑ\9aа Ñ\81 образложењем:",
-       "moveddeleted-notice": "Ð\9eва Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¾брисана.\nЕвиденција брисања, заштите и премештања странице је наведена испод као референца.",
-       "moveddeleted-notice-recent": "Ð\96ао Ð½Ð°Ð¼ Ñ\98е, Ð¾Ð²Ð° Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð½ÐµÐ´Ð°Ð²Ð½Ð¾ Ð¾Ð±Ñ\80иÑ\81ана (Ñ\83 Ð¿Ð¾Ñ\81ледÑ\9aиÑ\85 24 Ñ\81аÑ\82а).\nÐ\95виденÑ\86иÑ\98а Ñ\9aеног Ð±Ñ\80иÑ\81аÑ\9aа, Ð·Ð°Ñ\88Ñ\82иÑ\82е Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82аÑ\9aа Ð½Ð°Ð»Ð°Ð·Ð¸ Ñ\81е Ð¸Ñ\81под:",
+       "recreate-moveddeleted-warn": "<strong>УпозоÑ\80еÑ\9aе: Ð\9fоново Ð¿Ñ\80авиÑ\82е Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 ÐºÐ¾Ñ\98а Ñ\98е Ð¿Ñ\80еÑ\82Ñ\85одно Ð¸Ð·Ð±Ñ\80иÑ\81ана.</strong>\n\nРазмоÑ\82Ñ\80иÑ\82е Ð´Ð° Ð»Ð¸ Ñ\98е Ð¿Ñ\80икладно Ð´Ð° Ð½Ð°Ñ\81Ñ\82авиÑ\82е Ñ\81а Ñ\83Ñ\80еÑ\92иваÑ\9aем Ð¾Ð²Ðµ Ñ\81Ñ\82Ñ\80аниÑ\86е.\nÐ\9eвде Ñ\98е Ð½Ð°Ð²ÐµÐ´ÐµÐ½Ð° ÐµÐ²Ð¸Ð´ÐµÐ½Ñ\86иÑ\98а Ð±Ñ\80иÑ\81аÑ\9aа Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82аÑ\9aа Ñ\81а образложењем:",
+       "moveddeleted-notice": "Ð\9eва Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¸Ð·брисана.\nЕвиденција брисања, заштите и премештања странице је наведена испод као референца.",
+       "moveddeleted-notice-recent": "Ð\9dажалоÑ\81Ñ\82, Ð¾Ð²Ð° Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð½ÐµÐ´Ð°Ð²Ð½Ð¾ Ð¸Ð·Ð±Ñ\80иÑ\81ана (Ñ\83 Ð¿Ð¾Ñ\81ледÑ\9aиÑ\85 24 Ñ\81аÑ\82а).\nÐ\95виденÑ\86иÑ\98а Ð±Ñ\80иÑ\81аÑ\9aа, Ð·Ð°Ñ\88Ñ\82иÑ\82е Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82аÑ\9aа Ñ\81Ñ\82Ñ\80аниÑ\86е Ð½Ð°Ð²ÐµÐ´ÐµÐ½Ð° Ñ\98е Ð¸Ñ\81под ÐºÐ°Ð¾ Ñ\80еÑ\84еÑ\80енÑ\86а:",
        "log-fulllog": "Погледај целу евиденцију",
        "edit-hook-aborted": "Измену је прекинула кука.\nНије дато никакво образложење.",
-       "edit-gone-missing": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð°Ð¶Ñ\83Ñ\80иÑ\80ам Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83.\nÐ\98згледа Ð´Ð° Ñ\98е Ð¾брисана.",
+       "edit-gone-missing": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð°Ð¶Ñ\83Ñ\80иÑ\80ам Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83.\nÐ\98згледа Ð´Ð° Ñ\98е Ð¸Ð·брисана.",
        "edit-conflict": "Сукоб измена.",
        "edit-no-change": "Ваша измена је занемарена јер није било никаквих промена у тексту.",
        "postedit-confirmation-created": "Страница је направљена.",
        "post-expand-template-argument-warning": "'''Упозорење:''' Ова страница садржи најмање један аргумент у шаблону који има превелику величину.\nОвакви аргументи требају да се избегавају.",
        "post-expand-template-argument-category": "Странице које садрже изостављене аргументе у шаблону",
        "parser-template-loop-warning": "Откривена је петља шаблона: [[$1]]",
-       "template-loop-category": "СÑ\82Ñ\80аниÑ\86е Ñ\81а Ð¿ÐµÑ\82Ñ\99ама Ñ\83 Ñ\88аблонима",
+       "template-loop-category": "СÑ\82Ñ\80аниÑ\86е Ñ\81а Ð¿ÐµÑ\82Ñ\99ама Ñ\88аблона",
        "template-loop-category-desc": "Страница садржи петљу шаблона, тј. шаблон који позива сам ребе рекурзивно.",
        "parser-template-recursion-depth-warning": "Дубина укључивања шаблона је прекорачена ($1)",
        "language-converter-depth-warning": "Прекорачена је граница дубине језичког претварача ($1)",
        "converter-manual-rule-error": "Пронађена је грешка у правилу за ручно претварање језика",
        "undo-success": "Измена се може поништити.\nПроверите разлике испод, па сачувајте измене.",
        "undo-failure": "Ова измена се не може поништити због сукоба измена.",
-       "undo-norev": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð²Ñ\80аÑ\82им Ð¸Ð·Ð¼ÐµÐ½Ñ\83 Ñ\98еÑ\80 Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и Ð¸Ð»Ð¸ Ñ\98е Ð¾брисана.",
+       "undo-norev": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð²Ñ\80аÑ\82им Ð¸Ð·Ð¼ÐµÐ½Ñ\83 Ñ\98еÑ\80 Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и Ð¸Ð»Ð¸ Ñ\98е Ð¸Ð·брисана.",
        "undo-nochange": "Изгледа да је измена већ поништена.",
        "undo-summary": "Поништена ревизија $1 {{GENDER:$2|корисника|кориснице}} [[Special:Contributions/$2|$2]] ([[User talk:$2|разговор]])",
        "undo-summary-username-hidden": "Поништи измену $1 скривеног корисника",
        "page_last": "последња",
        "histlegend": "Избор разлика: означите кутијице ревизија за упоређивање и притисните enter или дугме на дну.<br />\nОбјашњење: <strong>({{int:cur}})</strong> = разлика са актуелном ревизијом, <strong>({{int:last}})</strong> = разлика са претходном ревизијом, <strong>{{int:minoreditletter}}</strong> = мања измена",
        "history-fieldset-title": "Претрага измена",
-       "history-show-deleted": "Само Ð¾брисане ревизије",
+       "history-show-deleted": "Само Ð¸Ð·брисане ревизије",
        "histfirst": "најстарије",
        "histlast": "најновије",
        "historysize": "({{PLURAL:$1|1 бајт|$1 бајта|$1 бајтова}})",
        "history-feed-title": "Историја ревизија",
        "history-feed-description": "Историја измена ове странице на викију",
        "history-feed-item-nocomment": "$1 у $2",
-       "history-feed-empty": "ТÑ\80ажена Ñ\81Ñ\82Ñ\80аниÑ\86а Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и.\nÐ\9cогÑ\83Ñ\9bе Ð´Ð° Ñ\98е Ð¾Ð±Ñ\80иÑ\81ана Ñ\81 Ð²Ð¸ÐºÐ¸Ñ\98а Ð¸Ð»Ð¸ Ñ\98е Ð¿Ñ\80еименована.\nÐ\9fокÑ\83Ñ\88аÑ\98Ñ\82е Ð´Ð° [[Special:Search|пÑ\80еÑ\82Ñ\80ажиÑ\82е Ð²Ð¸ÐºÐ¸]] Ð·Ð° Ñ\81лиÑ\87не странице.",
+       "history-feed-empty": "ТÑ\80ажена Ñ\81Ñ\82Ñ\80аниÑ\86а Ð½Ðµ Ð¿Ð¾Ñ\81Ñ\82оÑ\98и.\nÐ\9cогÑ\83Ñ\9bе Ð´Ð° Ñ\98е Ð¸Ð·Ð±Ñ\80иÑ\81ана Ñ\81а Ð²Ð¸ÐºÐ¸Ñ\98а Ð¸Ð»Ð¸ Ñ\98е Ð¿Ñ\80еименована.\nÐ\9fокÑ\83Ñ\88аÑ\98Ñ\82е Ð´Ð° [[Special:Search|пÑ\80еÑ\82Ñ\80ажиÑ\82е Ð²Ð¸ÐºÐ¸]] Ð·Ð° Ñ\80елеванÑ\82не Ð½Ð¾Ð²е странице.",
        "history-edit-tags": "Уреди ознаке изабраних ревизија",
        "rev-deleted-comment": "(опис измене уклоњен)",
        "rev-deleted-user": "(корисничко име уклоњено)",
        "rev-delundel": "промени видљивост",
        "rev-showdeleted": "прикажи",
        "revisiondelete": "Брисање/враћање ревизија",
-       "revdelete-nooldid-title": "Ð\9dема Ñ\82Ñ\80ажене Ð¸Ð·Ð¼ÐµÐ½Ðµ",
+       "revdelete-nooldid-title": "Ð\9dеважеÑ\9bа Ð¾Ð´Ñ\80едиÑ\88на Ñ\80евизиÑ\98а",
        "revdelete-nooldid-text": "Нисте навели одредишну ревизију на којој треба да се изврши ова функција, та ревизија не постоји, или покушавате да сакријете актуелну ревизију.",
        "revdelete-no-file": "Тражена датотека не постоји.",
-       "revdelete-show-file-confirm": "Ð\88еÑ\81Ñ\82е Ð»Ð¸ Ñ\81игÑ\83Ñ\80ни Ð´Ð° Ð¶ÐµÐ»Ð¸Ñ\82е Ð´Ð° Ð²Ð¸Ð´Ð¸Ñ\82е Ð¾брисану ревизију датотеке „<nowiki>$1</nowiki>“ од $2; $3?",
+       "revdelete-show-file-confirm": "Ð\88еÑ\81Ñ\82е Ð»Ð¸ Ñ\81игÑ\83Ñ\80ни Ð´Ð° Ð¶ÐµÐ»Ð¸Ñ\82е Ð´Ð° Ð²Ð¸Ð´Ð¸Ñ\82е Ð¸Ð·брисану ревизију датотеке „<nowiki>$1</nowiki>“ од $2; $3?",
        "revdelete-show-file-submit": "Да",
        "revdelete-selected-text": "{{PLURAL:$1|Изабрана ревизија|Изабране ревизије|Изабраних ревизија}} [[:$2]]:",
        "revdelete-selected-file": "{{PLURAL:$1|Изабрана верзија датотеке|Изабране верзије датотеке}} [[:$2]]:",
        "logdelete-selected": "{{PLURAL:$1|Изабрана ставка у историји|Изабране ставке у историји}}:",
        "revdelete-text-text": "Избрисане ревизије ће и даље бити видљиве у историји странице, али делови њиховог садржаја неће бити јавно доступни.",
        "revdelete-text-file": "Избрисане верзије датотеке ће и даље бити видљиве у историји датотеке, али делови њиховог садржаја неће бити јавно доступни.",
-       "logdelete-text": "Ð\9eбрисани догађаји у евиденцијама ће се идаље појављивати у евиденцији, али ће делови њиховог садржаја бити недоступни јавности.",
+       "logdelete-text": "Ð\98збрисани догађаји у евиденцијама ће се идаље појављивати у евиденцији, али ће делови њиховог садржаја бити недоступни јавности.",
        "revdelete-text-others": "Остали администратори ће и даље моћи да приступе скривеном садржају и врате га, осим ако се поставе додатна ограничења.",
        "revdelete-confirm": "Потврдите да намеравате ово урадити, да разумете последице и да то чините у складу са [[{{MediaWiki:Policy-url}}|правилима]].",
        "revdelete-suppress-text": "Сакривање измена би требало користити <strong>само</strong> у следећим случајевима:\n* злонамерни или погрдни подаци\n* неприкладни лични подаци\n*: <em>кућна адреса и број телефона, број кредитне картице, ЈМБГ итд.</em>",
        "logdelete-failure": "'''Не могу да поставим видљивост историје:'''\n$1",
        "revdel-restore": "промени видљивост",
        "pagehist": "Историја странице",
-       "deletedhist": "Ð\9eбрисана историја",
+       "deletedhist": "Ð\98збрисана историја",
        "revdelete-hide-current": "Грешка при сакривању ставке од $1, $2: ово је актуелна ревизија.\nНе може да буде сакривена.",
        "revdelete-show-no-access": "Грешка при приказивању ставке од $1, $2: означена је као „ограничена“.\nНемате приступ до ње.",
        "revdelete-modify-no-access": "Грешка при мењању ставке од $1, $2: означена је као „ограничена“.\nНемате приступ до ње.",
        "mergehistory-empty": "Нема измена за спајање.",
        "mergehistory-done": "$3 {{PLURAL:$3|ревизија странице $1 је спојена|ревизије странице $1 су спојене|ревизија странице $1 је спојено}} у [[:$2]].",
        "mergehistory-fail": "Не могу да спојим историје. Проверите страницу и временске параметре.",
-       "mergehistory-fail-bad-timestamp": "Временска ознака није валидна.",
+       "mergehistory-fail-bad-timestamp": "Временска ознака је неважећа.",
        "mergehistory-fail-invalid-source": "Изворна страница није валидна.",
-       "mergehistory-fail-invalid-dest": "Одредишна страница није валидна.",
+       "mergehistory-fail-invalid-dest": "Одредишна страница је неважећа.",
        "mergehistory-fail-no-change": "Спајање историје није спојило ниједну ревизију. Проверите параметре странице и времена.",
        "mergehistory-fail-permission": "Немате овлашћење за спајање историје.",
        "mergehistory-fail-self-merge": "Изворна и одредишна страница не могу бити исте.",
        "diff-multi-sameuser": "({{PLURAL:$1|Једна међуревизија истог корисника није приказана|$1 међуревизија истог корисника нису приказане|$1 међуревизија истог корисника није приказано}})",
        "diff-multi-otherusers": "({{PLURAL:$1|Једна међуревизија|$1 међуревизије|$1 међуревизија}} од стране {{PLURAL:$2|још једног корисника није приказана|$2 корисника није приказано}})",
        "diff-multi-manyusers": "({{PLURAL:$1|Није приказана међуизмена|Нису приказане $1 међуизмене|Није приказано $1 међуизмена}} од више од $2 корисника)",
-       "diff-paragraph-moved-tonew": "Пасус је премештен. Кликните да пређете на његово ново место.",
-       "diff-paragraph-moved-toold": "Ð\9fаÑ\81Ñ\83Ñ\81 Ñ\98е Ð¿Ñ\80емеÑ\88Ñ\82ен. Ð\9aликниÑ\82е Ð´Ð° Ð¿Ñ\80еÑ\92еÑ\82е Ð½Ð° Ñ\9aегово Ñ\81Ñ\82аÑ\80о Ð¼ÐµÑ\81Ñ\82о.",
-       "difference-missing-revision": "{{PLURAL:$2|Ð\88една Ñ\80евизиÑ\98а|$2 Ñ\80евизиÑ\98е}} Ð¾Ð´ Ð¾Ð²Ðµ Ñ\80азлике ($1) Ð½Ðµ {{PLURAL:$2|поÑ\81Ñ\82оÑ\98и|поÑ\81Ñ\82оÑ\98е}}.\n\nÐ\9eво Ñ\81е Ð¾Ð±Ð¸Ñ\87но Ð´ÐµÑ\88ава ÐºÐ°Ð´Ð° Ð¿Ñ\80аÑ\82иÑ\82е Ð·Ð°Ñ\81Ñ\82аÑ\80ели Ð»Ð¸Ð½Ðº Ð´Ð¾ Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\98а Ñ\98е Ð¾брисана.\nДетаље можете да пронађете у [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} евиденцији брисања].",
+       "diff-paragraph-moved-tonew": "Пасус је премештен. Кликните да пређете на нову локацију.",
+       "diff-paragraph-moved-toold": "Ð\9fаÑ\81Ñ\83Ñ\81 Ñ\98е Ð¿Ñ\80емеÑ\88Ñ\82ен. Ð\9aликниÑ\82е Ð´Ð° Ð¿Ñ\80еÑ\92еÑ\82е Ð½Ð° Ñ\81Ñ\82аÑ\80Ñ\83 Ð»Ð¾ÐºÐ°Ñ\86иÑ\98Ñ\83.",
+       "difference-missing-revision": "{{PLURAL:$2|Ð\88една Ñ\80евизиÑ\98а|$2 Ñ\80евизиÑ\98е}} Ð¾Ð²Ðµ Ñ\80азлике ($1) Ð½Ðµ {{PLURAL:$2|поÑ\81Ñ\82оÑ\98и|поÑ\81Ñ\82оÑ\98е}}.\n\nÐ\9eво Ñ\81е Ð¾Ð±Ð¸Ñ\87но Ð´ÐµÑ\88ава ÐºÐ°Ð´Ð° Ð¿Ñ\80аÑ\82иÑ\82е Ð·Ð°Ñ\81Ñ\82аÑ\80ели Ð»Ð¸Ð½Ðº Ð´Ð¾ Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\98а Ñ\98е Ð¸Ð·брисана.\nДетаље можете да пронађете у [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} евиденцији брисања].",
        "searchresults": "Резултати претраге",
        "search-filter-title-prefix-reset": "Претражи све странице",
        "searchresults-title": "Резултати претраге за „$1“",
        "prefs-watchlist-managetokens": "Управљај жетонима",
        "prefs-misc": "Друга подешавања",
        "prefs-resetpass": "промени лозинку",
-       "prefs-changeemail": "промени или уклони имејл адресу",
-       "prefs-setemail": "постави имејл адресу",
+       "prefs-changeemail": "промени или уклони имејл-адресу",
+       "prefs-setemail": "постави имејл-адресу",
        "prefs-email": "Опције имејла",
        "prefs-rendering": "Изглед",
        "saveprefs": "Сачувај",
        "prefs-help-realname": "Право име је опционално.\nАко је наведено, биће коришћено за приписивање вашег рада.",
        "prefs-help-email": "Имејл адреса је опционална, али је потребна за ресетовање лозинке, ако је заборавите.",
        "prefs-help-email-others": "Такође можете изабрати да допустите другима да вас контактирају преко имејла путем линка на вашој корисничкој страници или страници за разговор.\nВаша имејл адреса неће бити приказана другим корисницима који вас контактирају.",
-       "prefs-help-email-required": "Ð\9fоÑ\82Ñ\80ебна Ñ\98е Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81а.",
+       "prefs-help-email-required": "Ð\98меÑ\98л-адÑ\80еÑ\81а Ñ\98е Ð½ÐµÐ¾Ð¿Ñ\85одна.",
        "prefs-info": "Основне информације",
        "prefs-i18n": "Интернационализација",
        "prefs-signature": "Потпис",
        "userrights-expiry-existing": "Постојеће време истека: $3, $2",
        "userrights-expiry-othertime": "Друго време:",
        "userrights-expiry-options": "1 дан:1 day,1 недеља:1 week,1 месец:1 month,3 месеца:3 months,6 месеци:6 months,1 година:1 year",
-       "userrights-invalid-expiry": "Време истицања групе „$1“ није валидно.",
+       "userrights-invalid-expiry": "Време истицања групе „$1“ је неважеће.",
        "userrights-expiry-in-past": "Време истицања групе „$1“ је прошло.",
        "userrights-cannot-shorten-expiry": "Не можете убрзати истек чланства у групи „$1”. Само корисници са дозволом да додају или уклоне ову групу могу да убрзају рок истека.",
        "userrights-conflict": "Сукоб промена корисничких права! Прегледајте и проверите ваше промене.",
        "right-bigdelete": "брисање страница са великом историјом",
        "right-deletelogentry": "брисање и враћање одређених уноса у евиденцији",
        "right-deleterevision": "брисање и враћање одређених ревизија страница",
-       "right-deletedhistory": "пÑ\80егледаÑ\9aе Ð¾брисаних ставки историје без повезаног текста",
-       "right-deletedtext": "пÑ\80егледаÑ\9aе Ð¾Ð±Ñ\80иÑ\81аног Ñ\82екÑ\81Ñ\82а Ð¸ Ð¿Ñ\80омена Ð¸Ð·Ð¼ÐµÑ\92Ñ\83 Ð¾брисаних ревизија",
-       "right-browsearchive": "пÑ\80еÑ\82Ñ\80ага Ð¾брисаних страница",
-       "right-undelete": "вÑ\80аÑ\9bаÑ\9aе Ð¾брисаних страница",
+       "right-deletedhistory": "пÑ\80егледаÑ\9aе Ð¸Ð·брисаних ставки историје без повезаног текста",
+       "right-deletedtext": "пÑ\80егледаÑ\9aе Ð¸Ð·Ð±Ñ\80иÑ\81аног Ñ\82екÑ\81Ñ\82а Ð¸ Ð¿Ñ\80омена Ð¸Ð·Ð¼ÐµÑ\92Ñ\83 Ð¸Ð·брисаних ревизија",
+       "right-browsearchive": "пÑ\80еÑ\82Ñ\80ага Ð¸Ð·брисаних страница",
+       "right-undelete": "вÑ\80аÑ\9bаÑ\9aе Ð¸Ð·брисаних страница",
        "right-suppressrevision": "прегледање, скривање и враћање одређених ревизија страница од свих корисника",
        "right-viewsuppressed": "прегледање измена скривених од свих корисника",
        "right-suppressionlog": "прегледање приватних евиденција",
        "right-editmyuserjs": "уређивање сопствених JavaScript датотека",
        "right-viewmywatchlist": "преглед сопственог списка надгледања",
        "right-editmywatchlist": "уређивање сопственог списка надгледања; неке предузете радње ће свеједно додати странице на списак и без овог права",
-       "right-viewmyprivateinfo": "пÑ\80еглед Ñ\81воÑ\98иÑ\85 Ð»Ð¸Ñ\87ниÑ\85 Ð¿Ð¾Ð´Ð°Ñ\82ака (нпÑ\80. Ð¸Ð¼ÐµÑ\98л адресу, право име)",
-       "right-editmyprivateinfo": "Ñ\83Ñ\80еÑ\92иваÑ\9aе Ñ\81опÑ\81Ñ\82вениÑ\85 Ð»Ð¸Ñ\87ниÑ\85 Ð¿Ð¾Ð´Ð°Ñ\82ака (нпÑ\80. Ð¸Ð¼ÐµÑ\98л адресе, правог имена)",
+       "right-viewmyprivateinfo": "пÑ\80еглед Ñ\81воÑ\98иÑ\85 Ð¿Ñ\80иваÑ\82ниÑ\85 Ð¿Ð¾Ð´Ð°Ñ\82ака (нпÑ\80. Ð¸Ð¼ÐµÑ\98л-адресу, право име)",
+       "right-editmyprivateinfo": "Ñ\83Ñ\80еÑ\92иваÑ\9aе Ñ\81опÑ\81Ñ\82вениÑ\85 Ð¿Ñ\80иваÑ\82ниÑ\85 Ð¿Ð¾Ð´Ð°Ñ\82ака (нпÑ\80. Ð¸Ð¼ÐµÑ\98л-адресе, правог имена)",
        "right-editmyoptions": "уређивање сопствених подешавања",
        "right-rollback": "брзо враћање измена последњег корисника који је мењао одређену страницу",
        "right-markbotedits": "означавање враћених измена као измене бота",
        "grant-uploadeditmovefile": "Отпремање, замена и премештање датотека",
        "grant-uploadfile": "Отпремање нових датотека",
        "grant-basic": "Основна права",
-       "grant-viewdeleted": "Ð\9fÑ\80еглед Ð¾брисаних страница и датотека",
+       "grant-viewdeleted": "Ð\9fÑ\80еглед Ð¸Ð·брисаних страница и датотека",
        "grant-viewmywatchlist": "Преглед вашег списак надгледања",
        "grant-viewrestrictedlogs": "Прегледање ограничених уноса у евиденцији",
        "newuserlogpage": "Евиденција нових корисника",
        "action-reupload-shared": "премостите ову датотеку са заједничког складишта",
        "action-upload_by_url": "отпремите ову датотеку путем УРЛ-а",
        "action-writeapi": "користите API за писање",
-       "action-delete": "обришете ову страницу",
+       "action-delete": "избришете ову страницу",
        "action-deleterevision": "бришете ревизије",
        "action-deletelogentry": "бришете уносе у евиденцијама",
-       "action-deletedhistory": "пÑ\80егледаÑ\82е Ð¾брисану историју странице",
-       "action-deletedtext": "пÑ\80егледаÑ\82е Ð¾брисани текст ревизије",
-       "action-browsearchive": "пÑ\80еÑ\82Ñ\80ажÑ\83Ñ\98еÑ\82е Ð¾брисане странице",
+       "action-deletedhistory": "пÑ\80егледаÑ\82е Ð¸Ð·брисану историју странице",
+       "action-deletedtext": "пÑ\80егледаÑ\82е Ð¸Ð·брисани текст ревизије",
+       "action-browsearchive": "пÑ\80еÑ\82Ñ\80ажÑ\83Ñ\98еÑ\82е Ð¸Ð·брисане странице",
        "action-undelete": "враћате странице",
        "action-suppressrevision": "прегледате и враћате сакривене ревизије",
        "action-suppressionlog": "прегледате ову приватну евиденције",
        "rcfilters-savedqueries-apply-label": "Направи филтер",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Направи подразумевани филтер",
        "rcfilters-savedqueries-cancel-label": "Откажи",
-       "rcfilters-savedqueries-add-new-title": "Сачувајте актуелна подешавања филтера",
+       "rcfilters-savedqueries-add-new-title": "Сачувајте тренутна подешавања филтера",
        "rcfilters-savedqueries-already-saved": "Ови филтери су већ сачувани. Промените своја подешавања да бисте направили нове сачуване филтере.",
        "rcfilters-restore-default-filters": "Врати подразумеване филтере",
        "rcfilters-clear-all-filters": "Уклоните све филтере",
        "rcfilters-filter-lastrevision-label": "Последња измена",
        "rcfilters-filter-lastrevision-description": "Само најновија промена на страници.",
        "rcfilters-filter-previousrevision-label": "Није последња ревизија",
-       "rcfilters-filter-previousrevision-description": "Све Ð¿Ñ\80омене ÐºÐ¾Ñ\98е Ð½Ð¸Ñ\81Ñ\83 â\80\9eпоÑ\81ледÑ\9aе Ñ\80евизиÑ\98еâ\80\9c.",
-       "rcfilters-filter-excluded": "Изостављено",
+       "rcfilters-filter-previousrevision-description": "Све Ð¿Ñ\80омене ÐºÐ¾Ñ\98е Ð½Ð¸Ñ\81Ñ\83 â\80\9eпоÑ\81ледÑ\9aе Ñ\80евизиÑ\98еâ\80\9d.",
+       "rcfilters-filter-excluded": "Изузето",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:није</strong> $1",
-       "rcfilters-exclude-button-off": "Изостави означено",
-       "rcfilters-exclude-button-on": "Изостави одабрано",
+       "rcfilters-exclude-button-off": "Изузми изабрано",
+       "rcfilters-exclude-button-on": "Изузми изабрано",
        "rcfilters-view-tags": "Означене измене",
        "rcfilters-view-namespaces-tooltip": "Филтрирајте резултате према именском простору",
        "rcfilters-view-tags-tooltip": "Филтрирајте резултате према ознаци измене",
        "upload_directory_missing": "Фасцикла за слање ($1) недостаје и сервер је не може направити.",
        "upload_directory_read_only": "Сервер не може да пише по фасцикли за слање ($1).",
        "uploaderror": "Грешка при отпремању",
-       "upload-recreate-warning": "<strong>УпозоÑ\80еÑ\9aе: Ð´Ð°Ñ\82оÑ\82ека Ñ\81 Ñ\82им Ð½Ð°Ð·Ð¸Ð²Ð¾Ð¼ Ñ\98е Ð¾Ð±Ñ\80иÑ\81ана Ð¸Ð»Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82ена.</strong>\n\nÐ\95виденÑ\86иÑ\98а Ð±Ñ\80иÑ\81аÑ\9aа Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82аÑ\9aа Ñ\81е Ð½Ð°Ð»Ð°Ð·Ð¸ Ð¸Ñ\81под:",
+       "upload-recreate-warning": "<strong>УпозоÑ\80еÑ\9aе: Ð\94аÑ\82оÑ\82ека Ñ\81а Ñ\82им Ð¸Ð¼ÐµÐ½Ð¾Ð¼ Ñ\98е Ð¸Ð·Ð±Ñ\80иÑ\81ана Ð¸Ð»Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82ена.</strong>\n\nÐ\95виденÑ\86иÑ\98а Ð±Ñ\80иÑ\81аÑ\9aа Ð¸ Ð¿Ñ\80емеÑ\88Ñ\82аÑ\9aа Ñ\81Ñ\82Ñ\80аниÑ\86е Ð½Ð°Ð²ÐµÐ´ÐµÐ½Ð° Ñ\98е Ð¸Ñ\81под Ñ\81а Ð¾Ð±Ñ\80азложеÑ\9aем:",
        "uploadtext": "Користите образац испод да бисте отпремили датотеке.\nЗа преглед или претрагу постојећих датотека, погледајте [[Special:FileList|списак отпремљених датотека]], поновна отпремања су наведена у [[Special:Log/upload|евиденцији отпремања]], а брисања у [[Special:Log/delete|евиденцији брисања]].\n\nДатотеку додајете на жељену страницу користећи следеће обрасце:\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Слика.jpg]]</nowiki></code>''' за верзију слике у пуној величини\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Слика.png|200п|мини|лево|опис]]</nowiki></code>''' за верзију слике с величином од 200 пиксела која је приказана у засебном оквиру, заједно с описом.\n* '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:Датотека.ogg]]</nowiki></code>''' за директно повезивање с датотеком без њеног приказивања",
        "upload-permitted": "Дозвољени {{PLURAL:$2|тип|типови}} датотека: $1.",
        "upload-preferred": "Препоручени {{PLURAL:$2|тип|типови}} датотека: $1.",
        "ignorewarning": "Занемари упозорења и сачувај датотеку",
        "ignorewarnings": "Занемари сва упозорења",
        "minlength1": "Назив датотеке мора имати барем један знак.",
-       "illegalfilename": "Ð\94аÑ\82оÑ\82ека â\80\9e$1â\80\9c Ñ\81адÑ\80жи Ð·Ð½Ð°ÐºÐ¾Ð²Ðµ ÐºÐ¾Ñ\98и Ð½Ð¸Ñ\81Ñ\83 Ð´Ð¾Ð·Ð²Ð¾Ñ\99ени Ñ\83 Ð½Ð°Ð·Ð¸Ð²Ð¸Ð¼Ð° Ñ\81Ñ\82Ñ\80аниÑ\86а.\nÐ\9fÑ\80омениÑ\82е Ð½Ð°Ð·Ð¸Ð² Ð´Ð°Ñ\82оÑ\82еке Ð¸ Ð¿Ð¾Ð½Ð¾Ð²Ð¾ Ñ\98е Ð¿Ð¾Ñ\88аÑ\99ите.",
+       "illegalfilename": "Ð\98ме Ð´Ð°Ñ\82оÑ\82еке â\80\9e$1â\80\9c Ñ\81адÑ\80жи Ð·Ð½Ð°ÐºÐ¾Ð²Ðµ ÐºÐ¾Ñ\98и Ð½Ð¸Ñ\81Ñ\83 Ð´Ð¾Ð·Ð²Ð¾Ñ\99ени Ñ\83 Ð½Ð°Ñ\81ловима Ñ\81Ñ\82Ñ\80аниÑ\86а.\nÐ\9fÑ\80еименÑ\83Ñ\98Ñ\82е Ð´Ð°Ñ\82оÑ\82екÑ\83 Ð¸ Ð¿Ð¾ÐºÑ\83Ñ\88аÑ\82е Ð´Ð° Ñ\98е Ð¿Ð¾Ð½Ð¾Ð²Ð¾ Ð¾Ñ\82пÑ\80емите.",
        "filename-toolong": "Називи датотека могу имати највише 240 бајтова.",
        "badfilename": "Име датотеке је промењено у „$1“.",
        "filetype-mime-mismatch": "Проширење датотеке „.$1“ не одговара препознатом типу MIME датотеке ($2).",
        "fileexists-thumbnail-yes": "Изгледа да је датотека умањено издање слике ''(thumbnail)''.\n[[$1|thumb]]\nПроверите датотеку <strong>[[:$1]]</strong>.\nАко је проверена датотека иста слика оригиналне величине, није потребно слати додатну слику.",
        "file-thumbnail-no": "Датотека почиње са <strong>$1</strong>.\nИзгледа да се ради о умањеној слици ''(thumbnail)''.\nУколико имате ову слику у пуној величини, пошаљите је, а ако немате, промените назив датотеке.",
        "fileexists-forbidden": "Датотека с овим називом већ постоји и не може се заменити.\nАко и даље желите да пошаљете датотеку, вратите се и изаберите други назив.\n[[File:$1|thumb|center|$1]]",
-       "fileexists-shared-forbidden": "Ð\94аÑ\82оÑ\82ека Ñ\81а Ð¾Ð²Ð¸Ð¼ Ð½Ð°Ð·Ð¸Ð²Ð¾Ð¼ Ð²ÐµÑ\9b Ð¿Ð¾Ñ\81Ñ\82оÑ\98и Ñ\83 Ð·Ð°Ñ\98едниÑ\87коÑ\98 Ð¾Ñ\81Ñ\82ави.\nÐ\92Ñ\80аÑ\82иÑ\82е Ñ\81е Ð¸ Ð¿Ð¾Ñ\88аÑ\99иÑ\82е Ð´Ð°Ñ\82оÑ\82екÑ\83 Ñ\81а Ð´Ñ\80Ñ\83гим Ð½Ð°Ð·Ð¸Ð²Ð¾Ð¼.\n[[File:$1|thumb|center|$1]]",
+       "fileexists-shared-forbidden": "Ð\94аÑ\82оÑ\82ека Ñ\81а Ð¾Ð²Ð¸Ð¼ Ð¸Ð¼ÐµÐ½Ð¾Ð¼ Ð²ÐµÑ\9b Ð¿Ð¾Ñ\81Ñ\82оÑ\98и Ñ\83 Ð·Ð°Ñ\98едниÑ\87коÑ\98 Ð¾Ñ\81Ñ\82ави.\nÐ\90ко Ñ\98оÑ\88 Ñ\83век Ð¶ÐµÐ»Ð¸Ñ\82е Ð´Ð° Ð¾Ñ\82пÑ\80емиÑ\82е Ð´Ð°Ñ\82оÑ\82екÑ\83, Ð²Ñ\80аÑ\82иÑ\82е Ñ\81е Ð¸ ÐºÐ¾Ñ\80иÑ\81Ñ\82иÑ\82е Ð½Ð¾Ð²Ð¾ Ð¸Ð¼Ðµ.\n[[File:$1|thumb|center|$1]]",
        "fileexists-no-change": "Датотека је дупликат актуелне верзије <strong>[[:$1]]</strong>.",
        "fileexists-duplicate-version": "Датотека је дупликат {{PLURAL:$2|старе верзије|старих верзија}} <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Ово је дупликат {{PLURAL:$1|следеће датотеке|следећих датотека}}:",
-       "file-deleted-duplicate": "Ð\94аÑ\82оÑ\82ека Ð¸Ñ\81Ñ\82овеÑ\82на Ð¾Ð²Ð¾Ñ\98 ([[:$1]]) Ñ\98е Ð¿Ñ\80еÑ\82Ñ\85одно Ð¾Ð±Ñ\80иÑ\81ана.\nÐ\9fогледаÑ\98Ñ\82е Ð¸Ñ\81Ñ\82оÑ\80иÑ\98Ñ\83 Ð±Ñ\80иÑ\81аÑ\9aа Ð¿Ñ\80е Ð¿Ð¾Ð½Ð¾Ð²Ð½Ð¾Ð³ Ñ\81лаÑ\9aа.",
-       "file-deleted-duplicate-notitle": "Ð\94аÑ\82оÑ\82ека Ð¸Ð´ÐµÐ½Ñ\82иÑ\87на Ð¾Ð²Ð¾Ñ\98 Ð¿Ñ\80еÑ\82Ñ\85одно Ñ\98е Ð¾Ð±Ñ\80иÑ\81ана Ð¸ Ð¸Ð¼Ðµ Ñ\98оÑ\98 Ñ\98е Ñ\81акÑ\80ивено.\nТÑ\80ебали Ð±Ð¸Ñ\81Ñ\82е Ð¿Ð¸Ñ\82аÑ\82и некога ко може видети податке скривених датотека да прегледа ситуацију пре него што поново отпремите датотеку.",
+       "file-deleted-duplicate": "Ð\94аÑ\82оÑ\82ека ÐºÐ¾Ñ\98а Ñ\98е Ð¸Ð´ÐµÐ½Ñ\82иÑ\87на Ð¾Ð²Ð¾Ñ\98 ([[:$1]]) Ñ\98е Ñ\80аниÑ\98е Ð±Ð¸Ð»Ð° Ð¸Ð·Ð±Ñ\80иÑ\81ана.\nТÑ\80ебаÑ\82е Ð´Ð° Ð¿Ñ\80овеÑ\80иÑ\82е Ð¸Ñ\81Ñ\82оÑ\80иÑ\98Ñ\83 Ð±Ñ\80иÑ\81аÑ\9aа Ñ\82е Ð´Ð°Ñ\82оÑ\82еке Ð¿Ñ\80е Ð½ÐµÐ³Ð¾ Ñ\88Ñ\82о Ð½Ð°Ñ\81Ñ\82авиÑ\82е Ñ\81а Ñ\9aеним Ð¿Ð¾Ð½Ð¾Ð²Ð½Ð¸Ð¼ Ð¾Ð¿Ñ\82Ñ\80емаÑ\9aем.",
+       "file-deleted-duplicate-notitle": "Ð\94аÑ\82оÑ\82ека ÐºÐ¾Ñ\98а Ñ\98е Ð¸Ð´ÐµÐ½Ñ\82иÑ\87на Ð¾Ð²Ð¾Ñ\98 Ñ\80аниÑ\98е Ñ\98е Ð¸Ð·Ð±Ñ\80иÑ\81ана Ð¸ Ð¸Ð¼Ðµ Ñ\98оÑ\98 Ñ\98е Ñ\81акÑ\80ивено.\nТÑ\80ебаÑ\82е Ð´Ð° Ð¿Ð¸Ñ\82аÑ\82е некога ко може видети податке скривених датотека да прегледа ситуацију пре него што поново отпремите датотеку.",
        "uploadwarning": "Упозорење при отпремању",
        "uploadwarning-text": "Измените опис датотеке и покушајте поново.",
        "uploadwarning-text-nostash": "Ре-отпремите датотеку, измените опис испод и покушајте поново.",
        "upload-description": "Опис датотеке",
        "upload-options": "Опције отпремања",
        "watchthisupload": "Надгледај ову датотеку",
-       "filewasdeleted": "Датотека с овим називом је раније послата, али је обрисана.\nПроверите $1 пре него што наставите с поновним слањем.",
+       "filewasdeleted": "Датотека са овим именом је раније оптремљена и након тога избрисана.\nТребате да проверите $1 пре него што наставите са њеним поновним оптремањем.",
        "filename-bad-prefix": "Назив датотеке коју шаљете почиње са <strong>„$1“</strong>, а њега обично додељују дигитални фотоапарати.\nИзаберите назив датотеке који описује њен садржај.",
        "filename-prefix-blacklist": " #<!-- оставите овај ред онаквим какав јесте --> <pre>\n# Синтакса је следећа:\n#   * Све од тарабе па до краја реда је коментар\n#   * Сваки ред означава префикс типичних назива датотека које додељивају дигитални апарати\nCIMG # Касио\nDSC_ # Никон\nDSCF # Фуџи\nDSCN # Никон\nDUW # неки мобилни телефони\nIMG # опште\nJD # Џеноптик\nMGP # Пентакс\nPICT # разно\n #</pre> <!-- оставите овај ред онаквим какав јесте -->",
        "upload-proto-error": "Неважећи протокол",
        "backend-fail-hashes": "Не могу да добијем дисперзије датотеке за упоређивање.",
        "backend-fail-notsame": "Већ постоји неистоветна датотека – $1.",
        "backend-fail-invalidpath": "$1 није важећа путања за складиштење.",
-       "backend-fail-delete": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¾бришем датотеку „$1”.",
+       "backend-fail-delete": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¸Ð·бришем датотеку „$1”.",
        "backend-fail-describe": "Не могу да променим метаподатке за датотеку „$1“.",
        "backend-fail-alreadyexists": "Датотека $1 већ постоји.",
        "backend-fail-store": "Не могу да сместим датотеку $1 у $2.",
        "filejournal-fail-dbquery": "Не могу да ажурирам новинарску базу за складишну основу „$1“.",
        "lockmanager-notlocked": "Не могу да откључам „$1“ јер није закључан.",
        "lockmanager-fail-closelock": "Не могу да затворим катанац за „$1“.",
-       "lockmanager-fail-deletelock": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¾бришем катанац за „$1“.",
+       "lockmanager-fail-deletelock": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¸Ð·бришем катанац за „$1“.",
        "lockmanager-fail-acquirelock": "Не могу да се закључам за „$1“.",
        "lockmanager-fail-openlock": "Не могу да отворим катанац за „$1“. Уверите се да је ваш директоријум за отпремање исправно конфигурисан и да ваш веб-сервер има дозволу да пише у том директоријуму. Погледајте https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgUploadDirectory за више информација.",
        "lockmanager-fail-releaselock": "Не могу да ослободим катанац за „$1“.",
        "invalid-chunk-offset": "Неважећа полазна тачка",
        "img-auth-accessdenied": "Приступ је одбијен",
        "img-auth-nopathinfo": "Недостаје PATH_INFO.\nВаш сервер није подешен да прослеђује овакве податке.\nМожда је заснован на CGI-ју који не подржава img_auth.\nПогледајте https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization?uselang=sr-ec.",
-       "img-auth-notindir": "Ð\97аÑ\85Ñ\82евана Ð¿Ñ\83Ñ\82аÑ\9aа Ð½Ð¸Ñ\98е Ñ\83 Ð¿Ð¾Ð´ÐµÑ\88еноÑ\98 Ñ\84аÑ\81Ñ\86икли за отпремање.",
-       "img-auth-badtitle": "Не могу да направим валидан наслов за „$1“.",
+       "img-auth-notindir": "ТÑ\80ажена Ð¿Ñ\83Ñ\82аÑ\9aа Ð½Ð¸Ñ\98е Ñ\83 Ð¿Ð¾Ð´ÐµÑ\88еном Ð´Ð¸Ñ\80екÑ\82оÑ\80иÑ\98Ñ\83мÑ\83 за отпремање.",
+       "img-auth-badtitle": "Не могу да саставим важећи наслов из „$1“.",
        "img-auth-nologinnWL": "Нисте пријављени и „$1” није на списку дозвољених.",
        "img-auth-nofile": "Датотека „$1“ не постоји.",
        "img-auth-isdir": "Покушавате да приступите фасцикли „$1“.\nДозвољен је само приступ датотекама.",
        "licenses-edit": "Уреди избор лиценци",
        "license-nopreview": "(преглед није доступан)",
        "upload_source_url": "(ваша изабрана датотека од важећих, јавно доступних адреса)",
-       "upload_source_file": "(ваша одабрана датотека са вашег рачунара)",
-       "listfiles-delete": "обриши",
+       "upload_source_file": "(ваша одабрана датотека са рачунара)",
+       "listfiles-delete": "избриши",
        "listfiles-summary": "Ова посебна страница приказује све отпремљене датотеке.",
        "listfiles_search_for": "Назив датотеке:",
        "listfiles-userdoesnotexist": "Кориснички налог „$1“ није отворен.",
        "file-anchor-link": "Датотека",
        "filehist": "Историја датотеке",
        "filehist-help": "Кликните на датум/време да бисте видели тадашњу верзију датотеке.",
-       "filehist-deleteall": "обриши све",
-       "filehist-deleteone": "обриши",
+       "filehist-deleteall": "избриши све",
+       "filehist-deleteone": "избриши",
        "filehist-revert": "врати",
        "filehist-current": "актуелна",
        "filehist-datetime": "Датум/време",
        "filehist-comment": "Коментар",
        "imagelinks": "Употреба датотеке",
        "linkstoimage": "{{PLURAL:$1|Следећа страница користи|$1 следеће странице користе|$1 следећих страница користи}} ову датотеку:",
-       "linkstoimage-more": "Ð\92иÑ\88е Ð¾Ð´ $1 {{PLURAL:$1|Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\80иÑ\81Ñ\82и|Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\80иÑ\81Ñ\82е|Ñ\81Ñ\82Ñ\80аниÑ\86а ÐºÐ¾Ñ\80иÑ\81Ñ\82е}} Ð¾Ð²Ñ\83 Ð´Ð°Ñ\82оÑ\82екÑ\83.\nСледеÑ\9bи Ñ\81пиÑ\81ак Ð¿Ñ\80иказÑ\83Ñ\98е Ñ\81амо {{PLURAL:$1|пÑ\80вÑ\83 Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 ÐºÐ¾Ñ\98а ÐºÐ¾Ñ\80иÑ\81Ñ\82и|пÑ\80ве $1 Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\98е ÐºÐ¾Ñ\80иÑ\81Ñ\82е|пÑ\80виÑ\85 $1 Ñ\81Ñ\82Ñ\80аниÑ\86а ÐºÐ¾Ñ\98е ÐºÐ¾Ñ\80иÑ\81Ñ\82е}} ову датотеку.\nДоступан је и [[Special:WhatLinksHere/$2|потпуни списак]].",
+       "linkstoimage-more": "Ð\92иÑ\88е Ð¾Ð´ $1 {{PLURAL:$1|Ñ\81Ñ\82Ñ\80аниÑ\86а ÐºÐ¾Ñ\80иÑ\81Ñ\82и|Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\80иÑ\81Ñ\82е|Ñ\81Ñ\82Ñ\80аниÑ\86а ÐºÐ¾Ñ\80иÑ\81Ñ\82и}} Ð¾Ð²Ñ\83 Ð´Ð°Ñ\82оÑ\82екÑ\83.\nСледеÑ\9bи Ñ\81пиÑ\81ак Ð¿Ñ\80иказÑ\83Ñ\98е {{PLURAL:$1|пÑ\80вÑ\83 Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 ÐºÐ¾Ñ\98а ÐºÐ¾Ñ\80иÑ\81Ñ\82и|пÑ\80ве $1 Ñ\81Ñ\82Ñ\80аниÑ\86е ÐºÐ¾Ñ\98е ÐºÐ¾Ñ\80иÑ\81Ñ\82е|пÑ\80виÑ\85 $1 Ñ\81Ñ\82Ñ\80аниÑ\86а ÐºÐ¾Ñ\98е ÐºÐ¾Ñ\80иÑ\81Ñ\82е}} Ñ\81амо ову датотеку.\nДоступан је и [[Special:WhatLinksHere/$2|потпуни списак]].",
        "nolinkstoimage": "Нема страница које користе ову датотеку.",
        "morelinkstoimage": "Погледајте [[Special:WhatLinksHere/$1|више линкова]] до ове датотеке.",
        "linkstoimage-redirect": "$1 (преусмерење датотеке) $2",
        "sharedupload-desc-edit": "Ова датотека се налази на $1 и може да се користи на другим пројектима.\nЊен опис можете да измените на [$2 одговарајућој страници].",
        "sharedupload-desc-create": "Ова датотека се налази на $1 и може да се користи на другим пројектима.\nЊен опис можете да измените на [$2 одговарајућој страници].",
        "filepage-nofile": "Не постоји датотека с овим називом.",
-       "filepage-nofile-link": "Не постоји датотека с овим називом, али је можете [$1 послати].",
+       "filepage-nofile-link": "Не постоји датотека са овим именом, али је можете [$1 опремити].",
        "uploadnewversion-linktext": "Отпреми нову верзију ове датотеке",
        "shared-repo-from": "из $1",
        "shared-repo": "заједничко складиште",
        "filerevert-success": "Датотека <strong>[[Media:$1|$1]]</strong> је враћена на [$4 верзију од $2; $3].",
        "filerevert-badversion": "Не постоји ранија локална верзија ове датотеке са наведеном временском ознаком.",
        "filerevert-identical": "Актуелна верзија датотеке је индентична изабраној.",
-       "filedelete": "Ð\9eбÑ\80иÑ\88и $1",
-       "filedelete-legend": "Ð\9eбриши датотеку",
+       "filedelete": "Ð\91Ñ\80иÑ\81аÑ\9aе Ð´Ð°Ñ\82оÑ\82еке/Ñ\81Ñ\82Ñ\80аниÑ\86е $1",
+       "filedelete-legend": "Ð\98збриши датотеку",
        "filedelete-intro": "Бришете датотеку '''[[Media:$1|$1]]''' заједно с њеном историјом.",
        "filedelete-intro-old": "Бришете верзију датотеке '''[[Media:$1|$1]]''' од [$4 $2; $3].",
        "filedelete-comment": "Разлог:",
-       "filedelete-submit": "Ð\9eбриши",
-       "filedelete-success": "Датотека '''$1''' је обрисана.",
-       "filedelete-success-old": "Ð\92еÑ\80зиÑ\98а Ð´Ð°Ñ\82оÑ\82еке <strong>[[Media:$1|$1]]</strong> Ð¾Ð´ $2, $3 Ñ\98е Ð¾брисана.",
+       "filedelete-submit": "Ð\98збриши",
+       "filedelete-success": "Датотека <strong>$1</strong> је избрисана.",
+       "filedelete-success-old": "Ð\92еÑ\80зиÑ\98а Ð´Ð°Ñ\82оÑ\82еке <strong>[[Media:$1|$1]]</strong> Ð¾Ð´ $2, $3 Ñ\98е Ð¸Ð·брисана.",
        "filedelete-nofile": "Датотека '''$1''' не постоји.",
        "filedelete-nofile-old": "Не постоји архивирана верзија датотеке <strong>$1</strong> са наведеним особинама.",
        "filedelete-otherreason": "Други/додатни разлог:",
        "filedelete-reason-dropdown": "*Најчешћи разлози брисања\n** Кршење ауторских права\n** Дупликати датотека",
        "filedelete-edit-reasonlist": "Уреди разлоге брисања",
        "filedelete-maintenance": "Брисање и враћање датотека је привремено онемогућено због одржавања.",
-       "filedelete-maintenance-title": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¾бришем датотеку",
+       "filedelete-maintenance-title": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¸Ð·бришем датотеку",
        "mimesearch": "MIME претрага",
        "mimesearch-summary": "Ова страница омогућава филтрирање датотека према њиховим MIME типовима.\nУлазни подаци: contenttype/subtype или contenttype/*, нпр. <code>image/jpeg</code>.",
        "mimetype": "MIME тип:",
        "listduplicatedfiles-entry": "[[:File:$1|$1]] има [[$3|{{PLURAL:$2|један дупликат|$2 дупликата}}]].",
        "unusedtemplates": "Некоришћени шаблони",
        "unusedtemplatestext": "Ова страница наводи све странице у именском простору {{ns:template}} које нису укључене ни на једној другој страници.\nПре брисања проверите да ли друге странице воде до тих шаблона.",
-       "unusedtemplateswlh": "оÑ\81Ñ\82але Ð²ÐµÐ·Ðµ",
+       "unusedtemplateswlh": "оÑ\81Ñ\82али Ð»Ð¸Ð½ÐºÐ¾Ð²Ð¸",
        "randompage": "Случајна страница",
        "randompage-nopages": "Нема страница у {{PLURAL:$2|следећем именском простору|следећим именским просторима}}: $1.",
        "randomincategory": "Случајна страница у категорији",
        "statistics-header-users": "Корисници",
        "statistics-header-hooks": "Остало",
        "statistics-articles": "Странице са садржајем",
-       "statistics-pages": "СÑ\82Ñ\80аниÑ\86а",
+       "statistics-pages": "СÑ\82Ñ\80аниÑ\86е",
        "statistics-pages-desc": "Све странице на викију, укључујући странице за разговор, преусмерења итд.",
-       "statistics-files": "Ð\91Ñ\80оÑ\98 Ð¿Ð¾Ñ\81лаÑ\82иÑ\85 Ð´Ð°Ñ\82оÑ\82ека",
+       "statistics-files": "Ð\9eÑ\82пÑ\80емÑ\99ене Ð´Ð°Ñ\82оÑ\82еке",
        "statistics-edits": "Број измена страница откад постоји {{SITENAME}}",
        "statistics-edits-average": "Просечан број измена по страници",
        "statistics-users": "Регистровани корисници",
        "brokenredirects": "Покварена преусмерења",
        "brokenredirectstext": "Следећа преусмерења воде на непостојеће странице:",
        "brokenredirects-edit": "уреди",
-       "brokenredirects-delete": "обриши",
+       "brokenredirects-delete": "избриши",
        "withoutinterwiki": "Странице без језичких линкова",
        "withoutinterwiki-summary": "Следеће странице немају линкове према верзијама на другим језицима.",
        "withoutinterwiki-legend": "Префикс",
        "wantedcategories": "Тражене категорије",
        "wantedpages": "Тражене странице",
        "wantedpages-summary": "Списак непостојећих страница са највише линкова до њих, на овом списку се не налазе странице до којих воде преусмерења. За списак покварених преусмерења погледајте [[{{#special:BrokenRedirects}}|списак покварених преусмерења]].",
-       "wantedpages-badtitle": "Ð\9dеважеÑ\9bи наслов у скупу резултата: $1",
+       "wantedpages-badtitle": "Ð\9dевалидан наслов у скупу резултата: $1",
        "wantedfiles": "Тражене датотеке",
        "wantedfiletext-cat": "Следеће датотеке се користе, али не постоје. Датотеке из других ризница могу бити наведене иако не постоје. Такве датотеке ће бити <del>поништене</del> са списка. Поред тога, странице које садрже непостојеће датотеке се налазе [[:$1|овде]].",
        "wantedfiletext-nocat": "Следеће датотеке се користе, али не постоје. Датотеке из других ризница могу бити наведене иако не постоје. Такве датотеке ће бити <del>поништене</del> са списка.",
        "wantedfiletext-nocat-noforeign": "Следеће датотеке се користе, али не постоје.",
        "wantedtemplates": "Тражени шаблони",
        "mostlinked": "Странице са највише линкова",
-       "mostlinkedcategories": "Ð\9aаÑ\82егоÑ\80иÑ\98е Ñ\81а Ð½Ð°Ñ\98виÑ\88е Ð²ÐµÐ·а",
-       "mostlinkedtemplates": "СÑ\82Ñ\80аниÑ\86е Ñ\81а Ð½Ð°Ñ\98виÑ\88е Ð²ÐµÐ·а",
+       "mostlinkedcategories": "Ð\9aаÑ\82егоÑ\80иÑ\98е Ñ\81а Ð½Ð°Ñ\98виÑ\88е Ð»Ð¸Ð½ÐºÐ¾Ð²а",
+       "mostlinkedtemplates": "СÑ\82Ñ\80аниÑ\86е Ñ\81а Ð½Ð°Ñ\98виÑ\88е Ð»Ð¸Ð½ÐºÐ¾Ð²а",
        "mostcategories": "Странице са највише категорија",
        "mostimages": "Датотеке са највише линкова",
        "mostinterwikis": "Странице са највише међувикија",
        "apisandbox-submit-invalid-fields-title": "Нека поља нису валидна",
        "apisandbox-submit-invalid-fields-message": "Молимо Вас поправите означена поља и покушајте поново.",
        "apisandbox-results": "Резултати",
-       "apisandbox-sending-request": "СлаÑ\9aе API Ð·Ð°Ñ\85Ñ\82ева...",
+       "apisandbox-sending-request": "ШаÑ\99ем API Ð·Ð°Ñ\85Ñ\82евâ\80¦",
        "apisandbox-loading-results": "Пријем API резултата...",
        "apisandbox-results-error": "Дошло је до грешке приликом учитавања резултата API упита: $1.",
        "apisandbox-request-selectformat-label": "Прикажи сахтеване податке као:",
        "speciallogtitlelabel": "Циљ (наслов или {{ns:user}}:корисничко име):",
        "log": "Евиденције",
        "logeventslist-submit": "Прикажи",
-       "logeventslist-more-filters": "Ð\9fÑ\80иказ Ð´Ð¾Ð´Ð°Ñ\82ниÑ\85 Ð´Ð½ÐµÐ²Ð½Ð¸Ðºа:",
+       "logeventslist-more-filters": "Ð\9fÑ\80иказ Ð´Ð¾Ð´Ð°Ñ\82ниÑ\85 ÐµÐ²Ð¸Ð´ÐµÐ½Ñ\86иÑ\98а:",
        "logeventslist-patrol-log": "Евиденција патролирања",
        "logeventslist-tag-log": "Евиденција ознака",
        "all-logs-page": "Све јавне евиденције",
        "categories-submit": "Прикажи",
        "categoriespagetext": "{{PLURAL:$1|1=Следећа категорија садржи|Следеће категорије садрже}} странице или датотеке.\n[[Special:UnusedCategories|Некоришћене категорије]] нису приказане овде.\nПогледајте и [[Special:WantedCategories|тражене категорије]].",
        "categoriesfrom": "Прикажи категорије почев од:",
-       "deletedcontributions": "Ð\9eбрисани кориснички доприноси",
-       "deletedcontributions-title": "Ð\9eбрисани кориснички доприноси",
+       "deletedcontributions": "Ð\98збрисани кориснички доприноси",
+       "deletedcontributions-title": "Ð\98збрисани кориснички доприноси",
        "sp-deletedcontributions-contribs": "доприноси",
        "linksearch": "Претрага спољашњих линкова",
        "linksearch-pat": "Образац претраге:",
        "defemailsubject": "{{SITENAME}} — Имејл од {{GENDER:$1|корисника|кориснице}} „$1”",
        "usermaildisabled": "Кориснички имејл је онемогућен",
        "usermaildisabledtext": "Не можете да шаљете имејлове другим корисницима на овом викију",
-       "noemailtitle": "Нема имејл адресе",
-       "noemailtext": "Ð\9eваÑ\98 ÐºÐ¾Ñ\80иÑ\81ник Ð½Ð¸Ñ\98е Ð½Ð°Ð²ÐµÐ¾ Ð²Ð°Ð»Ð¸Ð´Ð½Ñ\83 Ð¸Ð¼ÐµÑ\98л адресу.",
+       "noemailtitle": "Нема имејл-адресе",
+       "noemailtext": "Ð\9eваÑ\98 ÐºÐ¾Ñ\80иÑ\81ник Ð½Ð¸Ñ\98е Ð½Ð°Ð²ÐµÐ¾ Ð²Ð°Ð¶ÐµÑ\9bÑ\83 Ð¸Ð¼ÐµÑ\98л-адресу.",
        "nowikiemailtext": "Овај корисник је одлучио да не прима имејлове од других корисника.",
        "emailnotarget": "Непостојеће или наважеће корисничко име примаоца.",
        "emailtarget": "Унос корисничког имена примаоца",
        "unwatch": "Прекини надгледање",
        "unwatchthispage": "Прекини надгледање",
        "notanarticle": "Није страница са садржајем",
-       "notvisiblerev": "Ð\9fоÑ\81ледÑ\9aа Ñ\80евизиÑ\98а Ð´Ñ\80Ñ\83гог ÐºÐ¾Ñ\80иÑ\81ника Ñ\98е Ð¾брисана.",
+       "notvisiblerev": "Ð\9fоÑ\81ледÑ\9aа Ñ\80евизиÑ\98а Ð´Ñ\80Ñ\83гог ÐºÐ¾Ñ\80иÑ\81ника Ñ\98е Ð¸Ð·брисана.",
        "watchlist-details": "Имате {{PLURAL:$1|$1 страницу|$1 странице|$1 страница}} на свом списку надгледања (плус странице за разговор).",
        "wlheader-enotif": "Обавештење имејлом је омогућено.",
        "wlheader-showupdated": "Странице које су промењене откад сте их последњи пут посетили су <strong>подебљане</strong>.",
        "enotif_minoredit": "Ово је мања измена",
        "created": "направљена",
        "changed": "измењена",
-       "deletepage": "Ð\9eбриши страницу",
+       "deletepage": "Ð\98збриши страницу",
        "confirm": "Потврди",
        "excontent": "садржај је био: „$1“",
        "excontentauthor": "садржај је био: „$1“, а једини уредник „[[Special:Contributions/$2|$2]]“ ([[User talk:$2|разговор]])",
        "exbeforeblank": "садржај пре брисања је био: „$1“",
        "delete-confirm": "Брисање странице „$1“",
        "delete-legend": "Брисање",
-       "historywarning": "<strong>Упозорење:</strong> страница коју желите да обришете има историју са $1 {{PLURAL:$1|ревизијом|ревизије|ревизија}}:",
+       "historywarning": "<strong>Упозорење:</strong> Страница коју желите да избришете има историју са $1 {{PLURAL:$1|ревизијом|ревизије|ревизија}}:",
        "historyaction-submit": "Прикажи",
-       "confirmdeletetext": "УпÑ\80аво Ñ\9bеÑ\82е Ð¾Ð±Ñ\80иÑ\81аÑ\82и Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83, Ñ\83кÑ\99Ñ\83Ñ\87Ñ\83Ñ\98Ñ\83Ñ\9bи Ð¸ Ñ\9aенÑ\83 Ð¸Ñ\81Ñ\82оÑ\80иÑ\98Ñ\83.\nÐ\9fоÑ\82вÑ\80диÑ\82е Ñ\81воÑ\98Ñ\83 Ð½Ð°Ð¼ÐµÑ\80Ñ\83, Ð´Ð° Ñ\80азÑ\83меÑ\82е Ð¿Ð¾Ñ\81ледиÑ\86е Ð¸ Ð´Ð° Ð¾Ð²Ð¾ Ñ\80адиÑ\82е Ñ\83 Ñ\81кладÑ\83 Ñ\81 [[{{MediaWiki:Policy-url}}|правилима]].",
+       "confirmdeletetext": "УпÑ\80аво Ñ\9bеÑ\82е Ð¸Ð·Ð±Ñ\80иÑ\81аÑ\82и Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83, Ñ\83кÑ\99Ñ\83Ñ\87Ñ\83Ñ\98Ñ\83Ñ\9bи Ð¸ Ñ\9aенÑ\83 Ð¸Ñ\81Ñ\82оÑ\80иÑ\98Ñ\83.\nÐ\9fоÑ\82вÑ\80диÑ\82е Ñ\81воÑ\98Ñ\83 Ð½Ð°Ð¼ÐµÑ\80Ñ\83, Ð´Ð° Ñ\80азÑ\83меÑ\82е Ð¿Ð¾Ñ\81ледиÑ\86е Ð¸ Ð´Ð° Ð¾Ð²Ð¾ Ñ\80адиÑ\82е Ñ\83 Ñ\81кладÑ\83 Ñ\81а [[{{MediaWiki:Policy-url}}|правилима]].",
        "actioncomplete": "Радња је завршена",
        "actionfailed": "Радња није успела",
-       "deletedtext": "СÑ\82Ñ\80аниÑ\86а â\80\9e$1â\80\9c Ñ\98е Ð¾Ð±Ñ\80иÑ\81ана.\nÐ\9fогледаÑ\98Ñ\82е ''$2'' за запис недавних брисања.",
+       "deletedtext": "СÑ\82Ñ\80аниÑ\86а â\80\9e$1â\80\9c Ñ\98е Ð¸Ð·Ð±Ñ\80иÑ\81ана.\nÐ\9fогледаÑ\98Ñ\82е $2 за запис недавних брисања.",
        "dellogpage": "Евиденција брисања",
        "dellogpagetext": "Испод је списак недавних брисања.",
        "deletionlog": "евиденција брисања",
        "delete-edit-reasonlist": "Уреди разлоге брисања",
        "delete-toobig": "Ова страница има велику историју измена, преко $1 {{PLURAL:$1|ревизија|ревизије|ревизија}}.\nБрисање таквих страница је ограничено да би се спречило случајно оптерећење сервера.",
        "delete-warning-toobig": "Ова страница има велику историју измена, преко $1 {{PLURAL:$1|ревизија|ревизије|ревизија}}.\nЊено брисање може да поремети базу података, стога поступајте с опрезом.",
-       "deleteprotected": "Ð\9dе Ð¼Ð¾Ð¶ÐµÑ\82е Ð¾Ð±Ñ\80иÑ\81аÑ\82и Ð¾Ð²Ñ\83 Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ð·Ð°Ñ\82о Ñ\88Ñ\82о је заштићена.",
+       "deleteprotected": "Ð\9dе Ð¼Ð¾Ð¶ÐµÑ\82е Ð´Ð° Ð¸Ð·Ð±Ñ\80иÑ\88еÑ\82е Ð¾Ð²Ñ\83 Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ñ\98еÑ\80 је заштићена.",
        "deleting-backlinks-warning": "<strong>Упозорење:</strong> бришете страницу која је укључена у [[Special:WhatLinksHere/{{FULLPAGENAME}}|друге странице]] или друге странице воде на њу.",
-       "deleting-subpages-warning": "<strong>Ð\9fажÑ\9aа:</strong> Ð¡Ñ\82Ñ\80аниÑ\86Ñ\83 ÐºÐ¾Ñ\98Ñ\83 Ð¶ÐµÐ»Ð¸Ñ\82е Ð¾брисати има [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|подстраницу|$1 подстранице|$1 подстраница|51=преко 50 подстраница}}]].",
+       "deleting-subpages-warning": "<strong>УпозоÑ\80еÑ\9aе:</strong> Ð¡Ñ\82Ñ\80аниÑ\86а ÐºÐ¾Ñ\98Ñ\83 Ð¶ÐµÐ»Ð¸Ñ\82е Ð¸Ð·брисати има [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|подстраницу|$1 подстранице|$1 подстраница|51=преко 50 подстраница}}]].",
        "rollback": "Врати измене",
        "rollbacklink": "врати",
        "rollbacklinkcount": "врати $1 {{PLURAL:$1|измену|измене|измена}}",
        "protect-legend": "Подешавања заштите",
        "protectcomment": "Разлог:",
        "protectexpiry": "Истиче:",
-       "protect_expiry_invalid": "Време истека није важеће.",
+       "protect_expiry_invalid": "Време истека је неважеће.",
        "protect_expiry_old": "Време истека је у прошлости.",
        "protect-unchain-permissions": "Откључај даљња подешавања заштите",
        "protect-text": "Овде можете да погледате и промените ниво заштите странице <strong>$1</strong>.",
        "restriction-level-sysop": "потпуно заштићено",
        "restriction-level-autoconfirmed": "полузаштићено",
        "restriction-level-all": "сви нивои",
-       "undelete": "Ð\9fÑ\80еглед Ð¾брисаних страница",
-       "undeletepage": "Ð\9fÑ\80еглед Ð¸ Ð²Ñ\80аÑ\9bаÑ\9aе Ð¾брисаних страница",
-       "undeletepagetitle": "<strong>СледеÑ\9bи Ñ\81адÑ\80жаÑ\98 Ñ\81е Ñ\81аÑ\81Ñ\82оÑ\98и Ð¾Ð´ Ð¾брисаних ревизија странице [[:$1|$1]]</strong>.",
-       "viewdeletedpage": "Ð\9fÑ\80иказ Ð¾брисаних страница",
-       "undeletepagetext": "{{PLURAL:$1|СледеÑ\9bа Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¾Ð±Ñ\80иÑ\81ана, Ð°Ð»Ð¸ Ñ\98е Ñ\98оÑ\88 Ñ\83 Ð°Ñ\80Ñ\85иви Ð¸ Ð¼Ð¾Ð¶Ðµ Ð±Ð¸Ñ\82и Ð²Ñ\80аÑ\9bена|СледеÑ\9bе $1 Ñ\81Ñ\82Ñ\80аниÑ\86е Ñ\81Ñ\83 Ð¾Ð±Ñ\80иÑ\81ане, Ð°Ð»Ð¸ Ñ\81Ñ\83 Ñ\98оÑ\88 Ñ\83 Ð°Ñ\80Ñ\85иви Ð¸ Ð¼Ð¾Ð³Ñ\83 Ð±Ð¸Ñ\82и Ð²Ñ\80аÑ\9bене|СледеÑ\9bиÑ\85 $1 Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¾брисано, али су још у архиви и могу бити враћене}}.\nАрхива се повремено чисти од оваквих страница.",
+       "undelete": "Ð\9fÑ\80еглед Ð¸Ð·брисаних страница",
+       "undeletepage": "Ð\9fÑ\80еглед Ð¸ Ð²Ñ\80аÑ\9bаÑ\9aе Ð¸Ð·брисаних страница",
+       "undeletepagetitle": "<strong>СледеÑ\9bи Ñ\81адÑ\80жаÑ\98 Ñ\81е Ñ\81аÑ\81Ñ\82оÑ\98и Ð¾Ð´ Ð¸Ð·брисаних ревизија странице [[:$1|$1]]</strong>.",
+       "viewdeletedpage": "Ð\9fÑ\80еглед Ð¸Ð·брисаних страница",
+       "undeletepagetext": "{{PLURAL:$1|СледеÑ\9bа Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¸Ð·Ð±Ñ\80иÑ\81ана, Ð°Ð»Ð¸ Ñ\98е Ñ\98оÑ\88 Ñ\83 Ð°Ñ\80Ñ\85иви Ð¸ Ð¼Ð¾Ð¶Ðµ Ð±Ð¸Ñ\82и Ð²Ñ\80аÑ\9bена|СледеÑ\9bе $1 Ñ\81Ñ\82Ñ\80аниÑ\86е Ñ\81Ñ\83 Ð¸Ð·Ð±Ñ\80иÑ\81ане, Ð°Ð»Ð¸ Ñ\81Ñ\83 Ñ\98оÑ\88 Ñ\83 Ð°Ñ\80Ñ\85иви Ð¸ Ð¼Ð¾Ð³Ñ\83 Ð±Ð¸Ñ\82и Ð²Ñ\80аÑ\9bене|СледеÑ\9bиÑ\85 $1 Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¸Ð·брисано, али су још у архиви и могу бити враћене}}.\nАрхива се повремено чисти од оваквих страница.",
        "undelete-fieldset-title": "Враћање ревизија",
        "undeleteextrahelp": "Да бисте вратили целу историју странице, оставите све кућице неозначене и кликните на дугме <strong><em>{{int:undeletebtn}}</em></strong>.\nАко желите да вратите одређене ревизије, означите их и кликните на <strong><em>{{int:undeletebtn}}</em></strong>.",
-       "undeleterevisions": "{{PLURAL:$1|Ð\98змена}} Ð¾Ð±Ñ\80иÑ\81ано: $1",
+       "undeleterevisions": "{{PLURAL:$1|Ð\98збÑ\80иÑ\81ана Ñ\98е|Ð\98збÑ\80иÑ\81ане Ñ\81Ñ\83\98збÑ\80иÑ\81ано Ñ\98е}} $1 {{PLURAL:$1|Ñ\80евизиÑ\98а|Ñ\80евизиÑ\98е|Ñ\80евизиÑ\98а}}",
        "undeletehistory": "Ако вратите страницу, све ревизије ће бити враћене њеној историји.\nАко је у међувремену направљена нова страница с истим називом, враћене ревизије ће се појавити у њеној ранијој историји.",
-       "undeleterevdel": "Ð\92Ñ\80аÑ\9bаÑ\9aе Ð½ÐµÑ\9bе Ð±Ð¸Ñ\82и Ð¸Ð·Ð²Ñ\80Ñ\88ено Ð°ÐºÐ¾ Ñ\98е Ñ\80езÑ\83лÑ\82аÑ\82 Ñ\82ога Ð´ÐµÐ»Ð¸Ð¼Ð¸Ñ\87но Ð±Ñ\80иÑ\81аÑ\9aе Ð¿Ð¾Ñ\81ледÑ\9aе Ñ\80евизиÑ\98е.\nУ Ñ\82аквим Ñ\81лÑ\83Ñ\87аÑ\98евима Ð¼Ð¾Ñ\80аÑ\82е Ð¸Ñ\81кÑ\99Ñ\83Ñ\87иÑ\82и Ð¸Ð»Ð¸ Ð¾Ñ\82кÑ\80иÑ\82и Ð½Ð°Ñ\98новиÑ\98е Ð¾брисане ревизије.",
-       "undeletehistorynoadmin": "Ð\9eва Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¾Ð±Ñ\80иÑ\81ана.\nРазлог Ð·Ð° Ð±Ñ\80иÑ\81аÑ\9aе Ñ\81е Ð½Ð°Ð»Ð°Ð·Ð¸ Ð¸Ñ\81под, Ð·Ð°Ñ\98едно Ñ\81 Ð´ÐµÑ\82аÑ\99има Ð¾ ÐºÐ¾Ñ\80иÑ\81никÑ\83 ÐºÐ¾Ñ\98и Ñ\98е Ñ\83Ñ\80едио Ð¾Ð²Ñ\83 Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ð¿Ñ\80е Ð±Ñ\80иÑ\81аÑ\9aа.\nТекÑ\81Ñ\82 Ð¾брисаних ревизија је доступан само администраторима.",
-       "undelete-revision": "Ð\9eбÑ\80иÑ\81ана Ð¸Ð·Ð¼ÐµÐ½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86е $1 (дана $4; $5) Ð¾Ð´ Ñ\81Ñ\82Ñ\80ане {{GENDER:$3|коÑ\80иÑ\81ника|коÑ\80иÑ\81ниÑ\86е|коÑ\80иÑ\81ника}} $3:",
+       "undeleterevdel": "Ð\92Ñ\80аÑ\9bаÑ\9aе Ð½ÐµÑ\9bе Ð±Ð¸Ñ\82и Ð¸Ð·Ð²Ñ\80Ñ\88ено Ð°ÐºÐ¾ Ñ\98е Ñ\80езÑ\83лÑ\82аÑ\82 Ñ\82ога Ð´ÐµÐ»Ð¸Ð¼Ð¸Ñ\87но Ð±Ñ\80иÑ\81аÑ\9aе Ð¿Ð¾Ñ\81ледÑ\9aе Ñ\80евизиÑ\98е.\nУ Ñ\82аквим Ñ\81лÑ\83Ñ\87аÑ\98евима Ð¼Ð¾Ñ\80аÑ\82е Ð¸Ñ\81кÑ\99Ñ\83Ñ\87иÑ\82и Ð¸Ð»Ð¸ Ð¾Ñ\82кÑ\80иÑ\82и Ð½Ð°Ñ\98новиÑ\98е Ð¸Ð·брисане ревизије.",
+       "undeletehistorynoadmin": "Ð\9eва Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¸Ð·Ð±Ñ\80иÑ\81ана.\nРазлог Ð·Ð° Ð±Ñ\80иÑ\81аÑ\9aе Ñ\81е Ð½Ð°Ð»Ð°Ð·Ð¸ Ð¸Ñ\81под, Ð·Ð°Ñ\98едно Ñ\81а Ð´ÐµÑ\82аÑ\99има Ð¾ ÐºÐ¾Ñ\80иÑ\81никÑ\83 ÐºÐ¾Ñ\98и Ñ\98е Ñ\83Ñ\80едио Ð¾Ð²Ñ\83 Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ð¿Ñ\80е Ð±Ñ\80иÑ\81аÑ\9aа.\nТекÑ\81Ñ\82 Ð¸Ð·брисаних ревизија је доступан само администраторима.",
+       "undelete-revision": "Ð\98збÑ\80иÑ\81ана Ñ\80евизиÑ\98а Ñ\81Ñ\82Ñ\80аниÑ\86е $1 (дана $4; $5) Ð¾Ð´ Ñ\81Ñ\82Ñ\80ане {{GENDER:$3|коÑ\80иÑ\81ника|коÑ\80иÑ\81ниÑ\86е}} $3:",
        "undeleterevision-missing": "Неважећа или недостајућа ревизија.\nМожда сте унели лош линк или је ревизија враћена или уклоњена из архиве.",
        "undeleterevision-duplicate-revid": "Не могу вратити {{PLURAL:$1|ревизију|$1 ревизије|$1 ревизија}} јер се {{PLURAL:$1|њен|њихов}} <code>rev_id</code> већ користи.",
        "undelete-nodiff": "Претходне измене нису пронађене.",
        "undeletecomment": "Разлог:",
        "cannotundelete": "Враћање једне или свих није успело:\n$1",
        "undeletedpage": "<strong>Страница $1 је враћена</strong>\n\nПогледајте [[Special:Log/delete|евиденцију брисања]] за записе о недавним брисањима и враћањима.",
-       "undelete-header": "Ð\9fогледаÑ\98Ñ\82е [[Special:Log/delete|евиденÑ\86иÑ\98Ñ\83 Ð±Ñ\80иÑ\81аÑ\9aа]] Ð·Ð° Ð½ÐµÐ´Ð°Ð²Ð½Ð¾ Ð¾брисане странице.",
-       "undelete-search-title": "Ð\9fÑ\80еÑ\82Ñ\80ага Ð¾брисаних страница",
-       "undelete-search-box": "Ð\9fÑ\80еÑ\82Ñ\80ажи Ð¾Ð±Ñ\80иÑ\81ане Ñ\81Ñ\82Ñ\80аниÑ\86е",
+       "undelete-header": "Ð\9fогледаÑ\98Ñ\82е [[Special:Log/delete|евиденÑ\86иÑ\98Ñ\83 Ð±Ñ\80иÑ\81аÑ\9aа]] Ð·Ð° Ð½ÐµÐ´Ð°Ð²Ð½Ð¾ Ð¸Ð·брисане странице.",
+       "undelete-search-title": "Ð\9fÑ\80еÑ\82Ñ\80ага Ð¸Ð·брисаних страница",
+       "undelete-search-box": "Ð\9fÑ\80еÑ\82Ñ\80ага Ð¸Ð·Ð±Ñ\80иÑ\81аниÑ\85 Ñ\81Ñ\82Ñ\80аниÑ\86а",
        "undelete-search-prefix": "Прикажи странице које почињу са:",
        "undelete-search-full": "Прикажи наслове који садрже:",
        "undelete-search-submit": "Претражи",
        "undelete-bad-store-key": "Не могу да вратим измену датотеке од $1: датотека је недостајала пре брисања.",
        "undelete-cleanup-error": "Грешка при брисању некоришћене архиве „$1“.",
        "undelete-missing-filearchive": "Не могу да вратим архиву с ИБ $1 јер се она не налази у бази података.\nМожда је већ била враћена.",
-       "undelete-error": "Ð\94оÑ\88ло Ñ\98е Ð´Ð¾ Ð³Ñ\80еÑ\88ке Ð¿Ñ\80и Ð²Ñ\80аÑ\9bаÑ\9aÑ\83 Ð¾брисане странице",
+       "undelete-error": "Ð\94оÑ\88ло Ñ\98е Ð´Ð¾ Ð³Ñ\80еÑ\88ке Ð¿Ñ\80и Ð²Ñ\80аÑ\9bаÑ\9aÑ\83 Ð¸Ð·брисане странице",
        "undelete-error-short": "Грешка при враћању датотеке: $1",
        "undelete-error-long": "Дошло је до грешке при враћању датотеке:\n\n$1",
-       "undelete-show-file-confirm": "Ð\94а Ð»Ð¸ Ñ\81Ñ\82е Ñ\81игÑ\83Ñ\80ни Ð´Ð° Ð¶ÐµÐ»Ð¸Ñ\82е Ð´Ð° Ð¿Ð¾Ð³Ð»ÐµÐ´Ð°Ñ\82е Ð¾Ð±Ñ\80иÑ\81анÑ\83 Ð¸Ð·Ð¼ÐµÐ½у датотеке „<nowiki>$1</nowiki>“ од $2 у $3?",
+       "undelete-show-file-confirm": "Ð\88еÑ\81Ñ\82е Ð»Ð¸ Ñ\81игÑ\83Ñ\80ни Ð´Ð° Ð¶ÐµÐ»Ð¸Ñ\82е Ð´Ð° Ð¿Ð¾Ð³Ð»ÐµÐ´Ð°Ñ\82е Ð¸Ð·Ð±Ñ\80иÑ\81анÑ\83 Ñ\80евизиÑ\98у датотеке „<nowiki>$1</nowiki>“ од $2 у $3?",
        "undelete-show-file-submit": "Да",
        "namespace": "Именски простор:",
        "invert": "Обрни избор",
        "sp-contributions-newbies-sub": "За нове кориснике",
        "sp-contributions-newbies-title": "Доприноси нових корисника",
        "sp-contributions-blocklog": "евиденција блокирања",
-       "sp-contributions-suppresslog": "обрисани доприноси {{GENDER:$1|корисника|кориснице}}",
-       "sp-contributions-deleted": "обрисани доприноси {{GENDER:$1|корисника|кориснице}}",
+       "sp-contributions-suppresslog": "избрисани доприноси {{GENDER:$1|корисника|кориснице}}",
+       "sp-contributions-deleted": "избрисани доприноси {{GENDER:$1|корисника|кориснице}}",
        "sp-contributions-uploads": "отпремања",
        "sp-contributions-logs": "евиденције",
        "sp-contributions-talk": "разговор",
        "move-page": "Премештање „$1”",
        "move-page-legend": "Премештање странице",
        "movepagetext": "Доњи образац ће преименовати страницу, премештајући целу историју на ново име.\nСтари наслов постаће преусмерење на нови.\nМожете ажурирати преусмерења која воде до изворног наслова;\nпогледајте [[Special:DoubleRedirects|двострука]] или [[Special:BrokenRedirects|покварена]] преусмерења.\nНа вама је одговорност да линкови и даље иду тамо где треба.\n\nСтраница <strong>неће</strong> бити премештена ако већ постоји страница с тим именом (осим ако је празна, садржи преусмерење или нема историју измена).\nТо значи да можете вратити страницу на претходно име ако погрешите, али не можете ''преписати'' постојећу.\n\n<strong>Напомена:</strong>\nОво може представљати драстичну и неочекивану измену за популарну страницу;\nдобро размислите о последицама пре него што наставите.",
-       "movepagetext-noredirectfixer": "Ð\94оÑ\9aи Ð¾Ð±Ñ\80азаÑ\86 Ñ\9bе Ð¿Ñ\80еименоваÑ\82и Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83, Ð¿Ñ\80емеÑ\88Ñ\82аÑ\98Ñ\83Ñ\9bи Ñ\86елÑ\83 Ð¸Ñ\81Ñ\82оÑ\80иÑ\98Ñ\83 Ð½Ð° Ð½Ð¾Ð²Ð¾ Ð¸Ð¼Ðµ.\nСÑ\82аÑ\80и Ð½Ð°Ñ\81лов Ð¿Ð¾Ñ\81Ñ\82аÑ\9bе Ð¿Ñ\80еÑ\83Ñ\81меÑ\80еÑ\9aе Ð½Ð° Ð½Ð¾Ð²Ð¸.\nÐ\9fогледаÑ\98Ñ\82е [[Special:DoubleRedirects|двоÑ\81Ñ\82Ñ\80Ñ\83ка]] Ð¸Ð»Ð¸ [[Special:BrokenRedirects|покваÑ\80ена]] Ð¿Ñ\80еÑ\83Ñ\81меÑ\80еÑ\9aа.\nÐ\9dа Ð²Ð°Ð¼Ð° Ñ\98е Ð¾Ð´Ð³Ð¾Ð²Ð¾Ñ\80ноÑ\81Ñ\82 Ð´Ð° Ð²ÐµÐ·Ðµ и даље иду тамо где треба.\n\nСтраница <strong>неће</strong> бити премештена ако већ постоји страница с тим именом (осим ако је празна, садржи преусмерење или нема историју измена).\nТо значи да можете вратити страницу на претходно име ако погрешите, али не можете ''преписати'' постојећу.\n\n<strong>Напомена:</strong>\nОво може представљати драстичну и неочекивану измену за популарну страницу;\nдобро размислите о последицама пре него што наставите.",
+       "movepagetext-noredirectfixer": "Ð\94оÑ\9aи Ð¾Ð±Ñ\80азаÑ\86 Ñ\9bе Ð¿Ñ\80еименоваÑ\82и Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83, Ð¿Ñ\80емеÑ\88Ñ\82аÑ\98Ñ\83Ñ\9bи Ñ\86елÑ\83 Ð¸Ñ\81Ñ\82оÑ\80иÑ\98Ñ\83 Ð½Ð° Ð½Ð¾Ð²Ð¾ Ð¸Ð¼Ðµ.\nСÑ\82аÑ\80и Ð½Ð°Ñ\81лов Ð¿Ð¾Ñ\81Ñ\82аÑ\9bе Ð¿Ñ\80еÑ\83Ñ\81меÑ\80еÑ\9aе Ð½Ð° Ð½Ð¾Ð²Ð¸.\nÐ\9fогледаÑ\98Ñ\82е [[Special:DoubleRedirects|двоÑ\81Ñ\82Ñ\80Ñ\83ка]] Ð¸Ð»Ð¸ [[Special:BrokenRedirects|покваÑ\80ена]] Ð¿Ñ\80еÑ\83Ñ\81меÑ\80еÑ\9aа.\nÐ\9dа Ð²Ð°Ð¼Ð° Ñ\98е Ð¾Ð´Ð³Ð¾Ð²Ð¾Ñ\80ноÑ\81Ñ\82 Ð´Ð° Ð»Ð¸Ð½ÐºÐ¾Ð²Ð¸ и даље иду тамо где треба.\n\nСтраница <strong>неће</strong> бити премештена ако већ постоји страница с тим именом (осим ако је празна, садржи преусмерење или нема историју измена).\nТо значи да можете вратити страницу на претходно име ако погрешите, али не можете ''преписати'' постојећу.\n\n<strong>Напомена:</strong>\nОво може представљати драстичну и неочекивану измену за популарну страницу;\nдобро размислите о последицама пре него што наставите.",
        "movepagetalktext": "Ако сте означили овај квадратић, одговарајућа страница за разговор биће аутоматски премештена на нови наслов, осим ако већ постоји страница за разговор са истим насловом.\n\nУ том случају, мораћете ручно да је преместите или спојите, ако има потребе за тим.",
        "moveuserpage-warning": "'''Упозорење:''' на путу сте да преместите корисничку страницу. Имајте у виду да ће само страница бити премештена, а сам корисник ''неће'' бити преименован.",
        "movecategorypage-warning": "<strong>Упозорење:</strong> премештате страницу категорије. Имајте на уму да ће само страница бити премештена и да све странице у старој категорији <em>неће</em> бити рекатегорисане у нову категорију.",
        "movepage-moved": "'''„$1“ је премештена на „$2“'''",
        "movepage-moved-redirect": "Преусмерење је направљено.",
        "movepage-moved-noredirect": "Стварање преусмерења је онемогућено.",
-       "articleexists": "Страница с тим именом већ постоји или име које сте одабрали није важеће.\nОдаберите друго.",
-       "cantmove-titleprotected": "Не можете да преместите страницу на то место јер је жељени наслов заштићен од стварања",
+       "articleexists": "Страница са тим именом већ постоји или име које сте одабрали није важеће.\nОдаберите друго.",
+       "cantmove-titleprotected": "Не можете да преместите страницу на ову локацију јер је прављење новог наслова заштићено.",
        "movetalk": "Премести и страницу за разговор",
        "move-subpages": "Премести и подстранице (до $1)",
        "move-talk-subpages": "Премести подстранице странице за разговор (до $1)",
        "movenosubpage": "Ова страница нема подстрана.",
        "movereason": "Разлог:",
        "revertmove": "врати",
-       "delete_and_move_text": "Ð\9eдÑ\80едиÑ\88на Ñ\81Ñ\82Ñ\80аниÑ\86а â\80\9e[[:$1]]â\80\9c Ð²ÐµÑ\9b Ð¿Ð¾Ñ\81Ñ\82оÑ\98и. \nÐ\96елиÑ\82е Ð»Ð¸ Ð´Ð° Ñ\98е Ð¾бришете да бисте ослободили место за премештање?",
-       "delete_and_move_confirm": "Ð\94а, Ð¾бриши страницу",
-       "delete_and_move_reason": "Ð\9eбрисано да се ослободи место за премештање из „[[$1]]“",
+       "delete_and_move_text": "Ð\9eдÑ\80едиÑ\88на Ñ\81Ñ\82Ñ\80аниÑ\86а â\80\9e[[:$1]]â\80\9c Ð²ÐµÑ\9b Ð¿Ð¾Ñ\81Ñ\82оÑ\98и. \nÐ\96елиÑ\82е Ð»Ð¸ Ð´Ð° Ñ\98е Ð¸Ð·бришете да бисте ослободили место за премештање?",
+       "delete_and_move_confirm": "Ð\94а, Ð¸Ð·бриши страницу",
+       "delete_and_move_reason": "Ð\98збрисано да се ослободи место за премештање из „[[$1]]“",
        "selfmove": "Наслов је истоветан;\nне можете преместити страницу преко саме себе.",
        "immobile-source-namespace": "Не могу преместити странице у именски простор „$1“.",
        "immobile-target-namespace": "Не могу преместити странице у именски простор „$1“.",
        "import-upload-filename": "Назив датотеке:",
        "import-upload-username-prefix": "Међувики префикс:",
        "import-comment": "Коментар:",
-       "importtext": "Извезите датотеку с изворног викија користећи [[Special:Export|извоз]].\nСачувајте је на рачунар и пошаљите овде.",
+       "importtext": "Извезите датотеку сa изворног викија користећи [[Special:Export|алат за извоз]].\nСачувајте је на рачунар и оптремите овде.",
        "importstart": "Увозим странице…",
        "import-revision-count": "$1 {{PLURAL:$1|ревизија|ревизије|ревизија}}",
        "importnopages": "Нема страница за увоз.",
        "tooltip-ca-history": "Претходне ревизије ове странице",
        "tooltip-ca-protect": "Заштитите ову страницу",
        "tooltip-ca-unprotect": "Промени заштиту ове странице",
-       "tooltip-ca-delete": "Ð\9eбришите ову страницу",
-       "tooltip-ca-undelete": "Ð\92Ñ\80аÑ\82и Ð¸Ð·Ð¼ÐµÐ½Ðµ Ð½Ð°Ð¿Ñ\80авÑ\99ене Ð½Ð° Ð¾Ð²Ð¾Ñ\98 Ñ\81Ñ\82Ñ\80аниÑ\86и Ð¿Ñ\80е Ð½ÐµÐ³Ð¾ Ñ\88Ñ\82о Ð±Ñ\83де Ð¾Ð±Ñ\80иÑ\81ана",
+       "tooltip-ca-delete": "Ð\98збришите ову страницу",
+       "tooltip-ca-undelete": "Ð\92Ñ\80аÑ\82и Ð¸Ð·Ð¼ÐµÐ½Ðµ ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ð½Ð°Ñ\87иÑ\9aене Ð½Ð° Ð¾Ð²Ð¾Ñ\98 Ñ\81Ñ\82Ñ\80аниÑ\86и Ð¿Ñ\80е Ð±Ñ\80иÑ\81аÑ\9aа Ñ\81Ñ\82Ñ\80аниÑ\86е",
        "tooltip-ca-move": "Премести ову страницу",
        "tooltip-ca-watch": "Додајте ову страницу на свој списак надгледања",
        "tooltip-ca-unwatch": "Уклоните ову страницу са списка надгледања",
        "tooltip-n-currentevents": "Пронађите додатне информације о актуелностима",
        "tooltip-n-recentchanges": "Списак недавних промена на викију",
        "tooltip-n-randompage": "Учитајте случајну страницу",
-       "tooltip-n-help": "Ð\9cеÑ\81Ñ\82о Ð³Ð´Ðµ Ð¼Ð¾Ð¶ÐµÑ\82е Ð´Ð° Ð½Ð°Ñ\83Ñ\87иÑ\82е Ð½ÐµÑ\88Ñ\82о",
+       "tooltip-n-help": "Ð\9cеÑ\81Ñ\82о Ð³Ð´Ðµ Ð¼Ð¾Ð¶ÐµÑ\82е Ð½ÐµÑ\88Ñ\82о Ð´Ð° Ð½Ð°Ñ\83Ñ\87иÑ\82е",
        "tooltip-t-whatlinkshere": "Списак свих вики страница које воде овде",
        "tooltip-t-recentchangeslinked": "Недавне промене на страницама које су повезане с овом страницом",
        "tooltip-feed-rss": "RSS фид за ову страницу",
        "tooltip-watch": "Додајте ову страницу на свој списак надгледања",
        "tooltip-watchlistedit-normal-submit": "Уклоните наслове",
        "tooltip-watchlistedit-raw-submit": "Ажурирај списак",
-       "tooltip-recreate": "Ð\9fоново Ð½Ð°Ð¿Ñ\80авиÑ\82е Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ð¸Ð°ÐºÐ¾ Ñ\98е Ð¾брисана",
+       "tooltip-recreate": "Ð\9fоново Ð½Ð°Ð¿Ñ\80авиÑ\82е Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83 Ð¸Ð°ÐºÐ¾ Ñ\98е Ð²ÐµÑ\9b Ð¸Ð·брисана",
        "tooltip-upload": "Започните отпремање",
        "tooltip-rollback": "„Врати“ враћа измене последњег доприносиоца ове странице једним кликом",
        "tooltip-undo": "„Поништи” враћа ову измену и отвара образац за уређивање у претпрегледном моду. Дозвољава додавање разлога у резимеу.",
        "spamprotectionmatch": "Следећи текст је активирао наш филтер за нежељене поруке: $1",
        "spambot_username": "Чишћење непожељних порука у Медијавикији",
        "spam_reverting": "Враћам на последњу ревизију која не садржи линкове до $1",
-       "spam_blanking": "Све измене садрже везе до $1. Чистим",
+       "spam_blanking": "Све ревизије садрже линкове до $1. Празним",
        "spam_deleting": "Све ревизије садрже линкове до $1. Бришем",
        "simpleantispam-label": "Провера против нежељеног садржаја. \n<strong>Не</strong> попуњавајте ово!",
        "pageinfo-title": "Информације за „$1“",
        "markaspatrolledtext": "Означи страницу као патролирану",
        "markaspatrolledtext-file": "Означи ову верзију датотеке као патролирану",
        "markedaspatrolled": "Означено као патролирано",
-       "markedaspatrolledtext": "Изабрана ревизија странице [[:$1]] је означена као патролирана.",
+       "markedaspatrolledtext": "Изабрана ревизија странице [[:$1]] означена је као патролирана.",
        "rcpatroldisabled": "Патролирање скорашњих измена је онемогућено",
        "rcpatroldisabledtext": "Могућност патролирања скорашњих измена је актуелно онемогућена.",
        "markedaspatrollederror": "Не могу да означим као патролирано.",
        "patrol-log-header": "Ово је евиденција патролираних ревизија.",
        "confirm-markpatrolled-button": "У реду",
        "confirm-markpatrolled-top": "Означити ревизију $3 странице $2 као патролирану?",
-       "deletedrevision": "Ð\9eбрисана стара ревизија $1.",
+       "deletedrevision": "Ð\98збрисана стара ревизија $1.",
        "filedeleteerror-short": "Грешка при брисању датотеке: $1",
        "filedeleteerror-long": "Дошло је до грешака при брисању датотеке:\n\n$1",
-       "filedelete-missing": "Ð\94аÑ\82оÑ\82ека â\80\9e$1â\80\9c Ñ\81е Ð½Ðµ Ð¼Ð¾Ð¶Ðµ Ð¾Ð±Ñ\80иÑ\81аÑ\82и јер не постоји.",
+       "filedelete-missing": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð¸Ð·Ð±Ñ\80иÑ\88ем Ð´Ð°Ñ\82оÑ\82екÑ\83 â\80\9e$1â\80\9c јер не постоји.",
        "filedelete-old-unregistered": "Наведена ревизија датотеке „$1“ не постоји у бази података.",
        "filedelete-current-unregistered": "Наведена датотека „$1“ не постоји у бази података.",
        "filedelete-archive-read-only": "Сервер не може да пише по складишној фасцикли ($1).",
        "newimages-label": "Назив датотеке (или њен део):",
        "newimages-user": "IP адреса или корисничко име",
        "newimages-newbies": "Прикажи само доприносе нових налога",
-       "newimages-showbots": "Ð\9fÑ\80икажи Ð´Ð°Ñ\82оÑ\82еке ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ð¿Ð¾Ñ\81лали Ð±Ð¾Ñ\82ови",
+       "newimages-showbots": "Ð\9fÑ\80икажи Ð¾Ñ\82пÑ\80емаÑ\9aа Ð±Ð¾Ñ\82ова",
        "newimages-hidepatrolled": "Сакриј патролирана отпремања",
        "newimages-mediatype": "Тип датотеке:",
        "noimages": "Нема ништа.",
        "exif-ycbcrpositioning": "Положај Y и C",
        "exif-xresolution": "Водоравна резолуција",
        "exif-yresolution": "Усправна резолуција",
-       "exif-stripoffsets": "Ð\9cеÑ\81Ñ\82о Ð¿Ð¾Ð´Ð°Ñ\82ака",
+       "exif-stripoffsets": "Ð\9bокаÑ\86иÑ\98а Ð¿Ð¾Ð´Ð°Ñ\82ака Ñ\81лике",
        "exif-rowsperstrip": "Број редова по линији",
        "exif-stripbytecounts": "Бајтова по сажетом блоку",
        "exif-jpeginterchangeformat": "Почетак JPEG прегледа",
        "exif-urgency": "Хитност",
        "exif-fixtureidentifier": "Назив рубрике",
        "exif-locationdest": "Приказана локација",
-       "exif-locationdestcode": "Код приказаног места",
+       "exif-locationdestcode": "Кôд приказане локације",
        "exif-objectcycle": "Доба дана за који је медиј намењен",
        "exif-contact": "Подаци за контакт",
        "exif-writer": "Писац",
        "exif-urgency-other": "Прилагођени приоритет ($1)",
        "namespacesall": "сви",
        "monthsall": "све",
-       "confirmemail": "Потврда имејл адресе",
-       "confirmemail_noemail": "Нисте унели валидну имејл адресу у [[Special:Preferences|подешавањима]].",
+       "confirmemail": "Потврда имејл-адресе",
+       "confirmemail_noemail": "Нисте поставили важећу имејл-адресу у [[Special:Preferences|корисничким подешавањима]].",
        "confirmemail_text": "{{SITENAME}} захтева да потврдите имејл адресу пре него што почнете да користите могућности имејла.\nКликните на дугме испод за слање поруке на вашу адресу.\nУ поруци ће се налазити линк са потврдним кодом;\nунесите је у прегледач да бисте потврдили да је ваша имејл адреса важећа.",
-       "confirmemail_pending": "Ð\9fоÑ\82вÑ\80дни ÐºÐ¾Ð´ Ð²Ð°Ð¼ Ñ\98е Ð²ÐµÑ\9b Ð¿Ð¾Ñ\81лаÑ\82. Ð\90ко Ñ\81Ñ\82е Ð½ÐµÐ´Ð°Ð²Ð½Ð¾ Ð¾Ñ\82воÑ\80или Ð½Ð°Ð»Ð¾Ð³, Ð¾Ð½Ð´Ð° Ð²ÐµÑ\80оваÑ\82но Ñ\82Ñ\80еба Ð´Ð° Ñ\81аÑ\87екаÑ\82е Ð½ÐµÐºÐ¾Ð»Ð¸ÐºÐ¾ Ð¼Ð¸Ð½Ñ\83Ñ\82а Ð´Ð° Ð¿Ñ\80иÑ\81Ñ\82игне, пре него што поново затражите нови код.",
-       "confirmemail_send": "Ð\9fоÑ\88аÑ\99и Ð¿Ð¾Ñ\82вÑ\80дни ÐºÐ¾Ð´",
+       "confirmemail_pending": "Ð\9aод Ð·Ð° Ð¿Ð¾Ñ\82вÑ\80дÑ\83 Ð²Ð°Ð¼ Ñ\98е Ð²ÐµÑ\9b Ð¿Ð¾Ñ\81лаÑ\82 Ð¸Ð¼ÐµÑ\98лом.\nÐ\90ко Ñ\81Ñ\82е Ð½ÐµÐ´Ð°Ð²Ð½Ð¾ Ð¾Ñ\82воÑ\80или Ð½Ð°Ð»Ð¾Ð³, Ð¼Ð¾Ð¶Ð´Ð° Ñ\82Ñ\80еба Ð´Ð° Ñ\81аÑ\87екаÑ\82е Ð½ÐµÐºÐ¾Ð»Ð¸ÐºÐ¾ Ð¼Ð¸Ð½Ñ\83Ñ\82а Ð´Ð° Ð¿Ñ\80иÑ\81Ñ\82игне пре него што поново затражите нови код.",
+       "confirmemail_send": "Ð\9fоÑ\88аÑ\99и ÐºÐ¾Ð´ Ð·Ð° Ð¿Ð¾Ñ\82вÑ\80дÑ\83",
        "confirmemail_sent": "Потврдна порука је послата.",
-       "confirmemail_oncreate": "Ð\9fоÑ\81лаÑ\82 Ñ\98е Ð¿Ð¾Ñ\82вÑ\80дни ÐºÐ¾Ð´ на вашу имејл адресу.\nОвај код није потребан за пријављивање, али вам треба да бисте укључили могућности имејла на викију.",
+       "confirmemail_oncreate": "Ð\9fоÑ\81лаÑ\82 Ñ\98е ÐºÐ¾Ð´ Ð·Ð° Ð¿Ð¾Ñ\82вÑ\80дÑ\83 на вашу имејл адресу.\nОвај код није потребан за пријављивање, али вам треба да бисте укључили могућности имејла на викију.",
        "confirmemail_sendfailed": "{{SITENAME}} не може да пошаље имејл потврду.\nПроверите да ли је имејл адреса правилно написана.\n\nГрешка: $1",
        "confirmemail_invalid": "Неважећи код за потврду.\nКод је можда истекао.",
-       "confirmemail_needlogin": "Морате бити $1 да бисте потврдили имејл адресу.",
-       "confirmemail_success": "Ваша имејл адреса је потврђена.\nСада можете да се [[Special:UserLogin|пријавите]] и уживате у викију.",
-       "confirmemail_loggedin": "Ваша имејл адреса је сада потврђена.",
-       "confirmemail_subject": "{{SITENAME}} – потврда имејл адресе",
-       "confirmemail_body": "Ð\9dеко, Ð²ÐµÑ\80оваÑ\82но Ð\92и, Ñ\81а IP Ð°Ð´Ñ\80еÑ\81е $1,\nÑ\80егиÑ\81Ñ\82Ñ\80овао Ñ\98е Ð½Ð°Ð»Ð¾Ð³ â\80\9e$2â\80\9c Ñ\81а Ð¾Ð²Ð¾Ð¼ Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81ом Ð½Ð° Ð¿Ñ\80оÑ\98екÑ\82Ñ\83 {{SITENAME}}.\n\nÐ\94а Ð±Ð¸Ñ\81Ñ\82е Ð¿Ð¾Ñ\82вÑ\80дили Ð´Ð° Ð¾Ð²Ð°Ñ\98 Ð½Ð°Ð»Ð¾Ð³ Ñ\81Ñ\82ваÑ\80но Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð° Ð¸ Ð°ÐºÑ\82ивиÑ\80али Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82и Ð¸Ð¼ÐµÑ\98ла Ð½Ð° Ð¿Ñ\80оÑ\98екÑ\82Ñ\83 {{SITENAME}}, Ð¾Ñ\82воÑ\80иÑ\82е Ð¾Ð²Ð°Ñ\98 Ð»Ð¸Ð½Ðº Ñ\83 Ð¿Ñ\80егледаÑ\87Ñ\83:\n\n$3\n\nÐ\90ко Ð²Ð¸ *ниÑ\81Ñ\82е* Ñ\80егиÑ\81Ñ\82Ñ\80овали Ð½Ð°Ð»Ð¾Ð³, Ð¿Ñ\80аÑ\82иÑ\82е Ð¾Ð²Ð°Ñ\98 Ð»Ð¸Ð½Ðº\nда Ð±Ð¸Ñ\81Ñ\82е Ð¾Ñ\82казали Ð¿Ð¾Ñ\82вÑ\80дÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81е:\n\n$5\n\nÐ\9eваÑ\98 Ð¿Ð¾Ñ\82вÑ\80дни ÐºÐ¾Ð´ истиче у $4.",
-       "confirmemail_body_changed": "Ð\9dеко, Ð²ÐµÑ\80оваÑ\82но Ð\92и, Ñ\81 IP Ð°Ð´Ñ\80еÑ\81е $1,\nпÑ\80оменио Ñ\98е Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð°Ð»Ð¾Ð³Ð° â\80\9e$2â\80\9c Ñ\83 Ð¾Ð²Ñ\83 Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð° Ð¿Ñ\80оÑ\98екÑ\82Ñ\83 {{SITENAME}}.\n\nÐ\94а Ð±Ð¸Ñ\81Ñ\82е Ð¿Ð¾Ñ\82вÑ\80дили Ð´Ð° Ð¾Ð²Ð°Ñ\98 Ð½Ð°Ð»Ð¾Ð³ Ñ\81Ñ\82ваÑ\80но Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð° Ð¸ Ð¿Ð¾Ð½Ð¾Ð²Ð¾ Ð°ÐºÑ\82ивиÑ\80али Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82и Ð¸Ð¼ÐµÑ\98ла, Ð¾Ñ\82воÑ\80иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ñ\83 Ð¿Ñ\80егледаÑ\87Ñ\83:\n\n$3\n\nÐ\90ко Ð½Ð°Ð»Ð¾Ð³ *не* Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð°, Ð¿Ñ\80аÑ\82иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ð´Ð° Ð¾Ñ\82кажеÑ\82е Ð¿Ð¾Ñ\82вÑ\80дÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81е:\n\n$5\n\nÐ\9eваÑ\98 Ð¿Ð¾Ñ\82вÑ\80дни ÐºÐ¾Ð´ истиче $6 у $7",
-       "confirmemail_body_set": "Ð\9dеко, Ð²ÐµÑ\80оваÑ\82но Ð\92и, Ñ\81 IP Ð°Ð´Ñ\80еÑ\81е $1,\nпÑ\80оменио Ñ\98е Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð°Ð»Ð¾Ð³Ð° â\80\9e$2â\80\9c Ñ\83 Ð¾Ð²Ñ\83 Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð° {{SITENAME}}.\n\nÐ\94а Ð±Ð¸Ñ\81мо Ð¿Ð¾Ñ\82вÑ\80дили Ð´Ð° Ð¾Ð²Ð°Ñ\98 Ð½Ð°Ð»Ð¾Ð³ Ñ\81Ñ\82ваÑ\80но Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð° Ð¸ Ð¿Ð¾Ð½Ð¾Ð²Ð¾ Ð°ÐºÑ\82ивиÑ\80али\nмогÑ\83Ñ\9bноÑ\81Ñ\82и Ð¸Ð¼ÐµÑ\98ла Ð½Ð° {{SITENAME}}, Ð¾Ñ\82воÑ\80иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ñ\83 Ð¿Ñ\80егледаÑ\87Ñ\83:\n\n$3\n\nÐ\90ко Ð½Ð°Ð»Ð¾Ð³ *не* Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð°, Ð¿Ñ\80аÑ\82иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ð´Ð° Ð¾Ñ\82кажеÑ\82е Ð¿Ð¾Ñ\82вÑ\80дÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81е:\n\n$5\n\nÐ\9eваÑ\98 Ð¿Ð¾Ñ\82вÑ\80дни ÐºÐ¾Ð´ истиче $4.",
+       "confirmemail_needlogin": "Морате бити $1 да бисте потврдили своју имејл-адресу.",
+       "confirmemail_success": "Ваша имејл-адреса је потврђена.\nСада можете да се [[Special:UserLogin|пријавите]] и уживате у викију.",
+       "confirmemail_loggedin": "Ваша имејл-адреса је сада потврђена.",
+       "confirmemail_subject": "{{SITENAME}} – потврда имејл-адресе",
+       "confirmemail_body": "Ð\9dеко, Ð²ÐµÑ\80оваÑ\82но Ð\92и, Ñ\81а IP Ð°Ð´Ñ\80еÑ\81е $1,\nÑ\80егиÑ\81Ñ\82Ñ\80овао Ñ\98е Ð½Ð°Ð»Ð¾Ð³ â\80\9e$2â\80\9c Ñ\81а Ð¾Ð²Ð¾Ð¼ Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81ом Ð½Ð° Ð¿Ñ\80оÑ\98екÑ\82Ñ\83 {{SITENAME}}.\n\nÐ\94а Ð±Ð¸Ñ\81Ñ\82е Ð¿Ð¾Ñ\82вÑ\80дили Ð´Ð° Ð¾Ð²Ð°Ñ\98 Ð½Ð°Ð»Ð¾Ð³ Ñ\81Ñ\82ваÑ\80но Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð° Ð¸ Ð°ÐºÑ\82ивиÑ\80али Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82и Ð¸Ð¼ÐµÑ\98ла Ð½Ð° Ð¿Ñ\80оÑ\98екÑ\82Ñ\83 {{SITENAME}}, Ð¾Ñ\82воÑ\80иÑ\82е Ð¾Ð²Ð°Ñ\98 Ð»Ð¸Ð½Ðº Ñ\83 Ð¿Ñ\80егледаÑ\87Ñ\83:\n\n$3\n\nÐ\90ко Ð²Ð¸ *ниÑ\81Ñ\82е* Ñ\80егиÑ\81Ñ\82Ñ\80овали Ð½Ð°Ð»Ð¾Ð³, Ð¿Ñ\80аÑ\82иÑ\82е Ð¾Ð²Ð°Ñ\98 Ð»Ð¸Ð½Ðº\nда Ð±Ð¸Ñ\81Ñ\82е Ð¾Ñ\82казали Ð¿Ð¾Ñ\82вÑ\80дÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81е:\n\n$5\n\nÐ\9eваÑ\98 ÐºÐ¾Ð´ Ð·Ð° Ð¿Ð¾Ñ\82вÑ\80дÑ\83 истиче у $4.",
+       "confirmemail_body_changed": "Ð\9dеко, Ð²ÐµÑ\80оваÑ\82но Ð\92и, Ñ\81 IP Ð°Ð´Ñ\80еÑ\81е $1,\nпÑ\80оменио Ñ\98е Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð°Ð»Ð¾Ð³Ð° â\80\9e$2â\80\9c Ñ\83 Ð¾Ð²Ñ\83 Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð° Ð¿Ñ\80оÑ\98екÑ\82Ñ\83 {{SITENAME}}.\n\nÐ\94а Ð±Ð¸Ñ\81Ñ\82е Ð¿Ð¾Ñ\82вÑ\80дили Ð´Ð° Ð¾Ð²Ð°Ñ\98 Ð½Ð°Ð»Ð¾Ð³ Ñ\81Ñ\82ваÑ\80но Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð° Ð¸ Ð¿Ð¾Ð½Ð¾Ð²Ð¾ Ð°ÐºÑ\82ивиÑ\80али Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82и Ð¸Ð¼ÐµÑ\98ла, Ð¾Ñ\82воÑ\80иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ñ\83 Ð¿Ñ\80егледаÑ\87Ñ\83:\n\n$3\n\nÐ\90ко Ð½Ð°Ð»Ð¾Ð³ *не* Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð°, Ð¿Ñ\80аÑ\82иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ð´Ð° Ð¾Ñ\82кажеÑ\82е Ð¿Ð¾Ñ\82вÑ\80дÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81е:\n\n$5\n\nÐ\9eваÑ\98 ÐºÐ¾Ð´ Ð·Ð° Ð¿Ð¾Ñ\82вÑ\80дÑ\83 истиче $6 у $7",
+       "confirmemail_body_set": "Ð\9dеко, Ð²ÐµÑ\80оваÑ\82но Ð\92и, Ñ\81 IP Ð°Ð´Ñ\80еÑ\81е $1,\nпÑ\80оменио Ñ\98е Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð°Ð»Ð¾Ð³Ð° â\80\9e$2â\80\9c Ñ\83 Ð¾Ð²Ñ\83 Ð°Ð´Ñ\80еÑ\81Ñ\83 Ð½Ð° {{SITENAME}}.\n\nÐ\94а Ð±Ð¸Ñ\81мо Ð¿Ð¾Ñ\82вÑ\80дили Ð´Ð° Ð¾Ð²Ð°Ñ\98 Ð½Ð°Ð»Ð¾Ð³ Ñ\81Ñ\82ваÑ\80но Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð° Ð¸ Ð¿Ð¾Ð½Ð¾Ð²Ð¾ Ð°ÐºÑ\82ивиÑ\80али\nмогÑ\83Ñ\9bноÑ\81Ñ\82и Ð¸Ð¼ÐµÑ\98ла Ð½Ð° {{SITENAME}}, Ð¾Ñ\82воÑ\80иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ñ\83 Ð¿Ñ\80егледаÑ\87Ñ\83:\n\n$3\n\nÐ\90ко Ð½Ð°Ð»Ð¾Ð³ *не* Ð¿Ñ\80ипада Ð²Ð°Ð¼Ð°, Ð¿Ñ\80аÑ\82иÑ\82е Ñ\81ледеÑ\9bи Ð»Ð¸Ð½Ðº Ð´Ð° Ð¾Ñ\82кажеÑ\82е Ð¿Ð¾Ñ\82вÑ\80дÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81е:\n\n$5\n\nÐ\9eваÑ\98 ÐºÐ¾Ð´ Ð·Ð° Ð¿Ð¾Ñ\82вÑ\80дÑ\83 истиче $4.",
        "confirmemail_invalidated": "Потврда имејл адресе је отказана",
        "invalidateemail": "Отказивање потврде имејла",
        "notificationemail_subject_changed": "Регистрована имејл адреса на пројекту {{SITENAME}} је промењена",
        "scarytranscludefailed": "[Добављање шаблона за $1 није успело]",
        "scarytranscludefailed-httpstatus": "[Не могу да преузмем шаблон $1: HTTP $2]",
        "scarytranscludetoolong": "[URL адреса је предугачка]",
-       "deletedwhileediting": "<strong>УпозоÑ\80еÑ\9aе</strong>: Ð\9eва Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¾брисана након што сте почели са уређивањем!",
+       "deletedwhileediting": "<strong>УпозоÑ\80еÑ\9aе</strong>: Ð\9eва Ñ\81Ñ\82Ñ\80аниÑ\86а Ñ\98е Ð¸Ð·брисана након што сте почели са уређивањем!",
        "confirmrecreate": "{{GENDER:$1|Корисник|Корисница}} [[User:$1|$1]] ([[User talk:$1|разговор]]) је {{GENDER:$1|обрисао|обрисала}} ову страницу након што сте почели да је уређујете из следећег разлога:\n: <em>$2</em>\nПотврдите да стварно желите да направите страницу.",
        "confirmrecreate-noreason": "{{GENDER:$1|Корисник|Корисница}} [[User:$1|$1]] ([[User talk:$1|разговор]]) је {{GENDER:$1|обрисао|обрисала}} ову страницу након што сте почели да је уређујете. Потврдите да стварно желите да поново направите ову страницу.",
        "recreate": "Поново направи",
        "imgmultigo": "Иди!",
        "imgmultigoto": "Иди на страницу $1",
        "img-lang-default": "(подразумевани језик)",
-       "img-lang-info": "Ð\9fÑ\80икажи Ð¾Ð²Ñ\83 Ñ\81ликÑ\83 Ð½Ð° $1. $2",
+       "img-lang-info": "РендеÑ\80Ñ\83Ñ\98 Ð¾Ð²Ñ\83 Ñ\81ликÑ\83 Ñ\83 $1. $2",
        "img-lang-go": "Иди",
        "ascending_abbrev": "раст.",
        "descending_abbrev": "опад.",
        "watchlistedit-too-many": "Има превише страница за приказ овде.",
        "watchlisttools-clear": "очисти списак надгледања",
        "watchlisttools-view": "погледај релевантне промене",
-       "watchlisttools-edit": "прикажи и уреди списак надгледања",
+       "watchlisttools-edit": "погледај и уреди списак надгледања",
        "watchlisttools-raw": "уреди сиров списак надгледања",
        "iranian-calendar-m1": "Фарвардин",
        "iranian-calendar-m2": "Ордибехешт",
        "tags-source-manual": "Ручно је додају корисници и ботови",
        "tags-source-none": "Ван употребе",
        "tags-edit": "уреди",
-       "tags-delete": "обриши",
+       "tags-delete": "избриши",
        "tags-activate": "активирај",
        "tags-deactivate": "деактивирај",
        "tags-hitcount": "$1 {{PLURAL:$1|промена|промене|промена}}",
        "tags-create-warnings-below": "Правите нову ознаку, желите ли да наставите?",
        "tags-delete-title": "Брисање ознака",
        "tags-delete-explanation-initial": "Бришете ознаку „$1“ из базе података.",
-       "tags-delete-explanation-warning": "Ова радња је <strong>неповратна</strong> и <strong>не може се поништити</strong>, чак ни администратори базе података је не могу поништити. Будите сигурни да је ово ознака коју желите обрисати.",
+       "tags-delete-explanation-warning": "Ова радња је <strong>неповратна</strong> и <strong>не може да се поништи</strong>. Ово не могу да ураде чак ни администратори базе података. Будите сигурни да је ово ознака коју желите избрисати.",
        "tags-delete-reason": "Разлог:",
-       "tags-delete-submit": "Ð\9dеповÑ\80аÑ\82но Ð¾бриши ову ознаку",
+       "tags-delete-submit": "Ð\9dеповÑ\80аÑ\82но Ð¸Ð·бриши ову ознаку",
        "tags-delete-not-found": "Ознака „$1“ не постоји.",
-       "tags-delete-too-many-uses": "Ð\9eзнака â\80\9e$1â\80\9d Ñ\98е Ð¿Ñ\80имеÑ\9aена Ð½Ð° Ð²Ð¸Ñ\88е Ð¾Ð´ $2 {{PLURAL:$2|Ñ\80евизиÑ\98е|Ñ\80евизиÑ\98а}}, Ñ\88Ñ\82о Ð·Ð½Ð°Ñ\87и Ð´Ð° Ñ\81е Ð½Ðµ Ð¼Ð¾Ð¶Ðµ Ð¾брисати.",
+       "tags-delete-too-many-uses": "Ð\9eзнака â\80\9e$1â\80\9d Ñ\98е Ð¿Ñ\80имеÑ\9aена Ð½Ð° Ð²Ð¸Ñ\88е Ð¾Ð´ $2 {{PLURAL:$2|Ñ\80евизиÑ\98е|Ñ\80евизиÑ\98а}}, Ñ\88Ñ\82о Ð·Ð½Ð°Ñ\87и Ð´Ð° Ñ\81е Ð½Ðµ Ð¼Ð¾Ð¶Ðµ Ð¸Ð·брисати.",
        "tags-delete-no-permission": "Немате дозволу да бришете ознаке промена.",
        "tags-activate-title": "Активирање ознака",
        "tags-activate-question": "Активирате ознаку „$1“.",
        "logentry-newusers-create": "$1 је {{GENDER:$2|отворио|отворила}} кориснички налог",
        "logentry-newusers-create2": "$1 је {{GENDER:$2|отворио|отворила}} кориснички налог $3",
        "logentry-newusers-byemail": "$1 је {{GENDER:$2|отворио|отворила}} кориснички налог $3 и лозинка је послата на имејл",
-       "logentry-newusers-autocreate": "Кориснички налог $1 је аутоматски {{GENDER:$2|отворен}}",
+       "logentry-newusers-autocreate": "$1 је аутоматски {{GENDER:$2|отворио|отворила}} кориснички налог",
        "logentry-protect-move_prot": "$1 је {{GENDER:$2|преместио|преместила}} подешавања заштите са $4 на $3",
        "logentry-protect-unprotect": "$1 je {{GENDER:$2|скинуо|скинула}} заштиту са странице $3",
        "logentry-protect-protect": "$1 је {{GENDER:$2|заштитио|заштитила}} $3 $4",
        "logentry-rights-rights-legacy": "$1 је {{GENDER:$2|променио|променила}} чланство групе за $3",
        "logentry-rights-autopromote": "$1 је аутоматски {{GENDER:$2|унапређен|унапређена}} из $4 у $5",
        "logentry-upload-upload": "$1 је {{GENDER:$2|отпремио|отпремила}} $3",
-       "logentry-upload-overwrite": "$1 је {{GENDER:$2|отпремио|отпремила}} нову верзију $3",
+       "logentry-upload-overwrite": "$1 је {{GENDER:$2|отпремио|отпремила}} нову верзију датотеке $3",
        "logentry-upload-revert": "$1 је {{GENDER:$2|отпремио|отпремила}} $3",
        "log-name-managetags": "Евиденција управљања ознакама",
-       "log-description-managetags": "Ð\9dа Ð¾Ð²Ð¾Ñ\98 Ñ\81Ñ\82Ñ\80аниÑ\86и Ñ\81е Ð½Ð°Ð»Ð°Ð·Ð¸ Ñ\81пиÑ\81ак Ð¸Ð·Ð¼ÐµÐ½Ð° Ñ\83 Ð²ÐµÐ·Ð¸ [[Special:Tags|ознака]]. Ð\95виденÑ\86иÑ\98а Ñ\81адÑ\80жи Ñ\81амо Ñ\80адÑ\9aе ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ñ\80Ñ\83Ñ\87но Ð¸Ð·Ð²Ñ\80Ñ\88или Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñ\81Ñ\82Ñ\80аÑ\82оÑ\80и; Ñ\83ноÑ\81и Ð·Ð° Ð¾Ð·Ð½Ð°ÐºÐµ ÐºÐ¾Ñ\98е Ñ\98е Ð½Ð°Ð¿Ñ\80авио Ð¸Ð»Ð¸ Ð¾брисао вики софтвера се не налазе у овој евиденцији.",
+       "log-description-managetags": "Ð\9dа Ð¾Ð²Ð¾Ñ\98 Ñ\81Ñ\82Ñ\80аниÑ\86и Ñ\81е Ð½Ð°Ð»Ð°Ð·Ð¸ Ñ\81пиÑ\81ак Ð¸Ð·Ð¼ÐµÐ½Ð° Ñ\83 Ð²ÐµÐ·Ð¸ [[Special:Tags|ознака]]. Ð\95виденÑ\86иÑ\98а Ñ\81адÑ\80жи Ñ\81амо Ñ\80адÑ\9aе ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ñ\80Ñ\83Ñ\87но Ð¸Ð·Ð²Ñ\80Ñ\88или Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñ\81Ñ\82Ñ\80аÑ\82оÑ\80и; Ñ\83ноÑ\81и Ð·Ð° Ð¾Ð·Ð½Ð°ÐºÐµ ÐºÐ¾Ñ\98е Ñ\98е Ð½Ð°Ð¿Ñ\80авио Ð¸Ð»Ð¸ Ð¸Ð·брисао вики софтвера се не налазе у овој евиденцији.",
        "logentry-managetags-create": "$1 је {{GENDER:$2|направио|направила}} ознаку „$4“",
        "logentry-managetags-delete": "$1 је {{GENDER:$2|обрисао|обрисала}} ознаку „$4“ (уклоњена је из $5 {{PLURAL:$5|ревизије или уноса у евиденцији|ревизија и/или уноса у евиденцији}})",
        "logentry-managetags-activate": "$1 је {{GENDER:$2|активирао|активирала}} ознаку „$4“ за употребу од стране корисника и ботова",
        "feedback-thanks-title": "Хвала вам!",
        "feedback-useragent": "Кориснички агент:",
        "searchsuggest-search": "Претрага",
-       "searchsuggest-containing": "садржи...",
+       "searchsuggest-containing": "садржи",
        "api-error-badtoken": "Унутрашња грешка: лош токен.",
        "api-error-emptypage": "Стварање нових празних страница није дозвољено.",
        "api-error-publishfailed": "Унутрашња грешка: сервер није успео да објави привремену датотеку.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (омогућена)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>онемогућена</strong>)",
        "mediastatistics": "Статистика медија",
-       "mediastatistics-summary": "СÑ\82аÑ\82иÑ\81Ñ\82ике Ð¾ Ñ\82иповима Ð¿Ð¾Ñ\81лаÑ\82иÑ\85 Ð´Ð°Ñ\82оÑ\82ека. Ð\9eвде Ñ\81Ñ\83 Ñ\83Ñ\80аÑ\87Ñ\83наÑ\82е Ñ\81амо Ð½Ð°Ñ\98Ñ\81коÑ\80иÑ\98е Ð²ÐµÑ\80зиÑ\98е Ð´Ð°Ñ\82оÑ\82ека. Ð¡Ñ\82аÑ\80е Ð¸Ð»Ð¸ Ð¾брисане верзије нису урачунате.",
+       "mediastatistics-summary": "СÑ\82аÑ\82иÑ\81Ñ\82ике Ð¾ Ñ\82иповима Ð¾Ñ\82пÑ\80емÑ\99ениÑ\85 Ð´Ð°Ñ\82оÑ\82ека. Ð\9eвде Ñ\81Ñ\83 Ñ\83Ñ\80аÑ\87Ñ\83наÑ\82е Ñ\81амо Ð½Ð°Ñ\98Ñ\81коÑ\80иÑ\98е Ð²ÐµÑ\80зиÑ\98е Ð´Ð°Ñ\82оÑ\82ека. Ð¡Ñ\82аÑ\80е Ð¸Ð»Ð¸ Ð¸Ð·брисане верзије нису урачунате.",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 бајт|$1 бајта|$1 бајтова}} ($2; $3%)",
        "mediastatistics-bytespertype": "Укупна величина датотеке овог одељка: {{PLURAL:$1|$1 бајт|$1 бајта|$1 бајтова}} ($2; $3%).",
        "mediastatistics-allbytes": "Укупна величина свих датотека: {{PLURAL:$1|$1 бајт|$1 бајта|$1 бајтова}} ($2).",
        "mediastatistics-header-drawing": "Цртежи (векторске слике)",
        "mediastatistics-header-audio": "Аудио",
        "mediastatistics-header-video": "Видео",
+       "mediastatistics-header-multimedia": "Обогаћени медији",
        "mediastatistics-header-office": "Канцеларија",
        "mediastatistics-header-text": "Текстуалне",
        "mediastatistics-header-executable": "Извршне",
        "mw-widgets-mediasearch-noresults": "Нема резултата.",
        "mw-widgets-titleinput-description-new-page": "страница још увек не постоји",
        "mw-widgets-titleinput-description-redirect": "преусмерава на $1",
-       "mw-widgets-categoryselector-add-category-placeholder": "Додај категорију...",
+       "mw-widgets-categoryselector-add-category-placeholder": "Додајте категорију…",
        "mw-widgets-usersmultiselect-placeholder": "Додај још...",
        "date-range-from": "Од датума:",
        "date-range-to": "До датума:",
        "authform-wrongtoken": "Погрешан токен",
        "specialpage-securitylevel-not-allowed-title": "Није дозвољено",
        "specialpage-securitylevel-not-allowed": "Жао нам је, није вам дозвољено да користите ову страницу јер не могу да потврдим ваш идентитет.",
-       "authpage-cannot-login": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð·Ð°Ð¿Ð¾Ñ\87еÑ\82и пријаву.",
+       "authpage-cannot-login": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð·Ð°Ð¿Ð¾Ñ\87нем пријаву.",
        "authpage-cannot-login-continue": "Не могу да наставим са пријавом. Ваша сесија је највероватније истекла.",
-       "authpage-cannot-create": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð·Ð°Ð¿Ð¾Ñ\87еÑ\82и Ñ\81тварање налога.",
-       "authpage-cannot-link": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð·Ð°Ð¿Ð¾Ñ\87еÑ\82и Ñ\81паÑ\98ање налога.",
+       "authpage-cannot-create": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð·Ð°Ð¿Ð¾Ñ\87нем Ð¾тварање налога.",
+       "authpage-cannot-link": "Ð\9dе Ð¼Ð¾Ð³Ñ\83 Ð´Ð° Ð·Ð°Ð¿Ð¾Ñ\87нем Ð¿Ð¾Ð²ÐµÐ·Ð¸Ð²ање налога.",
        "cannotauth-not-allowed-title": "Приступ је одбијен",
        "cannotauth-not-allowed": "Није вам дозвољено да користите ову страницу",
        "changecredentials": "Промена акредитива",
        "pageid": "ID странице: $1",
        "rawhtml-notallowed": "&lt;html&gt; тагови не могу да се користе ван нормалних страница.",
        "gotointerwiki": "Напуштање пројекта {{SITENAME}}",
-       "gotointerwiki-invalid": "Ð\9eдабÑ\80ани наслов је невалидан.",
+       "gotointerwiki-invalid": "Ð\9dаведени наслов је невалидан.",
        "gotointerwiki-external": "Управо ћете да напустите пројекат {{SITENAME}} да бисте на засебном веб-сајту посетили [[$2]].\n\n'''[$1 Продужи на $1]'''",
        "undelete-cantedit": "Не можете повратити ову страницу јер немате дозволу да је уређујете.",
        "undelete-cantcreate": "Не можете повратити ову страницу јер нема постојеће странице са овим именом и немате дозволу да направите ову страницу.",
        "pagedata-title": "Подаци странице",
        "pagedata-not-acceptable": "Није пронађен одговарајући облик. Подржане MIME-врсте: $1",
-       "pagedata-bad-title": "Ð\9dеважеÑ\9bи наслов: $1.",
+       "pagedata-bad-title": "Ð\9dевалидан наслов: $1.",
        "passwordpolicies": "Правила за лозинке",
        "passwordpolicies-group": "Група",
        "passwordpolicies-policies": "Правила",
index 0616922..ab4cbfc 100644 (file)
        "filehist-filesize": "Dosya boyutu",
        "filehist-comment": "Açıklama",
        "imagelinks": "Dosya kullanımı",
-       "linkstoimage": "Bu görüntü dosyasına bağlantısı olan {{PLURAL:$1|sayfa|$1 sayfa}}:",
-       "linkstoimage-more": "$1'den fazla {{PLURAL:$1|sayfa|sayfa}} bu dosyaya bağlantı veriyor.\nSıradaki liste sadece bu dosyaya bağlantı veren {{PLURAL:$1|ilk dosyayı|ilk $1 dosyayı}} gösteriyor.\n[[Special:WhatLinksHere/$2|Tam bir liste]] mevcuttur.",
-       "nolinkstoimage": "Bu dosyaya bağlantı veren bir sayfa yok.",
+       "linkstoimage": "Aşağıdaki {{PLURAL:$1|sayfa|$1 sayfa}} bu dosyayı kullanmaktadır:",
+       "linkstoimage-more": "$1 {{PLURAL:$1|sayfadan|sayfadan}} fazlası bu dosyayı kullanıyor.\nAşağıdaki listede sadece bu dosyayı kullanan  {{PLURAL:$1|ilk sayfa|ilk $1 sayfa}} gösterilmektedir.\n[[Special:WhatLinksHere/$2|Tam listesi]] mevcuttur.",
+       "nolinkstoimage": "Bu dosyayı kullanan sayfa yok.",
        "morelinkstoimage": "Bu dosyaya [[Special:WhatLinksHere/$1|daha fazla bağlantıları]] gör.",
        "linkstoimage-redirect": "$1 (dosya yönlendirme) $2",
        "duplicatesoffile": "Şu {{PLURAL:$1|dosya|$1 dosya}}, bu dosyanın kopyası ([[Special:FileDuplicateSearch/$2|daha fazla ayrıntı]]):",
index 10922a6..ac26098 100644 (file)
        "action-delete": "删除本页",
        "action-deleterevision": "删除修订",
        "action-deletelogentry": "删除日志记录",
-       "action-deletedhistory": "查看页面被删除的历史",
+       "action-deletedhistory": "查看已被删除的页面历史",
        "action-deletedtext": "查看已删除的修订版本文字",
        "action-browsearchive": "搜索已被删除的页面",
        "action-undelete": "还原页面",
index a55372c..b1109ca 100644 (file)
        "action-delete": "刪除此頁面",
        "action-deleterevision": "刪除修訂",
        "action-deletelogentry": "刪除日誌項目",
-       "action-deletedhistory": "檢視頁面的刪除歷史",
+       "action-deletedhistory": "檢視已被刪除的頁面歷史",
        "action-deletedtext": "查看已刪除的修訂版本文字",
        "action-browsearchive": "搜尋已刪除頁面",
        "action-undelete": "取消刪除頁面",
diff --git a/maintenance/addChangeTag.php b/maintenance/addChangeTag.php
new file mode 100644 (file)
index 0000000..b63a2e2
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * Adds a change tag to the wiki.
+ *
+ * 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 Maintenance
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Adds a change tag to the wiki
+ *
+ * @ingroup Maintenance
+ * @since 1.32
+ */
+class AddChangeTag extends Maintenance {
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Adds a change tag to the wiki.' );
+
+               $this->addOption( 'tag', 'Tag to add', true, true );
+               $this->addOption( 'reason', 'Reason for adding the tag', true, true );
+       }
+
+       public function execute() {
+               $user = User::newSystemUser( 'Maintenance script', [ 'steal' => true ] );
+
+               $tag = $this->getOption( 'tag' );
+
+               $status = ChangeTags::createTagWithChecks(
+                       $tag,
+                       $this->getOption( 'reason' ),
+                       $user
+               );
+
+               if ( !$status->isGood() ) {
+                       $this->fatalError( $status->getWikiText( null, null, 'en' ) );
+               }
+
+               $this->output( "$tag was created.\n" );
+       }
+}
+
+$maintClass = AddChangeTag::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/maintenance/backup.inc b/maintenance/backup.inc
deleted file mode 100644 (file)
index 6eeb81b..0000000
+++ /dev/null
@@ -1,423 +0,0 @@
-<?php
-/**
- * Base classes for database dumpers
- *
- * Copyright © 2005 Brion Vibber <brion@pobox.com>
- * https://www.mediawiki.org/
- *
- * 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 Dump Maintenance
- */
-
-require_once __DIR__ . '/Maintenance.php';
-
-use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\IDatabase;
-
-/**
- * @ingroup Dump Maintenance
- */
-class BackupDumper extends Maintenance {
-       public $reporting = true;
-       public $pages = null; // all pages
-       public $skipHeader = false; // don't output <mediawiki> and <siteinfo>
-       public $skipFooter = false; // don't output </mediawiki>
-       public $startId = 0;
-       public $endId = 0;
-       public $revStartId = 0;
-       public $revEndId = 0;
-       public $dumpUploads = false;
-       public $dumpUploadFileContents = false;
-       public $orderRevs = false;
-
-       protected $reportingInterval = 100;
-       protected $pageCount = 0;
-       protected $revCount = 0;
-       protected $server = null; // use default
-       protected $sink = null; // Output filters
-       protected $lastTime = 0;
-       protected $pageCountLast = 0;
-       protected $revCountLast = 0;
-
-       protected $outputTypes = [];
-       protected $filterTypes = [];
-
-       protected $ID = 0;
-
-       /**
-        * The dependency-injected database to use.
-        *
-        * @var IDatabase|null
-        *
-        * @see self::setDB
-        */
-       protected $forcedDb = null;
-
-       /** @var LoadBalancer */
-       protected $lb;
-
-       // @todo Unused?
-       private $stubText = false; // include rev_text_id instead of text; for 2-pass dump
-
-       /**
-        * @param array|null $args For backward compatibility
-        */
-       function __construct( $args = null ) {
-               parent::__construct();
-               $this->stderr = fopen( "php://stderr", "wt" );
-
-               // Built-in output and filter plugins
-               $this->registerOutput( 'file', DumpFileOutput::class );
-               $this->registerOutput( 'gzip', DumpGZipOutput::class );
-               $this->registerOutput( 'bzip2', DumpBZip2Output::class );
-               $this->registerOutput( 'dbzip2', DumpDBZip2Output::class );
-               $this->registerOutput( '7zip', Dump7ZipOutput::class );
-
-               $this->registerFilter( 'latest', DumpLatestFilter::class );
-               $this->registerFilter( 'notalk', DumpNotalkFilter::class );
-               $this->registerFilter( 'namespace', DumpNamespaceFilter::class );
-
-               // These three can be specified multiple times
-               $this->addOption( 'plugin', 'Load a dump plugin class. Specify as <class>[:<file>].',
-                       false, true, false, true );
-               $this->addOption( 'output', 'Begin a filtered output stream; Specify as <type>:<file>. ' .
-                       '<type>s: file, gzip, bzip2, 7zip, dbzip2', false, true, false, true );
-               $this->addOption( 'filter', 'Add a filter on an output branch. Specify as ' .
-                       '<type>[:<options>]. <types>s: latest, notalk, namespace', false, true, false, true );
-               $this->addOption( 'report', 'Report position and speed after every n pages processed. ' .
-                       'Default: 100.', false, true );
-               $this->addOption( 'server', 'Force reading from MySQL server', false, true );
-               $this->addOption( '7ziplevel', '7zip compression level for all 7zip outputs. Used for ' .
-                       '-mx option to 7za command.', false, true );
-
-               if ( $args ) {
-                       // Args should be loaded and processed so that dump() can be called directly
-                       // instead of execute()
-                       $this->loadWithArgv( $args );
-                       $this->processOptions();
-               }
-       }
-
-       /**
-        * @param string $name
-        * @param string $class Name of output filter plugin class
-        */
-       function registerOutput( $name, $class ) {
-               $this->outputTypes[$name] = $class;
-       }
-
-       /**
-        * @param string $name
-        * @param string $class Name of filter plugin class
-        */
-       function registerFilter( $name, $class ) {
-               $this->filterTypes[$name] = $class;
-       }
-
-       /**
-        * Load a plugin and register it
-        *
-        * @param string $class Name of plugin class; must have a static 'register'
-        *   method that takes a BackupDumper as a parameter.
-        * @param string $file Full or relative path to the PHP file to load, or empty
-        */
-       function loadPlugin( $class, $file ) {
-               if ( $file != '' ) {
-                       require_once $file;
-               }
-               $register = [ $class, 'register' ];
-               $register( $this );
-       }
-
-       function execute() {
-               throw new MWException( 'execute() must be overridden in subclasses' );
-       }
-
-       /**
-        * Processes arguments and sets $this->$sink accordingly
-        */
-       function processOptions() {
-               $sink = null;
-               $sinks = [];
-
-               $options = $this->orderedOptions;
-               foreach ( $options as $arg ) {
-                       $opt = $arg[0];
-                       $param = $arg[1];
-
-                       switch ( $opt ) {
-                               case 'plugin':
-                                       $val = explode( ':', $param );
-
-                                       if ( count( $val ) === 1 ) {
-                                               $this->loadPlugin( $val[0], '' );
-                                       } elseif ( count( $val ) === 2 ) {
-                                               $this->loadPlugin( $val[0], $val[1] );
-                                       } else {
-                                               $this->fatalError( 'Invalid plugin parameter' );
-                                               return;
-                                       }
-
-                                       break;
-                               case 'output':
-                                       $split = explode( ':', $param, 2 );
-                                       if ( count( $split ) !== 2 ) {
-                                               $this->fatalError( 'Invalid output parameter' );
-                                       }
-                                       list( $type, $file ) = $split;
-                                       if ( !is_null( $sink ) ) {
-                                               $sinks[] = $sink;
-                                       }
-                                       if ( !isset( $this->outputTypes[$type] ) ) {
-                                               $this->fatalError( "Unrecognized output sink type '$type'" );
-                                       }
-                                       $class = $this->outputTypes[$type];
-                                       if ( $type === "7zip" ) {
-                                               $sink = new $class( $file, intval( $this->getOption( '7ziplevel' ) ) );
-                                       } else {
-                                               $sink = new $class( $file );
-                                       }
-
-                                       break;
-                               case 'filter':
-                                       if ( is_null( $sink ) ) {
-                                               $sink = new DumpOutput();
-                                       }
-
-                                       $split = explode( ':', $param );
-                                       $key = $split[0];
-
-                                       if ( !isset( $this->filterTypes[$key] ) ) {
-                                               $this->fatalError( "Unrecognized filter type '$key'" );
-                                       }
-
-                                       $type = $this->filterTypes[$key];
-
-                                       if ( count( $split ) === 1 ) {
-                                               $filter = new $type( $sink );
-                                       } elseif ( count( $split ) === 2 ) {
-                                               $filter = new $type( $sink, $split[1] );
-                                       } else {
-                                               $this->fatalError( 'Invalid filter parameter' );
-                                       }
-
-                                       // references are lame in php...
-                                       unset( $sink );
-                                       $sink = $filter;
-
-                                       break;
-                       }
-               }
-
-               if ( $this->hasOption( 'report' ) ) {
-                       $this->reportingInterval = intval( $this->getOption( 'report' ) );
-               }
-
-               if ( $this->hasOption( 'server' ) ) {
-                       $this->server = $this->getOption( 'server' );
-               }
-
-               if ( is_null( $sink ) ) {
-                       $sink = new DumpOutput();
-               }
-               $sinks[] = $sink;
-
-               if ( count( $sinks ) > 1 ) {
-                       $this->sink = new DumpMultiWriter( $sinks );
-               } else {
-                       $this->sink = $sink;
-               }
-       }
-
-       function dump( $history, $text = WikiExporter::TEXT ) {
-               # Notice messages will foul up your XML output even if they're
-               # relatively harmless.
-               if ( ini_get( 'display_errors' ) ) {
-                       ini_set( 'display_errors', 'stderr' );
-               }
-
-               $this->initProgress( $history );
-
-               $db = $this->backupDb();
-               $exporter = new WikiExporter( $db, $history, WikiExporter::STREAM, $text );
-               $exporter->dumpUploads = $this->dumpUploads;
-               $exporter->dumpUploadFileContents = $this->dumpUploadFileContents;
-
-               $wrapper = new ExportProgressFilter( $this->sink, $this );
-               $exporter->setOutputSink( $wrapper );
-
-               if ( !$this->skipHeader ) {
-                       $exporter->openStream();
-               }
-               # Log item dumps: all or by range
-               if ( $history & WikiExporter::LOGS ) {
-                       if ( $this->startId || $this->endId ) {
-                               $exporter->logsByRange( $this->startId, $this->endId );
-                       } else {
-                               $exporter->allLogs();
-                       }
-               } elseif ( is_null( $this->pages ) ) {
-                       # Page dumps: all or by page ID range
-                       if ( $this->startId || $this->endId ) {
-                               $exporter->pagesByRange( $this->startId, $this->endId, $this->orderRevs );
-                       } elseif ( $this->revStartId || $this->revEndId ) {
-                               $exporter->revsByRange( $this->revStartId, $this->revEndId );
-                       } else {
-                               $exporter->allPages();
-                       }
-               } else {
-                       # Dump of specific pages
-                       $exporter->pagesByName( $this->pages );
-               }
-
-               if ( !$this->skipFooter ) {
-                       $exporter->closeStream();
-               }
-
-               $this->report( true );
-       }
-
-       /**
-        * Initialise starting time and maximum revision count.
-        * We'll make ETA calculations based an progress, assuming relatively
-        * constant per-revision rate.
-        * @param int $history WikiExporter::CURRENT or WikiExporter::FULL
-        */
-       function initProgress( $history = WikiExporter::FULL ) {
-               $table = ( $history == WikiExporter::CURRENT ) ? 'page' : 'revision';
-               $field = ( $history == WikiExporter::CURRENT ) ? 'page_id' : 'rev_id';
-
-               $dbr = $this->forcedDb;
-               if ( $this->forcedDb === null ) {
-                       $dbr = wfGetDB( DB_REPLICA );
-               }
-               $this->maxCount = $dbr->selectField( $table, "MAX($field)", '', __METHOD__ );
-               $this->startTime = microtime( true );
-               $this->lastTime = $this->startTime;
-               $this->ID = getmypid();
-       }
-
-       /**
-        * @todo Fixme: the --server parameter is currently not respected, as it
-        * doesn't seem terribly easy to ask the load balancer for a particular
-        * connection by name.
-        * @return IDatabase
-        */
-       function backupDb() {
-               if ( $this->forcedDb !== null ) {
-                       return $this->forcedDb;
-               }
-
-               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-               $this->lb = $lbFactory->newMainLB();
-               $db = $this->lb->getConnection( DB_REPLICA, 'dump' );
-
-               // Discourage the server from disconnecting us if it takes a long time
-               // to read out the big ol' batch query.
-               $db->setSessionOptions( [ 'connTimeout' => 3600 * 24 ] );
-
-               return $db;
-       }
-
-       /**
-        * Force the dump to use the provided database connection for database
-        * operations, wherever possible.
-        *
-        * @param IDatabase|null $db (Optional) the database connection to use. If null, resort to
-        *   use the globally provided ways to get database connections.
-        */
-       function setDB( IDatabase $db = null ) {
-               parent::setDB( $db );
-               $this->forcedDb = $db;
-       }
-
-       function __destruct() {
-               if ( isset( $this->lb ) ) {
-                       $this->lb->closeAll();
-               }
-       }
-
-       function backupServer() {
-               global $wgDBserver;
-
-               return $this->server
-                       ? $this->server
-                       : $wgDBserver;
-       }
-
-       function reportPage() {
-               $this->pageCount++;
-       }
-
-       function revCount() {
-               $this->revCount++;
-               $this->report();
-       }
-
-       function report( $final = false ) {
-               if ( $final xor ( $this->revCount % $this->reportingInterval == 0 ) ) {
-                       $this->showReport();
-               }
-       }
-
-       function showReport() {
-               if ( $this->reporting ) {
-                       $now = wfTimestamp( TS_DB );
-                       $nowts = microtime( true );
-                       $deltaAll = $nowts - $this->startTime;
-                       $deltaPart = $nowts - $this->lastTime;
-                       $this->pageCountPart = $this->pageCount - $this->pageCountLast;
-                       $this->revCountPart = $this->revCount - $this->revCountLast;
-
-                       if ( $deltaAll ) {
-                               $portion = $this->revCount / $this->maxCount;
-                               $eta = $this->startTime + $deltaAll / $portion;
-                               $etats = wfTimestamp( TS_DB, intval( $eta ) );
-                               $pageRate = $this->pageCount / $deltaAll;
-                               $revRate = $this->revCount / $deltaAll;
-                       } else {
-                               $pageRate = '-';
-                               $revRate = '-';
-                               $etats = '-';
-                       }
-                       if ( $deltaPart ) {
-                               $pageRatePart = $this->pageCountPart / $deltaPart;
-                               $revRatePart = $this->revCountPart / $deltaPart;
-                       } else {
-                               $pageRatePart = '-';
-                               $revRatePart = '-';
-                       }
-                       $this->progress( sprintf(
-                               "%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), "
-                                       . "%d revs (%0.1f|%0.1f/sec all|curr), ETA %s [max %d]",
-                               $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate,
-                               $pageRatePart, $this->revCount, $revRate, $revRatePart, $etats,
-                               $this->maxCount
-                       ) );
-                       $this->lastTime = $nowts;
-                       $this->revCountLast = $this->revCount;
-               }
-       }
-
-       function progress( $string ) {
-               if ( $this->reporting ) {
-                       fwrite( $this->stderr, $string . "\n" );
-               }
-       }
-}
diff --git a/maintenance/benchmarks/bench_strtr_str_replace.php b/maintenance/benchmarks/bench_strtr_str_replace.php
deleted file mode 100644 (file)
index 2c065f6..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-/**
- * Benchmark for strtr() vs str_replace().
- *
- * This come from r75429 message.
- *
- * 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 Benchmark
- */
-
-require_once __DIR__ . '/Benchmarker.php';
-
-function bfNormalizeTitleStrTr( $str ) {
-       return strtr( $str, '_', ' ' );
-}
-
-function bfNormalizeTitleStrReplace( $str ) {
-       return str_replace( '_', ' ', $str );
-}
-
-/**
- * Maintenance script that benchmarks for strtr() vs str_replace().
- *
- * @ingroup Benchmark
- */
-class BenchStrtrStrReplace extends Benchmarker {
-       public function __construct() {
-               parent::__construct();
-               $this->addDescription( 'Benchmark for strtr() vs str_replace().' );
-       }
-
-       public function execute() {
-               $this->bench( [
-                       [ 'function' => [ $this, 'benchstrtr' ] ],
-                       [ 'function' => [ $this, 'benchstr_replace' ] ],
-                       [ 'function' => [ $this, 'benchstrtr_indirect' ] ],
-                       [ 'function' => [ $this, 'benchstr_replace_indirect' ] ],
-               ] );
-       }
-
-       protected function benchstrtr() {
-               strtr( "[[MediaWiki:Some_random_test_page]]", "_", " " );
-       }
-
-       protected function benchstr_replace() {
-               str_replace( "_", " ", "[[MediaWiki:Some_random_test_page]]" );
-       }
-
-       protected function benchstrtr_indirect() {
-               bfNormalizeTitleStrTr( "[[MediaWiki:Some_random_test_page]]" );
-       }
-
-       protected function benchstr_replace_indirect() {
-               bfNormalizeTitleStrReplace( "[[MediaWiki:Some_random_test_page]]" );
-       }
-}
-
-$maintClass = BenchStrtrStrReplace::class;
-require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/maintenance/benchmarks/benchmarkStringReplacement.php b/maintenance/benchmarks/benchmarkStringReplacement.php
new file mode 100644 (file)
index 0000000..6db024c
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Benchmark
+ */
+
+require_once __DIR__ . '/Benchmarker.php';
+
+/**
+ * Maintenance script that benchmarks string replacement methods.
+ *
+ * @ingroup Benchmark
+ */
+class BenchmarkStringReplacement extends Benchmarker {
+       protected $defaultCount = 10000;
+       private $input = 'MediaWiki:Some_random_test_page';
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Benchmark for string replacement methods.' );
+       }
+
+       public function execute() {
+               $this->bench( [
+                       'strtr' => [ $this, 'bench_strtr' ],
+                       'str_replace' => [ $this, 'bench_str_replace' ],
+               ] );
+       }
+
+       protected function bench_strtr() {
+               strtr( $this->input, "_", " " );
+       }
+
+       protected function bench_str_replace() {
+               str_replace( "_", " ", $this->input );
+       }
+}
+
+$maintClass = BenchmarkStringReplacement::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
index 6bbd86d..b942302 100644 (file)
  * http://www.gnu.org/copyleft/gpl.html
  *
  * @file
- * @ingroup Dump Maintenance
+ * @ingroup Dump
+ * @ingroup Maintenance
  */
 
-require_once __DIR__ . '/backup.inc';
+require_once __DIR__ . '/includes/BackupDumper.php';
 
 class DumpBackup extends BackupDumper {
        function __construct( $args = null ) {
index 05db622..512910c 100644 (file)
@@ -24,7 +24,7 @@
  * @ingroup Maintenance
  */
 
-require_once __DIR__ . '/backup.inc';
+require_once __DIR__ . '/includes/BackupDumper.php';
 require_once __DIR__ . '/7zip.inc';
 require_once __DIR__ . '/../includes/export/WikiExporter.php';
 
diff --git a/maintenance/includes/BackupDumper.php b/maintenance/includes/BackupDumper.php
new file mode 100644 (file)
index 0000000..e8993e4
--- /dev/null
@@ -0,0 +1,425 @@
+<?php
+/**
+ * Base classes for database-dumping maintenance scripts.
+ *
+ * Copyright © 2005 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * 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 Dump
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/../Maintenance.php';
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * @ingroup Dump
+ * @ingroup Maintenance
+ */
+abstract class BackupDumper extends Maintenance {
+       public $reporting = true;
+       public $pages = null; // all pages
+       public $skipHeader = false; // don't output <mediawiki> and <siteinfo>
+       public $skipFooter = false; // don't output </mediawiki>
+       public $startId = 0;
+       public $endId = 0;
+       public $revStartId = 0;
+       public $revEndId = 0;
+       public $dumpUploads = false;
+       public $dumpUploadFileContents = false;
+       public $orderRevs = false;
+
+       protected $reportingInterval = 100;
+       protected $pageCount = 0;
+       protected $revCount = 0;
+       protected $server = null; // use default
+       protected $sink = null; // Output filters
+       protected $lastTime = 0;
+       protected $pageCountLast = 0;
+       protected $revCountLast = 0;
+
+       protected $outputTypes = [];
+       protected $filterTypes = [];
+
+       protected $ID = 0;
+
+       /**
+        * The dependency-injected database to use.
+        *
+        * @var IDatabase|null
+        *
+        * @see self::setDB
+        */
+       protected $forcedDb = null;
+
+       /** @var LoadBalancer */
+       protected $lb;
+
+       // @todo Unused?
+       private $stubText = false; // include rev_text_id instead of text; for 2-pass dump
+
+       /**
+        * @param array|null $args For backward compatibility
+        */
+       function __construct( $args = null ) {
+               parent::__construct();
+               $this->stderr = fopen( "php://stderr", "wt" );
+
+               // Built-in output and filter plugins
+               $this->registerOutput( 'file', DumpFileOutput::class );
+               $this->registerOutput( 'gzip', DumpGZipOutput::class );
+               $this->registerOutput( 'bzip2', DumpBZip2Output::class );
+               $this->registerOutput( 'dbzip2', DumpDBZip2Output::class );
+               $this->registerOutput( '7zip', Dump7ZipOutput::class );
+
+               $this->registerFilter( 'latest', DumpLatestFilter::class );
+               $this->registerFilter( 'notalk', DumpNotalkFilter::class );
+               $this->registerFilter( 'namespace', DumpNamespaceFilter::class );
+
+               // These three can be specified multiple times
+               $this->addOption( 'plugin', 'Load a dump plugin class. Specify as <class>[:<file>].',
+                       false, true, false, true );
+               $this->addOption( 'output', 'Begin a filtered output stream; Specify as <type>:<file>. ' .
+                       '<type>s: file, gzip, bzip2, 7zip, dbzip2', false, true, false, true );
+               $this->addOption( 'filter', 'Add a filter on an output branch. Specify as ' .
+                       '<type>[:<options>]. <types>s: latest, notalk, namespace', false, true, false, true );
+               $this->addOption( 'report', 'Report position and speed after every n pages processed. ' .
+                       'Default: 100.', false, true );
+               $this->addOption( 'server', 'Force reading from MySQL server', false, true );
+               $this->addOption( '7ziplevel', '7zip compression level for all 7zip outputs. Used for ' .
+                       '-mx option to 7za command.', false, true );
+
+               if ( $args ) {
+                       // Args should be loaded and processed so that dump() can be called directly
+                       // instead of execute()
+                       $this->loadWithArgv( $args );
+                       $this->processOptions();
+               }
+       }
+
+       /**
+        * @param string $name
+        * @param string $class Name of output filter plugin class
+        */
+       function registerOutput( $name, $class ) {
+               $this->outputTypes[$name] = $class;
+       }
+
+       /**
+        * @param string $name
+        * @param string $class Name of filter plugin class
+        */
+       function registerFilter( $name, $class ) {
+               $this->filterTypes[$name] = $class;
+       }
+
+       /**
+        * Load a plugin and register it
+        *
+        * @param string $class Name of plugin class; must have a static 'register'
+        *   method that takes a BackupDumper as a parameter.
+        * @param string $file Full or relative path to the PHP file to load, or empty
+        */
+       function loadPlugin( $class, $file ) {
+               if ( $file != '' ) {
+                       require_once $file;
+               }
+               $register = [ $class, 'register' ];
+               $register( $this );
+       }
+
+       function execute() {
+               throw new MWException( 'execute() must be overridden in subclasses' );
+       }
+
+       /**
+        * Processes arguments and sets $this->$sink accordingly
+        */
+       function processOptions() {
+               $sink = null;
+               $sinks = [];
+
+               $options = $this->orderedOptions;
+               foreach ( $options as $arg ) {
+                       $opt = $arg[0];
+                       $param = $arg[1];
+
+                       switch ( $opt ) {
+                               case 'plugin':
+                                       $val = explode( ':', $param );
+
+                                       if ( count( $val ) === 1 ) {
+                                               $this->loadPlugin( $val[0], '' );
+                                       } elseif ( count( $val ) === 2 ) {
+                                               $this->loadPlugin( $val[0], $val[1] );
+                                       } else {
+                                               $this->fatalError( 'Invalid plugin parameter' );
+                                               return;
+                                       }
+
+                                       break;
+                               case 'output':
+                                       $split = explode( ':', $param, 2 );
+                                       if ( count( $split ) !== 2 ) {
+                                               $this->fatalError( 'Invalid output parameter' );
+                                       }
+                                       list( $type, $file ) = $split;
+                                       if ( !is_null( $sink ) ) {
+                                               $sinks[] = $sink;
+                                       }
+                                       if ( !isset( $this->outputTypes[$type] ) ) {
+                                               $this->fatalError( "Unrecognized output sink type '$type'" );
+                                       }
+                                       $class = $this->outputTypes[$type];
+                                       if ( $type === "7zip" ) {
+                                               $sink = new $class( $file, intval( $this->getOption( '7ziplevel' ) ) );
+                                       } else {
+                                               $sink = new $class( $file );
+                                       }
+
+                                       break;
+                               case 'filter':
+                                       if ( is_null( $sink ) ) {
+                                               $sink = new DumpOutput();
+                                       }
+
+                                       $split = explode( ':', $param );
+                                       $key = $split[0];
+
+                                       if ( !isset( $this->filterTypes[$key] ) ) {
+                                               $this->fatalError( "Unrecognized filter type '$key'" );
+                                       }
+
+                                       $type = $this->filterTypes[$key];
+
+                                       if ( count( $split ) === 1 ) {
+                                               $filter = new $type( $sink );
+                                       } elseif ( count( $split ) === 2 ) {
+                                               $filter = new $type( $sink, $split[1] );
+                                       } else {
+                                               $this->fatalError( 'Invalid filter parameter' );
+                                       }
+
+                                       // references are lame in php...
+                                       unset( $sink );
+                                       $sink = $filter;
+
+                                       break;
+                       }
+               }
+
+               if ( $this->hasOption( 'report' ) ) {
+                       $this->reportingInterval = intval( $this->getOption( 'report' ) );
+               }
+
+               if ( $this->hasOption( 'server' ) ) {
+                       $this->server = $this->getOption( 'server' );
+               }
+
+               if ( is_null( $sink ) ) {
+                       $sink = new DumpOutput();
+               }
+               $sinks[] = $sink;
+
+               if ( count( $sinks ) > 1 ) {
+                       $this->sink = new DumpMultiWriter( $sinks );
+               } else {
+                       $this->sink = $sink;
+               }
+       }
+
+       function dump( $history, $text = WikiExporter::TEXT ) {
+               # Notice messages will foul up your XML output even if they're
+               # relatively harmless.
+               if ( ini_get( 'display_errors' ) ) {
+                       ini_set( 'display_errors', 'stderr' );
+               }
+
+               $this->initProgress( $history );
+
+               $db = $this->backupDb();
+               $exporter = new WikiExporter( $db, $history, WikiExporter::STREAM, $text );
+               $exporter->dumpUploads = $this->dumpUploads;
+               $exporter->dumpUploadFileContents = $this->dumpUploadFileContents;
+
+               $wrapper = new ExportProgressFilter( $this->sink, $this );
+               $exporter->setOutputSink( $wrapper );
+
+               if ( !$this->skipHeader ) {
+                       $exporter->openStream();
+               }
+               # Log item dumps: all or by range
+               if ( $history & WikiExporter::LOGS ) {
+                       if ( $this->startId || $this->endId ) {
+                               $exporter->logsByRange( $this->startId, $this->endId );
+                       } else {
+                               $exporter->allLogs();
+                       }
+               } elseif ( is_null( $this->pages ) ) {
+                       # Page dumps: all or by page ID range
+                       if ( $this->startId || $this->endId ) {
+                               $exporter->pagesByRange( $this->startId, $this->endId, $this->orderRevs );
+                       } elseif ( $this->revStartId || $this->revEndId ) {
+                               $exporter->revsByRange( $this->revStartId, $this->revEndId );
+                       } else {
+                               $exporter->allPages();
+                       }
+               } else {
+                       # Dump of specific pages
+                       $exporter->pagesByName( $this->pages );
+               }
+
+               if ( !$this->skipFooter ) {
+                       $exporter->closeStream();
+               }
+
+               $this->report( true );
+       }
+
+       /**
+        * Initialise starting time and maximum revision count.
+        * We'll make ETA calculations based an progress, assuming relatively
+        * constant per-revision rate.
+        * @param int $history WikiExporter::CURRENT or WikiExporter::FULL
+        */
+       function initProgress( $history = WikiExporter::FULL ) {
+               $table = ( $history == WikiExporter::CURRENT ) ? 'page' : 'revision';
+               $field = ( $history == WikiExporter::CURRENT ) ? 'page_id' : 'rev_id';
+
+               $dbr = $this->forcedDb;
+               if ( $this->forcedDb === null ) {
+                       $dbr = wfGetDB( DB_REPLICA );
+               }
+               $this->maxCount = $dbr->selectField( $table, "MAX($field)", '', __METHOD__ );
+               $this->startTime = microtime( true );
+               $this->lastTime = $this->startTime;
+               $this->ID = getmypid();
+       }
+
+       /**
+        * @todo Fixme: the --server parameter is currently not respected, as it
+        * doesn't seem terribly easy to ask the load balancer for a particular
+        * connection by name.
+        * @return IDatabase
+        */
+       function backupDb() {
+               if ( $this->forcedDb !== null ) {
+                       return $this->forcedDb;
+               }
+
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $this->lb = $lbFactory->newMainLB();
+               $db = $this->lb->getConnection( DB_REPLICA, 'dump' );
+
+               // Discourage the server from disconnecting us if it takes a long time
+               // to read out the big ol' batch query.
+               $db->setSessionOptions( [ 'connTimeout' => 3600 * 24 ] );
+
+               return $db;
+       }
+
+       /**
+        * Force the dump to use the provided database connection for database
+        * operations, wherever possible.
+        *
+        * @param IDatabase|null $db (Optional) the database connection to use. If null, resort to
+        *   use the globally provided ways to get database connections.
+        */
+       function setDB( IDatabase $db = null ) {
+               parent::setDB( $db );
+               $this->forcedDb = $db;
+       }
+
+       function __destruct() {
+               if ( isset( $this->lb ) ) {
+                       $this->lb->closeAll();
+               }
+       }
+
+       function backupServer() {
+               global $wgDBserver;
+
+               return $this->server
+                       ? $this->server
+                       : $wgDBserver;
+       }
+
+       function reportPage() {
+               $this->pageCount++;
+       }
+
+       function revCount() {
+               $this->revCount++;
+               $this->report();
+       }
+
+       function report( $final = false ) {
+               if ( $final xor ( $this->revCount % $this->reportingInterval == 0 ) ) {
+                       $this->showReport();
+               }
+       }
+
+       function showReport() {
+               if ( $this->reporting ) {
+                       $now = wfTimestamp( TS_DB );
+                       $nowts = microtime( true );
+                       $deltaAll = $nowts - $this->startTime;
+                       $deltaPart = $nowts - $this->lastTime;
+                       $this->pageCountPart = $this->pageCount - $this->pageCountLast;
+                       $this->revCountPart = $this->revCount - $this->revCountLast;
+
+                       if ( $deltaAll ) {
+                               $portion = $this->revCount / $this->maxCount;
+                               $eta = $this->startTime + $deltaAll / $portion;
+                               $etats = wfTimestamp( TS_DB, intval( $eta ) );
+                               $pageRate = $this->pageCount / $deltaAll;
+                               $revRate = $this->revCount / $deltaAll;
+                       } else {
+                               $pageRate = '-';
+                               $revRate = '-';
+                               $etats = '-';
+                       }
+                       if ( $deltaPart ) {
+                               $pageRatePart = $this->pageCountPart / $deltaPart;
+                               $revRatePart = $this->revCountPart / $deltaPart;
+                       } else {
+                               $pageRatePart = '-';
+                               $revRatePart = '-';
+                       }
+                       $this->progress( sprintf(
+                               "%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), "
+                                       . "%d revs (%0.1f|%0.1f/sec all|curr), ETA %s [max %d]",
+                               $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate,
+                               $pageRatePart, $this->revCount, $revRate, $revRatePart, $etats,
+                               $this->maxCount
+                       ) );
+                       $this->lastTime = $nowts;
+                       $this->revCountLast = $this->revCount;
+               }
+       }
+
+       function progress( $string ) {
+               if ( $this->reporting ) {
+                       fwrite( $this->stderr, $string . "\n" );
+               }
+       }
+}
diff --git a/maintenance/resources/foreign-resources.yaml b/maintenance/resources/foreign-resources.yaml
new file mode 100644 (file)
index 0000000..b8d9848
--- /dev/null
@@ -0,0 +1,50 @@
+### Format of this file
+#
+# The top-level keys are module names (as registered in Resources.php).
+# The values of these keys are resource descriptors.
+#
+# In each resource descriptor object, the `src` and `integrity` keys are required.
+#
+# * `src`: Full URL to a remote resource.
+# * `integrity`: Cryptographic hash used to verify the remote content.
+#    Uses the "integrity metadata" format defined at <https://www.w3.org/TR/SRI/>.
+# * `dest`: An object mapping paths from the remote resource to a destination in
+#    `/resources/lib/$module/`. The value may be omitted to indicate that
+#    paths should be extracted to the destination directory itself.
+oojs:
+  src: https://registry.npmjs.org/oojs/-/oojs-2.2.2.tgz
+  integrity: sha256-ebgQW2EGrSkBCnDJBGqDpsBDjA3PMN/M8U5DyLHt9mw=
+  dest:
+    package/dist/oojs.jquery.js:
+    package/AUTHORS.txt:
+    package/LICENSE-MIT:
+    package/README.md:
+oojs-ui:
+  src: https://registry.npmjs.org/oojs-ui/-/oojs-ui-0.28.0.tgz
+  integrity: sha384-j8bzlCPrfS4sca+U9JO9tdcewDlLlDlOVOsLn+Vqlcg5GU59vLSd7TVm4FiuTowy
+  dest:
+    # Main stuff
+    package/dist/oojs-ui-core.js{,.map.json}:
+    package/dist/oojs-ui-core-{wikimediaui,apex}.css:
+    package/dist/oojs-ui-widgets.js{,.map.json}:
+    package/dist/oojs-ui-widgets-{wikimediaui,apex}.css:
+    package/dist/oojs-ui-toolbars.js{,.map.json}:
+    package/dist/oojs-ui-toolbars-{wikimediaui,apex}.css:
+    package/dist/oojs-ui-windows.js{,.map.json}:
+    package/dist/oojs-ui-windows-{wikimediaui,apex}.css:
+    package/dist/oojs-ui-{wikimediaui,apex}.js{,.map.json}:
+    package/dist/i18n:
+    package/dist/images:
+    # WikimediaUI theme
+    package/dist/themes/wikimediaui/images/icons/*.{svg,png}: themes/wikimediaui/images/icons
+    package/dist/themes/wikimediaui/images/indicators/*.{svg,png}: themes/wikimediaui/images/indicators
+    package/dist/themes/wikimediaui/images/textures/*.{gif,svg}: themes/wikimediaui/images/textures
+    package/src/themes/wikimediaui/*.json: themes/wikimediaui
+    package/dist/wikimedia-ui-base.less:
+    # Apex theme (icons, indicators, and textures)
+    package/src/themes/apex/*.json: themes/apex
+    # Misc stuff
+    package/dist/AUTHORS.txt:
+    package/dist/History.md:
+    package/dist/LICENSE-MIT:
+    package/dist/README.md:
diff --git a/maintenance/resources/manageForeignResources.php b/maintenance/resources/manageForeignResources.php
new file mode 100644 (file)
index 0000000..528d6e7
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/../Maintenance.php';
+
+/**
+ * Manage foreign resources registered with ResourceLoader.
+ *
+ * @ingroup Maintenance
+ * @since 1.32
+ */
+class ManageForeignResources extends Maintenance {
+       private $defaultAlgo = 'sha384';
+       private $tmpParentDir;
+
+       public function __construct() {
+               global $IP;
+               parent::__construct();
+               $this->addDescription( <<<TEXT
+Manage foreign resources registered with ResourceLoader.
+
+This helps developers to download, verify and update local copies of upstream
+libraries registered as ResourceLoader modules. See also foreign-resources.yaml.
+
+For sources that don't publish an integrity hash, leave the value empty at
+first, and run this script with --make-sri to compute the hashes.
+
+This script runs in dry mode by default. Use --update to actually change, remove,
+or add files to /resources/lib/.
+TEXT
+               );
+               $this->addArg( 'module', 'Name of a single module (Default: all)', false );
+               $this->addOption( 'update', ' resources/lib/ missing integrity metadata' );
+               $this->addOption( 'make-sri', 'Compute missing integrity metadata' );
+               $this->addOption( 'verbose', 'Be verbose' );
+
+               // Use a directory in $IP instead of wfTempDir() because
+               // PHP's rename() does not work across file systems.
+               $this->tmpParentDir = "{$IP}/resources/tmp";
+       }
+
+       public function execute() {
+               global $IP;
+               $module = $this->getArg();
+               $makeSRI = $this->hasOption( 'make-sri' );
+
+               $registry = $this->parseBasicYaml(
+                       file_get_contents( __DIR__ . '/foreign-resources.yaml' )
+               );
+               foreach ( $registry as $moduleName => $info ) {
+                       if ( $module !== null && $moduleName !== $module ) {
+                               continue;
+                       }
+                       $this->verbose( "\n### {$moduleName}\n\n" );
+
+                       // Validate required keys
+                       $info += [ 'src' => null, 'integrity' => null, 'dest' => null ];
+                       if ( $info['src'] === null ) {
+                               $this->fatalError( "Module '$moduleName' must have a 'src' key." );
+                       }
+                       $integrity = is_string( $info['integrity'] ) ? $info['integrity'] : $makeSRI;
+                       if ( $integrity === false ) {
+                               $this->fatalError( "Module '$moduleName' must have an 'integrity' key." );
+                       }
+
+                       // Download the resource
+                       $data = Http::get( $info['src'], [ 'followRedirects' => false ] );
+                       if ( $data === false ) {
+                               $this->fatalError( "Failed to download resource for '$moduleName'." );
+                       }
+
+                       // Validate integrity metadata
+                       $this->output( "... checking integrity of '{$moduleName}'\n" );
+                       $algo = $integrity === true ? $this->defaultAlgo : explode( '-', $integrity )[0];
+                       $actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) );
+                       if ( $integrity === true ) {
+                               $this->output( "Integrity for '{$moduleName}':\n\t${actualIntegrity}\n" );
+                               continue;
+                       } elseif ( $integrity !== $actualIntegrity ) {
+                               $this->fatalError( "Integrity check failed for '{$moduleName}:\n" .
+                                       "Expected: {$integrity}\n" .
+                                       "Actual: {$actualIntegrity}"
+                               );
+                       }
+
+                       // Determine destination
+                       $destDir = "{$IP}/resources/lib/$moduleName";
+                       $this->output( "... extracting files for '{$moduleName}'\n" );
+                       $this->handleTypeTar( $moduleName, $data, $destDir, $info );
+               }
+
+               // Clean up
+               wfRecursiveRemoveDir( $this->tmpParentDir );
+               $this->output( "\nDone!\n" );
+       }
+
+       private function handleTypeTar( $moduleName, $data, $destDir, array $info ) {
+               global $IP;
+               wfRecursiveRemoveDir( $this->tmpParentDir );
+               if ( !wfMkdirParents( $this->tmpParentDir ) ) {
+                       $this->fatalError( "Unable to create {$this->tmpParentDir}" );
+               }
+
+               // Write resource to temporary file and open it
+               $tmpFile = "{$this->tmpParentDir}/$moduleName.tar";
+               $this->verbose( "... writing '$moduleName' src to $tmpFile\n" );
+               file_put_contents( $tmpFile, $data );
+               $p = new PharData( $tmpFile );
+               $tmpDir = "{$this->tmpParentDir}/$moduleName";
+               $p->extractTo( $tmpDir );
+               unset( $data, $p );
+
+               if ( $info['dest'] === null ) {
+                       // Replace the entire directory as-is
+                       if ( !$this->hasOption( 'update' ) ) {
+                               $this->output( "[dry run] Would replace /resources/lib/$moduleName\n" );
+                       } else {
+                               wfRecursiveRemoveDir( $destDir );
+                               if ( !rename( $tmpDir, $destDir ) ) {
+                                       $this->fatalError( "Could not move $destDir to $tmpDir." );
+                               }
+                       }
+                       return;
+               }
+
+               // Create and/or empty the destination
+               if ( !$this->hasOption( 'update' ) ) {
+                       $this->output( "... [dry run] would empty /resources/lib/$moduleName\n" );
+               } else {
+                       wfRecursiveRemoveDir( $destDir );
+                       wfMkdirParents( $destDir );
+               }
+
+               // Expand and normalise the 'dest' entries
+               $toCopy = [];
+               foreach ( $info['dest'] as $fromSubPath => $toSubPath ) {
+                       // Use glob() to expand wildcards and check existence
+                       $fromPaths = glob( "{$tmpDir}/{$fromSubPath}", GLOB_BRACE );
+                       if ( !$fromPaths ) {
+                               $this->fatalError( "Path '$fromSubPath' of '$moduleName' not found." );
+                       }
+                       foreach ( $fromPaths as $fromPath ) {
+                               $toCopy[$fromPath] = $toSubPath === null
+                                       ? "$destDir/" . basename( $fromPath )
+                                       : "$destDir/$toSubPath/" . basename( $fromPath );
+                       }
+               }
+               foreach ( $toCopy as $from => $to ) {
+                       if ( !$this->hasOption( 'update' ) ) {
+                               $shortFrom = strtr( $from, [ "$tmpDir/" => '' ] );
+                               $shortTo = strtr( $to, [ "$IP/" => '' ] );
+                               $this->output( "... [dry run] would move $shortFrom to $shortTo\n" );
+                       } else {
+                               $this->verbose( "... moving $from to $to\n" );
+                               wfMkdirParents( dirname( $to ) );
+                               if ( !rename( $from, $to ) ) {
+                                       $this->fatalError( "Could not move $from to $to." );
+                               }
+                       }
+               }
+       }
+
+       private function verbose( $text ) {
+               if ( $this->hasOption( 'verbose' ) ) {
+                       $this->output( $text );
+               }
+       }
+
+       /**
+        * Basic YAML parser.
+        *
+        * Supports only string or object values, and 2 spaces indentation.
+        *
+        * @todo Just ship symfony/yaml.
+        * @param string $input
+        * @return array
+        */
+       private function parseBasicYaml( $input ) {
+               $lines = explode( "\n", $input );
+               $root = [];
+               $stack = [ &$root ];
+               $prev = 0;
+               foreach ( $lines as $i => $text ) {
+                       $line = $i + 1;
+                       $trimmed = ltrim( $text, ' ' );
+                       if ( $trimmed === '' || $trimmed[0] === '#' ) {
+                               continue;
+                       }
+                       $indent = strlen( $text ) - strlen( $trimmed );
+                       if ( $indent % 2 !== 0 ) {
+                               throw new Exception( __METHOD__ . ": Odd indentation on line $line." );
+                       }
+                       $depth = $indent === 0 ? 0 : ( $indent / 2 );
+                       if ( $depth < $prev ) {
+                               // Close previous branches we can't re-enter
+                               array_splice( $stack, $depth + 1 );
+                       }
+                       if ( !array_key_exists( $depth, $stack ) ) {
+                               throw new Exception( __METHOD__ . ": Too much indentation on line $line." );
+                       }
+                       if ( strpos( $trimmed, ':' ) === false ) {
+                               throw new Exception( __METHOD__ . ": Missing colon on line $line." );
+                       }
+                       $dest =& $stack[ $depth ];
+                       if ( $dest === null ) {
+                               // Promote from null to object
+                               $dest = [];
+                       }
+                       list( $key, $val ) = explode( ':', $trimmed, 2 );
+                       $val = ltrim( $val, ' ' );
+                       if ( $val !== '' ) {
+                               // Add string
+                               $dest[ $key ] = $val;
+                       } else {
+                               // Add null (may become an object later)
+                               $val = null;
+                               $stack[] = &$val;
+                               $dest[ $key ] = &$val;
+                       }
+                       $prev = $depth;
+                       unset( $dest, $val );
+               }
+               return $root;
+       }
+}
+
+$maintClass = ManageForeignResources::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/maintenance/resources/update-oojs.sh b/maintenance/resources/update-oojs.sh
deleted file mode 100755 (executable)
index fd7b860..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/bin/bash -eu
-
-# This script generates a commit that updates our copy of OOjs
-
-if [ -n "${2:-}" ]
-then
-       # Too many parameters
-       echo >&2 "Usage: $0 [<version>]"
-       exit 1
-fi
-
-REPO_DIR=$(cd "$(dirname $0)/../.."; pwd) # Root dir of the git repo working tree
-TARGET_DIR="resources/lib/oojs" # Destination relative to the root of the repo
-NPM_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'update-oojs') # e.g. /tmp/update-oojs.rI0I5Vir
-
-# Prepare working tree
-cd "$REPO_DIR"
-git reset -- $TARGET_DIR
-git checkout -- $TARGET_DIR
-git fetch origin
-git checkout -B upstream-oojs origin/master
-
-# Fetch upstream version
-cd $NPM_DIR
-if [ -n "${1:-}" ]
-then
-       npm install "oojs@$1"
-else
-       npm install oojs
-fi
-
-OOJS_VERSION=$(node -e 'console.log(require("./node_modules/oojs/package.json").version);')
-if [ "$OOJS_VERSION" == "" ]
-then
-       echo 'Could not find OOjs version'
-       exit 1
-fi
-
-# Copy file(s)
-rsync --force ./node_modules/oojs/dist/oojs.jquery.js "$REPO_DIR/$TARGET_DIR"
-rsync --force ./node_modules/oojs/AUTHORS.txt "$REPO_DIR/$TARGET_DIR"
-rsync --force ./node_modules/oojs/LICENSE-MIT "$REPO_DIR/$TARGET_DIR"
-rsync --force ./node_modules/oojs/README.md "$REPO_DIR/$TARGET_DIR"
-
-# Clean up temporary area
-rm -rf "$NPM_DIR"
-
-# Generate commit
-cd $REPO_DIR
-
-COMMITMSG=$(cat <<END
-Update OOjs to v$OOJS_VERSION
-
-Release notes:
- https://gerrit.wikimedia.org/r/plugins/gitiles/oojs/core/+/v$OOJS_VERSION/History.md
-END
-)
-
-# Stage deletion, modification and creation of files. Then commit.
-git add --update $TARGET_DIR
-git add $TARGET_DIR
-git commit -m "$COMMITMSG"
diff --git a/maintenance/resources/update-ooui.sh b/maintenance/resources/update-ooui.sh
deleted file mode 100755 (executable)
index 889ab42..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/bin/bash -eu
-
-# This script generates a commit that updates our copy of OOUI
-
-if [ -n "${2:-}" ]
-then
-       # Too many parameters
-       echo >&2 "Usage: $0 [<version>]"
-       exit 1
-fi
-
-REPO_DIR=$(cd "$(dirname $0)/../.."; pwd) # Root dir of the git repo working tree
-TARGET_DIR="resources/lib/oojs-ui" # Destination relative to the root of the repo
-NPM_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'update-ooui') # e.g. /tmp/update-ooui.rI0I5Vir
-
-# Prepare working tree
-cd "$REPO_DIR"
-git reset composer.json
-git checkout composer.json
-git reset -- $TARGET_DIR
-git checkout -- $TARGET_DIR
-git fetch origin
-git checkout -B upstream-ooui origin/master
-
-# Fetch upstream version
-cd $NPM_DIR
-if [ -n "${1:-}" ]
-then
-       npm install "oojs-ui@$1"
-else
-       npm install oojs-ui
-fi
-
-OOUI_VERSION=$(node -e 'console.log(require("./node_modules/oojs-ui/package.json").version);')
-if [ "$OOUI_VERSION" == "" ]
-then
-       echo 'Could not find OOUI version'
-       exit 1
-fi
-
-# Copy files, picking the necessary ones from source and distribution
-rm -r "$REPO_DIR/$TARGET_DIR"
-
-# Core and thematic code and styling
-mkdir -p "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-core.js{,.map.json} "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-core-{wikimediaui,apex}.css "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-widgets.js{,.map.json} "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-widgets-{wikimediaui,apex}.css "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-toolbars.js{,.map.json} "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-toolbars-{wikimediaui,apex}.css "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-windows.js{,.map.json} "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-windows-{wikimediaui,apex}.css "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/oojs-ui-{wikimediaui,apex}.js{,.map.json} "$REPO_DIR/$TARGET_DIR"
-
-# i18n
-mkdir -p "$REPO_DIR/$TARGET_DIR/i18n"
-cp -R ./node_modules/oojs-ui/dist/i18n "$REPO_DIR/$TARGET_DIR"
-
-# Core images (currently two .cur files)
-mkdir -p "$REPO_DIR/$TARGET_DIR/images"
-cp -R ./node_modules/oojs-ui/dist/images "$REPO_DIR/$TARGET_DIR"
-
-# WikimediaUI theme icons, indicators, and textures
-mkdir -p "$REPO_DIR/$TARGET_DIR/themes/wikimediaui/images/icons"
-cp ./node_modules/oojs-ui/dist/themes/wikimediaui/images/icons/*.{svg,png} "$REPO_DIR/$TARGET_DIR/themes/wikimediaui/images/icons"
-mkdir -p "$REPO_DIR/$TARGET_DIR/themes/wikimediaui/images/indicators"
-cp ./node_modules/oojs-ui/dist/themes/wikimediaui/images/indicators/*.{svg,png} "$REPO_DIR/$TARGET_DIR/themes/wikimediaui/images/indicators"
-mkdir -p "$REPO_DIR/$TARGET_DIR/themes/wikimediaui/images/textures"
-cp ./node_modules/oojs-ui/dist/themes/wikimediaui/images/textures/*.{gif,svg} "$REPO_DIR/$TARGET_DIR/themes/wikimediaui/images/textures"
-
-cp ./node_modules/oojs-ui/src/themes/wikimediaui/*.json "$REPO_DIR/$TARGET_DIR/themes/wikimediaui"
-
-# Apex theme icons, indicators, and textures
-mkdir -p "$REPO_DIR/$TARGET_DIR/themes/apex"
-cp ./node_modules/oojs-ui/src/themes/apex/*.json "$REPO_DIR/$TARGET_DIR/themes/apex"
-
-# WikimediaUI LESS variables for sharing
-cp ./node_modules/oojs-ui/dist/wikimedia-ui-base.less "$REPO_DIR/$TARGET_DIR"
-
-# Misc stuff
-cp ./node_modules/oojs-ui/dist/AUTHORS.txt "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/History.md "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/LICENSE-MIT "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/README.md "$REPO_DIR/$TARGET_DIR"
-
-# Clean up temporary area
-rm -rf "$NPM_DIR"
-
-# Generate commit
-cd $REPO_DIR
-
-COMMITMSG=$(cat <<END
-Update OOUI to v$OOUI_VERSION
-
-Release notes:
- https://phabricator.wikimedia.org/diffusion/GOJU/browse/master/History.md;v$OOUI_VERSION
-END
-)
-
-# Update composer.json as well
-composer require oojs/oojs-ui $OOUI_VERSION --no-update
-
-# Stage deletion, modification and creation of files. Then commit.
-git add --update $TARGET_DIR
-git add $TARGET_DIR
-git add composer.json
-git commit -m "$COMMITMSG"
index 96a76df..c40ef93 100644 (file)
@@ -174,7 +174,7 @@ return [
                'targets' => [ 'mobile', 'desktop' ],
        ],
        'jquery.async' => [
-               'scripts' => 'resources/lib/jquery/jquery.async.js',
+               'scripts' => 'resources/lib/jquery.async.js',
        ],
        'jquery.byteLength' => [
                'scripts' => 'resources/src/jquery/jquery.byteLength.js',
@@ -221,14 +221,14 @@ return [
                'dependencies' => 'mediawiki.jqueryMsg',
        ],
        'jquery.cookie' => [
-               'scripts' => 'resources/lib/jquery/jquery.cookie.js',
+               'scripts' => 'resources/lib/jquery.cookie.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'jquery.form' => [
-               'scripts' => 'resources/lib/jquery/jquery.form.js',
+               'scripts' => 'resources/lib/jquery.form.js',
        ],
        'jquery.fullscreen' => [
-               'scripts' => 'resources/lib/jquery/jquery.fullscreen.js',
+               'scripts' => 'resources/lib/jquery.fullscreen.js',
        ],
        'jquery.getAttrs' => [
                'targets' => [ 'desktop', 'mobile' ],
@@ -248,7 +248,7 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'jquery.hoverIntent' => [
-               'scripts' => 'resources/lib/jquery/jquery.hoverIntent.js',
+               'scripts' => 'resources/lib/jquery.hoverIntent.js',
        ],
        'jquery.i18n' => [
                'scripts' => [
@@ -295,7 +295,7 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'jquery.mockjax' => [
-               'scripts' => 'resources/lib/jquery/jquery.mockjax.js',
+               'scripts' => 'resources/lib/jquery.mockjax.js',
        ],
        'jquery.mw-jump' => [
                'scripts' => 'resources/src/jquery/jquery.mw-jump.js',
@@ -313,7 +313,7 @@ return [
        ],
        'jquery.jStorage' => [
                'deprecated' => 'Please use "mediawiki.storage" instead.',
-               'scripts' => 'resources/lib/jquery/jquery.jStorage.js',
+               'scripts' => 'resources/lib/jquery.jStorage.js',
        ],
        'jquery.suggestions' => [
                'targets' => [ 'desktop', 'mobile' ],
@@ -340,11 +340,11 @@ return [
                'targets' => [ 'mobile', 'desktop' ],
        ],
        'jquery.throttle-debounce' => [
-               'scripts' => 'resources/lib/jquery/jquery.ba-throttle-debounce.js',
+               'scripts' => 'resources/lib/jquery.ba-throttle-debounce.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'jquery.xmldom' => [
-               'scripts' => 'resources/lib/jquery/jquery.xmldom.js',
+               'scripts' => 'resources/lib/jquery.xmldom.js',
        ],
 
        /* jQuery Tipsy */
@@ -2629,6 +2629,15 @@ return [
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'mediawiki.widgets.CheckMatrixWidget' => [
+               'scripts' => [
+                       'resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js',
+               ],
+               'dependencies' => [
+                       'oojs-ui-core',
+               ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
        'mediawiki.widgets.CategoryMultiselectWidget' => [
                'scripts' => [
                        'resources/src/mediawiki.widgets/mw.widgets.CategoryTagItemWidget.js',
diff --git a/resources/lib/jquery.async.js b/resources/lib/jquery.async.js
new file mode 100644 (file)
index 0000000..2161f6b
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * jQuery Asynchronous Plugin 1.0
+ *
+ * Copyright (c) 2008 Vincent Robert (genezys.net)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ */
+(function($){
+
+// opts.delay : (default 10) delay between async call in ms
+// opts.bulk : (default 500) delay during which the loop can continue synchronously without yielding the CPU
+// opts.test : (default true) function to test in the while test part
+// opts.loop : (default empty) function to call in the while loop part
+// opts.end : (default empty) function to call at the end of the while loop
+$.whileAsync = function(opts) {
+       var delay = Math.abs(opts.delay) || 10,
+               bulk = isNaN(opts.bulk) ? 500 : Math.abs(opts.bulk),
+               test = opts.test || function(){ return true; },
+               loop = opts.loop || function(){},
+               end = opts.end || function(){};
+       
+       (function(){
+
+               var t = false,
+                       begin = new Date();
+                       
+               while( t = test() ) {
+                       loop();
+                       if( bulk === 0 || (new Date() - begin) > bulk ) {
+                               break;
+                       }
+               }
+               if( t ) {
+                       setTimeout(arguments.callee, delay);
+               }
+               else {
+                       end();
+               }
+               
+       })();
+};
+
+// opts.delay : (default 10) delay between async call in ms
+// opts.bulk : (default 500) delay during which the loop can continue synchronously without yielding the CPU
+// opts.loop : (default empty) function to call in the each loop part, signature: function(index, value) this = value
+// opts.end : (default empty) function to call at the end of the each loop
+$.eachAsync = function(array, opts) {
+       var     i = 0,
+               l = array.length,
+               loop = opts.loop || function(){};
+       
+       $.whileAsync(
+               $.extend(opts, {
+                       test: function() { return i < l; },
+                       loop: function() {
+                               var val = array[i];
+                               return loop.call(val, i++, val);
+                       }
+               })
+       );
+};
+
+$.fn.eachAsync = function(opts) {
+       $.eachAsync(this, opts);
+       return this;
+}
+
+})(jQuery);
\ No newline at end of file
diff --git a/resources/lib/jquery.ba-throttle-debounce.js b/resources/lib/jquery.ba-throttle-debounce.js
new file mode 100644 (file)
index 0000000..fa30bdf
--- /dev/null
@@ -0,0 +1,252 @@
+/*!
+ * jQuery throttle / debounce - v1.1 - 3/7/2010
+ * http://benalman.com/projects/jquery-throttle-debounce-plugin/
+ * 
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+
+// Script: jQuery throttle / debounce: Sometimes, less is more!
+//
+// *Version: 1.1, Last updated: 3/7/2010*
+// 
+// Project Home - http://benalman.com/projects/jquery-throttle-debounce-plugin/
+// GitHub       - http://github.com/cowboy/jquery-throttle-debounce/
+// Source       - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.js
+// (Minified)   - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.min.js (0.7kb)
+// 
+// About: License
+// 
+// Copyright (c) 2010 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+// 
+// About: Examples
+// 
+// These working examples, complete with fully commented code, illustrate a few
+// ways in which this plugin can be used.
+// 
+// Throttle - http://benalman.com/code/projects/jquery-throttle-debounce/examples/throttle/
+// Debounce - http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
+// 
+// About: Support and Testing
+// 
+// Information about what version or versions of jQuery this plugin has been
+// tested with, what browsers it has been tested in, and where the unit tests
+// reside (so you can test it yourself).
+// 
+// jQuery Versions - none, 1.3.2, 1.4.2
+// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome 4-5, Opera 9.6-10.1.
+// Unit Tests      - http://benalman.com/code/projects/jquery-throttle-debounce/unit/
+// 
+// About: Release History
+// 
+// 1.1 - (3/7/2010) Fixed a bug in <jQuery.throttle> where trailing callbacks
+//       executed later than they should. Reworked a fair amount of internal
+//       logic as well.
+// 1.0 - (3/6/2010) Initial release as a stand-alone project. Migrated over
+//       from jquery-misc repo v0.4 to jquery-throttle repo v1.0, added the
+//       no_trailing throttle parameter and debounce functionality.
+// 
+// Topic: Note for non-jQuery users
+// 
+// jQuery isn't actually required for this plugin, because nothing internal
+// uses any jQuery methods or properties. jQuery is just used as a namespace
+// under which these methods can exist.
+// 
+// Since jQuery isn't actually required for this plugin, if jQuery doesn't exist
+// when this plugin is loaded, the method described below will be created in
+// the `Cowboy` namespace. Usage will be exactly the same, but instead of
+// $.method() or jQuery.method(), you'll need to use Cowboy.method().
+
+(function(window,undefined){
+  '$:nomunge'; // Used by YUI compressor.
+  
+  // Since jQuery really isn't required for this plugin, use `jQuery` as the
+  // namespace only if it already exists, otherwise use the `Cowboy` namespace,
+  // creating it if necessary.
+  var $ = window.jQuery || window.Cowboy || ( window.Cowboy = {} ),
+    
+    // Internal method reference.
+    jq_throttle;
+  
+  // Method: jQuery.throttle
+  // 
+  // Throttle execution of a function. Especially useful for rate limiting
+  // execution of handlers on events like resize and scroll. If you want to
+  // rate-limit execution of a function to a single time, see the
+  // <jQuery.debounce> method.
+  // 
+  // In this visualization, | is a throttled-function call and X is the actual
+  // callback execution:
+  // 
+  // > Throttled with `no_trailing` specified as false or unspecified:
+  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+  // > X    X    X    X    X    X        X    X    X    X    X    X
+  // > 
+  // > Throttled with `no_trailing` specified as true:
+  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+  // > X    X    X    X    X             X    X    X    X    X
+  // 
+  // Usage:
+  // 
+  // > var throttled = jQuery.throttle( delay, [ no_trailing, ] callback );
+  // > 
+  // > jQuery('selector').bind( 'someevent', throttled );
+  // > jQuery('selector').unbind( 'someevent', throttled );
+  // 
+  // This also works in jQuery 1.4+:
+  // 
+  // > jQuery('selector').bind( 'someevent', jQuery.throttle( delay, [ no_trailing, ] callback ) );
+  // > jQuery('selector').unbind( 'someevent', callback );
+  // 
+  // Arguments:
+  // 
+  //  delay - (Number) A zero-or-greater delay in milliseconds. For event
+  //    callbacks, values around 100 or 250 (or even higher) are most useful.
+  //  no_trailing - (Boolean) Optional, defaults to false. If no_trailing is
+  //    true, callback will only execute every `delay` milliseconds while the
+  //    throttled-function is being called. If no_trailing is false or
+  //    unspecified, callback will be executed one final time after the last
+  //    throttled-function call. (After the throttled-function has not been
+  //    called for `delay` milliseconds, the internal counter is reset)
+  //  callback - (Function) A function to be executed after delay milliseconds.
+  //    The `this` context and all arguments are passed through, as-is, to
+  //    `callback` when the throttled-function is executed.
+  // 
+  // Returns:
+  // 
+  //  (Function) A new, throttled, function.
+  
+  $.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) {
+    // After wrapper has stopped being called, this timeout ensures that
+    // `callback` is executed at the proper times in `throttle` and `end`
+    // debounce modes.
+    var timeout_id,
+      
+      // Keep track of the last time `callback` was executed.
+      last_exec = 0;
+    
+    // `no_trailing` defaults to falsy.
+    if ( typeof no_trailing !== 'boolean' ) {
+      debounce_mode = callback;
+      callback = no_trailing;
+      no_trailing = undefined;
+    }
+    
+    // The `wrapper` function encapsulates all of the throttling / debouncing
+    // functionality and when executed will limit the rate at which `callback`
+    // is executed.
+    function wrapper() {
+      var that = this,
+        elapsed = +new Date() - last_exec,
+        args = arguments;
+      
+      // Execute `callback` and update the `last_exec` timestamp.
+      function exec() {
+        last_exec = +new Date();
+        callback.apply( that, args );
+      };
+      
+      // If `debounce_mode` is true (at_begin) this is used to clear the flag
+      // to allow future `callback` executions.
+      function clear() {
+        timeout_id = undefined;
+      };
+      
+      if ( debounce_mode && !timeout_id ) {
+        // Since `wrapper` is being called for the first time and
+        // `debounce_mode` is true (at_begin), execute `callback`.
+        exec();
+      }
+      
+      // Clear any existing timeout.
+      timeout_id && clearTimeout( timeout_id );
+      
+      if ( debounce_mode === undefined && elapsed > delay ) {
+        // In throttle mode, if `delay` time has been exceeded, execute
+        // `callback`.
+        exec();
+        
+      } else if ( no_trailing !== true ) {
+        // In trailing throttle mode, since `delay` time has not been
+        // exceeded, schedule `callback` to execute `delay` ms after most
+        // recent execution.
+        // 
+        // If `debounce_mode` is true (at_begin), schedule `clear` to execute
+        // after `delay` ms.
+        // 
+        // If `debounce_mode` is false (at end), schedule `callback` to
+        // execute after `delay` ms.
+        timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay );
+      }
+    };
+    
+    // Set the guid of `wrapper` function to the same of original callback, so
+    // it can be removed in jQuery 1.4+ .unbind or .die by using the original
+    // callback as a reference.
+    if ( $.guid ) {
+      wrapper.guid = callback.guid = callback.guid || $.guid++;
+    }
+    
+    // Return the wrapper function.
+    return wrapper;
+  };
+  
+  // Method: jQuery.debounce
+  // 
+  // Debounce execution of a function. Debouncing, unlike throttling,
+  // guarantees that a function is only executed a single time, either at the
+  // very beginning of a series of calls, or at the very end. If you want to
+  // simply rate-limit execution of a function, see the <jQuery.throttle>
+  // method.
+  // 
+  // In this visualization, | is a debounced-function call and X is the actual
+  // callback execution:
+  // 
+  // > Debounced with `at_begin` specified as false or unspecified:
+  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+  // >                          X                                 X
+  // > 
+  // > Debounced with `at_begin` specified as true:
+  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
+  // > X                                 X
+  // 
+  // Usage:
+  // 
+  // > var debounced = jQuery.debounce( delay, [ at_begin, ] callback );
+  // > 
+  // > jQuery('selector').bind( 'someevent', debounced );
+  // > jQuery('selector').unbind( 'someevent', debounced );
+  // 
+  // This also works in jQuery 1.4+:
+  // 
+  // > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) );
+  // > jQuery('selector').unbind( 'someevent', callback );
+  // 
+  // Arguments:
+  // 
+  //  delay - (Number) A zero-or-greater delay in milliseconds. For event
+  //    callbacks, values around 100 or 250 (or even higher) are most useful.
+  //  at_begin - (Boolean) Optional, defaults to false. If at_begin is false or
+  //    unspecified, callback will only be executed `delay` milliseconds after
+  //    the last debounced-function call. If at_begin is true, callback will be
+  //    executed only at the first debounced-function call. (After the
+  //    throttled-function has not been called for `delay` milliseconds, the
+  //    internal counter is reset)
+  //  callback - (Function) A function to be executed after delay milliseconds.
+  //    The `this` context and all arguments are passed through, as-is, to
+  //    `callback` when the debounced-function is executed.
+  // 
+  // Returns:
+  // 
+  //  (Function) A new, debounced, function.
+  
+  $.debounce = function( delay, at_begin, callback ) {
+    return callback === undefined
+      ? jq_throttle( delay, at_begin, false )
+      : jq_throttle( delay, callback, at_begin !== false );
+  };
+  
+})(this);
diff --git a/resources/lib/jquery.cookie.js b/resources/lib/jquery.cookie.js
new file mode 100644 (file)
index 0000000..3fb201c
--- /dev/null
@@ -0,0 +1,90 @@
+/*!
+ * jQuery Cookie Plugin v1.3.1
+ * https://github.com/carhartl/jquery-cookie
+ *
+ * Copyright 2013 Klaus Hartl
+ * Released under the MIT license
+ */
+(function ($, document, undefined) {
+
+       var pluses = /\+/g;
+
+       function raw(s) {
+               return s;
+       }
+
+       function decoded(s) {
+               return unRfc2068(decodeURIComponent(s.replace(pluses, ' ')));
+       }
+
+       function unRfc2068(value) {
+               if (value.indexOf('"') === 0) {
+                       // This is a quoted cookie as according to RFC2068, unescape
+                       value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
+               }
+               return value;
+       }
+
+       function fromJSON(value) {
+               return config.json ? JSON.parse(value) : value;
+       }
+
+       var config = $.cookie = function (key, value, options) {
+
+               // write
+               if (value !== undefined) {
+                       options = $.extend({}, config.defaults, options);
+
+                       if (value === null) {
+                               options.expires = -1;
+                       }
+
+                       if (typeof options.expires === 'number') {
+                               var days = options.expires, t = options.expires = new Date();
+                               t.setDate(t.getDate() + days);
+                       }
+
+                       value = config.json ? JSON.stringify(value) : String(value);
+
+                       return (document.cookie = [
+                               encodeURIComponent(key), '=', config.raw ? value : encodeURIComponent(value),
+                               options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+                               options.path    ? '; path=' + options.path : '',
+                               options.domain  ? '; domain=' + options.domain : '',
+                               options.secure  ? '; secure' : ''
+                       ].join(''));
+               }
+
+               // read
+               var decode = config.raw ? raw : decoded;
+               var cookies = document.cookie.split('; ');
+               var result = key ? null : {};
+               for (var i = 0, l = cookies.length; i < l; i++) {
+                       var parts = cookies[i].split('=');
+                       var name = decode(parts.shift());
+                       var cookie = decode(parts.join('='));
+
+                       if (key && key === name) {
+                               result = fromJSON(cookie);
+                               break;
+                       }
+
+                       if (!key) {
+                               result[name] = fromJSON(cookie);
+                       }
+               }
+
+               return result;
+       };
+
+       config.defaults = {};
+
+       $.removeCookie = function (key, options) {
+               if ($.cookie(key) !== null) {
+                       $.cookie(key, null, options);
+                       return true;
+               }
+               return false;
+       };
+
+})(jQuery, document);
diff --git a/resources/lib/jquery.form.js b/resources/lib/jquery.form.js
new file mode 100644 (file)
index 0000000..13e9a55
--- /dev/null
@@ -0,0 +1,1089 @@
+/*!
+ * jQuery Form Plugin
+ * version: 3.14 (30-JUL-2012)
+ * @requires jQuery v1.3.2 or later
+ *
+ * Examples and documentation at: http://malsup.com/jquery/form/
+ * Project repository: https://github.com/malsup/form
+ * Dual licensed under the MIT and GPL licenses:
+ *    http://malsup.github.com/mit-license.txt
+ *    http://malsup.github.com/gpl-license-v2.txt
+ */
+/*global ActiveXObject alert */
+;(function($) {
+"use strict";
+
+/*
+    Usage Note:
+    -----------
+    Do not use both ajaxSubmit and ajaxForm on the same form.  These
+    functions are mutually exclusive.  Use ajaxSubmit if you want
+    to bind your own submit handler to the form.  For example,
+
+    $(document).ready(function() {
+        $('#myForm').on('submit', function(e) {
+            e.preventDefault(); // <-- important
+            $(this).ajaxSubmit({
+                target: '#output'
+            });
+        });
+    });
+
+    Use ajaxForm when you want the plugin to manage all the event binding
+    for you.  For example,
+
+    $(document).ready(function() {
+        $('#myForm').ajaxForm({
+            target: '#output'
+        });
+    });
+    
+    You can also use ajaxForm with delegation (requires jQuery v1.7+), so the
+    form does not have to exist when you invoke ajaxForm:
+
+    $('#myForm').ajaxForm({
+        delegation: true,
+        target: '#output'
+    });
+    
+    When using ajaxForm, the ajaxSubmit function will be invoked for you
+    at the appropriate time.
+*/
+
+/**
+ * Feature detection
+ */
+var feature = {};
+feature.fileapi = $("<input type='file'/>").get(0).files !== undefined;
+feature.formdata = window.FormData !== undefined;
+
+/**
+ * ajaxSubmit() provides a mechanism for immediately submitting
+ * an HTML form using AJAX.
+ */
+$.fn.ajaxSubmit = function(options) {
+    /*jshint scripturl:true */
+
+    // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
+    if (!this.length) {
+        log('ajaxSubmit: skipping submit process - no element selected');
+        return this;
+    }
+    
+    var method, action, url, $form = this;
+
+    if (typeof options == 'function') {
+        options = { success: options };
+    }
+
+    method = this.attr('method');
+    action = this.attr('action');
+    url = (typeof action === 'string') ? $.trim(action) : '';
+    url = url || window.location.href || '';
+    if (url) {
+        // clean url (don't include hash vaue)
+        url = (url.match(/^([^#]+)/)||[])[1];
+    }
+
+    options = $.extend(true, {
+        url:  url,
+        success: $.ajaxSettings.success,
+        type: method || 'GET',
+        iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
+    }, options);
+
+    // hook for manipulating the form data before it is extracted;
+    // convenient for use with rich editors like tinyMCE or FCKEditor
+    var veto = {};
+    this.trigger('form-pre-serialize', [this, options, veto]);
+    if (veto.veto) {
+        log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
+        return this;
+    }
+
+    // provide opportunity to alter form data before it is serialized
+    if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
+        log('ajaxSubmit: submit aborted via beforeSerialize callback');
+        return this;
+    }
+
+    var traditional = options.traditional;
+    if ( traditional === undefined ) {
+        traditional = $.ajaxSettings.traditional;
+    }
+    
+    var elements = [];
+    var qx, a = this.formToArray(options.semantic, elements);
+    if (options.data) {
+        options.extraData = options.data;
+        qx = $.param(options.data, traditional);
+    }
+
+    // give pre-submit callback an opportunity to abort the submit
+    if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
+        log('ajaxSubmit: submit aborted via beforeSubmit callback');
+        return this;
+    }
+
+    // fire vetoable 'validate' event
+    this.trigger('form-submit-validate', [a, this, options, veto]);
+    if (veto.veto) {
+        log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
+        return this;
+    }
+
+    var q = $.param(a, traditional);
+    if (qx) {
+        q = ( q ? (q + '&' + qx) : qx );
+    }    
+    if (options.type.toUpperCase() == 'GET') {
+        options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+        options.data = null;  // data is null for 'get'
+    }
+    else {
+        options.data = q; // data is the query string for 'post'
+    }
+
+    var callbacks = [];
+    if (options.resetForm) {
+        callbacks.push(function() { $form.resetForm(); });
+    }
+    if (options.clearForm) {
+        callbacks.push(function() { $form.clearForm(options.includeHidden); });
+    }
+
+    // perform a load on the target only if dataType is not provided
+    if (!options.dataType && options.target) {
+        var oldSuccess = options.success || function(){};
+        callbacks.push(function(data) {
+            var fn = options.replaceTarget ? 'replaceWith' : 'html';
+            $(options.target)[fn](data).each(oldSuccess, arguments);
+        });
+    }
+    else if (options.success) {
+        callbacks.push(options.success);
+    }
+
+    options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
+        var context = options.context || this ;    // jQuery 1.4+ supports scope context 
+        for (var i=0, max=callbacks.length; i < max; i++) {
+            callbacks[i].apply(context, [data, status, xhr || $form, $form]);
+        }
+    };
+
+    // are there files to upload?
+    var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113)
+    var hasFileInputs = fileInputs.length > 0;
+    var mp = 'multipart/form-data';
+    var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
+
+    var fileAPI = feature.fileapi && feature.formdata;
+    log("fileAPI :" + fileAPI);
+    var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
+
+    // options.iframe allows user to force iframe mode
+    // 06-NOV-09: now defaulting to iframe mode if file input is detected
+    if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
+        // hack to fix Safari hang (thanks to Tim Molendijk for this)
+        // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+        if (options.closeKeepAlive) {
+            $.get(options.closeKeepAlive, function() {
+                fileUploadIframe(a);
+            });
+        }
+          else {
+            fileUploadIframe(a);
+          }
+    }
+    else if ((hasFileInputs || multipart) && fileAPI) {
+        fileUploadXhr(a);
+    }
+    else {
+        $.ajax(options);
+    }
+
+    // clear element array
+    for (var k=0; k < elements.length; k++)
+        elements[k] = null;
+
+    // fire 'notify' event
+    this.trigger('form-submit-notify', [this, options]);
+    return this;
+
+     // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
+    function fileUploadXhr(a) {
+        var formdata = new FormData();
+
+        for (var i=0; i < a.length; i++) {
+            formdata.append(a[i].name, a[i].value);
+        }
+
+        if (options.extraData) {
+            for (var p in options.extraData)
+                if (options.extraData.hasOwnProperty(p))
+                    formdata.append(p, options.extraData[p]);
+        }
+
+        options.data = null;
+
+        var s = $.extend(true, {}, $.ajaxSettings, options, {
+            contentType: false,
+            processData: false,
+            cache: false,
+            type: 'POST'
+        });
+        
+        if (options.uploadProgress) {
+            // workaround because jqXHR does not expose upload property
+            s.xhr = function() {
+                var xhr = jQuery.ajaxSettings.xhr();
+                if (xhr.upload) {
+                    xhr.upload.onprogress = function(event) {
+                        var percent = 0;
+                        var position = event.loaded || event.position; /*event.position is deprecated*/
+                        var total = event.total;
+                        if (event.lengthComputable) {
+                            percent = Math.ceil(position / total * 100);
+                        }
+                        options.uploadProgress(event, position, total, percent);
+                    };
+                }
+                return xhr;
+            };
+        }
+
+        s.data = null;
+            var beforeSend = s.beforeSend;
+            s.beforeSend = function(xhr, o) {
+                o.data = formdata;
+                if(beforeSend)
+                    beforeSend.call(this, xhr, o);
+        };
+        $.ajax(s);
+    }
+
+    // private function for handling file uploads (hat tip to YAHOO!)
+    function fileUploadIframe(a) {
+        var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
+        var useProp = !!$.fn.prop;
+
+        if ($(':input[name=submit],:input[id=submit]', form).length) {
+            // if there is an input with a name or id of 'submit' then we won't be
+            // able to invoke the submit fn on the form (at least not x-browser)
+            alert('Error: Form elements must not have name or id of "submit".');
+            return;
+        }
+        
+        if (a) {
+            // ensure that every serialized input is still enabled
+            for (i=0; i < elements.length; i++) {
+                el = $(elements[i]);
+                if ( useProp )
+                    el.prop('disabled', false);
+                else
+                    el.removeAttr('disabled');
+            }
+        }
+
+        s = $.extend(true, {}, $.ajaxSettings, options);
+        s.context = s.context || s;
+        id = 'jqFormIO' + (new Date().getTime());
+        if (s.iframeTarget) {
+            $io = $(s.iframeTarget);
+            n = $io.attr('name');
+            if (!n)
+                 $io.attr('name', id);
+            else
+                id = n;
+        }
+        else {
+            $io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
+            $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+        }
+        io = $io[0];
+
+
+        xhr = { // mock object
+            aborted: 0,
+            responseText: null,
+            responseXML: null,
+            status: 0,
+            statusText: 'n/a',
+            getAllResponseHeaders: function() {},
+            getResponseHeader: function() {},
+            setRequestHeader: function() {},
+            abort: function(status) {
+                var e = (status === 'timeout' ? 'timeout' : 'aborted');
+                log('aborting upload... ' + e);
+                this.aborted = 1;
+                // #214
+                if (io.contentWindow.document.execCommand) {
+                    try { // #214
+                        io.contentWindow.document.execCommand('Stop');
+                    } catch(ignore) {}
+                }
+                $io.attr('src', s.iframeSrc); // abort op in progress
+                xhr.error = e;
+                if (s.error)
+                    s.error.call(s.context, xhr, e, status);
+                if (g)
+                    $.event.trigger("ajaxError", [xhr, s, e]);
+                if (s.complete)
+                    s.complete.call(s.context, xhr, e);
+            }
+        };
+
+        g = s.global;
+        // trigger ajax global events so that activity/block indicators work like normal
+        if (g && 0 === $.active++) {
+            $.event.trigger("ajaxStart");
+        }
+        if (g) {
+            $.event.trigger("ajaxSend", [xhr, s]);
+        }
+
+        if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
+            if (s.global) {
+                $.active--;
+            }
+            return;
+        }
+        if (xhr.aborted) {
+            return;
+        }
+
+        // add submitting element to data if we know it
+        sub = form.clk;
+        if (sub) {
+            n = sub.name;
+            if (n && !sub.disabled) {
+                s.extraData = s.extraData || {};
+                s.extraData[n] = sub.value;
+                if (sub.type == "image") {
+                    s.extraData[n+'.x'] = form.clk_x;
+                    s.extraData[n+'.y'] = form.clk_y;
+                }
+            }
+        }
+        
+        var CLIENT_TIMEOUT_ABORT = 1;
+        var SERVER_ABORT = 2;
+
+        function getDoc(frame) {
+            var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
+            return doc;
+        }
+        
+        // Rails CSRF hack (thanks to Yvan Barthelemy)
+        var csrf_token = $('meta[name=csrf-token]').attr('content');
+        var csrf_param = $('meta[name=csrf-param]').attr('content');
+        if (csrf_param && csrf_token) {
+            s.extraData = s.extraData || {};
+            s.extraData[csrf_param] = csrf_token;
+        }
+
+        // take a breath so that pending repaints get some cpu time before the upload starts
+        function doSubmit() {
+            // make sure form attrs are set
+            var t = $form.attr('target'), a = $form.attr('action');
+
+            // update form attrs in IE friendly way
+            form.setAttribute('target',id);
+            if (!method) {
+                form.setAttribute('method', 'POST');
+            }
+            if (a != s.url) {
+                form.setAttribute('action', s.url);
+            }
+
+            // ie borks in some cases when setting encoding
+            if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
+                $form.attr({
+                    encoding: 'multipart/form-data',
+                    enctype:  'multipart/form-data'
+                });
+            }
+
+            // support timout
+            if (s.timeout) {
+                timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
+            }
+            
+            // look for server aborts
+            function checkState() {
+                try {
+                    var state = getDoc(io).readyState;
+                    log('state = ' + state);
+                    if (state && state.toLowerCase() == 'uninitialized')
+                        setTimeout(checkState,50);
+                }
+                catch(e) {
+                    log('Server abort: ' , e, ' (', e.name, ')');
+                    cb(SERVER_ABORT);
+                    if (timeoutHandle)
+                        clearTimeout(timeoutHandle);
+                    timeoutHandle = undefined;
+                }
+            }
+
+            // add "extra" data to form if provided in options
+            var extraInputs = [];
+            try {
+                if (s.extraData) {
+                    for (var n in s.extraData) {
+                        if (s.extraData.hasOwnProperty(n)) {
+                           // if using the $.param format that allows for multiple values with the same name
+                           if($.isPlainObject(s.extraData[n]) && s.extraData[n].hasOwnProperty('name') && s.extraData[n].hasOwnProperty('value')) {
+                               extraInputs.push(
+                               $('<input type="hidden" name="'+s.extraData[n].name+'">').attr('value',s.extraData[n].value)
+                                   .appendTo(form)[0]);
+                           } else {
+                               extraInputs.push(
+                               $('<input type="hidden" name="'+n+'">').attr('value',s.extraData[n])
+                                   .appendTo(form)[0]);
+                           }
+                        }
+                    }
+                }
+
+                if (!s.iframeTarget) {
+                    // add iframe to doc and submit the form
+                    $io.appendTo('body');
+                    if (io.attachEvent)
+                        io.attachEvent('onload', cb);
+                    else
+                        io.addEventListener('load', cb, false);
+                }
+                setTimeout(checkState,15);
+                form.submit();
+            }
+            finally {
+                // reset attrs and remove "extra" input elements
+                form.setAttribute('action',a);
+                if(t) {
+                    form.setAttribute('target', t);
+                } else {
+                    $form.removeAttr('target');
+                }
+                $(extraInputs).remove();
+            }
+        }
+
+        if (s.forceSync) {
+            doSubmit();
+        }
+        else {
+            setTimeout(doSubmit, 10); // this lets dom updates render
+        }
+
+        var data, doc, domCheckCount = 50, callbackProcessed;
+
+        function cb(e) {
+            if (xhr.aborted || callbackProcessed) {
+                return;
+            }
+            try {
+                doc = getDoc(io);
+            }
+            catch(ex) {
+                log('cannot access response document: ', ex);
+                e = SERVER_ABORT;
+            }
+            if (e === CLIENT_TIMEOUT_ABORT && xhr) {
+                xhr.abort('timeout');
+                return;
+            }
+            else if (e == SERVER_ABORT && xhr) {
+                xhr.abort('server abort');
+                return;
+            }
+
+            if (!doc || doc.location.href == s.iframeSrc) {
+                // response not received yet
+                if (!timedOut)
+                    return;
+            }
+            if (io.detachEvent)
+                io.detachEvent('onload', cb);
+            else    
+                io.removeEventListener('load', cb, false);
+
+            var status = 'success', errMsg;
+            try {
+                if (timedOut) {
+                    throw 'timeout';
+                }
+
+                var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
+                log('isXml='+isXml);
+                if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) {
+                    if (--domCheckCount) {
+                        // in some browsers (Opera) the iframe DOM is not always traversable when
+                        // the onload callback fires, so we loop a bit to accommodate
+                        log('requeing onLoad callback, DOM not available');
+                        setTimeout(cb, 250);
+                        return;
+                    }
+                    // let this fall through because server response could be an empty document
+                    //log('Could not access iframe DOM after mutiple tries.');
+                    //throw 'DOMException: not available';
+                }
+
+                //log('response detected');
+                var docRoot = doc.body ? doc.body : doc.documentElement;
+                xhr.responseText = docRoot ? docRoot.innerHTML : null;
+                xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+                if (isXml)
+                    s.dataType = 'xml';
+                xhr.getResponseHeader = function(header){
+                    var headers = {'content-type': s.dataType};
+                    return headers[header];
+                };
+                // support for XHR 'status' & 'statusText' emulation :
+                if (docRoot) {
+                    xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
+                    xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
+                }
+
+                var dt = (s.dataType || '').toLowerCase();
+                var scr = /(json|script|text)/.test(dt);
+                if (scr || s.textarea) {
+                    // see if user embedded response in textarea
+                    var ta = doc.getElementsByTagName('textarea')[0];
+                    if (ta) {
+                        xhr.responseText = ta.value;
+                        // support for XHR 'status' & 'statusText' emulation :
+                        xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
+                        xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
+                    }
+                    else if (scr) {
+                        // account for browsers injecting pre around json response
+                        var pre = doc.getElementsByTagName('pre')[0];
+                        var b = doc.getElementsByTagName('body')[0];
+                        if (pre) {
+                            xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
+                        }
+                        else if (b) {
+                            xhr.responseText = b.textContent ? b.textContent : b.innerText;
+                        }
+                    }
+                }
+                else if (dt == 'xml' && !xhr.responseXML && xhr.responseText) {
+                    xhr.responseXML = toXml(xhr.responseText);
+                }
+
+                try {
+                    data = httpData(xhr, dt, s);
+                }
+                catch (e) {
+                    status = 'parsererror';
+                    xhr.error = errMsg = (e || status);
+                }
+            }
+            catch (e) {
+                log('error caught: ',e);
+                status = 'error';
+                xhr.error = errMsg = (e || status);
+            }
+
+            if (xhr.aborted) {
+                log('upload aborted');
+                status = null;
+            }
+
+            if (xhr.status) { // we've set xhr.status
+                status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
+            }
+
+            // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+            if (status === 'success') {
+                if (s.success)
+                    s.success.call(s.context, data, 'success', xhr);
+                if (g)
+                    $.event.trigger("ajaxSuccess", [xhr, s]);
+            }
+            else if (status) {
+                if (errMsg === undefined)
+                    errMsg = xhr.statusText;
+                if (s.error)
+                    s.error.call(s.context, xhr, status, errMsg);
+                if (g)
+                    $.event.trigger("ajaxError", [xhr, s, errMsg]);
+            }
+
+            if (g)
+                $.event.trigger("ajaxComplete", [xhr, s]);
+
+            if (g && ! --$.active) {
+                $.event.trigger("ajaxStop");
+            }
+
+            if (s.complete)
+                s.complete.call(s.context, xhr, status);
+
+            callbackProcessed = true;
+            if (s.timeout)
+                clearTimeout(timeoutHandle);
+
+            // clean up
+            setTimeout(function() {
+                if (!s.iframeTarget)
+                    $io.remove();
+                xhr.responseXML = null;
+            }, 100);
+        }
+
+        var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
+            if (window.ActiveXObject) {
+                doc = new ActiveXObject('Microsoft.XMLDOM');
+                doc.async = 'false';
+                doc.loadXML(s);
+            }
+            else {
+                doc = (new DOMParser()).parseFromString(s, 'text/xml');
+            }
+            return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
+        };
+        var parseJSON = $.parseJSON || function(s) {
+            /*jslint evil:true */
+            return window['eval']('(' + s + ')');
+        };
+
+        var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
+
+            var ct = xhr.getResponseHeader('content-type') || '',
+                xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
+                data = xml ? xhr.responseXML : xhr.responseText;
+
+            if (xml && data.documentElement.nodeName === 'parsererror') {
+                if ($.error)
+                    $.error('parsererror');
+            }
+            if (s && s.dataFilter) {
+                data = s.dataFilter(data, type);
+            }
+            if (typeof data === 'string') {
+                if (type === 'json' || !type && ct.indexOf('json') >= 0) {
+                    data = parseJSON(data);
+                } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
+                    $.globalEval(data);
+                }
+            }
+            return data;
+        };
+    }
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
+ *    is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ *    used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself.
+ */
+$.fn.ajaxForm = function(options) {
+    options = options || {};
+    options.delegation = options.delegation && $.isFunction($.fn.on);
+    
+    // in jQuery 1.3+ we can fix mistakes with the ready state
+    if (!options.delegation && this.length === 0) {
+        var o = { s: this.selector, c: this.context };
+        if (!$.isReady && o.s) {
+            log('DOM not ready, queuing ajaxForm');
+            $(function() {
+                $(o.s,o.c).ajaxForm(options);
+            });
+            return this;
+        }
+        // is your DOM ready?  http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
+        log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
+        return this;
+    }
+
+    if ( options.delegation ) {
+        $(document)
+            .off('submit.form-plugin', this.selector, doAjaxSubmit)
+            .off('click.form-plugin', this.selector, captureSubmittingElement)
+            .on('submit.form-plugin', this.selector, options, doAjaxSubmit)
+            .on('click.form-plugin', this.selector, options, captureSubmittingElement);
+        return this;
+    }
+
+    return this.ajaxFormUnbind()
+        .bind('submit.form-plugin', options, doAjaxSubmit)
+        .bind('click.form-plugin', options, captureSubmittingElement);
+};
+
+// private event handlers    
+function doAjaxSubmit(e) {
+    /*jshint validthis:true */
+    var options = e.data;
+    if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
+        e.preventDefault();
+        $(this).ajaxSubmit(options);
+    }
+}
+    
+function captureSubmittingElement(e) {
+    /*jshint validthis:true */
+    var target = e.target;
+    var $el = $(target);
+    if (!($el.is(":submit,input:image"))) {
+        // is this a child element of the submit el?  (ex: a span within a button)
+        var t = $el.closest(':submit');
+        if (t.length === 0) {
+            return;
+        }
+        target = t[0];
+    }
+    var form = this;
+    form.clk = target;
+    if (target.type == 'image') {
+        if (e.offsetX !== undefined) {
+            form.clk_x = e.offsetX;
+            form.clk_y = e.offsetY;
+        } else if (typeof $.fn.offset == 'function') {
+            var offset = $el.offset();
+            form.clk_x = e.pageX - offset.left;
+            form.clk_y = e.pageY - offset.top;
+        } else {
+            form.clk_x = e.pageX - target.offsetLeft;
+            form.clk_y = e.pageY - target.offsetTop;
+        }
+    }
+    // clear form vars
+    setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
+}
+
+
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+$.fn.ajaxFormUnbind = function() {
+    return this.unbind('submit.form-plugin click.form-plugin');
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property.  An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic, elements) {
+    var a = [];
+    if (this.length === 0) {
+        return a;
+    }
+
+    var form = this[0];
+    var els = semantic ? form.getElementsByTagName('*') : form.elements;
+    if (!els) {
+        return a;
+    }
+
+    var i,j,n,v,el,max,jmax;
+    for(i=0, max=els.length; i < max; i++) {
+        el = els[i];
+        n = el.name;
+        if (!n) {
+            continue;
+        }
+
+        if (semantic && form.clk && el.type == "image") {
+            // handle image inputs on the fly when semantic == true
+            if(!el.disabled && form.clk == el) {
+                a.push({name: n, value: $(el).val(), type: el.type });
+                a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+            }
+            continue;
+        }
+
+        v = $.fieldValue(el, true);
+        if (v && v.constructor == Array) {
+            if (elements) 
+                elements.push(el);
+            for(j=0, jmax=v.length; j < jmax; j++) {
+                a.push({name: n, value: v[j]});
+            }
+        }
+        else if (feature.fileapi && el.type == 'file' && !el.disabled) {
+            if (elements) 
+                elements.push(el);
+            var files = el.files;
+            if (files.length) {
+                for (j=0; j < files.length; j++) {
+                    a.push({name: n, value: files[j], type: el.type});
+                }
+            }
+            else {
+                // #180
+                a.push({ name: n, value: '', type: el.type });
+            }
+        }
+        else if (v !== null && typeof v != 'undefined') {
+            if (elements) 
+                elements.push(el);
+            a.push({name: n, value: v, type: el.type, required: el.required});
+        }
+    }
+
+    if (!semantic && form.clk) {
+        // input type=='image' are not found in elements array! handle it here
+        var $input = $(form.clk), input = $input[0];
+        n = input.name;
+        if (n && !input.disabled && input.type == 'image') {
+            a.push({name: n, value: $input.val()});
+            a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+        }
+    }
+    return a;
+};
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&amp;name2=value2
+ */
+$.fn.formSerialize = function(semantic) {
+    //hand off to jQuery.param for proper encoding
+    return $.param(this.formToArray(semantic));
+};
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&amp;name2=value2
+ */
+$.fn.fieldSerialize = function(successful) {
+    var a = [];
+    this.each(function() {
+        var n = this.name;
+        if (!n) {
+            return;
+        }
+        var v = $.fieldValue(this, successful);
+        if (v && v.constructor == Array) {
+            for (var i=0,max=v.length; i < max; i++) {
+                a.push({name: n, value: v[i]});
+            }
+        }
+        else if (v !== null && typeof v != 'undefined') {
+            a.push({name: this.name, value: v});
+        }
+    });
+    //hand off to jQuery.param for proper encoding
+    return $.param(a);
+};
+
+/**
+ * Returns the value(s) of the element in the matched set.  For example, consider the following form:
+ *
+ *  <form><fieldset>
+ *      <input name="A" type="text" />
+ *      <input name="A" type="text" />
+ *      <input name="B" type="checkbox" value="B1" />
+ *      <input name="B" type="checkbox" value="B2"/>
+ *      <input name="C" type="radio" value="C1" />
+ *      <input name="C" type="radio" value="C2" />
+ *  </fieldset></form>
+ *
+ *  var v = $(':text').fieldValue();
+ *  // if no values are entered into the text inputs
+ *  v == ['','']
+ *  // if values entered into the text inputs are 'foo' and 'bar'
+ *  v == ['foo','bar']
+ *
+ *  var v = $(':checkbox').fieldValue();
+ *  // if neither checkbox is checked
+ *  v === undefined
+ *  // if both checkboxes are checked
+ *  v == ['B1', 'B2']
+ *
+ *  var v = $(':radio').fieldValue();
+ *  // if neither radio is checked
+ *  v === undefined
+ *  // if first radio is checked
+ *  v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true.  If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array.  If no valid value can be determined the
+ *    array will be empty, otherwise it will contain one or more values.
+ */
+$.fn.fieldValue = function(successful) {
+    for (var val=[], i=0, max=this.length; i < max; i++) {
+        var el = this[i];
+        var v = $.fieldValue(el, successful);
+        if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
+            continue;
+        }
+        if (v.constructor == Array)
+            $.merge(val, v);
+        else
+            val.push(v);
+    }
+    return val;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+    var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+    if (successful === undefined) {
+        successful = true;
+    }
+
+    if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+        (t == 'checkbox' || t == 'radio') && !el.checked ||
+        (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+        tag == 'select' && el.selectedIndex == -1)) {
+            return null;
+    }
+
+    if (tag == 'select') {
+        var index = el.selectedIndex;
+        if (index < 0) {
+            return null;
+        }
+        var a = [], ops = el.options;
+        var one = (t == 'select-one');
+        var max = (one ? index+1 : ops.length);
+        for(var i=(one ? index : 0); i < max; i++) {
+            var op = ops[i];
+            if (op.selected) {
+                var v = op.value;
+                if (!v) { // extra pain for IE...
+                    v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
+                }
+                if (one) {
+                    return v;
+                }
+                a.push(v);
+            }
+        }
+        return a;
+    }
+    return $(el).val();
+};
+
+/**
+ * Clears the form data.  Takes the following actions on the form's input fields:
+ *  - input text fields will have their 'value' property set to the empty string
+ *  - select elements will have their 'selectedIndex' property set to -1
+ *  - checkbox and radio inputs will have their 'checked' property set to false
+ *  - inputs of type submit, button, reset, and hidden will *not* be effected
+ *  - button elements will *not* be effected
+ */
+$.fn.clearForm = function(includeHidden) {
+    return this.each(function() {
+        $('input,select,textarea', this).clearFields(includeHidden);
+    });
+};
+
+/**
+ * Clears the selected form elements.
+ */
+$.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
+    var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
+    return this.each(function() {
+        var t = this.type, tag = this.tagName.toLowerCase();
+        if (re.test(t) || tag == 'textarea') {
+            this.value = '';
+        }
+        else if (t == 'checkbox' || t == 'radio') {
+            this.checked = false;
+        }
+        else if (tag == 'select') {
+            this.selectedIndex = -1;
+        }
+        else if (includeHidden) {
+            // includeHidden can be the value true, or it can be a selector string
+            // indicating a special test; for example:
+            //  $('#myForm').clearForm('.special:hidden')
+            // the above would clean hidden inputs that have the class of 'special'
+            if ( (includeHidden === true && /hidden/.test(t)) ||
+                 (typeof includeHidden == 'string' && $(this).is(includeHidden)) )
+                this.value = '';
+        }
+    });
+};
+
+/**
+ * Resets the form data.  Causes all form elements to be reset to their original value.
+ */
+$.fn.resetForm = function() {
+    return this.each(function() {
+        // guard against an input with the name of 'reset'
+        // note that IE reports the reset function as an 'object'
+        if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
+            this.reset();
+        }
+    });
+};
+
+/**
+ * Enables or disables any matching elements.
+ */
+$.fn.enable = function(b) {
+    if (b === undefined) {
+        b = true;
+    }
+    return this.each(function() {
+        this.disabled = !b;
+    });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ */
+$.fn.selected = function(select) {
+    if (select === undefined) {
+        select = true;
+    }
+    return this.each(function() {
+        var t = this.type;
+        if (t == 'checkbox' || t == 'radio') {
+            this.checked = select;
+        }
+        else if (this.tagName.toLowerCase() == 'option') {
+            var $sel = $(this).parent('select');
+            if (select && $sel[0] && $sel[0].type == 'select-one') {
+                // deselect all other options
+                $sel.find('option').selected(false);
+            }
+            this.selected = select;
+        }
+    });
+};
+
+// expose debug var
+$.fn.ajaxSubmit.debug = false;
+
+// helper fn for console logging
+function log() {
+    if (!$.fn.ajaxSubmit.debug) 
+        return;
+    var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
+    if (window.console && window.console.log) {
+        window.console.log(msg);
+    }
+    else if (window.opera && window.opera.postError) {
+        window.opera.postError(msg);
+    }
+}
+
+})(jQuery);
diff --git a/resources/lib/jquery.fullscreen.js b/resources/lib/jquery.fullscreen.js
new file mode 100644 (file)
index 0000000..30e4484
--- /dev/null
@@ -0,0 +1,175 @@
+/**
+ * jQuery fullscreen plugin v2.0.0-git (9f8f97d127)
+ * https://github.com/theopolisme/jquery-fullscreen
+ *
+ * Copyright (c) 2013 Theopolisme <theopolismewiki@gmail.com>
+ *
+ * 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.
+ */
+( function ( $ ) {
+       var setupFullscreen,
+               fsClass = 'jq-fullscreened';
+
+       /**
+        * On fullscreenchange, trigger a jq-fullscreen-change event
+        * The event is given an object, which contains the fullscreened DOM element (element), if any
+        * and a boolean value (fullscreen) indicating if we've entered or exited fullscreen mode
+        * Also remove the 'fullscreened' class from elements that are no longer fullscreen
+        */
+       function handleFullscreenChange () {
+               var fullscreenElement = document.fullscreenElement ||
+                       document.mozFullScreenElement ||
+                       document.webkitFullscreenElement ||
+                       document.msFullscreenElement;
+
+               if ( !fullscreenElement ) {
+                       $( '.' + fsClass ).data( 'isFullscreened', false ).removeClass( fsClass );
+               }
+
+               $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: fullscreenElement, fullscreen: !!fullscreenElement } ) );
+       }
+
+       /**
+        * Enters full screen with the "this" element in focus.
+        * Check the .data( 'isFullscreened' ) of the return value to check
+        * success or failure, if you're into that sort of thing.
+        * @chainable
+        * @return {jQuery}
+        */
+       function enterFullscreen () {
+               var element = this.get(0),
+                       $element = this.first();
+               if ( element ) {
+                       if ( element.requestFullscreen ) {
+                               element.requestFullscreen();
+                       } else if ( element.mozRequestFullScreen ) {
+                               element.mozRequestFullScreen();
+                       } else if ( element.webkitRequestFullscreen ) {
+                               element.webkitRequestFullscreen();
+                       } else if ( element.msRequestFullscreen ) {
+                               element.msRequestFullscreen();
+                       } else {
+                               // Unable to make fullscreen
+                               $element.data( 'isFullscreened', false );
+                               return this;
+                       }
+                       // Add the fullscreen class and data attribute to `element`
+                       $element.addClass( fsClass ).data( 'isFullscreened', true );
+                       return this;
+               } else {
+                       $element.data( 'isFullscreened', false );
+                       return this;
+               }
+       }
+
+       /**
+        * Brings the "this" element out of fullscreen.
+        * Check the .data( 'isFullscreened' ) of the return value to check
+        * success or failure, if you're into that sort of thing.
+        * @chainable
+        * @return {jQuery}
+        */
+       function exitFullscreen () {
+               var fullscreenElement = ( document.fullscreenElement ||
+                               document.mozFullScreenElement ||
+                               document.webkitFullscreenElement ||
+                               document.msFullscreenElement );
+
+               // Ensure that we only exit fullscreen if exitFullscreen() is being called on the same element that is currently fullscreen
+               if ( fullscreenElement && this.get(0) === fullscreenElement ) {
+                       if ( document.exitFullscreen ) {
+                               document.exitFullscreen();
+                       } else if ( document.mozCancelFullScreen ) {
+                               document.mozCancelFullScreen();
+                       } else if ( document.webkitCancelFullScreen ) {
+                               document.webkitCancelFullScreen();
+                       } else if ( document.msExitFullscreen ) {
+                               document.msExitFullscreen();
+                       } else {
+                               // Unable to cancel fullscreen mode
+                               return this;
+                       }
+                       // We don't need to remove the fullscreen class here,
+                       // because it will be removed in handleFullscreenChange.
+                       // But we should change the data on the element so the
+                       // caller can check for success.
+                       this.first().data( 'isFullscreened', false );
+               }
+
+               return this;
+       }
+
+       /**
+        * Set up fullscreen handling and install necessary event handlers.
+        * Return false if fullscreen is not supported.
+        */
+       setupFullscreen = function () {
+               if ( $.support.fullscreen ) {
+                       // When the fullscreen mode is changed, trigger the
+                       // fullscreen events (and when exiting,
+                       // also remove the fullscreen class)
+                       $( document ).on( 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', handleFullscreenChange);
+                       // Convenience wrapper so that one only needs to listen for
+                       // 'fullscreenerror', not all of the prefixed versions
+                       $( document ).on( 'webkitfullscreenerror mozfullscreenerror MSFullscreenError', function () {
+                               $( document ).trigger( $.Event( 'fullscreenerror' ) );
+                       } );
+                       // Fullscreen has been set up, so always return true
+                       setupFullscreen = function () { return true; };
+                       return true;
+               } else {
+                       // Always return false from now on, since fullscreen is not supported
+                       setupFullscreen = function () { return false; };
+                       return false;
+               }
+       };
+
+       /**
+        * Set up fullscreen handling if necessary, then make the first element
+        * matching the given selector fullscreen
+        * @chainable
+        * @return {jQuery}
+        */
+       $.fn.enterFullscreen = function () {
+               if ( setupFullscreen() ) {
+                       $.fn.enterFullscreen = enterFullscreen;
+                       return this.enterFullscreen();
+               } else {
+                       $.fn.enterFullscreen = function () { return this; };
+                       return this;
+               }
+       };
+
+       /**
+        * Set up fullscreen handling if necessary, then cancel fullscreen mode
+        * for the first element matching the given selector.
+        * @chainable
+        * @return {jQuery}
+        */
+       $.fn.exitFullscreen = function () {
+               if ( setupFullscreen() ) {
+                       $.fn.exitFullscreen = exitFullscreen;
+                       return this.exitFullscreen();
+               } else {
+                       $.fn.exitFullscreen = function () { return this; };
+                       return this;
+               }
+       };
+
+       $.support.fullscreen = document.fullscreenEnabled ||
+               document.webkitFullscreenEnabled ||
+               document.mozFullScreenEnabled ||
+               document.msFullscreenEnabled;
+}( jQuery ) );
diff --git a/resources/lib/jquery.hoverIntent.js b/resources/lib/jquery.hoverIntent.js
new file mode 100644 (file)
index 0000000..adf948d
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+* hoverIntent is similar to jQuery's built-in "hover" function except that
+* instead of firing the onMouseOver event immediately, hoverIntent checks
+* to see if the user's mouse has slowed down (beneath the sensitivity
+* threshold) before firing the onMouseOver event.
+* 
+* hoverIntent r5 // 2007.03.27 // jQuery 1.1.2+
+* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
+* 
+* hoverIntent is currently available for use in all personal or commercial 
+* projects under both MIT and GPL licenses. This means that you can choose 
+* the license that best suits your project, and use it accordingly.
+* 
+* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
+* $("ul li").hoverIntent( showNav , hideNav );
+* 
+* // advanced usage receives configuration object only
+* $("ul li").hoverIntent({
+*      sensitivity: 7, // number = sensitivity threshold (must be 1 or higher)
+*      interval: 100,   // number = milliseconds of polling interval
+*      over: showNav,  // function = onMouseOver callback (required)
+*      timeout: 0,   // number = milliseconds delay before onMouseOut function call
+*      out: hideNav    // function = onMouseOut callback (required)
+* });
+* 
+* @param  f  onMouseOver function || An object with configuration options
+* @param  g  onMouseOut function  || Nothing (use configuration options object)
+* @author    Brian Cherne <brian@cherne.net>
+*/
+(function($) {
+       $.fn.hoverIntent = function(f,g) {
+               // default configuration options
+               var cfg = {
+                       sensitivity: 7,
+                       interval: 100,
+                       timeout: 0
+               };
+               // override configuration options with user supplied object
+               cfg = $.extend(cfg, g ? { over: f, out: g } : f );
+
+               // instantiate variables
+               // cX, cY = current X and Y position of mouse, updated by mousemove event
+               // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
+               var cX, cY, pX, pY;
+
+               // A private function for getting mouse position
+               var track = function(ev) {
+                       cX = ev.pageX;
+                       cY = ev.pageY;
+               };
+
+               // A private function for comparing current and previous mouse position
+               var compare = function(ev,ob) {
+                       ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
+                       // compare mouse positions to see if they've crossed the threshold
+                       if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) {
+                               $(ob).unbind("mousemove",track);
+                               // set hoverIntent state to true (so mouseOut can be called)
+                               ob.hoverIntent_s = 1;
+                               return cfg.over.apply(ob,[ev]);
+                       } else {
+                               // set previous coordinates for next time
+                               pX = cX; pY = cY;
+                               // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
+                               ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
+                       }
+               };
+
+               // A private function for delaying the mouseOut function
+               var delay = function(ev,ob) {
+                       ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
+                       ob.hoverIntent_s = 0;
+                       return cfg.out.apply(ob,[ev]);
+               };
+
+               // A private function for handling mouse 'hovering'
+               var handleHover = function(e) {
+                       // next three lines copied from jQuery.hover, ignore children onMouseOver/onMouseOut
+                       var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget;
+                       while ( p && p != this ) { try { p = p.parentNode; } catch(e) { p = this; } }
+                       if ( p == this ) { return false; }
+
+                       // copy objects to be passed into t (required for event object to be passed in IE)
+                       var ev = $.extend({},e);
+                       var ob = this;
+
+                       // cancel hoverIntent timer if it exists
+                       if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }
+
+                       // else e.type == "onmouseover"
+                       if (e.type == "mouseover") {
+                               // set "previous" X and Y position based on initial entry point
+                               pX = ev.pageX; pY = ev.pageY;
+                               // update "current" X and Y position based on mousemove
+                               $(ob).bind("mousemove",track);
+                               // start polling interval (self-calling timeout) to compare mouse coordinates over time
+                               if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
+
+                       // else e.type == "onmouseout"
+                       } else {
+                               // unbind expensive mousemove event
+                               $(ob).unbind("mousemove",track);
+                               // if hoverIntent state is true, then call the mouseOut function after the specified delay
+                               if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
+                       }
+               };
+
+               // bind the function to the two event listeners
+               return this.mouseover(handleHover).mouseout(handleHover);
+       };
+})(jQuery);
\ No newline at end of file
diff --git a/resources/lib/jquery.jStorage.js b/resources/lib/jquery.jStorage.js
new file mode 100644 (file)
index 0000000..45e19ac
--- /dev/null
@@ -0,0 +1,996 @@
+/*
+ * ----------------------------- JSTORAGE -------------------------------------
+ * Simple local storage wrapper to save data on the browser side, supporting
+ * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
+ *
+ * Author: Andris Reinman, andris.reinman@gmail.com
+ * Project homepage: www.jstorage.info
+ *
+ * Licensed under Unlicense:
+ *
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * For more information, please refer to <http://unlicense.org/>
+ */
+
+/* global ActiveXObject: false */
+/* jshint browser: true */
+
+(function() {
+    'use strict';
+
+    var
+    /* jStorage version */
+        JSTORAGE_VERSION = '0.4.12',
+
+        /* detect a dollar object or create one if not found */
+        $ = window.jQuery || window.$ || (window.$ = {}),
+
+        /* check for a JSON handling support */
+        JSON = {
+            parse: window.JSON && (window.JSON.parse || window.JSON.decode) ||
+                String.prototype.evalJSON && function(str) {
+                    return String(str).evalJSON();
+            } ||
+                $.parseJSON ||
+                $.evalJSON,
+            stringify: Object.toJSON ||
+                window.JSON && (window.JSON.stringify || window.JSON.encode) ||
+                $.toJSON
+        };
+
+    // Break if no JSON support was found
+    if (typeof JSON.parse !== 'function' || typeof JSON.stringify !== 'function') {
+        throw new Error('No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page');
+    }
+
+    var
+    /* This is the object, that holds the cached values */
+        _storage = {
+            __jstorage_meta: {
+                CRC32: {}
+            }
+        },
+
+        /* Actual browser storage (localStorage or globalStorage['domain']) */
+        _storage_service = {
+            jStorage: '{}'
+        },
+
+        /* DOM element for older IE versions, holds userData behavior */
+        _storage_elm = null,
+
+        /* How much space does the storage take */
+        _storage_size = 0,
+
+        /* which backend is currently used */
+        _backend = false,
+
+        /* onchange observers */
+        _observers = {},
+
+        /* timeout to wait after onchange event */
+        _observer_timeout = false,
+
+        /* last update time */
+        _observer_update = 0,
+
+        /* pubsub observers */
+        _pubsub_observers = {},
+
+        /* skip published items older than current timestamp */
+        _pubsub_last = +new Date(),
+
+        /* Next check for TTL */
+        _ttl_timeout,
+
+        /**
+         * XML encoding and decoding as XML nodes can't be JSON'ized
+         * XML nodes are encoded and decoded if the node is the value to be saved
+         * but not if it's as a property of another object
+         * Eg. -
+         *   $.jStorage.set('key', xmlNode);        // IS OK
+         *   $.jStorage.set('key', {xml: xmlNode}); // NOT OK
+         */
+        _XMLService = {
+
+            /**
+             * Validates a XML node to be XML
+             * based on jQuery.isXML function
+             */
+            isXML: function(elm) {
+                var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
+                return documentElement ? documentElement.nodeName !== 'HTML' : false;
+            },
+
+            /**
+             * Encodes a XML node to string
+             * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
+             */
+            encode: function(xmlNode) {
+                if (!this.isXML(xmlNode)) {
+                    return false;
+                }
+                try { // Mozilla, Webkit, Opera
+                    return new XMLSerializer().serializeToString(xmlNode);
+                } catch (E1) {
+                    try { // IE
+                        return xmlNode.xml;
+                    } catch (E2) {}
+                }
+                return false;
+            },
+
+            /**
+             * Decodes a XML node from string
+             * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
+             */
+            decode: function(xmlString) {
+                var dom_parser = ('DOMParser' in window && (new DOMParser()).parseFromString) ||
+                    (window.ActiveXObject && function(_xmlString) {
+                        var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
+                        xml_doc.async = 'false';
+                        xml_doc.loadXML(_xmlString);
+                        return xml_doc;
+                    }),
+                    resultXML;
+                if (!dom_parser) {
+                    return false;
+                }
+                resultXML = dom_parser.call('DOMParser' in window && (new DOMParser()) || window, xmlString, 'text/xml');
+                return this.isXML(resultXML) ? resultXML : false;
+            }
+        };
+
+
+    ////////////////////////// PRIVATE METHODS ////////////////////////
+
+    /**
+     * Initialization function. Detects if the browser supports DOM Storage
+     * or userData behavior and behaves accordingly.
+     */
+    function _init() {
+        /* Check if browser supports localStorage */
+        var localStorageReallyWorks = false;
+        if ('localStorage' in window) {
+            try {
+                window.localStorage.setItem('_tmptest', 'tmpval');
+                localStorageReallyWorks = true;
+                window.localStorage.removeItem('_tmptest');
+            } catch (BogusQuotaExceededErrorOnIos5) {
+                // Thanks be to iOS5 Private Browsing mode which throws
+                // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
+            }
+        }
+
+        if (localStorageReallyWorks) {
+            try {
+                if (window.localStorage) {
+                    _storage_service = window.localStorage;
+                    _backend = 'localStorage';
+                    _observer_update = _storage_service.jStorage_update;
+                }
+            } catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
+        }
+        /* Check if browser supports globalStorage */
+        else if ('globalStorage' in window) {
+            try {
+                if (window.globalStorage) {
+                    if (window.location.hostname == 'localhost') {
+                        _storage_service = window.globalStorage['localhost.localdomain'];
+                    } else {
+                        _storage_service = window.globalStorage[window.location.hostname];
+                    }
+                    _backend = 'globalStorage';
+                    _observer_update = _storage_service.jStorage_update;
+                }
+            } catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
+        }
+        /* Check if browser supports userData behavior */
+        else {
+            _storage_elm = document.createElement('link');
+            if (_storage_elm.addBehavior) {
+
+                /* Use a DOM element to act as userData storage */
+                _storage_elm.style.behavior = 'url(#default#userData)';
+
+                /* userData element needs to be inserted into the DOM! */
+                document.getElementsByTagName('head')[0].appendChild(_storage_elm);
+
+                try {
+                    _storage_elm.load('jStorage');
+                } catch (E) {
+                    // try to reset cache
+                    _storage_elm.setAttribute('jStorage', '{}');
+                    _storage_elm.save('jStorage');
+                    _storage_elm.load('jStorage');
+                }
+
+                var data = '{}';
+                try {
+                    data = _storage_elm.getAttribute('jStorage');
+                } catch (E5) {}
+
+                try {
+                    _observer_update = _storage_elm.getAttribute('jStorage_update');
+                } catch (E6) {}
+
+                _storage_service.jStorage = data;
+                _backend = 'userDataBehavior';
+            } else {
+                _storage_elm = null;
+                return;
+            }
+        }
+
+        // Load data from storage
+        _load_storage();
+
+        // remove dead keys
+        _handleTTL();
+
+        // start listening for changes
+        _setupObserver();
+
+        // initialize publish-subscribe service
+        _handlePubSub();
+
+        // handle cached navigation
+        if ('addEventListener' in window) {
+            window.addEventListener('pageshow', function(event) {
+                if (event.persisted) {
+                    _storageObserver();
+                }
+            }, false);
+        }
+    }
+
+    /**
+     * Reload data from storage when needed
+     */
+    function _reloadData() {
+        var data = '{}';
+
+        if (_backend == 'userDataBehavior') {
+            _storage_elm.load('jStorage');
+
+            try {
+                data = _storage_elm.getAttribute('jStorage');
+            } catch (E5) {}
+
+            try {
+                _observer_update = _storage_elm.getAttribute('jStorage_update');
+            } catch (E6) {}
+
+            _storage_service.jStorage = data;
+        }
+
+        _load_storage();
+
+        // remove dead keys
+        _handleTTL();
+
+        _handlePubSub();
+    }
+
+    /**
+     * Sets up a storage change observer
+     */
+    function _setupObserver() {
+        if (_backend == 'localStorage' || _backend == 'globalStorage') {
+            if ('addEventListener' in window) {
+                window.addEventListener('storage', _storageObserver, false);
+            } else {
+                document.attachEvent('onstorage', _storageObserver);
+            }
+        } else if (_backend == 'userDataBehavior') {
+            setInterval(_storageObserver, 1000);
+        }
+    }
+
+    /**
+     * Fired on any kind of data change, needs to check if anything has
+     * really been changed
+     */
+    function _storageObserver() {
+        var updateTime;
+        // cumulate change notifications with timeout
+        clearTimeout(_observer_timeout);
+        _observer_timeout = setTimeout(function() {
+
+            if (_backend == 'localStorage' || _backend == 'globalStorage') {
+                updateTime = _storage_service.jStorage_update;
+            } else if (_backend == 'userDataBehavior') {
+                _storage_elm.load('jStorage');
+                try {
+                    updateTime = _storage_elm.getAttribute('jStorage_update');
+                } catch (E5) {}
+            }
+
+            if (updateTime && updateTime != _observer_update) {
+                _observer_update = updateTime;
+                _checkUpdatedKeys();
+            }
+
+        }, 25);
+    }
+
+    /**
+     * Reloads the data and checks if any keys are changed
+     */
+    function _checkUpdatedKeys() {
+        var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
+            newCrc32List;
+
+        _reloadData();
+        newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
+
+        var key,
+            updated = [],
+            removed = [];
+
+        for (key in oldCrc32List) {
+            if (oldCrc32List.hasOwnProperty(key)) {
+                if (!newCrc32List[key]) {
+                    removed.push(key);
+                    continue;
+                }
+                if (oldCrc32List[key] != newCrc32List[key] && String(oldCrc32List[key]).substr(0, 2) == '2.') {
+                    updated.push(key);
+                }
+            }
+        }
+
+        for (key in newCrc32List) {
+            if (newCrc32List.hasOwnProperty(key)) {
+                if (!oldCrc32List[key]) {
+                    updated.push(key);
+                }
+            }
+        }
+
+        _fireObservers(updated, 'updated');
+        _fireObservers(removed, 'deleted');
+    }
+
+    /**
+     * Fires observers for updated keys
+     *
+     * @param {Array|String} keys Array of key names or a key
+     * @param {String} action What happened with the value (updated, deleted, flushed)
+     */
+    function _fireObservers(keys, action) {
+        keys = [].concat(keys || []);
+
+        var i, j, len, jlen;
+
+        if (action == 'flushed') {
+            keys = [];
+            for (var key in _observers) {
+                if (_observers.hasOwnProperty(key)) {
+                    keys.push(key);
+                }
+            }
+            action = 'deleted';
+        }
+        for (i = 0, len = keys.length; i < len; i++) {
+            if (_observers[keys[i]]) {
+                for (j = 0, jlen = _observers[keys[i]].length; j < jlen; j++) {
+                    _observers[keys[i]][j](keys[i], action);
+                }
+            }
+            if (_observers['*']) {
+                for (j = 0, jlen = _observers['*'].length; j < jlen; j++) {
+                    _observers['*'][j](keys[i], action);
+                }
+            }
+        }
+    }
+
+    /**
+     * Publishes key change to listeners
+     */
+    function _publishChange() {
+        var updateTime = (+new Date()).toString();
+
+        if (_backend == 'localStorage' || _backend == 'globalStorage') {
+            try {
+                _storage_service.jStorage_update = updateTime;
+            } catch (E8) {
+                // safari private mode has been enabled after the jStorage initialization
+                _backend = false;
+            }
+        } else if (_backend == 'userDataBehavior') {
+            _storage_elm.setAttribute('jStorage_update', updateTime);
+            _storage_elm.save('jStorage');
+        }
+
+        _storageObserver();
+    }
+
+    /**
+     * Loads the data from the storage based on the supported mechanism
+     */
+    function _load_storage() {
+        /* if jStorage string is retrieved, then decode it */
+        if (_storage_service.jStorage) {
+            try {
+                _storage = JSON.parse(String(_storage_service.jStorage));
+            } catch (E6) {
+                _storage_service.jStorage = '{}';
+            }
+        } else {
+            _storage_service.jStorage = '{}';
+        }
+        _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
+
+        if (!_storage.__jstorage_meta) {
+            _storage.__jstorage_meta = {};
+        }
+        if (!_storage.__jstorage_meta.CRC32) {
+            _storage.__jstorage_meta.CRC32 = {};
+        }
+    }
+
+    /**
+     * This functions provides the 'save' mechanism to store the jStorage object
+     */
+    function _save() {
+        _dropOldEvents(); // remove expired events
+        try {
+            _storage_service.jStorage = JSON.stringify(_storage);
+            // If userData is used as the storage engine, additional
+            if (_storage_elm) {
+                _storage_elm.setAttribute('jStorage', _storage_service.jStorage);
+                _storage_elm.save('jStorage');
+            }
+            _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
+        } catch (E7) { /* probably cache is full, nothing is saved this way*/ }
+    }
+
+    /**
+     * Function checks if a key is set and is string or numberic
+     *
+     * @param {String} key Key name
+     */
+    function _checkKey(key) {
+        if (typeof key != 'string' && typeof key != 'number') {
+            throw new TypeError('Key name must be string or numeric');
+        }
+        if (key == '__jstorage_meta') {
+            throw new TypeError('Reserved key name');
+        }
+        return true;
+    }
+
+    /**
+     * Removes expired keys
+     */
+    function _handleTTL() {
+        var curtime, i, TTL, CRC32, nextExpire = Infinity,
+            changed = false,
+            deleted = [];
+
+        clearTimeout(_ttl_timeout);
+
+        if (!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != 'object') {
+            // nothing to do here
+            return;
+        }
+
+        curtime = +new Date();
+        TTL = _storage.__jstorage_meta.TTL;
+
+        CRC32 = _storage.__jstorage_meta.CRC32;
+        for (i in TTL) {
+            if (TTL.hasOwnProperty(i)) {
+                if (TTL[i] <= curtime) {
+                    delete TTL[i];
+                    delete CRC32[i];
+                    delete _storage[i];
+                    changed = true;
+                    deleted.push(i);
+                } else if (TTL[i] < nextExpire) {
+                    nextExpire = TTL[i];
+                }
+            }
+        }
+
+        // set next check
+        if (nextExpire != Infinity) {
+            _ttl_timeout = setTimeout(_handleTTL, Math.min(nextExpire - curtime, 0x7FFFFFFF));
+        }
+
+        // save changes
+        if (changed) {
+            _save();
+            _publishChange();
+            _fireObservers(deleted, 'deleted');
+        }
+    }
+
+    /**
+     * Checks if there's any events on hold to be fired to listeners
+     */
+    function _handlePubSub() {
+        var i, len;
+        if (!_storage.__jstorage_meta.PubSub) {
+            return;
+        }
+        var pubelm,
+            _pubsubCurrent = _pubsub_last,
+            needFired = [];
+
+        for (i = len = _storage.__jstorage_meta.PubSub.length - 1; i >= 0; i--) {
+            pubelm = _storage.__jstorage_meta.PubSub[i];
+            if (pubelm[0] > _pubsub_last) {
+                _pubsubCurrent = pubelm[0];
+                needFired.unshift(pubelm);
+            }
+        }
+
+        for (i = needFired.length - 1; i >= 0; i--) {
+            _fireSubscribers(needFired[i][1], needFired[i][2]);
+        }
+
+        _pubsub_last = _pubsubCurrent;
+    }
+
+    /**
+     * Fires all subscriber listeners for a pubsub channel
+     *
+     * @param {String} channel Channel name
+     * @param {Mixed} payload Payload data to deliver
+     */
+    function _fireSubscribers(channel, payload) {
+        if (_pubsub_observers[channel]) {
+            for (var i = 0, len = _pubsub_observers[channel].length; i < len; i++) {
+                // send immutable data that can't be modified by listeners
+                try {
+                    _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
+                } catch (E) {}
+            }
+        }
+    }
+
+    /**
+     * Remove old events from the publish stream (at least 2sec old)
+     */
+    function _dropOldEvents() {
+        if (!_storage.__jstorage_meta.PubSub) {
+            return;
+        }
+
+        var retire = +new Date() - 2000;
+
+        for (var i = 0, len = _storage.__jstorage_meta.PubSub.length; i < len; i++) {
+            if (_storage.__jstorage_meta.PubSub[i][0] <= retire) {
+                // deleteCount is needed for IE6
+                _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
+                break;
+            }
+        }
+
+        if (!_storage.__jstorage_meta.PubSub.length) {
+            delete _storage.__jstorage_meta.PubSub;
+        }
+
+    }
+
+    /**
+     * Publish payload to a channel
+     *
+     * @param {String} channel Channel name
+     * @param {Mixed} payload Payload to send to the subscribers
+     */
+    function _publish(channel, payload) {
+        if (!_storage.__jstorage_meta) {
+            _storage.__jstorage_meta = {};
+        }
+        if (!_storage.__jstorage_meta.PubSub) {
+            _storage.__jstorage_meta.PubSub = [];
+        }
+
+        _storage.__jstorage_meta.PubSub.unshift([+new Date(), channel, payload]);
+
+        _save();
+        _publishChange();
+    }
+
+
+    /**
+     * JS Implementation of MurmurHash2
+     *
+     *  SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
+     *
+     * @author <a href='mailto:gary.court@gmail.com'>Gary Court</a>
+     * @see http://github.com/garycourt/murmurhash-js
+     * @author <a href='mailto:aappleby@gmail.com'>Austin Appleby</a>
+     * @see http://sites.google.com/site/murmurhash/
+     *
+     * @param {string} str ASCII only
+     * @param {number} seed Positive integer only
+     * @return {number} 32-bit positive integer hash
+     */
+
+    function murmurhash2_32_gc(str, seed) {
+        var
+            l = str.length,
+            h = seed ^ l,
+            i = 0,
+            k;
+
+        while (l >= 4) {
+            k =
+                ((str.charCodeAt(i) & 0xff)) |
+                ((str.charCodeAt(++i) & 0xff) << 8) |
+                ((str.charCodeAt(++i) & 0xff) << 16) |
+                ((str.charCodeAt(++i) & 0xff) << 24);
+
+            k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+            k ^= k >>> 24;
+            k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+
+            h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
+
+            l -= 4;
+            ++i;
+        }
+
+        switch (l) {
+            case 3:
+                h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
+                /* falls through */
+            case 2:
+                h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
+                /* falls through */
+            case 1:
+                h ^= (str.charCodeAt(i) & 0xff);
+                h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+        }
+
+        h ^= h >>> 13;
+        h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
+        h ^= h >>> 15;
+
+        return h >>> 0;
+    }
+
+    ////////////////////////// PUBLIC INTERFACE /////////////////////////
+
+    $.jStorage = {
+        /* Version number */
+        version: JSTORAGE_VERSION,
+
+        /**
+         * Sets a key's value.
+         *
+         * @param {String} key Key to set. If this value is not set or not
+         *              a string an exception is raised.
+         * @param {Mixed} value Value to set. This can be any value that is JSON
+         *              compatible (Numbers, Strings, Objects etc.).
+         * @param {Object} [options] - possible options to use
+         * @param {Number} [options.TTL] - optional TTL value, in milliseconds
+         * @return {Mixed} the used value
+         */
+        set: function(key, value, options) {
+            _checkKey(key);
+
+            options = options || {};
+
+            // undefined values are deleted automatically
+            if (typeof value == 'undefined') {
+                this.deleteKey(key);
+                return value;
+            }
+
+            if (_XMLService.isXML(value)) {
+                value = {
+                    _is_xml: true,
+                    xml: _XMLService.encode(value)
+                };
+            } else if (typeof value == 'function') {
+                return undefined; // functions can't be saved!
+            } else if (value && typeof value == 'object') {
+                // clone the object before saving to _storage tree
+                value = JSON.parse(JSON.stringify(value));
+            }
+
+            _storage[key] = value;
+
+            _storage.__jstorage_meta.CRC32[key] = '2.' + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
+
+            this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
+
+            _fireObservers(key, 'updated');
+            return value;
+        },
+
+        /**
+         * Looks up a key in cache
+         *
+         * @param {String} key - Key to look up.
+         * @param {mixed} def - Default value to return, if key didn't exist.
+         * @return {Mixed} the key value, default value or null
+         */
+        get: function(key, def) {
+            _checkKey(key);
+            if (key in _storage) {
+                if (_storage[key] && typeof _storage[key] == 'object' && _storage[key]._is_xml) {
+                    return _XMLService.decode(_storage[key].xml);
+                } else {
+                    return _storage[key];
+                }
+            }
+            return typeof(def) == 'undefined' ? null : def;
+        },
+
+        /**
+         * Deletes a key from cache.
+         *
+         * @param {String} key - Key to delete.
+         * @return {Boolean} true if key existed or false if it didn't
+         */
+        deleteKey: function(key) {
+            _checkKey(key);
+            if (key in _storage) {
+                delete _storage[key];
+                // remove from TTL list
+                if (typeof _storage.__jstorage_meta.TTL == 'object' &&
+                    key in _storage.__jstorage_meta.TTL) {
+                    delete _storage.__jstorage_meta.TTL[key];
+                }
+
+                delete _storage.__jstorage_meta.CRC32[key];
+
+                _save();
+                _publishChange();
+                _fireObservers(key, 'deleted');
+                return true;
+            }
+            return false;
+        },
+
+        /**
+         * Sets a TTL for a key, or remove it if ttl value is 0 or below
+         *
+         * @param {String} key - key to set the TTL for
+         * @param {Number} ttl - TTL timeout in milliseconds
+         * @return {Boolean} true if key existed or false if it didn't
+         */
+        setTTL: function(key, ttl) {
+            var curtime = +new Date();
+            _checkKey(key);
+            ttl = Number(ttl) || 0;
+            if (key in _storage) {
+
+                if (!_storage.__jstorage_meta.TTL) {
+                    _storage.__jstorage_meta.TTL = {};
+                }
+
+                // Set TTL value for the key
+                if (ttl > 0) {
+                    _storage.__jstorage_meta.TTL[key] = curtime + ttl;
+                } else {
+                    delete _storage.__jstorage_meta.TTL[key];
+                }
+
+                _save();
+
+                _handleTTL();
+
+                _publishChange();
+                return true;
+            }
+            return false;
+        },
+
+        /**
+         * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
+         *
+         * @param {String} key Key to check
+         * @return {Number} Remaining TTL in milliseconds
+         */
+        getTTL: function(key) {
+            var curtime = +new Date(),
+                ttl;
+            _checkKey(key);
+            if (key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]) {
+                ttl = _storage.__jstorage_meta.TTL[key] - curtime;
+                return ttl || 0;
+            }
+            return 0;
+        },
+
+        /**
+         * Deletes everything in cache.
+         *
+         * @return {Boolean} Always true
+         */
+        flush: function() {
+            _storage = {
+                __jstorage_meta: {
+                    CRC32: {}
+                }
+            };
+            _save();
+            _publishChange();
+            _fireObservers(null, 'flushed');
+            return true;
+        },
+
+        /**
+         * Returns a read-only copy of _storage
+         *
+         * @return {Object} Read-only copy of _storage
+         */
+        storageObj: function() {
+            function F() {}
+            F.prototype = _storage;
+            return new F();
+        },
+
+        /**
+         * Returns an index of all used keys as an array
+         * ['key1', 'key2',..'keyN']
+         *
+         * @return {Array} Used keys
+         */
+        index: function() {
+            var index = [],
+                i;
+            for (i in _storage) {
+                if (_storage.hasOwnProperty(i) && i != '__jstorage_meta') {
+                    index.push(i);
+                }
+            }
+            return index;
+        },
+
+        /**
+         * How much space in bytes does the storage take?
+         *
+         * @return {Number} Storage size in chars (not the same as in bytes,
+         *                  since some chars may take several bytes)
+         */
+        storageSize: function() {
+            return _storage_size;
+        },
+
+        /**
+         * Which backend is currently in use?
+         *
+         * @return {String} Backend name
+         */
+        currentBackend: function() {
+            return _backend;
+        },
+
+        /**
+         * Test if storage is available
+         *
+         * @return {Boolean} True if storage can be used
+         */
+        storageAvailable: function() {
+            return !!_backend;
+        },
+
+        /**
+         * Register change listeners
+         *
+         * @param {String} key Key name
+         * @param {Function} callback Function to run when the key changes
+         */
+        listenKeyChange: function(key, callback) {
+            _checkKey(key);
+            if (!_observers[key]) {
+                _observers[key] = [];
+            }
+            _observers[key].push(callback);
+        },
+
+        /**
+         * Remove change listeners
+         *
+         * @param {String} key Key name to unregister listeners against
+         * @param {Function} [callback] If set, unregister the callback, if not - unregister all
+         */
+        stopListening: function(key, callback) {
+            _checkKey(key);
+
+            if (!_observers[key]) {
+                return;
+            }
+
+            if (!callback) {
+                delete _observers[key];
+                return;
+            }
+
+            for (var i = _observers[key].length - 1; i >= 0; i--) {
+                if (_observers[key][i] == callback) {
+                    _observers[key].splice(i, 1);
+                }
+            }
+        },
+
+        /**
+         * Subscribe to a Publish/Subscribe event stream
+         *
+         * @param {String} channel Channel name
+         * @param {Function} callback Function to run when the something is published to the channel
+         */
+        subscribe: function(channel, callback) {
+            channel = (channel || '').toString();
+            if (!channel) {
+                throw new TypeError('Channel not defined');
+            }
+            if (!_pubsub_observers[channel]) {
+                _pubsub_observers[channel] = [];
+            }
+            _pubsub_observers[channel].push(callback);
+        },
+
+        /**
+         * Publish data to an event stream
+         *
+         * @param {String} channel Channel name
+         * @param {Mixed} payload Payload to deliver
+         */
+        publish: function(channel, payload) {
+            channel = (channel || '').toString();
+            if (!channel) {
+                throw new TypeError('Channel not defined');
+            }
+
+            _publish(channel, payload);
+        },
+
+        /**
+         * Reloads the data from browser storage
+         */
+        reInit: function() {
+            _reloadData();
+        },
+
+        /**
+         * Removes reference from global objects and saves it as jStorage
+         *
+         * @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
+         */
+        noConflict: function(saveInGlobal) {
+            delete window.$.jStorage;
+
+            if (saveInGlobal) {
+                window.jStorage = this;
+            }
+
+            return this;
+        }
+    };
+
+    // Initialize jStorage
+    _init();
+
+})();
diff --git a/resources/lib/jquery.mockjax.js b/resources/lib/jquery.mockjax.js
new file mode 100644 (file)
index 0000000..5f6e130
--- /dev/null
@@ -0,0 +1,382 @@
+/*!
+ * MockJax - jQuery Plugin to Mock Ajax requests
+ *
+ * Version:  1.4.0
+ * Released: 2011-02-04
+ * Source:   http://github.com/appendto/jquery-mockjax
+ * Docs:     http://enterprisejquery.com/2010/07/mock-your-ajax-requests-with-mockjax-for-rapid-development
+ * Plugin:   mockjax
+ * Author:   Jonathan Sharp (http://jdsharp.com)
+ * License:  MIT,GPL
+ * 
+ * Copyright (c) 2010 appendTo LLC.
+ * Dual licensed under the MIT or GPL licenses.
+ * http://appendto.com/open-source-licenses
+ */
+(function($) {
+       var _ajax = $.ajax,
+               mockHandlers = [];
+       
+       function parseXML(xml) {
+               if ( window['DOMParser'] == undefined && window.ActiveXObject ) {
+                       DOMParser = function() { };
+                       DOMParser.prototype.parseFromString = function( xmlString ) {
+                               var doc = new ActiveXObject('Microsoft.XMLDOM');
+                       doc.async = 'false';
+                       doc.loadXML( xmlString );
+                               return doc;
+                       };
+               }
+               
+               try {
+                       var xmlDoc      = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
+                       if ( $.isXMLDoc( xmlDoc ) ) {
+                               var err = $('parsererror', xmlDoc);
+                               if ( err.length == 1 ) {
+                                       throw('Error: ' + $(xmlDoc).text() );
+                               }
+                       } else {
+                               throw('Unable to parse XML');
+                       }
+               } catch( e ) {
+                       var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
+                       $(document).trigger('xmlParseError', [ msg ]);
+                       return undefined;
+               }
+               return xmlDoc;
+       }
+       
+       $.extend({
+               ajax: function(origSettings) {
+                       var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings),
+                           mock = false;
+                       // Iterate over our mock handlers (in registration order) until we find
+                       // one that is willing to intercept the request
+                       $.each(mockHandlers, function(k, v) {
+                               if ( !mockHandlers[k] ) {
+                                       return;
+                               }
+                               var m = null;
+                               // If the mock was registered with a function, let the function decide if we 
+                               // want to mock this request
+                               if ( $.isFunction(mockHandlers[k]) ) {
+                                       m = mockHandlers[k](s);
+                               } else {
+                                       m = mockHandlers[k];
+                                       // Inspect the URL of the request and check if the mock handler's url 
+                                       // matches the url for this ajax request
+                                       if ( $.isFunction(m.url.test) ) {
+                                               // The user provided a regex for the url, test it
+                                               if ( !m.url.test( s.url ) ) {
+                                                       m = null;
+                                               }
+                                       } else {
+                                               // Look for a simple wildcard '*' or a direct URL match
+                                               var star = m.url.indexOf('*');
+                                               if ( ( m.url != '*' && m.url != s.url && star == -1 ) ||
+                                                       ( star > -1 && m.url.substr(0, star) != s.url.substr(0, star) ) ) {
+                                                        // The url we tested did not match the wildcard *
+                                                        m = null;
+                                               }
+                                       }
+                                       if ( m ) {
+                                               // Inspect the data submitted in the request (either POST body or GET query string)
+                                               if ( m.data && s.data ) {
+                                                       var identical = false;
+                                                       // Deep inspect the identity of the objects
+                                                       (function ident(mock, live) {
+                                                               // Test for situations where the data is a querystring (not an object)
+                                                               if (typeof live === 'string') {
+                                                                       // Querystring may be a regex
+                                                                       identical = $.isFunction( mock.test ) ? mock.test(live) : mock == live;
+                                                                       return identical;
+                                                               }
+                                                               $.each(mock, function(k, v) {
+                                                                       if ( live[k] === undefined ) {
+                                                                               identical = false;
+                                                                               return false;
+                                                                       } else {
+                                                                               identical = true;
+                                                                               if ( typeof live[k] == 'object' ) {
+                                                                                       return ident(mock[k], live[k]);
+                                                                               } else {
+                                                                                       if ( $.isFunction( mock[k].test ) ) {
+                                                                                               identical = mock[k].test(live[k]);
+                                                                                       } else {
+                                                                                               identical = ( mock[k] == live[k] );
+                                                                                       }
+                                                                                       return identical;
+                                                                               }
+                                                                       }
+                                                               });
+                                                       })(m.data, s.data);
+                                                       // They're not identical, do not mock this request
+                                                       if ( identical == false ) {
+                                                               m = null;
+                                                       }
+                                               }
+                                               // Inspect the request type
+                                               if ( m && m.type && m.type != s.type ) {
+                                                       // The request type doesn't match (GET vs. POST)
+                                                       m = null;
+                                               }
+                                       }
+                               }
+                               if ( m ) {
+                                       mock = true;
+
+                                       // Handle console logging
+                                       var c = $.extend({}, $.mockjaxSettings, m);
+                                       if ( c.log && $.isFunction(c.log) ) {
+                                               c.log('MOCK ' + s.type.toUpperCase() + ': ' + s.url, $.extend({}, s));
+                                       }
+                                       
+                                       var jsre = /=\?(&|$)/, jsc = (new Date()).getTime();
+
+                                       // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here
+                                       // because there isn't an easy hook for the cross domain script tag of jsonp
+                                       if ( s.dataType === "jsonp" ) {
+                                               if ( s.type.toUpperCase() === "GET" ) {
+                                                       if ( !jsre.test( s.url ) ) {
+                                                               s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
+                                                       }
+                                               } else if ( !s.data || !jsre.test(s.data) ) {
+                                                       s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
+                                               }
+                                               s.dataType = "json";
+                                       }
+                       
+                                       // Build temporary JSONP function
+                                       if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
+                                               jsonp = s.jsonpCallback || ("jsonp" + jsc++);
+                       
+                                               // Replace the =? sequence both in the query string and the data
+                                               if ( s.data ) {
+                                                       s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
+                                               }
+                       
+                                               s.url = s.url.replace(jsre, "=" + jsonp + "$1");
+                       
+                                               // We need to make sure
+                                               // that a JSONP style response is executed properly
+                                               s.dataType = "script";
+                       
+                                               // Handle JSONP-style loading
+                                               window[ jsonp ] = window[ jsonp ] || function( tmp ) {
+                                                       data = tmp;
+                                                       success();
+                                                       complete();
+                                                       // Garbage collect
+                                                       window[ jsonp ] = undefined;
+                       
+                                                       try {
+                                                               delete window[ jsonp ];
+                                                       } catch(e) {}
+                       
+                                                       if ( head ) {
+                                                               head.removeChild( script );
+                                                       }
+                                               };
+                                       }
+                                       
+                                       var rurl = /^(\w+:)?\/\/([^\/?#]+)/,
+                                               parts = rurl.exec( s.url ),
+                                               remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
+                                       
+                                       // Test if we are going to create a script tag (if so, intercept & mock)
+                                       if ( s.dataType === "script" && s.type.toUpperCase() === "GET" && remote ) {
+                                               // Synthesize the mock request for adding a script tag
+                                               var callbackContext = origSettings && origSettings.context || s;
+                                               
+                                               function success() {
+                                                       // If a local callback was specified, fire it and pass it the data
+                                                       if ( s.success ) {
+                                                               s.success.call( callbackContext, ( m.response ? m.response.toString() : m.responseText || ''), status, {} );
+                                                       }
+                               
+                                                       // Fire the global callback
+                                                       if ( s.global ) {
+                                                               trigger( "ajaxSuccess", [{}, s] );
+                                                       }
+                                               }
+                               
+                                               function complete() {
+                                                       // Process result
+                                                       if ( s.complete ) {
+                                                               s.complete.call( callbackContext, {} , status );
+                                                       }
+                               
+                                                       // The request was completed
+                                                       if ( s.global ) {
+                                                               trigger( "ajaxComplete", [{}, s] );
+                                                       }
+                               
+                                                       // Handle the global AJAX counter
+                                                       if ( s.global && ! --jQuery.active ) {
+                                                               jQuery.event.trigger( "ajaxStop" );
+                                                       }
+                                               }
+                                               
+                                               function trigger(type, args) {
+                                                       (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
+                                               }
+                                               
+                                               if ( m.response && $.isFunction(m.response) ) {
+                                                       m.response(origSettings);
+                                               } else {
+                                                       $.globalEval(m.responseText);
+                                               }
+                                               success();
+                                               complete();
+                                               return false;
+                                       }
+                                       mock = _ajax.call($, $.extend(true, {}, origSettings, {
+                                               // Mock the XHR object
+                                               xhr: function() {
+                                                       // Extend with our default mockjax settings
+                                                       m = $.extend({}, $.mockjaxSettings, m);
+
+                                                       if ( m.contentType ) {
+                                                               m.headers['content-type'] = m.contentType;
+                                                       }
+
+                                                       // Return our mock xhr object
+                                                       return {
+                                                               status: m.status,
+                                                               readyState: 1,
+                                                               open: function() { },
+                                                               send: function() {
+                                                                       // This is a substitute for < 1.4 which lacks $.proxy
+                                                                       var process = (function(that) {
+                                                                               return function() {
+                                                                                       return (function() {
+                                                                                               // The request has returned
+                                                                                               this.status             = m.status;
+                                                                                               this.readyState         = 4;
+                                                                               
+                                                                                               // We have an executable function, call it to give 
+                                                                                               // the mock handler a chance to update it's data
+                                                                                               if ( $.isFunction(m.response) ) {
+                                                                                                       m.response(origSettings);
+                                                                                               }
+                                                                                               // Copy over our mock to our xhr object before passing control back to 
+                                                                                               // jQuery's onreadystatechange callback
+                                                                                               if ( s.dataType == 'json' && ( typeof m.responseText == 'object' ) ) {
+                                                                                                       this.responseText = JSON.stringify(m.responseText);
+                                                                                               } else if ( s.dataType == 'xml' ) {
+                                                                                                       if ( typeof m.responseXML == 'string' ) {
+                                                                                                               this.responseXML = parseXML(m.responseXML);
+                                                                                                       } else {
+                                                                                                               this.responseXML = m.responseXML;
+                                                                                                       }
+                                                                                               } else {
+                                                                                                       this.responseText = m.responseText;
+                                                                                               }
+                                                                                               // jQuery < 1.4 doesn't have onreadystate change for xhr
+                                                                                               if ( $.isFunction(this.onreadystatechange) ) {
+                                                                                                       this.onreadystatechange( m.isTimeout ? 'timeout' : undefined );
+                                                                                               }
+                                                                                       }).apply(that);
+                                                                               };
+                                                                       })(this);
+
+                                                                       if ( m.proxy ) {
+                                                                               // We're proxying this request and loading in an external file instead
+                                                                               _ajax({
+                                                                                       global: false,
+                                                                                       url: m.proxy,
+                                                                                       type: m.proxyType,
+                                                                                       data: m.data,
+                                                                                       dataType: s.dataType,
+                                                                                       complete: function(xhr, txt) {
+                                                                                               m.responseXML = xhr.responseXML;
+                                                                                               m.responseText = xhr.responseText;
+                                                                                               this.responseTimer = setTimeout(process, m.responseTime || 0);
+                                                                                       }
+                                                                               });
+                                                                       } else {
+                                                                               // type == 'POST' || 'GET' || 'DELETE'
+                                                                               if ( s.async === false ) {
+                                                                                       // TODO: Blocking delay
+                                                                                       process();
+                                                                               } else {
+                                                                                       this.responseTimer = setTimeout(process, m.responseTime || 50);
+                                                                               }
+                                                                       }
+                                                               },
+                                                               abort: function() {
+                                                                       clearTimeout(this.responseTimer);
+                                                               },
+                                                               setRequestHeader: function() { },
+                                                               getResponseHeader: function(header) {
+                                                                       // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery
+                                                                       if ( m.headers && m.headers[header] ) {
+                                                                               // Return arbitrary headers
+                                                                               return m.headers[header];
+                                                                       } else if ( header.toLowerCase() == 'last-modified' ) {
+                                                                               return m.lastModified || (new Date()).toString();
+                                                                       } else if ( header.toLowerCase() == 'etag' ) {
+                                                                               return m.etag || '';
+                                                                       } else if ( header.toLowerCase() == 'content-type' ) {
+                                                                               return m.contentType || 'text/plain';
+                                                                       }
+                                                               },
+                                                               getAllResponseHeaders: function() {
+                                                                       var headers = '';
+                                                                       $.each(m.headers, function(k, v) {
+                                                                               headers += k + ': ' + v + "\n";
+                                                                       });
+                                                                       return headers;
+                                                               }
+                                                       };
+                                               }
+                                       }));
+                                       return false;
+                               }
+                       });
+                       // We don't have a mock request, trigger a normal request
+                       if ( !mock ) {
+                               return _ajax.apply($, arguments);
+                       } else {
+                               return mock;
+                       }
+               }
+       });
+
+       $.mockjaxSettings = {
+               //url:        null,
+               //type:       'GET',
+               log:          function(msg) {
+                               window['console'] && window.console.log && window.console.log(msg);
+                             },
+               status:       200,
+               responseTime: 500,
+               isTimeout:    false,
+               contentType:  'text/plain',
+               response:     '', 
+               responseText: '',
+               responseXML:  '',
+               proxy:        '',
+               proxyType:    'GET',
+               
+               lastModified: null,
+               etag:         '',
+               headers: {
+                       etag: 'IJF@H#@923uf8023hFO@I#H#',
+                       'content-type' : 'text/plain'
+               }
+       };
+
+       $.mockjax = function(settings) {
+               var i = mockHandlers.length;
+               mockHandlers[i] = settings;
+               return i;
+       };
+       $.mockjaxClear = function(i) {
+               if ( arguments.length == 1 ) {
+                       mockHandlers[i] = null;
+               } else {
+                       mockHandlers = [];
+               }
+       };
+})(jQuery);
diff --git a/resources/lib/jquery.xmldom.js b/resources/lib/jquery.xmldom.js
new file mode 100644 (file)
index 0000000..85d0083
--- /dev/null
@@ -0,0 +1,46 @@
+/*!
+ * jQuery xmlDOM Plugin v1.0
+ * http://outwestmedia.com/jquery-plugins/xmldom/
+ *
+ * Released: 2009-04-06
+ * Version: 1.0
+ *
+ * Copyright (c) 2009 Jonathan Sharp, Out West Media LLC.
+ * Dual licensed under the MIT and GPL licenses.
+ * http://docs.jquery.com/License
+ */
+(function($) {
+       // IE DOMParser wrapper
+       if ( window['DOMParser'] == undefined && window.ActiveXObject ) {
+               DOMParser = function() { };
+               DOMParser.prototype.parseFromString = function( xmlString ) {
+                       var doc = new ActiveXObject('Microsoft.XMLDOM');
+               doc.async = 'false';
+               doc.loadXML( xmlString );
+                       return doc;
+               };
+       }
+       
+       $.xmlDOM = function(xml, onErrorFn) {
+               try {
+                       var xmlDoc      = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
+                       if ( $.isXMLDoc( xmlDoc ) ) {
+                               var err = $('parsererror', xmlDoc);
+                               if ( err.length == 1 ) {
+                                       throw('Error: ' + $(xmlDoc).text() );
+                               }
+                       } else {
+                               throw('Unable to parse XML');
+                       }
+               } catch( e ) {
+                       var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
+                       if ( $.isFunction( onErrorFn ) ) {
+                               onErrorFn( msg );
+                       } else {
+                               $(document).trigger('xmlParseError', [ msg ]);
+                       }
+                       return $([]);
+               }
+               return $( xmlDoc );
+       };
+})(jQuery);
\ No newline at end of file
diff --git a/resources/lib/jquery/jquery.async.js b/resources/lib/jquery/jquery.async.js
deleted file mode 100644 (file)
index 2161f6b..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * jQuery Asynchronous Plugin 1.0
- *
- * Copyright (c) 2008 Vincent Robert (genezys.net)
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * and GPL (GPL-LICENSE.txt) licenses.
- *
- */
-(function($){
-
-// opts.delay : (default 10) delay between async call in ms
-// opts.bulk : (default 500) delay during which the loop can continue synchronously without yielding the CPU
-// opts.test : (default true) function to test in the while test part
-// opts.loop : (default empty) function to call in the while loop part
-// opts.end : (default empty) function to call at the end of the while loop
-$.whileAsync = function(opts) {
-       var delay = Math.abs(opts.delay) || 10,
-               bulk = isNaN(opts.bulk) ? 500 : Math.abs(opts.bulk),
-               test = opts.test || function(){ return true; },
-               loop = opts.loop || function(){},
-               end = opts.end || function(){};
-       
-       (function(){
-
-               var t = false,
-                       begin = new Date();
-                       
-               while( t = test() ) {
-                       loop();
-                       if( bulk === 0 || (new Date() - begin) > bulk ) {
-                               break;
-                       }
-               }
-               if( t ) {
-                       setTimeout(arguments.callee, delay);
-               }
-               else {
-                       end();
-               }
-               
-       })();
-};
-
-// opts.delay : (default 10) delay between async call in ms
-// opts.bulk : (default 500) delay during which the loop can continue synchronously without yielding the CPU
-// opts.loop : (default empty) function to call in the each loop part, signature: function(index, value) this = value
-// opts.end : (default empty) function to call at the end of the each loop
-$.eachAsync = function(array, opts) {
-       var     i = 0,
-               l = array.length,
-               loop = opts.loop || function(){};
-       
-       $.whileAsync(
-               $.extend(opts, {
-                       test: function() { return i < l; },
-                       loop: function() {
-                               var val = array[i];
-                               return loop.call(val, i++, val);
-                       }
-               })
-       );
-};
-
-$.fn.eachAsync = function(opts) {
-       $.eachAsync(this, opts);
-       return this;
-}
-
-})(jQuery);
\ No newline at end of file
diff --git a/resources/lib/jquery/jquery.ba-throttle-debounce.js b/resources/lib/jquery/jquery.ba-throttle-debounce.js
deleted file mode 100644 (file)
index fa30bdf..0000000
+++ /dev/null
@@ -1,252 +0,0 @@
-/*!
- * jQuery throttle / debounce - v1.1 - 3/7/2010
- * http://benalman.com/projects/jquery-throttle-debounce-plugin/
- * 
- * Copyright (c) 2010 "Cowboy" Ben Alman
- * Dual licensed under the MIT and GPL licenses.
- * http://benalman.com/about/license/
- */
-
-// Script: jQuery throttle / debounce: Sometimes, less is more!
-//
-// *Version: 1.1, Last updated: 3/7/2010*
-// 
-// Project Home - http://benalman.com/projects/jquery-throttle-debounce-plugin/
-// GitHub       - http://github.com/cowboy/jquery-throttle-debounce/
-// Source       - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.js
-// (Minified)   - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.min.js (0.7kb)
-// 
-// About: License
-// 
-// Copyright (c) 2010 "Cowboy" Ben Alman,
-// Dual licensed under the MIT and GPL licenses.
-// http://benalman.com/about/license/
-// 
-// About: Examples
-// 
-// These working examples, complete with fully commented code, illustrate a few
-// ways in which this plugin can be used.
-// 
-// Throttle - http://benalman.com/code/projects/jquery-throttle-debounce/examples/throttle/
-// Debounce - http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
-// 
-// About: Support and Testing
-// 
-// Information about what version or versions of jQuery this plugin has been
-// tested with, what browsers it has been tested in, and where the unit tests
-// reside (so you can test it yourself).
-// 
-// jQuery Versions - none, 1.3.2, 1.4.2
-// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome 4-5, Opera 9.6-10.1.
-// Unit Tests      - http://benalman.com/code/projects/jquery-throttle-debounce/unit/
-// 
-// About: Release History
-// 
-// 1.1 - (3/7/2010) Fixed a bug in <jQuery.throttle> where trailing callbacks
-//       executed later than they should. Reworked a fair amount of internal
-//       logic as well.
-// 1.0 - (3/6/2010) Initial release as a stand-alone project. Migrated over
-//       from jquery-misc repo v0.4 to jquery-throttle repo v1.0, added the
-//       no_trailing throttle parameter and debounce functionality.
-// 
-// Topic: Note for non-jQuery users
-// 
-// jQuery isn't actually required for this plugin, because nothing internal
-// uses any jQuery methods or properties. jQuery is just used as a namespace
-// under which these methods can exist.
-// 
-// Since jQuery isn't actually required for this plugin, if jQuery doesn't exist
-// when this plugin is loaded, the method described below will be created in
-// the `Cowboy` namespace. Usage will be exactly the same, but instead of
-// $.method() or jQuery.method(), you'll need to use Cowboy.method().
-
-(function(window,undefined){
-  '$:nomunge'; // Used by YUI compressor.
-  
-  // Since jQuery really isn't required for this plugin, use `jQuery` as the
-  // namespace only if it already exists, otherwise use the `Cowboy` namespace,
-  // creating it if necessary.
-  var $ = window.jQuery || window.Cowboy || ( window.Cowboy = {} ),
-    
-    // Internal method reference.
-    jq_throttle;
-  
-  // Method: jQuery.throttle
-  // 
-  // Throttle execution of a function. Especially useful for rate limiting
-  // execution of handlers on events like resize and scroll. If you want to
-  // rate-limit execution of a function to a single time, see the
-  // <jQuery.debounce> method.
-  // 
-  // In this visualization, | is a throttled-function call and X is the actual
-  // callback execution:
-  // 
-  // > Throttled with `no_trailing` specified as false or unspecified:
-  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
-  // > X    X    X    X    X    X        X    X    X    X    X    X
-  // > 
-  // > Throttled with `no_trailing` specified as true:
-  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
-  // > X    X    X    X    X             X    X    X    X    X
-  // 
-  // Usage:
-  // 
-  // > var throttled = jQuery.throttle( delay, [ no_trailing, ] callback );
-  // > 
-  // > jQuery('selector').bind( 'someevent', throttled );
-  // > jQuery('selector').unbind( 'someevent', throttled );
-  // 
-  // This also works in jQuery 1.4+:
-  // 
-  // > jQuery('selector').bind( 'someevent', jQuery.throttle( delay, [ no_trailing, ] callback ) );
-  // > jQuery('selector').unbind( 'someevent', callback );
-  // 
-  // Arguments:
-  // 
-  //  delay - (Number) A zero-or-greater delay in milliseconds. For event
-  //    callbacks, values around 100 or 250 (or even higher) are most useful.
-  //  no_trailing - (Boolean) Optional, defaults to false. If no_trailing is
-  //    true, callback will only execute every `delay` milliseconds while the
-  //    throttled-function is being called. If no_trailing is false or
-  //    unspecified, callback will be executed one final time after the last
-  //    throttled-function call. (After the throttled-function has not been
-  //    called for `delay` milliseconds, the internal counter is reset)
-  //  callback - (Function) A function to be executed after delay milliseconds.
-  //    The `this` context and all arguments are passed through, as-is, to
-  //    `callback` when the throttled-function is executed.
-  // 
-  // Returns:
-  // 
-  //  (Function) A new, throttled, function.
-  
-  $.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) {
-    // After wrapper has stopped being called, this timeout ensures that
-    // `callback` is executed at the proper times in `throttle` and `end`
-    // debounce modes.
-    var timeout_id,
-      
-      // Keep track of the last time `callback` was executed.
-      last_exec = 0;
-    
-    // `no_trailing` defaults to falsy.
-    if ( typeof no_trailing !== 'boolean' ) {
-      debounce_mode = callback;
-      callback = no_trailing;
-      no_trailing = undefined;
-    }
-    
-    // The `wrapper` function encapsulates all of the throttling / debouncing
-    // functionality and when executed will limit the rate at which `callback`
-    // is executed.
-    function wrapper() {
-      var that = this,
-        elapsed = +new Date() - last_exec,
-        args = arguments;
-      
-      // Execute `callback` and update the `last_exec` timestamp.
-      function exec() {
-        last_exec = +new Date();
-        callback.apply( that, args );
-      };
-      
-      // If `debounce_mode` is true (at_begin) this is used to clear the flag
-      // to allow future `callback` executions.
-      function clear() {
-        timeout_id = undefined;
-      };
-      
-      if ( debounce_mode && !timeout_id ) {
-        // Since `wrapper` is being called for the first time and
-        // `debounce_mode` is true (at_begin), execute `callback`.
-        exec();
-      }
-      
-      // Clear any existing timeout.
-      timeout_id && clearTimeout( timeout_id );
-      
-      if ( debounce_mode === undefined && elapsed > delay ) {
-        // In throttle mode, if `delay` time has been exceeded, execute
-        // `callback`.
-        exec();
-        
-      } else if ( no_trailing !== true ) {
-        // In trailing throttle mode, since `delay` time has not been
-        // exceeded, schedule `callback` to execute `delay` ms after most
-        // recent execution.
-        // 
-        // If `debounce_mode` is true (at_begin), schedule `clear` to execute
-        // after `delay` ms.
-        // 
-        // If `debounce_mode` is false (at end), schedule `callback` to
-        // execute after `delay` ms.
-        timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay );
-      }
-    };
-    
-    // Set the guid of `wrapper` function to the same of original callback, so
-    // it can be removed in jQuery 1.4+ .unbind or .die by using the original
-    // callback as a reference.
-    if ( $.guid ) {
-      wrapper.guid = callback.guid = callback.guid || $.guid++;
-    }
-    
-    // Return the wrapper function.
-    return wrapper;
-  };
-  
-  // Method: jQuery.debounce
-  // 
-  // Debounce execution of a function. Debouncing, unlike throttling,
-  // guarantees that a function is only executed a single time, either at the
-  // very beginning of a series of calls, or at the very end. If you want to
-  // simply rate-limit execution of a function, see the <jQuery.throttle>
-  // method.
-  // 
-  // In this visualization, | is a debounced-function call and X is the actual
-  // callback execution:
-  // 
-  // > Debounced with `at_begin` specified as false or unspecified:
-  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
-  // >                          X                                 X
-  // > 
-  // > Debounced with `at_begin` specified as true:
-  // > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
-  // > X                                 X
-  // 
-  // Usage:
-  // 
-  // > var debounced = jQuery.debounce( delay, [ at_begin, ] callback );
-  // > 
-  // > jQuery('selector').bind( 'someevent', debounced );
-  // > jQuery('selector').unbind( 'someevent', debounced );
-  // 
-  // This also works in jQuery 1.4+:
-  // 
-  // > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) );
-  // > jQuery('selector').unbind( 'someevent', callback );
-  // 
-  // Arguments:
-  // 
-  //  delay - (Number) A zero-or-greater delay in milliseconds. For event
-  //    callbacks, values around 100 or 250 (or even higher) are most useful.
-  //  at_begin - (Boolean) Optional, defaults to false. If at_begin is false or
-  //    unspecified, callback will only be executed `delay` milliseconds after
-  //    the last debounced-function call. If at_begin is true, callback will be
-  //    executed only at the first debounced-function call. (After the
-  //    throttled-function has not been called for `delay` milliseconds, the
-  //    internal counter is reset)
-  //  callback - (Function) A function to be executed after delay milliseconds.
-  //    The `this` context and all arguments are passed through, as-is, to
-  //    `callback` when the debounced-function is executed.
-  // 
-  // Returns:
-  // 
-  //  (Function) A new, debounced, function.
-  
-  $.debounce = function( delay, at_begin, callback ) {
-    return callback === undefined
-      ? jq_throttle( delay, at_begin, false )
-      : jq_throttle( delay, callback, at_begin !== false );
-  };
-  
-})(this);
diff --git a/resources/lib/jquery/jquery.cookie.js b/resources/lib/jquery/jquery.cookie.js
deleted file mode 100644 (file)
index 3fb201c..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-/*!
- * jQuery Cookie Plugin v1.3.1
- * https://github.com/carhartl/jquery-cookie
- *
- * Copyright 2013 Klaus Hartl
- * Released under the MIT license
- */
-(function ($, document, undefined) {
-
-       var pluses = /\+/g;
-
-       function raw(s) {
-               return s;
-       }
-
-       function decoded(s) {
-               return unRfc2068(decodeURIComponent(s.replace(pluses, ' ')));
-       }
-
-       function unRfc2068(value) {
-               if (value.indexOf('"') === 0) {
-                       // This is a quoted cookie as according to RFC2068, unescape
-                       value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
-               }
-               return value;
-       }
-
-       function fromJSON(value) {
-               return config.json ? JSON.parse(value) : value;
-       }
-
-       var config = $.cookie = function (key, value, options) {
-
-               // write
-               if (value !== undefined) {
-                       options = $.extend({}, config.defaults, options);
-
-                       if (value === null) {
-                               options.expires = -1;
-                       }
-
-                       if (typeof options.expires === 'number') {
-                               var days = options.expires, t = options.expires = new Date();
-                               t.setDate(t.getDate() + days);
-                       }
-
-                       value = config.json ? JSON.stringify(value) : String(value);
-
-                       return (document.cookie = [
-                               encodeURIComponent(key), '=', config.raw ? value : encodeURIComponent(value),
-                               options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
-                               options.path    ? '; path=' + options.path : '',
-                               options.domain  ? '; domain=' + options.domain : '',
-                               options.secure  ? '; secure' : ''
-                       ].join(''));
-               }
-
-               // read
-               var decode = config.raw ? raw : decoded;
-               var cookies = document.cookie.split('; ');
-               var result = key ? null : {};
-               for (var i = 0, l = cookies.length; i < l; i++) {
-                       var parts = cookies[i].split('=');
-                       var name = decode(parts.shift());
-                       var cookie = decode(parts.join('='));
-
-                       if (key && key === name) {
-                               result = fromJSON(cookie);
-                               break;
-                       }
-
-                       if (!key) {
-                               result[name] = fromJSON(cookie);
-                       }
-               }
-
-               return result;
-       };
-
-       config.defaults = {};
-
-       $.removeCookie = function (key, options) {
-               if ($.cookie(key) !== null) {
-                       $.cookie(key, null, options);
-                       return true;
-               }
-               return false;
-       };
-
-})(jQuery, document);
diff --git a/resources/lib/jquery/jquery.form.js b/resources/lib/jquery/jquery.form.js
deleted file mode 100644 (file)
index 13e9a55..0000000
+++ /dev/null
@@ -1,1089 +0,0 @@
-/*!
- * jQuery Form Plugin
- * version: 3.14 (30-JUL-2012)
- * @requires jQuery v1.3.2 or later
- *
- * Examples and documentation at: http://malsup.com/jquery/form/
- * Project repository: https://github.com/malsup/form
- * Dual licensed under the MIT and GPL licenses:
- *    http://malsup.github.com/mit-license.txt
- *    http://malsup.github.com/gpl-license-v2.txt
- */
-/*global ActiveXObject alert */
-;(function($) {
-"use strict";
-
-/*
-    Usage Note:
-    -----------
-    Do not use both ajaxSubmit and ajaxForm on the same form.  These
-    functions are mutually exclusive.  Use ajaxSubmit if you want
-    to bind your own submit handler to the form.  For example,
-
-    $(document).ready(function() {
-        $('#myForm').on('submit', function(e) {
-            e.preventDefault(); // <-- important
-            $(this).ajaxSubmit({
-                target: '#output'
-            });
-        });
-    });
-
-    Use ajaxForm when you want the plugin to manage all the event binding
-    for you.  For example,
-
-    $(document).ready(function() {
-        $('#myForm').ajaxForm({
-            target: '#output'
-        });
-    });
-    
-    You can also use ajaxForm with delegation (requires jQuery v1.7+), so the
-    form does not have to exist when you invoke ajaxForm:
-
-    $('#myForm').ajaxForm({
-        delegation: true,
-        target: '#output'
-    });
-    
-    When using ajaxForm, the ajaxSubmit function will be invoked for you
-    at the appropriate time.
-*/
-
-/**
- * Feature detection
- */
-var feature = {};
-feature.fileapi = $("<input type='file'/>").get(0).files !== undefined;
-feature.formdata = window.FormData !== undefined;
-
-/**
- * ajaxSubmit() provides a mechanism for immediately submitting
- * an HTML form using AJAX.
- */
-$.fn.ajaxSubmit = function(options) {
-    /*jshint scripturl:true */
-
-    // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
-    if (!this.length) {
-        log('ajaxSubmit: skipping submit process - no element selected');
-        return this;
-    }
-    
-    var method, action, url, $form = this;
-
-    if (typeof options == 'function') {
-        options = { success: options };
-    }
-
-    method = this.attr('method');
-    action = this.attr('action');
-    url = (typeof action === 'string') ? $.trim(action) : '';
-    url = url || window.location.href || '';
-    if (url) {
-        // clean url (don't include hash vaue)
-        url = (url.match(/^([^#]+)/)||[])[1];
-    }
-
-    options = $.extend(true, {
-        url:  url,
-        success: $.ajaxSettings.success,
-        type: method || 'GET',
-        iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
-    }, options);
-
-    // hook for manipulating the form data before it is extracted;
-    // convenient for use with rich editors like tinyMCE or FCKEditor
-    var veto = {};
-    this.trigger('form-pre-serialize', [this, options, veto]);
-    if (veto.veto) {
-        log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
-        return this;
-    }
-
-    // provide opportunity to alter form data before it is serialized
-    if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
-        log('ajaxSubmit: submit aborted via beforeSerialize callback');
-        return this;
-    }
-
-    var traditional = options.traditional;
-    if ( traditional === undefined ) {
-        traditional = $.ajaxSettings.traditional;
-    }
-    
-    var elements = [];
-    var qx, a = this.formToArray(options.semantic, elements);
-    if (options.data) {
-        options.extraData = options.data;
-        qx = $.param(options.data, traditional);
-    }
-
-    // give pre-submit callback an opportunity to abort the submit
-    if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
-        log('ajaxSubmit: submit aborted via beforeSubmit callback');
-        return this;
-    }
-
-    // fire vetoable 'validate' event
-    this.trigger('form-submit-validate', [a, this, options, veto]);
-    if (veto.veto) {
-        log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
-        return this;
-    }
-
-    var q = $.param(a, traditional);
-    if (qx) {
-        q = ( q ? (q + '&' + qx) : qx );
-    }    
-    if (options.type.toUpperCase() == 'GET') {
-        options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
-        options.data = null;  // data is null for 'get'
-    }
-    else {
-        options.data = q; // data is the query string for 'post'
-    }
-
-    var callbacks = [];
-    if (options.resetForm) {
-        callbacks.push(function() { $form.resetForm(); });
-    }
-    if (options.clearForm) {
-        callbacks.push(function() { $form.clearForm(options.includeHidden); });
-    }
-
-    // perform a load on the target only if dataType is not provided
-    if (!options.dataType && options.target) {
-        var oldSuccess = options.success || function(){};
-        callbacks.push(function(data) {
-            var fn = options.replaceTarget ? 'replaceWith' : 'html';
-            $(options.target)[fn](data).each(oldSuccess, arguments);
-        });
-    }
-    else if (options.success) {
-        callbacks.push(options.success);
-    }
-
-    options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
-        var context = options.context || this ;    // jQuery 1.4+ supports scope context 
-        for (var i=0, max=callbacks.length; i < max; i++) {
-            callbacks[i].apply(context, [data, status, xhr || $form, $form]);
-        }
-    };
-
-    // are there files to upload?
-    var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113)
-    var hasFileInputs = fileInputs.length > 0;
-    var mp = 'multipart/form-data';
-    var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
-
-    var fileAPI = feature.fileapi && feature.formdata;
-    log("fileAPI :" + fileAPI);
-    var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
-
-    // options.iframe allows user to force iframe mode
-    // 06-NOV-09: now defaulting to iframe mode if file input is detected
-    if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
-        // hack to fix Safari hang (thanks to Tim Molendijk for this)
-        // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
-        if (options.closeKeepAlive) {
-            $.get(options.closeKeepAlive, function() {
-                fileUploadIframe(a);
-            });
-        }
-          else {
-            fileUploadIframe(a);
-          }
-    }
-    else if ((hasFileInputs || multipart) && fileAPI) {
-        fileUploadXhr(a);
-    }
-    else {
-        $.ajax(options);
-    }
-
-    // clear element array
-    for (var k=0; k < elements.length; k++)
-        elements[k] = null;
-
-    // fire 'notify' event
-    this.trigger('form-submit-notify', [this, options]);
-    return this;
-
-     // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
-    function fileUploadXhr(a) {
-        var formdata = new FormData();
-
-        for (var i=0; i < a.length; i++) {
-            formdata.append(a[i].name, a[i].value);
-        }
-
-        if (options.extraData) {
-            for (var p in options.extraData)
-                if (options.extraData.hasOwnProperty(p))
-                    formdata.append(p, options.extraData[p]);
-        }
-
-        options.data = null;
-
-        var s = $.extend(true, {}, $.ajaxSettings, options, {
-            contentType: false,
-            processData: false,
-            cache: false,
-            type: 'POST'
-        });
-        
-        if (options.uploadProgress) {
-            // workaround because jqXHR does not expose upload property
-            s.xhr = function() {
-                var xhr = jQuery.ajaxSettings.xhr();
-                if (xhr.upload) {
-                    xhr.upload.onprogress = function(event) {
-                        var percent = 0;
-                        var position = event.loaded || event.position; /*event.position is deprecated*/
-                        var total = event.total;
-                        if (event.lengthComputable) {
-                            percent = Math.ceil(position / total * 100);
-                        }
-                        options.uploadProgress(event, position, total, percent);
-                    };
-                }
-                return xhr;
-            };
-        }
-
-        s.data = null;
-            var beforeSend = s.beforeSend;
-            s.beforeSend = function(xhr, o) {
-                o.data = formdata;
-                if(beforeSend)
-                    beforeSend.call(this, xhr, o);
-        };
-        $.ajax(s);
-    }
-
-    // private function for handling file uploads (hat tip to YAHOO!)
-    function fileUploadIframe(a) {
-        var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
-        var useProp = !!$.fn.prop;
-
-        if ($(':input[name=submit],:input[id=submit]', form).length) {
-            // if there is an input with a name or id of 'submit' then we won't be
-            // able to invoke the submit fn on the form (at least not x-browser)
-            alert('Error: Form elements must not have name or id of "submit".');
-            return;
-        }
-        
-        if (a) {
-            // ensure that every serialized input is still enabled
-            for (i=0; i < elements.length; i++) {
-                el = $(elements[i]);
-                if ( useProp )
-                    el.prop('disabled', false);
-                else
-                    el.removeAttr('disabled');
-            }
-        }
-
-        s = $.extend(true, {}, $.ajaxSettings, options);
-        s.context = s.context || s;
-        id = 'jqFormIO' + (new Date().getTime());
-        if (s.iframeTarget) {
-            $io = $(s.iframeTarget);
-            n = $io.attr('name');
-            if (!n)
-                 $io.attr('name', id);
-            else
-                id = n;
-        }
-        else {
-            $io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
-            $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
-        }
-        io = $io[0];
-
-
-        xhr = { // mock object
-            aborted: 0,
-            responseText: null,
-            responseXML: null,
-            status: 0,
-            statusText: 'n/a',
-            getAllResponseHeaders: function() {},
-            getResponseHeader: function() {},
-            setRequestHeader: function() {},
-            abort: function(status) {
-                var e = (status === 'timeout' ? 'timeout' : 'aborted');
-                log('aborting upload... ' + e);
-                this.aborted = 1;
-                // #214
-                if (io.contentWindow.document.execCommand) {
-                    try { // #214
-                        io.contentWindow.document.execCommand('Stop');
-                    } catch(ignore) {}
-                }
-                $io.attr('src', s.iframeSrc); // abort op in progress
-                xhr.error = e;
-                if (s.error)
-                    s.error.call(s.context, xhr, e, status);
-                if (g)
-                    $.event.trigger("ajaxError", [xhr, s, e]);
-                if (s.complete)
-                    s.complete.call(s.context, xhr, e);
-            }
-        };
-
-        g = s.global;
-        // trigger ajax global events so that activity/block indicators work like normal
-        if (g && 0 === $.active++) {
-            $.event.trigger("ajaxStart");
-        }
-        if (g) {
-            $.event.trigger("ajaxSend", [xhr, s]);
-        }
-
-        if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
-            if (s.global) {
-                $.active--;
-            }
-            return;
-        }
-        if (xhr.aborted) {
-            return;
-        }
-
-        // add submitting element to data if we know it
-        sub = form.clk;
-        if (sub) {
-            n = sub.name;
-            if (n && !sub.disabled) {
-                s.extraData = s.extraData || {};
-                s.extraData[n] = sub.value;
-                if (sub.type == "image") {
-                    s.extraData[n+'.x'] = form.clk_x;
-                    s.extraData[n+'.y'] = form.clk_y;
-                }
-            }
-        }
-        
-        var CLIENT_TIMEOUT_ABORT = 1;
-        var SERVER_ABORT = 2;
-
-        function getDoc(frame) {
-            var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
-            return doc;
-        }
-        
-        // Rails CSRF hack (thanks to Yvan Barthelemy)
-        var csrf_token = $('meta[name=csrf-token]').attr('content');
-        var csrf_param = $('meta[name=csrf-param]').attr('content');
-        if (csrf_param && csrf_token) {
-            s.extraData = s.extraData || {};
-            s.extraData[csrf_param] = csrf_token;
-        }
-
-        // take a breath so that pending repaints get some cpu time before the upload starts
-        function doSubmit() {
-            // make sure form attrs are set
-            var t = $form.attr('target'), a = $form.attr('action');
-
-            // update form attrs in IE friendly way
-            form.setAttribute('target',id);
-            if (!method) {
-                form.setAttribute('method', 'POST');
-            }
-            if (a != s.url) {
-                form.setAttribute('action', s.url);
-            }
-
-            // ie borks in some cases when setting encoding
-            if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
-                $form.attr({
-                    encoding: 'multipart/form-data',
-                    enctype:  'multipart/form-data'
-                });
-            }
-
-            // support timout
-            if (s.timeout) {
-                timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
-            }
-            
-            // look for server aborts
-            function checkState() {
-                try {
-                    var state = getDoc(io).readyState;
-                    log('state = ' + state);
-                    if (state && state.toLowerCase() == 'uninitialized')
-                        setTimeout(checkState,50);
-                }
-                catch(e) {
-                    log('Server abort: ' , e, ' (', e.name, ')');
-                    cb(SERVER_ABORT);
-                    if (timeoutHandle)
-                        clearTimeout(timeoutHandle);
-                    timeoutHandle = undefined;
-                }
-            }
-
-            // add "extra" data to form if provided in options
-            var extraInputs = [];
-            try {
-                if (s.extraData) {
-                    for (var n in s.extraData) {
-                        if (s.extraData.hasOwnProperty(n)) {
-                           // if using the $.param format that allows for multiple values with the same name
-                           if($.isPlainObject(s.extraData[n]) && s.extraData[n].hasOwnProperty('name') && s.extraData[n].hasOwnProperty('value')) {
-                               extraInputs.push(
-                               $('<input type="hidden" name="'+s.extraData[n].name+'">').attr('value',s.extraData[n].value)
-                                   .appendTo(form)[0]);
-                           } else {
-                               extraInputs.push(
-                               $('<input type="hidden" name="'+n+'">').attr('value',s.extraData[n])
-                                   .appendTo(form)[0]);
-                           }
-                        }
-                    }
-                }
-
-                if (!s.iframeTarget) {
-                    // add iframe to doc and submit the form
-                    $io.appendTo('body');
-                    if (io.attachEvent)
-                        io.attachEvent('onload', cb);
-                    else
-                        io.addEventListener('load', cb, false);
-                }
-                setTimeout(checkState,15);
-                form.submit();
-            }
-            finally {
-                // reset attrs and remove "extra" input elements
-                form.setAttribute('action',a);
-                if(t) {
-                    form.setAttribute('target', t);
-                } else {
-                    $form.removeAttr('target');
-                }
-                $(extraInputs).remove();
-            }
-        }
-
-        if (s.forceSync) {
-            doSubmit();
-        }
-        else {
-            setTimeout(doSubmit, 10); // this lets dom updates render
-        }
-
-        var data, doc, domCheckCount = 50, callbackProcessed;
-
-        function cb(e) {
-            if (xhr.aborted || callbackProcessed) {
-                return;
-            }
-            try {
-                doc = getDoc(io);
-            }
-            catch(ex) {
-                log('cannot access response document: ', ex);
-                e = SERVER_ABORT;
-            }
-            if (e === CLIENT_TIMEOUT_ABORT && xhr) {
-                xhr.abort('timeout');
-                return;
-            }
-            else if (e == SERVER_ABORT && xhr) {
-                xhr.abort('server abort');
-                return;
-            }
-
-            if (!doc || doc.location.href == s.iframeSrc) {
-                // response not received yet
-                if (!timedOut)
-                    return;
-            }
-            if (io.detachEvent)
-                io.detachEvent('onload', cb);
-            else    
-                io.removeEventListener('load', cb, false);
-
-            var status = 'success', errMsg;
-            try {
-                if (timedOut) {
-                    throw 'timeout';
-                }
-
-                var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
-                log('isXml='+isXml);
-                if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) {
-                    if (--domCheckCount) {
-                        // in some browsers (Opera) the iframe DOM is not always traversable when
-                        // the onload callback fires, so we loop a bit to accommodate
-                        log('requeing onLoad callback, DOM not available');
-                        setTimeout(cb, 250);
-                        return;
-                    }
-                    // let this fall through because server response could be an empty document
-                    //log('Could not access iframe DOM after mutiple tries.');
-                    //throw 'DOMException: not available';
-                }
-
-                //log('response detected');
-                var docRoot = doc.body ? doc.body : doc.documentElement;
-                xhr.responseText = docRoot ? docRoot.innerHTML : null;
-                xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
-                if (isXml)
-                    s.dataType = 'xml';
-                xhr.getResponseHeader = function(header){
-                    var headers = {'content-type': s.dataType};
-                    return headers[header];
-                };
-                // support for XHR 'status' & 'statusText' emulation :
-                if (docRoot) {
-                    xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
-                    xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
-                }
-
-                var dt = (s.dataType || '').toLowerCase();
-                var scr = /(json|script|text)/.test(dt);
-                if (scr || s.textarea) {
-                    // see if user embedded response in textarea
-                    var ta = doc.getElementsByTagName('textarea')[0];
-                    if (ta) {
-                        xhr.responseText = ta.value;
-                        // support for XHR 'status' & 'statusText' emulation :
-                        xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
-                        xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
-                    }
-                    else if (scr) {
-                        // account for browsers injecting pre around json response
-                        var pre = doc.getElementsByTagName('pre')[0];
-                        var b = doc.getElementsByTagName('body')[0];
-                        if (pre) {
-                            xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
-                        }
-                        else if (b) {
-                            xhr.responseText = b.textContent ? b.textContent : b.innerText;
-                        }
-                    }
-                }
-                else if (dt == 'xml' && !xhr.responseXML && xhr.responseText) {
-                    xhr.responseXML = toXml(xhr.responseText);
-                }
-
-                try {
-                    data = httpData(xhr, dt, s);
-                }
-                catch (e) {
-                    status = 'parsererror';
-                    xhr.error = errMsg = (e || status);
-                }
-            }
-            catch (e) {
-                log('error caught: ',e);
-                status = 'error';
-                xhr.error = errMsg = (e || status);
-            }
-
-            if (xhr.aborted) {
-                log('upload aborted');
-                status = null;
-            }
-
-            if (xhr.status) { // we've set xhr.status
-                status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
-            }
-
-            // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
-            if (status === 'success') {
-                if (s.success)
-                    s.success.call(s.context, data, 'success', xhr);
-                if (g)
-                    $.event.trigger("ajaxSuccess", [xhr, s]);
-            }
-            else if (status) {
-                if (errMsg === undefined)
-                    errMsg = xhr.statusText;
-                if (s.error)
-                    s.error.call(s.context, xhr, status, errMsg);
-                if (g)
-                    $.event.trigger("ajaxError", [xhr, s, errMsg]);
-            }
-
-            if (g)
-                $.event.trigger("ajaxComplete", [xhr, s]);
-
-            if (g && ! --$.active) {
-                $.event.trigger("ajaxStop");
-            }
-
-            if (s.complete)
-                s.complete.call(s.context, xhr, status);
-
-            callbackProcessed = true;
-            if (s.timeout)
-                clearTimeout(timeoutHandle);
-
-            // clean up
-            setTimeout(function() {
-                if (!s.iframeTarget)
-                    $io.remove();
-                xhr.responseXML = null;
-            }, 100);
-        }
-
-        var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
-            if (window.ActiveXObject) {
-                doc = new ActiveXObject('Microsoft.XMLDOM');
-                doc.async = 'false';
-                doc.loadXML(s);
-            }
-            else {
-                doc = (new DOMParser()).parseFromString(s, 'text/xml');
-            }
-            return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
-        };
-        var parseJSON = $.parseJSON || function(s) {
-            /*jslint evil:true */
-            return window['eval']('(' + s + ')');
-        };
-
-        var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
-
-            var ct = xhr.getResponseHeader('content-type') || '',
-                xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
-                data = xml ? xhr.responseXML : xhr.responseText;
-
-            if (xml && data.documentElement.nodeName === 'parsererror') {
-                if ($.error)
-                    $.error('parsererror');
-            }
-            if (s && s.dataFilter) {
-                data = s.dataFilter(data, type);
-            }
-            if (typeof data === 'string') {
-                if (type === 'json' || !type && ct.indexOf('json') >= 0) {
-                    data = parseJSON(data);
-                } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
-                    $.globalEval(data);
-                }
-            }
-            return data;
-        };
-    }
-};
-
-/**
- * ajaxForm() provides a mechanism for fully automating form submission.
- *
- * The advantages of using this method instead of ajaxSubmit() are:
- *
- * 1: This method will include coordinates for <input type="image" /> elements (if the element
- *    is used to submit the form).
- * 2. This method will include the submit element's name/value data (for the element that was
- *    used to submit the form).
- * 3. This method binds the submit() method to the form for you.
- *
- * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
- * passes the options argument along after properly binding events for submit elements and
- * the form itself.
- */
-$.fn.ajaxForm = function(options) {
-    options = options || {};
-    options.delegation = options.delegation && $.isFunction($.fn.on);
-    
-    // in jQuery 1.3+ we can fix mistakes with the ready state
-    if (!options.delegation && this.length === 0) {
-        var o = { s: this.selector, c: this.context };
-        if (!$.isReady && o.s) {
-            log('DOM not ready, queuing ajaxForm');
-            $(function() {
-                $(o.s,o.c).ajaxForm(options);
-            });
-            return this;
-        }
-        // is your DOM ready?  http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
-        log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
-        return this;
-    }
-
-    if ( options.delegation ) {
-        $(document)
-            .off('submit.form-plugin', this.selector, doAjaxSubmit)
-            .off('click.form-plugin', this.selector, captureSubmittingElement)
-            .on('submit.form-plugin', this.selector, options, doAjaxSubmit)
-            .on('click.form-plugin', this.selector, options, captureSubmittingElement);
-        return this;
-    }
-
-    return this.ajaxFormUnbind()
-        .bind('submit.form-plugin', options, doAjaxSubmit)
-        .bind('click.form-plugin', options, captureSubmittingElement);
-};
-
-// private event handlers    
-function doAjaxSubmit(e) {
-    /*jshint validthis:true */
-    var options = e.data;
-    if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
-        e.preventDefault();
-        $(this).ajaxSubmit(options);
-    }
-}
-    
-function captureSubmittingElement(e) {
-    /*jshint validthis:true */
-    var target = e.target;
-    var $el = $(target);
-    if (!($el.is(":submit,input:image"))) {
-        // is this a child element of the submit el?  (ex: a span within a button)
-        var t = $el.closest(':submit');
-        if (t.length === 0) {
-            return;
-        }
-        target = t[0];
-    }
-    var form = this;
-    form.clk = target;
-    if (target.type == 'image') {
-        if (e.offsetX !== undefined) {
-            form.clk_x = e.offsetX;
-            form.clk_y = e.offsetY;
-        } else if (typeof $.fn.offset == 'function') {
-            var offset = $el.offset();
-            form.clk_x = e.pageX - offset.left;
-            form.clk_y = e.pageY - offset.top;
-        } else {
-            form.clk_x = e.pageX - target.offsetLeft;
-            form.clk_y = e.pageY - target.offsetTop;
-        }
-    }
-    // clear form vars
-    setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
-}
-
-
-// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
-$.fn.ajaxFormUnbind = function() {
-    return this.unbind('submit.form-plugin click.form-plugin');
-};
-
-/**
- * formToArray() gathers form element data into an array of objects that can
- * be passed to any of the following ajax functions: $.get, $.post, or load.
- * Each object in the array has both a 'name' and 'value' property.  An example of
- * an array for a simple login form might be:
- *
- * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
- *
- * It is this array that is passed to pre-submit callback functions provided to the
- * ajaxSubmit() and ajaxForm() methods.
- */
-$.fn.formToArray = function(semantic, elements) {
-    var a = [];
-    if (this.length === 0) {
-        return a;
-    }
-
-    var form = this[0];
-    var els = semantic ? form.getElementsByTagName('*') : form.elements;
-    if (!els) {
-        return a;
-    }
-
-    var i,j,n,v,el,max,jmax;
-    for(i=0, max=els.length; i < max; i++) {
-        el = els[i];
-        n = el.name;
-        if (!n) {
-            continue;
-        }
-
-        if (semantic && form.clk && el.type == "image") {
-            // handle image inputs on the fly when semantic == true
-            if(!el.disabled && form.clk == el) {
-                a.push({name: n, value: $(el).val(), type: el.type });
-                a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
-            }
-            continue;
-        }
-
-        v = $.fieldValue(el, true);
-        if (v && v.constructor == Array) {
-            if (elements) 
-                elements.push(el);
-            for(j=0, jmax=v.length; j < jmax; j++) {
-                a.push({name: n, value: v[j]});
-            }
-        }
-        else if (feature.fileapi && el.type == 'file' && !el.disabled) {
-            if (elements) 
-                elements.push(el);
-            var files = el.files;
-            if (files.length) {
-                for (j=0; j < files.length; j++) {
-                    a.push({name: n, value: files[j], type: el.type});
-                }
-            }
-            else {
-                // #180
-                a.push({ name: n, value: '', type: el.type });
-            }
-        }
-        else if (v !== null && typeof v != 'undefined') {
-            if (elements) 
-                elements.push(el);
-            a.push({name: n, value: v, type: el.type, required: el.required});
-        }
-    }
-
-    if (!semantic && form.clk) {
-        // input type=='image' are not found in elements array! handle it here
-        var $input = $(form.clk), input = $input[0];
-        n = input.name;
-        if (n && !input.disabled && input.type == 'image') {
-            a.push({name: n, value: $input.val()});
-            a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
-        }
-    }
-    return a;
-};
-
-/**
- * Serializes form data into a 'submittable' string. This method will return a string
- * in the format: name1=value1&amp;name2=value2
- */
-$.fn.formSerialize = function(semantic) {
-    //hand off to jQuery.param for proper encoding
-    return $.param(this.formToArray(semantic));
-};
-
-/**
- * Serializes all field elements in the jQuery object into a query string.
- * This method will return a string in the format: name1=value1&amp;name2=value2
- */
-$.fn.fieldSerialize = function(successful) {
-    var a = [];
-    this.each(function() {
-        var n = this.name;
-        if (!n) {
-            return;
-        }
-        var v = $.fieldValue(this, successful);
-        if (v && v.constructor == Array) {
-            for (var i=0,max=v.length; i < max; i++) {
-                a.push({name: n, value: v[i]});
-            }
-        }
-        else if (v !== null && typeof v != 'undefined') {
-            a.push({name: this.name, value: v});
-        }
-    });
-    //hand off to jQuery.param for proper encoding
-    return $.param(a);
-};
-
-/**
- * Returns the value(s) of the element in the matched set.  For example, consider the following form:
- *
- *  <form><fieldset>
- *      <input name="A" type="text" />
- *      <input name="A" type="text" />
- *      <input name="B" type="checkbox" value="B1" />
- *      <input name="B" type="checkbox" value="B2"/>
- *      <input name="C" type="radio" value="C1" />
- *      <input name="C" type="radio" value="C2" />
- *  </fieldset></form>
- *
- *  var v = $(':text').fieldValue();
- *  // if no values are entered into the text inputs
- *  v == ['','']
- *  // if values entered into the text inputs are 'foo' and 'bar'
- *  v == ['foo','bar']
- *
- *  var v = $(':checkbox').fieldValue();
- *  // if neither checkbox is checked
- *  v === undefined
- *  // if both checkboxes are checked
- *  v == ['B1', 'B2']
- *
- *  var v = $(':radio').fieldValue();
- *  // if neither radio is checked
- *  v === undefined
- *  // if first radio is checked
- *  v == ['C1']
- *
- * The successful argument controls whether or not the field element must be 'successful'
- * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
- * The default value of the successful argument is true.  If this value is false the value(s)
- * for each element is returned.
- *
- * Note: This method *always* returns an array.  If no valid value can be determined the
- *    array will be empty, otherwise it will contain one or more values.
- */
-$.fn.fieldValue = function(successful) {
-    for (var val=[], i=0, max=this.length; i < max; i++) {
-        var el = this[i];
-        var v = $.fieldValue(el, successful);
-        if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
-            continue;
-        }
-        if (v.constructor == Array)
-            $.merge(val, v);
-        else
-            val.push(v);
-    }
-    return val;
-};
-
-/**
- * Returns the value of the field element.
- */
-$.fieldValue = function(el, successful) {
-    var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
-    if (successful === undefined) {
-        successful = true;
-    }
-
-    if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
-        (t == 'checkbox' || t == 'radio') && !el.checked ||
-        (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
-        tag == 'select' && el.selectedIndex == -1)) {
-            return null;
-    }
-
-    if (tag == 'select') {
-        var index = el.selectedIndex;
-        if (index < 0) {
-            return null;
-        }
-        var a = [], ops = el.options;
-        var one = (t == 'select-one');
-        var max = (one ? index+1 : ops.length);
-        for(var i=(one ? index : 0); i < max; i++) {
-            var op = ops[i];
-            if (op.selected) {
-                var v = op.value;
-                if (!v) { // extra pain for IE...
-                    v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
-                }
-                if (one) {
-                    return v;
-                }
-                a.push(v);
-            }
-        }
-        return a;
-    }
-    return $(el).val();
-};
-
-/**
- * Clears the form data.  Takes the following actions on the form's input fields:
- *  - input text fields will have their 'value' property set to the empty string
- *  - select elements will have their 'selectedIndex' property set to -1
- *  - checkbox and radio inputs will have their 'checked' property set to false
- *  - inputs of type submit, button, reset, and hidden will *not* be effected
- *  - button elements will *not* be effected
- */
-$.fn.clearForm = function(includeHidden) {
-    return this.each(function() {
-        $('input,select,textarea', this).clearFields(includeHidden);
-    });
-};
-
-/**
- * Clears the selected form elements.
- */
-$.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
-    var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
-    return this.each(function() {
-        var t = this.type, tag = this.tagName.toLowerCase();
-        if (re.test(t) || tag == 'textarea') {
-            this.value = '';
-        }
-        else if (t == 'checkbox' || t == 'radio') {
-            this.checked = false;
-        }
-        else if (tag == 'select') {
-            this.selectedIndex = -1;
-        }
-        else if (includeHidden) {
-            // includeHidden can be the value true, or it can be a selector string
-            // indicating a special test; for example:
-            //  $('#myForm').clearForm('.special:hidden')
-            // the above would clean hidden inputs that have the class of 'special'
-            if ( (includeHidden === true && /hidden/.test(t)) ||
-                 (typeof includeHidden == 'string' && $(this).is(includeHidden)) )
-                this.value = '';
-        }
-    });
-};
-
-/**
- * Resets the form data.  Causes all form elements to be reset to their original value.
- */
-$.fn.resetForm = function() {
-    return this.each(function() {
-        // guard against an input with the name of 'reset'
-        // note that IE reports the reset function as an 'object'
-        if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
-            this.reset();
-        }
-    });
-};
-
-/**
- * Enables or disables any matching elements.
- */
-$.fn.enable = function(b) {
-    if (b === undefined) {
-        b = true;
-    }
-    return this.each(function() {
-        this.disabled = !b;
-    });
-};
-
-/**
- * Checks/unchecks any matching checkboxes or radio buttons and
- * selects/deselects and matching option elements.
- */
-$.fn.selected = function(select) {
-    if (select === undefined) {
-        select = true;
-    }
-    return this.each(function() {
-        var t = this.type;
-        if (t == 'checkbox' || t == 'radio') {
-            this.checked = select;
-        }
-        else if (this.tagName.toLowerCase() == 'option') {
-            var $sel = $(this).parent('select');
-            if (select && $sel[0] && $sel[0].type == 'select-one') {
-                // deselect all other options
-                $sel.find('option').selected(false);
-            }
-            this.selected = select;
-        }
-    });
-};
-
-// expose debug var
-$.fn.ajaxSubmit.debug = false;
-
-// helper fn for console logging
-function log() {
-    if (!$.fn.ajaxSubmit.debug) 
-        return;
-    var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
-    if (window.console && window.console.log) {
-        window.console.log(msg);
-    }
-    else if (window.opera && window.opera.postError) {
-        window.opera.postError(msg);
-    }
-}
-
-})(jQuery);
diff --git a/resources/lib/jquery/jquery.fullscreen.js b/resources/lib/jquery/jquery.fullscreen.js
deleted file mode 100644 (file)
index 30e4484..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * jQuery fullscreen plugin v2.0.0-git (9f8f97d127)
- * https://github.com/theopolisme/jquery-fullscreen
- *
- * Copyright (c) 2013 Theopolisme <theopolismewiki@gmail.com>
- *
- * 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.
- */
-( function ( $ ) {
-       var setupFullscreen,
-               fsClass = 'jq-fullscreened';
-
-       /**
-        * On fullscreenchange, trigger a jq-fullscreen-change event
-        * The event is given an object, which contains the fullscreened DOM element (element), if any
-        * and a boolean value (fullscreen) indicating if we've entered or exited fullscreen mode
-        * Also remove the 'fullscreened' class from elements that are no longer fullscreen
-        */
-       function handleFullscreenChange () {
-               var fullscreenElement = document.fullscreenElement ||
-                       document.mozFullScreenElement ||
-                       document.webkitFullscreenElement ||
-                       document.msFullscreenElement;
-
-               if ( !fullscreenElement ) {
-                       $( '.' + fsClass ).data( 'isFullscreened', false ).removeClass( fsClass );
-               }
-
-               $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: fullscreenElement, fullscreen: !!fullscreenElement } ) );
-       }
-
-       /**
-        * Enters full screen with the "this" element in focus.
-        * Check the .data( 'isFullscreened' ) of the return value to check
-        * success or failure, if you're into that sort of thing.
-        * @chainable
-        * @return {jQuery}
-        */
-       function enterFullscreen () {
-               var element = this.get(0),
-                       $element = this.first();
-               if ( element ) {
-                       if ( element.requestFullscreen ) {
-                               element.requestFullscreen();
-                       } else if ( element.mozRequestFullScreen ) {
-                               element.mozRequestFullScreen();
-                       } else if ( element.webkitRequestFullscreen ) {
-                               element.webkitRequestFullscreen();
-                       } else if ( element.msRequestFullscreen ) {
-                               element.msRequestFullscreen();
-                       } else {
-                               // Unable to make fullscreen
-                               $element.data( 'isFullscreened', false );
-                               return this;
-                       }
-                       // Add the fullscreen class and data attribute to `element`
-                       $element.addClass( fsClass ).data( 'isFullscreened', true );
-                       return this;
-               } else {
-                       $element.data( 'isFullscreened', false );
-                       return this;
-               }
-       }
-
-       /**
-        * Brings the "this" element out of fullscreen.
-        * Check the .data( 'isFullscreened' ) of the return value to check
-        * success or failure, if you're into that sort of thing.
-        * @chainable
-        * @return {jQuery}
-        */
-       function exitFullscreen () {
-               var fullscreenElement = ( document.fullscreenElement ||
-                               document.mozFullScreenElement ||
-                               document.webkitFullscreenElement ||
-                               document.msFullscreenElement );
-
-               // Ensure that we only exit fullscreen if exitFullscreen() is being called on the same element that is currently fullscreen
-               if ( fullscreenElement && this.get(0) === fullscreenElement ) {
-                       if ( document.exitFullscreen ) {
-                               document.exitFullscreen();
-                       } else if ( document.mozCancelFullScreen ) {
-                               document.mozCancelFullScreen();
-                       } else if ( document.webkitCancelFullScreen ) {
-                               document.webkitCancelFullScreen();
-                       } else if ( document.msExitFullscreen ) {
-                               document.msExitFullscreen();
-                       } else {
-                               // Unable to cancel fullscreen mode
-                               return this;
-                       }
-                       // We don't need to remove the fullscreen class here,
-                       // because it will be removed in handleFullscreenChange.
-                       // But we should change the data on the element so the
-                       // caller can check for success.
-                       this.first().data( 'isFullscreened', false );
-               }
-
-               return this;
-       }
-
-       /**
-        * Set up fullscreen handling and install necessary event handlers.
-        * Return false if fullscreen is not supported.
-        */
-       setupFullscreen = function () {
-               if ( $.support.fullscreen ) {
-                       // When the fullscreen mode is changed, trigger the
-                       // fullscreen events (and when exiting,
-                       // also remove the fullscreen class)
-                       $( document ).on( 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', handleFullscreenChange);
-                       // Convenience wrapper so that one only needs to listen for
-                       // 'fullscreenerror', not all of the prefixed versions
-                       $( document ).on( 'webkitfullscreenerror mozfullscreenerror MSFullscreenError', function () {
-                               $( document ).trigger( $.Event( 'fullscreenerror' ) );
-                       } );
-                       // Fullscreen has been set up, so always return true
-                       setupFullscreen = function () { return true; };
-                       return true;
-               } else {
-                       // Always return false from now on, since fullscreen is not supported
-                       setupFullscreen = function () { return false; };
-                       return false;
-               }
-       };
-
-       /**
-        * Set up fullscreen handling if necessary, then make the first element
-        * matching the given selector fullscreen
-        * @chainable
-        * @return {jQuery}
-        */
-       $.fn.enterFullscreen = function () {
-               if ( setupFullscreen() ) {
-                       $.fn.enterFullscreen = enterFullscreen;
-                       return this.enterFullscreen();
-               } else {
-                       $.fn.enterFullscreen = function () { return this; };
-                       return this;
-               }
-       };
-
-       /**
-        * Set up fullscreen handling if necessary, then cancel fullscreen mode
-        * for the first element matching the given selector.
-        * @chainable
-        * @return {jQuery}
-        */
-       $.fn.exitFullscreen = function () {
-               if ( setupFullscreen() ) {
-                       $.fn.exitFullscreen = exitFullscreen;
-                       return this.exitFullscreen();
-               } else {
-                       $.fn.exitFullscreen = function () { return this; };
-                       return this;
-               }
-       };
-
-       $.support.fullscreen = document.fullscreenEnabled ||
-               document.webkitFullscreenEnabled ||
-               document.mozFullScreenEnabled ||
-               document.msFullscreenEnabled;
-}( jQuery ) );
diff --git a/resources/lib/jquery/jquery.hoverIntent.js b/resources/lib/jquery/jquery.hoverIntent.js
deleted file mode 100644 (file)
index adf948d..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
-* hoverIntent is similar to jQuery's built-in "hover" function except that
-* instead of firing the onMouseOver event immediately, hoverIntent checks
-* to see if the user's mouse has slowed down (beneath the sensitivity
-* threshold) before firing the onMouseOver event.
-* 
-* hoverIntent r5 // 2007.03.27 // jQuery 1.1.2+
-* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
-* 
-* hoverIntent is currently available for use in all personal or commercial 
-* projects under both MIT and GPL licenses. This means that you can choose 
-* the license that best suits your project, and use it accordingly.
-* 
-* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
-* $("ul li").hoverIntent( showNav , hideNav );
-* 
-* // advanced usage receives configuration object only
-* $("ul li").hoverIntent({
-*      sensitivity: 7, // number = sensitivity threshold (must be 1 or higher)
-*      interval: 100,   // number = milliseconds of polling interval
-*      over: showNav,  // function = onMouseOver callback (required)
-*      timeout: 0,   // number = milliseconds delay before onMouseOut function call
-*      out: hideNav    // function = onMouseOut callback (required)
-* });
-* 
-* @param  f  onMouseOver function || An object with configuration options
-* @param  g  onMouseOut function  || Nothing (use configuration options object)
-* @author    Brian Cherne <brian@cherne.net>
-*/
-(function($) {
-       $.fn.hoverIntent = function(f,g) {
-               // default configuration options
-               var cfg = {
-                       sensitivity: 7,
-                       interval: 100,
-                       timeout: 0
-               };
-               // override configuration options with user supplied object
-               cfg = $.extend(cfg, g ? { over: f, out: g } : f );
-
-               // instantiate variables
-               // cX, cY = current X and Y position of mouse, updated by mousemove event
-               // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
-               var cX, cY, pX, pY;
-
-               // A private function for getting mouse position
-               var track = function(ev) {
-                       cX = ev.pageX;
-                       cY = ev.pageY;
-               };
-
-               // A private function for comparing current and previous mouse position
-               var compare = function(ev,ob) {
-                       ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
-                       // compare mouse positions to see if they've crossed the threshold
-                       if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) {
-                               $(ob).unbind("mousemove",track);
-                               // set hoverIntent state to true (so mouseOut can be called)
-                               ob.hoverIntent_s = 1;
-                               return cfg.over.apply(ob,[ev]);
-                       } else {
-                               // set previous coordinates for next time
-                               pX = cX; pY = cY;
-                               // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
-                               ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
-                       }
-               };
-
-               // A private function for delaying the mouseOut function
-               var delay = function(ev,ob) {
-                       ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
-                       ob.hoverIntent_s = 0;
-                       return cfg.out.apply(ob,[ev]);
-               };
-
-               // A private function for handling mouse 'hovering'
-               var handleHover = function(e) {
-                       // next three lines copied from jQuery.hover, ignore children onMouseOver/onMouseOut
-                       var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget;
-                       while ( p && p != this ) { try { p = p.parentNode; } catch(e) { p = this; } }
-                       if ( p == this ) { return false; }
-
-                       // copy objects to be passed into t (required for event object to be passed in IE)
-                       var ev = $.extend({},e);
-                       var ob = this;
-
-                       // cancel hoverIntent timer if it exists
-                       if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }
-
-                       // else e.type == "onmouseover"
-                       if (e.type == "mouseover") {
-                               // set "previous" X and Y position based on initial entry point
-                               pX = ev.pageX; pY = ev.pageY;
-                               // update "current" X and Y position based on mousemove
-                               $(ob).bind("mousemove",track);
-                               // start polling interval (self-calling timeout) to compare mouse coordinates over time
-                               if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
-
-                       // else e.type == "onmouseout"
-                       } else {
-                               // unbind expensive mousemove event
-                               $(ob).unbind("mousemove",track);
-                               // if hoverIntent state is true, then call the mouseOut function after the specified delay
-                               if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
-                       }
-               };
-
-               // bind the function to the two event listeners
-               return this.mouseover(handleHover).mouseout(handleHover);
-       };
-})(jQuery);
\ No newline at end of file
diff --git a/resources/lib/jquery/jquery.jStorage.js b/resources/lib/jquery/jquery.jStorage.js
deleted file mode 100644 (file)
index 45e19ac..0000000
+++ /dev/null
@@ -1,996 +0,0 @@
-/*
- * ----------------------------- JSTORAGE -------------------------------------
- * Simple local storage wrapper to save data on the browser side, supporting
- * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
- *
- * Author: Andris Reinman, andris.reinman@gmail.com
- * Project homepage: www.jstorage.info
- *
- * Licensed under Unlicense:
- *
- * This is free and unencumbered software released into the public domain.
- *
- * Anyone is free to copy, modify, publish, use, compile, sell, or
- * distribute this software, either in source code form or as a compiled
- * binary, for any purpose, commercial or non-commercial, and by any
- * means.
- *
- * In jurisdictions that recognize copyright laws, the author or authors
- * of this software dedicate any and all copyright interest in the
- * software to the public domain. We make this dedication for the benefit
- * of the public at large and to the detriment of our heirs and
- * successors. We intend this dedication to be an overt act of
- * relinquishment in perpetuity of all present and future rights to this
- * software under copyright law.
- *
- * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
- * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
- * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- * OTHER DEALINGS IN THE SOFTWARE.
- *
- * For more information, please refer to <http://unlicense.org/>
- */
-
-/* global ActiveXObject: false */
-/* jshint browser: true */
-
-(function() {
-    'use strict';
-
-    var
-    /* jStorage version */
-        JSTORAGE_VERSION = '0.4.12',
-
-        /* detect a dollar object or create one if not found */
-        $ = window.jQuery || window.$ || (window.$ = {}),
-
-        /* check for a JSON handling support */
-        JSON = {
-            parse: window.JSON && (window.JSON.parse || window.JSON.decode) ||
-                String.prototype.evalJSON && function(str) {
-                    return String(str).evalJSON();
-            } ||
-                $.parseJSON ||
-                $.evalJSON,
-            stringify: Object.toJSON ||
-                window.JSON && (window.JSON.stringify || window.JSON.encode) ||
-                $.toJSON
-        };
-
-    // Break if no JSON support was found
-    if (typeof JSON.parse !== 'function' || typeof JSON.stringify !== 'function') {
-        throw new Error('No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page');
-    }
-
-    var
-    /* This is the object, that holds the cached values */
-        _storage = {
-            __jstorage_meta: {
-                CRC32: {}
-            }
-        },
-
-        /* Actual browser storage (localStorage or globalStorage['domain']) */
-        _storage_service = {
-            jStorage: '{}'
-        },
-
-        /* DOM element for older IE versions, holds userData behavior */
-        _storage_elm = null,
-
-        /* How much space does the storage take */
-        _storage_size = 0,
-
-        /* which backend is currently used */
-        _backend = false,
-
-        /* onchange observers */
-        _observers = {},
-
-        /* timeout to wait after onchange event */
-        _observer_timeout = false,
-
-        /* last update time */
-        _observer_update = 0,
-
-        /* pubsub observers */
-        _pubsub_observers = {},
-
-        /* skip published items older than current timestamp */
-        _pubsub_last = +new Date(),
-
-        /* Next check for TTL */
-        _ttl_timeout,
-
-        /**
-         * XML encoding and decoding as XML nodes can't be JSON'ized
-         * XML nodes are encoded and decoded if the node is the value to be saved
-         * but not if it's as a property of another object
-         * Eg. -
-         *   $.jStorage.set('key', xmlNode);        // IS OK
-         *   $.jStorage.set('key', {xml: xmlNode}); // NOT OK
-         */
-        _XMLService = {
-
-            /**
-             * Validates a XML node to be XML
-             * based on jQuery.isXML function
-             */
-            isXML: function(elm) {
-                var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
-                return documentElement ? documentElement.nodeName !== 'HTML' : false;
-            },
-
-            /**
-             * Encodes a XML node to string
-             * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
-             */
-            encode: function(xmlNode) {
-                if (!this.isXML(xmlNode)) {
-                    return false;
-                }
-                try { // Mozilla, Webkit, Opera
-                    return new XMLSerializer().serializeToString(xmlNode);
-                } catch (E1) {
-                    try { // IE
-                        return xmlNode.xml;
-                    } catch (E2) {}
-                }
-                return false;
-            },
-
-            /**
-             * Decodes a XML node from string
-             * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
-             */
-            decode: function(xmlString) {
-                var dom_parser = ('DOMParser' in window && (new DOMParser()).parseFromString) ||
-                    (window.ActiveXObject && function(_xmlString) {
-                        var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
-                        xml_doc.async = 'false';
-                        xml_doc.loadXML(_xmlString);
-                        return xml_doc;
-                    }),
-                    resultXML;
-                if (!dom_parser) {
-                    return false;
-                }
-                resultXML = dom_parser.call('DOMParser' in window && (new DOMParser()) || window, xmlString, 'text/xml');
-                return this.isXML(resultXML) ? resultXML : false;
-            }
-        };
-
-
-    ////////////////////////// PRIVATE METHODS ////////////////////////
-
-    /**
-     * Initialization function. Detects if the browser supports DOM Storage
-     * or userData behavior and behaves accordingly.
-     */
-    function _init() {
-        /* Check if browser supports localStorage */
-        var localStorageReallyWorks = false;
-        if ('localStorage' in window) {
-            try {
-                window.localStorage.setItem('_tmptest', 'tmpval');
-                localStorageReallyWorks = true;
-                window.localStorage.removeItem('_tmptest');
-            } catch (BogusQuotaExceededErrorOnIos5) {
-                // Thanks be to iOS5 Private Browsing mode which throws
-                // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
-            }
-        }
-
-        if (localStorageReallyWorks) {
-            try {
-                if (window.localStorage) {
-                    _storage_service = window.localStorage;
-                    _backend = 'localStorage';
-                    _observer_update = _storage_service.jStorage_update;
-                }
-            } catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
-        }
-        /* Check if browser supports globalStorage */
-        else if ('globalStorage' in window) {
-            try {
-                if (window.globalStorage) {
-                    if (window.location.hostname == 'localhost') {
-                        _storage_service = window.globalStorage['localhost.localdomain'];
-                    } else {
-                        _storage_service = window.globalStorage[window.location.hostname];
-                    }
-                    _backend = 'globalStorage';
-                    _observer_update = _storage_service.jStorage_update;
-                }
-            } catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
-        }
-        /* Check if browser supports userData behavior */
-        else {
-            _storage_elm = document.createElement('link');
-            if (_storage_elm.addBehavior) {
-
-                /* Use a DOM element to act as userData storage */
-                _storage_elm.style.behavior = 'url(#default#userData)';
-
-                /* userData element needs to be inserted into the DOM! */
-                document.getElementsByTagName('head')[0].appendChild(_storage_elm);
-
-                try {
-                    _storage_elm.load('jStorage');
-                } catch (E) {
-                    // try to reset cache
-                    _storage_elm.setAttribute('jStorage', '{}');
-                    _storage_elm.save('jStorage');
-                    _storage_elm.load('jStorage');
-                }
-
-                var data = '{}';
-                try {
-                    data = _storage_elm.getAttribute('jStorage');
-                } catch (E5) {}
-
-                try {
-                    _observer_update = _storage_elm.getAttribute('jStorage_update');
-                } catch (E6) {}
-
-                _storage_service.jStorage = data;
-                _backend = 'userDataBehavior';
-            } else {
-                _storage_elm = null;
-                return;
-            }
-        }
-
-        // Load data from storage
-        _load_storage();
-
-        // remove dead keys
-        _handleTTL();
-
-        // start listening for changes
-        _setupObserver();
-
-        // initialize publish-subscribe service
-        _handlePubSub();
-
-        // handle cached navigation
-        if ('addEventListener' in window) {
-            window.addEventListener('pageshow', function(event) {
-                if (event.persisted) {
-                    _storageObserver();
-                }
-            }, false);
-        }
-    }
-
-    /**
-     * Reload data from storage when needed
-     */
-    function _reloadData() {
-        var data = '{}';
-
-        if (_backend == 'userDataBehavior') {
-            _storage_elm.load('jStorage');
-
-            try {
-                data = _storage_elm.getAttribute('jStorage');
-            } catch (E5) {}
-
-            try {
-                _observer_update = _storage_elm.getAttribute('jStorage_update');
-            } catch (E6) {}
-
-            _storage_service.jStorage = data;
-        }
-
-        _load_storage();
-
-        // remove dead keys
-        _handleTTL();
-
-        _handlePubSub();
-    }
-
-    /**
-     * Sets up a storage change observer
-     */
-    function _setupObserver() {
-        if (_backend == 'localStorage' || _backend == 'globalStorage') {
-            if ('addEventListener' in window) {
-                window.addEventListener('storage', _storageObserver, false);
-            } else {
-                document.attachEvent('onstorage', _storageObserver);
-            }
-        } else if (_backend == 'userDataBehavior') {
-            setInterval(_storageObserver, 1000);
-        }
-    }
-
-    /**
-     * Fired on any kind of data change, needs to check if anything has
-     * really been changed
-     */
-    function _storageObserver() {
-        var updateTime;
-        // cumulate change notifications with timeout
-        clearTimeout(_observer_timeout);
-        _observer_timeout = setTimeout(function() {
-
-            if (_backend == 'localStorage' || _backend == 'globalStorage') {
-                updateTime = _storage_service.jStorage_update;
-            } else if (_backend == 'userDataBehavior') {
-                _storage_elm.load('jStorage');
-                try {
-                    updateTime = _storage_elm.getAttribute('jStorage_update');
-                } catch (E5) {}
-            }
-
-            if (updateTime && updateTime != _observer_update) {
-                _observer_update = updateTime;
-                _checkUpdatedKeys();
-            }
-
-        }, 25);
-    }
-
-    /**
-     * Reloads the data and checks if any keys are changed
-     */
-    function _checkUpdatedKeys() {
-        var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
-            newCrc32List;
-
-        _reloadData();
-        newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
-
-        var key,
-            updated = [],
-            removed = [];
-
-        for (key in oldCrc32List) {
-            if (oldCrc32List.hasOwnProperty(key)) {
-                if (!newCrc32List[key]) {
-                    removed.push(key);
-                    continue;
-                }
-                if (oldCrc32List[key] != newCrc32List[key] && String(oldCrc32List[key]).substr(0, 2) == '2.') {
-                    updated.push(key);
-                }
-            }
-        }
-
-        for (key in newCrc32List) {
-            if (newCrc32List.hasOwnProperty(key)) {
-                if (!oldCrc32List[key]) {
-                    updated.push(key);
-                }
-            }
-        }
-
-        _fireObservers(updated, 'updated');
-        _fireObservers(removed, 'deleted');
-    }
-
-    /**
-     * Fires observers for updated keys
-     *
-     * @param {Array|String} keys Array of key names or a key
-     * @param {String} action What happened with the value (updated, deleted, flushed)
-     */
-    function _fireObservers(keys, action) {
-        keys = [].concat(keys || []);
-
-        var i, j, len, jlen;
-
-        if (action == 'flushed') {
-            keys = [];
-            for (var key in _observers) {
-                if (_observers.hasOwnProperty(key)) {
-                    keys.push(key);
-                }
-            }
-            action = 'deleted';
-        }
-        for (i = 0, len = keys.length; i < len; i++) {
-            if (_observers[keys[i]]) {
-                for (j = 0, jlen = _observers[keys[i]].length; j < jlen; j++) {
-                    _observers[keys[i]][j](keys[i], action);
-                }
-            }
-            if (_observers['*']) {
-                for (j = 0, jlen = _observers['*'].length; j < jlen; j++) {
-                    _observers['*'][j](keys[i], action);
-                }
-            }
-        }
-    }
-
-    /**
-     * Publishes key change to listeners
-     */
-    function _publishChange() {
-        var updateTime = (+new Date()).toString();
-
-        if (_backend == 'localStorage' || _backend == 'globalStorage') {
-            try {
-                _storage_service.jStorage_update = updateTime;
-            } catch (E8) {
-                // safari private mode has been enabled after the jStorage initialization
-                _backend = false;
-            }
-        } else if (_backend == 'userDataBehavior') {
-            _storage_elm.setAttribute('jStorage_update', updateTime);
-            _storage_elm.save('jStorage');
-        }
-
-        _storageObserver();
-    }
-
-    /**
-     * Loads the data from the storage based on the supported mechanism
-     */
-    function _load_storage() {
-        /* if jStorage string is retrieved, then decode it */
-        if (_storage_service.jStorage) {
-            try {
-                _storage = JSON.parse(String(_storage_service.jStorage));
-            } catch (E6) {
-                _storage_service.jStorage = '{}';
-            }
-        } else {
-            _storage_service.jStorage = '{}';
-        }
-        _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
-
-        if (!_storage.__jstorage_meta) {
-            _storage.__jstorage_meta = {};
-        }
-        if (!_storage.__jstorage_meta.CRC32) {
-            _storage.__jstorage_meta.CRC32 = {};
-        }
-    }
-
-    /**
-     * This functions provides the 'save' mechanism to store the jStorage object
-     */
-    function _save() {
-        _dropOldEvents(); // remove expired events
-        try {
-            _storage_service.jStorage = JSON.stringify(_storage);
-            // If userData is used as the storage engine, additional
-            if (_storage_elm) {
-                _storage_elm.setAttribute('jStorage', _storage_service.jStorage);
-                _storage_elm.save('jStorage');
-            }
-            _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
-        } catch (E7) { /* probably cache is full, nothing is saved this way*/ }
-    }
-
-    /**
-     * Function checks if a key is set and is string or numberic
-     *
-     * @param {String} key Key name
-     */
-    function _checkKey(key) {
-        if (typeof key != 'string' && typeof key != 'number') {
-            throw new TypeError('Key name must be string or numeric');
-        }
-        if (key == '__jstorage_meta') {
-            throw new TypeError('Reserved key name');
-        }
-        return true;
-    }
-
-    /**
-     * Removes expired keys
-     */
-    function _handleTTL() {
-        var curtime, i, TTL, CRC32, nextExpire = Infinity,
-            changed = false,
-            deleted = [];
-
-        clearTimeout(_ttl_timeout);
-
-        if (!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != 'object') {
-            // nothing to do here
-            return;
-        }
-
-        curtime = +new Date();
-        TTL = _storage.__jstorage_meta.TTL;
-
-        CRC32 = _storage.__jstorage_meta.CRC32;
-        for (i in TTL) {
-            if (TTL.hasOwnProperty(i)) {
-                if (TTL[i] <= curtime) {
-                    delete TTL[i];
-                    delete CRC32[i];
-                    delete _storage[i];
-                    changed = true;
-                    deleted.push(i);
-                } else if (TTL[i] < nextExpire) {
-                    nextExpire = TTL[i];
-                }
-            }
-        }
-
-        // set next check
-        if (nextExpire != Infinity) {
-            _ttl_timeout = setTimeout(_handleTTL, Math.min(nextExpire - curtime, 0x7FFFFFFF));
-        }
-
-        // save changes
-        if (changed) {
-            _save();
-            _publishChange();
-            _fireObservers(deleted, 'deleted');
-        }
-    }
-
-    /**
-     * Checks if there's any events on hold to be fired to listeners
-     */
-    function _handlePubSub() {
-        var i, len;
-        if (!_storage.__jstorage_meta.PubSub) {
-            return;
-        }
-        var pubelm,
-            _pubsubCurrent = _pubsub_last,
-            needFired = [];
-
-        for (i = len = _storage.__jstorage_meta.PubSub.length - 1; i >= 0; i--) {
-            pubelm = _storage.__jstorage_meta.PubSub[i];
-            if (pubelm[0] > _pubsub_last) {
-                _pubsubCurrent = pubelm[0];
-                needFired.unshift(pubelm);
-            }
-        }
-
-        for (i = needFired.length - 1; i >= 0; i--) {
-            _fireSubscribers(needFired[i][1], needFired[i][2]);
-        }
-
-        _pubsub_last = _pubsubCurrent;
-    }
-
-    /**
-     * Fires all subscriber listeners for a pubsub channel
-     *
-     * @param {String} channel Channel name
-     * @param {Mixed} payload Payload data to deliver
-     */
-    function _fireSubscribers(channel, payload) {
-        if (_pubsub_observers[channel]) {
-            for (var i = 0, len = _pubsub_observers[channel].length; i < len; i++) {
-                // send immutable data that can't be modified by listeners
-                try {
-                    _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
-                } catch (E) {}
-            }
-        }
-    }
-
-    /**
-     * Remove old events from the publish stream (at least 2sec old)
-     */
-    function _dropOldEvents() {
-        if (!_storage.__jstorage_meta.PubSub) {
-            return;
-        }
-
-        var retire = +new Date() - 2000;
-
-        for (var i = 0, len = _storage.__jstorage_meta.PubSub.length; i < len; i++) {
-            if (_storage.__jstorage_meta.PubSub[i][0] <= retire) {
-                // deleteCount is needed for IE6
-                _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
-                break;
-            }
-        }
-
-        if (!_storage.__jstorage_meta.PubSub.length) {
-            delete _storage.__jstorage_meta.PubSub;
-        }
-
-    }
-
-    /**
-     * Publish payload to a channel
-     *
-     * @param {String} channel Channel name
-     * @param {Mixed} payload Payload to send to the subscribers
-     */
-    function _publish(channel, payload) {
-        if (!_storage.__jstorage_meta) {
-            _storage.__jstorage_meta = {};
-        }
-        if (!_storage.__jstorage_meta.PubSub) {
-            _storage.__jstorage_meta.PubSub = [];
-        }
-
-        _storage.__jstorage_meta.PubSub.unshift([+new Date(), channel, payload]);
-
-        _save();
-        _publishChange();
-    }
-
-
-    /**
-     * JS Implementation of MurmurHash2
-     *
-     *  SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
-     *
-     * @author <a href='mailto:gary.court@gmail.com'>Gary Court</a>
-     * @see http://github.com/garycourt/murmurhash-js
-     * @author <a href='mailto:aappleby@gmail.com'>Austin Appleby</a>
-     * @see http://sites.google.com/site/murmurhash/
-     *
-     * @param {string} str ASCII only
-     * @param {number} seed Positive integer only
-     * @return {number} 32-bit positive integer hash
-     */
-
-    function murmurhash2_32_gc(str, seed) {
-        var
-            l = str.length,
-            h = seed ^ l,
-            i = 0,
-            k;
-
-        while (l >= 4) {
-            k =
-                ((str.charCodeAt(i) & 0xff)) |
-                ((str.charCodeAt(++i) & 0xff) << 8) |
-                ((str.charCodeAt(++i) & 0xff) << 16) |
-                ((str.charCodeAt(++i) & 0xff) << 24);
-
-            k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
-            k ^= k >>> 24;
-            k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
-
-            h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
-
-            l -= 4;
-            ++i;
-        }
-
-        switch (l) {
-            case 3:
-                h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
-                /* falls through */
-            case 2:
-                h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
-                /* falls through */
-            case 1:
-                h ^= (str.charCodeAt(i) & 0xff);
-                h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
-        }
-
-        h ^= h >>> 13;
-        h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
-        h ^= h >>> 15;
-
-        return h >>> 0;
-    }
-
-    ////////////////////////// PUBLIC INTERFACE /////////////////////////
-
-    $.jStorage = {
-        /* Version number */
-        version: JSTORAGE_VERSION,
-
-        /**
-         * Sets a key's value.
-         *
-         * @param {String} key Key to set. If this value is not set or not
-         *              a string an exception is raised.
-         * @param {Mixed} value Value to set. This can be any value that is JSON
-         *              compatible (Numbers, Strings, Objects etc.).
-         * @param {Object} [options] - possible options to use
-         * @param {Number} [options.TTL] - optional TTL value, in milliseconds
-         * @return {Mixed} the used value
-         */
-        set: function(key, value, options) {
-            _checkKey(key);
-
-            options = options || {};
-
-            // undefined values are deleted automatically
-            if (typeof value == 'undefined') {
-                this.deleteKey(key);
-                return value;
-            }
-
-            if (_XMLService.isXML(value)) {
-                value = {
-                    _is_xml: true,
-                    xml: _XMLService.encode(value)
-                };
-            } else if (typeof value == 'function') {
-                return undefined; // functions can't be saved!
-            } else if (value && typeof value == 'object') {
-                // clone the object before saving to _storage tree
-                value = JSON.parse(JSON.stringify(value));
-            }
-
-            _storage[key] = value;
-
-            _storage.__jstorage_meta.CRC32[key] = '2.' + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
-
-            this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
-
-            _fireObservers(key, 'updated');
-            return value;
-        },
-
-        /**
-         * Looks up a key in cache
-         *
-         * @param {String} key - Key to look up.
-         * @param {mixed} def - Default value to return, if key didn't exist.
-         * @return {Mixed} the key value, default value or null
-         */
-        get: function(key, def) {
-            _checkKey(key);
-            if (key in _storage) {
-                if (_storage[key] && typeof _storage[key] == 'object' && _storage[key]._is_xml) {
-                    return _XMLService.decode(_storage[key].xml);
-                } else {
-                    return _storage[key];
-                }
-            }
-            return typeof(def) == 'undefined' ? null : def;
-        },
-
-        /**
-         * Deletes a key from cache.
-         *
-         * @param {String} key - Key to delete.
-         * @return {Boolean} true if key existed or false if it didn't
-         */
-        deleteKey: function(key) {
-            _checkKey(key);
-            if (key in _storage) {
-                delete _storage[key];
-                // remove from TTL list
-                if (typeof _storage.__jstorage_meta.TTL == 'object' &&
-                    key in _storage.__jstorage_meta.TTL) {
-                    delete _storage.__jstorage_meta.TTL[key];
-                }
-
-                delete _storage.__jstorage_meta.CRC32[key];
-
-                _save();
-                _publishChange();
-                _fireObservers(key, 'deleted');
-                return true;
-            }
-            return false;
-        },
-
-        /**
-         * Sets a TTL for a key, or remove it if ttl value is 0 or below
-         *
-         * @param {String} key - key to set the TTL for
-         * @param {Number} ttl - TTL timeout in milliseconds
-         * @return {Boolean} true if key existed or false if it didn't
-         */
-        setTTL: function(key, ttl) {
-            var curtime = +new Date();
-            _checkKey(key);
-            ttl = Number(ttl) || 0;
-            if (key in _storage) {
-
-                if (!_storage.__jstorage_meta.TTL) {
-                    _storage.__jstorage_meta.TTL = {};
-                }
-
-                // Set TTL value for the key
-                if (ttl > 0) {
-                    _storage.__jstorage_meta.TTL[key] = curtime + ttl;
-                } else {
-                    delete _storage.__jstorage_meta.TTL[key];
-                }
-
-                _save();
-
-                _handleTTL();
-
-                _publishChange();
-                return true;
-            }
-            return false;
-        },
-
-        /**
-         * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
-         *
-         * @param {String} key Key to check
-         * @return {Number} Remaining TTL in milliseconds
-         */
-        getTTL: function(key) {
-            var curtime = +new Date(),
-                ttl;
-            _checkKey(key);
-            if (key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]) {
-                ttl = _storage.__jstorage_meta.TTL[key] - curtime;
-                return ttl || 0;
-            }
-            return 0;
-        },
-
-        /**
-         * Deletes everything in cache.
-         *
-         * @return {Boolean} Always true
-         */
-        flush: function() {
-            _storage = {
-                __jstorage_meta: {
-                    CRC32: {}
-                }
-            };
-            _save();
-            _publishChange();
-            _fireObservers(null, 'flushed');
-            return true;
-        },
-
-        /**
-         * Returns a read-only copy of _storage
-         *
-         * @return {Object} Read-only copy of _storage
-         */
-        storageObj: function() {
-            function F() {}
-            F.prototype = _storage;
-            return new F();
-        },
-
-        /**
-         * Returns an index of all used keys as an array
-         * ['key1', 'key2',..'keyN']
-         *
-         * @return {Array} Used keys
-         */
-        index: function() {
-            var index = [],
-                i;
-            for (i in _storage) {
-                if (_storage.hasOwnProperty(i) && i != '__jstorage_meta') {
-                    index.push(i);
-                }
-            }
-            return index;
-        },
-
-        /**
-         * How much space in bytes does the storage take?
-         *
-         * @return {Number} Storage size in chars (not the same as in bytes,
-         *                  since some chars may take several bytes)
-         */
-        storageSize: function() {
-            return _storage_size;
-        },
-
-        /**
-         * Which backend is currently in use?
-         *
-         * @return {String} Backend name
-         */
-        currentBackend: function() {
-            return _backend;
-        },
-
-        /**
-         * Test if storage is available
-         *
-         * @return {Boolean} True if storage can be used
-         */
-        storageAvailable: function() {
-            return !!_backend;
-        },
-
-        /**
-         * Register change listeners
-         *
-         * @param {String} key Key name
-         * @param {Function} callback Function to run when the key changes
-         */
-        listenKeyChange: function(key, callback) {
-            _checkKey(key);
-            if (!_observers[key]) {
-                _observers[key] = [];
-            }
-            _observers[key].push(callback);
-        },
-
-        /**
-         * Remove change listeners
-         *
-         * @param {String} key Key name to unregister listeners against
-         * @param {Function} [callback] If set, unregister the callback, if not - unregister all
-         */
-        stopListening: function(key, callback) {
-            _checkKey(key);
-
-            if (!_observers[key]) {
-                return;
-            }
-
-            if (!callback) {
-                delete _observers[key];
-                return;
-            }
-
-            for (var i = _observers[key].length - 1; i >= 0; i--) {
-                if (_observers[key][i] == callback) {
-                    _observers[key].splice(i, 1);
-                }
-            }
-        },
-
-        /**
-         * Subscribe to a Publish/Subscribe event stream
-         *
-         * @param {String} channel Channel name
-         * @param {Function} callback Function to run when the something is published to the channel
-         */
-        subscribe: function(channel, callback) {
-            channel = (channel || '').toString();
-            if (!channel) {
-                throw new TypeError('Channel not defined');
-            }
-            if (!_pubsub_observers[channel]) {
-                _pubsub_observers[channel] = [];
-            }
-            _pubsub_observers[channel].push(callback);
-        },
-
-        /**
-         * Publish data to an event stream
-         *
-         * @param {String} channel Channel name
-         * @param {Mixed} payload Payload to deliver
-         */
-        publish: function(channel, payload) {
-            channel = (channel || '').toString();
-            if (!channel) {
-                throw new TypeError('Channel not defined');
-            }
-
-            _publish(channel, payload);
-        },
-
-        /**
-         * Reloads the data from browser storage
-         */
-        reInit: function() {
-            _reloadData();
-        },
-
-        /**
-         * Removes reference from global objects and saves it as jStorage
-         *
-         * @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
-         */
-        noConflict: function(saveInGlobal) {
-            delete window.$.jStorage;
-
-            if (saveInGlobal) {
-                window.jStorage = this;
-            }
-
-            return this;
-        }
-    };
-
-    // Initialize jStorage
-    _init();
-
-})();
diff --git a/resources/lib/jquery/jquery.mockjax.js b/resources/lib/jquery/jquery.mockjax.js
deleted file mode 100644 (file)
index 5f6e130..0000000
+++ /dev/null
@@ -1,382 +0,0 @@
-/*!
- * MockJax - jQuery Plugin to Mock Ajax requests
- *
- * Version:  1.4.0
- * Released: 2011-02-04
- * Source:   http://github.com/appendto/jquery-mockjax
- * Docs:     http://enterprisejquery.com/2010/07/mock-your-ajax-requests-with-mockjax-for-rapid-development
- * Plugin:   mockjax
- * Author:   Jonathan Sharp (http://jdsharp.com)
- * License:  MIT,GPL
- * 
- * Copyright (c) 2010 appendTo LLC.
- * Dual licensed under the MIT or GPL licenses.
- * http://appendto.com/open-source-licenses
- */
-(function($) {
-       var _ajax = $.ajax,
-               mockHandlers = [];
-       
-       function parseXML(xml) {
-               if ( window['DOMParser'] == undefined && window.ActiveXObject ) {
-                       DOMParser = function() { };
-                       DOMParser.prototype.parseFromString = function( xmlString ) {
-                               var doc = new ActiveXObject('Microsoft.XMLDOM');
-                       doc.async = 'false';
-                       doc.loadXML( xmlString );
-                               return doc;
-                       };
-               }
-               
-               try {
-                       var xmlDoc      = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
-                       if ( $.isXMLDoc( xmlDoc ) ) {
-                               var err = $('parsererror', xmlDoc);
-                               if ( err.length == 1 ) {
-                                       throw('Error: ' + $(xmlDoc).text() );
-                               }
-                       } else {
-                               throw('Unable to parse XML');
-                       }
-               } catch( e ) {
-                       var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
-                       $(document).trigger('xmlParseError', [ msg ]);
-                       return undefined;
-               }
-               return xmlDoc;
-       }
-       
-       $.extend({
-               ajax: function(origSettings) {
-                       var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings),
-                           mock = false;
-                       // Iterate over our mock handlers (in registration order) until we find
-                       // one that is willing to intercept the request
-                       $.each(mockHandlers, function(k, v) {
-                               if ( !mockHandlers[k] ) {
-                                       return;
-                               }
-                               var m = null;
-                               // If the mock was registered with a function, let the function decide if we 
-                               // want to mock this request
-                               if ( $.isFunction(mockHandlers[k]) ) {
-                                       m = mockHandlers[k](s);
-                               } else {
-                                       m = mockHandlers[k];
-                                       // Inspect the URL of the request and check if the mock handler's url 
-                                       // matches the url for this ajax request
-                                       if ( $.isFunction(m.url.test) ) {
-                                               // The user provided a regex for the url, test it
-                                               if ( !m.url.test( s.url ) ) {
-                                                       m = null;
-                                               }
-                                       } else {
-                                               // Look for a simple wildcard '*' or a direct URL match
-                                               var star = m.url.indexOf('*');
-                                               if ( ( m.url != '*' && m.url != s.url && star == -1 ) ||
-                                                       ( star > -1 && m.url.substr(0, star) != s.url.substr(0, star) ) ) {
-                                                        // The url we tested did not match the wildcard *
-                                                        m = null;
-                                               }
-                                       }
-                                       if ( m ) {
-                                               // Inspect the data submitted in the request (either POST body or GET query string)
-                                               if ( m.data && s.data ) {
-                                                       var identical = false;
-                                                       // Deep inspect the identity of the objects
-                                                       (function ident(mock, live) {
-                                                               // Test for situations where the data is a querystring (not an object)
-                                                               if (typeof live === 'string') {
-                                                                       // Querystring may be a regex
-                                                                       identical = $.isFunction( mock.test ) ? mock.test(live) : mock == live;
-                                                                       return identical;
-                                                               }
-                                                               $.each(mock, function(k, v) {
-                                                                       if ( live[k] === undefined ) {
-                                                                               identical = false;
-                                                                               return false;
-                                                                       } else {
-                                                                               identical = true;
-                                                                               if ( typeof live[k] == 'object' ) {
-                                                                                       return ident(mock[k], live[k]);
-                                                                               } else {
-                                                                                       if ( $.isFunction( mock[k].test ) ) {
-                                                                                               identical = mock[k].test(live[k]);
-                                                                                       } else {
-                                                                                               identical = ( mock[k] == live[k] );
-                                                                                       }
-                                                                                       return identical;
-                                                                               }
-                                                                       }
-                                                               });
-                                                       })(m.data, s.data);
-                                                       // They're not identical, do not mock this request
-                                                       if ( identical == false ) {
-                                                               m = null;
-                                                       }
-                                               }
-                                               // Inspect the request type
-                                               if ( m && m.type && m.type != s.type ) {
-                                                       // The request type doesn't match (GET vs. POST)
-                                                       m = null;
-                                               }
-                                       }
-                               }
-                               if ( m ) {
-                                       mock = true;
-
-                                       // Handle console logging
-                                       var c = $.extend({}, $.mockjaxSettings, m);
-                                       if ( c.log && $.isFunction(c.log) ) {
-                                               c.log('MOCK ' + s.type.toUpperCase() + ': ' + s.url, $.extend({}, s));
-                                       }
-                                       
-                                       var jsre = /=\?(&|$)/, jsc = (new Date()).getTime();
-
-                                       // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here
-                                       // because there isn't an easy hook for the cross domain script tag of jsonp
-                                       if ( s.dataType === "jsonp" ) {
-                                               if ( s.type.toUpperCase() === "GET" ) {
-                                                       if ( !jsre.test( s.url ) ) {
-                                                               s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
-                                                       }
-                                               } else if ( !s.data || !jsre.test(s.data) ) {
-                                                       s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
-                                               }
-                                               s.dataType = "json";
-                                       }
-                       
-                                       // Build temporary JSONP function
-                                       if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
-                                               jsonp = s.jsonpCallback || ("jsonp" + jsc++);
-                       
-                                               // Replace the =? sequence both in the query string and the data
-                                               if ( s.data ) {
-                                                       s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
-                                               }
-                       
-                                               s.url = s.url.replace(jsre, "=" + jsonp + "$1");
-                       
-                                               // We need to make sure
-                                               // that a JSONP style response is executed properly
-                                               s.dataType = "script";
-                       
-                                               // Handle JSONP-style loading
-                                               window[ jsonp ] = window[ jsonp ] || function( tmp ) {
-                                                       data = tmp;
-                                                       success();
-                                                       complete();
-                                                       // Garbage collect
-                                                       window[ jsonp ] = undefined;
-                       
-                                                       try {
-                                                               delete window[ jsonp ];
-                                                       } catch(e) {}
-                       
-                                                       if ( head ) {
-                                                               head.removeChild( script );
-                                                       }
-                                               };
-                                       }
-                                       
-                                       var rurl = /^(\w+:)?\/\/([^\/?#]+)/,
-                                               parts = rurl.exec( s.url ),
-                                               remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
-                                       
-                                       // Test if we are going to create a script tag (if so, intercept & mock)
-                                       if ( s.dataType === "script" && s.type.toUpperCase() === "GET" && remote ) {
-                                               // Synthesize the mock request for adding a script tag
-                                               var callbackContext = origSettings && origSettings.context || s;
-                                               
-                                               function success() {
-                                                       // If a local callback was specified, fire it and pass it the data
-                                                       if ( s.success ) {
-                                                               s.success.call( callbackContext, ( m.response ? m.response.toString() : m.responseText || ''), status, {} );
-                                                       }
-                               
-                                                       // Fire the global callback
-                                                       if ( s.global ) {
-                                                               trigger( "ajaxSuccess", [{}, s] );
-                                                       }
-                                               }
-                               
-                                               function complete() {
-                                                       // Process result
-                                                       if ( s.complete ) {
-                                                               s.complete.call( callbackContext, {} , status );
-                                                       }
-                               
-                                                       // The request was completed
-                                                       if ( s.global ) {
-                                                               trigger( "ajaxComplete", [{}, s] );
-                                                       }
-                               
-                                                       // Handle the global AJAX counter
-                                                       if ( s.global && ! --jQuery.active ) {
-                                                               jQuery.event.trigger( "ajaxStop" );
-                                                       }
-                                               }
-                                               
-                                               function trigger(type, args) {
-                                                       (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
-                                               }
-                                               
-                                               if ( m.response && $.isFunction(m.response) ) {
-                                                       m.response(origSettings);
-                                               } else {
-                                                       $.globalEval(m.responseText);
-                                               }
-                                               success();
-                                               complete();
-                                               return false;
-                                       }
-                                       mock = _ajax.call($, $.extend(true, {}, origSettings, {
-                                               // Mock the XHR object
-                                               xhr: function() {
-                                                       // Extend with our default mockjax settings
-                                                       m = $.extend({}, $.mockjaxSettings, m);
-
-                                                       if ( m.contentType ) {
-                                                               m.headers['content-type'] = m.contentType;
-                                                       }
-
-                                                       // Return our mock xhr object
-                                                       return {
-                                                               status: m.status,
-                                                               readyState: 1,
-                                                               open: function() { },
-                                                               send: function() {
-                                                                       // This is a substitute for < 1.4 which lacks $.proxy
-                                                                       var process = (function(that) {
-                                                                               return function() {
-                                                                                       return (function() {
-                                                                                               // The request has returned
-                                                                                               this.status             = m.status;
-                                                                                               this.readyState         = 4;
-                                                                               
-                                                                                               // We have an executable function, call it to give 
-                                                                                               // the mock handler a chance to update it's data
-                                                                                               if ( $.isFunction(m.response) ) {
-                                                                                                       m.response(origSettings);
-                                                                                               }
-                                                                                               // Copy over our mock to our xhr object before passing control back to 
-                                                                                               // jQuery's onreadystatechange callback
-                                                                                               if ( s.dataType == 'json' && ( typeof m.responseText == 'object' ) ) {
-                                                                                                       this.responseText = JSON.stringify(m.responseText);
-                                                                                               } else if ( s.dataType == 'xml' ) {
-                                                                                                       if ( typeof m.responseXML == 'string' ) {
-                                                                                                               this.responseXML = parseXML(m.responseXML);
-                                                                                                       } else {
-                                                                                                               this.responseXML = m.responseXML;
-                                                                                                       }
-                                                                                               } else {
-                                                                                                       this.responseText = m.responseText;
-                                                                                               }
-                                                                                               // jQuery < 1.4 doesn't have onreadystate change for xhr
-                                                                                               if ( $.isFunction(this.onreadystatechange) ) {
-                                                                                                       this.onreadystatechange( m.isTimeout ? 'timeout' : undefined );
-                                                                                               }
-                                                                                       }).apply(that);
-                                                                               };
-                                                                       })(this);
-
-                                                                       if ( m.proxy ) {
-                                                                               // We're proxying this request and loading in an external file instead
-                                                                               _ajax({
-                                                                                       global: false,
-                                                                                       url: m.proxy,
-                                                                                       type: m.proxyType,
-                                                                                       data: m.data,
-                                                                                       dataType: s.dataType,
-                                                                                       complete: function(xhr, txt) {
-                                                                                               m.responseXML = xhr.responseXML;
-                                                                                               m.responseText = xhr.responseText;
-                                                                                               this.responseTimer = setTimeout(process, m.responseTime || 0);
-                                                                                       }
-                                                                               });
-                                                                       } else {
-                                                                               // type == 'POST' || 'GET' || 'DELETE'
-                                                                               if ( s.async === false ) {
-                                                                                       // TODO: Blocking delay
-                                                                                       process();
-                                                                               } else {
-                                                                                       this.responseTimer = setTimeout(process, m.responseTime || 50);
-                                                                               }
-                                                                       }
-                                                               },
-                                                               abort: function() {
-                                                                       clearTimeout(this.responseTimer);
-                                                               },
-                                                               setRequestHeader: function() { },
-                                                               getResponseHeader: function(header) {
-                                                                       // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery
-                                                                       if ( m.headers && m.headers[header] ) {
-                                                                               // Return arbitrary headers
-                                                                               return m.headers[header];
-                                                                       } else if ( header.toLowerCase() == 'last-modified' ) {
-                                                                               return m.lastModified || (new Date()).toString();
-                                                                       } else if ( header.toLowerCase() == 'etag' ) {
-                                                                               return m.etag || '';
-                                                                       } else if ( header.toLowerCase() == 'content-type' ) {
-                                                                               return m.contentType || 'text/plain';
-                                                                       }
-                                                               },
-                                                               getAllResponseHeaders: function() {
-                                                                       var headers = '';
-                                                                       $.each(m.headers, function(k, v) {
-                                                                               headers += k + ': ' + v + "\n";
-                                                                       });
-                                                                       return headers;
-                                                               }
-                                                       };
-                                               }
-                                       }));
-                                       return false;
-                               }
-                       });
-                       // We don't have a mock request, trigger a normal request
-                       if ( !mock ) {
-                               return _ajax.apply($, arguments);
-                       } else {
-                               return mock;
-                       }
-               }
-       });
-
-       $.mockjaxSettings = {
-               //url:        null,
-               //type:       'GET',
-               log:          function(msg) {
-                               window['console'] && window.console.log && window.console.log(msg);
-                             },
-               status:       200,
-               responseTime: 500,
-               isTimeout:    false,
-               contentType:  'text/plain',
-               response:     '', 
-               responseText: '',
-               responseXML:  '',
-               proxy:        '',
-               proxyType:    'GET',
-               
-               lastModified: null,
-               etag:         '',
-               headers: {
-                       etag: 'IJF@H#@923uf8023hFO@I#H#',
-                       'content-type' : 'text/plain'
-               }
-       };
-
-       $.mockjax = function(settings) {
-               var i = mockHandlers.length;
-               mockHandlers[i] = settings;
-               return i;
-       };
-       $.mockjaxClear = function(i) {
-               if ( arguments.length == 1 ) {
-                       mockHandlers[i] = null;
-               } else {
-                       mockHandlers = [];
-               }
-       };
-})(jQuery);
diff --git a/resources/lib/jquery/jquery.xmldom.js b/resources/lib/jquery/jquery.xmldom.js
deleted file mode 100644 (file)
index 85d0083..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*!
- * jQuery xmlDOM Plugin v1.0
- * http://outwestmedia.com/jquery-plugins/xmldom/
- *
- * Released: 2009-04-06
- * Version: 1.0
- *
- * Copyright (c) 2009 Jonathan Sharp, Out West Media LLC.
- * Dual licensed under the MIT and GPL licenses.
- * http://docs.jquery.com/License
- */
-(function($) {
-       // IE DOMParser wrapper
-       if ( window['DOMParser'] == undefined && window.ActiveXObject ) {
-               DOMParser = function() { };
-               DOMParser.prototype.parseFromString = function( xmlString ) {
-                       var doc = new ActiveXObject('Microsoft.XMLDOM');
-               doc.async = 'false';
-               doc.loadXML( xmlString );
-                       return doc;
-               };
-       }
-       
-       $.xmlDOM = function(xml, onErrorFn) {
-               try {
-                       var xmlDoc      = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
-                       if ( $.isXMLDoc( xmlDoc ) ) {
-                               var err = $('parsererror', xmlDoc);
-                               if ( err.length == 1 ) {
-                                       throw('Error: ' + $(xmlDoc).text() );
-                               }
-                       } else {
-                               throw('Unable to parse XML');
-                       }
-               } catch( e ) {
-                       var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
-                       if ( $.isFunction( onErrorFn ) ) {
-                               onErrorFn( msg );
-                       } else {
-                               $(document).trigger('xmlParseError', [ msg ]);
-                       }
-                       return $([]);
-               }
-               return $( xmlDoc );
-       };
-})(jQuery);
\ No newline at end of file
index 91b7035..674f62c 100644 (file)
         * @param {HTMLElement} titleElement Element with the title to update (may be the same as `element`)
         */
        function updateTooltipOnElement( element, titleElement ) {
-               var oldTitle, parts, regexp, newTitle, accessKeyLabel;
+               var oldTitle, parts, regexp, newTitle, accessKeyLabel,
+                       separatorMsg = mw.message( 'word-separator' ).plain();
 
                oldTitle = titleElement.title;
                if ( !oldTitle ) {
                        return;
                }
 
-               parts = ( mw.msg( 'word-separator' ) + mw.msg( 'brackets' ) ).split( '$1' );
+               parts = ( separatorMsg + mw.message( 'brackets' ).plain() ).split( '$1' );
                regexp = new RegExp( parts.map( mw.RegExp.escape ).join( '.*?' ) + '$' );
                newTitle = oldTitle.replace( regexp, '' );
                accessKeyLabel = getAccessKeyLabel( element );
 
                if ( accessKeyLabel ) {
                        // Should be build the same as in Linker::titleAttrib
-                       newTitle += mw.msg( 'word-separator' ) + mw.msg( 'brackets', accessKeyLabel );
+                       newTitle += separatorMsg + mw.message( 'brackets', accessKeyLabel ).plain();
                }
                if ( oldTitle !== newTitle ) {
                        titleElement.title = newTitle;
index cec537f..6c4b80d 100644 (file)
         */
        inspect.dumpTable = function ( data ) {
                try {
-                       // Bartosz made me put this here.
-                       if ( window.opera ) { throw window.opera; }
                        // Use Function.prototype#call to force an exception on Firefox,
                        // which doesn't define console#table but doesn't complain if you
                        // try to invoke it.
                } catch ( e ) {}
                try {
                        console.log( JSON.stringify( data, null, 2 ) );
-                       return;
                } catch ( e ) {}
-               mw.log( data );
        };
 
        /**
index 5fc1990..b2985d1 100644 (file)
@@ -4,7 +4,7 @@
  */
 /* global Uint32Array */
 ( function ( mw, $ ) {
-       var userInfoPromise, stickyRandomSessionId;
+       var userInfoPromise, pageviewRandomId;
 
        /**
         * Get the current user's groups or rights
 
                /**
                 * A sticky generateRandomSessionId for the current JS execution context,
-                * cached within this class.
+                * cached within this class (also known as a page view token).
                 *
+                * @since 1.32
                 * @return {string} 64 bit integer in hex format, padded
                 */
-               stickyRandomId: function () {
-                       if ( !stickyRandomSessionId ) {
-                               stickyRandomSessionId = mw.user.generateRandomSessionId();
+               getPageviewToken: function () {
+                       if ( !pageviewRandomId ) {
+                               pageviewRandomId = mw.user.generateRandomSessionId();
                        }
 
-                       return stickyRandomSessionId;
+                       return pageviewRandomId;
                },
 
                /**
                }
        } );
 
+       /**
+        * @method stickyRandomId
+        * @deprecated since 1.32 use getPageviewToken instead
+        */
+       mw.log.deprecate( mw.user, 'stickyRandomId', mw.user.getPageviewToken, 'Please use getPageviewToken instead' );
+
 }( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js b/resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js
new file mode 100644 (file)
index 0000000..e13c6fa
--- /dev/null
@@ -0,0 +1,142 @@
+( function ( $, mw ) {
+       /**
+        * A JavaScript version of CheckMatrixWidget.
+        *
+        * @class
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        * @cfg {Object} columns Required object representing the column labels and associated
+        *  tags in the matrix.
+        * @cfg {Object} rows Required object representing the row labels and associated
+        *  tags in the matrix.
+        * @cfg {string[]} [forcedOn] An array of column-row tags to be displayed as
+        *  enabled but unavailable to change
+        * @cfg {string[]} [forcedOff] An array of column-row tags to be displayed as
+        *  disnabled but unavailable to change
+        * @cfg {Object} Object mapping row label to tooltip content
+        */
+       mw.widgets.CheckMatrixWidget = function MWWCheckMatrixWidget( config ) {
+               var $headRow = $( '<tr>' ),
+                       $table = $( '<table>' ),
+                       widget = this;
+               config = config || {};
+
+               // Parent constructor
+               mw.widgets.CheckMatrixWidget.parent.call( this, config );
+               this.checkboxes = {};
+               this.name = config.name;
+               this.id = config.id;
+               this.rows = config.rows || {};
+               this.columns = config.columns || {};
+               this.tooltips = config.tooltips || [];
+               this.values = config.values || [];
+               this.forcedOn = config.forcedOn || [];
+               this.forcedOff = config.forcedOff || [];
+
+               // Build header
+               $headRow.append( $( '<td>' ).html( '&#160;' ) );
+
+               // Iterate over the columns object (ignore the value)
+               $.each( this.columns, function ( columnLabel ) {
+                       $headRow.append( $( '<td>' ).text( columnLabel ) );
+               } );
+               $table.append( $headRow );
+
+               // Build table
+               $.each( this.rows, function ( rowLabel, rowTag ) {
+                       var $row = $( '<tr>' ),
+                               labelField = new OO.ui.FieldLayout(
+                                       new OO.ui.Widget(), // Empty widget, since we don't have the checkboxes here
+                                       {
+                                               label: rowLabel,
+                                               help: widget.tooltips[ rowLabel ],
+                                               align: 'inline'
+                                       }
+                               );
+
+                       // Label
+                       $row.append( $( '<td>' ).append( labelField.$element ) );
+
+                       // Columns
+                       $.each( widget.columns, function ( columnLabel, columnTag ) {
+                               var thisTag = columnTag + '-' + rowTag,
+                                       checkbox = new OO.ui.CheckboxInputWidget( {
+                                               value: thisTag,
+                                               name: widget.name ? widget.name + '[]' : undefined,
+                                               id: widget.id ? widget.id + '-' + thisTag : undefined,
+                                               selected: widget.isTagSelected( thisTag ),
+                                               disabled: widget.isTagDisabled( thisTag )
+                                       } );
+
+                               widget.checkboxes[ thisTag ] = checkbox;
+                               $row.append( $( '<td>' ).append( checkbox.$element ) );
+                       } );
+
+                       $table.append( $row );
+               } );
+
+               this.$element
+                       .addClass( 'mw-widget-checkMatrixWidget' )
+                       .append( $table );
+       };
+
+       /* Setup */
+
+       OO.inheritClass( mw.widgets.CheckMatrixWidget, OO.ui.Widget );
+
+       /* Methods */
+
+       /**
+        * Check whether the given tag is selected
+        *
+        * @param {string} tagName Tag name
+        * @return {boolean} Tag is selected
+        */
+       mw.widgets.CheckMatrixWidget.prototype.isTagSelected = function ( tagName ) {
+               return (
+                       // If tag is not forced off
+                       this.forcedOff.indexOf( tagName ) === -1 &&
+                       (
+                               // If tag is in values
+                               this.values.indexOf( tagName ) > -1 ||
+                               // If tag is forced on
+                               this.forcedOn.indexOf( tagName ) > -1
+                       )
+               );
+       };
+
+       /**
+        * Check whether the given tag is disabled
+        *
+        * @param {string} tagName Tag name
+        * @return {boolean} Tag is disabled
+        */
+       mw.widgets.CheckMatrixWidget.prototype.isTagDisabled = function ( tagName ) {
+               return (
+                       // If the entire widget is disabled
+                       this.isDisabled() ||
+                       // If tag is forced off or forced on
+                       this.forcedOff.indexOf( tagName ) > -1 ||
+                       this.forcedOn.indexOf( tagName ) > -1
+               );
+       };
+       /**
+        * @inheritdoc
+        */
+       mw.widgets.CheckMatrixWidget.prototype.setDisabled = function ( isDisabled ) {
+               var widget = this;
+
+               // Parent method
+               mw.widgets.CheckMatrixWidget.parent.prototype.setDisabled.call( this, isDisabled );
+
+               // setDisabled sometimes gets called before the widget is ready
+               if ( this.checkboxes && Object.keys( this.checkboxes ).length > 0 ) {
+                       // Propagate to all checkboxes and update their disabled state
+                       $.each( this.checkboxes, function ( name, checkbox ) {
+                               checkbox.setDisabled( widget.isTagDisabled( name ) );
+                       } );
+               }
+       };
+}( jQuery, mediaWiki ) );
index 0757b34..067905e 100644 (file)
@@ -17,4 +17,8 @@ class ParserTestMockParser {
        ) {
                return new ParserOutput;
        }
+
+       public function getOutput() {
+               return new ParserOutput;
+       }
 }
index bcb3379..f76b1e3 100644 (file)
@@ -44,7 +44,6 @@ return [
                class_exists( PHPUnit_TextUI_Command::class ) ? [] : [ 'tests/phan/stubs/phpunit4.php' ],
                [
                        'maintenance/7zip.inc',
-                       'maintenance/backup.inc',
                        'maintenance/cleanupTable.inc',
                        'maintenance/CodeCleanerGlobalsPass.inc',
                        'maintenance/commandLine.inc',
index 0428335..30e1d0c 100644 (file)
@@ -73,6 +73,9 @@ class ApiComparePagesTest extends ApiTestCase {
                self::$repl['revF1'] = $this->addPage( 'F', "== Section 1 ==\nF 1.1\n\n== Section 2 ==\nF 1.2" );
                self::$repl['pageF'] = Title::newFromText( 'ApiComparePagesTest F' )->getArticleId();
 
+               self::$repl['revG1'] = $this->addPage( 'G', "== Section 1 ==\nG 1.1", CONTENT_MODEL_TEXT );
+               self::$repl['pageG'] = Title::newFromText( 'ApiComparePagesTest G' )->getArticleId();
+
                WikiPage::factory( Title::newFromText( 'ApiComparePagesTest C' ) )
                        ->doDeleteArticleReal( 'Test for ApiComparePagesTest' );
 
@@ -132,6 +135,7 @@ class ApiComparePagesTest extends ApiTestCase {
 
                $params += [
                        'action' => 'compare',
+                       'errorformat' => 'none',
                ];
 
                $user = $sysop
@@ -153,6 +157,25 @@ class ApiComparePagesTest extends ApiTestCase {
                }
        }
 
+       private static function makeDeprecationWarnings( ...$params ) {
+               $warn = [];
+               foreach ( $params as $p ) {
+                       $warn[] = [
+                               'code' => 'deprecation',
+                               'data' => [ 'feature' => "action=compare&{$p}" ],
+                               'module' => 'compare',
+                       ];
+                       if ( count( $warn ) === 1 ) {
+                               $warn[] = [
+                                       'code' => 'deprecation-help',
+                                       'module' => 'main',
+                               ];
+                       }
+               }
+
+               return $warn;
+       }
+
        public static function provideDiff() {
                // phpcs:disable Generic.Files.LineLength.TooLong
                return [
@@ -269,10 +292,12 @@ class ApiComparePagesTest extends ApiTestCase {
                        ],
                        'Basic diff, text' => [
                                [
-                                       'fromtext' => 'From text',
-                                       'fromcontentmodel' => 'wikitext',
-                                       'totext' => 'To text {{subst:PAGENAME}}',
-                                       'tocontentmodel' => 'wikitext',
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => 'From text',
+                                       'fromcontentmodel-main' => 'wikitext',
+                                       'toslots' => 'main',
+                                       'totext-main' => 'To text {{subst:PAGENAME}}',
+                                       'tocontentmodel-main' => 'wikitext',
                                ],
                                [
                                        'compare' => [
@@ -284,9 +309,11 @@ class ApiComparePagesTest extends ApiTestCase {
                        ],
                        'Basic diff, text 2' => [
                                [
-                                       'fromtext' => 'From text',
-                                       'totext' => 'To text {{subst:PAGENAME}}',
-                                       'tocontentmodel' => 'wikitext',
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => 'From text',
+                                       'toslots' => 'main',
+                                       'totext-main' => 'To text {{subst:PAGENAME}}',
+                                       'tocontentmodel-main' => 'wikitext',
                                ],
                                [
                                        'compare' => [
@@ -298,15 +325,13 @@ class ApiComparePagesTest extends ApiTestCase {
                        ],
                        'Basic diff, guessed model' => [
                                [
-                                       'fromtext' => 'From text',
-                                       'totext' => 'To text',
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => 'From text',
+                                       'toslots' => 'main',
+                                       'totext-main' => 'To text',
                                ],
                                [
-                                       'warnings' => [
-                                               'compare' => [
-                                                       'warnings' => 'No content model could be determined, assuming wikitext.',
-                                               ],
-                                       ],
+                                       'warnings' => [ [ 'code' => 'compare-nocontentmodel', 'module' => 'compare' ] ],
                                        'compare' => [
                                                'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
                                                        . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
@@ -316,9 +341,11 @@ class ApiComparePagesTest extends ApiTestCase {
                        ],
                        'Basic diff, text with title and PST' => [
                                [
-                                       'fromtext' => 'From text',
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => 'From text',
                                        'totitle' => 'Test',
-                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'toslots' => 'main',
+                                       'totext-main' => 'To text {{subst:PAGENAME}}',
                                        'topst' => true,
                                ],
                                [
@@ -331,9 +358,11 @@ class ApiComparePagesTest extends ApiTestCase {
                        ],
                        'Basic diff, text with page ID and PST' => [
                                [
-                                       'fromtext' => 'From text',
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => 'From text',
                                        'toid' => '{{REPL:pageB}}',
-                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'toslots' => 'main',
+                                       'totext-main' => 'To text {{subst:PAGENAME}}',
                                        'topst' => true,
                                ],
                                [
@@ -346,9 +375,11 @@ class ApiComparePagesTest extends ApiTestCase {
                        ],
                        'Basic diff, text with revision and PST' => [
                                [
-                                       'fromtext' => 'From text',
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => 'From text',
                                        'torev' => '{{REPL:revB2}}',
-                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'toslots' => 'main',
+                                       'totext-main' => 'To text {{subst:PAGENAME}}',
                                        'topst' => true,
                                ],
                                [
@@ -361,9 +392,11 @@ class ApiComparePagesTest extends ApiTestCase {
                        ],
                        'Basic diff, text with deleted revision and PST' => [
                                [
-                                       'fromtext' => 'From text',
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => 'From text',
                                        'torev' => '{{REPL:revC2}}',
-                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'toslots' => 'main',
+                                       'totext-main' => 'To text {{subst:PAGENAME}}',
                                        'topst' => true,
                                ],
                                [
@@ -378,20 +411,23 @@ class ApiComparePagesTest extends ApiTestCase {
                        'Basic diff, test with sections' => [
                                [
                                        'fromtitle' => 'ApiComparePagesTest F',
-                                       'fromsection' => 1,
-                                       'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?",
-                                       'tosection' => 2,
+                                       'fromslots' => 'main',
+                                       'fromtext-main' => "== Section 2 ==\nFrom text?",
+                                       'fromsection-main' => 2,
+                                       'totitle' => 'ApiComparePagesTest F',
+                                       'toslots' => 'main',
+                                       'totext-main' => "== Section 1 ==\nTo text?",
+                                       'tosection-main' => 1,
                                ],
                                [
                                        'compare' => [
                                                'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
                                                        . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
-                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>== Section <del class="diffchange diffchange-inline">1 </del>==</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>== Section <ins class="diffchange diffchange-inline">2 </ins>==</div></td></tr>' . "\n"
-                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">F 1.1</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To text?</ins></div></td></tr>' . "\n",
-                                               'fromid' => '{{REPL:pageF}}',
-                                               'fromrevid' => '{{REPL:revF1}}',
-                                               'fromns' => '0',
-                                               'fromtitle' => 'ApiComparePagesTest F',
+                                                       . '<tr><td class=\'diff-marker\'> </td><td class=\'diff-context\'><div>== Section 1 ==</div></td><td class=\'diff-marker\'> </td><td class=\'diff-context\'><div>== Section 1 ==</div></td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">F 1.1</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To text?</ins></div></td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'> </td><td class=\'diff-context\'></td><td class=\'diff-marker\'> </td><td class=\'diff-context\'></td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'> </td><td class=\'diff-context\'><div>== Section 2 ==</div></td><td class=\'diff-marker\'> </td><td class=\'diff-context\'><div>== Section 2 ==</div></td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From text?</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">F 1.2</ins></div></td></tr>' . "\n",
                                        ]
                                ],
                        ],
@@ -517,6 +553,197 @@ class ApiComparePagesTest extends ApiTestCase {
                                        ]
                                ],
                        ],
+                       'Diff for specific slots' => [
+                               // @todo Use a page with multiple slots here
+                               [
+                                       'fromrev' => '{{REPL:revA1}}',
+                                       'torev' => '{{REPL:revA3}}',
+                                       'prop' => 'diff',
+                                       'slots' => 'main',
+                               ],
+                               [
+                                       'compare' => [
+                                               'bodies' => [
+                                                       'main' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                               . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                               . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>A <del class="diffchange diffchange-inline">1</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>A <ins class="diffchange diffchange-inline">3</ins></div></td></tr>' . "\n",
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       // @todo Add a test for diffing with a deleted slot. Deleting 'main' doesn't work.
+
+                       'Basic diff, deprecated text' => [
+                               [
+                                       'fromtext' => 'From text',
+                                       'fromcontentmodel' => 'wikitext',
+                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'tocontentmodel' => 'wikitext',
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromtext', 'fromcontentmodel', 'totext', 'tocontentmodel' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">{{subst:PAGENAME}}</ins></div></td></tr>' . "\n",
+                                       ]
+                               ],
+                       ],
+                       'Basic diff, deprecated text 2' => [
+                               [
+                                       'fromtext' => 'From text',
+                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'tocontentmodel' => 'wikitext',
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromtext', 'totext', 'tocontentmodel' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">{{subst:PAGENAME}}</ins></div></td></tr>' . "\n",
+                                       ]
+                               ],
+                       ],
+                       'Basic diff, deprecated text, guessed model' => [
+                               [
+                                       'fromtext' => 'From text',
+                                       'totext' => 'To text',
+                               ],
+                               [
+                                       'warnings' => array_merge( self::makeDeprecationWarnings( 'fromtext', 'totext' ), [
+                                               [ 'code' => 'compare-nocontentmodel', 'module' => 'compare' ],
+                                       ] ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text</div></td></tr>' . "\n",
+                                       ]
+                               ],
+                       ],
+                       'Basic diff, deprecated text with title and PST' => [
+                               [
+                                       'fromtext' => 'From text',
+                                       'totitle' => 'Test',
+                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'topst' => true,
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromtext', 'totext' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">Test</ins></div></td></tr>' . "\n",
+                                       ]
+                               ],
+                       ],
+                       'Basic diff, deprecated text with page ID and PST' => [
+                               [
+                                       'fromtext' => 'From text',
+                                       'toid' => '{{REPL:pageB}}',
+                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'topst' => true,
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromtext', 'totext' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">ApiComparePagesTest B</ins></div></td></tr>' . "\n",
+                                       ]
+                               ],
+                       ],
+                       'Basic diff, deprecated text with revision and PST' => [
+                               [
+                                       'fromtext' => 'From text',
+                                       'torev' => '{{REPL:revB2}}',
+                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'topst' => true,
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromtext', 'totext' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">ApiComparePagesTest B</ins></div></td></tr>' . "\n",
+                                       ]
+                               ],
+                       ],
+                       'Basic diff, deprecated text with deleted revision and PST' => [
+                               [
+                                       'fromtext' => 'From text',
+                                       'torev' => '{{REPL:revC2}}',
+                                       'totext' => 'To text {{subst:PAGENAME}}',
+                                       'topst' => true,
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromtext', 'totext' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">ApiComparePagesTest C</ins></div></td></tr>' . "\n",
+                                       ]
+                               ],
+                               false, true
+                       ],
+                       'Basic diff, test with deprecated sections' => [
+                               [
+                                       'fromtitle' => 'ApiComparePagesTest F',
+                                       'fromsection' => 1,
+                                       'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?",
+                                       'tosection' => 2,
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromsection', 'totext', 'tosection' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>== Section <del class="diffchange diffchange-inline">1 </del>==</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>== Section <ins class="diffchange diffchange-inline">2 </ins>==</div></td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">F 1.1</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To text?</ins></div></td></tr>' . "\n",
+                                               'fromid' => '{{REPL:pageF}}',
+                                               'fromrevid' => '{{REPL:revF1}}',
+                                               'fromns' => '0',
+                                               'fromtitle' => 'ApiComparePagesTest F',
+                                       ]
+                               ],
+                       ],
+                       'Basic diff, test with deprecated sections and revdel, non-sysop' => [
+                               [
+                                       'fromrev' => '{{REPL:revB2}}',
+                                       'fromsection' => 0,
+                                       'torev' => '{{REPL:revB4}}',
+                                       'tosection' => 0,
+                               ],
+                               [],
+                               'missingcontent'
+                       ],
+                       'Basic diff, test with deprecated sections and revdel, sysop' => [
+                               [
+                                       'fromrev' => '{{REPL:revB2}}',
+                                       'fromsection' => 0,
+                                       'torev' => '{{REPL:revB4}}',
+                                       'tosection' => 0,
+                               ],
+                               [
+                                       'warnings' => self::makeDeprecationWarnings( 'fromsection', 'tosection' ),
+                                       'compare' => [
+                                               'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+                                                       . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+                                                       . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>B <del class="diffchange diffchange-inline">2</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>B <ins class="diffchange diffchange-inline">4</ins></div></td></tr>' . "\n",
+                                               'fromid' => '{{REPL:pageB}}',
+                                               'fromrevid' => '{{REPL:revB2}}',
+                                               'fromns' => 0,
+                                               'fromtitle' => 'ApiComparePagesTest B',
+                                               'fromtexthidden' => true,
+                                               'fromuserhidden' => true,
+                                               'fromcommenthidden' => true,
+                                               'toid' => '{{REPL:pageB}}',
+                                               'torevid' => '{{REPL:revB4}}',
+                                               'tons' => 0,
+                                               'totitle' => 'ApiComparePagesTest B',
+                                       ]
+                               ],
+                               false, true,
+                       ],
 
                        'Error, missing title' => [
                                [
@@ -647,6 +874,68 @@ class ApiComparePagesTest extends ApiTestCase {
                                [],
                                'missingcontent'
                        ],
+                       'Error, Relative diff, no prev' => [
+                               [
+                                       'fromrev' => '{{REPL:revA1}}',
+                                       'torelative' => 'prev',
+                                       'prop' => 'ids',
+                               ],
+                               [],
+                               'baddiff'
+                       ],
+                       'Error, Relative diff, no next' => [
+                               [
+                                       'fromrev' => '{{REPL:revA4}}',
+                                       'torelative' => 'next',
+                                       'prop' => 'ids',
+                               ],
+                               [],
+                               'baddiff'
+                       ],
+                       'Error, section diff with no revision' => [
+                               [
+                                       'fromtitle' => 'ApiComparePagesTest F',
+                                       'toslots' => 'main',
+                                       'totext-main' => "== Section 1 ==\nTo text?",
+                                       'tosection-main' => 1,
+                               ],
+                               [],
+                               'compare-notorevision',
+                       ],
+                       'Error, section diff with revdeleted revision' => [
+                               [
+                                       'fromtitle' => 'ApiComparePagesTest F',
+                                       'torev' => '{{REPL:revB2}}',
+                                       'toslots' => 'main',
+                                       'totext-main' => "== Section 1 ==\nTo text?",
+                                       'tosection-main' => 1,
+                               ],
+                               [],
+                               'missingcontent',
+                       ],
+                       'Error, section diff with a content model not supporting sections' => [
+                               [
+                                       'fromtitle' => 'ApiComparePagesTest G',
+                                       'torev' => '{{REPL:revG1}}',
+                                       'toslots' => 'main',
+                                       'totext-main' => "== Section 1 ==\nTo text?",
+                                       'tosection-main' => 1,
+                               ],
+                               [],
+                               'sectionsnotsupported',
+                       ],
+                       'Error, section diff with bad content model' => [
+                               [
+                                       'fromtitle' => 'ApiComparePagesTest F',
+                                       'torev' => '{{REPL:revF1}}',
+                                       'toslots' => 'main',
+                                       'totext-main' => "== Section 1 ==\nTo text?",
+                                       'tosection-main' => 1,
+                                       'tocontentmodel-main' => CONTENT_MODEL_TEXT,
+                               ],
+                               [],
+                               'sectionreplacefailed',
+                       ],
                ];
                // phpcs:enable
        }
index 60cda09..d5d33fb 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use Wikimedia\TestingAccessWrapper;
+
 /**
  * @covers ApiStashEdit
  * @group API
  * @group Database
  */
 class ApiStashEditTest extends ApiTestCase {
+       public function setUp() {
+               parent::setUp();
+
+               // We need caching here, but note that the cache gets cleared in between tests, so it
+               // doesn't work with @depends
+               $this->setMwGlobals( 'wgMainCacheType', 'hash' );
+       }
+
+       /**
+        * Make a stashedit API call with suitable default parameters
+        *
+        * @param array $params Query parameters for API request.  All are optional and will have
+        *   sensible defaults filled in.  To make a parameter actually not passed, set to null.
+        * @param User $user User to do the request
+        * @param string $expectedResult 'stashed', 'editconflict'
+        */
+       protected function doStash(
+               array $params = [], User $user = null, $expectedResult = 'stashed'
+       ) {
+               $params = array_merge( [
+                       'action' => 'stashedit',
+                       'title' => __CLASS__,
+                       'contentmodel' => 'wikitext',
+                       'contentformat' => 'text/x-wiki',
+                       'baserevid' => 0,
+               ], $params );
+               if ( !array_key_exists( 'text', $params ) &&
+                       !array_key_exists( 'stashedtexthash', $params )
+               ) {
+                       $params['text'] = 'Content';
+               }
+               foreach ( $params as $key => $val ) {
+                       if ( $val === null ) {
+                               unset( $params[$key] );
+                       }
+               }
+
+               if ( isset( $params['text'] ) ) {
+                       $expectedText = $params['text'];
+               } elseif ( isset( $params['stashedtexthash'] ) ) {
+                       $expectedText = $this->getStashedText( $params['stashedtexthash'] );
+               }
+               if ( isset( $expectedText ) ) {
+                       $expectedText = rtrim( str_replace( "\r\n", "\n", $expectedText ) );
+                       $expectedHash = sha1( $expectedText );
+                       $origText = $this->getStashedText( $expectedHash );
+               }
+
+               $res = $this->doApiRequestWithToken( $params, null, $user );
+
+               $this->assertSame( $expectedResult, $res[0]['stashedit']['status'] );
+               $this->assertCount( $expectedResult === 'stashed' ? 2 : 1, $res[0]['stashedit'] );
+
+               if ( $expectedResult === 'stashed' ) {
+                       $hash = $res[0]['stashedit']['texthash'];
+
+                       $this->assertSame( $expectedText, $this->getStashedText( $hash ) );
+
+                       $this->assertSame( $expectedHash, $hash );
+
+                       if ( isset( $params['stashedtexthash'] ) ) {
+                               $this->assertSame( $params['stashedtexthash'], $expectedHash, 'Sanity' );
+                       }
+               } else {
+                       $this->assertSame( $origText, $this->getStashedText( $expectedHash ) );
+               }
+
+               $this->assertArrayNotHasKey( 'warnings', $res[0] );
+
+               return $res;
+       }
+
+       /**
+        * Return the text stashed for $hash.
+        *
+        * @param string $hash
+        * @return string
+        */
+       protected function getStashedText( $hash ) {
+               $cache = ObjectCache::getLocalClusterInstance();
+               $key = $cache->makeKey( 'stashedit', 'text', $hash );
+               return $cache->get( $key );
+       }
+
+       /**
+        * Return a key that can be passed to the cache to obtain a PreparedEdit object.
+        *
+        * @param string $title Title of page
+        * @param string Content $text Content of edit
+        * @param User $user User who made edit
+        * @return string
+        */
+       protected function getStashKey( $title = __CLASS__, $text = 'Content', User $user = null ) {
+               $titleObj = Title::newFromText( $title );
+               $content = new WikitextContent( $text );
+               if ( !$user ) {
+                       $user = $this->getTestSysop()->getUser();
+               }
+               $wrapper = TestingAccessWrapper::newFromClass( ApiStashEdit::class );
+               return $wrapper->getStashKey( $titleObj, $wrapper->getContentHash( $content ), $user );
+       }
 
        public function testBasicEdit() {
-               $apiResult = $this->doApiRequestWithToken(
-                       [
-                               'action' => 'stashedit',
-                               'title' => 'ApistashEdit_Page',
-                               'contentmodel' => 'wikitext',
-                               'contentformat' => 'text/x-wiki',
-                               'text' => 'Text for ' . __METHOD__ . ' page',
-                               'baserevid' => 0,
-                       ]
+               $this->doStash();
+       }
+
+       public function testBot() {
+               // @todo This restriction seems arbitrary, is there any good reason to keep it?
+               $this->setExpectedApiException( 'apierror-botsnotsupported' );
+
+               $this->doStash( [], $this->getTestUser( [ 'bot' ] )->getUser() );
+       }
+
+       public function testUnrecognizedFormat() {
+               $this->setExpectedApiException(
+                       [ 'apierror-badformat-generic', 'application/json', 'wikitext' ] );
+
+               $this->doStash( [ 'contentformat' => 'application/json' ] );
+       }
+
+       public function testMissingTextAndStashedTextHash() {
+               $this->setExpectedApiException( [
+                       'apierror-missingparam-one-of',
+                       Message::listParam( [ '<var>stashedtexthash</var>', '<var>text</var>' ] ),
+                       2
+               ] );
+               $this->doStash( [ 'text' => null ] );
+       }
+
+       public function testStashedTextHash() {
+               $res = $this->doStash();
+
+               $this->doStash( [ 'stashedtexthash' => $res[0]['stashedit']['texthash'] ] );
+       }
+
+       public function testMalformedStashedTextHash() {
+               $this->setExpectedApiException( 'apierror-stashedit-missingtext' );
+               $this->doStash( [ 'stashedtexthash' => 'abc' ] );
+       }
+
+       public function testMissingStashedTextHash() {
+               $this->setExpectedApiException( 'apierror-stashedit-missingtext' );
+               $this->doStash( [ 'stashedtexthash' => str_repeat( '0', 40 ) ] );
+       }
+
+       public function testHashNormalization() {
+               $res1 = $this->doStash( [ 'text' => "a\r\nb\rc\nd \t\n\r" ] );
+               $res2 = $this->doStash( [ 'text' => "a\nb\rc\nd" ] );
+
+               $this->assertSame( $res1[0]['stashedit']['texthash'], $res2[0]['stashedit']['texthash'] );
+               $this->assertSame( "a\nb\rc\nd",
+                       $this->getStashedText( $res1[0]['stashedit']['texthash'] ) );
+       }
+
+       public function testNonexistentBaseRevId() {
+               $this->setExpectedApiException( [ 'apierror-nosuchrevid', pow( 2, 31 ) - 1 ] );
+
+               $name = ucfirst( __FUNCTION__ );
+               $this->editPage( $name, '' );
+               $this->doStash( [ 'title' => $name, 'baserevid' => pow( 2, 31 ) - 1 ] );
+       }
+
+       public function testPageWithNoRevisions() {
+               $name = ucfirst( __FUNCTION__ );
+               $rev = $this->editPage( $name, '' )->value['revision'];
+
+               $this->setExpectedApiException( [ 'apierror-missingrev-pageid', $rev->getPage() ] );
+
+               // Corrupt the database.  @todo Does the API really need to fail gracefully for this case?
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'page',
+                       [ 'page_latest' => 0 ],
+                       [ 'page_id' => $rev->getPage() ],
+                       __METHOD__
                );
-               $apiResult = $apiResult[0];
-               $this->assertArrayHasKey( 'stashedit', $apiResult );
-               $this->assertEquals( 'stashed', $apiResult['stashedit']['status'] );
+
+               $this->doStash( [ 'title' => $name, 'baserevid' => $rev->getId() ] );
+       }
+
+       public function testExistingPage() {
+               $name = ucfirst( __FUNCTION__ );
+               $rev = $this->editPage( $name, '' )->value['revision'];
+
+               $this->doStash( [ 'title' => $name, 'baserevid' => $rev->getId() ] );
+       }
+
+       public function testInterveningEdit() {
+               $name = ucfirst( __FUNCTION__ );
+               $oldRev = $this->editPage( $name, "A\n\nB" )->value['revision'];
+               $this->editPage( $name, "A\n\nC" );
+
+               $this->doStash( [
+                       'title' => $name,
+                       'baserevid' => $oldRev->getId(),
+                       'text' => "D\n\nB",
+               ] );
+       }
+
+       public function testEditConflict() {
+               $name = ucfirst( __FUNCTION__ );
+               $oldRev = $this->editPage( $name, 'A' )->value['revision'];
+               $this->editPage( $name, 'B' );
+
+               $this->doStash( [
+                       'title' => $name,
+                       'baserevid' => $oldRev->getId(),
+                       'text' => 'C',
+               ], null, 'editconflict' );
+       }
+
+       public function testDeletedRevision() {
+               $name = ucfirst( __FUNCTION__ );
+               $oldRev = $this->editPage( $name, 'A' )->value['revision'];
+               $this->editPage( $name, 'B' );
+
+               $this->setExpectedApiException( [ 'apierror-missingcontent-pageid', $oldRev->getPage() ] );
+
+               $this->revisionDelete( $oldRev );
+
+               $this->doStash( [
+                       'title' => $name,
+                       'baserevid' => $oldRev->getId(),
+                       'text' => 'C',
+               ] );
        }
 
+       public function testDeletedRevisionSection() {
+               $name = ucfirst( __FUNCTION__ );
+               $oldRev = $this->editPage( $name, 'A' )->value['revision'];
+               $this->editPage( $name, 'B' );
+
+               $this->setExpectedApiException( 'apierror-sectionreplacefailed' );
+
+               $this->revisionDelete( $oldRev );
+
+               $this->doStash( [
+                       'title' => $name,
+                       'baserevid' => $oldRev->getId(),
+                       'text' => 'C',
+                       'section' => '1',
+               ] );
+       }
+
+       public function testPingLimiter() {
+               global $wgRateLimits;
+
+               $this->stashMwGlobals( 'wgRateLimits' );
+               $wgRateLimits['stashedit'] = [ '&can-bypass' => false, 'user' => [ 1, 60 ] ];
+
+               $this->doStash( [ 'text' => 'A' ] );
+
+               $this->doStash( [ 'text' => 'B' ], null, 'ratelimited' );
+       }
+
+       /**
+        * Shortcut for calling ApiStashEdit::checkCache() without having to create Titles and Contents
+        * in every test.
+        *
+        * @param User $user
+        * @param string $text The text of the article
+        * @return stdClass|bool Return value of ApiStashEdit::checkCache(), false if not in cache
+        */
+       protected function doCheckCache( User $user, $text = 'Content' ) {
+               return ApiStashEdit::checkCache(
+                       Title::newFromText( __CLASS__ ),
+                       new WikitextContent( $text ),
+                       $user
+               );
+       }
+
+       public function testCheckCache() {
+               $user = $this->getMutableTestUser()->getUser();
+
+               $this->doStash( [], $user );
+
+               $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
+
+               // Another user doesn't see the cache
+               $this->assertFalse(
+                       $this->doCheckCache( $this->getTestUser()->getUser() ),
+                       'Cache is user-specific'
+               );
+
+               // Nor does the original one if they become a bot
+               $user->addGroup( 'bot' );
+               $this->assertFalse(
+                       $this->doCheckCache( $user ),
+                       "We assume bots don't have cache entries"
+               );
+
+               // But other groups are okay
+               $user->removeGroup( 'bot' );
+               $user->addGroup( 'sysop' );
+               $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
+       }
+
+       public function testCheckCacheAnon() {
+               $user = new User();
+
+               $this->doStash( [], $user );
+
+               $this->assertInstanceOf( stdClass::class, $this->docheckCache( $user ) );
+       }
+
+       /**
+        * Stash an edit some time in the past, for testing expiry and freshness logic.
+        *
+        * @param User $user Who's doing the editing
+        * @param string $text What text should be cached
+        * @param int $howOld How many seconds is "old" (we actually set it one second before this)
+        */
+       protected function doStashOld(
+               User $user, $text = 'Content', $howOld = ApiStashEdit::PRESUME_FRESH_TTL_SEC
+       ) {
+               $this->doStash( [ 'text' => $text ], $user );
+
+               // Monkey with the cache to make the edit look old.  @todo Is there a less fragile way to
+               // fake the time?
+               $key = $this->getStashKey( __CLASS__, $text, $user );
+
+               $cache = ObjectCache::getLocalClusterInstance();
+
+               $editInfo = $cache->get( $key );
+               $editInfo->output->setCacheTime( wfTimestamp( TS_MW,
+                       wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ) - $howOld - 1 ) );
+
+               $cache->set( $key, $editInfo );
+       }
+
+       public function testCheckCacheOldNoEdits() {
+               $user = $this->getTestSysop()->getUser();
+
+               $this->doStashOld( $user );
+
+               // Should still be good, because no intervening edits
+               $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
+       }
+
+       public function testCheckCacheOldNoEditsAnon() {
+               // Specify a made-up IP address to make sure no edits are lying around
+               $user = User::newFromName( '192.0.2.77', false );
+
+               $this->doStashOld( $user );
+
+               // Should still be good, because no intervening edits
+               $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
+       }
+
+       public function testCheckCacheInterveningEdits() {
+               $user = $this->getTestSysop()->getUser();
+
+               $this->doStashOld( $user );
+
+               // Now let's also increment our editcount
+               $this->editPage( ucfirst( __FUNCTION__ ), '' );
+
+               $this->assertFalse( $this->doCheckCache( $user ),
+                       "Cache should be invalidated when it's old and the user has an intervening edit" );
+       }
+
+       /**
+        * @dataProvider signatureProvider
+        * @param string $text Which signature to test (~~~, ~~~~, or ~~~~~)
+        * @param int $ttl Expected TTL in seconds
+        */
+       public function testSignatureTtl( $text, $ttl ) {
+               $this->doStash( [ 'text' => $text ] );
+
+               $cache = ObjectCache::getLocalClusterInstance();
+               $key = $this->getStashKey( __CLASS__, $text );
+
+               $wrapper = TestingAccessWrapper::newFromObject( $cache );
+
+               $this->assertEquals( $ttl, $wrapper->bag[$key][HashBagOStuff::KEY_EXP] - time(), '', 1 );
+       }
+
+       public function signatureProvider() {
+               return [
+                       '~~~' => [ '~~~', ApiStashEdit::MAX_SIGNATURE_TTL ],
+                       '~~~~' => [ '~~~~', ApiStashEdit::MAX_SIGNATURE_TTL ],
+                       '~~~~~' => [ '~~~~~', ApiStashEdit::MAX_SIGNATURE_TTL ],
+               ];
+       }
+
+       public function testIsInternal() {
+               $res = $this->doApiRequest( [
+                       'action' => 'paraminfo',
+                       'modules' => 'stashedit',
+               ] );
+
+               $this->assertCount( 1, $res[0]['paraminfo']['modules'] );
+               $this->assertSame( true, $res[0]['paraminfo']['modules'][0]['internal'] );
+       }
+
+       public function testBusy() {
+               // @todo This doesn't work because both lock acquisitions are in the same MySQL session, so
+               // they don't conflict.  How do I open a different session?
+               $this->markTestSkipped();
+
+               $key = $this->getStashKey();
+               $this->db->lock( $key, __METHOD__, 0 );
+               try {
+                       $this->doStash( [], null, 'busy' );
+               } finally {
+                       $this->db->unlock( $key, __METHOD__ );
+               }
+       }
 }
index e1b98ec..20f0039 100644 (file)
@@ -164,7 +164,7 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
                        // getGenderCache() provides a mock that considers first
                        // names ending in "a" to be female.
                        [ NS_USER, 'Lisa_Müller', '', 'de', 'Benutzerin:Lisa Müller' ],
-                       [ 1000000, 'Invalid_namespace', '', 'en', ':Invalid namespace' ],
+                       [ 1000000, 'Invalid_namespace', '', 'en', 'Special:Badtitle/NS1000000:Invalid namespace' ],
                ];
        }
 
@@ -195,7 +195,7 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
                        [ NS_MAIN, 'Remote_page', '', 'remotetestiw', 'en', 'remotetestiw:Remote_page' ],
 
                        // non-existent namespace
-                       [ 10000000, 'Foobar', '', '', 'en', ':Foobar' ],
+                       [ 10000000, 'Foobar', '', '', 'en', 'Special:Badtitle/NS10000000:Foobar' ],
                ];
        }
 
index f0a3543..7044069 100644 (file)
@@ -97,9 +97,9 @@
                assert.notEqual( result, result2, 'different when called multiple times' );
        } );
 
-       QUnit.test( 'stickyRandomId', function ( assert ) {
-               var result = mw.user.stickyRandomId(),
-                       result2 = mw.user.stickyRandomId();
+       QUnit.test( 'getPageviewToken', function ( assert ) {
+               var result = mw.user.getPageviewToken(),
+                       result2 = mw.user.getPageviewToken();
                assert.strictEqual( typeof result, 'string', 'type' );
                assert.strictEqual( /^[a-f0-9]{16}$/.test( result ), true, '16 HEX symbols string' );
                assert.strictEqual( result2, result, 'sticky' );