Merge "Fix navigation buttons on Special:Listfiles for first and last page"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 17 Jul 2014 00:20:43 +0000 (00:20 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 17 Jul 2014 00:20:43 +0000 (00:20 +0000)
338 files changed:
RELEASE-NOTES-1.24
api.php
docs/hooks.txt
includes/AutoLoader.php
includes/Block.php
includes/ChangesFeed.php [deleted file]
includes/DefaultSettings.php
includes/EditPage.php
includes/GitInfo.php
includes/GlobalFunctions.php
includes/Html.php
includes/Init.php [deleted file]
includes/MWNamespace.php [new file with mode: 0644]
includes/MediaWiki.php [new file with mode: 0644]
includes/MimeMagic.php
includes/Namespace.php [deleted file]
includes/OutputPage.php
includes/PHPVersionError.php
includes/Preferences.php
includes/PrefixSearch.php
includes/Sanitizer.php
includes/Setup.php
includes/Skin.php
includes/SkinTemplate.php
includes/Title.php
includes/User.php
includes/UserMailer.php
includes/WebStart.php
includes/Wiki.php [deleted file]
includes/actions/Action.php
includes/actions/FormAction.php
includes/actions/FormlessAction.php
includes/actions/RevertAction.php
includes/api/ApiBase.php
includes/api/ApiFormatBase.php
includes/api/ApiMain.php
includes/api/ApiQuery.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiResult.php
includes/cache/LocalisationCache.php
includes/changes/ChangesFeed.php [new file with mode: 0644]
includes/clientpool/RedisConnectionPool.php
includes/context/DerivativeContext.php
includes/context/RequestContext.php
includes/db/DatabaseMssql.php
includes/db/DatabasePostgres.php
includes/db/LoadBalancer.php
includes/debug/Debug.php [deleted file]
includes/debug/MWDebug.php [new file with mode: 0644]
includes/filerepo/file/File.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormField.php
includes/installer/CliInstaller.php
includes/installer/Installer.php
includes/installer/LocalSettingsGenerator.php
includes/installer/MssqlInstaller.php
includes/installer/MssqlUpdater.php
includes/installer/PostgresInstaller.php
includes/installer/WebInstaller.php
includes/installer/i18n/bto.json [new file with mode: 0644]
includes/installer/i18n/ca.json
includes/installer/i18n/cs.json
includes/installer/i18n/de.json
includes/installer/i18n/en.json
includes/installer/i18n/eo.json
includes/installer/i18n/es.json
includes/installer/i18n/fa.json
includes/installer/i18n/fr.json
includes/installer/i18n/gl.json
includes/installer/i18n/he.json
includes/installer/i18n/id.json
includes/installer/i18n/ko.json
includes/installer/i18n/mk.json
includes/installer/i18n/nb.json
includes/installer/i18n/pl.json
includes/installer/i18n/pt-br.json
includes/installer/i18n/qqq.json
includes/installer/i18n/ru.json
includes/installer/i18n/sv.json
includes/installer/i18n/tr.json
includes/installer/i18n/zh-hans.json
includes/jobqueue/JobQueueRedis.php
includes/libs/CSSMin.php
includes/libs/HttpStatus.php
includes/media/Bitmap.php
includes/media/ImageHandler.php
includes/media/Jpeg.php
includes/media/MediaHandler.php
includes/media/PNG.php
includes/objectcache/MemcachedPeclBagOStuff.php
includes/objectcache/ObjectCache.php
includes/objectcache/SqlBagOStuff.php
includes/page/Article.php
includes/page/WikiFilePage.php
includes/page/WikiPage.php
includes/parser/CoreParserFunctions.php
includes/parser/MWTidy.php [new file with mode: 0644]
includes/parser/ParserOptions.php
includes/parser/Tidy.php [deleted file]
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/revisiondelete/RevisionDelete.php
includes/search/SearchMySQL.php
includes/search/SearchResultSet.php
includes/search/SearchSqlite.php
includes/specials/SpecialActiveusers.php
includes/specials/SpecialListfiles.php
includes/specials/SpecialMergeHistory.php
includes/specials/SpecialSearch.php
includes/specials/SpecialUnblock.php
includes/specials/SpecialUpload.php
includes/specials/SpecialVersion.php
includes/specials/SpecialWantedfiles.php
includes/templates/Userlogin.php
index.php
languages/Language.php
languages/Names.php
languages/i18n/ar.json
languages/i18n/arq.json
languages/i18n/arz.json
languages/i18n/az.json
languages/i18n/bar.json
languages/i18n/bcc.json
languages/i18n/bcl.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bho.json
languages/i18n/bn.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/ce.json
languages/i18n/cs.json
languages/i18n/cy.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/eu.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gan-hans.json
languages/i18n/gd.json
languages/i18n/gl.json
languages/i18n/gsw.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/ia.json
languages/i18n/id.json
languages/i18n/ilo.json
languages/i18n/is.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/kiu.json
languages/i18n/kk-cyrl.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/mg.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/mt.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/oc.json
languages/i18n/pa.json
languages/i18n/pl.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ro.json
languages/i18n/ru.json
languages/i18n/sl.json
languages/i18n/sq.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/uk.json
languages/i18n/vi.json
languages/i18n/wuu.json
languages/i18n/yi.json
languages/i18n/yo.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesEn.php
load.php
maintenance/benchmarks/benchmarkParse.php
maintenance/install.php
maintenance/resources/update-oojs-ui.sh
maintenance/runJobs.php
maintenance/update.php
mw-config/index.php
resources/Resources.php
resources/lib/es5-shim/es5-shim.js
resources/lib/jquery/jquery.cookie.js
resources/lib/oojs-ui/i18n/ar.json
resources/lib/oojs-ui/i18n/ca.json
resources/lib/oojs-ui/i18n/cs.json
resources/lib/oojs-ui/i18n/de.json
resources/lib/oojs-ui/i18n/en.json
resources/lib/oojs-ui/i18n/es.json
resources/lib/oojs-ui/i18n/et.json
resources/lib/oojs-ui/i18n/fa.json
resources/lib/oojs-ui/i18n/fi.json
resources/lib/oojs-ui/i18n/fr.json
resources/lib/oojs-ui/i18n/gd.json [new file with mode: 0644]
resources/lib/oojs-ui/i18n/gl.json
resources/lib/oojs-ui/i18n/he.json
resources/lib/oojs-ui/i18n/hu.json
resources/lib/oojs-ui/i18n/ia.json
resources/lib/oojs-ui/i18n/it.json
resources/lib/oojs-ui/i18n/lb.json
resources/lib/oojs-ui/i18n/lv.json
resources/lib/oojs-ui/i18n/mk.json
resources/lib/oojs-ui/i18n/pl.json
resources/lib/oojs-ui/i18n/pt.json
resources/lib/oojs-ui/i18n/qqq.json
resources/lib/oojs-ui/i18n/ro.json
resources/lib/oojs-ui/i18n/ru.json
resources/lib/oojs-ui/i18n/sq.json
resources/lib/oojs-ui/i18n/sr-ec.json
resources/lib/oojs-ui/i18n/sv.json
resources/lib/oojs-ui/i18n/uk.json
resources/lib/oojs-ui/i18n/vi.json
resources/lib/oojs-ui/i18n/yi.json
resources/lib/oojs-ui/i18n/zh-hans.json
resources/lib/oojs-ui/images/anchor.svg [new file with mode: 0644]
resources/lib/oojs-ui/images/tail.svg [deleted file]
resources/lib/oojs-ui/oojs-ui-agora.css
resources/lib/oojs-ui/oojs-ui-apex.css
resources/lib/oojs-ui/oojs-ui.js
resources/lib/oojs-ui/oojs-ui.svg.css
resources/lib/sinonjs/sinon-1.10.3.js [new file with mode: 0644]
resources/lib/sinonjs/sinon-1.9.0.js [deleted file]
resources/lib/sinonjs/sinon-ie-1.10.3.js [new file with mode: 0644]
resources/lib/sinonjs/sinon-ie-1.9.0.js [deleted file]
resources/src/es5-skip.js
resources/src/jquery.json-deprecate.js
resources/src/jquery.ui-themes/vector/jquery.ui.theme.css
resources/src/jquery/jquery.arrowSteps.js
resources/src/jquery/jquery.makeCollapsible.js
resources/src/jquery/jquery.textSelection.js
resources/src/mediawiki.api/mediawiki.api.edit.js
resources/src/mediawiki.hidpi-skip.js [new file with mode: 0644]
resources/src/mediawiki.less/mediawiki.mixins.less
resources/src/mediawiki.less/mediawiki.ui/mixins.less [new file with mode: 0644]
resources/src/mediawiki.less/mediawiki.ui/variables.less [new file with mode: 0644]
resources/src/mediawiki.page/mediawiki.page.gallery.js
resources/src/mediawiki.skinning/content.parsoid.less
resources/src/mediawiki.special/mediawiki.special.search.css
resources/src/mediawiki.ui/components/buttons.less [new file with mode: 0644]
resources/src/mediawiki.ui/components/default/buttons.less [deleted file]
resources/src/mediawiki.ui/components/default/forms.less [deleted file]
resources/src/mediawiki.ui/components/forms.less [new file with mode: 0644]
resources/src/mediawiki.ui/components/utilities.less
resources/src/mediawiki.ui/components/vector/buttons.less [deleted file]
resources/src/mediawiki.ui/components/vector/containers.less [deleted file]
resources/src/mediawiki.ui/components/vector/forms.less [deleted file]
resources/src/mediawiki.ui/default.less
resources/src/mediawiki.ui/mixins/effects.less [deleted file]
resources/src/mediawiki.ui/mixins/forms.less [deleted file]
resources/src/mediawiki.ui/mixins/type.less [deleted file]
resources/src/mediawiki.ui/mixins/utilities.less [deleted file]
resources/src/mediawiki.ui/settings/colors.less [deleted file]
resources/src/mediawiki.ui/settings/typography.less [deleted file]
resources/src/mediawiki.ui/vector.less [deleted file]
resources/src/mediawiki/mediawiki.feedback.js
resources/src/mediawiki/mediawiki.js
skins/MonoBook/i18n/be.json
skins/MonoBook/i18n/ca.json
skins/MonoBook/i18n/gl.json
skins/MonoBook/i18n/id.json
skins/MonoBook/i18n/pt.json
skins/MonoBook/i18n/zh-hant.json
skins/Vector/components/common.less
skins/Vector/components/notifications.less
skins/Vector/i18n/be.json
skins/Vector/i18n/ca.json
skins/Vector/i18n/gl.json
skins/Vector/i18n/id.json
skins/Vector/i18n/lrc.json
skins/Vector/i18n/nb.json
skins/Vector/i18n/uk.json
skins/Vector/i18n/zh-hant.json
skins/common/images/tipsy-arrow.gif [deleted file]
skins/common/shared.css
tests/parser/parserTests.txt
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/GitInfoTest.php
tests/phpunit/includes/GlobalFunctions/GlobalTest.php
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/TitlePermissionTest.php
tests/phpunit/includes/filerepo/file/FileTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/MWMessagePackTest.php
tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
tests/phpunit/includes/media/BitmapScalingTest.php
tests/phpunit/includes/media/DjVuTest.php
tests/phpunit/includes/media/ExifBitmapTest.php
tests/phpunit/includes/media/ExifRotationTest.php
tests/phpunit/includes/media/ExifTest.php
tests/phpunit/includes/media/FakeDimensionFile.php
tests/phpunit/includes/media/FormatMetadataTest.php
tests/phpunit/includes/media/GIFMetadataExtractorTest.php
tests/phpunit/includes/media/GIFTest.php
tests/phpunit/includes/media/IPTCTest.php
tests/phpunit/includes/media/JpegMetadataExtractorTest.php
tests/phpunit/includes/media/JpegTest.php
tests/phpunit/includes/media/MediaHandlerTest.php
tests/phpunit/includes/media/PNGMetadataExtractorTest.php
tests/phpunit/includes/media/PNGTest.php
tests/phpunit/includes/media/SVGMetadataExtractorTest.php
tests/phpunit/includes/media/SVGTest.php
tests/phpunit/includes/media/TiffTest.php
tests/phpunit/includes/media/XCFTest.php
tests/phpunit/includes/media/XMPTest.php
tests/phpunit/includes/media/XMPValidateTest.php
tests/phpunit/includes/specials/SpecialSearchTest.php
tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php
tests/phpunit/maintenance/getSlaveServerTest.php [deleted file]
tests/phpunit/structure/AutoLoaderTest.php
tests/qunit/QUnitTestResources.php
tests/qunit/data/testrunner.js
tests/testHelpers.inc
thumb.php

index 9c605d0..f9280fc 100644 (file)
@@ -1,6 +1,5 @@
-Security reminder: If you have PHP's register_globals option set, you should
-turn it off. Although MediaWiki will work with it enabled, it exposes your
-server to potential security vulnerabilities.
+Security reminder: If you have PHP's register_globals option set, you must
+turn it off. MediaWiki will no longer work with it enabled.
 
 == MediaWiki 1.24 ==
 
@@ -10,6 +9,9 @@ MediaWiki 1.24 is an alpha-quality branch and is not recommended for use in
 production.
 
 === Configuration changes in 1.24 ===
+* MediaWiki will no longer run if register_globals is enabled. It has been
+  deprecated for 5 years now, and was removed in PHP 5.4. For more information
+  about why, see <https://www.mediawiki.org/wiki/register_globals>.
 * The server's canonical hostname is available as $wgServerName, which is
   exposed in both mw.config and ApiQuerySiteInfo.
 * Introduced $wgPagePropsHaveSortkey as a backwards-compatibility switch,
@@ -27,6 +29,14 @@ production.
   prefixes (i.e. turned into interlanguage links when $wgInterwikiMagic is set
   to true).
 * $wgParserTestRemote has been removed.
+* $wgCountTotalSearchHits has been removed. If you're concerned about efficiency
+  of search, you should use something like CirrusSearch instead of built in
+  search.
+* Users in the 'sysop' group have access to Special:MergeHistory by default.
+* $wgFileStore was removed after having been deprecated in 1.17. Alternative
+  configurations are $wgDeletedDirectory and $wgHashedUploadDirectory.
+* The deprecated $wgUseCommaCount variable has been removed.
+* $wgEnableSorbs and $wgSorbsUrl have been removed.
 
 === New features in 1.24 ===
 * Added a new hook, "WhatLinksHereProps", to allow extensions to annotate
@@ -102,6 +112,10 @@ production.
   The feature needs to be enabled with $wgPageLanguageUseDB=true and
   permission needs to be set for 'pagelang'.
 * Upgrade Moment.js to v2.7.0.
+* (bug 67042) Added support for the HTML5 <rtc> tag for East Asian typography.
+* Upgrade Sinon.JS to 1.10.3.
+* Added the es5-shim polyfill for older or non-compliant javascript engines.
+* Upgrade jQuery Cookie to v1.2.0.
 
 === Bug fixes in 1.24 ===
 * (bug 49116) Footer copyright notice is now always displayed in user language
@@ -211,6 +225,27 @@ changes to languages because of Bugzilla reports.
   set of hooks has been removed and replaced by a single new hook
   SpecialPageBeforeFormDisplay.
 * (bug 65781) Removed block warning on included {{Special:Contributions}}
+* Removed Skin::makeGlobalVariablesScript. (deprecated since 1.19)
+* Removed MWNamespace::isMain(). (deprecated since 1.19)
+* Removed Preferences::loadOldSearchNs(). (deprecated since 1.19)
+* Removed OutputPage::getStatusMessage(). (deprecated since 1.18)
+* Removed OutputPage::isUserJsAllowed(). (deprecated since 1.18)
+* Removed Title::updateTitleProtection(). (deprecated since 1.19)
+* Removed ParserOptions::setSkin(). (deprecated since 1.19)
+* Removed Title::escapeCanonicalURL(). (deprecated since 1.19)
+* Removed Title::escapeLocalURL(). (deprecated since 1.19)
+* Removed Title::escapeFullURL(). (deprecated since 1.19)
+* Removed User::isValidEmailAddr(). (deprecated since 1.18)
+* Removed Title::getEscapedText(). (deprecated since 1.19)
+* Removed Language::getFallbackLanguageCode(). (deprecated since 1.19)
+* Removed WikiPage::isBigDeletion(). (deprecated since 1.19)
+* Removed MWInit class which contained functions related to a now discontinued
+  PHP compiler called hphpc. (deprecated since 1.22)
+* ApiResult::enableSizeCheck() and disableSizeCheck() are now obsolete.
+* Removed ResourceLoaderGetStartupModules hook. (deprecated since 1.23)
+* Removed getFormFields(), onSubmit() and onSuccess() from FormlessAction, as
+  these were meant specifically for FormAction instead.
+* Removed Action::execute().
 
 ==== Renamed classes ====
 * CLDRPluralRuleConverter_Expression to CLDRPluralRuleConverterExpression
@@ -254,6 +289,7 @@ changes to languages because of Bugzilla reports.
 * IPBlockForm - Use SpecialBlock directly
 * WatchlistEditor - Use SpecialEditWatchlist directly
 * FormatExif - Use FormatMetadata directly
+* RevertFileAction - Use RevertAction directly
 
 == Compatibility ==
 
diff --git a/api.php b/api.php
index e55ec75..e8a911e 100644 (file)
--- a/api.php
+++ b/api.php
@@ -34,7 +34,7 @@
 define( 'MW_API', true );
 
 // Bail if PHP is too low
-if ( !function_exists( 'version_compare' ) || version_compare( phpversion(), '5.3.2' ) < 0 ) {
+if ( !function_exists( 'version_compare' ) || version_compare( PHP_VERSION, '5.3.2' ) < 0 ) {
        // We need to use dirname( __FILE__ ) here cause __DIR__ is PHP5.3+
        require dirname( __FILE__ ) . '/includes/PHPVersionError.php';
        wfPHPVersionError( 'api.php' );
index b1ef47c..0bbcd0e 100644 (file)
@@ -1712,8 +1712,8 @@ $refreshLinks: RefreshLinks object
 'MagicWordwgVariableIDs': When defining new magic words IDs.
 $variableIDs: array of strings
 
-'MakeGlobalVariablesScript': Called right before Skin::makeVariablesScript is
-executed. Ideally, this hook should only be used to add variables that depend on
+'MakeGlobalVariablesScript': Called at end of OutputPage::getJSVars.
+Ideally, this hook should only be used to add variables that depend on
 the current page/request; static configuration should be added through
 ResourceLoaderGetConfigVars instead.
 &$vars: variable (or multiple variables) to be added into the output of
@@ -1757,6 +1757,30 @@ caches.
 $title: name of the page changed.
 $text: new contents of the page.
 
+'MimeMagicInit': Before processing the list mapping MIME types to media types
+and the list mapping MIME types to file extensions.
+As an extension author, you are encouraged to submit patches to MediaWiki's
+core to add new MIME types to mime.types.
+$mimeMagic: Instance of MimeMagic.
+  Use $mimeMagic->addExtraInfo( $stringOfInfo );
+  for adding new MIME info to the list.
+  Use $mimeMagic->addExtraTypes( $stringOfTypes );
+  for adding new MIME types to the list.
+
+'MimeMagicImproveFromExtension': Allows MW extensions to further improve the
+MIME type detected by considering the file extension.
+$mimeMagic: Instance of MimeMagic.
+$ext: File extension.
+&$mime: MIME type (in/out).
+
+'MimeMagicGuessFromContent': Allows MW extensions guess the MIME by content.
+$mimeMagic: Instance of MimeMagic.
+&$head: First 1024 bytes of the file in a string (in - Do not alter!).
+&$tail: More or equal than last 65558 bytes of the file in a string
+  (in - Do not alter!).
+$file: File path.
+&$mime: MIME type (out).
+
 'ModifyExportQuery': Modify the query used by the exporter.
 $db: The database object to be queried.
 &$tables: Tables in the query.
@@ -2134,12 +2158,6 @@ configuration variables to JavaScript. Things that depend on the current page
 or request state must be added through MakeGlobalVariablesScript instead.
 &$vars: array( variable name => value )
 
-'ResourceLoaderGetStartupModules': DEPRECATED. Run once the startup module is being
-generated. This allows you to add modules to the startup module. This hook
-should be used sparingly since any module added here will be loaded on all
-pages. This hook is useful if you want to make code available to module loader
-scripts.
-
 'ResourceLoaderRegisterModules': Right before modules information is required,
 such as when responding to a resource
 loader request or generating HTML output.
@@ -2896,6 +2914,15 @@ to be switched to HTTPS.
 $user: User in question.
 &$https: Boolean whether $user should be switched to HTTPS.
 
+'UserResetAllOptions': Called in User::resetOptions() when user preferences
+have been requested to be reset. This hook can be used to exclude certain
+options from being reset even when the user has requested all prefs to be reset,
+because certain options might be stored in the user_properties database table
+despite not being visible and editable via Special:Preferences.
+$user: the User (object) whose preferences are being reset
+&$newOptions: array of new (site default) preferences
+$options: array of the user's old preferences
+$resetKinds: array containing the kinds of preferences to reset
 
 'UserRetrieveNewTalks': Called when retrieving "You have new messages!"
 message(s).
@@ -2936,6 +2963,11 @@ invalidated and GetExtendedMetadata hook called again).
 $timestamp: The timestamp metadata was generated
 $file: The file the metadata is for
 
+'UserMailerChangeReturnPath': Called to generate a VERP return address
+when UserMailer sends an email, with a bounce handling extension.
+$to: Array of MailAddress objects for the recipients
+&$returnPath: The return address string
+
 'WantedPages::getQueryInfo': Called in WantedPagesPage::getQueryInfo(), can be
 used to alter the SQL query which gets the list of wanted pages.
 &$wantedPages: WantedPagesPage object
index 67f9a1c..016886b 100644 (file)
@@ -42,7 +42,6 @@ $wgAutoloadLocalClasses = array(
        'Category' => 'includes/Category.php',
        'Categoryfinder' => 'includes/Categoryfinder.php',
        'CategoryViewer' => 'includes/CategoryViewer.php',
-       'ChangesFeed' => 'includes/ChangesFeed.php',
        'ChangeTags' => 'includes/ChangeTags.php',
        'ChannelFeed' => 'includes/Feed.php',
        'Collation' => 'includes/Collation.php',
@@ -114,21 +113,14 @@ $wgAutoloadLocalClasses = array(
        'ImportStringSource' => 'includes/Import.php',
        'IndexPager' => 'includes/Pager.php',
        'Interwiki' => 'includes/interwiki/Interwiki.php',
-       'LCStore' => 'includes/cache/LocalisationCache.php',
-       'LCStoreAccel' => 'includes/cache/LocalisationCache.php',
-       'LCStoreCDB' => 'includes/cache/LocalisationCache.php',
-       'LCStoreDB' => 'includes/cache/LocalisationCache.php',
-       'LCStoreNull' => 'includes/cache/LocalisationCache.php',
        'License' => 'includes/Licenses.php',
        'Licenses' => 'includes/Licenses.php',
        'Linker' => 'includes/Linker.php',
        'LinkFilter' => 'includes/LinkFilter.php',
-       'LocalisationCache' => 'includes/cache/LocalisationCache.php',
-       'LocalisationCacheBulkLoad' => 'includes/cache/LocalisationCache.php',
        'MagicWord' => 'includes/MagicWord.php',
        'MagicWordArray' => 'includes/MagicWord.php',
        'MailAddress' => 'includes/UserMailer.php',
-       'MediaWiki' => 'includes/Wiki.php',
+       'MediaWiki' => 'includes/MediaWiki.php',
        'MediaWikiI18N' => 'includes/SkinTemplate.php',
        'MediaWikiVersionFetcher' => 'includes/MediaWikiVersionFetcher.php',
        'Message' => 'includes/Message.php',
@@ -136,8 +128,7 @@ $wgAutoloadLocalClasses = array(
        'MimeMagic' => 'includes/MimeMagic.php',
        'MWHookException' => 'includes/Hooks.php',
        'MWHttpRequest' => 'includes/HttpFunctions.php',
-       'MWInit' => 'includes/Init.php',
-       'MWNamespace' => 'includes/Namespace.php',
+       'MWNamespace' => 'includes/MWNamespace.php',
        'OutputPage' => 'includes/OutputPage.php',
        'Pager' => 'includes/Pager.php',
        'PasswordError' => 'includes/User.php',
@@ -190,7 +181,6 @@ $wgAutoloadLocalClasses = array(
        'User' => 'includes/User.php',
        'UserArray' => 'includes/UserArray.php',
        'UserArrayFromResult' => 'includes/UserArrayFromResult.php',
-       'UserCache' => 'includes/cache/UserCache.php',
        'UserMailer' => 'includes/UserMailer.php',
        'UserRightsProxy' => 'includes/UserRightsProxy.php',
        'WatchedItem' => 'includes/WatchedItem.php',
@@ -227,7 +217,6 @@ $wgAutoloadLocalClasses = array(
        'RawPage' => 'includes/actions/RawAction.php',
        'RenderAction' => 'includes/actions/RenderAction.php',
        'RevertAction' => 'includes/actions/RevertAction.php',
-       'RevertFileAction' => 'includes/actions/RevertAction.php',
        'RevisiondeleteAction' => 'includes/actions/RevisiondeleteAction.php',
        'RollbackAction' => 'includes/actions/RollbackAction.php',
        'SubmitAction' => 'includes/actions/EditAction.php',
@@ -354,14 +343,23 @@ $wgAutoloadLocalClasses = array(
        'GenderCache' => 'includes/cache/GenderCache.php',
        'GlobalDependency' => 'includes/cache/CacheDependency.php',
        'HTMLFileCache' => 'includes/cache/HTMLFileCache.php',
+       'LCStore' => 'includes/cache/LocalisationCache.php',
+       'LCStoreAccel' => 'includes/cache/LocalisationCache.php',
+       'LCStoreCDB' => 'includes/cache/LocalisationCache.php',
+       'LCStoreDB' => 'includes/cache/LocalisationCache.php',
+       'LCStoreNull' => 'includes/cache/LocalisationCache.php',
        'LinkBatch' => 'includes/cache/LinkBatch.php',
        'LinkCache' => 'includes/cache/LinkCache.php',
+       'LocalisationCache' => 'includes/cache/LocalisationCache.php',
+       'LocalisationCacheBulkLoad' => 'includes/cache/LocalisationCache.php',
        'MapCacheLRU' => 'includes/cache/MapCacheLRU.php',
        'MessageCache' => 'includes/cache/MessageCache.php',
        'ObjectFileCache' => 'includes/cache/ObjectFileCache.php',
        'ResourceFileCache' => 'includes/cache/ResourceFileCache.php',
+       'UserCache' => 'includes/cache/UserCache.php',
 
        # includes/changes
+       'ChangesFeed' => 'includes/changes/ChangesFeed.php',
        'ChangesList' => 'includes/changes/ChangesList.php',
        'EnhancedChangesList' => 'includes/changes/EnhancedChangesList.php',
        'OldChangesList' => 'includes/changes/OldChangesList.php',
@@ -465,7 +463,7 @@ $wgAutoloadLocalClasses = array(
        'SQLiteField' => 'includes/db/DatabaseSqlite.php',
 
        # includes/debug
-       'MWDebug' => 'includes/debug/Debug.php',
+       'MWDebug' => 'includes/debug/MWDebug.php',
 
        # includes/deferred
        'DataUpdate' => 'includes/deferred/DataUpdate.php',
@@ -800,8 +798,8 @@ $wgAutoloadLocalClasses = array(
        'CoreTagHooks' => 'includes/parser/CoreTagHooks.php',
        'DateFormatter' => 'includes/parser/DateFormatter.php',
        'LinkHolderArray' => 'includes/parser/LinkHolderArray.php',
-       'MWTidy' => 'includes/parser/Tidy.php',
-       'MWTidyWrapper' => 'includes/parser/Tidy.php',
+       'MWTidy' => 'includes/parser/MWTidy.php',
+       'MWTidyWrapper' => 'includes/parser/MWTidy.php',
        'PPCustomFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
        'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
        'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php',
index 3896369..c393a79 100644 (file)
@@ -1368,7 +1368,7 @@ class Block {
                        $this->getId(),
                        $lang->formatExpiry( $this->mExpiry ),
                        (string)$intended,
-                       $lang->timeanddate( wfTimestamp( TS_MW, $this->mTimestamp ), true ),
+                       $lang->userTimeAndDate( $this->mTimestamp, $context->getUser() ),
                );
        }
 }
diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php
deleted file mode 100644 (file)
index fb491e5..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-<?php
-/**
- * Feed for list of changes.
- *
- * 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
- */
-
-/**
- * Feed to Special:RecentChanges and Special:RecentChangesLiked
- *
- * @ingroup Feed
- */
-class ChangesFeed {
-       public $format, $type, $titleMsg, $descMsg;
-
-       /**
-        * Constructor
-        *
-        * @param string $format Feed's format (either 'rss' or 'atom')
-        * @param string $type Type of feed (for cache keys)
-        */
-       public function __construct( $format, $type ) {
-               $this->format = $format;
-               $this->type = $type;
-       }
-
-       /**
-        * Get a ChannelFeed subclass object to use
-        *
-        * @param string $title Feed's title
-        * @param string $description Feed's description
-        * @param string $url Url of origin page
-        * @return ChannelFeed|bool ChannelFeed subclass or false on failure
-        */
-       public function getFeedObject( $title, $description, $url ) {
-               global $wgSitename, $wgLanguageCode, $wgFeedClasses;
-
-               if ( !isset( $wgFeedClasses[$this->format] ) ) {
-                       return false;
-               }
-
-               if ( !array_key_exists( $this->format, $wgFeedClasses ) ) {
-                       // falling back to atom
-                       $this->format = 'atom';
-               }
-
-               $feedTitle = "$wgSitename  - {$title} [$wgLanguageCode]";
-               return new $wgFeedClasses[$this->format](
-                       $feedTitle, htmlspecialchars( $description ), $url );
-       }
-
-       /**
-        * Generates feed's content
-        *
-        * @param ChannelFeed $feed ChannelFeed subclass object (generally the one returned
-        *   by getFeedObject())
-        * @param ResultWrapper $rows ResultWrapper object with rows in recentchanges table
-        * @param int $lastmod Timestamp of the last item in the recentchanges table (only
-        *   used for the cache key)
-        * @param FormOptions $opts As in SpecialRecentChanges::getDefaultOptions()
-        * @return null|bool True or null
-        */
-       public function execute( $feed, $rows, $lastmod, $opts ) {
-               global $wgLang, $wgRenderHashAppend;
-
-               if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
-                       return null;
-               }
-
-               $optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend;
-               $timekey = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash, 'timestamp' );
-               $key = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash );
-
-               FeedUtils::checkPurge( $timekey, $key );
-
-               /**
-                * Bumping around loading up diffs can be pretty slow, so where
-                * possible we want to cache the feed output so the next visitor
-                * gets it quick too.
-                */
-               $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key );
-               if ( is_string( $cachedFeed ) ) {
-                       wfDebug( "RC: Outputting cached feed\n" );
-                       $feed->httpHeaders();
-                       echo $cachedFeed;
-               } else {
-                       wfDebug( "RC: rendering new feed and caching it\n" );
-                       ob_start();
-                       self::generateFeed( $rows, $feed );
-                       $cachedFeed = ob_get_contents();
-                       ob_end_flush();
-                       $this->saveToCache( $cachedFeed, $timekey, $key );
-               }
-               return true;
-       }
-
-       /**
-        * Save to feed result to $messageMemc
-        *
-        * @param string $feed Feed's content
-        * @param string $timekey Memcached key of the last modification
-        * @param string $key Memcached key of the content
-        */
-       public function saveToCache( $feed, $timekey, $key ) {
-               global $messageMemc;
-               $expire = 3600 * 24; # One day
-               $messageMemc->set( $key, $feed, $expire );
-               $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
-       }
-
-       /**
-        * Try to load the feed result from $messageMemc
-        *
-        * @param int $lastmod Timestamp of the last item in the recentchanges table
-        * @param string $timekey Memcached key of the last modification
-        * @param string $key Memcached key of the content
-        * @return string|bool Feed's content on cache hit or false on cache miss
-        */
-       public function loadFromCache( $lastmod, $timekey, $key ) {
-               global $wgFeedCacheTimeout, $wgOut, $messageMemc;
-
-               $feedLastmod = $messageMemc->get( $timekey );
-
-               if ( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) {
-                       /**
-                        * If the cached feed was rendered very recently, we may
-                        * go ahead and use it even if there have been edits made
-                        * since it was rendered. This keeps a swarm of requests
-                        * from being too bad on a super-frequently edited wiki.
-                        */
-
-                       $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod );
-                       $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod );
-                       $lastmodUnix = wfTimestamp( TS_UNIX, $lastmod );
-
-                       if ( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix ) {
-                               wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
-                               if ( $feedLastmodUnix < $lastmodUnix ) {
-                                       $wgOut->setLastModified( $feedLastmod ); // bug 21916
-                               }
-                               return $messageMemc->get( $key );
-                       } else {
-                               wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Generate the feed items given a row from the database, printing the feed.
-        * @param object $rows DatabaseBase resource with recentchanges rows
-        * @param Feed $feed
-        */
-       public static function generateFeed( $rows, &$feed ) {
-               wfProfileIn( __METHOD__ );
-               $items = self::buildItems( $rows );
-               $feed->outHeader();
-               foreach ( $items as $item ) {
-                       $feed->outItem( $item );
-               }
-               $feed->outFooter();
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Generate the feed items given a row from the database.
-        * @param object $rows DatabaseBase resource with recentchanges rows
-        */
-       public static function buildItems( $rows ) {
-               wfProfileIn( __METHOD__ );
-               $items = array();
-
-               # Merge adjacent edits by one user
-               $sorted = array();
-               $n = 0;
-               foreach ( $rows as $obj ) {
-                       if ( $n > 0 &&
-                               $obj->rc_type == RC_EDIT &&
-                               $obj->rc_namespace >= 0 &&
-                               $obj->rc_cur_id == $sorted[$n - 1]->rc_cur_id &&
-                               $obj->rc_user_text == $sorted[$n - 1]->rc_user_text ) {
-                               $sorted[$n - 1]->rc_last_oldid = $obj->rc_last_oldid;
-                       } else {
-                               $sorted[$n] = $obj;
-                               $n++;
-                       }
-               }
-
-               foreach ( $sorted as $obj ) {
-                       $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
-                       $talkpage = MWNamespace::canTalk( $obj->rc_namespace )
-                               ? $title->getTalkPage()->getFullURL()
-                               : '';
-
-                       // Skip items with deleted content (avoids partially complete/inconsistent output)
-                       if ( $obj->rc_deleted ) {
-                               continue;
-                       }
-
-                       if ( $obj->rc_this_oldid ) {
-                               $url = $title->getFullURL( array(
-                                       'diff' => $obj->rc_this_oldid,
-                                       'oldid' => $obj->rc_last_oldid,
-                               ) );
-                       } else {
-                               // log entry or something like that.
-                               $url = $title->getFullURL();
-                       }
-
-                       $items[] = new FeedItem(
-                               $title->getPrefixedText(),
-                               FeedUtils::formatDiff( $obj ),
-                               $url,
-                               $obj->rc_timestamp,
-                               ( $obj->rc_deleted & Revision::DELETED_USER )
-                                       ? wfMessage( 'rev-deleted-user' )->escaped() : $obj->rc_user_text,
-                               $talkpage
-                       );
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $items;
-       }
-}
index 11196ae..7c81294 100644 (file)
@@ -355,11 +355,6 @@ $wgEnableAsyncUploads = false;
  */
 $wgIllegalFileChars = ":";
 
-/**
- * @deprecated since 1.17 use $wgDeletedDirectory
- */
-$wgFileStore = array();
-
 /**
  * What directory to place deleted uploads in.
  * Defaults to "{$wgUploadDirectory}/deleted".
@@ -1217,6 +1212,33 @@ $wgThumbLimits = array(
        300
 );
 
+/**
+ * When defined, is an array of image widths used as buckets for thumbnail generation.
+ * The goal is to save resources by generating thumbnails based on reference buckets instead of
+ * always using the original. This will incur a speed gain but cause a quality loss.
+ *
+ * The buckets generation is chained, with each bucket generated based on the above bucket
+ * when possible. File handlers have to opt into using that feature. For now only BitmapHandler
+ * supports it.
+ */
+$wgThumbnailBuckets = null;
+
+/**
+ * When using thumbnail buckets as defined above, this sets the minimum distance with the bucket
+ * above the requested size. The distance represents how pany extra pixels of width the bucket needs
+ * in order to be used as the reference for a given thumbnail. For example, with the following buckets:
+ *
+ * $wgThumbnailBuckets = array ( 128, 256, 512 );
+ *
+ * and a distance of 50:
+ *
+ * $wgThumbnailMinimumBucketDistance = 50;
+ *
+ * If we want to render a thumbnail of width 220px, the 512px bucket will be used,
+ * because 220 + 50 = 270 and the closest bucket bigger than 270px is 512.
+ */
+$wgThumbnailMinimumBucketDistance = 0;
+
 /**
  * Default parameters for the "<gallery>" tag
  */
@@ -2163,6 +2185,12 @@ $wgCachePages = true;
  */
 $wgCacheEpoch = '20030516000000';
 
+/**
+ * Directory where GitInfo will look for pre-computed cache files. If false,
+ * $wgCacheDirectory/gitinfo will be used.
+ */
+$wgGitInfoCacheDirectory = false;
+
 /**
  * Bump this number when changing the global style sheets and JavaScript.
  *
@@ -3951,9 +3979,6 @@ $wgTranscludeCacheExpiry = 3600;
  * - 'any': all pages as considered as valid articles
  * - 'comma': the page must contain a comma to be considered valid
  * - 'link': the page must contain a [[wiki link]] to be considered valid
- * - null: the value will be set at run time depending on $wgUseCommaCount:
- *         if $wgUseCommaCount is false, it will be 'link', if it is true
- *         it will be 'comma'
  *
  * See also See https://www.mediawiki.org/wiki/Manual:Article_count
  *
@@ -3961,13 +3986,7 @@ $wgTranscludeCacheExpiry = 3600;
  * to update it, you will need to run the maintenance/updateArticleCount.php
  * script.
  */
-$wgArticleCountMethod = null;
-
-/**
- * Backward compatibility setting, will set $wgArticleCountMethod if it is null.
- * @deprecated since 1.18; use $wgArticleCountMethod instead
- */
-$wgUseCommaCount = false;
+$wgArticleCountMethod = 'link';
 
 /**
  * wgHitcounterUpdateFreq sets how often page counters should be updated, higher
@@ -4364,7 +4383,7 @@ $wgGroupPermissions['sysop']['unblockself'] = true;
 $wgGroupPermissions['sysop']['suppressredirect'] = true;
 #$wgGroupPermissions['sysop']['pagelang'] = true;
 #$wgGroupPermissions['sysop']['upload_by_url'] = true;
-#$wgGroupPermissions['sysop']['mergehistory'] = true;
+$wgGroupPermissions['sysop']['mergehistory'] = true;
 
 // Permission to change users' group assignments
 $wgGroupPermissions['bureaucrat']['userrights'] = true;
@@ -4679,12 +4698,6 @@ $wgSummarySpamRegex = array();
  */
 $wgEnableDnsBlacklist = false;
 
-/**
- * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for
- * backward compatibility.
- */
-$wgEnableSorbs = false;
-
 /**
  * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true.
  *
@@ -4710,12 +4723,6 @@ $wgEnableSorbs = false;
  */
 $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' );
 
-/**
- * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for
- * backward compatibility.
- */
-$wgSorbsUrl = array();
-
 /**
  * Proxy whitelist, list of addresses that are assumed to be non-proxy despite
  * what the other methods might say.
@@ -5278,18 +5285,6 @@ $wgAdvancedSearchHighlighting = false;
  */
 $wgSearchHighlightBoundaries = '[\p{Z}\p{P}\p{C}]';
 
-/**
- * Set to true to have the search engine count total
- * search matches to present in the Special:Search UI.
- * Not supported by every search engine shipped with MW.
- *
- * This could however be slow on larger wikis, and is pretty flaky
- * with the current title vs content split. Recommend avoiding until
- * that's been worked out cleanly; but this may aid in testing the
- * search UI and API to confirm that the result count works.
- */
-$wgCountTotalSearchHits = false;
-
 /**
  * Template for OpenSearch suggestions, defaults to API action=opensearch
  *
index 3d57e95..88c49a7 100644 (file)
@@ -1523,6 +1523,37 @@ class EditPage {
                return true;
        }
 
+       /**
+        * Return the summary to be used for a new section.
+        *
+        * @param string $sectionanchor Set to the section anchor text
+        * @return string
+        */
+       private function newSectionSummary( &$sectionanchor = null ) {
+               global $wgParser;
+
+               if ( $this->sectiontitle !== '' ) {
+                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
+                       // If no edit summary was specified, create one automatically from the section
+                       // title and have it link to the new section. Otherwise, respect the summary as
+                       // passed.
+                       if ( $this->summary === '' ) {
+                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
+                               return wfMessage( 'newsectionsummary' )
+                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
+                       }
+               } elseif ( $this->summary !== '' ) {
+                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
+                       # This is a new section, so create a link to the new section
+                       # in the revision summary.
+                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
+                       return wfMessage( 'newsectionsummary' )
+                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
+               } else {
+                       return $this->summary;
+               }
+       }
+
        /**
         * Attempt submission (no UI)
         *
@@ -1764,31 +1795,11 @@ class EditPage {
                                if ( $this->sectiontitle !== '' ) {
                                        // Insert the section title above the content.
                                        $content = $content->addSectionHeader( $this->sectiontitle );
-
-                                       // Jump to the new section
-                                       $result['sectionanchor'] =
-                                               $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-
-                                       // If no edit summary was specified, create one automatically from the section
-                                       // title and have it link to the new section. Otherwise, respect the summary as
-                                       // passed.
-                                       if ( $this->summary === '' ) {
-                                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary' )
-                                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
-                                       }
                                } elseif ( $this->summary !== '' ) {
                                        // Insert the section title above the content.
                                        $content = $content->addSectionHeader( $this->summary );
-
-                                       // Jump to the new section
-                                       $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
-
-                                       // Create a link to the new section from the edit summary.
-                                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary' )
-                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
                                }
+                               $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
                        }
 
                        $status->value = self::AS_SUCCESS_NEW_ARTICLE;
@@ -1806,7 +1817,8 @@ class EditPage {
                                $this->isConflict = true;
                                if ( $this->section == 'new' ) {
                                        if ( $this->mArticle->getUserText() == $wgUser->getName() &&
-                                               $this->mArticle->getComment() == $this->summary ) {
+                                               $this->mArticle->getComment() == $this->newSectionSummary()
+                                       ) {
                                                // Probably a duplicate submission of a new comment.
                                                // This can happen when squid resends a request after
                                                // a timeout but the first one actually went through.
@@ -1920,24 +1932,7 @@ class EditPage {
                        wfProfileIn( __METHOD__ . '-sectionanchor' );
                        $sectionanchor = '';
                        if ( $this->section == 'new' ) {
-                               if ( $this->sectiontitle !== '' ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-                                       // If no edit summary was specified, create one automatically from the section
-                                       // title and have it link to the new section. Otherwise, respect the summary as
-                                       // passed.
-                                       if ( $this->summary === '' ) {
-                                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary' )
-                                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
-                                       }
-                               } elseif ( $this->summary !== '' ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
-                                       # This is a new section, so create a link to the new section
-                                       # in the revision summary.
-                                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary' )
-                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
-                               }
+                               $this->summary = $this->newSectionSummary( $sectionanchor );
                        } elseif ( $this->section != '' ) {
                                # Try to get a section anchor from the section source, redirect
                                # to edited section if header found.
index 304c1bc..acf1bf6 100644 (file)
@@ -57,6 +57,9 @@ class GitInfo {
         */
        public function __construct( $repoDir, $usePrecomputed = true ) {
                $this->cacheFile = self::getCacheFilePath( $repoDir );
+               wfDebugLog( 'gitinfo',
+                       "Computed cacheFile={$this->cacheFile} for {$repoDir}"
+               );
                if ( $usePrecomputed &&
                        $this->cacheFile !== null &&
                        is_readable( $this->cacheFile )
@@ -65,9 +68,11 @@ class GitInfo {
                                file_get_contents( $this->cacheFile ),
                                true
                        );
+                       wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" );
                }
 
                if ( !$this->cacheIsComplete() ) {
+                       wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" );
                        $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
                        if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
                                $GITfile = file_get_contents( $this->basedir );
@@ -90,24 +95,31 @@ class GitInfo {
         * Compute the path to the cache file for a given directory.
         *
         * @param string $repoDir The root directory of the repo where .git can be found
-        * @return string Path to GitInfo cache file in $wgCacheDirectory or null if
-        * $wgCacheDirectory is false (cache disabled).
+        * @return string Path to GitInfo cache file in $wgGitInfoCacheDirectory or
+        * null if $wgGitInfoCacheDirectory is false (cache disabled).
+        * @since 1.24
         */
        protected static function getCacheFilePath( $repoDir ) {
-               global $IP, $wgCacheDirectory;
-               if ( $wgCacheDirectory ) {
-                       // Transform path to git repo to something we can safely embed in a filename
-                       $repoName = $repoDir;
-                       if ( strpos( $repoName, $IP ) === 0 ) {
+               global $IP, $wgGitInfoCacheDirectory;
+
+               if ( $wgGitInfoCacheDirectory ) {
+                       // Convert both $IP and $repoDir to canonical paths to protect against
+                       // $IP having changed between the settings files and runtime.
+                       $realIP = realpath( $IP );
+                       $repoName = realpath( $repoDir );
+                       if ( $repoName === false ) {
+                               // Unit tests use fake path names
+                               $repoName = $repoDir;
+                       }
+                       if ( strpos( $repoName, $realIP ) === 0 ) {
                                // Strip $IP from path
-                               $repoName = substr( $repoName, strlen( $IP ) );
+                               $repoName = substr( $repoName, strlen( $realIP ) );
                        }
+                       // Transform path to git repo to something we can safely embed in
+                       // a filename
                        $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
                        $fileName = 'info' . $repoName . '.json';
-                       return implode(
-                               DIRECTORY_SEPARATOR,
-                               array( $wgCacheDirectory, 'gitinfo', $fileName )
-                       );
+                       return "{$wgGitInfoCacheDirectory}/{$fileName}";
                }
                return null;
        }
@@ -330,7 +342,9 @@ class GitInfo {
                        $this->getRemoteUrl();
 
                        if ( !$this->cacheIsComplete() ) {
-                               wfDebugLog( "Failed to compute GitInfo for \"{$this->basedir}\"" );
+                               wfDebugLog( 'gitinfo',
+                                       "Failed to compute GitInfo for \"{$this->basedir}\""
+                               );
                                return;
                        }
 
index c1908af..cb5b7fd 100644 (file)
@@ -2580,10 +2580,12 @@ function wfIsHHVM() {
 /**
  * Swap two variables
  *
+ * @deprecated since 1.24
  * @param mixed $x
  * @param mixed $y
  */
 function swap( &$x, &$y ) {
+       wfDeprecated( __FUNCTION__, '1.24' );
        $z = $x;
        $x = $y;
        $y = $z;
index 5262ffe..5f4655c 100644 (file)
@@ -386,7 +386,7 @@ class Html {
         * For instance, it will omit quotation marks if $wgWellFormedXml is false,
         * and will treat boolean attributes specially.
         *
-        * Attributes that should contain space-separated lists (such as 'class') array
+        * Attributes that can contain space-separated lists ('class', 'accesskey' and 'rel') array
         * values are allowed as well, which will automagically be normalized
         * and converted to a space-separated string. In addition to a numerical
         * array, the attribute value may also be an associative array. See the
@@ -413,6 +413,8 @@ class Html {
         *   A value of false means to omit the attribute.  For boolean attributes,
         *   you can omit the key, e.g., array( 'checked' ) instead of
         *   array( 'checked' => 'checked' ) or such.
+        *
+        * @throws MWException if an attribute that doesn't allow lists is set to an array
         * @return string HTML fragment that goes between element name and '>'
         *   (starting with a space if at least one attribute is output)
         */
@@ -500,6 +502,8 @@ class Html {
 
                                // Remove duplicates and create the string
                                $value = implode( ' ', array_unique( $value ) );
+                       } else if ( is_array( $value ) ) {
+                               throw new MWException( "HTML attribute $key can not contain a list of values" );
                        }
 
                        // See the "Attributes" section in the HTML syntax part of HTML5,
diff --git a/includes/Init.php b/includes/Init.php
deleted file mode 100644 (file)
index d9aeb7b..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-<?php
-/**
- * Some functions that are useful during startup.
- *
- * This class previously contained some functionality related to a PHP compiler
- * called hphpc. That compiler has now been discontinued.
- *
- * 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
- */
-
-/**
- * Some functions that are useful during startup.
- *
- * This class previously contained some functionality related to a PHP compiler
- * called hphpc. That compiler has now been discontinued. All methods are now
- * deprecated.
- */
-class MWInit {
-       private static $compilerVersion;
-
-       /**
-        * @deprecated since 1.22
-        */
-       static function getCompilerVersion() {
-               return false;
-       }
-
-       /**
-        * Returns true if we are running under HipHop, whether in compiled or
-        * interpreted mode.
-        *
-        * @deprecated since 1.22
-        * @return bool
-        */
-       static function isHipHop() {
-               return wfIsHHVM();
-       }
-
-       /**
-        * Get a fully-qualified path for a source file relative to $IP.
-        * @deprecated since 1.22
-        *
-        * @param string $file
-        *
-        * @return string
-        */
-       static function interpretedPath( $file ) {
-               global $IP;
-               return "$IP/$file";
-       }
-
-       /**
-        * @deprecated since 1.22
-        * @param string $file
-        * @return string
-        */
-       static function compiledPath( $file ) {
-               global $IP;
-               return "$IP/$file";
-       }
-
-       /**
-        * @deprecated since 1.22
-        * @param string $file
-        * @return string
-        */
-       static function extCompiledPath( $file ) {
-               return false;
-       }
-
-       /**
-        * Deprecated wrapper for class_exists()
-        * @deprecated since 1.22
-        *
-        * @param string $class
-        *
-        * @return bool
-        */
-       static function classExists( $class ) {
-               return class_exists( $class );
-       }
-
-       /**
-        * Deprecated wrapper for method_exists()
-        * @deprecated since 1.22
-        *
-        * @param string $class
-        * @param string $method
-        *
-        * @return bool
-        */
-       static function methodExists( $class, $method ) {
-               return method_exists( $class, $method );
-       }
-
-       /**
-        * Deprecated wrapper for function_exists()
-        * @deprecated since 1.22
-        *
-        * @param string $function
-        *
-        * @return bool
-        */
-       static function functionExists( $function ) {
-               return function_exists( $function );
-       }
-
-       /**
-        * Deprecated wrapper for call_user_func_array()
-        * @deprecated since 1.22
-        *
-        * @param string $className
-        * @param string $methodName
-        * @param array $args
-        *
-        * @return mixed
-        */
-       static function callStaticMethod( $className, $methodName, $args ) {
-               return call_user_func_array( array( $className, $methodName ), $args );
-       }
-}
diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php
new file mode 100644 (file)
index 0000000..392f558
--- /dev/null
@@ -0,0 +1,496 @@
+<?php
+/**
+ * Provide things related to namespaces.
+ *
+ * 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
+ */
+
+/**
+ * This is a utility class with only static functions
+ * for dealing with namespaces that encodes all the
+ * "magic" behaviors of them based on index.  The textual
+ * names of the namespaces are handled by Language.php.
+ *
+ * These are synonyms for the names given in the language file
+ * Users and translators should not change them
+ *
+ */
+class MWNamespace {
+
+       /**
+        * These namespaces should always be first-letter capitalized, now and
+        * forevermore. Historically, they could've probably been lowercased too,
+        * but some things are just too ingrained now. :)
+        */
+       private static $alwaysCapitalizedNamespaces = array( NS_SPECIAL, NS_USER, NS_MEDIAWIKI );
+
+       /**
+        * Throw an exception when trying to get the subject or talk page
+        * for a given namespace where it does not make sense.
+        * Special namespaces are defined in includes/Defines.php and have
+        * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
+        *
+        * @param int $index
+        * @param string $method
+        *
+        * @throws MWException
+        * @return bool
+        */
+       private static function isMethodValidFor( $index, $method ) {
+               if ( $index < NS_MAIN ) {
+                       throw new MWException( "$method does not make any sense for given namespace $index" );
+               }
+               return true;
+       }
+
+       /**
+        * Can pages in the given namespace be moved?
+        *
+        * @param int $index Namespace index
+        * @return bool
+        */
+       public static function isMovable( $index ) {
+               global $wgAllowImageMoving;
+
+               $result = !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) );
+
+               /**
+                * @since 1.20
+                */
+               wfRunHooks( 'NamespaceIsMovable', array( $index, &$result ) );
+
+               return $result;
+       }
+
+       /**
+        * Is the given namespace is a subject (non-talk) namespace?
+        *
+        * @param int $index Namespace index
+        * @return bool
+        * @since 1.19
+        */
+       public static function isSubject( $index ) {
+               return !self::isTalk( $index );
+       }
+
+       /**
+        * Is the given namespace a talk namespace?
+        *
+        * @param int $index Namespace index
+        * @return bool
+        */
+       public static function isTalk( $index ) {
+               return $index > NS_MAIN
+                       && $index % 2;
+       }
+
+       /**
+        * Get the talk namespace index for a given namespace
+        *
+        * @param int $index Namespace index
+        * @return int
+        */
+       public static function getTalk( $index ) {
+               self::isMethodValidFor( $index, __METHOD__ );
+               return self::isTalk( $index )
+                       ? $index
+                       : $index + 1;
+       }
+
+       /**
+        * Get the subject namespace index for a given namespace
+        * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
+        *
+        * @param int $index Namespace index
+        * @return int
+        */
+       public static function getSubject( $index ) {
+               # Handle special namespaces
+               if ( $index < NS_MAIN ) {
+                       return $index;
+               }
+
+               return self::isTalk( $index )
+                       ? $index - 1
+                       : $index;
+       }
+
+       /**
+        * Get the associated namespace.
+        * For talk namespaces, returns the subject (non-talk) namespace
+        * For subject (non-talk) namespaces, returns the talk namespace
+        *
+        * @param int $index Namespace index
+        * @return int|null If no associated namespace could be found
+        */
+       public static function getAssociated( $index ) {
+               self::isMethodValidFor( $index, __METHOD__ );
+
+               if ( self::isSubject( $index ) ) {
+                       return self::getTalk( $index );
+               } elseif ( self::isTalk( $index ) ) {
+                       return self::getSubject( $index );
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Returns whether the specified namespace exists
+        *
+        * @param int $index
+        *
+        * @return bool
+        * @since 1.19
+        */
+       public static function exists( $index ) {
+               $nslist = self::getCanonicalNamespaces();
+               return isset( $nslist[$index] );
+       }
+
+       /**
+        * Returns whether the specified namespaces are the same namespace
+        *
+        * @note It's possible that in the future we may start using something
+        * other than just namespace indexes. Under that circumstance making use
+        * of this function rather than directly doing comparison will make
+        * sure that code will not potentially break.
+        *
+        * @param int $ns1 The first namespace index
+        * @param int $ns2 The second namespace index
+        *
+        * @return bool
+        * @since 1.19
+        */
+       public static function equals( $ns1, $ns2 ) {
+               return $ns1 == $ns2;
+       }
+
+       /**
+        * Returns whether the specified namespaces share the same subject.
+        * eg: NS_USER and NS_USER wil return true, as well
+        *     NS_USER and NS_USER_TALK will return true.
+        *
+        * @param int $ns1 The first namespace index
+        * @param int $ns2 The second namespace index
+        *
+        * @return bool
+        * @since 1.19
+        */
+       public static function subjectEquals( $ns1, $ns2 ) {
+               return self::getSubject( $ns1 ) == self::getSubject( $ns2 );
+       }
+
+       /**
+        * Returns array of all defined namespaces with their canonical
+        * (English) names.
+        *
+        * @param bool $rebuild Rebuild namespace list (default = false). Used for testing.
+        *
+        * @return array
+        * @since 1.17
+        */
+       public static function getCanonicalNamespaces( $rebuild = false ) {
+               static $namespaces = null;
+               if ( $namespaces === null || $rebuild ) {
+                       global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
+                       $namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames;
+                       if ( is_array( $wgExtraNamespaces ) ) {
+                               $namespaces += $wgExtraNamespaces;
+                       }
+                       wfRunHooks( 'CanonicalNamespaces', array( &$namespaces ) );
+               }
+               return $namespaces;
+       }
+
+       /**
+        * Returns the canonical (English) name for a given index
+        *
+        * @param int $index Namespace index
+        * @return string|bool If no canonical definition.
+        */
+       public static function getCanonicalName( $index ) {
+               $nslist = self::getCanonicalNamespaces();
+               if ( isset( $nslist[$index] ) ) {
+                       return $nslist[$index];
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Returns the index for a given canonical name, or NULL
+        * The input *must* be converted to lower case first
+        *
+        * @param string $name Namespace name
+        * @return int
+        */
+       public static function getCanonicalIndex( $name ) {
+               static $xNamespaces = false;
+               if ( $xNamespaces === false ) {
+                       $xNamespaces = array();
+                       foreach ( self::getCanonicalNamespaces() as $i => $text ) {
+                               $xNamespaces[strtolower( $text )] = $i;
+                       }
+               }
+               if ( array_key_exists( $name, $xNamespaces ) ) {
+                       return $xNamespaces[$name];
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Returns an array of the namespaces (by integer id) that exist on the
+        * wiki. Used primarily by the api in help documentation.
+        * @return array
+        */
+       public static function getValidNamespaces() {
+               static $mValidNamespaces = null;
+
+               if ( is_null( $mValidNamespaces ) ) {
+                       foreach ( array_keys( self::getCanonicalNamespaces() ) as $ns ) {
+                               if ( $ns >= 0 ) {
+                                       $mValidNamespaces[] = $ns;
+                               }
+                       }
+               }
+
+               return $mValidNamespaces;
+       }
+
+       /**
+        * Can this namespace ever have a talk namespace?
+        *
+        * @param int $index Namespace index
+        * @return bool
+        */
+       public static function canTalk( $index ) {
+               return $index >= NS_MAIN;
+       }
+
+       /**
+        * Does this namespace contain content, for the purposes of calculating
+        * statistics, etc?
+        *
+        * @param int $index Index to check
+        * @return bool
+        */
+       public static function isContent( $index ) {
+               global $wgContentNamespaces;
+               return $index == NS_MAIN || in_array( $index, $wgContentNamespaces );
+       }
+
+       /**
+        * Can pages in a namespace be watched?
+        *
+        * @param int $index
+        * @return bool
+        */
+       public static function isWatchable( $index ) {
+               return $index >= NS_MAIN;
+       }
+
+       /**
+        * Does the namespace allow subpages?
+        *
+        * @param int $index Index to check
+        * @return bool
+        */
+       public static function hasSubpages( $index ) {
+               global $wgNamespacesWithSubpages;
+               return !empty( $wgNamespacesWithSubpages[$index] );
+       }
+
+       /**
+        * Get a list of all namespace indices which are considered to contain content
+        * @return array Array of namespace indices
+        */
+       public static function getContentNamespaces() {
+               global $wgContentNamespaces;
+               if ( !is_array( $wgContentNamespaces ) || $wgContentNamespaces === array() ) {
+                       return array( NS_MAIN );
+               } elseif ( !in_array( NS_MAIN, $wgContentNamespaces ) ) {
+                       // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
+                       return array_merge( array( NS_MAIN ), $wgContentNamespaces );
+               } else {
+                       return $wgContentNamespaces;
+               }
+       }
+
+       /**
+        * List all namespace indices which are considered subject, aka not a talk
+        * or special namespace. See also MWNamespace::isSubject
+        *
+        * @return array Array of namespace indices
+        */
+       public static function getSubjectNamespaces() {
+               return array_filter(
+                       MWNamespace::getValidNamespaces(),
+                       'MWNamespace::isSubject'
+               );
+       }
+
+       /**
+        * List all namespace indices which are considered talks, aka not a subject
+        * or special namespace. See also MWNamespace::isTalk
+        *
+        * @return array Array of namespace indices
+        */
+       public static function getTalkNamespaces() {
+               return array_filter(
+                       MWNamespace::getValidNamespaces(),
+                       'MWNamespace::isTalk'
+               );
+       }
+
+       /**
+        * Is the namespace first-letter capitalized?
+        *
+        * @param int $index Index to check
+        * @return bool
+        */
+       public static function isCapitalized( $index ) {
+               global $wgCapitalLinks, $wgCapitalLinkOverrides;
+               // Turn NS_MEDIA into NS_FILE
+               $index = $index === NS_MEDIA ? NS_FILE : $index;
+
+               // Make sure to get the subject of our namespace
+               $index = self::getSubject( $index );
+
+               // Some namespaces are special and should always be upper case
+               if ( in_array( $index, self::$alwaysCapitalizedNamespaces ) ) {
+                       return true;
+               }
+               if ( isset( $wgCapitalLinkOverrides[$index] ) ) {
+                       // $wgCapitalLinkOverrides is explicitly set
+                       return $wgCapitalLinkOverrides[$index];
+               }
+               // Default to the global setting
+               return $wgCapitalLinks;
+       }
+
+       /**
+        * Does the namespace (potentially) have different aliases for different
+        * genders. Not all languages make a distinction here.
+        *
+        * @since 1.18
+        * @param int $index Index to check
+        * @return bool
+        */
+       public static function hasGenderDistinction( $index ) {
+               return $index == NS_USER || $index == NS_USER_TALK;
+       }
+
+       /**
+        * It is not possible to use pages from this namespace as template?
+        *
+        * @since 1.20
+        * @param int $index Index to check
+        * @return bool
+        */
+       public static function isNonincludable( $index ) {
+               global $wgNonincludableNamespaces;
+               return $wgNonincludableNamespaces && in_array( $index, $wgNonincludableNamespaces );
+       }
+
+       /**
+        * Get the default content model for a namespace
+        * This does not mean that all pages in that namespace have the model
+        *
+        * @since 1.21
+        * @param int $index Index to check
+        * @return null|string Default model name for the given namespace, if set
+        */
+       public static function getNamespaceContentModel( $index ) {
+               global $wgNamespaceContentModels;
+               return isset( $wgNamespaceContentModels[$index] )
+                       ? $wgNamespaceContentModels[$index]
+                       : null;
+       }
+
+       /**
+        * Determine which restriction levels it makes sense to use in a namespace,
+        * optionally filtered by a user's rights.
+        *
+        * @since 1.23
+        * @param int $index Index to check
+        * @param User $user User to check
+        * @return array
+        */
+       public static function getRestrictionLevels( $index, User $user = null ) {
+               global $wgNamespaceProtection, $wgRestrictionLevels;
+
+               if ( !isset( $wgNamespaceProtection[$index] ) ) {
+                       // All levels are valid if there's no namespace restriction.
+                       // But still filter by user, if necessary
+                       $levels = $wgRestrictionLevels;
+                       if ( $user ) {
+                               $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
+                                       $right = $level;
+                                       if ( $right == 'sysop' ) {
+                                               $right = 'editprotected'; // BC
+                                       }
+                                       if ( $right == 'autoconfirmed' ) {
+                                               $right = 'editsemiprotected'; // BC
+                                       }
+                                       return ( $right == '' || $user->isAllowed( $right ) );
+                               } ) );
+                       }
+                       return $levels;
+               }
+
+               // First, get the list of groups that can edit this namespace.
+               $namespaceGroups = array();
+               $combine = 'array_merge';
+               foreach ( (array)$wgNamespaceProtection[$index] as $right ) {
+                       if ( $right == 'sysop' ) {
+                               $right = 'editprotected'; // BC
+                       }
+                       if ( $right == 'autoconfirmed' ) {
+                               $right = 'editsemiprotected'; // BC
+                       }
+                       if ( $right != '' ) {
+                               $namespaceGroups = call_user_func( $combine, $namespaceGroups,
+                                       User::getGroupsWithPermission( $right ) );
+                               $combine = 'array_intersect';
+                       }
+               }
+
+               // Now, keep only those restriction levels where there is at least one
+               // group that can edit the namespace but would be blocked by the
+               // restriction.
+               $usableLevels = array( '' );
+               foreach ( $wgRestrictionLevels as $level ) {
+                       $right = $level;
+                       if ( $right == 'sysop' ) {
+                               $right = 'editprotected'; // BC
+                       }
+                       if ( $right == 'autoconfirmed' ) {
+                               $right = 'editsemiprotected'; // BC
+                       }
+                       if ( $right != '' && ( !$user || $user->isAllowed( $right ) ) &&
+                               array_diff( $namespaceGroups, User::getGroupsWithPermission( $right ) )
+                       ) {
+                               $usableLevels[] = $level;
+                       }
+               }
+
+               return $usableLevels;
+       }
+}
diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php
new file mode 100644 (file)
index 0000000..a8bafa3
--- /dev/null
@@ -0,0 +1,722 @@
+<?php
+/**
+ * Helper class for the index.php entry point.
+ *
+ * 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
+ */
+
+/**
+ * The MediaWiki class is the helper class for the index.php entry point.
+ *
+ * @internal documentation reviewed 15 Mar 2010
+ */
+class MediaWiki {
+       /**
+        * @todo Fold $output, etc, into this
+        * @var IContextSource
+        */
+       private $context;
+
+       /**
+        * @param null|WebRequest $x
+        * @return WebRequest
+        */
+       public function request( WebRequest $x = null ) {
+               $old = $this->context->getRequest();
+               $this->context->setRequest( $x );
+               return $old;
+       }
+
+       /**
+        * @param null|OutputPage $x
+        * @return OutputPage
+        */
+       public function output( OutputPage $x = null ) {
+               $old = $this->context->getOutput();
+               $this->context->setOutput( $x );
+               return $old;
+       }
+
+       /**
+        * @param IContextSource|null $context
+        */
+       public function __construct( IContextSource $context = null ) {
+               if ( !$context ) {
+                       $context = RequestContext::getMain();
+               }
+
+               $this->context = $context;
+       }
+
+       /**
+        * Parse the request to get the Title object
+        *
+        * @return Title Title object to be $wgTitle
+        */
+       private function parseTitle() {
+               global $wgContLang;
+
+               $request = $this->context->getRequest();
+               $curid = $request->getInt( 'curid' );
+               $title = $request->getVal( 'title' );
+               $action = $request->getVal( 'action', 'view' );
+
+               if ( $request->getCheck( 'search' ) ) {
+                       // Compatibility with old search URLs which didn't use Special:Search
+                       // Just check for presence here, so blank requests still
+                       // show the search page when using ugly URLs (bug 8054).
+                       $ret = SpecialPage::getTitleFor( 'Search' );
+               } elseif ( $curid ) {
+                       // URLs like this are generated by RC, because rc_title isn't always accurate
+                       $ret = Title::newFromID( $curid );
+               } else {
+                       $ret = Title::newFromURL( $title );
+                       // Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA
+                       // in wikitext links to tell Parser to make a direct file link
+                       if ( !is_null( $ret ) && $ret->getNamespace() == NS_MEDIA ) {
+                               $ret = Title::makeTitle( NS_FILE, $ret->getDBkey() );
+                       }
+                       // Check variant links so that interwiki links don't have to worry
+                       // about the possible different language variants
+                       if ( count( $wgContLang->getVariants() ) > 1
+                               && !is_null( $ret ) && $ret->getArticleID() == 0
+                       ) {
+                               $wgContLang->findVariantLink( $title, $ret );
+                       }
+               }
+
+               // If title is not provided, always allow oldid and diff to set the title.
+               // If title is provided, allow oldid and diff to override the title, unless
+               // we are talking about a special page which might use these parameters for
+               // other purposes.
+               if ( $ret === null || !$ret->isSpecialPage() ) {
+                       // We can have urls with just ?diff=,?oldid= or even just ?diff=
+                       $oldid = $request->getInt( 'oldid' );
+                       $oldid = $oldid ? $oldid : $request->getInt( 'diff' );
+                       // Allow oldid to override a changed or missing title
+                       if ( $oldid ) {
+                               $rev = Revision::newFromId( $oldid );
+                               $ret = $rev ? $rev->getTitle() : $ret;
+                       }
+               }
+
+               // Use the main page as default title if nothing else has been provided
+               if ( $ret === null
+                       && strval( $title ) === ''
+                       && !$request->getCheck( 'curid' )
+                       && $action !== 'delete'
+               ) {
+                       $ret = Title::newMainPage();
+               }
+
+               if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) {
+                       $ret = SpecialPage::getTitleFor( 'Badtitle' );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Get the Title object that we'll be acting on, as specified in the WebRequest
+        * @return Title
+        */
+       public function getTitle() {
+               if ( $this->context->getTitle() === null ) {
+                       $this->context->setTitle( $this->parseTitle() );
+               }
+               return $this->context->getTitle();
+       }
+
+       /**
+        * Returns the name of the action that will be executed.
+        *
+        * @return string Action
+        */
+       public function getAction() {
+               static $action = null;
+
+               if ( $action === null ) {
+                       $action = Action::getActionName( $this->context );
+               }
+
+               return $action;
+       }
+
+       /**
+        * Performs the request.
+        * - bad titles
+        * - read restriction
+        * - local interwiki redirects
+        * - redirect loop
+        * - special pages
+        * - normal pages
+        *
+        * @throws MWException|PermissionsError|BadTitleError|HttpError
+        * @return void
+        */
+       private function performRequest() {
+               global $wgServer, $wgUsePathInfo, $wgTitle;
+
+               wfProfileIn( __METHOD__ );
+
+               $request = $this->context->getRequest();
+               $requestTitle = $title = $this->context->getTitle();
+               $output = $this->context->getOutput();
+               $user = $this->context->getUser();
+
+               if ( $request->getVal( 'printable' ) === 'yes' ) {
+                       $output->setPrintable();
+               }
+
+               $unused = null; // To pass it by reference
+               wfRunHooks( 'BeforeInitialize', array( &$title, &$unused, &$output, &$user, $request, $this ) );
+
+               // Invalid titles. Bug 21776: The interwikis must redirect even if the page name is empty.
+               if ( is_null( $title ) || ( $title->getDBkey() == '' && !$title->isExternal() )
+                       || $title->isSpecial( 'Badtitle' )
+               ) {
+                       $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
+                       wfProfileOut( __METHOD__ );
+                       throw new BadTitleError();
+               }
+
+               // Check user's permissions to read this page.
+               // We have to check here to catch special pages etc.
+               // We will check again in Article::view().
+               $permErrors = $title->getUserPermissionsErrors( 'read', $user );
+               if ( count( $permErrors ) ) {
+                       // Bug 32276: allowing the skin to generate output with $wgTitle or
+                       // $this->context->title set to the input title would allow anonymous users to
+                       // determine whether a page exists, potentially leaking private data. In fact, the
+                       // curid and oldid request  parameters would allow page titles to be enumerated even
+                       // when they are not guessable. So we reset the title to Special:Badtitle before the
+                       // permissions error is displayed.
+                       //
+                       // The skin mostly uses $this->context->getTitle() these days, but some extensions
+                       // still use $wgTitle.
+
+                       $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
+                       $this->context->setTitle( $badTitle );
+                       $wgTitle = $badTitle;
+
+                       wfProfileOut( __METHOD__ );
+                       throw new PermissionsError( 'read', $permErrors );
+               }
+
+               $pageView = false; // was an article or special page viewed?
+
+               // Interwiki redirects
+               if ( $title->isExternal() ) {
+                       $rdfrom = $request->getVal( 'rdfrom' );
+                       if ( $rdfrom ) {
+                               $url = $title->getFullURL( array( 'rdfrom' => $rdfrom ) );
+                       } else {
+                               $query = $request->getValues();
+                               unset( $query['title'] );
+                               $url = $title->getFullURL( $query );
+                       }
+                       // Check for a redirect loop
+                       if ( !preg_match( '/^' . preg_quote( $wgServer, '/' ) . '/', $url )
+                               && $title->isLocal()
+                       ) {
+                               // 301 so google et al report the target as the actual url.
+                               $output->redirect( $url, 301 );
+                       } else {
+                               $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
+                               wfProfileOut( __METHOD__ );
+                               throw new BadTitleError();
+                       }
+               // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant
+               } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted()
+                       && ( $request->getVal( 'title' ) === null
+                               || $title->getPrefixedDBkey() != $request->getVal( 'title' ) )
+                       && !count( $request->getValueNames( array( 'action', 'title' ) ) )
+                       && wfRunHooks( 'TestCanonicalRedirect', array( $request, $title, $output ) )
+               ) {
+                       if ( $title->isSpecialPage() ) {
+                               list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
+                               if ( $name ) {
+                                       $title = SpecialPage::getTitleFor( $name, $subpage );
+                               }
+                       }
+                       $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
+                       // Redirect to canonical url, make it a 301 to allow caching
+                       if ( $targetUrl == $request->getFullRequestURL() ) {
+                               $message = "Redirect loop detected!\n\n" .
+                                       "This means the wiki got confused about what page was " .
+                                       "requested; this sometimes happens when moving a wiki " .
+                                       "to a new server or changing the server configuration.\n\n";
+
+                               if ( $wgUsePathInfo ) {
+                                       $message .= "The wiki is trying to interpret the page " .
+                                               "title from the URL path portion (PATH_INFO), which " .
+                                               "sometimes fails depending on the web server. Try " .
+                                               "setting \"\$wgUsePathInfo = false;\" in your " .
+                                               "LocalSettings.php, or check that \$wgArticlePath " .
+                                               "is correct.";
+                               } else {
+                                       $message .= "Your web server was detected as possibly not " .
+                                               "supporting URL path components (PATH_INFO) correctly; " .
+                                               "check your LocalSettings.php for a customized " .
+                                               "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
+                                               "to true.";
+                               }
+                               throw new HttpError( 500, $message );
+                       } else {
+                               $output->setSquidMaxage( 1200 );
+                               $output->redirect( $targetUrl, '301' );
+                       }
+               // Special pages
+               } elseif ( NS_SPECIAL == $title->getNamespace() ) {
+                       $pageView = true;
+                       // Actions that need to be made when we have a special pages
+                       SpecialPageFactory::executePath( $title, $this->context );
+               } else {
+                       // ...otherwise treat it as an article view. The article
+                       // may be a redirect to another article or URL.
+                       $article = $this->initializeArticle();
+                       if ( is_object( $article ) ) {
+                               $pageView = true;
+                               $this->performAction( $article, $requestTitle );
+                       } elseif ( is_string( $article ) ) {
+                               $output->redirect( $article );
+                       } else {
+                               wfProfileOut( __METHOD__ );
+                               throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
+                                       . " returned neither an object nor a URL" );
+                       }
+               }
+
+               if ( $pageView ) {
+                       // Promote user to any groups they meet the criteria for
+                       $user->addAutopromoteOnceGroups( 'onView' );
+               }
+
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Initialize the main Article object for "standard" actions (view, etc)
+        * Create an Article object for the page, following redirects if needed.
+        *
+        * @return mixed An Article, or a string to redirect to another URL
+        */
+       private function initializeArticle() {
+               global $wgDisableHardRedirects;
+
+               wfProfileIn( __METHOD__ );
+
+               $title = $this->context->getTitle();
+               if ( $this->context->canUseWikiPage() ) {
+                       // Try to use request context wiki page, as there
+                       // is already data from db saved in per process
+                       // cache there from this->getAction() call.
+                       $page = $this->context->getWikiPage();
+                       $article = Article::newFromWikiPage( $page, $this->context );
+               } else {
+                       // This case should not happen, but just in case.
+                       $article = Article::newFromTitle( $title, $this->context );
+                       $this->context->setWikiPage( $article->getPage() );
+               }
+
+               // NS_MEDIAWIKI has no redirects.
+               // It is also used for CSS/JS, so performance matters here...
+               if ( $title->getNamespace() == NS_MEDIAWIKI ) {
+                       wfProfileOut( __METHOD__ );
+                       return $article;
+               }
+
+               $request = $this->context->getRequest();
+
+               // Namespace might change when using redirects
+               // Check for redirects ...
+               $action = $request->getVal( 'action', 'view' );
+               $file = ( $title->getNamespace() == NS_FILE ) ? $article->getFile() : null;
+               if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
+                       && !$request->getVal( 'oldid' ) // ... and are not old revisions
+                       && !$request->getVal( 'diff' ) // ... and not when showing diff
+                       && $request->getVal( 'redirect' ) != 'no' // ... unless explicitly told not to
+                       // ... and the article is not a non-redirect image page with associated file
+                       && !( is_object( $file ) && $file->exists() && !$file->getRedirected() )
+               ) {
+                       // Give extensions a change to ignore/handle redirects as needed
+                       $ignoreRedirect = $target = false;
+
+                       wfRunHooks( 'InitializeArticleMaybeRedirect',
+                               array( &$title, &$request, &$ignoreRedirect, &$target, &$article ) );
+
+                       // Follow redirects only for... redirects.
+                       // If $target is set, then a hook wanted to redirect.
+                       if ( !$ignoreRedirect && ( $target || $article->isRedirect() ) ) {
+                               // Is the target already set by an extension?
+                               $target = $target ? $target : $article->followRedirect();
+                               if ( is_string( $target ) ) {
+                                       if ( !$wgDisableHardRedirects ) {
+                                               // we'll need to redirect
+                                               wfProfileOut( __METHOD__ );
+                                               return $target;
+                                       }
+                               }
+                               if ( is_object( $target ) ) {
+                                       // Rewrite environment to redirected article
+                                       $rarticle = Article::newFromTitle( $target, $this->context );
+                                       $rarticle->loadPageData();
+                                       if ( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
+                                               $rarticle->setRedirectedFrom( $title );
+                                               $article = $rarticle;
+                                               $this->context->setTitle( $target );
+                                               $this->context->setWikiPage( $article->getPage() );
+                                       }
+                               }
+                       } else {
+                               $this->context->setTitle( $article->getTitle() );
+                               $this->context->setWikiPage( $article->getPage() );
+                       }
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $article;
+       }
+
+       /**
+        * Perform one of the "standard" actions
+        *
+        * @param Page $page
+        * @param Title $requestTitle The original title, before any redirects were applied
+        */
+       private function performAction( Page $page, Title $requestTitle ) {
+               global $wgUseSquid, $wgSquidMaxage;
+
+               wfProfileIn( __METHOD__ );
+
+               $request = $this->context->getRequest();
+               $output = $this->context->getOutput();
+               $title = $this->context->getTitle();
+               $user = $this->context->getUser();
+
+               if ( !wfRunHooks( 'MediaWikiPerformAction',
+                               array( $output, $page, $title, $user, $request, $this ) )
+               ) {
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               $act = $this->getAction();
+
+               $action = Action::factory( $act, $page, $this->context );
+
+               if ( $action instanceof Action ) {
+                       # Let Squid cache things if we can purge them.
+                       if ( $wgUseSquid &&
+                               in_array( $request->getFullRequestURL(), $requestTitle->getSquidURLs() )
+                       ) {
+                               $output->setSquidMaxage( $wgSquidMaxage );
+                       }
+
+                       $action->show();
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               if ( wfRunHooks( 'UnknownAction', array( $request->getVal( 'action', 'view' ), $page ) ) ) {
+                       $output->setStatusCode( 404 );
+                       $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
+               }
+
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Run the current MediaWiki instance
+        * index.php just calls this
+        */
+       public function run() {
+               try {
+                       $this->checkMaxLag();
+                       try {
+                               $this->main();
+                       } catch ( ErrorPageError $e ) {
+                               // Bug 62091: while exceptions are convenient to bubble up GUI errors,
+                               // they are not internal application faults. As with normal requests, this
+                               // should commit, print the output, do deferred updates, jobs, and profiling.
+                               wfGetLBFactory()->commitMasterChanges();
+                               $e->report(); // display the GUI error
+                       }
+                       if ( function_exists( 'fastcgi_finish_request' ) ) {
+                               fastcgi_finish_request();
+                       }
+                       $this->triggerJobs();
+                       $this->restInPeace();
+               } catch ( Exception $e ) {
+                       MWExceptionHandler::handle( $e );
+               }
+       }
+
+       /**
+        * Checks if the request should abort due to a lagged server,
+        * for given maxlag parameter.
+        * @return bool
+        */
+       private function checkMaxLag() {
+               global $wgShowHostnames;
+
+               wfProfileIn( __METHOD__ );
+               $maxLag = $this->context->getRequest()->getVal( 'maxlag' );
+               if ( !is_null( $maxLag ) ) {
+                       list( $host, $lag ) = wfGetLB()->getMaxLag();
+                       if ( $lag > $maxLag ) {
+                               $resp = $this->context->getRequest()->response();
+                               $resp->header( 'HTTP/1.1 503 Service Unavailable' );
+                               $resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
+                               $resp->header( 'X-Database-Lag: ' . intval( $lag ) );
+                               $resp->header( 'Content-Type: text/plain' );
+                               if ( $wgShowHostnames ) {
+                                       echo "Waiting for $host: $lag seconds lagged\n";
+                               } else {
+                                       echo "Waiting for a database server: $lag seconds lagged\n";
+                               }
+
+                               wfProfileOut( __METHOD__ );
+
+                               exit;
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+               return true;
+       }
+
+       private function main() {
+               global $wgUseFileCache, $wgTitle, $wgUseAjax;
+
+               wfProfileIn( __METHOD__ );
+
+               $request = $this->context->getRequest();
+
+               // Send Ajax requests to the Ajax dispatcher.
+               if ( $wgUseAjax && $request->getVal( 'action', 'view' ) == 'ajax' ) {
+
+                       // Set a dummy title, because $wgTitle == null might break things
+                       $title = Title::makeTitle( NS_MAIN, 'AJAX' );
+                       $this->context->setTitle( $title );
+                       $wgTitle = $title;
+
+                       $dispatcher = new AjaxDispatcher();
+                       $dispatcher->performAction();
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               // Get title from request parameters,
+               // is set on the fly by parseTitle the first time.
+               $title = $this->getTitle();
+               $action = $this->getAction();
+               $wgTitle = $title;
+
+               // If the user has forceHTTPS set to true, or if the user
+               // is in a group requiring HTTPS, or if they have the HTTPS
+               // preference set, redirect them to HTTPS.
+               // Note: Do this after $wgTitle is setup, otherwise the hooks run from
+               // isLoggedIn() will do all sorts of weird stuff.
+               if (
+                       $request->getProtocol() == 'http' &&
+                       (
+                               $request->getCookie( 'forceHTTPS', '' ) ||
+                               // check for prefixed version for currently logged in users
+                               $request->getCookie( 'forceHTTPS' ) ||
+                               // Avoid checking the user and groups unless it's enabled.
+                               (
+                                       $this->context->getUser()->isLoggedIn()
+                                       && $this->context->getUser()->requiresHTTPS()
+                               )
+                       )
+               ) {
+                       $oldUrl = $request->getFullRequestURL();
+                       $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
+
+                       // ATTENTION: This hook is likely to be removed soon due to overall design of the system.
+                       if ( wfRunHooks( 'BeforeHttpsRedirect', array( $this->context, &$redirUrl ) ) ) {
+
+                               if ( $request->wasPosted() ) {
+                                       // This is weird and we'd hope it almost never happens. This
+                                       // means that a POST came in via HTTP and policy requires us
+                                       // redirecting to HTTPS. It's likely such a request is going
+                                       // to fail due to post data being lost, but let's try anyway
+                                       // and just log the instance.
+                                       //
+                                       // @todo @fixme See if we could issue a 307 or 308 here, need
+                                       // to see how clients (automated & browser) behave when we do
+                                       wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
+                               }
+                               // Setup dummy Title, otherwise OutputPage::redirect will fail
+                               $title = Title::newFromText( NS_MAIN, 'REDIR' );
+                               $this->context->setTitle( $title );
+                               $output = $this->context->getOutput();
+                               // Since we only do this redir to change proto, always send a vary header
+                               $output->addVaryHeader( 'X-Forwarded-Proto' );
+                               $output->redirect( $redirUrl );
+                               $output->output();
+                               wfProfileOut( __METHOD__ );
+                               return;
+                       }
+               }
+
+               if ( $wgUseFileCache && $title->getNamespace() >= 0 ) {
+                       wfProfileIn( 'main-try-filecache' );
+                       if ( HTMLFileCache::useFileCache( $this->context ) ) {
+                               // Try low-level file cache hit
+                               $cache = HTMLFileCache::newFromTitle( $title, $action );
+                               if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
+                                       // Check incoming headers to see if client has this cached
+                                       $timestamp = $cache->cacheTimestamp();
+                                       if ( !$this->context->getOutput()->checkLastModified( $timestamp ) ) {
+                                               $cache->loadFromFileCache( $this->context );
+                                       }
+                                       // Do any stats increment/watchlist stuff
+                                       // Assume we're viewing the latest revision (this should always be the case with file cache)
+                                       $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
+                                       // Tell OutputPage that output is taken care of
+                                       $this->context->getOutput()->disable();
+                                       wfProfileOut( 'main-try-filecache' );
+                                       wfProfileOut( __METHOD__ );
+                                       return;
+                               }
+                       }
+                       wfProfileOut( 'main-try-filecache' );
+               }
+
+               // Actually do the work of the request and build up any output
+               $this->performRequest();
+
+               // Either all DB and deferred updates should happen or none.
+               // The later should not be cancelled due to client disconnect.
+               ignore_user_abort( true );
+               // Now commit any transactions, so that unreported errors after
+               // output() don't roll back the whole DB transaction
+               wfGetLBFactory()->commitMasterChanges();
+
+               // Output everything!
+               $this->context->getOutput()->output();
+
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Ends this task peacefully
+        */
+       public function restInPeace() {
+               // Do any deferred jobs
+               DeferredUpdates::doUpdates( 'commit' );
+
+               // Log profiling data, e.g. in the database or UDP
+               wfLogProfilingData();
+
+               // Commit and close up!
+               $factory = wfGetLBFactory();
+               $factory->commitMasterChanges();
+               $factory->shutdown();
+
+               wfDebug( "Request ended normally\n" );
+       }
+
+       /**
+        * Potentially open a socket and sent an HTTP request back to the server
+        * to run a specified number of jobs. This registers a callback to cleanup
+        * the socket once it's done.
+        */
+       protected function triggerJobs() {
+               global $wgJobRunRate, $wgServer, $wgRunJobsAsync;
+
+               if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
+                       return;
+               } elseif ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
+                       return; // recursion guard
+               }
+
+               $section = new ProfileSection( __METHOD__ );
+
+               if ( $wgJobRunRate < 1 ) {
+                       $max = mt_getrandmax();
+                       if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
+                               return; // the higher $wgJobRunRate, the less likely we return here
+                       }
+                       $n = 1;
+               } else {
+                       $n = intval( $wgJobRunRate );
+               }
+
+               if ( !$wgRunJobsAsync ) {
+                       // If running jobs asynchronously has been disabled, run the job here
+                       // while the user waits
+                       SpecialRunJobs::executeJobs( $n );
+                       return;
+               }
+
+               try {
+                       if ( !JobQueueGroup::singleton()->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
+                               return; // do not send request if there are probably no jobs
+                       }
+               } catch ( JobQueueError $e ) {
+                       MWExceptionHandler::logException( $e );
+                       return; // do not make the site unavailable
+               }
+
+               $query = array( 'title' => 'Special:RunJobs',
+                       'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 );
+               $query['signature'] = SpecialRunJobs::getQuerySignature( $query );
+
+               $errno = $errstr = null;
+               $info = wfParseUrl( $wgServer );
+               wfSuppressWarnings();
+               $sock = fsockopen(
+                       $info['host'],
+                       isset( $info['port'] ) ? $info['port'] : 80,
+                       $errno,
+                       $errstr,
+                       // If it takes more than 100ms to connect to ourselves there
+                       // is a problem elsewhere.
+                       0.1
+               );
+               wfRestoreWarnings();
+               if ( !$sock ) {
+                       wfDebugLog( 'runJobs', "Failed to start cron API (socket error $errno): $errstr\n" );
+                       // Fall back to running the job here while the user waits
+                       SpecialRunJobs::executeJobs( $n );
+                       return;
+               }
+
+               $url = wfAppendQuery( wfScript( 'index' ), $query );
+               $req = "POST $url HTTP/1.1\r\nHost: {$info['host']}\r\nConnection: Close\r\n\r\n";
+
+               wfDebugLog( 'runJobs', "Running $n job(s) via '$url'\n" );
+               // Send a cron API request to be performed in the background.
+               // Give up if this takes too long to send (which should be rare).
+               stream_set_timeout( $sock, 1 );
+               $bytes = fwrite( $sock, $req );
+               if ( $bytes !== strlen( $req ) ) {
+                       wfDebugLog( 'runJobs', "Failed to start cron API (socket write error)\n" );
+               } else {
+                       // Do not wait for the response (the script should handle client aborts).
+                       // Make sure that we don't close before that script reaches ignore_user_abort().
+                       $status = fgets( $sock );
+                       if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
+                               wfDebugLog( 'runJobs', "Failed to start cron API: received '$status'\n" );
+                       }
+               }
+               fclose( $sock );
+       }
+}
index 59f850c..b4d3ab1 100644 (file)
@@ -164,6 +164,14 @@ class MimeMagic {
         */
        protected $mIEAnalyzer;
 
+       /** @var string Extra MIME types, set for example by media handling extensions
+        */
+       private $mExtraTypes = '';
+
+       /** @var string Extra MIME info, set for example by media handling extensions
+        */
+       private $mExtraInfo = '';
+
        /** @var MimeMagic The singleton instance
         */
        private static $instance = null;
@@ -179,6 +187,9 @@ class MimeMagic {
 
                global $wgMimeTypeFile, $IP;
 
+               # Allow media handling extensions adding MIME-types and MIME-info
+               wfRunHooks( 'MimeMagicInit', array( $this ) );
+
                $types = MM_WELL_KNOWN_MIME_TYPES;
 
                if ( $wgMimeTypeFile == 'includes/mime.types' ) {
@@ -197,11 +208,13 @@ class MimeMagic {
                        wfDebug( __METHOD__ . ": no mime types file defined, using build-ins only.\n" );
                }
 
+               $types .= "\n" . $this->mExtraTypes;
+
                $types = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $types );
                $types = str_replace( "\t", " ", $types );
 
                $this->mMimeToExt = array();
-               $this->mToMime = array();
+               $this->mExtToMime = array();
 
                $lines = explode( "\n", $types );
                foreach ( $lines as $s ) {
@@ -272,6 +285,8 @@ class MimeMagic {
                        wfDebug( __METHOD__ . ": no mime info file defined, using build-ins only.\n" );
                }
 
+               $info .= "\n" . $this->mExtraInfo;
+
                $info = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $info );
                $info = str_replace( "\t", " ", $info );
 
@@ -342,6 +357,26 @@ class MimeMagic {
                return self::$instance;
        }
 
+       /**
+        * Adds to the list mapping MIME to file extensions.
+        * As an extension author, you are encouraged to submit patches to
+        * MediaWiki's core to add new MIME types to mime.types.
+        * @param string $types
+        */
+       public function addExtraTypes( $types ) {
+               $this->mExtraTypes .= "\n" . $types;
+       }
+
+       /**
+        * Adds to the list mapping MIME to media type.
+        * As an extension author, you are encouraged to submit patches to
+        * MediaWiki's core to add new MIME info to mime.info.
+        * @param string $info
+        */
+       public function addExtraInfo( $info ) {
+               $this->mExtraInfo .= "\n" . $info;
+       }
+
        /**
         * Returns a list of file extensions for a given mime type as a space
         * separated string or null if the mime type was unrecognized. Resolves
@@ -518,6 +553,9 @@ class MimeMagic {
                        $mime = $this->guessTypesForExtension( $ext );
                }
 
+               # Media handling extensions can improve the MIME detected
+               wfRunHooks( 'MimeMagicImproveFromExtension', array( $this, $ext, &$mime ) );
+
                if ( isset( $this->mMimeTypeAliases[$mime] ) ) {
                        $mime = $this->mMimeTypeAliases[$mime];
                }
@@ -744,7 +782,17 @@ class MimeMagic {
                        return 'image/vnd.djvu';
                }
 
-               return false;
+               # Media handling extensions can guess the MIME by content
+               # It's intentionally here so that if core is wrong about a type (false positive),
+               # people will hopefully nag and submit patches :)
+               $mime = false;
+               # Some strings by reference for performance - assuming well-behaved hooks
+               wfRunHooks(
+                       'MimeMagicGuessFromContent',
+                       array( $this, &$head, &$tail, $file, &$mime )
+               );
+
+               return $mime;
        }
 
        /**
diff --git a/includes/Namespace.php b/includes/Namespace.php
deleted file mode 100644 (file)
index 4edddbc..0000000
+++ /dev/null
@@ -1,506 +0,0 @@
-<?php
-/**
- * Provide things related to namespaces.
- *
- * 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
- */
-
-/**
- * This is a utility class with only static functions
- * for dealing with namespaces that encodes all the
- * "magic" behaviors of them based on index.  The textual
- * names of the namespaces are handled by Language.php.
- *
- * These are synonyms for the names given in the language file
- * Users and translators should not change them
- *
- */
-class MWNamespace {
-
-       /**
-        * These namespaces should always be first-letter capitalized, now and
-        * forevermore. Historically, they could've probably been lowercased too,
-        * but some things are just too ingrained now. :)
-        */
-       private static $alwaysCapitalizedNamespaces = array( NS_SPECIAL, NS_USER, NS_MEDIAWIKI );
-
-       /**
-        * Throw an exception when trying to get the subject or talk page
-        * for a given namespace where it does not make sense.
-        * Special namespaces are defined in includes/Defines.php and have
-        * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
-        *
-        * @param int $index
-        * @param string $method
-        *
-        * @throws MWException
-        * @return bool
-        */
-       private static function isMethodValidFor( $index, $method ) {
-               if ( $index < NS_MAIN ) {
-                       throw new MWException( "$method does not make any sense for given namespace $index" );
-               }
-               return true;
-       }
-
-       /**
-        * Can pages in the given namespace be moved?
-        *
-        * @param int $index Namespace index
-        * @return bool
-        */
-       public static function isMovable( $index ) {
-               global $wgAllowImageMoving;
-
-               $result = !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) );
-
-               /**
-                * @since 1.20
-                */
-               wfRunHooks( 'NamespaceIsMovable', array( $index, &$result ) );
-
-               return $result;
-       }
-
-       /**
-        * Is the given namespace is a subject (non-talk) namespace?
-        *
-        * @param int $index Namespace index
-        * @return bool
-        * @since 1.19
-        */
-       public static function isSubject( $index ) {
-               return !self::isTalk( $index );
-       }
-
-       /**
-        * @see self::isSubject
-        * @deprecated since 1.19 Please use the more consistently named isSubject
-        * @return bool
-        */
-       public static function isMain( $index ) {
-               wfDeprecated( __METHOD__, '1.19' );
-               return self::isSubject( $index );
-       }
-
-       /**
-        * Is the given namespace a talk namespace?
-        *
-        * @param int $index Namespace index
-        * @return bool
-        */
-       public static function isTalk( $index ) {
-               return $index > NS_MAIN
-                       && $index % 2;
-       }
-
-       /**
-        * Get the talk namespace index for a given namespace
-        *
-        * @param int $index Namespace index
-        * @return int
-        */
-       public static function getTalk( $index ) {
-               self::isMethodValidFor( $index, __METHOD__ );
-               return self::isTalk( $index )
-                       ? $index
-                       : $index + 1;
-       }
-
-       /**
-        * Get the subject namespace index for a given namespace
-        * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
-        *
-        * @param int $index Namespace index
-        * @return int
-        */
-       public static function getSubject( $index ) {
-               # Handle special namespaces
-               if ( $index < NS_MAIN ) {
-                       return $index;
-               }
-
-               return self::isTalk( $index )
-                       ? $index - 1
-                       : $index;
-       }
-
-       /**
-        * Get the associated namespace.
-        * For talk namespaces, returns the subject (non-talk) namespace
-        * For subject (non-talk) namespaces, returns the talk namespace
-        *
-        * @param int $index Namespace index
-        * @return int|null If no associated namespace could be found
-        */
-       public static function getAssociated( $index ) {
-               self::isMethodValidFor( $index, __METHOD__ );
-
-               if ( self::isSubject( $index ) ) {
-                       return self::getTalk( $index );
-               } elseif ( self::isTalk( $index ) ) {
-                       return self::getSubject( $index );
-               } else {
-                       return null;
-               }
-       }
-
-       /**
-        * Returns whether the specified namespace exists
-        *
-        * @param int $index
-        *
-        * @return bool
-        * @since 1.19
-        */
-       public static function exists( $index ) {
-               $nslist = self::getCanonicalNamespaces();
-               return isset( $nslist[$index] );
-       }
-
-       /**
-        * Returns whether the specified namespaces are the same namespace
-        *
-        * @note It's possible that in the future we may start using something
-        * other than just namespace indexes. Under that circumstance making use
-        * of this function rather than directly doing comparison will make
-        * sure that code will not potentially break.
-        *
-        * @param int $ns1 The first namespace index
-        * @param int $ns2 The second namespace index
-        *
-        * @return bool
-        * @since 1.19
-        */
-       public static function equals( $ns1, $ns2 ) {
-               return $ns1 == $ns2;
-       }
-
-       /**
-        * Returns whether the specified namespaces share the same subject.
-        * eg: NS_USER and NS_USER wil return true, as well
-        *     NS_USER and NS_USER_TALK will return true.
-        *
-        * @param int $ns1 The first namespace index
-        * @param int $ns2 The second namespace index
-        *
-        * @return bool
-        * @since 1.19
-        */
-       public static function subjectEquals( $ns1, $ns2 ) {
-               return self::getSubject( $ns1 ) == self::getSubject( $ns2 );
-       }
-
-       /**
-        * Returns array of all defined namespaces with their canonical
-        * (English) names.
-        *
-        * @param bool $rebuild Rebuild namespace list (default = false). Used for testing.
-        *
-        * @return array
-        * @since 1.17
-        */
-       public static function getCanonicalNamespaces( $rebuild = false ) {
-               static $namespaces = null;
-               if ( $namespaces === null || $rebuild ) {
-                       global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
-                       $namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames;
-                       if ( is_array( $wgExtraNamespaces ) ) {
-                               $namespaces += $wgExtraNamespaces;
-                       }
-                       wfRunHooks( 'CanonicalNamespaces', array( &$namespaces ) );
-               }
-               return $namespaces;
-       }
-
-       /**
-        * Returns the canonical (English) name for a given index
-        *
-        * @param int $index Namespace index
-        * @return string|bool If no canonical definition.
-        */
-       public static function getCanonicalName( $index ) {
-               $nslist = self::getCanonicalNamespaces();
-               if ( isset( $nslist[$index] ) ) {
-                       return $nslist[$index];
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Returns the index for a given canonical name, or NULL
-        * The input *must* be converted to lower case first
-        *
-        * @param string $name Namespace name
-        * @return int
-        */
-       public static function getCanonicalIndex( $name ) {
-               static $xNamespaces = false;
-               if ( $xNamespaces === false ) {
-                       $xNamespaces = array();
-                       foreach ( self::getCanonicalNamespaces() as $i => $text ) {
-                               $xNamespaces[strtolower( $text )] = $i;
-                       }
-               }
-               if ( array_key_exists( $name, $xNamespaces ) ) {
-                       return $xNamespaces[$name];
-               } else {
-                       return null;
-               }
-       }
-
-       /**
-        * Returns an array of the namespaces (by integer id) that exist on the
-        * wiki. Used primarily by the api in help documentation.
-        * @return array
-        */
-       public static function getValidNamespaces() {
-               static $mValidNamespaces = null;
-
-               if ( is_null( $mValidNamespaces ) ) {
-                       foreach ( array_keys( self::getCanonicalNamespaces() ) as $ns ) {
-                               if ( $ns >= 0 ) {
-                                       $mValidNamespaces[] = $ns;
-                               }
-                       }
-               }
-
-               return $mValidNamespaces;
-       }
-
-       /**
-        * Can this namespace ever have a talk namespace?
-        *
-        * @param int $index Namespace index
-        * @return bool
-        */
-       public static function canTalk( $index ) {
-               return $index >= NS_MAIN;
-       }
-
-       /**
-        * Does this namespace contain content, for the purposes of calculating
-        * statistics, etc?
-        *
-        * @param int $index Index to check
-        * @return bool
-        */
-       public static function isContent( $index ) {
-               global $wgContentNamespaces;
-               return $index == NS_MAIN || in_array( $index, $wgContentNamespaces );
-       }
-
-       /**
-        * Can pages in a namespace be watched?
-        *
-        * @param int $index
-        * @return bool
-        */
-       public static function isWatchable( $index ) {
-               return $index >= NS_MAIN;
-       }
-
-       /**
-        * Does the namespace allow subpages?
-        *
-        * @param int $index Index to check
-        * @return bool
-        */
-       public static function hasSubpages( $index ) {
-               global $wgNamespacesWithSubpages;
-               return !empty( $wgNamespacesWithSubpages[$index] );
-       }
-
-       /**
-        * Get a list of all namespace indices which are considered to contain content
-        * @return array Array of namespace indices
-        */
-       public static function getContentNamespaces() {
-               global $wgContentNamespaces;
-               if ( !is_array( $wgContentNamespaces ) || $wgContentNamespaces === array() ) {
-                       return array( NS_MAIN );
-               } elseif ( !in_array( NS_MAIN, $wgContentNamespaces ) ) {
-                       // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
-                       return array_merge( array( NS_MAIN ), $wgContentNamespaces );
-               } else {
-                       return $wgContentNamespaces;
-               }
-       }
-
-       /**
-        * List all namespace indices which are considered subject, aka not a talk
-        * or special namespace. See also MWNamespace::isSubject
-        *
-        * @return array Array of namespace indices
-        */
-       public static function getSubjectNamespaces() {
-               return array_filter(
-                       MWNamespace::getValidNamespaces(),
-                       'MWNamespace::isSubject'
-               );
-       }
-
-       /**
-        * List all namespace indices which are considered talks, aka not a subject
-        * or special namespace. See also MWNamespace::isTalk
-        *
-        * @return array Array of namespace indices
-        */
-       public static function getTalkNamespaces() {
-               return array_filter(
-                       MWNamespace::getValidNamespaces(),
-                       'MWNamespace::isTalk'
-               );
-       }
-
-       /**
-        * Is the namespace first-letter capitalized?
-        *
-        * @param int $index Index to check
-        * @return bool
-        */
-       public static function isCapitalized( $index ) {
-               global $wgCapitalLinks, $wgCapitalLinkOverrides;
-               // Turn NS_MEDIA into NS_FILE
-               $index = $index === NS_MEDIA ? NS_FILE : $index;
-
-               // Make sure to get the subject of our namespace
-               $index = self::getSubject( $index );
-
-               // Some namespaces are special and should always be upper case
-               if ( in_array( $index, self::$alwaysCapitalizedNamespaces ) ) {
-                       return true;
-               }
-               if ( isset( $wgCapitalLinkOverrides[$index] ) ) {
-                       // $wgCapitalLinkOverrides is explicitly set
-                       return $wgCapitalLinkOverrides[$index];
-               }
-               // Default to the global setting
-               return $wgCapitalLinks;
-       }
-
-       /**
-        * Does the namespace (potentially) have different aliases for different
-        * genders. Not all languages make a distinction here.
-        *
-        * @since 1.18
-        * @param int $index Index to check
-        * @return bool
-        */
-       public static function hasGenderDistinction( $index ) {
-               return $index == NS_USER || $index == NS_USER_TALK;
-       }
-
-       /**
-        * It is not possible to use pages from this namespace as template?
-        *
-        * @since 1.20
-        * @param int $index Index to check
-        * @return bool
-        */
-       public static function isNonincludable( $index ) {
-               global $wgNonincludableNamespaces;
-               return $wgNonincludableNamespaces && in_array( $index, $wgNonincludableNamespaces );
-       }
-
-       /**
-        * Get the default content model for a namespace
-        * This does not mean that all pages in that namespace have the model
-        *
-        * @since 1.21
-        * @param int $index Index to check
-        * @return null|string Default model name for the given namespace, if set
-        */
-       public static function getNamespaceContentModel( $index ) {
-               global $wgNamespaceContentModels;
-               return isset( $wgNamespaceContentModels[$index] )
-                       ? $wgNamespaceContentModels[$index]
-                       : null;
-       }
-
-       /**
-        * Determine which restriction levels it makes sense to use in a namespace,
-        * optionally filtered by a user's rights.
-        *
-        * @since 1.23
-        * @param int $index Index to check
-        * @param User $user User to check
-        * @return array
-        */
-       public static function getRestrictionLevels( $index, User $user = null ) {
-               global $wgNamespaceProtection, $wgRestrictionLevels;
-
-               if ( !isset( $wgNamespaceProtection[$index] ) ) {
-                       // All levels are valid if there's no namespace restriction.
-                       // But still filter by user, if necessary
-                       $levels = $wgRestrictionLevels;
-                       if ( $user ) {
-                               $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
-                                       $right = $level;
-                                       if ( $right == 'sysop' ) {
-                                               $right = 'editprotected'; // BC
-                                       }
-                                       if ( $right == 'autoconfirmed' ) {
-                                               $right = 'editsemiprotected'; // BC
-                                       }
-                                       return ( $right == '' || $user->isAllowed( $right ) );
-                               } ) );
-                       }
-                       return $levels;
-               }
-
-               // First, get the list of groups that can edit this namespace.
-               $namespaceGroups = array();
-               $combine = 'array_merge';
-               foreach ( (array)$wgNamespaceProtection[$index] as $right ) {
-                       if ( $right == 'sysop' ) {
-                               $right = 'editprotected'; // BC
-                       }
-                       if ( $right == 'autoconfirmed' ) {
-                               $right = 'editsemiprotected'; // BC
-                       }
-                       if ( $right != '' ) {
-                               $namespaceGroups = call_user_func( $combine, $namespaceGroups,
-                                       User::getGroupsWithPermission( $right ) );
-                               $combine = 'array_intersect';
-                       }
-               }
-
-               // Now, keep only those restriction levels where there is at least one
-               // group that can edit the namespace but would be blocked by the
-               // restriction.
-               $usableLevels = array( '' );
-               foreach ( $wgRestrictionLevels as $level ) {
-                       $right = $level;
-                       if ( $right == 'sysop' ) {
-                               $right = 'editprotected'; // BC
-                       }
-                       if ( $right == 'autoconfirmed' ) {
-                               $right = 'editsemiprotected'; // BC
-                       }
-                       if ( $right != '' && ( !$user || $user->isAllowed( $right ) ) &&
-                               array_diff( $namespaceGroups, User::getGroupsWithPermission( $right ) )
-                       ) {
-                               $usableLevels[] = $level;
-                       }
-               }
-
-               return $usableLevels;
-       }
-}
index f8b1b3e..8967938 100644 (file)
@@ -1342,18 +1342,6 @@ class OutputPage extends ContextSource {
                );
        }
 
-       /**
-        * Return whether user JavaScript is allowed for this page
-        * @deprecated since 1.18 Load modules with ResourceLoader, and origin and
-        *     trustworthiness is identified and enforced automagically.
-        * @return bool
-        */
-       public function isUserJsAllowed() {
-               wfDeprecated( __METHOD__, '1.18' );
-               return $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS ) >=
-                       ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL;
-       }
-
        /**
         * Show what level of JavaScript / CSS untrustworthiness is allowed on this page
         * @see ResourceLoaderModule::$origin
@@ -2066,19 +2054,6 @@ class OutputPage extends ContextSource {
                }
        }
 
-       /**
-        * Get the message associated with the HTTP response code $code
-        *
-        * @param int $code Status code
-        * @return string|null Message or null if $code is not in the list of messages
-        *
-        * @deprecated since 1.18 Use HttpStatus::getMessage() instead.
-        */
-       public static function getStatusMessage( $code ) {
-               wfDeprecated( __METHOD__, '1.18' );
-               return HttpStatus::getMessage( $code );
-       }
-
        /**
         * Finally, all the text has been munged and accumulated into
         * the object, let's actually output it:
@@ -3109,16 +3084,13 @@ $templates
        /**
         * Get an array containing the variables to be set in mw.config in JavaScript.
         *
-        * DO NOT CALL THIS FROM OUTSIDE OF THIS CLASS OR Skin::makeGlobalVariablesScript().
-        * This is only public until that function is removed. You have been warned.
-        *
         * Do not add things here which can be evaluated in ResourceLoaderStartUpModule
         * - in other words, page-independent/site-wide variables (without state).
         * You will only be adding bloat to the html page and causing page caches to
         * have to be purged on configuration changes.
         * @return array
         */
-       public function getJSVars() {
+       private function getJSVars() {
                global $wgContLang;
 
                $curRevisionId = 0;
index 0fb3952..0f5a6fc 100644 (file)
@@ -32,6 +32,7 @@
  *   - index.php
  *   - load.php
  *   - api.php
+ *   - mw-config/index.php
  *   - cli
  *
  * @note Since we can't rely on anything, the minimum PHP versions and MW current
@@ -41,7 +42,7 @@ function wfPHPVersionError( $type ) {
        $mwVersion = '1.24';
        $minimumVersionPHP = '5.3.2';
 
-       $phpVersion = phpversion();
+       $phpVersion = PHP_VERSION;
        $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
        $message = "MediaWiki $mwVersion requires at least "
                . "PHP version $minimumVersionPHP, you are using PHP $phpVersion.";
@@ -50,10 +51,15 @@ function wfPHPVersionError( $type ) {
                $finalOutput = "You are using PHP version $phpVersion "
                        . "but MediaWiki $mwVersion needs PHP $minimumVersionPHP or higher. ABORTING.\n"
                        . "Check if you have a newer php executable with a different name, such as php5.\n";
-       } elseif ( $type == 'index.php' ) {
+       } elseif ( $type == 'index.php' || $type == 'mw-config/index.php' ) {
                $pathinfo = pathinfo( $_SERVER['SCRIPT_NAME'] );
+               if ( $type == 'mw-config/index.php' ) {
+                       $dirname = dirname( $pathinfo['dirname'] );
+               } else {
+                       $dirname = $pathinfo['dirname'];
+               }
                $encLogo = htmlspecialchars(
-                       str_replace( '//', '/', $pathinfo['dirname'] . '/' ) .
+                       str_replace( '//', '/', $dirname . '/' ) .
                        'skins/common/images/mediawiki.png'
                );
 
index 081315e..fdb1a9d 100644 (file)
@@ -98,6 +98,20 @@ class Preferences {
 
                wfRunHooks( 'GetPreferences', array( $user, &$defaultPreferences ) );
 
+               self::loadPreferenceValues( $user, $context, $defaultPreferences );
+               self::$defaultPreferences = $defaultPreferences;
+               return $defaultPreferences;
+       }
+
+       /**
+        * Loads existing values for a given array of preferences
+        * @throws MWException
+        * @param User $user
+        * @param IContextSource $context
+        * @param array defaultPreferences to load values for
+        * @return array|null
+        */
+       static function loadPreferenceValues( $user, $context, &$defaultPreferences ) {
                ## Remove preferences that wikis don't want to use
                global $wgHiddenPrefs;
                foreach ( $wgHiddenPrefs as $pref ) {
@@ -138,8 +152,6 @@ class Preferences {
                        }
                }
 
-               self::$defaultPreferences = $defaultPreferences;
-
                return $defaultPreferences;
        }
 
@@ -1463,27 +1475,6 @@ class Preferences {
                        return array( $result, 'mailerror' );
                }
        }
-
-       /**
-        * @deprecated since 1.19
-        * @param User $user
-        * @return array
-        */
-       public static function loadOldSearchNs( $user ) {
-               wfDeprecated( __METHOD__, '1.19' );
-
-               $searchableNamespaces = SearchEngine::searchableNamespaces();
-               // Back compat with old format
-               $arr = array();
-
-               foreach ( $searchableNamespaces as $ns => $name ) {
-                       if ( $user->getOption( 'searchNs' . $ns ) ) {
-                               $arr[] = $ns;
-                       }
-               }
-
-               return $arr;
-       }
 }
 
 /** Some tweaks to allow js prefs to work */
index 13696ad..f794b2a 100644 (file)
@@ -166,7 +166,9 @@ abstract class PrefixSearch {
        protected function specialSearch( $search, $limit ) {
                global $wgContLang;
 
-               list( $searchKey, $subpageSearch ) = explode( '/', $search, 2 );
+               $searchParts = explode( '/', $search, 2 );
+               $searchKey = $searchParts[0];
+               $subpageSearch = isset( $searchParts[1] ) ? $searchParts[1] : null;
 
                // Handle subpage search separately.
                if ( $subpageSearch !== null ) {
index 6a568c2..ec17a08 100644 (file)
@@ -383,7 +383,7 @@ class Sanitizer {
                                'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
                                'strike', 'strong', 'tt', 'var', 'div', 'center',
                                'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
-                               'ruby', 'rt', 'rb', 'rp', 'p', 'span', 'abbr', 'dfn',
+                               'ruby', 'rb', 'rp', 'rt', 'rtc', 'p', 'span', 'abbr', 'dfn',
                                'kbd', 'samp', 'data', 'time', 'mark'
                        );
                        $htmlsingle = array(
@@ -1685,10 +1685,10 @@ class Sanitizer {
                        # http://www.whatwg.org/html/text-level-semantics.html#the-ruby-element
                        'ruby'       => $common,
                        # rbc
-                       # rtc
                        'rb'         => $common,
-                       'rt'         => $common, #array_merge( $common, array( 'rbspan' ) ),
                        'rp'         => $common,
+                       'rt'         => $common, #array_merge( $common, array( 'rbspan' ) ),
+                       'rtc'         => $common,
 
                        # MathML root element, where used for extensions
                        # 'title' may not be 100% valid here; it's XHTML
index fd4465b..040aba5 100644 (file)
@@ -106,8 +106,8 @@ if ( $wgDeletedDirectory === false ) {
        $wgDeletedDirectory = "{$wgUploadDirectory}/deleted";
 }
 
-if ( isset( $wgFileStore['deleted']['directory'] ) ) {
-       $wgDeletedDirectory = $wgFileStore['deleted']['directory'];
+if ( $wgGitInfoCacheDirectory === false && $wgCacheDirectory !== false ) {
+       $wgGitInfoCacheDirectory = "{$wgCacheDirectory}/gitinfo";
 }
 
 if ( isset( $wgFooterIcons['copyright'] )
@@ -169,11 +169,7 @@ $wgLockManagers[] = array(
  * Initialise $wgLocalFileRepo from backwards-compatible settings
  */
 if ( !$wgLocalFileRepo ) {
-       if ( isset( $wgFileStore['deleted']['hash'] ) ) {
-               $deletedHashLevel = $wgFileStore['deleted']['hash'];
-       } else {
-               $deletedHashLevel = $wgHashedUploadDirectory ? 3 : 0;
-       }
+       $deletedHashLevel = $wgHashedUploadDirectory ? 3 : 0;
        $wgLocalFileRepo = array(
                'class' => 'LocalRepo',
                'name' => 'local',
@@ -374,10 +370,6 @@ if ( !$wgHtml5Version && $wgAllowRdfaAttributes ) {
 // Blacklisted file extensions shouldn't appear on the "allowed" list
 $wgFileExtensions = array_values( array_diff ( $wgFileExtensions, $wgFileBlacklist ) );
 
-if ( $wgArticleCountMethod === null ) {
-       $wgArticleCountMethod = $wgUseCommaCount ? 'comma' : 'link';
-}
-
 if ( $wgInvalidateCacheOnLocalSettingsChange ) {
        // @codingStandardsIgnoreStart Generic.PHP.NoSilencedErrors.Discouraged - No GlobalFunction here yet.
        $wgCacheEpoch = max( $wgCacheEpoch, gmdate( 'YmdHis', @filemtime( "$IP/LocalSettings.php" ) ) );
@@ -623,6 +615,26 @@ $wgTitle = null;
 
 $wgDeferredUpdateList = array();
 
+// Disable all other email settings automatically if $wgEnableEmail
+// is set to false. - bug 63678
+if ( !$wgEnableEmail ) {
+       $wgAllowHTMLEmail = false;
+       $wgEmailAuthentication = false; // do not require auth if you're not sending email anyway
+       $wgEnableUserEmail = false;
+       $wgEnotifFromEditor = false;
+       $wgEnotifImpersonal = false;
+       $wgEnotifMaxRecips = 0;
+       $wgEnotifMinorEdits = false;
+       $wgEnotifRevealEditorAddress = false;
+       $wgEnotifUseJobQ = false;
+       $wgEnotifUseRealName = false;
+       $wgEnotifUserTalk = false;
+       $wgEnotifWatchlist = false;
+       unset( $wgGroupPermissions['user']['sendemail'] );
+       $wgUserEmailUseReplyTo = false;
+       $wgUsersNotifiedOnAllChanges = array();
+}
+
 wfProfileOut( $fname . '-globals' );
 wfProfileIn( $fname . '-extensions' );
 
index 14cd7af..35e1ef8 100644 (file)
@@ -155,10 +155,15 @@ abstract class Skin extends ContextSource {
 
                $skinNames = Skin::getSkinNames();
 
+               // Make keys lowercase for case-insensitive matching.
+               $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
+               $key = strtolower( $key );
+               $default = strtolower( $wgDefaultSkin );
+
                if ( $key == '' || $key == 'default' ) {
                        // Don't return the default immediately;
                        // in a misconfiguration we need to fall back.
-                       $key = $wgDefaultSkin;
+                       $key = $default;
                }
 
                if ( isset( $skinNames[$key] ) ) {
@@ -168,7 +173,7 @@ abstract class Skin extends ContextSource {
                // Older versions of the software used a numeric setting
                // in the user preferences.
                $fallback = array(
-                       0 => $wgDefaultSkin,
+                       0 => $default,
                        2 => 'cologneblue'
                );
 
@@ -178,8 +183,8 @@ abstract class Skin extends ContextSource {
 
                if ( isset( $skinNames[$key] ) ) {
                        return $key;
-               } elseif ( isset( $skinNames[$wgDefaultSkin] ) ) {
-                       return $wgDefaultSkin;
+               } elseif ( isset( $skinNames[$default] ) ) {
+                       return $default;
                } else {
                        return 'vector';
                }
@@ -421,21 +426,6 @@ abstract class Skin extends ContextSource {
                }
        }
 
-       /**
-        * Make a "<script>" tag containing global variables
-        *
-        * @deprecated since 1.19
-        * @param mixed $unused
-        * @return string HTML fragment
-        */
-       public static function makeGlobalVariablesScript( $unused ) {
-               global $wgOut;
-
-               wfDeprecated( __METHOD__, '1.19' );
-
-               return self::makeVariablesScript( $wgOut->getJSVars() );
-       }
-
        /**
         * Get the query to generate a dynamic stylesheet
         *
index 5f827e8..97c0ec4 100644 (file)
@@ -1097,7 +1097,7 @@ class SkinTemplate extends Skin {
                                        /**
                                         * The following actions use messages which, if made particular to
                                         * the any specific skins, would break the Ajax code which makes this
-                                        * action happen entirely inline. Skin::makeGlobalVariablesScript
+                                        * action happen entirely inline. OutputPage::getJSVars
                                         * defines a set of messages in a javascript object - and these
                                         * messages are assumed to be global for all skins. Without making
                                         * a change to that procedure these messages will have to remain as
index b8b0e30..746ffab 100644 (file)
@@ -977,7 +977,7 @@ class Title {
                }
 
                try {
-                       $formatter = $this->getTitleFormatter();
+                       $formatter = self::getTitleFormatter();
                        return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
                } catch ( InvalidArgumentException $ex )  {
                        wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
@@ -1546,18 +1546,6 @@ class Title {
                return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
        }
 
-       /**
-        * Get the HTML-escaped displayable text form.
-        * Used for the title field in <a> tags.
-        *
-        * @return string The text, including any prefixes
-        * @deprecated since 1.19
-        */
-       public function getEscapedText() {
-               wfDeprecated( __METHOD__, '1.19' );
-               return htmlspecialchars( $this->getPrefixedText() );
-       }
-
        /**
         * Get a URL-encoded form of the subpage text
         *
@@ -1775,34 +1763,6 @@ class Title {
                return $ret;
        }
 
-       /**
-        * Get an HTML-escaped version of the URL form, suitable for
-        * using in a link, without a server name or fragment
-        *
-        * @see self::getLocalURL for the arguments.
-        * @param string $query
-        * @param bool|string $query2
-        * @return string The URL
-        * @deprecated since 1.19
-        */
-       public function escapeLocalURL( $query = '', $query2 = false ) {
-               wfDeprecated( __METHOD__, '1.19' );
-               return htmlspecialchars( $this->getLocalURL( $query, $query2 ) );
-       }
-
-       /**
-        * Get an HTML-escaped version of the URL form, suitable for
-        * using in a link, including the server name and fragment
-        *
-        * @see self::getLocalURL for the arguments.
-        * @return string The URL
-        * @deprecated since 1.19
-        */
-       public function escapeFullURL( $query = '', $query2 = false ) {
-               wfDeprecated( __METHOD__, '1.19' );
-               return htmlspecialchars( $this->getFullURL( $query, $query2 ) );
-       }
-
        /**
         * Get the URL form for an internal link.
         * - Used in various Squid-related code, in case we have a different
@@ -1842,19 +1802,6 @@ class Title {
                return $url;
        }
 
-       /**
-        * HTML-escaped version of getCanonicalURL()
-        *
-        * @see self::getLocalURL for the arguments.
-        * @since 1.18
-        * @return string
-        * @deprecated since 1.19
-        */
-       public function escapeCanonicalURL( $query = '', $query2 = false ) {
-               wfDeprecated( __METHOD__, '1.19' );
-               return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) );
-       }
-
        /**
         * Get the edit URL for this Title
         *
@@ -2583,30 +2530,6 @@ class Title {
                return $this->mTitleProtection;
        }
 
-       /**
-        * Update the title protection status
-        *
-        * @deprecated since 1.19; use WikiPage::doUpdateRestrictions() instead.
-        * @param string $create_perm Permission required for creation
-        * @param string $reason Reason for protection
-        * @param string $expiry Expiry timestamp
-        * @return bool
-        */
-       public function updateTitleProtection( $create_perm, $reason, $expiry ) {
-               wfDeprecated( __METHOD__, '1.19' );
-
-               global $wgUser;
-
-               $limit = array( 'create' => $create_perm );
-               $expiry = array( 'create' => $expiry );
-
-               $page = WikiPage::factory( $this );
-               $cascade = false;
-               $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $wgUser );
-
-               return $status->isOK();
-       }
-
        /**
         * Remove any title protection due to page existing
         */
@@ -3368,7 +3291,7 @@ class Title {
                        // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
                        //        the parsing code with Title, while avoiding massive refactoring.
                        // @todo: get rid of secureAndSplit, refactor parsing code.
-                       $parser = $this->getTitleParser();
+                       $parser = self::getTitleParser();
                        $parts = $parser->splitTitleString( $dbkey, $this->getDefaultNamespace() );
                } catch ( MalformedTitleException $ex ) {
                        return false;
index 5a117bd..fc2b351 100644 (file)
@@ -892,38 +892,6 @@ class User implements IDBAccessObject {
                return $this->mPasswordExpires;
        }
 
-       /**
-        * Does a string look like an e-mail address?
-        *
-        * This validates an email address using an HTML5 specification found at:
-        * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
-        * Which as of 2011-01-24 says:
-        *
-        *     A valid e-mail address is a string that matches the ABNF production
-        *   1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
-        *   in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
-        *   3.5.
-        *
-        * This function is an implementation of the specification as requested in
-        * bug 22449.
-        *
-        * Client-side forms will use the same standard validation rules via JS or
-        * HTML 5 validation; additional restrictions can be enforced server-side
-        * by extensions via the 'isValidEmailAddr' hook.
-        *
-        * Note that this validation doesn't 100% match RFC 2822, but is believed
-        * to be liberal enough for wide use. Some invalid addresses will still
-        * pass validation here.
-        *
-        * @param string $addr E-mail address
-        * @return bool
-        * @deprecated since 1.18 call Sanitizer::isValidEmail() directly
-        */
-       public static function isValidEmailAddr( $addr ) {
-               wfDeprecated( __METHOD__, '1.18' );
-               return Sanitizer::validateEmail( $addr );
-       }
-
        /**
         * Given unvalidated user input, return a canonical username, or false if
         * the username is invalid.
@@ -1537,10 +1505,9 @@ class User implements IDBAccessObject {
         * @return bool True if blacklisted.
         */
        public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
-               global $wgEnableSorbs, $wgEnableDnsBlacklist,
-                       $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
+               global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
 
-               if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) {
+               if ( !$wgEnableDnsBlacklist ) {
                        return false;
                }
 
@@ -1548,8 +1515,7 @@ class User implements IDBAccessObject {
                        return false;
                }
 
-               $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
-               return $this->inDnsBlacklist( $ip, $urls );
+               return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
        }
 
        /**
@@ -2778,6 +2744,8 @@ class User implements IDBAccessObject {
                        }
                }
 
+               wfRunHooks( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
+
                $this->mOptions = $newOptions;
                $this->mOptionsLoaded = true;
        }
index d73f6b4..c9db90f 100644 (file)
@@ -239,7 +239,17 @@ class UserMailer {
                # -- hashar 20120218
 
                $headers['From'] = $from->toString();
-               $headers['Return-Path'] = $from->address;
+               $returnPath = $from->address;
+               $extraParams = $wgAdditionalMailParams;
+
+               // Hook to generate custom VERP address for 'Return-Path'
+               wfRunHooks( 'UserMailerChangeReturnPath', array( $to, &$returnPath ) );
+               # Add the envelope sender address using the -f command line option when PHP mail() is used.
+               # Will default to the $from->address when the UserMailerChangeReturnPath hook fails and the
+               # generated VERP address when the hook runs effectively.
+               $extraParams .= ' -f ' . $returnPath;
+
+               $headers['Return-Path'] = $returnPath;
 
                if ( $replyto ) {
                        $headers['Reply-To'] = $replyto->toString();
@@ -371,7 +381,7 @@ class UserMailer {
                                                        self::quotedPrintable( $subject ),
                                                        $body,
                                                        $headers,
-                                                       $wgAdditionalMailParams
+                                                       $extraParams
                                                );
                                        }
                                }
index 09f01c0..71454f0 100644 (file)
  * @file
  */
 
-# Protect against register_globals
+# Die if register_globals is enabled (PHP <=5.3)
 # This must be done before any globals are set by the code
 if ( ini_get( 'register_globals' ) ) {
-       if ( isset( $_REQUEST['GLOBALS'] ) || isset( $_FILES['GLOBALS'] ) ) {
-               die( '<a href="http://www.hardened-php.net/globals-problem">'
-                       . '$GLOBALS overwrite vulnerability</a>' );
-       }
-
-       $verboten = array(
-               'GLOBALS',
-               '_SERVER',
-               'HTTP_SERVER_VARS',
-               '_GET',
-               'HTTP_GET_VARS',
-               '_POST',
-               'HTTP_POST_VARS',
-               '_COOKIE',
-               'HTTP_COOKIE_VARS',
-               '_FILES',
-               'HTTP_POST_FILES',
-               '_ENV',
-               'HTTP_ENV_VARS',
-               '_REQUEST',
-               '_SESSION',
-               'HTTP_SESSION_VARS'
-       );
-
-       foreach ( $_REQUEST as $name => $value ) {
-               if ( in_array( $name, $verboten ) ) {
-                       header( "HTTP/1.1 500 Internal Server Error" );
-                       echo "register_globals security paranoia: trying to overwrite superglobals, aborting.";
-                       die( -1 );
-               }
-               unset( $GLOBALS[$name] );
-       }
+       die( 'MediaWiki does not support installations where register_globals is enabled. '
+               . 'Please see <a href="https://www.mediawiki.org/wiki/register_globals">mediawiki.org</a> '
+               . 'for help on how to disable it.' );
 }
 
 # bug 15461: Make IE8 turn off content sniffing. Everybody else should ignore this
diff --git a/includes/Wiki.php b/includes/Wiki.php
deleted file mode 100644 (file)
index a8bafa3..0000000
+++ /dev/null
@@ -1,722 +0,0 @@
-<?php
-/**
- * Helper class for the index.php entry point.
- *
- * 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
- */
-
-/**
- * The MediaWiki class is the helper class for the index.php entry point.
- *
- * @internal documentation reviewed 15 Mar 2010
- */
-class MediaWiki {
-       /**
-        * @todo Fold $output, etc, into this
-        * @var IContextSource
-        */
-       private $context;
-
-       /**
-        * @param null|WebRequest $x
-        * @return WebRequest
-        */
-       public function request( WebRequest $x = null ) {
-               $old = $this->context->getRequest();
-               $this->context->setRequest( $x );
-               return $old;
-       }
-
-       /**
-        * @param null|OutputPage $x
-        * @return OutputPage
-        */
-       public function output( OutputPage $x = null ) {
-               $old = $this->context->getOutput();
-               $this->context->setOutput( $x );
-               return $old;
-       }
-
-       /**
-        * @param IContextSource|null $context
-        */
-       public function __construct( IContextSource $context = null ) {
-               if ( !$context ) {
-                       $context = RequestContext::getMain();
-               }
-
-               $this->context = $context;
-       }
-
-       /**
-        * Parse the request to get the Title object
-        *
-        * @return Title Title object to be $wgTitle
-        */
-       private function parseTitle() {
-               global $wgContLang;
-
-               $request = $this->context->getRequest();
-               $curid = $request->getInt( 'curid' );
-               $title = $request->getVal( 'title' );
-               $action = $request->getVal( 'action', 'view' );
-
-               if ( $request->getCheck( 'search' ) ) {
-                       // Compatibility with old search URLs which didn't use Special:Search
-                       // Just check for presence here, so blank requests still
-                       // show the search page when using ugly URLs (bug 8054).
-                       $ret = SpecialPage::getTitleFor( 'Search' );
-               } elseif ( $curid ) {
-                       // URLs like this are generated by RC, because rc_title isn't always accurate
-                       $ret = Title::newFromID( $curid );
-               } else {
-                       $ret = Title::newFromURL( $title );
-                       // Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA
-                       // in wikitext links to tell Parser to make a direct file link
-                       if ( !is_null( $ret ) && $ret->getNamespace() == NS_MEDIA ) {
-                               $ret = Title::makeTitle( NS_FILE, $ret->getDBkey() );
-                       }
-                       // Check variant links so that interwiki links don't have to worry
-                       // about the possible different language variants
-                       if ( count( $wgContLang->getVariants() ) > 1
-                               && !is_null( $ret ) && $ret->getArticleID() == 0
-                       ) {
-                               $wgContLang->findVariantLink( $title, $ret );
-                       }
-               }
-
-               // If title is not provided, always allow oldid and diff to set the title.
-               // If title is provided, allow oldid and diff to override the title, unless
-               // we are talking about a special page which might use these parameters for
-               // other purposes.
-               if ( $ret === null || !$ret->isSpecialPage() ) {
-                       // We can have urls with just ?diff=,?oldid= or even just ?diff=
-                       $oldid = $request->getInt( 'oldid' );
-                       $oldid = $oldid ? $oldid : $request->getInt( 'diff' );
-                       // Allow oldid to override a changed or missing title
-                       if ( $oldid ) {
-                               $rev = Revision::newFromId( $oldid );
-                               $ret = $rev ? $rev->getTitle() : $ret;
-                       }
-               }
-
-               // Use the main page as default title if nothing else has been provided
-               if ( $ret === null
-                       && strval( $title ) === ''
-                       && !$request->getCheck( 'curid' )
-                       && $action !== 'delete'
-               ) {
-                       $ret = Title::newMainPage();
-               }
-
-               if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) {
-                       $ret = SpecialPage::getTitleFor( 'Badtitle' );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * Get the Title object that we'll be acting on, as specified in the WebRequest
-        * @return Title
-        */
-       public function getTitle() {
-               if ( $this->context->getTitle() === null ) {
-                       $this->context->setTitle( $this->parseTitle() );
-               }
-               return $this->context->getTitle();
-       }
-
-       /**
-        * Returns the name of the action that will be executed.
-        *
-        * @return string Action
-        */
-       public function getAction() {
-               static $action = null;
-
-               if ( $action === null ) {
-                       $action = Action::getActionName( $this->context );
-               }
-
-               return $action;
-       }
-
-       /**
-        * Performs the request.
-        * - bad titles
-        * - read restriction
-        * - local interwiki redirects
-        * - redirect loop
-        * - special pages
-        * - normal pages
-        *
-        * @throws MWException|PermissionsError|BadTitleError|HttpError
-        * @return void
-        */
-       private function performRequest() {
-               global $wgServer, $wgUsePathInfo, $wgTitle;
-
-               wfProfileIn( __METHOD__ );
-
-               $request = $this->context->getRequest();
-               $requestTitle = $title = $this->context->getTitle();
-               $output = $this->context->getOutput();
-               $user = $this->context->getUser();
-
-               if ( $request->getVal( 'printable' ) === 'yes' ) {
-                       $output->setPrintable();
-               }
-
-               $unused = null; // To pass it by reference
-               wfRunHooks( 'BeforeInitialize', array( &$title, &$unused, &$output, &$user, $request, $this ) );
-
-               // Invalid titles. Bug 21776: The interwikis must redirect even if the page name is empty.
-               if ( is_null( $title ) || ( $title->getDBkey() == '' && !$title->isExternal() )
-                       || $title->isSpecial( 'Badtitle' )
-               ) {
-                       $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
-                       wfProfileOut( __METHOD__ );
-                       throw new BadTitleError();
-               }
-
-               // Check user's permissions to read this page.
-               // We have to check here to catch special pages etc.
-               // We will check again in Article::view().
-               $permErrors = $title->getUserPermissionsErrors( 'read', $user );
-               if ( count( $permErrors ) ) {
-                       // Bug 32276: allowing the skin to generate output with $wgTitle or
-                       // $this->context->title set to the input title would allow anonymous users to
-                       // determine whether a page exists, potentially leaking private data. In fact, the
-                       // curid and oldid request  parameters would allow page titles to be enumerated even
-                       // when they are not guessable. So we reset the title to Special:Badtitle before the
-                       // permissions error is displayed.
-                       //
-                       // The skin mostly uses $this->context->getTitle() these days, but some extensions
-                       // still use $wgTitle.
-
-                       $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
-                       $this->context->setTitle( $badTitle );
-                       $wgTitle = $badTitle;
-
-                       wfProfileOut( __METHOD__ );
-                       throw new PermissionsError( 'read', $permErrors );
-               }
-
-               $pageView = false; // was an article or special page viewed?
-
-               // Interwiki redirects
-               if ( $title->isExternal() ) {
-                       $rdfrom = $request->getVal( 'rdfrom' );
-                       if ( $rdfrom ) {
-                               $url = $title->getFullURL( array( 'rdfrom' => $rdfrom ) );
-                       } else {
-                               $query = $request->getValues();
-                               unset( $query['title'] );
-                               $url = $title->getFullURL( $query );
-                       }
-                       // Check for a redirect loop
-                       if ( !preg_match( '/^' . preg_quote( $wgServer, '/' ) . '/', $url )
-                               && $title->isLocal()
-                       ) {
-                               // 301 so google et al report the target as the actual url.
-                               $output->redirect( $url, 301 );
-                       } else {
-                               $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
-                               wfProfileOut( __METHOD__ );
-                               throw new BadTitleError();
-                       }
-               // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant
-               } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted()
-                       && ( $request->getVal( 'title' ) === null
-                               || $title->getPrefixedDBkey() != $request->getVal( 'title' ) )
-                       && !count( $request->getValueNames( array( 'action', 'title' ) ) )
-                       && wfRunHooks( 'TestCanonicalRedirect', array( $request, $title, $output ) )
-               ) {
-                       if ( $title->isSpecialPage() ) {
-                               list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
-                               if ( $name ) {
-                                       $title = SpecialPage::getTitleFor( $name, $subpage );
-                               }
-                       }
-                       $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
-                       // Redirect to canonical url, make it a 301 to allow caching
-                       if ( $targetUrl == $request->getFullRequestURL() ) {
-                               $message = "Redirect loop detected!\n\n" .
-                                       "This means the wiki got confused about what page was " .
-                                       "requested; this sometimes happens when moving a wiki " .
-                                       "to a new server or changing the server configuration.\n\n";
-
-                               if ( $wgUsePathInfo ) {
-                                       $message .= "The wiki is trying to interpret the page " .
-                                               "title from the URL path portion (PATH_INFO), which " .
-                                               "sometimes fails depending on the web server. Try " .
-                                               "setting \"\$wgUsePathInfo = false;\" in your " .
-                                               "LocalSettings.php, or check that \$wgArticlePath " .
-                                               "is correct.";
-                               } else {
-                                       $message .= "Your web server was detected as possibly not " .
-                                               "supporting URL path components (PATH_INFO) correctly; " .
-                                               "check your LocalSettings.php for a customized " .
-                                               "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
-                                               "to true.";
-                               }
-                               throw new HttpError( 500, $message );
-                       } else {
-                               $output->setSquidMaxage( 1200 );
-                               $output->redirect( $targetUrl, '301' );
-                       }
-               // Special pages
-               } elseif ( NS_SPECIAL == $title->getNamespace() ) {
-                       $pageView = true;
-                       // Actions that need to be made when we have a special pages
-                       SpecialPageFactory::executePath( $title, $this->context );
-               } else {
-                       // ...otherwise treat it as an article view. The article
-                       // may be a redirect to another article or URL.
-                       $article = $this->initializeArticle();
-                       if ( is_object( $article ) ) {
-                               $pageView = true;
-                               $this->performAction( $article, $requestTitle );
-                       } elseif ( is_string( $article ) ) {
-                               $output->redirect( $article );
-                       } else {
-                               wfProfileOut( __METHOD__ );
-                               throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
-                                       . " returned neither an object nor a URL" );
-                       }
-               }
-
-               if ( $pageView ) {
-                       // Promote user to any groups they meet the criteria for
-                       $user->addAutopromoteOnceGroups( 'onView' );
-               }
-
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Initialize the main Article object for "standard" actions (view, etc)
-        * Create an Article object for the page, following redirects if needed.
-        *
-        * @return mixed An Article, or a string to redirect to another URL
-        */
-       private function initializeArticle() {
-               global $wgDisableHardRedirects;
-
-               wfProfileIn( __METHOD__ );
-
-               $title = $this->context->getTitle();
-               if ( $this->context->canUseWikiPage() ) {
-                       // Try to use request context wiki page, as there
-                       // is already data from db saved in per process
-                       // cache there from this->getAction() call.
-                       $page = $this->context->getWikiPage();
-                       $article = Article::newFromWikiPage( $page, $this->context );
-               } else {
-                       // This case should not happen, but just in case.
-                       $article = Article::newFromTitle( $title, $this->context );
-                       $this->context->setWikiPage( $article->getPage() );
-               }
-
-               // NS_MEDIAWIKI has no redirects.
-               // It is also used for CSS/JS, so performance matters here...
-               if ( $title->getNamespace() == NS_MEDIAWIKI ) {
-                       wfProfileOut( __METHOD__ );
-                       return $article;
-               }
-
-               $request = $this->context->getRequest();
-
-               // Namespace might change when using redirects
-               // Check for redirects ...
-               $action = $request->getVal( 'action', 'view' );
-               $file = ( $title->getNamespace() == NS_FILE ) ? $article->getFile() : null;
-               if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
-                       && !$request->getVal( 'oldid' ) // ... and are not old revisions
-                       && !$request->getVal( 'diff' ) // ... and not when showing diff
-                       && $request->getVal( 'redirect' ) != 'no' // ... unless explicitly told not to
-                       // ... and the article is not a non-redirect image page with associated file
-                       && !( is_object( $file ) && $file->exists() && !$file->getRedirected() )
-               ) {
-                       // Give extensions a change to ignore/handle redirects as needed
-                       $ignoreRedirect = $target = false;
-
-                       wfRunHooks( 'InitializeArticleMaybeRedirect',
-                               array( &$title, &$request, &$ignoreRedirect, &$target, &$article ) );
-
-                       // Follow redirects only for... redirects.
-                       // If $target is set, then a hook wanted to redirect.
-                       if ( !$ignoreRedirect && ( $target || $article->isRedirect() ) ) {
-                               // Is the target already set by an extension?
-                               $target = $target ? $target : $article->followRedirect();
-                               if ( is_string( $target ) ) {
-                                       if ( !$wgDisableHardRedirects ) {
-                                               // we'll need to redirect
-                                               wfProfileOut( __METHOD__ );
-                                               return $target;
-                                       }
-                               }
-                               if ( is_object( $target ) ) {
-                                       // Rewrite environment to redirected article
-                                       $rarticle = Article::newFromTitle( $target, $this->context );
-                                       $rarticle->loadPageData();
-                                       if ( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
-                                               $rarticle->setRedirectedFrom( $title );
-                                               $article = $rarticle;
-                                               $this->context->setTitle( $target );
-                                               $this->context->setWikiPage( $article->getPage() );
-                                       }
-                               }
-                       } else {
-                               $this->context->setTitle( $article->getTitle() );
-                               $this->context->setWikiPage( $article->getPage() );
-                       }
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $article;
-       }
-
-       /**
-        * Perform one of the "standard" actions
-        *
-        * @param Page $page
-        * @param Title $requestTitle The original title, before any redirects were applied
-        */
-       private function performAction( Page $page, Title $requestTitle ) {
-               global $wgUseSquid, $wgSquidMaxage;
-
-               wfProfileIn( __METHOD__ );
-
-               $request = $this->context->getRequest();
-               $output = $this->context->getOutput();
-               $title = $this->context->getTitle();
-               $user = $this->context->getUser();
-
-               if ( !wfRunHooks( 'MediaWikiPerformAction',
-                               array( $output, $page, $title, $user, $request, $this ) )
-               ) {
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-
-               $act = $this->getAction();
-
-               $action = Action::factory( $act, $page, $this->context );
-
-               if ( $action instanceof Action ) {
-                       # Let Squid cache things if we can purge them.
-                       if ( $wgUseSquid &&
-                               in_array( $request->getFullRequestURL(), $requestTitle->getSquidURLs() )
-                       ) {
-                               $output->setSquidMaxage( $wgSquidMaxage );
-                       }
-
-                       $action->show();
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-
-               if ( wfRunHooks( 'UnknownAction', array( $request->getVal( 'action', 'view' ), $page ) ) ) {
-                       $output->setStatusCode( 404 );
-                       $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
-               }
-
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Run the current MediaWiki instance
-        * index.php just calls this
-        */
-       public function run() {
-               try {
-                       $this->checkMaxLag();
-                       try {
-                               $this->main();
-                       } catch ( ErrorPageError $e ) {
-                               // Bug 62091: while exceptions are convenient to bubble up GUI errors,
-                               // they are not internal application faults. As with normal requests, this
-                               // should commit, print the output, do deferred updates, jobs, and profiling.
-                               wfGetLBFactory()->commitMasterChanges();
-                               $e->report(); // display the GUI error
-                       }
-                       if ( function_exists( 'fastcgi_finish_request' ) ) {
-                               fastcgi_finish_request();
-                       }
-                       $this->triggerJobs();
-                       $this->restInPeace();
-               } catch ( Exception $e ) {
-                       MWExceptionHandler::handle( $e );
-               }
-       }
-
-       /**
-        * Checks if the request should abort due to a lagged server,
-        * for given maxlag parameter.
-        * @return bool
-        */
-       private function checkMaxLag() {
-               global $wgShowHostnames;
-
-               wfProfileIn( __METHOD__ );
-               $maxLag = $this->context->getRequest()->getVal( 'maxlag' );
-               if ( !is_null( $maxLag ) ) {
-                       list( $host, $lag ) = wfGetLB()->getMaxLag();
-                       if ( $lag > $maxLag ) {
-                               $resp = $this->context->getRequest()->response();
-                               $resp->header( 'HTTP/1.1 503 Service Unavailable' );
-                               $resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
-                               $resp->header( 'X-Database-Lag: ' . intval( $lag ) );
-                               $resp->header( 'Content-Type: text/plain' );
-                               if ( $wgShowHostnames ) {
-                                       echo "Waiting for $host: $lag seconds lagged\n";
-                               } else {
-                                       echo "Waiting for a database server: $lag seconds lagged\n";
-                               }
-
-                               wfProfileOut( __METHOD__ );
-
-                               exit;
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-               return true;
-       }
-
-       private function main() {
-               global $wgUseFileCache, $wgTitle, $wgUseAjax;
-
-               wfProfileIn( __METHOD__ );
-
-               $request = $this->context->getRequest();
-
-               // Send Ajax requests to the Ajax dispatcher.
-               if ( $wgUseAjax && $request->getVal( 'action', 'view' ) == 'ajax' ) {
-
-                       // Set a dummy title, because $wgTitle == null might break things
-                       $title = Title::makeTitle( NS_MAIN, 'AJAX' );
-                       $this->context->setTitle( $title );
-                       $wgTitle = $title;
-
-                       $dispatcher = new AjaxDispatcher();
-                       $dispatcher->performAction();
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-
-               // Get title from request parameters,
-               // is set on the fly by parseTitle the first time.
-               $title = $this->getTitle();
-               $action = $this->getAction();
-               $wgTitle = $title;
-
-               // If the user has forceHTTPS set to true, or if the user
-               // is in a group requiring HTTPS, or if they have the HTTPS
-               // preference set, redirect them to HTTPS.
-               // Note: Do this after $wgTitle is setup, otherwise the hooks run from
-               // isLoggedIn() will do all sorts of weird stuff.
-               if (
-                       $request->getProtocol() == 'http' &&
-                       (
-                               $request->getCookie( 'forceHTTPS', '' ) ||
-                               // check for prefixed version for currently logged in users
-                               $request->getCookie( 'forceHTTPS' ) ||
-                               // Avoid checking the user and groups unless it's enabled.
-                               (
-                                       $this->context->getUser()->isLoggedIn()
-                                       && $this->context->getUser()->requiresHTTPS()
-                               )
-                       )
-               ) {
-                       $oldUrl = $request->getFullRequestURL();
-                       $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
-
-                       // ATTENTION: This hook is likely to be removed soon due to overall design of the system.
-                       if ( wfRunHooks( 'BeforeHttpsRedirect', array( $this->context, &$redirUrl ) ) ) {
-
-                               if ( $request->wasPosted() ) {
-                                       // This is weird and we'd hope it almost never happens. This
-                                       // means that a POST came in via HTTP and policy requires us
-                                       // redirecting to HTTPS. It's likely such a request is going
-                                       // to fail due to post data being lost, but let's try anyway
-                                       // and just log the instance.
-                                       //
-                                       // @todo @fixme See if we could issue a 307 or 308 here, need
-                                       // to see how clients (automated & browser) behave when we do
-                                       wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
-                               }
-                               // Setup dummy Title, otherwise OutputPage::redirect will fail
-                               $title = Title::newFromText( NS_MAIN, 'REDIR' );
-                               $this->context->setTitle( $title );
-                               $output = $this->context->getOutput();
-                               // Since we only do this redir to change proto, always send a vary header
-                               $output->addVaryHeader( 'X-Forwarded-Proto' );
-                               $output->redirect( $redirUrl );
-                               $output->output();
-                               wfProfileOut( __METHOD__ );
-                               return;
-                       }
-               }
-
-               if ( $wgUseFileCache && $title->getNamespace() >= 0 ) {
-                       wfProfileIn( 'main-try-filecache' );
-                       if ( HTMLFileCache::useFileCache( $this->context ) ) {
-                               // Try low-level file cache hit
-                               $cache = HTMLFileCache::newFromTitle( $title, $action );
-                               if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
-                                       // Check incoming headers to see if client has this cached
-                                       $timestamp = $cache->cacheTimestamp();
-                                       if ( !$this->context->getOutput()->checkLastModified( $timestamp ) ) {
-                                               $cache->loadFromFileCache( $this->context );
-                                       }
-                                       // Do any stats increment/watchlist stuff
-                                       // Assume we're viewing the latest revision (this should always be the case with file cache)
-                                       $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
-                                       // Tell OutputPage that output is taken care of
-                                       $this->context->getOutput()->disable();
-                                       wfProfileOut( 'main-try-filecache' );
-                                       wfProfileOut( __METHOD__ );
-                                       return;
-                               }
-                       }
-                       wfProfileOut( 'main-try-filecache' );
-               }
-
-               // Actually do the work of the request and build up any output
-               $this->performRequest();
-
-               // Either all DB and deferred updates should happen or none.
-               // The later should not be cancelled due to client disconnect.
-               ignore_user_abort( true );
-               // Now commit any transactions, so that unreported errors after
-               // output() don't roll back the whole DB transaction
-               wfGetLBFactory()->commitMasterChanges();
-
-               // Output everything!
-               $this->context->getOutput()->output();
-
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Ends this task peacefully
-        */
-       public function restInPeace() {
-               // Do any deferred jobs
-               DeferredUpdates::doUpdates( 'commit' );
-
-               // Log profiling data, e.g. in the database or UDP
-               wfLogProfilingData();
-
-               // Commit and close up!
-               $factory = wfGetLBFactory();
-               $factory->commitMasterChanges();
-               $factory->shutdown();
-
-               wfDebug( "Request ended normally\n" );
-       }
-
-       /**
-        * Potentially open a socket and sent an HTTP request back to the server
-        * to run a specified number of jobs. This registers a callback to cleanup
-        * the socket once it's done.
-        */
-       protected function triggerJobs() {
-               global $wgJobRunRate, $wgServer, $wgRunJobsAsync;
-
-               if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
-                       return;
-               } elseif ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
-                       return; // recursion guard
-               }
-
-               $section = new ProfileSection( __METHOD__ );
-
-               if ( $wgJobRunRate < 1 ) {
-                       $max = mt_getrandmax();
-                       if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
-                               return; // the higher $wgJobRunRate, the less likely we return here
-                       }
-                       $n = 1;
-               } else {
-                       $n = intval( $wgJobRunRate );
-               }
-
-               if ( !$wgRunJobsAsync ) {
-                       // If running jobs asynchronously has been disabled, run the job here
-                       // while the user waits
-                       SpecialRunJobs::executeJobs( $n );
-                       return;
-               }
-
-               try {
-                       if ( !JobQueueGroup::singleton()->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
-                               return; // do not send request if there are probably no jobs
-                       }
-               } catch ( JobQueueError $e ) {
-                       MWExceptionHandler::logException( $e );
-                       return; // do not make the site unavailable
-               }
-
-               $query = array( 'title' => 'Special:RunJobs',
-                       'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 );
-               $query['signature'] = SpecialRunJobs::getQuerySignature( $query );
-
-               $errno = $errstr = null;
-               $info = wfParseUrl( $wgServer );
-               wfSuppressWarnings();
-               $sock = fsockopen(
-                       $info['host'],
-                       isset( $info['port'] ) ? $info['port'] : 80,
-                       $errno,
-                       $errstr,
-                       // If it takes more than 100ms to connect to ourselves there
-                       // is a problem elsewhere.
-                       0.1
-               );
-               wfRestoreWarnings();
-               if ( !$sock ) {
-                       wfDebugLog( 'runJobs', "Failed to start cron API (socket error $errno): $errstr\n" );
-                       // Fall back to running the job here while the user waits
-                       SpecialRunJobs::executeJobs( $n );
-                       return;
-               }
-
-               $url = wfAppendQuery( wfScript( 'index' ), $query );
-               $req = "POST $url HTTP/1.1\r\nHost: {$info['host']}\r\nConnection: Close\r\n\r\n";
-
-               wfDebugLog( 'runJobs', "Running $n job(s) via '$url'\n" );
-               // Send a cron API request to be performed in the background.
-               // Give up if this takes too long to send (which should be rare).
-               stream_set_timeout( $sock, 1 );
-               $bytes = fwrite( $sock, $req );
-               if ( $bytes !== strlen( $req ) ) {
-                       wfDebugLog( 'runJobs', "Failed to start cron API (socket write error)\n" );
-               } else {
-                       // Do not wait for the response (the script should handle client aborts).
-                       // Make sure that we don't close before that script reaches ignore_user_abort().
-                       $status = fgets( $sock );
-                       if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
-                               wfDebugLog( 'runJobs', "Failed to start cron API: received '$status'\n" );
-                       }
-               }
-               fclose( $sock );
-       }
-}
index d4b08b2..839d0ed 100644 (file)
@@ -360,10 +360,4 @@ abstract class Action {
         * @throws ErrorPageError
         */
        abstract public function show();
-
-       /**
-        * Execute the action in a silent fashion: do not display anything or release any errors.
-        * @return bool whether execution was successful
-        */
-       abstract public function execute();
 }
index 7477d11..c6fcdde 100644 (file)
@@ -121,48 +121,4 @@ abstract class FormAction extends Action {
                        $this->onSuccess();
                }
        }
-
-       /**
-        * @see Action::execute()
-        *
-        * @param array|null $data
-        * @param bool $captureErrors
-        * @throws ErrorPageError|Exception
-        * @return bool
-        */
-       public function execute( array $data = null, $captureErrors = true ) {
-               try {
-                       // Set a new context so output doesn't leak.
-                       $this->context = clone $this->getContext();
-
-                       // This will throw exceptions if there's a problem
-                       $this->checkCanExecute( $this->getUser() );
-
-                       $fields = array();
-                       foreach ( $this->fields as $key => $params ) {
-                               if ( isset( $data[$key] ) ) {
-                                       $fields[$key] = $data[$key];
-                               } elseif ( isset( $params['default'] ) ) {
-                                       $fields[$key] = $params['default'];
-                               } else {
-                                       $fields[$key] = null;
-                               }
-                       }
-                       $status = $this->onSubmit( $fields );
-                       if ( $status === true ) {
-                               // This might do permanent stuff
-                               $this->onSuccess();
-                               return true;
-                       } else {
-                               return false;
-                       }
-               }
-               catch ( ErrorPageError $e ) {
-                       if ( $captureErrors ) {
-                               return false;
-                       } else {
-                               throw $e;
-                       }
-               }
-       }
 }
index 0039838..f61fc97 100644 (file)
@@ -35,29 +35,6 @@ abstract class FormlessAction extends Action {
         */
        abstract public function onView();
 
-       /**
-        * We don't want an HTMLForm
-        * @return bool
-        */
-       protected function getFormFields() {
-               return false;
-       }
-
-       /**
-        * @param array $data
-        * @return bool
-        */
-       public function onSubmit( $data ) {
-               return false;
-       }
-
-       /**
-        * @return bool
-        */
-       public function onSuccess() {
-               return false;
-       }
-
        public function show() {
                $this->setHeaders();
 
@@ -66,35 +43,4 @@ abstract class FormlessAction extends Action {
 
                $this->getOutput()->addHTML( $this->onView() );
        }
-
-       /**
-        * Execute the action silently, not giving any output.  Since these actions don't have
-        * forms, they probably won't have any data, but some (eg rollback) may do
-        * @param array $data Values that would normally be in the GET request
-        * @param bool $captureErrors Whether to catch exceptions and just return false
-        * @throws ErrorPageError|Exception
-        * @return bool Whether execution was successful
-        */
-       public function execute( array $data = null, $captureErrors = true ) {
-               try {
-                       // Set a new context so output doesn't leak.
-                       $this->context = clone $this->getContext();
-                       if ( is_array( $data ) ) {
-                               $this->context->setRequest( new FauxRequest( $data, false ) );
-                       }
-
-                       // This will throw exceptions if there's a problem
-                       $this->checkCanExecute( $this->getUser() );
-
-                       $this->onView();
-                       return true;
-               }
-               catch ( ErrorPageError $e ) {
-                       if ( $captureErrors ) {
-                               return false;
-                       } else {
-                               throw $e;
-                       }
-               }
-       }
 }
index cdd139e..6481630 100644 (file)
  */
 
 /**
- * Dummy class for pages not in NS_FILE
- *
- * @ingroup Actions
- */
-class RevertAction extends Action {
-
-       public function getName() {
-               return 'revert';
-       }
-
-       public function show() {
-               $this->getOutput()->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
-       }
-
-       public function execute() {
-       }
-}
-
-/**
- * Class for pages in NS_FILE
+ * File reversion user interface
  *
  * @ingroup Actions
  */
-class RevertFileAction extends FormAction {
+class RevertAction extends FormAction {
        /**
         * @var OldLocalFile
         */
@@ -62,6 +43,9 @@ class RevertFileAction extends FormAction {
        }
 
        protected function checkCanExecute( User $user ) {
+               if ( $this->getTitle()->getNamespace() !== NS_FILE ) {
+                       throw new ErrorPageError( $this->msg( 'nosuchaction' ), $this->msg( 'nosuchactiontext' ) );
+               }
                parent::checkCanExecute( $user );
 
                $oldimage = $this->getRequest()->getText( 'oldimage' );
@@ -86,6 +70,7 @@ class RevertFileAction extends FormAction {
                $form->setWrapperLegendMsg( 'filerevert-legend' );
                $form->setSubmitTextMsg( 'filerevert-submit' );
                $form->addHiddenField( 'oldimage', $this->getRequest()->getText( 'oldimage' ) );
+               $form->setTokenSalt( array( 'revert', $this->getTitle()->getPrefixedDBkey() ) );
        }
 
        protected function getFormFields() {
index d221799..7ebd0c3 100644 (file)
@@ -251,10 +251,8 @@ abstract class ApiBase extends ContextSource {
                }
                $msg = array();
                ApiResult::setContent( $msg, $warning );
-               $result->disableSizeCheck();
                $result->addValue( 'warnings', $moduleName,
-                       $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
-               $result->enableSizeCheck();
+                       $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
        }
 
        /**
@@ -1189,9 +1187,9 @@ abstract class ApiBase extends ContextSource {
         * @param mixed $value The value being parsed
         * @param bool $allowMultiple Can $value contain more than one value
         *  separated by '|'?
-        * @param mixed $allowedValues An array of values to check against. If
+        * @param string[]|null $allowedValues An array of values to check against. If
         *  null, all values are accepted.
-        * @return mixed (allowMultiple ? an_array_of_values : a_single_value)
+        * @return string|string[] (allowMultiple ? an_array_of_values : a_single_value)
         */
        protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ) {
                if ( trim( $value ) === '' && $allowMultiple ) {
index 03a6843..848d129 100644 (file)
@@ -362,10 +362,8 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
                // Disable size checking for this because we can't continue
                // cleanly; size checking would cause more problems than it'd
                // solve
-               $result->disableSizeCheck();
-               $result->addValue( null, '_feed', $feed );
-               $result->addValue( null, '_feeditems', $feedItems );
-               $result->enableSizeCheck();
+               $result->addValue( null, '_feed', $feed, ApiResult::NO_SIZE_CHECK );
+               $result->addValue( null, '_feeditems', $feedItems, ApiResult::NO_SIZE_CHECK );
        }
 
        /**
index 84db9ed..a5873e6 100644 (file)
@@ -699,21 +699,20 @@ class ApiMain extends ApiBase {
                $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
 
                $result->reset();
-               $result->disableSizeCheck();
                // Re-add the id
                $requestid = $this->getParameter( 'requestid' );
                if ( !is_null( $requestid ) ) {
-                       $result->addValue( null, 'requestid', $requestid );
+                       $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
                }
                if ( $config->get( 'ShowHostnames' ) ) {
                        // servedby is especially useful when debugging errors
-                       $result->addValue( null, 'servedby', wfHostName() );
+                       $result->addValue( null, 'servedby', wfHostName(), ApiResult::NO_SIZE_CHECK );
                }
                if ( $warnings !== null ) {
-                       $result->addValue( null, 'warnings', $warnings );
+                       $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
                }
 
-               $result->addValue( null, 'error', $errMessage );
+               $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
 
                return $errMessage['code'];
        }
index cd6a840..f4b28fa 100644 (file)
@@ -485,18 +485,16 @@ class ApiQuery extends ApiBase {
                // Don't check the size of exported stuff
                // It's not continuable, so it would cause more
                // problems than it'd solve
-               $result->disableSizeCheck();
                if ( $this->mParams['exportnowrap'] ) {
                        $result->reset();
                        // Raw formatter will handle this
-                       $result->addValue( null, 'text', $exportxml );
-                       $result->addValue( null, 'mime', 'text/xml' );
+                       $result->addValue( null, 'text', $exportxml, ApiResult::NO_SIZE_CHECK );
+                       $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
                } else {
                        $r = array();
                        ApiResult::setContent( $r, $exportxml );
-                       $result->addValue( 'query', 'export', $r );
+                       $result->addValue( 'query', 'export', $r, ApiResult::NO_SIZE_CHECK );
                }
-               $result->enableSizeCheck();
        }
 
        public function getAllowedParams( $flags = 0 ) {
index 2fd8597..fb88201 100644 (file)
@@ -75,8 +75,8 @@ abstract class ApiQueryBase extends ApiBase {
 
        /**
         * Add a set of tables to the internal array
-        * @param mixed $tables Table name or array of table names
-        * @param mixed $alias Table alias, or null for no alias. Cannot be
+        * @param string|string[] $tables Table name or array of table names
+        * @param string|null $alias Table alias, or null for no alias. Cannot be
         *  used with multiple tables
         */
        protected function addTables( $tables, $alias = null ) {
index af0d938..6994cd4 100644 (file)
@@ -134,11 +134,17 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                }
 
                if ( $fld_content ) {
+                       // Modern MediaWiki has the content for deleted revs in the 'text'
+                       // table using fields old_text and old_flags. But revisions deleted
+                       // pre-1.5 store the content in the 'archive' table directly using
+                       // fields ar_text and ar_flags, and no corresponding 'text' row. So
+                       // we have to LEFT JOIN and fetch all four fields, plus ar_text_id
+                       // to be able to tell the difference.
                        $this->addTables( 'text' );
                        $this->addJoinConds(
-                               array( 'text' => array( 'INNER JOIN', array( 'ar_text_id=old_id' ) ) )
+                               array( 'text' => array( 'LEFT JOIN', array( 'ar_text_id=old_id' ) ) )
                        );
-                       $this->addFields( array( 'ar_text', 'ar_text_id', 'old_text', 'old_flags' ) );
+                       $this->addFields( array( 'ar_text', 'ar_flags', 'ar_text_id', 'old_text', 'old_flags' ) );
 
                        // This also means stricter restrictions
                        if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
@@ -353,7 +359,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                        $anyHidden = true;
                                }
                                if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) {
-                                       ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
+                                       if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
+                                               // Pre-1.5 ar_text row (if condition from Revision::newFromArchiveRow)
+                                               ApiResult::setContent( $rev, Revision::getRevisionText( $row, 'ar_' ) );
+                                       } else {
+                                               ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
+                                       }
                                }
                        }
 
index 97918e3..25e1f38 100644 (file)
@@ -67,9 +67,8 @@ class ApiQueryFilearchive extends ApiQueryBase {
                $this->addTables( 'filearchive' );
 
                $this->addFields( ArchivedFile::selectFields() );
-               $this->addFields( array( 'fa_name', 'fa_deleted' ) );
+               $this->addFields( array( 'fa_id', 'fa_name', 'fa_timestamp', 'fa_deleted' ) );
                $this->addFieldsIf( 'fa_sha1', $fld_sha1 );
-               $this->addFieldsIf( 'fa_timestamp', $fld_timestamp );
                $this->addFieldsIf( array( 'fa_user', 'fa_user_text' ), $fld_user );
                $this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size );
                $this->addFieldsIf( 'fa_description', $fld_description );
@@ -81,18 +80,23 @@ class ApiQueryFilearchive extends ApiQueryBase {
 
                if ( !is_null( $params['continue'] ) ) {
                        $cont = explode( '|', $params['continue'] );
-                       $this->dieContinueUsageIf( count( $cont ) != 1 );
+                       $this->dieContinueUsageIf( count( $cont ) != 3 );
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $cont_from = $db->addQuotes( $cont[0] );
-                       $this->addWhere( "fa_name $op= $cont_from" );
+                       $cont_timestamp = $db->addQuotes( $db->timestamp( $cont[1] ) );
+                       $cont_id = (int)$cont[2];
+                       $this->dieContinueUsageIf( $cont[2] !== (string)$cont_id );
+                       $this->addWhere( "fa_name $op $cont_from OR " .
+                               "(fa_name = $cont_from AND " .
+                               "(fa_timestamp $op $cont_timestamp OR " .
+                               "(fa_timestamp = $cont_timestamp AND " .
+                               "fa_id $op= $cont_id )))"
+                       );
                }
 
                // Image filters
                $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
                $from = ( $params['from'] === null ? null : $this->titlePartToKey( $params['from'], NS_FILE ) );
-               if ( !is_null( $params['continue'] ) ) {
-                       $from = $params['continue'];
-               }
                $to = ( $params['to'] === null ? null : $this->titlePartToKey( $params['to'], NS_FILE ) );
                $this->addWhereRange( 'fa_name', $dir, $from, $to );
                if ( isset( $params['prefix'] ) ) {
@@ -137,7 +141,11 @@ class ApiQueryFilearchive extends ApiQueryBase {
                $limit = $params['limit'];
                $this->addOption( 'LIMIT', $limit + 1 );
                $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
-               $this->addOption( 'ORDER BY', 'fa_name' . $sort );
+               $this->addOption( 'ORDER BY', array(
+                       'fa_name' . $sort,
+                       'fa_timestamp' . $sort,
+                       'fa_id' . $sort,
+               ) );
 
                $res = $this->select( __METHOD__ );
 
@@ -147,11 +155,14 @@ class ApiQueryFilearchive extends ApiQueryBase {
                        if ( ++$count > $limit ) {
                                // We've reached the one extra which shows that there are
                                // additional pages to be had. Stop here...
-                               $this->setContinueEnumParameter( 'continue', $row->fa_name );
+                               $this->setContinueEnumParameter(
+                                       'continue', "$row->fa_name|$row->fa_timestamp|$row->fa_id"
+                               );
                                break;
                        }
 
                        $file = array();
+                       $file['id'] = $row->fa_id;
                        $file['name'] = $row->fa_name;
                        $title = Title::makeTitle( NS_FILE, $row->fa_name );
                        self::addTitleInfo( $file, $title );
@@ -222,7 +233,9 @@ class ApiQueryFilearchive extends ApiQueryBase {
 
                        $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file );
                        if ( !$fit ) {
-                               $this->setContinueEnumParameter( 'continue', $row->fa_name );
+                               $this->setContinueEnumParameter(
+                                       'continue', "$row->fa_name|$row->fa_timestamp|$row->fa_id"
+                               );
                                break;
                        }
                }
index e4078d5..c35d39b 100644 (file)
@@ -405,7 +405,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
        /**
         * Extracts from a single sql row the data needed to describe one recent change.
         *
-        * @param mixed $row The row from which to extract the data.
+        * @param stdClass $row The row from which to extract the data.
         * @return array An array mapping strings (descriptors) to their respective string values.
         * @access public
         */
index aacf091..089ce04 100644 (file)
@@ -136,7 +136,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
 
                $data['generator'] = "MediaWiki {$config->get( 'Version' )}";
 
-               $data['phpversion'] = phpversion();
+               $data['phpversion'] = PHP_VERSION;
                $data['phpsapi'] = PHP_SAPI;
                $data['dbtype'] = $config->get( 'DBtype' );
                $data['dbversion'] = $this->getDB()->getServerVersion();
index 24c4c20..5f93071 100644 (file)
@@ -326,7 +326,7 @@ class ApiQueryContributions extends ApiQueryBase {
        /**
         * Extract fields from the database row and append them to a result array
         *
-        * @param mixed $row
+        * @param stdClass $row
         * @return array
         */
        private function extractRowInfo( $row ) {
index b30d9dd..ac64cf0 100644 (file)
@@ -58,6 +58,14 @@ class ApiResult extends ApiBase {
         */
        const ADD_ON_TOP = 2;
 
+       /**
+        * For addValue() and setElement(), do not check size while adding a value
+        * Don't use this unless you REALLY know what you're doing.
+        * Values added while the size checking was disabled will never be counted
+        * @since 1.24
+        */
+       const NO_SIZE_CHECK = 4;
+
        private $mData, $mIsRawMode, $mSize, $mCheckingSize;
 
        private $continueAllModules = array();
@@ -143,6 +151,7 @@ class ApiResult extends ApiBase {
         * Disable size checking in addValue(). Don't use this unless you
         * REALLY know what you're doing. Values added while size checking
         * was disabled will not be counted (ever)
+        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
         */
        public function disableSizeCheck() {
                $this->mCheckingSize = false;
@@ -150,6 +159,7 @@ class ApiResult extends ApiBase {
 
        /**
         * Re-enable size checking in addValue()
+        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
         */
        public function enableSizeCheck() {
                $this->mCheckingSize = true;
@@ -312,17 +322,16 @@ class ApiResult extends ApiBase {
         * @param array|string|null $path
         * @param string $name
         * @param mixed $value
-        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This
-        *    parameter used to be boolean, and the value of OVERRIDE=1 was specifically
-        *    chosen so that it would be backwards compatible with the new method
-        *    signature.
+        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
+        *   This parameter used to be boolean, and the value of OVERRIDE=1 was specifically
+        *   chosen so that it would be backwards compatible with the new method signature.
         * @return bool True if $value fits in the result, false if not
         *
         * @since 1.21 int $flags replaced boolean $override
         */
        public function addValue( $path, $name, $value, $flags = 0 ) {
                $data = &$this->mData;
-               if ( $this->mCheckingSize ) {
+               if ( $this->mCheckingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
                        $newsize = $this->mSize + self::size( $value );
                        $maxResultSize = $this->getConfig()->get( 'APIMaxResultSize' );
                        if ( $newsize > $maxResultSize ) {
@@ -616,9 +625,7 @@ class ApiResult extends ApiBase {
                        }
                }
                if ( $data ) {
-                       $this->disableSizeCheck();
-                       $this->addValue( null, $key, $data, ApiResult::ADD_ON_TOP );
-                       $this->enableSizeCheck();
+                       $this->addValue( null, $key, $data, ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
                }
        }
 }
index ec36cfb..9dc3dc3 100644 (file)
@@ -1163,8 +1163,8 @@ class LCStoreDB implements LCStore {
        private $readOnly = false;
 
        public function get( $code, $key ) {
-               if ( $this->writesDone ) {
-                       $db = wfGetDB( DB_MASTER );
+               if ( $this->writesDone && $this->dbw ) {
+                       $db = $this->dbw;
                } else {
                        $db = wfGetDB( DB_SLAVE );
                }
@@ -1184,7 +1184,16 @@ class LCStoreDB implements LCStore {
                        throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
                }
 
-               $this->dbw = wfGetDB( DB_MASTER );
+               // We must keep a separate connection to MySQL in order to avoid breaking
+               // main transactions. However, SQLite deadlocks when using two connections.
+               // @TODO: get this trick to work on PostgreSQL too
+               if ( wfGetDB( DB_MASTER )->getType() == 'mysql' ) {
+                       $lb = wfGetLBFactory()->newMainLB();
+                       $this->dbw = $lb->getConnection( DB_MASTER );
+                       $this->dbw->clearFlag( DBO_TRX ); // auto-commit mode
+               } else {
+                       $this->dbw = wfGetDB( DB_MASTER );
+               }
 
                $this->currentLang = $code;
                $this->batch = array();
diff --git a/includes/changes/ChangesFeed.php b/includes/changes/ChangesFeed.php
new file mode 100644 (file)
index 0000000..fb491e5
--- /dev/null
@@ -0,0 +1,239 @@
+<?php
+/**
+ * Feed for list of changes.
+ *
+ * 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
+ */
+
+/**
+ * Feed to Special:RecentChanges and Special:RecentChangesLiked
+ *
+ * @ingroup Feed
+ */
+class ChangesFeed {
+       public $format, $type, $titleMsg, $descMsg;
+
+       /**
+        * Constructor
+        *
+        * @param string $format Feed's format (either 'rss' or 'atom')
+        * @param string $type Type of feed (for cache keys)
+        */
+       public function __construct( $format, $type ) {
+               $this->format = $format;
+               $this->type = $type;
+       }
+
+       /**
+        * Get a ChannelFeed subclass object to use
+        *
+        * @param string $title Feed's title
+        * @param string $description Feed's description
+        * @param string $url Url of origin page
+        * @return ChannelFeed|bool ChannelFeed subclass or false on failure
+        */
+       public function getFeedObject( $title, $description, $url ) {
+               global $wgSitename, $wgLanguageCode, $wgFeedClasses;
+
+               if ( !isset( $wgFeedClasses[$this->format] ) ) {
+                       return false;
+               }
+
+               if ( !array_key_exists( $this->format, $wgFeedClasses ) ) {
+                       // falling back to atom
+                       $this->format = 'atom';
+               }
+
+               $feedTitle = "$wgSitename  - {$title} [$wgLanguageCode]";
+               return new $wgFeedClasses[$this->format](
+                       $feedTitle, htmlspecialchars( $description ), $url );
+       }
+
+       /**
+        * Generates feed's content
+        *
+        * @param ChannelFeed $feed ChannelFeed subclass object (generally the one returned
+        *   by getFeedObject())
+        * @param ResultWrapper $rows ResultWrapper object with rows in recentchanges table
+        * @param int $lastmod Timestamp of the last item in the recentchanges table (only
+        *   used for the cache key)
+        * @param FormOptions $opts As in SpecialRecentChanges::getDefaultOptions()
+        * @return null|bool True or null
+        */
+       public function execute( $feed, $rows, $lastmod, $opts ) {
+               global $wgLang, $wgRenderHashAppend;
+
+               if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
+                       return null;
+               }
+
+               $optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend;
+               $timekey = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash, 'timestamp' );
+               $key = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash );
+
+               FeedUtils::checkPurge( $timekey, $key );
+
+               /**
+                * Bumping around loading up diffs can be pretty slow, so where
+                * possible we want to cache the feed output so the next visitor
+                * gets it quick too.
+                */
+               $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key );
+               if ( is_string( $cachedFeed ) ) {
+                       wfDebug( "RC: Outputting cached feed\n" );
+                       $feed->httpHeaders();
+                       echo $cachedFeed;
+               } else {
+                       wfDebug( "RC: rendering new feed and caching it\n" );
+                       ob_start();
+                       self::generateFeed( $rows, $feed );
+                       $cachedFeed = ob_get_contents();
+                       ob_end_flush();
+                       $this->saveToCache( $cachedFeed, $timekey, $key );
+               }
+               return true;
+       }
+
+       /**
+        * Save to feed result to $messageMemc
+        *
+        * @param string $feed Feed's content
+        * @param string $timekey Memcached key of the last modification
+        * @param string $key Memcached key of the content
+        */
+       public function saveToCache( $feed, $timekey, $key ) {
+               global $messageMemc;
+               $expire = 3600 * 24; # One day
+               $messageMemc->set( $key, $feed, $expire );
+               $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
+       }
+
+       /**
+        * Try to load the feed result from $messageMemc
+        *
+        * @param int $lastmod Timestamp of the last item in the recentchanges table
+        * @param string $timekey Memcached key of the last modification
+        * @param string $key Memcached key of the content
+        * @return string|bool Feed's content on cache hit or false on cache miss
+        */
+       public function loadFromCache( $lastmod, $timekey, $key ) {
+               global $wgFeedCacheTimeout, $wgOut, $messageMemc;
+
+               $feedLastmod = $messageMemc->get( $timekey );
+
+               if ( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) {
+                       /**
+                        * If the cached feed was rendered very recently, we may
+                        * go ahead and use it even if there have been edits made
+                        * since it was rendered. This keeps a swarm of requests
+                        * from being too bad on a super-frequently edited wiki.
+                        */
+
+                       $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod );
+                       $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod );
+                       $lastmodUnix = wfTimestamp( TS_UNIX, $lastmod );
+
+                       if ( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix ) {
+                               wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
+                               if ( $feedLastmodUnix < $lastmodUnix ) {
+                                       $wgOut->setLastModified( $feedLastmod ); // bug 21916
+                               }
+                               return $messageMemc->get( $key );
+                       } else {
+                               wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Generate the feed items given a row from the database, printing the feed.
+        * @param object $rows DatabaseBase resource with recentchanges rows
+        * @param Feed $feed
+        */
+       public static function generateFeed( $rows, &$feed ) {
+               wfProfileIn( __METHOD__ );
+               $items = self::buildItems( $rows );
+               $feed->outHeader();
+               foreach ( $items as $item ) {
+                       $feed->outItem( $item );
+               }
+               $feed->outFooter();
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Generate the feed items given a row from the database.
+        * @param object $rows DatabaseBase resource with recentchanges rows
+        */
+       public static function buildItems( $rows ) {
+               wfProfileIn( __METHOD__ );
+               $items = array();
+
+               # Merge adjacent edits by one user
+               $sorted = array();
+               $n = 0;
+               foreach ( $rows as $obj ) {
+                       if ( $n > 0 &&
+                               $obj->rc_type == RC_EDIT &&
+                               $obj->rc_namespace >= 0 &&
+                               $obj->rc_cur_id == $sorted[$n - 1]->rc_cur_id &&
+                               $obj->rc_user_text == $sorted[$n - 1]->rc_user_text ) {
+                               $sorted[$n - 1]->rc_last_oldid = $obj->rc_last_oldid;
+                       } else {
+                               $sorted[$n] = $obj;
+                               $n++;
+                       }
+               }
+
+               foreach ( $sorted as $obj ) {
+                       $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
+                       $talkpage = MWNamespace::canTalk( $obj->rc_namespace )
+                               ? $title->getTalkPage()->getFullURL()
+                               : '';
+
+                       // Skip items with deleted content (avoids partially complete/inconsistent output)
+                       if ( $obj->rc_deleted ) {
+                               continue;
+                       }
+
+                       if ( $obj->rc_this_oldid ) {
+                               $url = $title->getFullURL( array(
+                                       'diff' => $obj->rc_this_oldid,
+                                       'oldid' => $obj->rc_last_oldid,
+                               ) );
+                       } else {
+                               // log entry or something like that.
+                               $url = $title->getFullURL();
+                       }
+
+                       $items[] = new FeedItem(
+                               $title->getPrefixedText(),
+                               FeedUtils::formatDiff( $obj ),
+                               $url,
+                               $obj->rc_timestamp,
+                               ( $obj->rc_deleted & Revision::DELETED_USER )
+                                       ? wfMessage( 'rev-deleted-user' )->escaped() : $obj->rc_user_text,
+                               $talkpage
+                       );
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $items;
+       }
+}
index 36d2731..d2c504a 100644 (file)
@@ -101,7 +101,7 @@ class RedisConnectionPool {
                        $options['connectTimeout'] = 1;
                }
                if ( !isset( $options['readTimeout'] ) ) {
-                       $options['readTimeout'] = 31; // handles up to 30 second blocking commands
+                       $options['readTimeout'] = 1;
                }
                if ( !isset( $options['persistent'] ) ) {
                        $options['persistent'] = false;
@@ -120,7 +120,7 @@ class RedisConnectionPool {
         *                      Optional, default is 1 second.
         *   - readTimeout    : The timeout for operation reads, in seconds.
         *                      Commands like BLPOP can fail if told to wait longer than this.
-        *                      Optional, default is 60 seconds.
+        *                      Optional, default is 1 second.
         *   - persistent     : Set this to true to allow connections to persist across
         *                      multiple web requests. False by default.
         *   - password       : The authentication password, will be sent to Redis in clear text.
@@ -342,6 +342,16 @@ class RedisConnectionPool {
                return true;
        }
 
+       /**
+        * Adjust or reset the connection handle read timeout value
+        *
+        * @param Redis $conn
+        * @param integer $timeout Optional
+        */
+       public function resetTimeout( Redis $conn, $timeout = null ) {
+               $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
+       }
+
        /**
         * Make sure connections are closed for sanity
         */
@@ -401,17 +411,34 @@ class RedisConnRef {
        public function __call( $name, $arguments ) {
                $conn = $this->conn; // convenience
 
+               // Work around https://github.com/nicolasff/phpredis/issues/70
+               $lname = strtolower( $name );
+               if ( ( $lname === 'blpop' || $lname == 'brpop' )
+                       && is_array( $arguments[0] ) && isset( $arguments[1] )
+               ) {
+                       $this->pool->resetTimeout( $conn, $arguments[1] + 1 );
+               } elseif ( $lname === 'brpoplpush' && isset( $arguments[2] ) ) {
+                       $this->pool->resetTimeout( $conn, $arguments[2] + 1 );
+               }
+
                $conn->clearLastError();
-               $res = call_user_func_array( array( $conn, $name ), $arguments );
-               if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
-                       $this->pool->reauthenticateConnection( $this->server, $conn );
-                       $conn->clearLastError();
+               try {
                        $res = call_user_func_array( array( $conn, $name ), $arguments );
-                       wfDebugLog( 'redis', "Used automatic re-authentication for method '$name'." );
+                       if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
+                               $this->pool->reauthenticateConnection( $this->server, $conn );
+                               $conn->clearLastError();
+                               $res = call_user_func_array( array( $conn, $name ), $arguments );
+                               wfDebugLog( 'redis', "Used automatic re-authentication for method '$name'." );
+                       }
+               } catch ( RedisException $e ) {
+                       $this->pool->resetTimeout( $conn ); // restore
+                       throw $e;
                }
 
                $this->lastError = $conn->getLastError() ?: $this->lastError;
 
+               $this->pool->resetTimeout( $conn ); // restore
+
                return $res;
        }
 
index f550e9e..d78f420 100644 (file)
@@ -126,12 +126,8 @@ class DerivativeContext extends ContextSource {
         * Set the Title object
         *
         * @param Title $t
-        * @throws MWException
         */
-       public function setTitle( $t ) {
-               if ( $t !== null && !$t instanceof Title ) {
-                       throw new MWException( __METHOD__ . " expects an instance of Title" );
-               }
+       public function setTitle( Title $t ) {
                $this->title = $t;
        }
 
index d4bf0b4..efdc6db 100644 (file)
@@ -124,12 +124,8 @@ class RequestContext implements IContextSource {
         * Set the Title object
         *
         * @param Title $t
-        * @throws MWException
         */
-       public function setTitle( $t ) {
-               if ( $t !== null && !$t instanceof Title ) {
-                       throw new MWException( __METHOD__ . " expects an instance of Title" );
-               }
+       public function setTitle( Title $t ) {
                $this->title = $t;
                // Erase the WikiPage so a new one with the new title gets created.
                $this->wikipage = null;
@@ -475,7 +471,8 @@ class RequestContext implements IContextSource {
 
                if ( $params['userId'] ) { // logged-in user
                        $user = User::newFromId( $params['userId'] );
-                       if ( !$user ) {
+                       $user->load();
+                       if ( !$user->getId() ) {
                                throw new MWException( "No user with ID '{$params['userId']}'." );
                        }
                } elseif ( !IP::isValid( $params['ip'] ) ) {
index a03dd75..be01f1a 100644 (file)
@@ -1004,6 +1004,11 @@ class DatabaseMssql extends DatabaseBase {
                        return false;
                }
 
+               if ( $schema === false ) {
+                       global $wgDBmwschema;
+                       $schema = $wgDBmwschema;
+               }
+
                $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
                        WHERE TABLE_TYPE = 'BASE TABLE'
                        AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" );
@@ -1341,7 +1346,7 @@ class DatabaseMssql extends DatabaseBase {
                        // Used internally, we want the schema split off from the table name and returned
                        // as a list with 3 elements (database, schema, table)
                        $table = explode( '.', $table );
-                       if ( count( $table ) == 2 ) {
+                       while ( count( $table ) < 3 ) {
                                array_unshift( $table, false );
                        }
                }
index 3d7267a..fe5fa1f 100644 (file)
@@ -827,6 +827,8 @@ __INDEXATTR__;
         * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
         * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do
         * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly.
+        * 
+        * MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL.
         */
        function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
                $options = array(), $join_conds = array()
@@ -842,6 +844,10 @@ __INDEXATTR__;
                                        }
                                }
                        }
+
+                       if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
+                               unset( $options['ORDER BY'] );
+                       }
                }
 
                return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
index b3f9210..5353288 100644 (file)
@@ -1048,8 +1048,22 @@ class LoadBalancer {
                $maxLag = -1;
                $host = '';
                $maxIndex = 0;
-               if ( $this->getServerCount() > 1 ) { // no replication = no lag
+
+               if ( $this->getServerCount() <= 1 ) { // no replication = no lag
+                       return array( $host, $maxLag, $maxIndex );
+               }
+
+               // Try to get the max lag info from the server cache
+               $key = 'loadbalancer:maxlag:cluster:' . $this->mServers[0]['host'];
+               $cache = ObjectCache::newAccelerator( array(), 'hash' );
+               $maxLagInfo = $cache->get( $key ); // (host, lag, index)
+
+               // Fallback to connecting to each slave and getting the lag
+               if ( !$maxLagInfo ) {
                        foreach ( $this->mServers as $i => $conn ) {
+                               if ( $i == $this->getWriterIndex() ) {
+                                       continue; // nothing to check
+                               }
                                $conn = false;
                                if ( $wiki === false ) {
                                        $conn = $this->getAnyOpenConnection( $i );
@@ -1067,9 +1081,11 @@ class LoadBalancer {
                                        $maxIndex = $i;
                                }
                        }
+                       $maxLagInfo = array( $host, $maxLag, $maxIndex );
+                       $cache->set( $key, $maxLagInfo, 5 );
                }
 
-               return array( $host, $maxLag, $maxIndex );
+               return $maxLagInfo;
        }
 
        /**
diff --git a/includes/debug/Debug.php b/includes/debug/Debug.php
deleted file mode 100644 (file)
index 0cea658..0000000
+++ /dev/null
@@ -1,562 +0,0 @@
-<?php
-/**
- * Debug toolbar related code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * New debugger system that outputs a toolbar on page view.
- *
- * By default, most methods do nothing ( self::$enabled = false ). You have
- * to explicitly call MWDebug::init() to enabled them.
- *
- * @todo Profiler support
- *
- * @since 1.19
- */
-class MWDebug {
-       /**
-        * Log lines
-        *
-        * @var array $log
-        */
-       protected static $log = array();
-
-       /**
-        * Debug messages from wfDebug().
-        *
-        * @var array $debug
-        */
-       protected static $debug = array();
-
-       /**
-        * SQL statements of the databses queries.
-        *
-        * @var array $query
-        */
-       protected static $query = array();
-
-       /**
-        * Is the debugger enabled?
-        *
-        * @var bool $enabled
-        */
-       protected static $enabled = false;
-
-       /**
-        * Array of functions that have already been warned, formatted
-        * function-caller to prevent a buttload of warnings
-        *
-        * @var array $deprecationWarnings
-        */
-       protected static $deprecationWarnings = array();
-
-       /**
-        * Enabled the debugger and load resource module.
-        * This is called by Setup.php when $wgDebugToolbar is true.
-        *
-        * @since 1.19
-        */
-       public static function init() {
-               self::$enabled = true;
-       }
-
-       /**
-        * Add ResourceLoader modules to the OutputPage object if debugging is
-        * enabled.
-        *
-        * @since 1.19
-        * @param OutputPage $out
-        */
-       public static function addModules( OutputPage $out ) {
-               if ( self::$enabled ) {
-                       $out->addModules( 'mediawiki.debug.init' );
-               }
-       }
-
-       /**
-        * Adds a line to the log
-        *
-        * @todo Add support for passing objects
-        *
-        * @since 1.19
-        * @param string $str
-        */
-       public static function log( $str ) {
-               if ( !self::$enabled ) {
-                       return;
-               }
-
-               self::$log[] = array(
-                       'msg' => htmlspecialchars( $str ),
-                       'type' => 'log',
-                       'caller' => wfGetCaller(),
-               );
-       }
-
-       /**
-        * Returns internal log array
-        * @since 1.19
-        * @return array
-        */
-       public static function getLog() {
-               return self::$log;
-       }
-
-       /**
-        * Clears internal log array and deprecation tracking
-        * @since 1.19
-        */
-       public static function clearLog() {
-               self::$log = array();
-               self::$deprecationWarnings = array();
-       }
-
-       /**
-        * Adds a warning entry to the log
-        *
-        * @since 1.19
-        * @param string $msg
-        * @param int $callerOffset
-        * @param int $level A PHP error level. See sendMessage()
-        * @param string $log 'production' will always trigger a php error, 'auto'
-        *    will trigger an error if $wgDevelopmentWarnings is true, and 'debug'
-        *    will only write to the debug log(s).
-        *
-        * @return mixed
-        */
-       public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
-               global $wgDevelopmentWarnings;
-
-               if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
-                       $log = 'debug';
-               }
-
-               if ( $log === 'debug' ) {
-                       $level = false;
-               }
-
-               $callerDescription = self::getCallerDescription( $callerOffset );
-
-               self::sendMessage( $msg, $callerDescription, 'warning', $level );
-
-               if ( self::$enabled ) {
-                       self::$log[] = array(
-                               'msg' => htmlspecialchars( $msg ),
-                               'type' => 'warn',
-                               'caller' => $callerDescription['func'],
-                       );
-               }
-       }
-
-       /**
-        * Show a warning that $function is deprecated.
-        * This will send it to the following locations:
-        * - Debug toolbar, with one item per function and caller, if $wgDebugToolbar
-        *   is set to true.
-        * - PHP's error log, with level E_USER_DEPRECATED, if $wgDevelopmentWarnings
-        *   is set to true.
-        * - MediaWiki's debug log, if $wgDevelopmentWarnings is set to false.
-        *
-        * @since 1.19
-        * @param string $function Function that is deprecated.
-        * @param string|bool $version Version in which the function was deprecated.
-        * @param string|bool $component Component to which the function belongs.
-        *    If false, it is assumbed the function is in MediaWiki core.
-        * @param int $callerOffset How far up the callstack is the original
-        *    caller. 2 = function that called the function that called
-        *    MWDebug::deprecated() (Added in 1.20).
-        * @return mixed
-        */
-       public static function deprecated( $function, $version = false,
-               $component = false, $callerOffset = 2
-       ) {
-               $callerDescription = self::getCallerDescription( $callerOffset );
-               $callerFunc = $callerDescription['func'];
-
-               $sendToLog = true;
-
-               // Check to see if there already was a warning about this function
-               if ( isset( self::$deprecationWarnings[$function][$callerFunc] ) ) {
-                       return;
-               } elseif ( isset( self::$deprecationWarnings[$function] ) ) {
-                       if ( self::$enabled ) {
-                               $sendToLog = false;
-                       } else {
-                               return;
-                       }
-               }
-
-               self::$deprecationWarnings[$function][$callerFunc] = true;
-
-               if ( $version ) {
-                       global $wgDeprecationReleaseLimit;
-                       if ( $wgDeprecationReleaseLimit && $component === false ) {
-                               # Strip -* off the end of $version so that branches can use the
-                               # format #.##-branchname to avoid issues if the branch is merged into
-                               # a version of MediaWiki later than what it was branched from
-                               $comparableVersion = preg_replace( '/-.*$/', '', $version );
-
-                               # If the comparableVersion is larger than our release limit then
-                               # skip the warning message for the deprecation
-                               if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
-                                       $sendToLog = false;
-                               }
-                       }
-
-                       $component = $component === false ? 'MediaWiki' : $component;
-                       $msg = "Use of $function was deprecated in $component $version.";
-               } else {
-                       $msg = "Use of $function is deprecated.";
-               }
-
-               if ( $sendToLog ) {
-                       global $wgDevelopmentWarnings; // we could have a more specific $wgDeprecationWarnings setting.
-                       self::sendMessage(
-                               $msg,
-                               $callerDescription,
-                               'deprecated',
-                               $wgDevelopmentWarnings ? E_USER_DEPRECATED : false
-                       );
-               }
-
-               if ( self::$enabled ) {
-                       $logMsg = htmlspecialchars( $msg ) .
-                               Html::rawElement( 'div', array( 'class' => 'mw-debug-backtrace' ),
-                                       Html::element( 'span', array(), 'Backtrace:' ) . wfBacktrace()
-                               );
-
-                       self::$log[] = array(
-                               'msg' => $logMsg,
-                               'type' => 'deprecated',
-                               'caller' => $callerFunc,
-                       );
-               }
-       }
-
-       /**
-        * Get an array describing the calling function at a specified offset.
-        *
-        * @param int $callerOffset How far up the callstack is the original
-        *    caller. 0 = function that called getCallerDescription()
-        * @return array Array with two keys: 'file' and 'func'
-        */
-       private static function getCallerDescription( $callerOffset ) {
-               $callers = wfDebugBacktrace();
-
-               if ( isset( $callers[$callerOffset] ) ) {
-                       $callerfile = $callers[$callerOffset];
-                       if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
-                               $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
-                       } else {
-                               $file = '(internal function)';
-                       }
-               } else {
-                       $file = '(unknown location)';
-               }
-
-               if ( isset( $callers[$callerOffset + 1] ) ) {
-                       $callerfunc = $callers[$callerOffset + 1];
-                       $func = '';
-                       if ( isset( $callerfunc['class'] ) ) {
-                               $func .= $callerfunc['class'] . '::';
-                       }
-                       if ( isset( $callerfunc['function'] ) ) {
-                               $func .= $callerfunc['function'];
-                       }
-               } else {
-                       $func = 'unknown';
-               }
-
-               return array( 'file' => $file, 'func' => $func );
-       }
-
-       /**
-        * Send a message to the debug log and optionally also trigger a PHP
-        * error, depending on the $level argument.
-        *
-        * @param string $msg Message to send
-        * @param array $caller Caller description get from getCallerDescription()
-        * @param string $group Log group on which to send the message
-        * @param int|bool $level Error level to use; set to false to not trigger an error
-        */
-       private static function sendMessage( $msg, $caller, $group, $level ) {
-               $msg .= ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
-
-               if ( $level !== false ) {
-                       trigger_error( $msg, $level );
-               }
-
-               wfDebugLog( $group, $msg, 'log' );
-       }
-
-       /**
-        * This is a method to pass messages from wfDebug to the pretty debugger.
-        * Do NOT use this method, use MWDebug::log or wfDebug()
-        *
-        * @since 1.19
-        * @param string $str
-        */
-       public static function debugMsg( $str ) {
-               global $wgDebugComments, $wgShowDebug;
-
-               if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
-                       self::$debug[] = rtrim( UtfNormal::cleanUp( $str ) );
-               }
-       }
-
-       /**
-        * Begins profiling on a database query
-        *
-        * @since 1.19
-        * @param string $sql
-        * @param string $function
-        * @param bool $isMaster
-        * @return int ID number of the query to pass to queryTime or -1 if the
-        *  debugger is disabled
-        */
-       public static function query( $sql, $function, $isMaster ) {
-               if ( !self::$enabled ) {
-                       return -1;
-               }
-
-               self::$query[] = array(
-                       'sql' => $sql,
-                       'function' => $function,
-                       'master' => (bool)$isMaster,
-                       'time' => 0.0,
-                       '_start' => microtime( true ),
-               );
-
-               return count( self::$query ) - 1;
-       }
-
-       /**
-        * Calculates how long a query took.
-        *
-        * @since 1.19
-        * @param int $id
-        */
-       public static function queryTime( $id ) {
-               if ( $id === -1 || !self::$enabled ) {
-                       return;
-               }
-
-               self::$query[$id]['time'] = microtime( true ) - self::$query[$id]['_start'];
-               unset( self::$query[$id]['_start'] );
-       }
-
-       /**
-        * Returns a list of files included, along with their size
-        *
-        * @param IContextSource $context
-        * @return array
-        */
-       protected static function getFilesIncluded( IContextSource $context ) {
-               $files = get_included_files();
-               $fileList = array();
-               foreach ( $files as $file ) {
-                       $size = filesize( $file );
-                       $fileList[] = array(
-                               'name' => $file,
-                               'size' => $context->getLanguage()->formatSize( $size ),
-                       );
-               }
-
-               return $fileList;
-       }
-
-       /**
-        * Returns the HTML to add to the page for the toolbar
-        *
-        * @since 1.19
-        * @param IContextSource $context
-        * @return string
-        */
-       public static function getDebugHTML( IContextSource $context ) {
-               global $wgDebugComments;
-
-               $html = '';
-
-               if ( self::$enabled ) {
-                       MWDebug::log( 'MWDebug output complete' );
-                       $debugInfo = self::getDebugInfo( $context );
-
-                       // Cannot use OutputPage::addJsConfigVars because those are already outputted
-                       // by the time this method is called.
-                       $html = Html::inlineScript(
-                               ResourceLoader::makeLoaderConditionalScript(
-                                       ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) )
-                               )
-                       );
-               }
-
-               if ( $wgDebugComments ) {
-                       $html .= "<!-- Debug output:\n" .
-                               htmlspecialchars( implode( "\n", self::$debug ) ) .
-                               "\n\n-->";
-               }
-
-               return $html;
-       }
-
-       /**
-        * Generate debug log in HTML for displaying at the bottom of the main
-        * content area.
-        * If $wgShowDebug is false, an empty string is always returned.
-        *
-        * @since 1.20
-        * @return string HTML fragment
-        */
-       public static function getHTMLDebugLog() {
-               global $wgDebugTimestamps, $wgShowDebug;
-
-               if ( !$wgShowDebug ) {
-                       return '';
-               }
-
-               $curIdent = 0;
-               $ret = "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">\n<li>";
-
-               foreach ( self::$debug as $line ) {
-                       $pre = '';
-                       if ( $wgDebugTimestamps ) {
-                               $matches = array();
-                               if ( preg_match( '/^(\d+\.\d+ {1,3}\d+.\dM\s{2})/', $line, $matches ) ) {
-                                       $pre = $matches[1];
-                                       $line = substr( $line, strlen( $pre ) );
-                               }
-                       }
-                       $display = ltrim( $line );
-                       $ident = strlen( $line ) - strlen( $display );
-                       $diff = $ident - $curIdent;
-
-                       $display = $pre . $display;
-                       if ( $display == '' ) {
-                               $display = "\xc2\xa0";
-                       }
-
-                       if ( !$ident
-                               && $diff < 0
-                               && substr( $display, 0, 9 ) != 'Entering '
-                               && substr( $display, 0, 8 ) != 'Exiting '
-                       ) {
-                               $ident = $curIdent;
-                               $diff = 0;
-                               $display = '<span style="background:yellow;">' .
-                                       nl2br( htmlspecialchars( $display ) ) . '</span>';
-                       } else {
-                               $display = nl2br( htmlspecialchars( $display ) );
-                       }
-
-                       if ( $diff < 0 ) {
-                               $ret .= str_repeat( "</li></ul>\n", -$diff ) . "</li><li>\n";
-                       } elseif ( $diff == 0 ) {
-                               $ret .= "</li><li>\n";
-                       } else {
-                               $ret .= str_repeat( "<ul><li>\n", $diff );
-                       }
-                       $ret .= "<code>$display</code>\n";
-
-                       $curIdent = $ident;
-               }
-
-               $ret .= str_repeat( '</li></ul>', $curIdent ) . "</li>\n</ul>\n";
-
-               return $ret;
-       }
-
-       /**
-        * Append the debug info to given ApiResult
-        *
-        * @param IContextSource $context
-        * @param ApiResult $result
-        */
-       public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
-               if ( !self::$enabled ) {
-                       return;
-               }
-
-               // output errors as debug info, when display_errors is on
-               // this is necessary for all non html output of the api, because that clears all errors first
-               $obContents = ob_get_contents();
-               if ( $obContents ) {
-                       $obContentArray = explode( '<br />', $obContents );
-                       foreach ( $obContentArray as $obContent ) {
-                               if ( trim( $obContent ) ) {
-                                       self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
-                               }
-                       }
-               }
-
-               MWDebug::log( 'MWDebug output complete' );
-               $debugInfo = self::getDebugInfo( $context );
-
-               $result->setIndexedTagName( $debugInfo, 'debuginfo' );
-               $result->setIndexedTagName( $debugInfo['log'], 'line' );
-               $result->setIndexedTagName( $debugInfo['debugLog'], 'msg' );
-               $result->setIndexedTagName( $debugInfo['queries'], 'query' );
-               $result->setIndexedTagName( $debugInfo['includes'], 'queries' );
-               $result->setIndexedTagName( $debugInfo['profile'], 'function' );
-               $result->addValue( null, 'debuginfo', $debugInfo );
-       }
-
-       /**
-        * Returns the HTML to add to the page for the toolbar
-        *
-        * @param IContextSource $context
-        * @return array
-        */
-       public static function getDebugInfo( IContextSource $context ) {
-               if ( !self::$enabled ) {
-                       return array();
-               }
-
-               global $wgVersion, $wgRequestTime;
-               $request = $context->getRequest();
-
-               // HHVM's reported memory usage from memory_get_peak_usage()
-               // is not useful when passing false, but we continue passing
-               // false for consistency of historical data in zend.
-               // see: https://github.com/facebook/hhvm/issues/2257#issuecomment-39362246
-               $realMemoryUsage = wfIsHHVM();
-
-               return array(
-                       'mwVersion' => $wgVersion,
-                       'phpVersion' => PHP_VERSION,
-                       'gitRevision' => GitInfo::headSHA1(),
-                       'gitBranch' => GitInfo::currentBranch(),
-                       'gitViewUrl' => GitInfo::headViewUrl(),
-                       'time' => microtime( true ) - $wgRequestTime,
-                       'log' => self::$log,
-                       'debugLog' => self::$debug,
-                       'queries' => self::$query,
-                       'request' => array(
-                               'method' => $request->getMethod(),
-                               'url' => $request->getRequestURL(),
-                               'headers' => $request->getAllHeaders(),
-                               'params' => $request->getValues(),
-                       ),
-                       'memory' => $context->getLanguage()->formatSize( memory_get_usage( $realMemoryUsage ) ),
-                       'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage( $realMemoryUsage ) ),
-                       'includes' => self::getFilesIncluded( $context ),
-                       'profile' => Profiler::instance()->getRawData(),
-               );
-       }
-}
diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php
new file mode 100644 (file)
index 0000000..0cea658
--- /dev/null
@@ -0,0 +1,562 @@
+<?php
+/**
+ * Debug toolbar related code.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * New debugger system that outputs a toolbar on page view.
+ *
+ * By default, most methods do nothing ( self::$enabled = false ). You have
+ * to explicitly call MWDebug::init() to enabled them.
+ *
+ * @todo Profiler support
+ *
+ * @since 1.19
+ */
+class MWDebug {
+       /**
+        * Log lines
+        *
+        * @var array $log
+        */
+       protected static $log = array();
+
+       /**
+        * Debug messages from wfDebug().
+        *
+        * @var array $debug
+        */
+       protected static $debug = array();
+
+       /**
+        * SQL statements of the databses queries.
+        *
+        * @var array $query
+        */
+       protected static $query = array();
+
+       /**
+        * Is the debugger enabled?
+        *
+        * @var bool $enabled
+        */
+       protected static $enabled = false;
+
+       /**
+        * Array of functions that have already been warned, formatted
+        * function-caller to prevent a buttload of warnings
+        *
+        * @var array $deprecationWarnings
+        */
+       protected static $deprecationWarnings = array();
+
+       /**
+        * Enabled the debugger and load resource module.
+        * This is called by Setup.php when $wgDebugToolbar is true.
+        *
+        * @since 1.19
+        */
+       public static function init() {
+               self::$enabled = true;
+       }
+
+       /**
+        * Add ResourceLoader modules to the OutputPage object if debugging is
+        * enabled.
+        *
+        * @since 1.19
+        * @param OutputPage $out
+        */
+       public static function addModules( OutputPage $out ) {
+               if ( self::$enabled ) {
+                       $out->addModules( 'mediawiki.debug.init' );
+               }
+       }
+
+       /**
+        * Adds a line to the log
+        *
+        * @todo Add support for passing objects
+        *
+        * @since 1.19
+        * @param string $str
+        */
+       public static function log( $str ) {
+               if ( !self::$enabled ) {
+                       return;
+               }
+
+               self::$log[] = array(
+                       'msg' => htmlspecialchars( $str ),
+                       'type' => 'log',
+                       'caller' => wfGetCaller(),
+               );
+       }
+
+       /**
+        * Returns internal log array
+        * @since 1.19
+        * @return array
+        */
+       public static function getLog() {
+               return self::$log;
+       }
+
+       /**
+        * Clears internal log array and deprecation tracking
+        * @since 1.19
+        */
+       public static function clearLog() {
+               self::$log = array();
+               self::$deprecationWarnings = array();
+       }
+
+       /**
+        * Adds a warning entry to the log
+        *
+        * @since 1.19
+        * @param string $msg
+        * @param int $callerOffset
+        * @param int $level A PHP error level. See sendMessage()
+        * @param string $log 'production' will always trigger a php error, 'auto'
+        *    will trigger an error if $wgDevelopmentWarnings is true, and 'debug'
+        *    will only write to the debug log(s).
+        *
+        * @return mixed
+        */
+       public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
+               global $wgDevelopmentWarnings;
+
+               if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
+                       $log = 'debug';
+               }
+
+               if ( $log === 'debug' ) {
+                       $level = false;
+               }
+
+               $callerDescription = self::getCallerDescription( $callerOffset );
+
+               self::sendMessage( $msg, $callerDescription, 'warning', $level );
+
+               if ( self::$enabled ) {
+                       self::$log[] = array(
+                               'msg' => htmlspecialchars( $msg ),
+                               'type' => 'warn',
+                               'caller' => $callerDescription['func'],
+                       );
+               }
+       }
+
+       /**
+        * Show a warning that $function is deprecated.
+        * This will send it to the following locations:
+        * - Debug toolbar, with one item per function and caller, if $wgDebugToolbar
+        *   is set to true.
+        * - PHP's error log, with level E_USER_DEPRECATED, if $wgDevelopmentWarnings
+        *   is set to true.
+        * - MediaWiki's debug log, if $wgDevelopmentWarnings is set to false.
+        *
+        * @since 1.19
+        * @param string $function Function that is deprecated.
+        * @param string|bool $version Version in which the function was deprecated.
+        * @param string|bool $component Component to which the function belongs.
+        *    If false, it is assumbed the function is in MediaWiki core.
+        * @param int $callerOffset How far up the callstack is the original
+        *    caller. 2 = function that called the function that called
+        *    MWDebug::deprecated() (Added in 1.20).
+        * @return mixed
+        */
+       public static function deprecated( $function, $version = false,
+               $component = false, $callerOffset = 2
+       ) {
+               $callerDescription = self::getCallerDescription( $callerOffset );
+               $callerFunc = $callerDescription['func'];
+
+               $sendToLog = true;
+
+               // Check to see if there already was a warning about this function
+               if ( isset( self::$deprecationWarnings[$function][$callerFunc] ) ) {
+                       return;
+               } elseif ( isset( self::$deprecationWarnings[$function] ) ) {
+                       if ( self::$enabled ) {
+                               $sendToLog = false;
+                       } else {
+                               return;
+                       }
+               }
+
+               self::$deprecationWarnings[$function][$callerFunc] = true;
+
+               if ( $version ) {
+                       global $wgDeprecationReleaseLimit;
+                       if ( $wgDeprecationReleaseLimit && $component === false ) {
+                               # Strip -* off the end of $version so that branches can use the
+                               # format #.##-branchname to avoid issues if the branch is merged into
+                               # a version of MediaWiki later than what it was branched from
+                               $comparableVersion = preg_replace( '/-.*$/', '', $version );
+
+                               # If the comparableVersion is larger than our release limit then
+                               # skip the warning message for the deprecation
+                               if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
+                                       $sendToLog = false;
+                               }
+                       }
+
+                       $component = $component === false ? 'MediaWiki' : $component;
+                       $msg = "Use of $function was deprecated in $component $version.";
+               } else {
+                       $msg = "Use of $function is deprecated.";
+               }
+
+               if ( $sendToLog ) {
+                       global $wgDevelopmentWarnings; // we could have a more specific $wgDeprecationWarnings setting.
+                       self::sendMessage(
+                               $msg,
+                               $callerDescription,
+                               'deprecated',
+                               $wgDevelopmentWarnings ? E_USER_DEPRECATED : false
+                       );
+               }
+
+               if ( self::$enabled ) {
+                       $logMsg = htmlspecialchars( $msg ) .
+                               Html::rawElement( 'div', array( 'class' => 'mw-debug-backtrace' ),
+                                       Html::element( 'span', array(), 'Backtrace:' ) . wfBacktrace()
+                               );
+
+                       self::$log[] = array(
+                               'msg' => $logMsg,
+                               'type' => 'deprecated',
+                               'caller' => $callerFunc,
+                       );
+               }
+       }
+
+       /**
+        * Get an array describing the calling function at a specified offset.
+        *
+        * @param int $callerOffset How far up the callstack is the original
+        *    caller. 0 = function that called getCallerDescription()
+        * @return array Array with two keys: 'file' and 'func'
+        */
+       private static function getCallerDescription( $callerOffset ) {
+               $callers = wfDebugBacktrace();
+
+               if ( isset( $callers[$callerOffset] ) ) {
+                       $callerfile = $callers[$callerOffset];
+                       if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
+                               $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
+                       } else {
+                               $file = '(internal function)';
+                       }
+               } else {
+                       $file = '(unknown location)';
+               }
+
+               if ( isset( $callers[$callerOffset + 1] ) ) {
+                       $callerfunc = $callers[$callerOffset + 1];
+                       $func = '';
+                       if ( isset( $callerfunc['class'] ) ) {
+                               $func .= $callerfunc['class'] . '::';
+                       }
+                       if ( isset( $callerfunc['function'] ) ) {
+                               $func .= $callerfunc['function'];
+                       }
+               } else {
+                       $func = 'unknown';
+               }
+
+               return array( 'file' => $file, 'func' => $func );
+       }
+
+       /**
+        * Send a message to the debug log and optionally also trigger a PHP
+        * error, depending on the $level argument.
+        *
+        * @param string $msg Message to send
+        * @param array $caller Caller description get from getCallerDescription()
+        * @param string $group Log group on which to send the message
+        * @param int|bool $level Error level to use; set to false to not trigger an error
+        */
+       private static function sendMessage( $msg, $caller, $group, $level ) {
+               $msg .= ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
+
+               if ( $level !== false ) {
+                       trigger_error( $msg, $level );
+               }
+
+               wfDebugLog( $group, $msg, 'log' );
+       }
+
+       /**
+        * This is a method to pass messages from wfDebug to the pretty debugger.
+        * Do NOT use this method, use MWDebug::log or wfDebug()
+        *
+        * @since 1.19
+        * @param string $str
+        */
+       public static function debugMsg( $str ) {
+               global $wgDebugComments, $wgShowDebug;
+
+               if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
+                       self::$debug[] = rtrim( UtfNormal::cleanUp( $str ) );
+               }
+       }
+
+       /**
+        * Begins profiling on a database query
+        *
+        * @since 1.19
+        * @param string $sql
+        * @param string $function
+        * @param bool $isMaster
+        * @return int ID number of the query to pass to queryTime or -1 if the
+        *  debugger is disabled
+        */
+       public static function query( $sql, $function, $isMaster ) {
+               if ( !self::$enabled ) {
+                       return -1;
+               }
+
+               self::$query[] = array(
+                       'sql' => $sql,
+                       'function' => $function,
+                       'master' => (bool)$isMaster,
+                       'time' => 0.0,
+                       '_start' => microtime( true ),
+               );
+
+               return count( self::$query ) - 1;
+       }
+
+       /**
+        * Calculates how long a query took.
+        *
+        * @since 1.19
+        * @param int $id
+        */
+       public static function queryTime( $id ) {
+               if ( $id === -1 || !self::$enabled ) {
+                       return;
+               }
+
+               self::$query[$id]['time'] = microtime( true ) - self::$query[$id]['_start'];
+               unset( self::$query[$id]['_start'] );
+       }
+
+       /**
+        * Returns a list of files included, along with their size
+        *
+        * @param IContextSource $context
+        * @return array
+        */
+       protected static function getFilesIncluded( IContextSource $context ) {
+               $files = get_included_files();
+               $fileList = array();
+               foreach ( $files as $file ) {
+                       $size = filesize( $file );
+                       $fileList[] = array(
+                               'name' => $file,
+                               'size' => $context->getLanguage()->formatSize( $size ),
+                       );
+               }
+
+               return $fileList;
+       }
+
+       /**
+        * Returns the HTML to add to the page for the toolbar
+        *
+        * @since 1.19
+        * @param IContextSource $context
+        * @return string
+        */
+       public static function getDebugHTML( IContextSource $context ) {
+               global $wgDebugComments;
+
+               $html = '';
+
+               if ( self::$enabled ) {
+                       MWDebug::log( 'MWDebug output complete' );
+                       $debugInfo = self::getDebugInfo( $context );
+
+                       // Cannot use OutputPage::addJsConfigVars because those are already outputted
+                       // by the time this method is called.
+                       $html = Html::inlineScript(
+                               ResourceLoader::makeLoaderConditionalScript(
+                                       ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) )
+                               )
+                       );
+               }
+
+               if ( $wgDebugComments ) {
+                       $html .= "<!-- Debug output:\n" .
+                               htmlspecialchars( implode( "\n", self::$debug ) ) .
+                               "\n\n-->";
+               }
+
+               return $html;
+       }
+
+       /**
+        * Generate debug log in HTML for displaying at the bottom of the main
+        * content area.
+        * If $wgShowDebug is false, an empty string is always returned.
+        *
+        * @since 1.20
+        * @return string HTML fragment
+        */
+       public static function getHTMLDebugLog() {
+               global $wgDebugTimestamps, $wgShowDebug;
+
+               if ( !$wgShowDebug ) {
+                       return '';
+               }
+
+               $curIdent = 0;
+               $ret = "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">\n<li>";
+
+               foreach ( self::$debug as $line ) {
+                       $pre = '';
+                       if ( $wgDebugTimestamps ) {
+                               $matches = array();
+                               if ( preg_match( '/^(\d+\.\d+ {1,3}\d+.\dM\s{2})/', $line, $matches ) ) {
+                                       $pre = $matches[1];
+                                       $line = substr( $line, strlen( $pre ) );
+                               }
+                       }
+                       $display = ltrim( $line );
+                       $ident = strlen( $line ) - strlen( $display );
+                       $diff = $ident - $curIdent;
+
+                       $display = $pre . $display;
+                       if ( $display == '' ) {
+                               $display = "\xc2\xa0";
+                       }
+
+                       if ( !$ident
+                               && $diff < 0
+                               && substr( $display, 0, 9 ) != 'Entering '
+                               && substr( $display, 0, 8 ) != 'Exiting '
+                       ) {
+                               $ident = $curIdent;
+                               $diff = 0;
+                               $display = '<span style="background:yellow;">' .
+                                       nl2br( htmlspecialchars( $display ) ) . '</span>';
+                       } else {
+                               $display = nl2br( htmlspecialchars( $display ) );
+                       }
+
+                       if ( $diff < 0 ) {
+                               $ret .= str_repeat( "</li></ul>\n", -$diff ) . "</li><li>\n";
+                       } elseif ( $diff == 0 ) {
+                               $ret .= "</li><li>\n";
+                       } else {
+                               $ret .= str_repeat( "<ul><li>\n", $diff );
+                       }
+                       $ret .= "<code>$display</code>\n";
+
+                       $curIdent = $ident;
+               }
+
+               $ret .= str_repeat( '</li></ul>', $curIdent ) . "</li>\n</ul>\n";
+
+               return $ret;
+       }
+
+       /**
+        * Append the debug info to given ApiResult
+        *
+        * @param IContextSource $context
+        * @param ApiResult $result
+        */
+       public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
+               if ( !self::$enabled ) {
+                       return;
+               }
+
+               // output errors as debug info, when display_errors is on
+               // this is necessary for all non html output of the api, because that clears all errors first
+               $obContents = ob_get_contents();
+               if ( $obContents ) {
+                       $obContentArray = explode( '<br />', $obContents );
+                       foreach ( $obContentArray as $obContent ) {
+                               if ( trim( $obContent ) ) {
+                                       self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
+                               }
+                       }
+               }
+
+               MWDebug::log( 'MWDebug output complete' );
+               $debugInfo = self::getDebugInfo( $context );
+
+               $result->setIndexedTagName( $debugInfo, 'debuginfo' );
+               $result->setIndexedTagName( $debugInfo['log'], 'line' );
+               $result->setIndexedTagName( $debugInfo['debugLog'], 'msg' );
+               $result->setIndexedTagName( $debugInfo['queries'], 'query' );
+               $result->setIndexedTagName( $debugInfo['includes'], 'queries' );
+               $result->setIndexedTagName( $debugInfo['profile'], 'function' );
+               $result->addValue( null, 'debuginfo', $debugInfo );
+       }
+
+       /**
+        * Returns the HTML to add to the page for the toolbar
+        *
+        * @param IContextSource $context
+        * @return array
+        */
+       public static function getDebugInfo( IContextSource $context ) {
+               if ( !self::$enabled ) {
+                       return array();
+               }
+
+               global $wgVersion, $wgRequestTime;
+               $request = $context->getRequest();
+
+               // HHVM's reported memory usage from memory_get_peak_usage()
+               // is not useful when passing false, but we continue passing
+               // false for consistency of historical data in zend.
+               // see: https://github.com/facebook/hhvm/issues/2257#issuecomment-39362246
+               $realMemoryUsage = wfIsHHVM();
+
+               return array(
+                       'mwVersion' => $wgVersion,
+                       'phpVersion' => PHP_VERSION,
+                       'gitRevision' => GitInfo::headSHA1(),
+                       'gitBranch' => GitInfo::currentBranch(),
+                       'gitViewUrl' => GitInfo::headViewUrl(),
+                       'time' => microtime( true ) - $wgRequestTime,
+                       'log' => self::$log,
+                       'debugLog' => self::$debug,
+                       'queries' => self::$query,
+                       'request' => array(
+                               'method' => $request->getMethod(),
+                               'url' => $request->getRequestURL(),
+                               'headers' => $request->getAllHeaders(),
+                               'params' => $request->getValues(),
+                       ),
+                       'memory' => $context->getLanguage()->formatSize( memory_get_usage( $realMemoryUsage ) ),
+                       'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage( $realMemoryUsage ) ),
+                       'includes' => self::getFilesIncluded( $context ),
+                       'profile' => Profiler::instance()->getRawData(),
+               );
+       }
+}
index 1103e38..e970e38 100644 (file)
@@ -149,6 +149,9 @@ abstract class File {
        /** @var string Required Repository class type */
        protected $repoClass = 'FileRepo';
 
+       /** @var array Cache of tmp filepaths pointing to generated bucket thumbnails, keyed by width */
+       protected $tmpBucketedThumbCache = array();
+
        /**
         * Call this constructor from child classes.
         *
@@ -456,6 +459,50 @@ abstract class File {
                return false;
        }
 
+       /**
+        * Return the smallest bucket from $wgThumbnailBuckets which is at least
+        * $wgThumbnailMinimumBucketDistance larger than $desiredWidth. The returned bucket, if any,
+        * will always be bigger than $desiredWidth.
+        *
+        * @param int $desiredWidth
+        * @param int $page
+        * @return bool|int
+        */
+       public function getThumbnailBucket( $desiredWidth, $page = 1 ) {
+               global $wgThumbnailBuckets, $wgThumbnailMinimumBucketDistance;
+
+               $imageWidth = $this->getWidth( $page );
+
+               if ( $imageWidth === false ) {
+                       return false;
+               }
+
+               if ( $desiredWidth > $imageWidth ) {
+                       return false;
+               }
+
+               if ( !$wgThumbnailBuckets ) {
+                       return false;
+               }
+
+               $sortedBuckets = $wgThumbnailBuckets;
+
+               sort( $sortedBuckets );
+
+               foreach ( $sortedBuckets as $bucket ) {
+                       if ( $bucket > $imageWidth ) {
+                               return false;
+                       }
+
+                       if ( $bucket - $wgThumbnailMinimumBucketDistance > $desiredWidth ) {
+                               return $bucket;
+                       }
+               }
+
+               // Image is bigger than any available bucket
+               return false;
+       }
+
        /**
         * Returns ID or name of user who uploaded the file
         * STUB
@@ -877,9 +924,9 @@ abstract class File {
                        return null;
                }
                $extension = $this->getExtension();
-               list( $thumbExt, ) = $this->handler->getThumbType(
+               list( $thumbExt, ) = $this->getHandler()->getThumbType(
                        $extension, $this->getMimeType(), $params );
-               $thumbName = $this->handler->makeParamString( $params ) . '-' . $name;
+               $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $name;
                if ( $thumbExt != $extension ) {
                        $thumbName .= ".$thumbExt";
                }
@@ -947,7 +994,7 @@ abstract class File {
         * @return MediaTransformOutput|bool False on failure
         */
        function transform( $params, $flags = 0 ) {
-               global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch;
+               global $wgThumbnailEpoch;
 
                wfProfileIn( __METHOD__ );
                do {
@@ -1004,64 +1051,221 @@ abstract class File {
                                } elseif ( $flags & self::RENDER_FORCE ) {
                                        wfDebug( __METHOD__ . " forcing rendering per flag File::RENDER_FORCE\n" );
                                }
-                       }
 
-                       // If the backend is ready-only, don't keep generating thumbnails
-                       // only to return transformation errors, just return the error now.
-                       if ( $this->repo->getReadOnlyReason() !== false ) {
-                               $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
-                               break;
+                               // If the backend is ready-only, don't keep generating thumbnails
+                               // only to return transformation errors, just return the error now.
+                               if ( $this->repo->getReadOnlyReason() !== false ) {
+                                       $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
+                                       break;
+                               }
                        }
 
-                       // Create a temp FS file with the same extension and the thumbnail
-                       $thumbExt = FileBackend::extensionFromPath( $thumbPath );
-                       $tmpFile = TempFSFile::factory( 'transform_', $thumbExt );
+                       $tmpFile = $this->makeTransformTmpFile( $thumbPath );
+
                        if ( !$tmpFile ) {
                                $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
-                               break;
+                       } else {
+                               $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
                        }
-                       $tmpThumbPath = $tmpFile->getPath(); // path of 0-byte temp file
-
-                       // Actually render the thumbnail...
-                       wfProfileIn( __METHOD__ . '-doTransform' );
-                       $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $params );
-                       wfProfileOut( __METHOD__ . '-doTransform' );
-                       $tmpFile->bind( $thumb ); // keep alive with $thumb
-
-                       if ( !$thumb ) { // bad params?
-                               $thumb = false;
-                       } elseif ( $thumb->isError() ) { // transform error
-                               $this->lastError = $thumb->toText();
-                               // Ignore errors if requested
-                               if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
-                                       $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $params );
-                               }
-                       } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
-                               // Copy the thumbnail from the file system into storage...
-                               $disposition = $this->getThumbDisposition( $thumbName );
-                               $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition );
-                               if ( $status->isOK() ) {
-                                       $thumb->setStoragePath( $thumbPath );
-                               } else {
-                                       $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
+               } while ( false );
+
+               wfProfileOut( __METHOD__ );
+
+               return is_object( $thumb ) ? $thumb : false;
+       }
+
+       /**
+        * Generates a thumbnail according to the given parameters and saves it to storage
+        * @param TempFSFile $tmpFile Temporary file where the rendered thumbnail will be saved
+        * @param array $transformParams
+        * @param int $flags
+        * @return bool|MediaTransformOutput
+        */
+       public function generateAndSaveThumb( $tmpFile, $transformParams, $flags ) {
+               global $wgUseSquid, $wgIgnoreImageErrors;
+
+               $handler = $this->getHandler();
+
+               $normalisedParams = $transformParams;
+               $handler->normaliseParams( $this, $normalisedParams );
+
+               $thumbName = $this->thumbName( $normalisedParams );
+               $thumbUrl = $this->getThumbUrl( $thumbName );
+               $thumbPath = $this->getThumbPath( $thumbName ); // final thumb path
+
+               $tmpThumbPath = $tmpFile->getPath();
+
+               if ( $handler->supportsBucketing() ) {
+                       $this->generateBucketsIfNeeded( $normalisedParams, $flags );
+               }
+
+               // Actually render the thumbnail...
+               wfProfileIn( __METHOD__ . '-doTransform' );
+               $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
+               wfProfileOut( __METHOD__ . '-doTransform' );
+               $tmpFile->bind( $thumb ); // keep alive with $thumb
+
+               if ( !$thumb ) { // bad params?
+                       $thumb = false;
+               } elseif ( $thumb->isError() ) { // transform error
+                       $this->lastError = $thumb->toText();
+                       // Ignore errors if requested
+                       if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
+                               $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
+                       }
+               } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
+                       // Copy the thumbnail from the file system into storage...
+                       $disposition = $this->getThumbDisposition( $thumbName );
+                       $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition );
+                       if ( $status->isOK() ) {
+                               $thumb->setStoragePath( $thumbPath );
+                       } else {
+                               $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $transformParams, $flags );
+                       }
+                       // Give extensions a chance to do something with this thumbnail...
+                       wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) );
+               }
+
+               // Purge. Useful in the event of Core -> Squid connection failure or squid
+               // purge collisions from elsewhere during failure. Don't keep triggering for
+               // "thumbs" which have the main image URL though (bug 13776)
+               if ( $wgUseSquid ) {
+                       if ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL() ) {
+                               SquidUpdate::purge( array( $thumbUrl ) );
+                       }
+               }
+
+               return $thumb;
+       }
+
+       /**
+        * Generates chained bucketed thumbnails if needed
+        * @param array $params
+        * @param int $flags
+        * @return bool Whether at least one bucket was generated
+        */
+       protected function generateBucketsIfNeeded( $params, $flags = 0 ) {
+               if ( !$this->repo
+                       || !isset( $params['physicalWidth'] )
+                       || !isset( $params['physicalHeight'] )
+                       || !( $bucket = $this->getThumbnailBucket( $params['physicalWidth'] ) )
+                       || $bucket == $params['physicalWidth'] ) {
+                       return false;
+               }
+
+               $bucketPath = $this->getBucketThumbPath( $bucket );
+
+               if ( $this->repo->fileExists( $bucketPath ) ) {
+                       return false;
+               }
+
+               $params['physicalWidth'] = $bucket;
+               $params['width'] = $bucket;
+
+               $params = $this->getHandler()->sanitizeParamsForBucketing( $params );
+
+               $bucketName = $this->getBucketThumbName( $bucket );
+
+               $tmpFile = $this->makeTransformTmpFile( $bucketPath );
+
+               if ( !$tmpFile ) {
+                       return false;
+               }
+
+               $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
+
+               if ( !$thumb || $thumb->isError() ) {
+                       return false;
+               }
+
+               $this->tmpBucketedThumbCache[ $bucket ] = $tmpFile->getPath();
+               // For the caching to work, we need to make the tmp file survive as long as
+               // this object exists
+               $tmpFile->bind( $this );
+
+               return true;
+       }
+
+       /**
+        * Returns the most appropriate source image for the thumbnail, given a target thumbnail size
+        * @param array $params
+        * @return array source path and width/height of the source
+        */
+       public function getThumbnailSource( $params ) {
+               if ( $this->repo
+                       && $this->getHandler()->supportsBucketing()
+                       && isset( $params['physicalWidth'] )
+                       && $bucket = $this->getThumbnailBucket( $params['physicalWidth'] )
+               ) {
+                       if ( $this->getWidth() != 0 ) {
+                               $bucketHeight = round( $this->getHeight() * ( $bucket / $this->getWidth() ) );
+                       } else {
+                               $bucketHeight = 0;
+                       }
+
+                       // Try to avoid reading from storage if the file was generated by this script
+                       if ( isset( $this->tmpBucketedThumbCache[ $bucket ] ) ) {
+                               $tmpPath = $this->tmpBucketedThumbCache[ $bucket ];
+
+                               if ( file_exists( $tmpPath ) ) {
+                                       return array(
+                                               'path' => $tmpPath,
+                                               'width' => $bucket,
+                                               'height' => $bucketHeight
+                                       );
                                }
-                               // Give extensions a chance to do something with this thumbnail...
-                               wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) );
                        }
 
-                       // Purge. Useful in the event of Core -> Squid connection failure or squid
-                       // purge collisions from elsewhere during failure. Don't keep triggering for
-                       // "thumbs" which have the main image URL though (bug 13776)
-                       if ( $wgUseSquid ) {
-                               if ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL() ) {
-                                       SquidUpdate::purge( array( $thumbUrl ) );
+                       $bucketPath = $this->getBucketThumbPath( $bucket );
+
+                       if ( $this->repo->fileExists( $bucketPath ) ) {
+                               $fsFile = $this->repo->getLocalReference( $bucketPath );
+
+                               if ( $fsFile ) {
+                                       return array(
+                                               'path' => $fsFile->getPath(),
+                                               'width' => $bucket,
+                                               'height' => $bucketHeight
+                                       );
                                }
                        }
-               } while ( false );
+               }
 
-               wfProfileOut( __METHOD__ );
+               // Original file
+               return array(
+                       'path' => $this->getLocalRefPath(),
+                       'width' => $this->getWidth(),
+                       'height' => $this->getHeight()
+               );
+       }
 
-               return is_object( $thumb ) ? $thumb : false;
+       /**
+        * Returns the repo path of the thumb for a given bucket
+        * @param int $bucket
+        * @return string
+        */
+       protected function getBucketThumbPath( $bucket ) {
+               $thumbName = $this->getBucketThumbName( $bucket );
+               return $this->getThumbPath( $thumbName );
+       }
+
+       /**
+        * Returns the name of the thumb for a given bucket
+        * @param int $bucket
+        * @return string
+        */
+       protected function getBucketThumbName( $bucket ) {
+               return $this->thumbName( array( 'physicalWidth' => $bucket ) );
+       }
+
+       /**
+        * Creates a temp FS file with the same extension and the thumbnail
+        * @param string $thumbPath Thumbnail path
+        * @returns TempFSFile
+        */
+       protected function makeTransformTmpFile( $thumbPath ) {
+               $thumbExt = FileBackend::extensionFromPath( $thumbPath );
+               return TempFSFile::factory( 'transform_', $thumbExt );
        }
 
        /**
@@ -1741,7 +1945,7 @@ abstract class File {
                        return false;
                }
 
-               return $this->handler->getImageSize( $this, $filePath );
+               return $this->getHandler()->getImageSize( $this, $filePath );
        }
 
        /**
index b57b69d..df6582c 100644 (file)
@@ -50,6 +50,7 @@
  *    'default'             -- default value when the form is displayed
  *    'id'                  -- HTML id attribute
  *    'cssclass'            -- CSS class
+ *    'csshelpclass'        -- CSS class used to style help text
  *    'options'             -- associative array mapping labels to values.
  *                             Some field types support multi-level arrays.
  *    'options-messages'    -- associative array mapping message keys to values.
@@ -171,6 +172,12 @@ class HTMLForm extends ContextSource {
 
        protected $mWrapperLegend = false;
 
+       /**
+        * Salt for the edit token.
+        * @var string|array
+        */
+       protected $mTokenSalt = '';
+
        /**
         * If true, sections that contain both fields and subsections will
         * render their subsections before their fields.
@@ -397,7 +404,7 @@ class HTMLForm extends ContextSource {
                                // Session tokens for logged-out users have no security value.
                                // However, if the user gave one, check it in order to give a nice
                                // "session expired" error instead of "permission denied" or such.
-                               $submit = $this->getUser()->matchEditToken( $editToken );
+                               $submit = $this->getUser()->matchEditToken( $editToken, $this->mTokenSalt );
                        } else {
                                $submit = true;
                        }
@@ -728,6 +735,21 @@ class HTMLForm extends ContextSource {
                return $this;
        }
 
+       /**
+        * Set the salt for the edit token.
+        *
+        * Only useful when the method is "post".
+        *
+        * @since 1.24
+        * @param string|array Salt to use
+        * @return HTMLForm $this for chaining calls
+        */
+       public function setTokenSalt( $salt ) {
+               $this->mTokenSalt = $salt;
+
+               return $this;
+       }
+
        /**
         * Display the form (sending to the context's OutputPage object), with an
         * appropriate error message or stack of messages, and any validation errors, etc.
@@ -823,7 +845,7 @@ class HTMLForm extends ContextSource {
                if ( $this->getMethod() == 'post' ) {
                        $html .= Html::hidden(
                                'wpEditToken',
-                               $this->getUser()->getEditToken(),
+                               $this->getUser()->getEditToken( $this->mTokenSalt ),
                                array( 'id' => 'wpEditToken' )
                        ) . "\n";
                        $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
index 8076e8a..1fcc866 100644 (file)
@@ -13,6 +13,7 @@ abstract class HTMLFormField {
        protected $mLabel; # String label.  Set on construction
        protected $mID;
        protected $mClass = '';
+       protected $mHelpClass = false;
        protected $mDefault;
        protected $mOptions = false;
        protected $mOptionsLabelsNotFromMessage = false;
@@ -397,6 +398,10 @@ abstract class HTMLFormField {
                        $this->mClass = $params['cssclass'];
                }
 
+               if ( isset( $params['csshelpclass'] ) ) {
+                       $this->mHelpClass = $params['csshelpclass'];
+               }
+
                if ( isset( $params['validation-callback'] ) ) {
                        $this->mValidationCallback = $params['validation-callback'];
                }
@@ -562,7 +567,11 @@ abstract class HTMLFormField {
                        $rowAttributes['class'] = 'mw-htmlform-hide-if';
                }
 
-               $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ), $helptext );
+               $tdClasses = array( 'htmlform-tip' );
+               if ( $this->mHelpClass !== false ) {
+                       $tdClasses[] = $this->mHelpClass;
+               }
+               $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => $tdClasses ), $helptext );
                $row = Html::rawElement( 'tr', $rowAttributes, $row );
 
                return $row;
index 1c7762b..c6939a8 100644 (file)
@@ -179,16 +179,11 @@ class CliInstaller extends Installer {
        public function showHelpBox( $msg /*, ... */ ) {
        }
 
+       /**
+        * @param Status $status
+        */
        public function showStatusMessage( Status $status ) {
-               $warnings = array_merge( $status->getWarningsArray(),
-                       $status->getErrorsArray() );
-
-               if ( count( $warnings ) !== 0 ) {
-                       foreach ( $warnings as $w ) {
-                               call_user_func_array( array( $this, 'showMessage' ), $w );
-                       }
-               }
-
+               parent::showStatusMessage( $status );
                if ( !$status->isOk() ) {
                        echo "\n";
                        exit( 1 );
index 540b647..7c67ee8 100644 (file)
@@ -38,9 +38,6 @@
  */
 abstract class Installer {
 
-       // This is the absolute minimum PHP version we can support
-       const MINIMUM_PHP_VERSION = '5.3.2';
-
        /**
         * The oldest version of PCRE we can support.
         *
@@ -111,6 +108,10 @@ abstract class Installer {
         * These may output warnings using showMessage(), and/or abort the
         * installation process by returning false.
         *
+        * For the WebInstaller these are only called on the Welcome page,
+        * if these methods have side-effects that should affect later page loads
+        * (as well as the generated stylesheet), use envPreps instead.
+        *
         * @var array
         */
        protected $envChecks = array(
@@ -131,7 +132,6 @@ abstract class Installer {
                'envCheckGit',
                'envCheckServer',
                'envCheckPath',
-               'envCheckExtension',
                'envCheckShellLocale',
                'envCheckUploadsDirectory',
                'envCheckLibicu',
@@ -140,6 +140,17 @@ abstract class Installer {
                'envCheckJSON',
        );
 
+       /**
+        * A list of environment preparation methods called by doEnvironmentPreps().
+        *
+        * @var array
+        */
+       protected $envPreps = array(
+               'envPrepExtension',
+               'envPrepServer',
+               'envPrepPath',
+       );
+
        /**
         * MediaWiki configuration globals that will eventually be passed through
         * to LocalSettings.php. The names only are given here, the defaults
@@ -340,10 +351,17 @@ abstract class Installer {
        abstract public function showError( $msg /*, ... */ );
 
        /**
-        * Show a message to the installing user by using a Status object
+        * Shows messages to the user through a Status object
         * @param Status $status
         */
-       abstract public function showStatusMessage( Status $status );
+       public function showStatusMessage( Status $status ) {
+               $errors = array_merge( $status->getErrorsArray(), $status->getWarningsArray() );
+               if ( $errors ) {
+                       foreach ( $errors as $error ) {
+                               call_user_func( 'showMessage', $error );
+                       }
+               }
+       }
 
        /**
         * Constructor, always call this from child classes.
@@ -380,26 +398,17 @@ abstract class Installer {
                        $this->settings[$var] = $GLOBALS[$var];
                }
 
-               $compiledDBs = array();
+               $this->doEnvironmentPreps();
+
+               $this->compiledDBs = array();
                foreach ( self::getDBTypes() as $type ) {
                        $installer = $this->getDBInstaller( $type );
 
                        if ( !$installer->isCompiled() ) {
                                continue;
                        }
-                       $compiledDBs[] = $type;
-
-                       $defaults = $installer->getGlobalDefaults();
-
-                       foreach ( $installer->getGlobalNames() as $var ) {
-                               if ( isset( $defaults[$var] ) ) {
-                                       $this->settings[$var] = $defaults[$var];
-                               } else {
-                                       $this->settings[$var] = $GLOBALS[$var];
-                               }
-                       }
+                       $this->compiledDBs[] = $type;
                }
-               $this->compiledDBs = $compiledDBs;
 
                $this->parserTitle = Title::newFromText( 'Installer' );
                $this->parserOptions = new ParserOptions; // language will be wrong :(
@@ -429,25 +438,17 @@ abstract class Installer {
         * @return Status
         */
        public function doEnvironmentChecks() {
-               $phpVersion = phpversion();
-               if ( version_compare( $phpVersion, self::MINIMUM_PHP_VERSION, '>=' ) ) {
-                       $this->showMessage( 'config-env-php', $phpVersion );
-                       $good = true;
-               } else {
-                       $this->showMessage( 'config-env-php-toolow', $phpVersion, self::MINIMUM_PHP_VERSION );
-                       $good = false;
-               }
+               // Php version has already been checked by entry scripts
+               // Show message here for information purposes
+               $this->showMessage( 'config-env-php', PHP_VERSION );
 
+               $good = true;
                // Must go here because an old version of PCRE can prevent other checks from completing
-               if ( $good ) {
-                       list( $pcreVersion ) = explode( ' ', PCRE_VERSION, 2 );
-                       if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
-                               $this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
-                               $good = false;
-                       }
-               }
-
-               if ( $good ) {
+               list( $pcreVersion ) = explode( ' ', PCRE_VERSION, 2 );
+               if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
+                       $this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
+                       $good = false;
+               } else {
                        foreach ( $this->envChecks as $check ) {
                                $status = $this->$check();
                                if ( $status === false ) {
@@ -461,6 +462,12 @@ abstract class Installer {
                return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
        }
 
+       public function doEnvironmentPreps() {
+               foreach ( $this->envPreps as $prep ) {
+                       $this->$prep();
+               }
+       }
+
        /**
         * Set a MW configuration variable, or internal installer configuration variable.
         *
@@ -726,11 +733,15 @@ abstract class Installer {
 
        /**
         * Environment check for register_globals.
+        * Prevent installation if enabled
         */
        protected function envCheckRegisterGlobals() {
                if ( wfIniGetBool( 'register_globals' ) ) {
-                       $this->showMessage( 'config-register-globals' );
+                       $this->showMessage( 'config-register-globals-error' );
+                       return false;
                }
+
+               return true;
        }
 
        /**
@@ -971,55 +982,29 @@ abstract class Installer {
        }
 
        /**
-        * Environment check for the server hostname.
+        * Environment check to inform user which server we've assumed.
+        *
+        * @return bool
         */
        protected function envCheckServer() {
                $server = $this->envGetDefaultServer();
                if ( $server !== null ) {
                        $this->showMessage( 'config-using-server', $server );
-                       $this->setVar( 'wgServer', $server );
                }
-
                return true;
        }
 
        /**
-        * Helper function to be called from envCheckServer()
-        * @return string
-        */
-       abstract protected function envGetDefaultServer();
-
-       /**
-        * Environment check for setting $IP and $wgScriptPath.
+        * Environment check to inform user which paths we've assumed.
+        *
         * @return bool
         */
        protected function envCheckPath() {
-               global $IP;
-               $IP = dirname( dirname( __DIR__ ) );
-               $this->setVar( 'IP', $IP );
-
                $this->showMessage(
                        'config-using-uri',
                        $this->getVar( 'wgServer' ),
                        $this->getVar( 'wgScriptPath' )
                );
-
-               return true;
-       }
-
-       /**
-        * Environment check for setting the preferred PHP file extension.
-        * @return bool
-        */
-       protected function envCheckExtension() {
-               // @todo FIXME: Detect this properly
-               if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
-                       $ext = 'php5';
-               } else {
-                       $ext = 'php';
-               }
-               $this->setVar( 'wgScriptExtension', ".$ext" );
-
                return true;
        }
 
@@ -1234,6 +1219,44 @@ abstract class Installer {
                return true;
        }
 
+       /**
+        * Environment prep for the server hostname.
+        */
+       protected function envPrepServer() {
+               $server = $this->envGetDefaultServer();
+               if ( $server !== null ) {
+                       $this->setVar( 'wgServer', $server );
+               }
+       }
+
+       /**
+        * Helper function to be called from envPrepServer()
+        * @return string
+        */
+       abstract protected function envGetDefaultServer();
+
+       /**
+        * Environment prep for setting the preferred PHP file extension.
+        */
+       protected function envPrepExtension() {
+               // @todo FIXME: Detect this properly
+               if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
+                       $ext = '.php5';
+               } else {
+                       $ext = '.php';
+               }
+               $this->setVar( 'wgScriptExtension', $ext );
+       }
+
+       /**
+        * Environment prep for setting $IP and $wgScriptPath.
+        */
+       protected function envPrepPath() {
+               global $IP;
+               $IP = dirname( dirname( __DIR__ ) );
+               $this->setVar( 'IP', $IP );
+       }
+
        /**
         * Get an array of likely places we can find executables. Check a bunch
         * of known Unix-like defaults, as well as the PATH environment variable
index 3c8a5b1..100dd10 100644 (file)
@@ -202,7 +202,6 @@ class LocalSettingsGenerator {
                        $locale = '';
                }
 
-               //$rightsUrl = $this->values['wgRightsUrl'] ? '' : '#'; // @todo FIXME: I'm unused!
                $hashedUploads = $this->safeMode ? '' : '#';
                $metaNamespace = '';
                if ( $this->values['wgMetaNamespace'] !== $this->values['wgSitename'] ) {
index 51db148..83681b6 100644 (file)
@@ -218,6 +218,7 @@ class MssqlInstaller extends DatabaseInstaller {
                                'password' => $password,
                                'dbname' => false,
                                'flags' => 0,
+                               'schema' => $this->getVar( 'wgDBmwschema' ),
                                'tablePrefix' => $this->getVar( 'wgDBprefix' ) ) );
                        $db->prepareStatements( false );
                        $db->scrollableCursor( false );
@@ -648,6 +649,14 @@ class MssqlInstaller extends DatabaseInstaller {
                return $status;
        }
 
+       public function getGlobalDefaults() {
+               // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
+               // the use of a schema, so we need to set it here
+               return array(
+                       'wgDBmwschema' => 'mediawiki',
+               );
+       }
+
        /**
         * Try to see if the login exists
         * @param string $user Username to check
index d590a70..4d86d11 100644 (file)
@@ -65,6 +65,8 @@ class MssqlUpdater extends DatabaseUpdater {
         * @return bool False if patch is skipped.
         */
        protected function updateConstraints( $constraintType, $table, $field ) {
+               global $wgDBname, $wgDBmwschema;
+
                if ( !$this->doTable( $table ) ) {
                        return true;
                }
index 9e25f47..89a6978 100644 (file)
@@ -152,17 +152,18 @@ class PostgresInstaller extends DatabaseInstaller {
         * @param string $user User name
         * @param string $password Password
         * @param string $dbName Database name
+        * @param string $schema Database schema
         * @return Status
         */
-       protected function openConnectionWithParams( $user, $password, $dbName ) {
+       protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
                $status = Status::newGood();
                try {
-                       $db = new DatabasePostgres(
-                               $this->getVar( 'wgDBserver' ),
-                               $user,
-                               $password,
-                               $dbName
-                       );
+                       $db = Database::factory( 'postgres', array(
+                               'host' => $this->getVar( 'wgDBserver' ),
+                               'user' => $user,
+                               'password' => $password,
+                               'dbname' => $dbName,
+                               'schema' => $schema ) );
                        $status->value = $db;
                } catch ( DBConnectionError $e ) {
                        $status->fatal( 'config-connection-error', $e->getMessage() );
@@ -230,7 +231,8 @@ class PostgresInstaller extends DatabaseInstaller {
                                return $this->openConnectionWithParams(
                                        $this->getVar( '_InstallUser' ),
                                        $this->getVar( '_InstallPassword' ),
-                                       $this->getVar( 'wgDBname' ) );
+                                       $this->getVar( 'wgDBname' ),
+                                       $this->getVar( 'wgDBmwschema' ) );
                        case 'create-tables':
                                $status = $this->openPgConnection( 'create-schema' );
                                if ( $status->isOK() ) {
@@ -260,11 +262,11 @@ class PostgresInstaller extends DatabaseInstaller {
                $status = Status::newGood();
                foreach ( $dbs as $db ) {
                        try {
-                               $conn = new DatabasePostgres(
-                                       $this->getVar( 'wgDBserver' ),
+                               $conn = $this->openConnectionWithParams(
                                        $user,
                                        $password,
-                                       $db );
+                                       $db,
+                                       $this->getVar( 'wgDBmwschema' ) );
                        } catch ( DBConnectionError $error ) {
                                $conn = false;
                                $status->fatal( 'config-pg-test-error', $db,
@@ -622,6 +624,14 @@ class PostgresInstaller extends DatabaseInstaller {
                return $status;
        }
 
+       public function getGlobalDefaults() {
+               // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
+               // the use of a schema, so we need to set it here
+               return array(
+                       'wgDBmwschema' => 'mediawiki',
+               );
+       }
+
        public function setupPLpgSQL() {
                // Connect as the install user, since it owns the database and so is
                // the user that needs to run "CREATE LANGAUGE"
index 46348f9..ea86231 100644 (file)
@@ -729,16 +729,6 @@ class WebInstaller extends Installer {
                $this->output->addHTML( $html );
        }
 
-       /**
-        * @param Status $status
-        */
-       public function showStatusMessage( Status $status ) {
-               $errors = array_merge( $status->getErrorsArray(), $status->getWarningsArray() );
-               foreach ( $errors as $error ) {
-                       call_user_func_array( array( $this, 'showMessage' ), $error );
-               }
-       }
-
        /**
         * Label a control by wrapping a config-input div around it and putting a
         * label before it.
@@ -1135,8 +1125,18 @@ class WebInstaller extends Installer {
                        $path = $_SERVER['SCRIPT_NAME'];
                }
                if ( $path !== false ) {
-                       $uri = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
-                       $this->setVar( 'wgScriptPath', $uri );
+                       $scriptPath = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
+                       $scriptExtension = $this->getVar( 'wgScriptExtension' );
+
+                       $this->setVar( 'wgScriptPath', "$scriptPath" );
+                       // Update variables set from Setup.php that are derived from wgScriptPath
+                       $this->setVar( 'wgScript', "$scriptPath/index$scriptExtension" );
+                       $this->setVar( 'wgLoadScript', "$scriptPath/load$scriptExtension" );
+                       $this->setVar( 'wgStylePath', "$scriptPath/skins" );
+                       $this->setVar( 'wgLocalStylePath', "$scriptPath/skins" );
+                       $this->setVar( 'wgExtensionAssetsPath', "$scriptPath/extensions" );
+                       $this->setVar( 'wgUploadPath', "$scriptPath/images" );
+
                } else {
                        $this->showError( 'config-no-uri' );
 
diff --git a/includes/installer/i18n/bto.json b/includes/installer/i18n/bto.json
new file mode 100644 (file)
index 0000000..c1960fb
--- /dev/null
@@ -0,0 +1,17 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Filipinayzd"
+               ]
+       },
+       "config-information": "Impormasyon",
+       "config-page-welcome": "Dagos sa MediaWiki!",
+       "config-page-name": "Ngaran",
+       "config-restart": "Amo, uliton adi",
+       "config-profile-wiki": "Bukas na wiki",
+       "config-profile-private": "Pribadong wiki",
+       "config-logo": "URL ko logo:",
+       "config-cc-again": "Pumili dayday...",
+       "config-install-step-done": "tapus na",
+       "config-help": "tabang"
+}
index 8d678e0..3870ef9 100644 (file)
@@ -4,7 +4,8 @@
                        "Pitort",
                        "පසිඳු කාවින්ද",
                        "Kippelboy",
-                       "Toniher"
+                       "Toniher",
+                       "Fitoschido"
                ]
        },
        "config-desc": "L'instal·lador del MediaWiki",
@@ -45,7 +46,6 @@
        "config-env-good": "S'ha comprovat l'entorn.\nPodeu instal·lar el MediaWiki.",
        "config-env-bad": "S'ha comprovat l'entorn.\nNo podeu instal·lar el MediaWiki.",
        "config-env-php": "El PHP $1 està instal·lat.",
-       "config-env-php-toolow": "El PHP $1 està instal·lat.\nMalauradament, el MediaWiki necessita el PHP $2 o superior.",
        "config-memory-raised": "El <code>memory_limit</code> del PHP és $1 i s'ha aixecat a $2.",
        "config-memory-bad": "<strong>Avís:</strong> El <code>memory_limit</code> del PHP és $1.\nAixò és probablement massa baix.\nLa instal·lació pot fallar!",
        "config-diff3-bad": "No s'ha trobat el GNU diff3.",
@@ -73,6 +73,7 @@
        "config-db-charset": "Joc de caràcters de la base de dades",
        "config-charset-mysql5-binary": "Binari de MySQL 4.1/5.0",
        "config-charset-mysql5": "MySQL 4.1/5.0 UTF-8",
+       "config-mysql-old": "Cal el MySQL $1 o posterior. Teniu el $2.",
        "config-db-port": "Port de la base de dades:",
        "config-db-schema": "Esquema per a MediaWiki:",
        "config-db-schema-help": "Aquest esquema normalment ja serveix.\nNomés canvieu-lo si sabeu què us feu.",
        "config-type-mysql": "MySQL (o compatible)",
        "config-type-mssql": "Microsoft SQL Server",
        "config-header-mysql": "Paràmetres de MySQL",
+       "config-header-postgres": "Paràmetres del PostgreSQL",
+       "config-header-sqlite": "Paràmetres de l'SQLite",
+       "config-header-oracle": "Paràmetres de l'Oracle",
+       "config-header-mssql": "Paràmetres del Microsoft SQL Server",
        "config-invalid-db-type": "Tipus de base de dades no vàlid",
-       "config-missing-db-name": "Heu d'introduir un valor per al «nom de la base de dades»",
-       "config-missing-db-host": "Heu d'introduir un valor per al «servidor de la base de dades»",
+       "config-missing-db-name": "Heu d'introduir un valor per a «{{int:config-db-name}}».",
+       "config-missing-db-host": "Heu d'introduir un valor per a «{{int:config-db-host}}».",
+       "config-missing-db-server-oracle": "Heu d’introduir un valor per a «{{int:config-db-host-oracle}}».",
        "config-sqlite-readonly": "El fitxer <code>$1</code> no es pot escriure.",
        "config-sqlite-cant-create-db": "No s'ha pogut crear el fitxer de base de dades <code>$1</code>.",
        "config-upgrade-done-no-regenerate": "S'ha completat l'actualització.\n\nJa podeu [$1 començar a utilitzar el wiki].",
        "config-install-mainpage-failed": "No s'ha pogut inserir la pàgina principal: $1",
        "config-download-localsettings": "Baixa <code>LocalSettings.php</code>",
        "config-help": "ajuda",
+       "config-help-tooltip": "feu clic per ampliar",
        "config-nofile": "No s'ha pogut trobar el fitxer «$1». S'ha suprimit?",
        "mainpagetext": "'''El MediaWiki s'ha instal·lat correctament.'''",
        "mainpagedocfooter": "Consulteu la [//meta.wikimedia.org/wiki/Help:Contents Guia d'Usuari] per a més informació sobre com utilitzar-lo.\n\n== Per a començar ==\n\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Llista de característiques configurables]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF del MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de correu (''listserv'') per a anuncis del MediaWiki]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traduïu MediaWiki en la vostra llengua]"
index dbfef20..307faf3 100644 (file)
@@ -52,7 +52,6 @@
        "config-env-good": "Prostředí bylo zkontrolováno.\nMůžete nainstalovat MediaWiki.",
        "config-env-bad": "Prostředí bylo zkontrolováno.\nMediaWiki nelze nainstalovat.",
        "config-env-php": "Je nainstalováno PHP $1.",
-       "config-env-php-toolow": "Je nainstalováno PHP $1.\nMediaWiki ale vyžaduje PHP $2 nebo vyšší.",
        "config-unicode-using-utf8": "Pro normalizaci Unicode se používá utf8_normalize.so Briona Vibbera.",
        "config-unicode-using-intl": "Pro normalizaci Unicode se používá [http://pecl.php.net/intl PECL rozšíření intl].",
        "config-unicode-pure-php-warning": "'''Upozornění''': Není dostupné [http://pecl.php.net/intl PECL rozšíření intl] pro normalizaci Unicode, bude se využívat pomalá implementace v čistém PHP.\nPokud provozujete wiki s velkou návštěvností, měli byste si přečíst něco o [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizaci Unicode].",
@@ -60,7 +59,7 @@
        "config-no-db": "Nepodařilo se nalézt vhodný databázový ovladač! Musíte do PHP nainstalovat databázový ovladač.\nJsou podporovány následující typy databází: $1.\n\nPokud jste si PHP přeložili sami, překonfigurujte ho se zapnutým databázovým klientem, například pomocí <code>./configure --with-mysql</code>.\nPokud jste PHP nainstalovali z balíčku Debian či Ubuntu, potřebujete nainstalovat také modul php5-mysql.",
        "config-outdated-sqlite": "'''Upozornění''': Máte SQLite $1, které je starší než minimálně vyžadovaná verze $2. SQLite nebude dostupné.",
        "config-no-fts3": "'''Upozornění''': SQLite bylo přeloženo bez [//sqlite.org/fts3.html modulu FTS3], funkce pro vyhledávání zde nebudou dostupné.",
-       "config-register-globals": "'''Upozornění: Je zapnuta PHP volba <code>[http://php.net/register_globals register_globals]</code>.'''\n'''Pokud můžete, vypněte ji.'''\nMediaWiki bude fungovat, ale váš server je vystaven potenciálním bezpečnostním hrozbám.",
+       "config-register-globals-error": "<strong>Chyba: PHP nastavení <code>[http://php.net/register_globals register_globals]</code> je zapnuto. Pro pokračování v instalaci musí být vypnuto.</strong>\nRady, jak toho dosáhnout, najdete na [https://www.mediawiki.org/wiki/Register_globals https://www.mediawiki.org/wiki/register_globals].",
        "config-magic-quotes-runtime": "'''Kritická chyba: Je zapnuto [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime]!'''\nToto nastavení nepředvídatelně poškozuje vstupní data.\nMediaWiki nelze nainstalovat ani používat, dokud není toto nastavení vypnuto.",
        "config-magic-quotes-sybase": "'''Kritická chyba: Je zapnuto [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase]!'''\nToto nastavení nepředvídatelně poškozuje vstupní data.\nMediaWiki nelze nainstalovat ani používat, dokud není toto nastavení vypnuto.",
        "config-mbstring": "'''Kritická chyba: Je zapnuto [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]!'''\nToto nastavení způsobuje chyby a může nepředvídatelně poškozovat vstupní data.\nMediaWiki nelze nainstalovat ani používat, dokud není toto nastavení vypnuto.",
index cd18c92..f172355 100644 (file)
@@ -58,7 +58,6 @@
        "config-env-good": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann installiert werden.",
        "config-env-bad": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann nicht installiert werden.",
        "config-env-php": "Die Skriptsprache „PHP“ ($1) ist installiert.",
-       "config-env-php-toolow": "PHP $1 ist installiert.\nAllerdings benötigt MediaWiki PHP $2 oder höher.",
        "config-unicode-using-utf8": "Zur Unicode-Normalisierung wird Brion Vibbers <code>utf8_normalize.so</code> eingesetzt.",
        "config-unicode-using-intl": "Zur  Unicode-Normalisierung wird die [http://pecl.php.net/intl PECL-Erweiterung intl] eingesetzt.",
        "config-unicode-pure-php-warning": "'''Warnung:''' Die [http://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit großer Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Webseite [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] gelesen werden.",
@@ -66,7 +65,7 @@
        "config-no-db": "Es konnte kein adäquater Datenbanktreiber gefunden werden. Es muss daher ein Datenbanktreiber für PHP installiert werden.\nDie folgenden Datenbanksysteme werden unterstützt: $1\n\nWenn du PHP selbst kompiliert hast, konfiguriere es erneut mit einem aktivierten Datenbankclient, zum Beispiel durch Verwendung von <code>./configure --with-mysqli</code>.\nWenn du PHP von einem Debian- oder Ubuntu-Paket installiert hast, dann musst du auch beispielsweise das <code>php5-mysql</code>-Paket installieren.",
        "config-outdated-sqlite": "'''Warnung:''' SQLite $1 ist installiert. Allerdings benötigt MediaWiki SQLite $2 oder höher. SQLite wird daher nicht verfügbar sein.",
        "config-no-fts3": "'''Warnung:''' SQLite wurde ohne das [//sqlite.org/fts3.html FTS3-Modul] kompiliert, sodass keine Suchfunktionen für dieses Datenbanksystem zur Verfügung stehen werden.",
-       "config-register-globals": "'''Warnung: Der Parameter <code>[http://php.net/register_globals register_globals]</code> von PHP ist aktiviert.'''\n'''Sie sollte deaktiviert werden, sofern dies möglich ist.'''\nDie MediaWiki-Installation wird zwar laufen, wobei aber der Server für potentielle Sicherheitsprobleme anfällig ist.",
+       "config-register-globals-error": "<strong>Fehler: Die PHP-Option <code>[http://php.net/register_globals register_globals]</code> ist aktiviert.\nSie muss deaktiviert sein, um mit der Installation fortzufahren.</strong>\nSiehe [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] für Hilfe.",
        "config-magic-quotes-runtime": "'''Fataler Fehler: Der Parameter <code>[http://www.php.net/manual/de/function.set-magic-quotes-runtime.php set_magic_quotes_runtime]</code> von PHP ist aktiviert!'''\nDiese Einstellung führt zu unvorhersehbaren Problemen bei der Dateneingabe.\nMediaWiki kann nicht installiert werden, solange dieser Parameter nicht deaktiviert wurde.",
        "config-magic-quotes-sybase": "<strong>Fataler Fehler: Der Parameter <code>[http://www.php.net/manual/de/sybase.configuration.php#ini.magic-quotes-sybase magic_quotes_sybase]</code> von PHP ist aktiviert!</strong>\nDiese Einstellung führt zu unvorhersehbaren Problemen bei der Dateneingabe.\nMediaWiki kann nicht installiert werden, solange dieser Parameter nicht deaktiviert wurde.",
        "config-mbstring": "'''Fataler Fehler: Der Parameter <code>[http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]</code> von PHP ist aktiviert!'''\nDiese Einstellung verursacht Fehler und führt zu unvorhersehbaren Problemen bei der Dateneingabe.\nMediaWiki kann nicht installiert werden, solange dieser Parameter nicht deaktiviert wurde.",
index b19bdc0..e0cd1bf 100644 (file)
@@ -44,7 +44,6 @@
        "config-env-good": "The environment has been checked.\nYou can install MediaWiki.",
        "config-env-bad": "The environment has been checked.\nYou cannot install MediaWiki.",
        "config-env-php": "PHP $1 is installed.",
-       "config-env-php-toolow": "PHP $1 is installed.\nHowever, MediaWiki requires PHP $2 or higher.",
        "config-unicode-using-utf8": "Using Brion Vibber's utf8_normalize.so for Unicode normalization.",
        "config-unicode-using-intl": "Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalization.",
        "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
@@ -52,7 +51,7 @@
        "config-no-db": "Could not find a suitable database driver! You need to install a database driver for PHP.\nThe following database types are supported: $1.\n\nIf you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.\nIf you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php5-mysql</code> package.",
        "config-outdated-sqlite": "<strong>Warning:</strong> you have SQLite $1, which is lower than minimum required version $2. SQLite will be unavailable.",
        "config-no-fts3": "<strong>Warning:</strong> SQLite is compiled without the [//sqlite.org/fts3.html FTS3 module], search features will be unavailable on this backend.",
-       "config-register-globals": "<strong>Warning: PHP's <code>[http://php.net/register_globals register_globals]</code> option is enabled.\nDisable it if you can.</strong>\nMediaWiki will work, but your server is exposed to potential security vulnerabilities.",
+       "config-register-globals-error": "<strong>Error: PHP's <code>[http://php.net/register_globals register_globals]</code> option is enabled.\nIt must be disabled to continue with installation.</strong>\nSee [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] for help on how to do so.",
        "config-magic-quotes-runtime": "<strong>Fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] is active!'</strong>\nThis option corrupts data input unpredictably.\nYou cannot install or use MediaWiki unless this option is disabled.",
        "config-magic-quotes-sybase": "<strong>Fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] is active!</strong>\nThis option corrupts data input unpredictably.\nYou cannot install or use MediaWiki unless this option is disabled.",
        "config-mbstring": "<strong>Fatal: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] is active!</strong>\nThis option causes errors and may corrupt data unpredictably.\nYou cannot install or use MediaWiki unless this option is disabled.",
index 71da7f2..250816a 100644 (file)
        "config-env-good": "La medio estis kontrolita.\nVi povas instali MediaWiki.",
        "config-env-bad": "La medio estis kontrolita.\nNe eblas instali MediaWiki.",
        "config-env-php": "PHP $1 estas instalita.",
-       "config-env-php-toolow": "PHP $1 estas instalita.\nTamen, MediaWiki bezonas PHP $2 aŭ pli novan.",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] estas instalita.",
        "config-apc": "[http://www.php.net/apc APC] estas instalita",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] estas instalita",
        "config-diff3-bad": "GNU diff3 ne estis trovita.",
        "config-db-type": "Tipo de datumbazo:",
+       "config-db-wiki-settings": "Identigu ĉi tiun vikion",
+       "config-db-name": "Nomo de datumbazo:",
        "config-charset-mysql5": "MySQL 4.1/5.0 UTF-8",
        "config-type-mysql": "MySQL (aŭ kongrua)",
+       "config-admin-password": "Pasvorto:",
+       "config-admin-password-confirm": "Retajpu pasvorton:",
+       "config-admin-name-blank": "Enigu salutnomon de administranto.",
+       "config-admin-email": "Retpoŝtadreso:",
        "mainpagetext": "'''MediaWiki estis sukcese instalita.'''",
        "mainpagedocfooter": "Konsultu la [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Gvidilon por uzantoj de MediaWiki] por informoj pri uzado de vikia programaro.\n\n==Kiel komenci==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Listo de konfiguraĵoj] (angle)\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki Oftaj Demandoj] (angle)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Anonco-dissendolisto pri MediaWiki] (angle)\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Preklad MediaWiki do tvojho jazyka]"
 }
index b37a1b7..99555cb 100644 (file)
@@ -67,7 +67,6 @@
        "config-env-good": "El entorno ha sido comprobado.\nPuedes instalar MediaWiki.",
        "config-env-bad": "El entorno ha sido comprobado.\nNo puedes instalar MediaWiki.",
        "config-env-php": "PHP $1 está instalado.",
-       "config-env-php-toolow": "PHP $1 está instalado.\nSin embargo, MediaWiki requiere PHP $2 o superior.",
        "config-unicode-using-utf8": "Usando utf8_normalize.so de Brion Vibber para la normalización Unicode.",
        "config-unicode-using-intl": "Usando la [http://pecl.php.net/intl extensión intl PECL] para la normalización Unicode.",
        "config-unicode-pure-php-warning": "'''Advertencia''': La [http://pecl.php.net/intl extensión intl] no está disponible para efectuar la normalización Unicode. Utilizando la implementación más lenta en PHP.\nSi tu web tiene mucho tráfico, te recomendamos leer acerca de la [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
@@ -75,7 +74,6 @@
        "config-no-db": "¡No fue posible encontrar un controlador adecuado para la base de datos! Necesitas instalar un controlador de base de datos para PHP.\nLos siguientes sistemas gestores de bases de datos están soportados: $1.\n\nSi compilaste PHP tú mismo, debes reconfigurarlo habilitando un cliente de base de datos, por ejemplo, usando <code>./configure --with-mysqli</code>.\nSi instalaste PHP desde un paquete Debian o Ubuntu, entonces también necesitas instalar, por ejemplo, el paquete <code>php5-mysql</code>.",
        "config-outdated-sqlite": "''' Advertencia ''': tiene la versión SQLite $1, que es inferior a la mínima versión requerida: $2 . SQLite no estará disponible.",
        "config-no-fts3": "'''Advertencia''': SQLite está compilado sin el [//sqlite.org/fts3.html módulo FTS3]. Las funcionalidades de búsqueda no estarán disponibles en esta instalación.",
-       "config-register-globals": "'''Advertencia: La opción de <code>[http://php.net/register_globals register_globals]</code> de PHP está habilitada.'''\n'''Desactívela si puede.'''\nMediaWiki funcionará, pero tu servidor quedará expuesto a vulnerabilidades de seguridad potenciales.",
        "config-magic-quotes-runtime": "'''Fatal: ¡[http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] está activada!'''\nEsta opción causa la imprevisible corrupción de la entrada de datos.\nNo puedes instalar o utilizar MediaWiki a menos que esta opción esté inhabilitada.",
        "config-magic-quotes-sybase": "'''Fatal: ¡[http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] está activada!'''\nEsta opción causa la imprevisible corrupción de la entrada de datos.\nNo puedes instalar o utilizar MediaWiki a menos que esta opción esté inhabilitada.",
        "config-mbstring": "'''Fatal: La opción [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] está activada!'''\nEsta opción causa errores y puede corromper los datos de una forma imprevisible.\nNo se puede instalar o usar MediaWiki a menos que esta opción sea desactivada.",
index 23fe457..ccebf08 100644 (file)
@@ -51,7 +51,6 @@
        "config-env-good": "محیط بررسی شده‌است.\nشما می‌توانید مدیاویکی را نصب کنید.",
        "config-env-bad": "محیط بررسی شده‌است.\nشما نمی‌توانید مدیاویکی را نصب کنید.",
        "config-env-php": "پی‌اچ‌پی $1 نصب شده‌است.",
-       "config-env-php-toolow": "پی‌اچ‌پی $1 نصب شده است.\nدر هر صورت، مدیاویکی نیاز به پی‌اچ‌پی نسخهٔ $2 یا بالاتر دارد.",
        "config-unicode-using-utf8": "برای یونیکد عادی از Brion Vibber's utf8_normalize.so استفاده کنید.",
        "config-unicode-using-intl": "برای یونیکد عادی از [http://pecl.php.net/intl intl PECL extension] استفاده کنید.",
        "config-unicode-pure-php-warning": "'''هشدار:''' [http://pecl.php.net/intl intl PECL extension] برای کنترل یونیکد عادی در دسترس نیست،اجرای کاملاً آهسته به تعویق می‌افتد.\nاگر شما یک سایت پر‌ ترافیک را اجرا می‌کنید، باید کمی [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization] را بخوانید.",
@@ -59,7 +58,7 @@
        "config-no-db": "درایور پایگاه اطلاعاتی مناسب پیدا نشد! شما لازم دارید یک درایور پایگاه اطلاعاتی  برای پی‌اچ‌پی نصب کنید.انواع پایگاه اطلاعاتی زیر پشتیبانی شده‌اند:$1.\nاگر شما در گروه اشتراک‌گذاری هستید، از تهیه کنندهٔ گروه خود برای نصب یک درایور پایگاه اطلاعاتی مناسب سوأل کنید.\nاگر خود، پی‌اچ‌پی را تهیه کرده‌اید، با یک پردازشگر فعال دوباره پیکربندی کنید، برای مثال از <code>./configure --with-mysql</code> استفاده کنید.\nاگر پی‌اچ‌پی را از یک بستهٔ دبیان یا آبونتو نصب کرده‌اید، بنابراین لازم دارید بخش php5-mysql را نصب کنید.",
        "config-outdated-sqlite": "''' هشدار:''' شما اس‌کیولایت $1 دارید، که پایین‌تر از حداقل نسخهٔ $2 مورد نیاز است.اس‌کیولایت در دسترس نخواهد بود.",
        "config-no-fts3": "'''هشدار:''' اس‌کیولایت بدون [//sqlite.org/fts3.html FTS3 module] تهیه شده‌است ، جستجوی ویژگی‌ها در این بخش پیشین در دسترس نخواهد‌بود.",
-       "config-register-globals": "'''هشدار:''' گزینهٔ  PHP's <code>[http://php.net/register_globals register_globals]</code> فعال شده‌است.'''\n''' اگر می‌توانید غیر فعالش کنید.'''\nمدیاویکی کار خواهد‌کرد، اما سرور شما در معرض آسیب‌پذیری‌های امنیتی ممکن قرار دارد.",
+       "config-register-globals-error": "<strong>خطا:  پی‌اچ‌پی<code>[http://php.net/register_globals register_globals]</code> گزینه فعال است.\nبرای ادامه نصب باید غیر فعال باشد.</strong>\n[Https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] را برای کمک در مورد نحوه انجام این کار ببینید.",
        "config-magic-quotes-runtime": "'''مخرب: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] فعال است.\nاین گزینه اطلاعات داده شده به رایانه را به طور غیر‌قابل پیش‌بینی از بین می‌برد.\nشما نمی‌توانید مدیاویکی را نصب یا استفاده کنید مگر اینکه این گزینه غیر‌فعال باشد.",
        "config-magic-quotes-sybase": "'''مخرب: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] فعال است.\nاین گزینه اطلاعات داده شده به رایانه را به طور غیر‌قابل پیش‌بینی از بین می‌برد.\nشما نمی‌توانید مدیاویکی را نصب یا استفاده کنید مگر اینکه این گزینه غیر‌فعال باشد.",
        "config-mbstring": "''' مخرب:[http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] فعال است.\nاین گزینه باعث ایجاد خطا می‌شود و ممکن است اطلاعات را به طور غیر‌قابل پیش‌بینی از بین ببرد.\nشما نمی‌توانید مدیاویکی را نصب یا استفاده کنید مگر اینکه این گزینه غیر‌فعال باشد.",
index d9ef1ef..42e5e77 100644 (file)
@@ -66,7 +66,6 @@
        "config-env-good": "L’environnement a été vérifié.\nVous pouvez installer MediaWiki.",
        "config-env-bad": "L’environnement a été vérifié.\nVous ne pouvez pas installer MediaWiki.",
        "config-env-php": "PHP $1 est installé.",
-       "config-env-php-toolow": "PHP $1 est installé.\nCependant, MediaWiki requiert PHP $2 ou supérieur.",
        "config-unicode-using-utf8": "Utilisation de utf8_normalize.so par Brion Vibber pour la normalisation Unicode.",
        "config-unicode-using-intl": "Utilisation de [http://pecl.php.net/intl l'extension PECL intl] pour la normalisation Unicode.",
        "config-unicode-pure-php-warning": "<strong>Attention</strong> : L'[http://pecl.php.net/intl extension PECL intl] n'est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
@@ -74,7 +73,7 @@
        "config-no-db": "Impossible de trouver un pilote de base de données approprié ! Vous devez installer un pilote de base de données pour PHP. Les types de bases de données suivants sont reconnus : $1.\n\nSi vous avez compilé PHP vous-même, reconfigurez-le avec un client de base de données activé, par exemple en utilisant <code>./configure --with-mysqli</code>. Si vous avez installé PHP depuis un paquet Debian ou Ubuntu, alors vous devrez aussi installer, par exemple, le paquet <code>php5-mysql</code>.",
        "config-outdated-sqlite": "'''Attention''': vous avez SQLite $1, qui est inférieur à la version minimale requise $2. SQLite sera indisponible.",
        "config-no-fts3": "'''Attention :''' SQLite est compilé sans le module [//sqlite.org/fts3.html FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.",
-       "config-register-globals": "'''Attention : l'option <code>[http://php.net/register_globals register_globals]</code> de PHP est activée.'''\n'''Désactivez-la si vous le pouvez.'''\nMediaWiki fonctionnera, mais votre serveur sera exposé à de potentielles failles de sécurité.",
+       "config-register-globals-error": "<strong>Erreur : L’option <code>[http://php.net/register_globals register_globals]</code> de PHP est activée.\nElle doit être désactivée pour poursuivre l’installation.</strong>\nVoyez [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] pour avoir de l’aide sur la manière de faire cela.",
        "config-magic-quotes-runtime": "'''Erreur fatale : [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] est activé !'''\nCette option corrompt les données de manière imprévisible.\nVous ne pouvez pas installer ou utiliser MediaWiki tant que cette option est activée.",
        "config-magic-quotes-sybase": "'''Erreur fatale : [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybasee] est activé !'''\nCette option corrompt les données de manière imprévisible.\nVous ne pouvez pas installer ou utiliser MediaWiki tant que cette option est activée.",
        "config-mbstring": "'''Erreur fatale : [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] est activé !'''\nCette option provoque des erreurs et peut corrompre les données de manière imprévisible.\nVous ne pouvez pas installer ou utiliser MediaWiki tant que cette option est activée.",
index 6d5861b..b809a4c 100644 (file)
@@ -49,7 +49,6 @@
        "config-env-good": "Rematou a comprobación da contorna.\nPode instalar MediaWiki.",
        "config-env-bad": "Rematou a comprobación da contorna.\nNon pode instalar MediaWiki.",
        "config-env-php": "Está instalado o PHP $1.",
-       "config-env-php-toolow": "Está instalado o PHP $1.\nPorén, MediaWiki necesita o PHP $2 ou superior.",
        "config-unicode-using-utf8": "Usando utf8_normalize.so de Brion Vibber para a normalización Unicode.",
        "config-unicode-using-intl": "Usando a [http://pecl.php.net/intl extensión intl PECL] para a normalización Unicode.",
        "config-unicode-pure-php-warning": "'''Atención:''' A [http://pecl.php.net/intl extensión intl PECL] non está dispoñible para manexar a normalización Unicode; volvendo á implementación lenta de PHP puro.\nSe o seu sitio posúe un alto tráfico de visitantes, debería ler un chisco sobre a [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
@@ -57,7 +56,7 @@
        "config-no-db": "Non se puido atopar un controlador axeitado para a base de datos! Necesita instalar un controlador de base de datos para PHP.\nOs tipos de base de datos admitidos son os seguintes: $1.\n\nSe compilou o PHP vostede mesmo, reconfigúreo activando un cliente de base de datos, por exemplo, usando <code>./configure --with-mysql</code>.\nSe instalou o PHP desde un paquete Debian ou Ubuntu, entón tamén necesita instalar, por exemplo, o módulo <code>php5-mysql</code>.",
        "config-outdated-sqlite": "'''Atención:''' Ten o SQLite $1, que é inferior á versión mínima necesaria: $2. O SQLite non estará dispoñible.",
        "config-no-fts3": "'''Atención:''' O SQLite está compilado sen o [//sqlite.org/fts3.html módulo FTS3]; as características de procura non estarán dispoñibles nesta instalación.",
-       "config-register-globals": "'''Atención: A opción PHP <code>[http://php.net/register_globals register_globals]</code> está activada.'''\n'''Desactívea se pode.'''\nMediaWiki funcionará, pero o seu servidor está exposto a potenciais vulnerabilidades de seguridade.",
+       "config-register-globals-error": "<strong>Erro: A opción <code>[http://php.net/register_globals register_globals]</code> do PHP está activada.\nCómpre desactivala para continuar a instalación.</strong>\nConsulte o enderezo [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] para obter axuda sobre como facelo.",
        "config-magic-quotes-runtime": "'''Erro fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] está activado!'''\nEsta opción corrompe os datos de entrada de xeito imprevisible.\nNon pode instalar ou empregar MediaWiki a menos que esta opción estea desactivada.",
        "config-magic-quotes-sybase": "'''Erro fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] está activado!'''\nEsta opción corrompe os datos de entrada de xeito imprevisible.\nNon pode instalar ou empregar MediaWiki a menos que esta opción estea desactivada.",
        "config-mbstring": "'''Erro fatal: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] está activado!'''\nEsta opción causa erros e pode corromper os datos de xeito imprevisible.\nNon pode instalar ou empregar MediaWiki a menos que esta opción estea desactivada.",
index 5d4ba21..acede93 100644 (file)
@@ -51,7 +51,6 @@
        "config-env-good": "הסביבה שלכם נבדקה.\nאפשר להתקין מדיה־ויקי.",
        "config-env-bad": "הסביבה שלכם נבדקה.\nאי־אפשר להתקין מדיה־ויקי.",
        "config-env-php": "מותקנת <span dir=\"ltr\">PHP $1</span>.",
-       "config-env-php-toolow": "מותקנת <span dir=\"ltr\">PHP $1</span>.\nלמדיה־ויקי נדרשת <span dir=\"ltr\">PHP $2</span> או גרסה גבוהה יותר.",
        "config-unicode-using-utf8": "משתמש ב־utf8_normalize.so של בריון ויבר לנרמול יוניקוד.",
        "config-unicode-using-intl": "משתמש ב[http://pecl.php.net/intl הרחבת intl PECL] לנרמול יוניקוד.",
        "config-unicode-pure-php-warning": "'''אזהרה''': [http://pecl.php.net/intl הרחבת intl PECL] אינה זמינה לטיפול בנרמול יוניקוד. משתמש ביישום PHP טהור ואטי יותר.\nאם זהו אתר בעל תעבורה גבוהה, כדאי לקרוא את המסמך הבא: [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
@@ -59,7 +58,7 @@
        "config-no-db": "לא נמצא דרייבר מסד נתונים מתאים. יש להתקין דרייבר מסד נתונים ל־PHP.\nנתמכים הסוגים הבאים של מסדי נתונים: $1.\n\nאם קִמפלת את PHP בעצמך, יש להגדיר אותו מחדש ולהפעיל את לקוח מסד נתונים, למשל באמצעות <code dir=\"ltr\">./configure --with-mysqli</code>.\nאם התקנת את PHP מחבילה של דביאן או של אובונטו, יש להתקין, למשל, גם את המודול <code dir=\"ltr\">php5-mysql</code>.",
        "config-outdated-sqlite": "'''אזהרה''': במערכת מתוקן SQLite $1. גרסה זו לא נתמכת ולשימוש ב־SQLite נדרשת גרסה $2 לפחות. SQLlite לא יהיה זמין.",
        "config-no-fts3": "'''אזהרה''': SQLite מקומפל ללא [//sqlite.org/fts3.html מודול FTS]. יכולות חיפוש לא יהיו זמינות בהתקנה הזאת.",
-       "config-register-globals": "'''אזהרה: האפשרות <code>[http://php.net/register_globals register_globals]</code> של PHP מופעלת.'''\n'''כבו אותה אם זה אפשרי.'''\nמדיה־ויקי תעבוד, אבל השרת שלך חשוף לפגיעות אבטחה.",
+       "config-register-globals-error": "<strong>שגיאה: האפשרות <code>[http://php.net/register_globals register_globals]</code> של PHP מופעלת.\nצריך לכבות אותה כדי להמשיך בהתקנה.</strong>\nר' [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] להסבר איך לעשות את זה.",
        "config-magic-quotes-runtime": "<strong>שגיאה סופנית: האפשרות [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] פעילה!</strong>\nהאפשרות הזאת מעוותת את נתוני הקלט באופן בלתי־צפוי.\nלא ניתן להתקין את מדיה־ויקי אלא אם האפשרות הזאת תכובה.",
        "config-magic-quotes-sybase": "'''שגיאה סופנית''': האפשרות [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] פעילה!'''\nהאפשרות הזאת מעוותת את נתוני הקלט באופן בלתי־צפוי.\nלא ניתן להתקין את מדיה־ויקי או להשתמש בה אלא אם האפשרות הזאת תכובה.",
        "config-mbstring": "'''שגיאה סופנית''': האפשרות [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] פעילה!'''\nהאפשרות הזאת גורמת לשגיאות ומעוותת את נתוני הקלט באופן בלתי־צפוי.\nלא ניתן להתקין את מדיה־ויקי או להשתמש בה אלא אם האפשרות הזאת תכובה.",
index a37ed89..c2411ef 100644 (file)
@@ -53,7 +53,6 @@
        "config-env-good": "Kondisi telah diperiksa.\nAnda dapat menginstal MediaWiki.",
        "config-env-bad": "Kondisi telah diperiksa.\nAnda tidak dapat menginstal MediaWiki.",
        "config-env-php": "PHP $1 diinstal.",
-       "config-env-php-toolow": "PHP $1 telah terinstal.\nNamun, MediaWiki memerlukan PHP $2 atau lebih tinggi.",
        "config-unicode-using-utf8": "Menggunakan utf8_normalize.so Brion Vibber untuk normalisasi Unicode.",
        "config-unicode-using-intl": "Menggunakan [http://pecl.php.net/intl ekstensi PECL intl] untuk normalisasi Unicode.",
        "config-unicode-pure-php-warning": "'''Peringatan''': [http://pecl.php.net/intl Ekstensi intl PECL] untuk menangani normalisasi Unicode tidak tersedia, kembali menggunakan implementasi murni PHP yang lambat.\nJika Anda menjalankan situs berlalu lintas tinggi, Anda harus sedikit membaca [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisasi Unicode].",
@@ -61,7 +60,7 @@
        "config-no-db": "Pengandar basis data yang sesuai tidak ditemukan! Anda perlu menginstal pengandar basis data untuk PHP.\nJenis basis data yang didukung: $1.\n\nJika Anda mengompilasi sendiri PHP, ubahlah konfigurasinya dengan mengaktifkan klien basis data, misalnya menggunakan <code>./configure --with-mysql</code>.\nJika Anda menginstal PHP dari paket Debian atau Ubuntu, maka Anda juga perlu menginstal modul php5-mysql.",
        "config-outdated-sqlite": "<strong>Peringatan:</strong> Anda menggunakan SQLite $1, yang lebih rendah dari versi minimum yang diperlukan $2. SQLite akan tidak tersedia.",
        "config-no-fts3": "'''Peringatan''': SQLite dikompilasi tanpa [//sqlite.org/fts3.html modul FTS3], fitur pencarian tidak akan tersedia pada konfigurasi ini.",
-       "config-register-globals": "'''Peringatan: Opsi <code>[http://php.net/register_globals register_globals]</code> PHP diaktifkan.'''\n'''Nonaktifkan kalau bisa.'''\nMediaWiki akan bekerja, tetapi server Anda memiliki potensi kerentanan keamanan.",
+       "config-register-globals-error": "<strong>Kesalahan: Pilihan PHP <code>[http://php.net/register_globals register_globals]</code> diaktifkan.\nIni harus dinonaktifkan untuk melanjutkan instalasi.</strong>\nLihat [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] untuk bantuan tentang cara melakukannya.",
        "config-magic-quotes-runtime": "'''Fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] aktif!'''\nPilihan ini dapat merusak masukan data secara tidak terduga.\nAnda tidak dapat menginstal atau menggunakan MediaWiki kecuali pilihan ini dinonaktifkan.",
        "config-magic-quotes-sybase": "'''Fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic_quotes_sybase magic_quotes_sybase] aktif!'''\nPilihan ini dapat merusak masukan data secara tidak terduga.\nAnda tidak dapat menginstal atau menggunakan MediaWiki kecuali pilihan ini dinonaktifkan.",
        "config-mbstring": "'''Fatal: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] aktif!'' '\nPilihan ini dapat menyebabkan kesalahan dan kerusakan data yang tidak terduga.\nAnda tidak dapat menginstal atau menggunakan MediaWiki kecuali pilihan ini dinonaktifkan.",
@@ -72,6 +71,7 @@
        "config-memory-raised": "<code>memory_limit</code> PHP adalah $1, dinaikkan ke $2.",
        "config-memory-bad": "'''Peringatan:''' <code>memory_limit</code> PHP adalah $1.\nIni terlalu rendah.\nInstalasi terancam gagal!",
        "config-ctype": "<strong>Fatal:</strong> PHP harus disusun dengan dukungan untuk [http://www.php.net/manual/en/ctype.installation.php ekstensi Ctype].",
+       "config-json": "<strong>Fatal:</strong> PHP dikompilasi tanpa dukungan JSON.\nAnda harus menginstal salah satu pengaya PHP JSON atau pengaya [http://pecl.php.net/package/jsonc PECL jsonc] sebelum menginstal MediaWiki.\n* Pengaya PHP termasuk dalam Red Hat Enterprise Linux (CentOS) 5 dan 6, meskipun harus diaktifkan pada <code>/etc/php.ini</code> atau <code>/etc/php.d/json.ini</code>.\n* Beberapa distribusi Linux dirilis setelah Mei 2013 menghilangkan pengaya PHP, bukan kemasan pengaya PECL sebagai <code>php5-json</code> atau <code>php-pecl-jsonc</code>.",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] telah diinstal",
        "config-apc": "[http://www.php.net/apc APC] telah diinstal",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] telah diinstal",
@@ -88,6 +88,7 @@
        "config-using-server": "Menggunakan nama server \"<nowiki>$1</nowiki>\".",
        "config-using-uri": "Menggunakan URL server \"<nowiki>$1$2</nowiki>\".",
        "config-uploads-not-safe": "'''Peringatan:''' Direktori bawaan pengunggahan <code>$1</code> Anda rentan terhadap eksekusi skrip yang sewenang-wenang.\nMeskipun MediaWiki memeriksa semua berkas unggahan untuk ancaman keamanan, sangat dianjurkan untuk [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security menutup kerentanan keamanan ini] sebelum mengaktifkan pengunggahan.",
+       "config-no-cli-uploads-check": "<strong>Peringatan:</strong> Direktori default Anda untuk unggahan (<code>$1</code>) tidak diperiksa untuk kerentanan terhadap\neksekusi script sewenang-wenang selama instalasi CLI.",
        "config-brokenlibxml": "Sistem Anda memiliki kombinasi versi PHP dan libxml2 yang memiliki bug dan dapat menyebabkan kerusakan data tersembunyi pada MediaWiki dan aplikasi web lain.\nMutakhirkan ke PHP 5.2.9 atau yang lebih baru dan libxml2 2.7.3 atau yang lebih baru ([https://bugs.php.net/bug.php?id=45996 arsip bug di PHP]).\nInstalasi dibatalkan.",
        "config-suhosin-max-value-length": "Suhosin terpasang dan membatasi parameter GET <code>length</code> sebesar $1 bita. Komponen ResourceLoader MediaWiki akan berjalan dalam batasan ini, tetapi penanganannya akan menurunkan kinerja. Jika memungkinkan, Anda sebaiknya menetapkan nilai <code>suhosin.get.max_value_length</code> menjadi 1024 atau lebih tinggi dalam <code>php.ini</code> dan menyetel <code>$wgResourceLoaderMaxQueryLength</code> dengan nilai yang sama dalam <code>LocalSettings.php</code>.",
        "config-db-type": "Jenis basis data:",
        "config-missing-db-name": "Anda harus memasukkan nilai untuk \"{{int:config-db-name}}\"",
        "config-missing-db-host": "Anda harus memasukkan nilai untuk \"{{int:config-db-host}}\"",
        "config-missing-db-server-oracle": "Anda harus memasukkan nilai untuk \"{{int:config-db-host-oracle}}\"",
-       "config-invalid-db-server-oracle": "TNS basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan titik (.).",
+       "config-invalid-db-server-oracle": "TNS basis data \"$1\" tidak sah.\nGunakan baik \"Nama TNS\" atau string \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Metode Penamaan Oracle]).",
        "config-invalid-db-name": "Nama basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).",
        "config-invalid-db-prefix": "Prefiks basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).",
        "config-connection-error": "$1.\n\nPeriksa nama inang, pengguna, dan sandi di bawah ini dan coba lagi.",
index 0638452..72619bc 100644 (file)
@@ -50,7 +50,6 @@
        "config-env-good": "환경이 확인되었습니다.\n미디어위키를 설치할 수 있습니다.",
        "config-env-bad": "환경이 확인되었습니다.\n미디어위키를 설치할 수 없습니다.",
        "config-env-php": "PHP $1(이)가 설치되었습니다.",
-       "config-env-php-toolow": "PHP $1(이)가 설치되었습니다.\n하지만 미디어위키는 PHP $2 이상이 필요합니다.",
        "config-unicode-using-utf8": "유니코드 정규화에 Brion Vibber의 utf8_normalize.so를 사용합니다.",
        "config-unicode-using-intl": "유니코드 정규화에 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용합니다.",
        "config-unicode-pure-php-warning": "'''경고''': 유니코드 정규화를 처리할 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용할 수 없기 때문에 느린 pure-PHP 구현을 대신 사용합니다.\n트래픽이 높은 사이트에서 실행하시려면 [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 유니코드 정규화]를 읽어보시기 바랍니다.",
@@ -58,7 +57,7 @@
        "config-no-db": "적절한 데이터베이스 드라이버를 찾을 수 없습니다! PHP용 데이터베이스 드라이버를 설치해야 합니다.\n다음 데이터베이스 유형을 지원합니다: $1.\n\nPHP를 직접 컴파일했다면, 예를 들어 <code>./configure --with-mysql</code>을 사용하여, 데이터베이스 클라이언트를 활성화하도록 다시 설정하세요.\n데비안이나 우분투 패키지에서 PHP를 설치했다면 <code>php5-mysql</code> 모듈도 설치해야 합니다.",
        "config-outdated-sqlite": "'''경고''': 최소인 $2 버전보다 낮은 SQLite $1(이)가 있습니다. SQLite를 사용할 수 없습니다.",
        "config-no-fts3": "'''경고''': SQLite를 [//sqlite.org/fts3.html FTS3 모듈] 없이 컴파일하며, 검색 기능은 백엔드에 사용할 수 없습니다.",
-       "config-register-globals": "'''경고: PHP의 <code>[http://php.net/register_globals register_globals]</code> 옵션이 활성화되어 있습니다.'''\n'''가능하면 이를 비활성화하십시오.'''\n미디어위키는 작동하지만 서버에 잠재적인 보안 취약점이 노출됩니다.",
+       "config-register-globals-error": "<strong>오류: PHP의 <code>[http://php.net/register_globals register_globals]</code> 옵션이 활성화되어 있습니다.\n설치를 계속하려면 비활성화해야 합니다.</strong>\n어떻게 하는지에 대한 도움말에 대해서는 [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals]를 보세요.",
        "config-magic-quotes-runtime": "'''치명: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime]이 활성화됩니다!'''\n이 옵션은 데이터를 입력하는 데 예기치 않는 손상이 일으킵니다.\n이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
        "config-magic-quotes-sybase": "'''치명: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase]이 활성화됩니다!'''\n이 옵션은 데이터를 입력하는 데 예기치 않는 손상을 일으킵니다.\n이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
        "config-mbstring": "'''치명: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]이 활성화됩니다!'''\n이 옵션은 오류가 발생하고 데이터를 입력하는 데 예기치 않는 손상을 일으킬 수 있습니다.\n이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
index 7a6787b..b67f62f 100644 (file)
@@ -47,7 +47,6 @@
        "config-env-good": "Околината е проверена.\nМожете да го воспоставите МедијаВики.",
        "config-env-bad": "Околината е проверена.\nНе можете да го воспоставите МедијаВики.",
        "config-env-php": "PHP $1 е воспоставен.",
-       "config-env-php-toolow": "PHP $1 е воспоставен.\nМеѓутоа, МедијаВики бара PHP $2 или поново.",
        "config-unicode-using-utf8": "Со utf8_normalize.so за уникодна нормализација од Брајон Вибер (Brion Vibber).",
        "config-unicode-using-intl": "Со додатокот [http://pecl.php.net/intl intl PECL] за уникодна нормализација.",
        "config-unicode-pure-php-warning": "'''Предупредување''': Додатокот [http://pecl.php.net/intl intl PECL] не е достапен за врши уникодна нормализација, враќајќи се на бавна примена на чист PHP.\n\nАко имате високопрометно мрежно место, тогаш ќе треба да прочитате повеќе за [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations уникодната нормализација].",
@@ -55,7 +54,7 @@
        "config-no-db": "Не можев да најдам соодветен двигател за базата на податоци! Ќе треба да воспоставите двигател за PHP-база.\nПоддржани се следниве видови бази: $1.\n\nДоколку самите го срочивте овој PHP, овозможете го базниот клиент во поставките — на пр. со <code>./configure --with-mysqli</code>.\nАко овој PHP го воспоставите од пакет на Debian или Ubuntu, тогаш ќе треба исто така да го воспоставите, на пр., пакетот <code>php5-mysql</code>.",
        "config-outdated-sqlite": "'''Предупредување''': имате SQLite $1. Најстарата допуштена верзија е $2. Затоа, SQLite ќе биде недостапен.",
        "config-no-fts3": "'''Предупредување''': SQLite iе составен без модулот [//sqlite.org/fts3.html FTS3] - за оваа база нема да има можност за пребарување.",
-       "config-register-globals": "'''Предупредување: Можноста <code>[http://php.net/register_globals register_globals]</code> за PHP е овозможена.'''\n'''Оневозможете ја ако е можно.'''\nМедијаВики ќе работи, но опслужувачот ви е изложен на безбедносни ризици.",
+       "config-register-globals-error": "<strong>Грешка: Вклучена е можноста <code>[http://php.net/register_globals register_globals]</code> за PHP.\nМора да се исклучи за да продолжите со воспоставката.</strong>\nКако да го направите тоа можете да прочитате на [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals].",
        "config-magic-quotes-runtime": "'''Кобно: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] е активно!'''\nОваа можност непредвидливо го расипува вносот на податоци.\nОваа можност мора да е исклучена. Во спротивно нема да можете да го воспоставите и користите МедијаВики.",
        "config-magic-quotes-sybase": "'''Кобно: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] е активно!'''\nОваа можност непредвидливо го расипува вносот на податоци.\nОваа можност мора да е исклучена. Во спротивно нема да можете да го воспоставите и користите МедијаВики.",
        "config-mbstring": "'''Кобно: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] е активно!'''\nОваа можност предизвикува грешки и може непредвидиво да го расипува вносот на податоци.\nОваа можност мора да е исклучена. Во спротивно нема да можете да го воспоставите и користите МедијаВики.",
index c95e9df..93ddfa4 100644 (file)
@@ -50,7 +50,6 @@
        "config-env-good": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.",
        "config-env-bad": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.",
        "config-env-php": "PHP $1 er innstallert.",
-       "config-env-php-toolow": "PHP $1 er installert.\nMediaWiki krever imidlertid PHP $2 eller høyere.",
        "config-unicode-using-utf8": "Bruker Brion Vibbers utf8_normalize.so for Unicode-normalisering.",
        "config-unicode-using-intl": "Bruker [http://pecl.php.net/intl intl PECL-utvidelsen] for Unicode-normalisering.",
        "config-unicode-pure-php-warning": "'''Advarsel''': [http://pecl.php.net/intl intl PECL-utvidelsen] er ikke tilgjengelig for å håndtere Unicode-normaliseringen, faller tilbake til en langsommere ren-PHP-implementasjon.\nOm du kjører et nettsted med høy trafikk bør du lese litt om [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
@@ -58,7 +57,7 @@
        "config-no-db": "Fant ingen passende databasedriver! Du må installere en databasedriver for PHP.\nFølgende databasetyper støttes: $1\n\nOm du kompilerte PHP selv, rekonfigurer den med en aktivert databaseklient, for eksempel ved å bruke <code>./configure --with-mysql</code>.\nOm du installerte PHP fra en Debian- eller Ubuntu-pakke, må du også installere for eksempel <code>php5-mysql</code>-pakken.",
        "config-outdated-sqlite": "'''Advarsel''': Du har SQLite $1, som er en eldre versjon enn minimumskravet SQLite $2. SQLite vil ikke være tilgjengelig.",
        "config-no-fts3": "'''Advarsel''': SQLite er kompilert uten [//sqlite.org/fts3.html FTS3-modulen], søkefunksjoner vil ikke være tilgjengelig på dette bakstykket.",
-       "config-register-globals": "'''Advarsel: PHPs <code>[http://php.net/register_globals register_globals]</code>-alternativ er aktivert.'''\n'''Deaktiver det om du kan.'''\nMediaWiki vil fungere, men tjeneren din er utsatt for potensielle sikkerhetssårbarheter.",
+       "config-register-globals-error": "<strong>Feil: PHPs <code>[http://php.net/register_globals register_globals]</code>-valg er aktivt.\nDet må deaktiveres for å kunne fortsette med installeringen.</strong>\nSe [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] for å få hjelp til å gjøre dette.",
        "config-magic-quotes-runtime": "'''Kritisk: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] er aktiv!'''\nDette alternativet ødelegger inndata på en uforutsigbar måte.\nDu kan ikke installere eller bruke MediaWiki med mindre dette alternativet deaktiveres.",
        "config-magic-quotes-sybase": "'''Kritisk: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] er aktiv!'''\nDette alternativet ødelegger inndata på en uforutsigbar måte.\nDu kan ikke installere eller bruke MediaWiki med mindre dette alternativet deaktiveres.",
        "config-mbstring": "'''Kritisk: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] er aktiv!'''\nDette alternativet fører til feil og kan ødelegge data på en uforutsigbar måte.\nDu kan ikke installere eller bruke MediaWiki med mindre dette alternativet deaktiveres.",
index 1a3e7ae..eb16d0d 100644 (file)
@@ -61,7 +61,6 @@
        "config-env-good": "Środowisko oprogramowania zostało sprawdzone.\nMożesz teraz zainstalować MediaWiki.",
        "config-env-bad": "Środowisko oprogramowania zostało sprawdzone.\nNie możesz zainstalować MediaWiki.",
        "config-env-php": "Zainstalowane jest PHP w wersji $1.",
-       "config-env-php-toolow": "Zainstalowane jest PHP $1.\nJednak MediaWiki wymaga PHP $2 lub nowszego.",
        "config-unicode-using-utf8": "Korzystanie z normalizacji Unicode utf8_normalize.so napisanej przez Brion Vibbera.",
        "config-unicode-using-intl": "Korzystanie z [http://pecl.php.net/intl rozszerzenia intl PECL] do normalizacji Unicode.",
        "config-unicode-pure-php-warning": "'''Uwaga!''' [http://pecl.php.net/intl Rozszerzenie intl PECL] do obsługi normalizacji Unicode nie jest dostępne. Użyta zostanie mało wydajna zwykła implementacja w PHP.\nJeśli prowadzisz stronę o dużym natężeniu ruchu, powinieneś zapoznać się z informacjami o [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizacji Unicode].",
@@ -69,7 +68,7 @@
        "config-no-db": "Nie można odnaleźć właściwego sterownika bazy danych! Musisz zainstalować sterownik bazy danych dla PHP.\nMożna użyć następujących typów baz danych: $1.\n\nJeśli skompilowałeś PHP samodzielnie, skonfiguruj je ponownie z włączonym klientem bazy danych, na przykład za pomocą polecenia <code>./configure --with-mysqli</code>.\nJeśli zainstalowałeś PHP jako pakiet Debiana lub Ubuntu, musisz również zainstalować np. moduł <code>php5-mysql</code>.",
        "config-outdated-sqlite": "'''Ostrzeżenie''': masz SQLite  $1, która jest niższa od minimalnej wymaganej wersji  $2 . SQLite będzie niedostępne.",
        "config-no-fts3": "'''Uwaga''' – SQLite został skompilowany bez [//sqlite.org/fts3.html modułu FTS3] – funkcje wyszukiwania nie będą dostępne.",
-       "config-register-globals": "'''Uwaga –  w konfiguracji PHP włączona jest opcja <code>[http://php.net/register_globals register_globals]</code>.'''\n'''Jeśli możesz, wyłącz ją.'''\nMediaWiki będzie działać, ale Twój serwer może być narażony potencjalnymi lukami w zabezpieczeniach.",
+       "config-register-globals-error": "<strong>Błąd: dyrektywa PHP <code>[http://php.net/register_globals register_globals]</code> jest włączona.\nAby kontynuować instalację musi zostać wyłączona.</strong>\nPrzeczytaj [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals], aby dowiedzieć się, jak to zrobić.",
        "config-magic-quotes-runtime": "'''Błąd krytyczny – włączono [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime]!'''\nTa opcja powoduje nieprzewidywalne uszkodzenia wprowadzanych danych.\nZainstalować lub korzystać z MediaWiki można pod warunkiem, że ta opcja jest wyłączona.",
        "config-magic-quotes-sybase": "'''Błąd krytyczny – włączono [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase]!'''\nTa opcja powoduje nieprzewidywalne uszkodzenia wprowadzanych danych.\nZainstalować lub korzystać z MediaWiki można pod warunkiem, że ta opcja jest wyłączona.",
        "config-mbstring": "'''Błąd krytyczny – włączono [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]!'''\nTa opcja powoduje błędy i może wywołać nieprzewidywalne uszkodzenia wprowadzanych danych.\nZainstalować lub korzystać z MediaWiki można pod warunkiem, że ta opcja jest wyłączona.",
index 20f0b3e..68a94e8 100644 (file)
@@ -57,7 +57,6 @@
        "config-env-good": "O ambiente foi verificado.\nVocê pode instalar o MediaWiki.",
        "config-env-bad": "O ambiente foi verificado.\nVocê não pode instalar o MediaWiki.",
        "config-env-php": "O PHP $1 está instalado.",
-       "config-env-php-toolow": "PHP $1 está instalado.\nNo entanto, o MediaWiki requer PHP $2 ou superior.",
        "config-unicode-using-utf8": "Usando o utf8_normalize.so, de Brion Vibber, para a normalização Unicode.",
        "config-unicode-using-intl": "Usando a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.",
        "config-unicode-pure-php-warning": "<strong>Aviso</strong>: A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode, abortando e passando para a lenta implementação de PHP puro.\nSe o seu site tem um alto volume de tráfego, informe-se sobre a [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalização Unicode].",
@@ -65,7 +64,6 @@
        "config-no-db": "Não foi possível encontrar um driver de banco de dados adequado! É necessário instalar um driver de banco de dados para o PHP.\nSão suportados os seguintes tipos de bancos de dados: $1.\n\nSe você mesmo tiver compilado o PHP, reconfigure-o com um cliente de banco de dados ativado usando, por exemplo <code>./configure --with-mysqli</code>.\nSe você instalou o PHP a partir de um pacote do Debian ou do Ubuntu, então será também necessário instalar, por exemplo, o pacote <code>php5-mysql</code>.",
        "config-outdated-sqlite": "<strong>Aviso:</strong> você tem o SQLite versão $1, que é menor do que a versão mínima necessária $2. O SQLite não estará disponível.",
        "config-no-fts3": "<strong>Aviso</strong> O SQLite foi compilado sem o [//sqlite.org/fts3.html módulo FTS3], as funcionalidades de pesquisa não estarão disponíveis nesta instalação.",
-       "config-register-globals": "<strong>Aviso: A opção <code>[http://php.net/register_globals register_globals]</code> do PHP está ativada.\nDesative-a se puder.</strong>\nO MediaWiki funcionará mesmo assim, mas o seu servidor ficará exposto a potenciais vulnerabilidades de segurança.",
        "config-magic-quotes-runtime": "<strong>Erro fatal: A opção [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] está ativada!</strong>\nEsta opção causa corrupção dos dados de entrada de forma imprevisível.\nVocê não pode instalar ou utilizar o MediaWiki a menos que esta opção seja desativada.",
        "config-magic-quotes-sybase": "<strong>Erro fatal: A opção [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] está ativada!</strong>\nEsta opção corrompe os dados de entrada de forma imprevisível.\nVocê não pode instalar ou utilizar o MediaWiki a menos que esta opção seja desativada.",
        "config-mbstring": "<strong>Erro fatal: A opção [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] está ativada!</strong>\nEsta opção causa erros e pode corromper os dados de forma imprevisível.\nVocê não pode instalar ou utilizar o MediaWiki a menos que esta opção seja desativada.",
index 3a16079..5e86a92 100644 (file)
@@ -62,7 +62,6 @@
        "config-env-good": "See also:\n* {{msg-mw|Config-env-bad}}",
        "config-env-bad": "See also:\n* {{msg-mw|Config-env-good}}",
        "config-env-php": "Parameters:\n* $1 - the version of PHP that has been installed\nSee also:\n* {{msg-mw|config-env-php-toolow}}",
-       "config-env-php-toolow": "Parameters:\n* $1 - the version of PHP that has been installed\n* $2 - minimum PHP version number\nSee also:\n* {{msg-mw|config-env-php}}",
        "config-unicode-using-utf8": "Status message in the MediaWiki installer environment checks.",
        "config-unicode-using-intl": "Status message in the MediaWiki installer environment checks.",
        "config-unicode-pure-php-warning": "PECL is the name of a group producing standard pieces of software for PHP, and intl is the name of their library handling some aspects of internationalization.",
@@ -70,7 +69,7 @@
        "config-no-db": "{{doc-important|Do not translate \"<code>./configure --with-mysqli</code>\" and \"<code>php5-mysql</code>\".}}\nParameters:\n* $1 is comma separated list of database types supported by MediaWiki.",
        "config-outdated-sqlite": "Used as warning. Parameters:\n* $1 - the version of SQLite that has been installed\n* $2 - minimum version",
        "config-no-fts3": "A \"[[:wikipedia:Front and back ends|backend]]\" is a system or component that ordinary users don't interact with directly and don't need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are \"system\" or \"service\", or (depending on context and language) even leave it untranslated.",
-       "config-register-globals": "Status message in the MediaWiki installer environment checks.",
+       "config-register-globals-error": "Error message in the MediaWiki installer environment checks.",
        "config-magic-quotes-runtime": "{{Related|Config-fatal}}",
        "config-magic-quotes-sybase": "{{Related|Config-fatal}}",
        "config-mbstring": "{{Related|Config-fatal}}",
index 17f5fbc..3203df7 100644 (file)
@@ -62,7 +62,6 @@
        "config-env-good": "Проверка внешней среды была успешно проведена.\nВы можете установить MediaWiki.",
        "config-env-bad": "Была проведена проверка внешней среды.\nВы не можете установить MediaWiki.",
        "config-env-php": "Установленная версия PHP: $1.",
-       "config-env-php-toolow": "Найден PHP $1, тогда как MediaWiki требуется PHP версии $2 или выше.",
        "config-unicode-using-utf8": "Использовать Brion Vibber utf8_normalize.so для нормализации Юникода.",
        "config-unicode-using-intl": "Будет использовано [http://pecl.php.net/intl расширение «intl» для PECL] для нормализации Юникода.",
        "config-unicode-pure-php-warning": "'''Внимание!''': [http://pecl.php.net/intl расширение intl из PECL] недоступно для нормализации Юникода, будет использоваться медленная реализация на чистом PHP.\nЕсли ваш сайт работает под высокой нагрузкой, вам следует больше узнать о [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализации Юникода].",
@@ -70,7 +69,7 @@
        "config-no-db": "Не удалось найти подходящие драйвера баз данных! Вам необходимо установить драйвера базы данных для PHP.\nПоддерживаются следующие типы баз данных: $1.\nЕсли вы скомпилировали PHP сами, перенастройте его с включением клиента баз данных, например, с помощью <code>./configure --with-mysqli</code>.\nЕсли вы скомпилировали PHP сами, сконфигурируйте его снова с включенным клиентом базы данных, например, с помощью <code>./configure --with-mysql</code>.\nЕсли вы установили PHP из пакетов Debian или Ubuntu, то вам также необходимо установить, например, пакет <code>php5-mysql</code>.",
        "config-outdated-sqlite": "'''Предупреждение''': у Вас установлен SQLite  $1, версия которого ниже требуемой $2 . SQLite будет недоступен.",
        "config-no-fts3": "'''Внимание''': SQLite собран без модуля [//sqlite.org/fts3.html FTS3] — поиск не будет работать для этой базы данных.",
-       "config-register-globals": "'''Внимание: PHP-опция <code>[http://php.net/register_globals register_globals]</code> включена.'''\n'''Отключите её, если это возможно.'''\nMediaWiki будет работать, но это снизит безопасность сервера и увеличит риск проникновения извне.",
+       "config-register-globals-error": "<strong>Ошибка: Параметр PHP <code>[http://php.net/register_globals register_globals]</code> включен.\nОн должен быть отключен для того, чтобы можно было продолжить установку.</strong>\nПолучить справку о том, как это сделать, можно по адресу [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals].",
        "config-magic-quotes-runtime": "'''Проблема: включена опция PHP [http://www.php.net/manual/ru/function.magic-quotes-runtime.php magic_quotes_runtime]!'''\nЭто приводит к непредсказуемой порче вводимых данных.\nУстановка и использование MediaWiki без выключения этой опции невозможно.",
        "config-magic-quotes-sybase": "'''Проблема: включена опция PHP [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase]!'''\nЭто приводит к непредсказуемой порче вводимых данных.\nУстановка и использование MediaWiki без выключения этой опции невозможно.",
        "config-mbstring": "'''Проблема: включена опция PHP [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]!'''\nЭто приводит к ошибкам и непредсказуемой порче вводимых данных.\nУстановка и использование MediaWiki без выключения этой опции невозможно.",
index 53010fb..4cb5442 100644 (file)
@@ -51,7 +51,6 @@
        "config-env-good": "Miljön har kontrollerats.\nDu kan installera MediaWiki.",
        "config-env-bad": "Miljön har kontrollerats.\nDu kan inte installera MediaWiki.",
        "config-env-php": "PHP $1 är installerat.",
-       "config-env-php-toolow": "PHP $1 är installerat.\nMediaWiki kräver PHP $2 eller högre.",
        "config-unicode-using-utf8": "Använder Brion Vibbers utf8_normalize.so för Unicode-normalisering.",
        "config-unicode-using-intl": "Använder [http://pecl.php.net/intl intl PECL-tillägget] för Unicode-normalisering.",
        "config-unicode-pure-php-warning": "'''Varning:''' [http://pecl.php.net/intl intl PECL-tillägget] är inte tillgängligt för att hantera Unicode-normalisering, faller tillbaka till en långsamt implementering i ren PHP.\nOm du driver en högtrafikerad webbplats bör du läsa lite om [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
@@ -59,7 +58,7 @@
        "config-no-db": "Kunde inte hitta en lämplig databasdrivrutin! Du måste installera en databasdrivrutin för PHP.\nFöljande databastyper stöds: $1.\n\nI du själv kompilerat din PHP, konfigurera den med en databasklient aktiverad genom att t.ex. använda <code>./configure --with-mysqli</code>.\nOm du installerade PHP från ett Debian- eller Ubuntupaket måste du även installera, t.ex. <code>php5-mysql</code>-paketet.",
        "config-outdated-sqlite": "'''Varning:''' du har SQLite $1, vilket är lägre än minimikravet version $2. SQLite kommer inte att vara tillgänglig.",
        "config-no-fts3": "'''Varning:''' SQLite kompileras utan [//sqlite.org/fts3.html FTS3-modulen], sökfunktioner kommer att vara otillgängliga på denna backend.",
-       "config-register-globals": "'''Varning: PHP:s <code>[http://php.net/register_globals register_globals]</code>-tillval är aktiverat.'''\n'''Inaktivera den om du kan.'''\nMediaWiki kommer att fungera, men din server exponeras för potentiella säkerhetshål.",
+       "config-register-globals-error": "<strong>Fel: PHP-alternativet <code>[http://php.net/register_globals register_globals]</code> är aktiverad.\nDen måste vara inaktiverad för att fortsätta med installationen.</strong>\nSe [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] för hjälp om hur man gör så.",
        "config-magic-quotes-runtime": "'''Kritiskt: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] är aktiv!'''\nDetta alternativ korrumperar inmatad data oförutsägbart.\nDu kan inte installera eller använda MediaWiki om detta alternativ är aktiverat.",
        "config-magic-quotes-sybase": "'''Kritiskt: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] är aktiv!'''\nDetta alternativ korrumperar inmatad data oförutsägbart.\nDu kan inte installera eller använda MediaWiki om detta alternativ är aktiverat.",
        "config-mbstring": "'''Kritiskt: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] är aktiv!'''\nDetta alternativ orsakar fel och kan korrumpera data oförutsägbart.\nDu kan inte installera eller använda MediaWiki om detta alternativ är aktiverat.",
index 6ec8860..83c51ee 100644 (file)
@@ -18,7 +18,7 @@
        "config-localsettings-badkey": "Sağladığınız anahtar doğru değil.",
        "config-upgrade-key-missing": "Mevcut bir MediaWiki kurulumu algılandı.\nBu kurulumu güncelleştirmek için, lütfen aşağıdaki satırı <code>LocalSettings.php</code> dosyanızın en altına koyun:\n\n$1",
        "config-localsettings-incomplete": "Mevcut <code>LocalSettings.php</code> eksik gibi görünüyor.\n $1  değişkeni ayarlanmamış.\nLütfen <code>LocalSettings.php</code> dosyasını değiştirin bu değişkenleri kuracak, ve tıklayın  \"{{int:Config-cuntinue}}\".",
-       "config-localsettings-connection-error": "<code>LocalSettings.php</code> ya da <code>AdminSettings.php</code> dosyasında belirtilen ayarları kullanarak veritabanına bağlanırken bir hatayla karşılaşıldı. Lütfen bu ayarları düzeltin ve yeniden deneyin.\n\n$1",
+       "config-localsettings-connection-error": "<code>LocalSettings.php</code> içinde belirtilen ayarları kullanarak veritabanına bağlanırken bir hatayla karşılaşıldı. Lütfen bu ayarları düzeltin ve yeniden deneyin.\n\n$1",
        "config-session-error": "Oturum başlatılırken hata: $1",
        "config-session-expired": "Oturum bilgilerinizin süresi bitmiş.\nOturumların süresi $1 kadardır.\nBu süreyi php.ini' deki <code>session.gc_maxlifetime</code> ayarla arttırabilirsiniz.\nKurulum işlemini yeniden başlatın.",
        "config-no-session": "Oturum bilgileriniz silinmiş.\nphp.ini dosyanızı kontrol edin ve <code>session.save_path</code> ayarının uygun bir klasöre yönlendiğinden emin olun.",
        "config-restart": "Evet, yeniden başlat",
        "config-welcome": "===Ortam Kontrolleri===\nOrtamın Mediawiki kurulumuna uygun olup olmadığını anlamak için basit kontroller yapılacak.\nKurulumu nasıl tamamlayacağınız konusunda destek isterken bu bilgileri eklemeyi unutmayın.",
        "config-copyright": "=== Telif Hakları ve Koşulları ===\n\n$1\n\nBu program ücretsiz bir yazılımdır; yeniden dağıtabilir veya Özgür Yazılım Kuruluşu tarafından yayınlanan (GNU) Genel Kamu Lisansı koşulları altında değiştirebilirsiniz; isterseniz ikinci lisans sürümünü veya (sizin seçeneğiniz) herhangi bir sonraki lisans sürümünü kullanabilirsiniz.\n\nBu program, faydalı olacağı umuduyla dağıtılmaktadır, ancak ''' herhangi bir garantisi yoktur '''; ''' uygunluk ''' veya ''' belirli bir amaca uygunluk ''' gibi dolaylı garantileri bile yoktur.\nDaha fazla ayrıntı için (GNU) Genel Kamu Lisansına bakınız.\n\nBu program ile birlikte <doclink href=\"Copying\">bir (GNU) Genel Kamu Lisansının bir kopyasını </doclink> almış olmanız gerekir; bu program (GNU) Genel Kamu Lisansı ile dağıtılmadıysa, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, ABD adresine yazın veya [http://www.gnu.org/copyleft/gpl.html online olarak okuyun].",
+       "config-sidebar": "* [//www.mediawiki.org MediaWiki ana sayfa]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Kullanıcı Rehberi]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Yetkili Rehberi]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ SSS]\n----\n* <doclink href=Readme>Beni oku</doclink>\n* <doclink href=ReleaseNotes>Sürüm notları</doclink>\n* <doclink href=Copying>Kopyalama</doclink>\n* <doclink href=UpgradeDoc>Yükseltme</doclink>",
        "config-env-good": "Ortam kontrol edildi.\nMediaWiki'yi kurabilirsiniz.",
        "config-env-bad": "Ortam kontrol edildi.\nMediaWiki'yi kuramazsınız.",
        "config-env-php": "PHP $1 kurulu.",
-       "config-env-php-toolow": "PHP $1 kurulu.\nAncak, MediaWiki PHP $2 ya da daha yenisine ihtiyaç duyuyor.",
        "config-unicode-using-utf8": "Unikod normalleştirmesi için Brion Vibber'in utf8_normalize.so kullanılıyor.",
        "config-unicode-using-intl": "Unikod normalleştirmesi için [http://pecl.php.net/intl intl PECL uzantısı] kullanılıyor.",
        "config-xml-bad": "PHP 'nin XML modülü eksik.\nMediaWiki bu modüldeki fonksiyonlara ihtiyaç duyar ve şimdiki kurulumda çalışmayacaktır.\nMandrake kullanıyorsanız php-xml paketini yükleyin.",
        "config-install-mainpage-failed": "Ana sayfa eklenemedi:$1",
        "config-download-localsettings": "İndir <code>LocalSettings.php</code>",
        "config-help": "Yardım",
+       "config-help-tooltip": "genişletmek için tıklayın",
        "config-nofile": "\"$1\" dosyası bulunamadı. Silindi mi?",
        "config-extension-link": "Vikinizin [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions eklentileri] desteklediğini biliyor musunuz?\n\n[//www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category Eklentileri kategorilerine göre] inceleyebilir ya da tüm eklentilerin listesini görmek için [//www.mediawiki.org/wiki/Extension_Matrix Eklenti Matrisine] bakabilirsiniz.",
        "mainpagetext": "'''MediaWiki başarı ile kuruldu.'''",
index e39477b..9c2d37e 100644 (file)
@@ -65,7 +65,6 @@
        "config-env-good": "环境检查已经完成。您可以安装MediaWiki。",
        "config-env-bad": "环境检查已经完成。您不能安装MediaWiki。",
        "config-env-php": "PHP $1已安装。",
-       "config-env-php-toolow": "已安装PHP $1;但是,MediaWiki需要PHP $2或更高版本。",
        "config-unicode-using-utf8": "使用Brion Vibber的utf8_normalize.so实现Unicode正常化。",
        "config-unicode-using-intl": "使用[http://pecl.php.net/intl intl PECL扩展程序]标准化Unicode。",
        "config-unicode-pure-php-warning": "<strong>警告:</strong>因为尚未安装 [http://pecl.php.net/intl intl PECL 扩展]以处理 Unicode 正常化,故只能退而采用运行较慢的纯 PHP 实现的方法。\n如果您运行着一个高流量的站点,请参阅 [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode 正常化]一文。",
@@ -73,7 +72,6 @@
        "config-no-db": "找不到合适的数据库驱动!您需要为PHP安装数据库驱动。目前支持以下数据库:$1。如果您是自己编译的PHP,请重新配置他与数据库客户端将其启用,诸如,使用<code>./configure --with-mysqli</code>。如果您从Debian或Ubuntu包安装了PHP,之后您仍需要安装诸如<code>php5-mysql</code>包。",
        "config-outdated-sqlite": "'''警告''':您已安装SQLite $1,但是它的版本低于最低要求版本$2。因此您无法选择SQLite。",
        "config-no-fts3": "'''警告''':已编译的SQLite不包含[//sqlite.org/fts3.html FTS3模块],后台搜索功能将不可用。",
-       "config-register-globals": "'''警告:PHP的<code>[http://php.net/register_globals register_globals]</code>选项被启用。请尽量禁用该功能,'''虽然不会影响MediaWiki的运行,但您的服务器会被暴露给潜在的安全漏洞。",
        "config-magic-quotes-runtime": "'''毁灭性错误:[http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime]已启用!'''\n此选项会无法预测地破坏输入的数据,请将其禁用,否则您将不能安装或使用MediaWiki。",
        "config-magic-quotes-sybase": "'''毁灭性错误:[http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_sybase]已启用!'''\n此选项会无法预测地破坏输入的数据,请将其禁用,否则您将不能安装或使用MediaWiki。",
        "config-mbstring": "'''毁灭性错误:[http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]已启用!'''\n此选项会导致错误并不可预测地破坏数据,请将其禁用,否则您将不能安装或使用MediaWiki。",
index 088f447..522bae1 100644 (file)
@@ -62,9 +62,10 @@ class JobQueueRedis extends JobQueue {
 
        /** @var string Server address */
        protected $server;
-
        /** @var string Compression method to use */
        protected $compression;
+       /** @var bool */
+       protected $daemonized;
 
        const MAX_AGE_PRUNE = 604800; // integer; seconds a job can live once claimed (7 days)
 
@@ -79,6 +80,9 @@ class JobQueueRedis extends JobQueue {
         *                   If a hostname is specified but no port, the standard port number
         *                   6379 will be used. Required.
         *   - compression : The type of compression to use; one of (none,gzip).
+        *   - daemonized  : Set to true if the redisJobRunnerService runs in the background.
+        *                   This will disable job recycling/undelaying from the MediaWiki side
+        *                   to avoid redundance and out-of-sync configuration.
         * @param array $params
         */
        public function __construct( array $params ) {
@@ -87,6 +91,7 @@ class JobQueueRedis extends JobQueue {
                $this->server = $params['redisServer'];
                $this->compression = isset( $params['compression'] ) ? $params['compression'] : 'none';
                $this->redisPool = RedisConnectionPool::singleton( $params['redisConfig'] );
+               $this->daemonized = !empty( $params['daemonized'] );
        }
 
        protected function supportedOrders() {
@@ -716,6 +721,9 @@ LUA;
         * @return array
         */
        protected function doGetPeriodicTasks() {
+               if ( $this->daemonized ) {
+                       return array(); // managed in the runner loop
+               }
                $periods = array( 3600 ); // standard cleanup (useful on config change)
                if ( $this->claimTTL > 0 ) {
                        $periods[] = ceil( $this->claimTTL / 2 ); // avoid bad timing
index 6b10ae4..4885ae6 100644 (file)
@@ -163,6 +163,8 @@ class CSSMin {
         * Build a CSS 'url()' value for the given URL, quoting parentheses (and other funny characters)
         * and escaping quotes as necessary.
         *
+        * See http://www.w3.org/TR/css-syntax-3/#consume-a-url-token
+        *
         * @param string $url URL to process
         * @return string 'url()' value, usually just `"url($url)"`, quoted/escaped if necessary
         */
index d72ffca..809bfdf 100644 (file)
@@ -28,8 +28,6 @@ class HttpStatus {
        /**
         * Get the message associated with HTTP response code $code
         *
-        * Replace OutputPage::getStatusMessage( $code )
-        *
         * @param $code Integer: status code
         * @return String or null: message or null if $code is not in the list of
         *         messages
index 76dab03..9a3e927 100644 (file)
@@ -117,6 +117,7 @@ class BitmapHandler extends ImageHandler {
                if ( !$this->normaliseParams( $image, $params ) ) {
                        return new TransformParameterError( $params );
                }
+
                # Create a parameter array to pass to the scaler
                $scalerParams = array(
                        # The size to which the image will be resized
@@ -187,7 +188,12 @@ class BitmapHandler extends ImageHandler {
                }
 
                # Transform functions and binaries need a FS source file
-               $scalerParams['srcPath'] = $image->getLocalRefPath();
+               $thumbnailSource = $image->getThumbnailSource( $params );
+
+               $scalerParams['srcPath'] = $thumbnailSource['path'];
+               $scalerParams['srcWidth'] = $thumbnailSource['width'];
+               $scalerParams['srcHeight'] = $thumbnailSource['height'];
+
                if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
                        wfDebugLog( 'thumbnail',
                                sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
@@ -354,7 +360,17 @@ class BitmapHandler extends ImageHandler {
                                }
                        }
                } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
-                       $animation_post = array( '-layers', 'merge' );
+                       // Before merging layers, we need to set the background
+                       // to be transparent to preserve alpha, as -layers merge
+                       // merges all layers on to a canvas filled with the
+                       // background colour. After merging we reset the background
+                       // to be white for the default background colour setting
+                       // in the PNG image (which is used in old IE)
+                       $animation_post = array(
+                               '-background', 'transparent',
+                               '-layers', 'merge',
+                               '-background', 'white',
+                       );
                        wfSuppressWarnings();
                        $xcfMeta = unserialize( $image->getMetadata() );
                        wfRestoreWarnings();
@@ -858,7 +874,7 @@ class BitmapHandler extends ImageHandler {
        }
 
        /**
-        * Rerurns whether the file needs to be rendered. Returns true if the
+        * Returns whether the file needs to be rendered. Returns true if the
         * file requires rotation and we are able to rotate it.
         *
         * @param File $file
index 8a12e7e..6dd0453 100644 (file)
@@ -269,4 +269,20 @@ abstract class ImageHandler extends MediaHandler {
                                ->numParams( $file->getWidth(), $file->getHeight() )->text();
                }
        }
+
+       public function sanitizeParamsForBucketing( $params ) {
+               $params = parent::sanitizeParamsForBucketing( $params );
+
+               // We unset the height parameters in order to let normaliseParams recalculate them
+               // Otherwise there might be a height discrepancy
+               if ( isset( $params['height'] ) ) {
+                       unset( $params['height'] );
+               }
+
+               if ( isset( $params['physicalHeight'] ) ) {
+                       unset( $params['physicalHeight'] );
+               }
+
+               return $params;
+       }
 }
index a0f7acb..918d4ae 100644 (file)
@@ -158,4 +158,19 @@ class JpegHandler extends ExifBitmapHandler {
                        return parent::rotate( $file, $params );
                }
        }
+
+       public function supportsBucketing() {
+               return true;
+       }
+
+       public function sanitizeParamsForBucketing( $params ) {
+               $params = parent::sanitizeParamsForBucketing( $params );
+
+               // Quality needs to be cleared for bucketing. Buckets need to be default quality
+               if ( isset( $params['quality'] ) ) {
+                       unset( $params['quality'] );
+               }
+
+               return $params;
+       }
 }
index f6717cd..2612685 100644 (file)
@@ -56,6 +56,7 @@ abstract class MediaHandler {
                if ( !isset( self::$handlers[$class] ) ) {
                        self::$handlers[$class] = new $class;
                        if ( !self::$handlers[$class]->isEnabled() ) {
+                               wfDebug( __METHOD__ . ": $class is not enabled\n" );
                                self::$handlers[$class] = false;
                        }
                }
@@ -63,6 +64,13 @@ abstract class MediaHandler {
                return self::$handlers[$class];
        }
 
+       /**
+        * Resets all static caches
+        */
+       public static function resetCache() {
+               self::$handlers = array();
+       }
+
        /**
         * Get an associative array mapping magic word IDs to parameter names.
         * Will be used by the parser to identify parameters.
@@ -831,4 +839,24 @@ abstract class MediaHandler {
        public function isExpensiveToThumbnail( $file ) {
                return false;
        }
+
+       /**
+        * Returns whether or not this handler supports the chained generation of thumbnails according
+        * to buckets
+        * @return boolean
+        * @since  1.24
+        */
+       public function supportsBucketing() {
+               return false;
+       }
+
+       /**
+        * Returns a normalised params array for which parameters have been cleaned up for bucketing
+        * purposes
+        * @param array $params
+        * @return array
+        */
+       public function sanitizeParamsForBucketing( $params ) {
+               return $params;
+       }
 }
index 968db10..d879c12 100644 (file)
@@ -173,4 +173,8 @@ class PNGHandler extends BitmapHandler {
 
                return $wgLang->commaList( $info );
        }
+
+       public function supportsBucketing() {
+               return true;
+       }
 }
index f7dfe46..8700c8c 100644 (file)
@@ -269,10 +269,4 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                wfProfileOut( __METHOD__ );
                return $this->checkResult( false, $result );
        }
-
-
-       /* NOTE: there is no cas() method here because it is currently not supported
-        * by the BagOStuff interface and other BagOStuff subclasses, such as
-        * SqlBagOStuff.
-        */
 }
index 0009999..633b34a 100644 (file)
@@ -119,11 +119,15 @@ class ObjectCache {
        /**
         * Factory function referenced from DefaultSettings.php for CACHE_ACCEL.
         *
+        * This will look for any APC style server-local cache.
+        * A fallback cache can be specified if none is found.
+        *
         * @param array $params
+        * @param int|string $fallback Fallback cache, e.g. (CACHE_NONE, "hash") (since 1.24)
         * @throws MWException
         * @return BagOStuff
         */
-       static function newAccelerator( $params ) {
+       static function newAccelerator( $params, $fallback = null ) {
                if ( function_exists( 'apc_fetch' ) ) {
                        $id = 'apc';
                } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
@@ -131,6 +135,9 @@ class ObjectCache {
                } elseif ( function_exists( 'wincache_ucache_get' ) ) {
                        $id = 'wincache';
                } else {
+                       if ( $fallback ) {
+                               return self::newFromId( $fallback );
+                       }
                        throw new MWException( "CACHE_ACCEL requested but no suitable object " .
                                "cache is present. You may want to install APC." );
                }
index 483f8b9..0c91dab 100644 (file)
@@ -27,9 +27,6 @@
  * @ingroup Cache
  */
 class SqlBagOStuff extends BagOStuff {
-       /** @var LoadBalancer */
-       protected $lb;
-
        protected $serverInfos;
 
        /** @var array */
@@ -146,14 +143,12 @@ class SqlBagOStuff extends BagOStuff {
                                $db = DatabaseBase::factory( $type, $info );
                                $db->clearFlag( DBO_TRX );
                        } else {
-                               /*
-                                * We must keep a separate connection to MySQL in order to avoid deadlocks
-                                * However, SQLite has an opposite behavior. And PostgreSQL needs to know
-                                * if we are in transaction or no
-                                */
+                               // We must keep a separate connection to MySQL in order to avoid deadlocks
+                               // However, SQLite has an opposite behavior.
+                               // @TODO: get this trick to work on PostgreSQL too
                                if ( wfGetDB( DB_MASTER )->getType() == 'mysql' ) {
-                                       $this->lb = wfGetLBFactory()->newMainLB();
-                                       $db = $this->lb->getConnection( DB_MASTER );
+                                       $lb = wfGetLBFactory()->newMainLB();
+                                       $db = $lb->getConnection( DB_MASTER );
                                        $db->clearFlag( DBO_TRX ); // auto-commit mode
                                } else {
                                        $db = wfGetDB( DB_MASTER );
@@ -243,7 +238,12 @@ class SqlBagOStuff extends BagOStuff {
                                        $res = $db->select( $tableName,
                                                array( 'keyname', 'value', 'exptime' ),
                                                array( 'keyname' => $tableKeys ),
-                                               __METHOD__ );
+                                               __METHOD__,
+                                               // Approximate write-on-the-fly BagOStuff API via blocking.
+                                               // This approximation fails if a ROLLBACK happens (which is rare).
+                                               // We do not want to flush the TRX as that can break callers.
+                                               $db->trxLevel() ? array( 'LOCK IN SHARE MODE' ) : array()
+                                       );
                                        foreach ( $res as $row ) {
                                                $row->serverIndex = $serverIndex;
                                                $row->tableName = $tableName;
@@ -263,13 +263,11 @@ class SqlBagOStuff extends BagOStuff {
                                        $db = $this->getDB( $row->serverIndex );
                                        if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
                                                $this->debug( "get: key has expired, deleting" );
-                                               $db->commit( __METHOD__, 'flush' );
                                                # Put the expiry time in the WHERE condition to avoid deleting a
                                                # newly-inserted value
                                                $db->delete( $row->tableName,
                                                        array( 'keyname' => $key, 'exptime' => $row->exptime ),
                                                        __METHOD__ );
-                                               $db->commit( __METHOD__, 'flush' );
                                        } else { // HIT
                                                $values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) );
                                        }
@@ -332,14 +330,12 @@ class SqlBagOStuff extends BagOStuff {
                                }
 
                                try {
-                                       $db->commit( __METHOD__, 'flush' );
                                        $db->replace(
                                                $tableName,
                                                array( 'keyname' ),
                                                $rows,
                                                __METHOD__
                                        );
-                                       $db->commit( __METHOD__, 'flush' );
                                } catch ( DBError $e ) {
                                        $this->handleWriteError( $e, $serverIndex );
                                        $result = false;
@@ -379,7 +375,6 @@ class SqlBagOStuff extends BagOStuff {
 
                                $encExpiry = $db->timestamp( $exptime );
                        }
-                       $db->commit( __METHOD__, 'flush' );
                        // (bug 24425) use a replace if the db supports it instead of
                        // delete/insert to avoid clashes with conflicting keynames
                        $db->replace(
@@ -390,7 +385,6 @@ class SqlBagOStuff extends BagOStuff {
                                        'value' => $db->encodeBlob( $this->serialize( $value ) ),
                                        'exptime' => $encExpiry
                                ), __METHOD__ );
-                       $db->commit( __METHOD__, 'flush' );
                } catch ( DBError $e ) {
                        $this->handleWriteError( $e, $serverIndex );
                        return false;
@@ -424,7 +418,6 @@ class SqlBagOStuff extends BagOStuff {
                                }
                                $encExpiry = $db->timestamp( $exptime );
                        }
-                       $db->commit( __METHOD__, 'flush' );
                        // (bug 24425) use a replace if the db supports it instead of
                        // delete/insert to avoid clashes with conflicting keynames
                        $db->update(
@@ -440,7 +433,6 @@ class SqlBagOStuff extends BagOStuff {
                                ),
                                __METHOD__
                        );
-                       $db->commit( __METHOD__, 'flush' );
                } catch ( DBQueryError $e ) {
                        $this->handleWriteError( $e, $serverIndex );
 
@@ -459,12 +451,10 @@ class SqlBagOStuff extends BagOStuff {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
                try {
                        $db = $this->getDB( $serverIndex );
-                       $db->commit( __METHOD__, 'flush' );
                        $db->delete(
                                $tableName,
                                array( 'keyname' => $key ),
                                __METHOD__ );
-                       $db->commit( __METHOD__, 'flush' );
                } catch ( DBError $e ) {
                        $this->handleWriteError( $e, $serverIndex );
                        return false;
@@ -483,7 +473,6 @@ class SqlBagOStuff extends BagOStuff {
                try {
                        $db = $this->getDB( $serverIndex );
                        $step = intval( $step );
-                       $db->commit( __METHOD__, 'flush' );
                        $row = $db->selectRow(
                                $tableName,
                                array( 'value', 'exptime' ),
@@ -492,14 +481,12 @@ class SqlBagOStuff extends BagOStuff {
                                array( 'FOR UPDATE' ) );
                        if ( $row === false ) {
                                // Missing
-                               $db->commit( __METHOD__, 'flush' );
 
                                return null;
                        }
                        $db->delete( $tableName, array( 'keyname' => $key ), __METHOD__ );
                        if ( $this->isExpired( $db, $row->exptime ) ) {
                                // Expired, do not reinsert
-                               $db->commit( __METHOD__, 'flush' );
 
                                return null;
                        }
@@ -517,7 +504,6 @@ class SqlBagOStuff extends BagOStuff {
                                // Race condition. See bug 28611
                                $newValue = null;
                        }
-                       $db->commit( __METHOD__, 'flush' );
                } catch ( DBError $e ) {
                        $this->handleWriteError( $e, $serverIndex );
                        return null;
@@ -608,7 +594,6 @@ class SqlBagOStuff extends BagOStuff {
                                                        $maxExpTime = $row->exptime;
                                                }
 
-                                               $db->commit( __METHOD__, 'flush' );
                                                $db->delete(
                                                        $this->getTableNameByShard( $i ),
                                                        array(
@@ -617,7 +602,6 @@ class SqlBagOStuff extends BagOStuff {
                                                                'keyname' => $keys
                                                        ),
                                                        __METHOD__ );
-                                               $db->commit( __METHOD__, 'flush' );
 
                                                if ( $progressCallback ) {
                                                        if ( intval( $totalSeconds ) === 0 ) {
@@ -650,9 +634,7 @@ class SqlBagOStuff extends BagOStuff {
                        try {
                                $db = $this->getDB( $serverIndex );
                                for ( $i = 0; $i < $this->shards; $i++ ) {
-                                       $db->commit( __METHOD__, 'flush' );
                                        $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
-                                       $db->commit( __METHOD__, 'flush' );
                                }
                        } catch ( DBError $e ) {
                                $this->handleWriteError( $e, $serverIndex );
@@ -780,12 +762,10 @@ class SqlBagOStuff extends BagOStuff {
                        }
 
                        for ( $i = 0; $i < $this->shards; $i++ ) {
-                               $db->commit( __METHOD__, 'flush' );
                                $db->query(
                                        'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) .
                                        ' LIKE ' . $db->tableName( 'objectcache' ),
                                        __METHOD__ );
-                               $db->commit( __METHOD__, 'flush' );
                        }
                }
        }
index 0e989d3..2f27826 100644 (file)
@@ -480,7 +480,7 @@ class Article implements Page {
         * page of the given title.
         */
        public function view() {
-               global $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
+               global $wgUseFileCache, $wgUseETag, $wgDebugToolbar, $wgMaxRedirects;
 
                wfProfileIn( __METHOD__ );
 
@@ -542,8 +542,31 @@ class Article implements Page {
                                $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) );
                        }
 
+                       # Use the greatest of the page's timestamp or the timestamp of any
+                       # redirect in the chain (bug 67849)
+                       $timestamp = $this->mPage->getTouched();
+                       if ( isset( $this->mRedirectedFrom ) ) {
+                               $timestamp = max( $timestamp, $this->mRedirectedFrom->getTouched() );
+
+                               # If there can be more than one redirect in the chain, we have
+                               # to go through the whole chain too in case an intermediate
+                               # redirect was changed.
+                               if ( $wgMaxRedirects > 1 ) {
+                                       $titles = Revision::newFromTitle( $this->mRedirectedFrom )
+                                               ->getContent( Revision::FOR_THIS_USER, $user )
+                                               ->getRedirectChain();
+                                       $thisTitle = $this->getTitle();
+                                       foreach ( $titles as $title ) {
+                                               if ( Title::compare( $title, $thisTitle ) === 0 ) {
+                                                       break;
+                                               }
+                                               $timestamp = max( $timestamp, $title->getTouched() );
+                                       }
+                               }
+                       }
+
                        # Is it client cached?
-                       if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) {
+                       if ( $outputPage->checkLastModified( $timestamp ) ) {
                                wfDebug( __METHOD__ . ": done 304\n" );
                                wfProfileOut( __METHOD__ );
 
index 34f15c3..87cc7ba 100644 (file)
@@ -40,12 +40,6 @@ class WikiFilePage extends WikiPage {
                $this->mRepo = null;
        }
 
-       public function getActionOverrides() {
-               $overrides = parent::getActionOverrides();
-               $overrides['revert'] = 'RevertFileAction';
-               return $overrides;
-       }
-
        /**
         * @param File $file
         */
index 7c412ea..31f0ed0 100644 (file)
@@ -2047,7 +2047,9 @@ class WikiPage implements Page, IDBAccessObject {
                wfRunHooks( 'PageContentSaveComplete', $hook_args );
 
                // Promote user to any groups they meet the criteria for
-               $user->addAutopromoteOnceGroups( 'onEdit' );
+               $dbw->onTransactionIdle( function() use ( $user ) {
+                       $user->addAutopromoteOnceGroups( 'onEdit' );
+               } );
 
                wfProfileOut( __METHOD__ );
                return $status;
@@ -3503,17 +3505,6 @@ class WikiPage implements Page, IDBAccessObject {
                return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
        }
 
-       /**
-        * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
-        *
-        * @deprecated since 1.19; use Title::isBigDeletion() instead.
-        * @return bool
-        */
-       public function isBigDeletion() {
-               wfDeprecated( __METHOD__, '1.19' );
-               return $this->mTitle->isBigDeletion();
-       }
-
        /**
         * Get the  approximate revision count of this page.
         *
index cde7a2d..faff0e7 100644 (file)
@@ -365,9 +365,15 @@ class CoreParserFunctions {
         * @param string $text Desired title text
         * @return string
         */
-       static function displaytitle( $parser, $text = '' ) {
+       static function displaytitle( $parser, $text = '', $uarg = '' ) {
                global $wgRestrictDisplayTitle;
 
+               static $magicWords = null;
+               if ( is_null( $magicWords ) ) {
+                       $magicWords = new MagicWordArray( array( 'displaytitle_noerror', 'displaytitle_noreplace' ) );
+               }
+               $arg = $magicWords->matchStartToEnd( $uarg );
+
                // parse a limited subset of wiki markup (just the single quote items)
                $text = $parser->doQuotes( $text );
 
@@ -378,7 +384,7 @@ class CoreParserFunctions {
                // list of disallowed tags for DISPLAYTITLE
                // these will be escaped even though they are allowed in normal wiki text
                $bad = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr',
-                       'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rp', 'br' );
+                       'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rtc', 'rp', 'br' );
 
                // disallow some styles that could be used to bypass $wgRestrictDisplayTitle
                if ( $wgRestrictDisplayTitle ) {
@@ -413,13 +419,25 @@ class CoreParserFunctions {
                ) );
                $title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
 
-               if ( !$wgRestrictDisplayTitle ) {
-                       $parser->mOutput->setDisplayTitle( $text );
-               } elseif ( $title instanceof Title
+               if ( !$wgRestrictDisplayTitle ||
+                       ( $title instanceof Title
                        && !$title->hasFragment()
-                       && $title->equals( $parser->mTitle )
+                       && $title->equals( $parser->mTitle ) )
                ) {
-                       $parser->mOutput->setDisplayTitle( $text );
+                       $old = $parser->mOutput->getProperty( 'displaytitle' );
+                       if ( $old === false || $arg !== 'displaytitle_noreplace' ) {
+                               $parser->mOutput->setDisplayTitle( $text );
+                       }
+                       if ( $old !== false && $old !== $text && !$arg ) {
+                               $converter = $parser->getConverterLanguage()->getConverter();
+                               return '<span class="error">' .
+                                       wfMessage( 'duplicate-displaytitle',
+                                               // Message should be parsed, but these params should only be escaped.
+                                               $converter->markNoConversion( wfEscapeWikiText( $old ) ),
+                                               $converter->markNoConversion( wfEscapeWikiText( $text ) )
+                                       )->inContentLanguage()->text() .
+                                       '</span>';
+                       }
                }
 
                return '';
diff --git a/includes/parser/MWTidy.php b/includes/parser/MWTidy.php
new file mode 100644 (file)
index 0000000..f7fe5a8
--- /dev/null
@@ -0,0 +1,289 @@
+<?php
+/**
+ * HTML validation and correction
+ *
+ * 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 Parser
+ */
+
+/**
+ * Class used to hide mw:editsection tokens from Tidy so that it doesn't break them
+ * or break on them. This is a bit of a hack for now, but hopefully in the future
+ * we may create a real postprocessor or something that will replace this.
+ * It's called wrapper because for now it basically takes over MWTidy::tidy's task
+ * of wrapping the text in a xhtml block
+ *
+ * This re-uses some of the parser's UNIQ tricks, though some of it is private so it's
+ * duplicated. Perhaps we should create an abstract marker hiding class.
+ *
+ * @ingroup Parser
+ */
+class MWTidyWrapper {
+
+       /**
+        * @var ReplacementArray
+        */
+       protected $mTokens;
+
+       protected $mUniqPrefix;
+
+       protected $mMarkerIndex;
+
+       public function __construct() {
+               $this->mTokens = null;
+               $this->mUniqPrefix = null;
+       }
+
+       /**
+        * @param string $text
+        * @return string
+        */
+       public function getWrapped( $text ) {
+               $this->mTokens = new ReplacementArray;
+               $this->mUniqPrefix = "\x7fUNIQ" .
+                       dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) );
+               $this->mMarkerIndex = 0;
+
+               // Replace <mw:editsection> elements with placeholders
+               $wrappedtext = preg_replace_callback( ParserOutput::EDITSECTION_REGEX,
+                       array( &$this, 'replaceCallback' ), $text );
+               // ...and <mw:toc> markers
+               $wrappedtext = preg_replace_callback( '/\<\\/?mw:toc\>/',
+                       array( &$this, 'replaceCallback' ), $wrappedtext );
+
+               // Modify inline Microdata <link> and <meta> elements so they say <html-link> and <html-meta> so
+               // we can trick Tidy into not stripping them out by including them in tidy's new-empty-tags config
+               $wrappedtext = preg_replace( '!<(link|meta)([^>]*?)(/{0,1}>)!', '<html-$1$2$3', $wrappedtext );
+
+               // Wrap the whole thing in a doctype and body for Tidy.
+               $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' .
+                       ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>' .
+                       '<head><title>test</title></head><body>' . $wrappedtext . '</body></html>';
+
+               return $wrappedtext;
+       }
+
+       /**
+        * @param array $m
+        *
+        * @return string
+        */
+       function replaceCallback( $m ) {
+               $marker = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}" . Parser::MARKER_SUFFIX;
+               $this->mMarkerIndex++;
+               $this->mTokens->setPair( $marker, $m[0] );
+               return $marker;
+       }
+
+       /**
+        * @param string $text
+        * @return string
+        */
+       public function postprocess( $text ) {
+               // Revert <html-{link,meta}> back to <{link,meta}>
+               $text = preg_replace( '!<html-(link|meta)([^>]*?)(/{0,1}>)!', '<$1$2$3', $text );
+
+               // Restore the contents of placeholder tokens
+               $text = $this->mTokens->replace( $text );
+
+               return $text;
+       }
+
+}
+
+/**
+ * Class to interact with HTML tidy
+ *
+ * Either the external tidy program or the in-process tidy extension
+ * will be used depending on availability. Override the default
+ * $wgTidyInternal setting to disable the internal if it's not working.
+ *
+ * @ingroup Parser
+ */
+class MWTidy {
+       /**
+        * Interface with html tidy, used if $wgUseTidy = true.
+        * If tidy isn't able to correct the markup, the original will be
+        * returned in all its glory with a warning comment appended.
+        *
+        * @param string $text Hideous HTML input
+        * @return string Corrected HTML output
+        */
+       public static function tidy( $text ) {
+               global $wgTidyInternal;
+
+               $wrapper = new MWTidyWrapper;
+               $wrappedtext = $wrapper->getWrapped( $text );
+
+               $retVal = null;
+               if ( $wgTidyInternal ) {
+                       $correctedtext = self::execInternalTidy( $wrappedtext, false, $retVal );
+               } else {
+                       $correctedtext = self::execExternalTidy( $wrappedtext, false, $retVal );
+               }
+
+               if ( $retVal < 0 ) {
+                       wfDebug( "Possible tidy configuration error!\n" );
+                       return $text . "\n<!-- Tidy was unable to run -->\n";
+               } elseif ( is_null( $correctedtext ) ) {
+                       wfDebug( "Tidy error detected!\n" );
+                       return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
+               }
+
+               $correctedtext = $wrapper->postprocess( $correctedtext ); // restore any hidden tokens
+
+               return $correctedtext;
+       }
+
+       /**
+        * Check HTML for errors, used if $wgValidateAllHtml = true.
+        *
+        * @param string $text
+        * @param string &$errorStr Return the error string
+        * @return bool Whether the HTML is valid
+        */
+       public static function checkErrors( $text, &$errorStr = null ) {
+               global $wgTidyInternal;
+
+               $retval = 0;
+               if ( $wgTidyInternal ) {
+                       $errorStr = self::execInternalTidy( $text, true, $retval );
+               } else {
+                       $errorStr = self::execExternalTidy( $text, true, $retval );
+               }
+
+               return ( $retval < 0 && $errorStr == '' ) || $retval == 0;
+       }
+
+       /**
+        * Spawn an external HTML tidy process and get corrected markup back from it.
+        * Also called in OutputHandler.php for full page validation
+        *
+        * @param string $text HTML to check
+        * @param bool $stderr Whether to read result from STDERR rather than STDOUT
+        * @param int &$retval Exit code (-1 on internal error)
+        * @return string|null
+        */
+       private static function execExternalTidy( $text, $stderr = false, &$retval = null ) {
+               global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
+               wfProfileIn( __METHOD__ );
+
+               $cleansource = '';
+               $opts = ' -utf8';
+
+               if ( $stderr ) {
+                       $descriptorspec = array(
+                               0 => array( 'pipe', 'r' ),
+                               1 => array( 'file', wfGetNull(), 'a' ),
+                               2 => array( 'pipe', 'w' )
+                       );
+               } else {
+                       $descriptorspec = array(
+                               0 => array( 'pipe', 'r' ),
+                               1 => array( 'pipe', 'w' ),
+                               2 => array( 'file', wfGetNull(), 'a' )
+                       );
+               }
+
+               $readpipe = $stderr ? 2 : 1;
+               $pipes = array();
+
+               $process = proc_open(
+                       "$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes );
+
+               //NOTE: At least on linux, the process will be created even if tidy is not installed.
+               //      This means that missing tidy will be treated as a validation failure.
+
+               if ( is_resource( $process ) ) {
+                       // Theoretically, this style of communication could cause a deadlock
+                       // here. If the stdout buffer fills up, then writes to stdin could
+                       // block. This doesn't appear to happen with tidy, because tidy only
+                       // writes to stdout after it's finished reading from stdin. Search
+                       // for tidyParseStdin and tidySaveStdout in console/tidy.c
+                       fwrite( $pipes[0], $text );
+                       fclose( $pipes[0] );
+                       while ( !feof( $pipes[$readpipe] ) ) {
+                               $cleansource .= fgets( $pipes[$readpipe], 1024 );
+                       }
+                       fclose( $pipes[$readpipe] );
+                       $retval = proc_close( $process );
+               } else {
+                       wfWarn( "Unable to start external tidy process" );
+                       $retval = -1;
+               }
+
+               if ( !$stderr && $cleansource == '' && $text != '' ) {
+                       // Some kind of error happened, so we couldn't get the corrected text.
+                       // Just give up; we'll use the source text and append a warning.
+                       $cleansource = null;
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $cleansource;
+       }
+
+       /**
+        * Use the HTML tidy extension to use the tidy library in-process,
+        * saving the overhead of spawning a new process.
+        *
+        * @param string $text HTML to check
+        * @param bool $stderr Whether to read result from error status instead of output
+        * @param int &$retval Exit code (-1 on internal error)
+        * @return string|null
+        */
+       private static function execInternalTidy( $text, $stderr = false, &$retval = null ) {
+               global $wgTidyConf, $wgDebugTidy;
+               wfProfileIn( __METHOD__ );
+
+               if ( !class_exists( 'tidy' ) ) {
+                       wfWarn( "Unable to load internal tidy class." );
+                       $retval = -1;
+
+                       wfProfileOut( __METHOD__ );
+                       return null;
+               }
+
+               $tidy = new tidy;
+               $tidy->parseString( $text, $wgTidyConf, 'utf8' );
+
+               if ( $stderr ) {
+                       $retval = $tidy->getStatus();
+
+                       wfProfileOut( __METHOD__ );
+                       return $tidy->errorBuffer;
+               }
+
+               $tidy->cleanRepair();
+               $retval = $tidy->getStatus();
+               if ( $retval == 2 ) {
+                       // 2 is magic number for fatal error
+                       // http://www.php.net/manual/en/function.tidy-get-status.php
+                       $cleansource = null;
+               } else {
+                       $cleansource = tidy_get_output( $tidy );
+                       if ( $wgDebugTidy && $retval > 0 ) {
+                               $cleansource .= "<!--\nTidy reports:\n" .
+                                       str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
+                                       "\n-->";
+                       }
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $cleansource;
+       }
+}
index 90617b3..2cd208c 100644 (file)
@@ -422,11 +422,6 @@ class ParserOptions {
                return wfSetVar( $this->mTidy, $x );
        }
 
-       /** @deprecated since 1.19 */
-       function setSkin( $x ) {
-               wfDeprecated( __METHOD__, '1.19' );
-       }
-
        function setInterfaceMessage( $x ) {
                return wfSetVar( $this->mInterfaceMessage, $x );
        }
diff --git a/includes/parser/Tidy.php b/includes/parser/Tidy.php
deleted file mode 100644 (file)
index f7fe5a8..0000000
+++ /dev/null
@@ -1,289 +0,0 @@
-<?php
-/**
- * HTML validation and correction
- *
- * 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 Parser
- */
-
-/**
- * Class used to hide mw:editsection tokens from Tidy so that it doesn't break them
- * or break on them. This is a bit of a hack for now, but hopefully in the future
- * we may create a real postprocessor or something that will replace this.
- * It's called wrapper because for now it basically takes over MWTidy::tidy's task
- * of wrapping the text in a xhtml block
- *
- * This re-uses some of the parser's UNIQ tricks, though some of it is private so it's
- * duplicated. Perhaps we should create an abstract marker hiding class.
- *
- * @ingroup Parser
- */
-class MWTidyWrapper {
-
-       /**
-        * @var ReplacementArray
-        */
-       protected $mTokens;
-
-       protected $mUniqPrefix;
-
-       protected $mMarkerIndex;
-
-       public function __construct() {
-               $this->mTokens = null;
-               $this->mUniqPrefix = null;
-       }
-
-       /**
-        * @param string $text
-        * @return string
-        */
-       public function getWrapped( $text ) {
-               $this->mTokens = new ReplacementArray;
-               $this->mUniqPrefix = "\x7fUNIQ" .
-                       dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) );
-               $this->mMarkerIndex = 0;
-
-               // Replace <mw:editsection> elements with placeholders
-               $wrappedtext = preg_replace_callback( ParserOutput::EDITSECTION_REGEX,
-                       array( &$this, 'replaceCallback' ), $text );
-               // ...and <mw:toc> markers
-               $wrappedtext = preg_replace_callback( '/\<\\/?mw:toc\>/',
-                       array( &$this, 'replaceCallback' ), $wrappedtext );
-
-               // Modify inline Microdata <link> and <meta> elements so they say <html-link> and <html-meta> so
-               // we can trick Tidy into not stripping them out by including them in tidy's new-empty-tags config
-               $wrappedtext = preg_replace( '!<(link|meta)([^>]*?)(/{0,1}>)!', '<html-$1$2$3', $wrappedtext );
-
-               // Wrap the whole thing in a doctype and body for Tidy.
-               $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' .
-                       ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>' .
-                       '<head><title>test</title></head><body>' . $wrappedtext . '</body></html>';
-
-               return $wrappedtext;
-       }
-
-       /**
-        * @param array $m
-        *
-        * @return string
-        */
-       function replaceCallback( $m ) {
-               $marker = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}" . Parser::MARKER_SUFFIX;
-               $this->mMarkerIndex++;
-               $this->mTokens->setPair( $marker, $m[0] );
-               return $marker;
-       }
-
-       /**
-        * @param string $text
-        * @return string
-        */
-       public function postprocess( $text ) {
-               // Revert <html-{link,meta}> back to <{link,meta}>
-               $text = preg_replace( '!<html-(link|meta)([^>]*?)(/{0,1}>)!', '<$1$2$3', $text );
-
-               // Restore the contents of placeholder tokens
-               $text = $this->mTokens->replace( $text );
-
-               return $text;
-       }
-
-}
-
-/**
- * Class to interact with HTML tidy
- *
- * Either the external tidy program or the in-process tidy extension
- * will be used depending on availability. Override the default
- * $wgTidyInternal setting to disable the internal if it's not working.
- *
- * @ingroup Parser
- */
-class MWTidy {
-       /**
-        * Interface with html tidy, used if $wgUseTidy = true.
-        * If tidy isn't able to correct the markup, the original will be
-        * returned in all its glory with a warning comment appended.
-        *
-        * @param string $text Hideous HTML input
-        * @return string Corrected HTML output
-        */
-       public static function tidy( $text ) {
-               global $wgTidyInternal;
-
-               $wrapper = new MWTidyWrapper;
-               $wrappedtext = $wrapper->getWrapped( $text );
-
-               $retVal = null;
-               if ( $wgTidyInternal ) {
-                       $correctedtext = self::execInternalTidy( $wrappedtext, false, $retVal );
-               } else {
-                       $correctedtext = self::execExternalTidy( $wrappedtext, false, $retVal );
-               }
-
-               if ( $retVal < 0 ) {
-                       wfDebug( "Possible tidy configuration error!\n" );
-                       return $text . "\n<!-- Tidy was unable to run -->\n";
-               } elseif ( is_null( $correctedtext ) ) {
-                       wfDebug( "Tidy error detected!\n" );
-                       return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
-               }
-
-               $correctedtext = $wrapper->postprocess( $correctedtext ); // restore any hidden tokens
-
-               return $correctedtext;
-       }
-
-       /**
-        * Check HTML for errors, used if $wgValidateAllHtml = true.
-        *
-        * @param string $text
-        * @param string &$errorStr Return the error string
-        * @return bool Whether the HTML is valid
-        */
-       public static function checkErrors( $text, &$errorStr = null ) {
-               global $wgTidyInternal;
-
-               $retval = 0;
-               if ( $wgTidyInternal ) {
-                       $errorStr = self::execInternalTidy( $text, true, $retval );
-               } else {
-                       $errorStr = self::execExternalTidy( $text, true, $retval );
-               }
-
-               return ( $retval < 0 && $errorStr == '' ) || $retval == 0;
-       }
-
-       /**
-        * Spawn an external HTML tidy process and get corrected markup back from it.
-        * Also called in OutputHandler.php for full page validation
-        *
-        * @param string $text HTML to check
-        * @param bool $stderr Whether to read result from STDERR rather than STDOUT
-        * @param int &$retval Exit code (-1 on internal error)
-        * @return string|null
-        */
-       private static function execExternalTidy( $text, $stderr = false, &$retval = null ) {
-               global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
-               wfProfileIn( __METHOD__ );
-
-               $cleansource = '';
-               $opts = ' -utf8';
-
-               if ( $stderr ) {
-                       $descriptorspec = array(
-                               0 => array( 'pipe', 'r' ),
-                               1 => array( 'file', wfGetNull(), 'a' ),
-                               2 => array( 'pipe', 'w' )
-                       );
-               } else {
-                       $descriptorspec = array(
-                               0 => array( 'pipe', 'r' ),
-                               1 => array( 'pipe', 'w' ),
-                               2 => array( 'file', wfGetNull(), 'a' )
-                       );
-               }
-
-               $readpipe = $stderr ? 2 : 1;
-               $pipes = array();
-
-               $process = proc_open(
-                       "$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes );
-
-               //NOTE: At least on linux, the process will be created even if tidy is not installed.
-               //      This means that missing tidy will be treated as a validation failure.
-
-               if ( is_resource( $process ) ) {
-                       // Theoretically, this style of communication could cause a deadlock
-                       // here. If the stdout buffer fills up, then writes to stdin could
-                       // block. This doesn't appear to happen with tidy, because tidy only
-                       // writes to stdout after it's finished reading from stdin. Search
-                       // for tidyParseStdin and tidySaveStdout in console/tidy.c
-                       fwrite( $pipes[0], $text );
-                       fclose( $pipes[0] );
-                       while ( !feof( $pipes[$readpipe] ) ) {
-                               $cleansource .= fgets( $pipes[$readpipe], 1024 );
-                       }
-                       fclose( $pipes[$readpipe] );
-                       $retval = proc_close( $process );
-               } else {
-                       wfWarn( "Unable to start external tidy process" );
-                       $retval = -1;
-               }
-
-               if ( !$stderr && $cleansource == '' && $text != '' ) {
-                       // Some kind of error happened, so we couldn't get the corrected text.
-                       // Just give up; we'll use the source text and append a warning.
-                       $cleansource = null;
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $cleansource;
-       }
-
-       /**
-        * Use the HTML tidy extension to use the tidy library in-process,
-        * saving the overhead of spawning a new process.
-        *
-        * @param string $text HTML to check
-        * @param bool $stderr Whether to read result from error status instead of output
-        * @param int &$retval Exit code (-1 on internal error)
-        * @return string|null
-        */
-       private static function execInternalTidy( $text, $stderr = false, &$retval = null ) {
-               global $wgTidyConf, $wgDebugTidy;
-               wfProfileIn( __METHOD__ );
-
-               if ( !class_exists( 'tidy' ) ) {
-                       wfWarn( "Unable to load internal tidy class." );
-                       $retval = -1;
-
-                       wfProfileOut( __METHOD__ );
-                       return null;
-               }
-
-               $tidy = new tidy;
-               $tidy->parseString( $text, $wgTidyConf, 'utf8' );
-
-               if ( $stderr ) {
-                       $retval = $tidy->getStatus();
-
-                       wfProfileOut( __METHOD__ );
-                       return $tidy->errorBuffer;
-               }
-
-               $tidy->cleanRepair();
-               $retval = $tidy->getStatus();
-               if ( $retval == 2 ) {
-                       // 2 is magic number for fatal error
-                       // http://www.php.net/manual/en/function.tidy-get-status.php
-                       $cleansource = null;
-               } else {
-                       $cleansource = tidy_get_output( $tidy );
-                       if ( $wgDebugTidy && $retval > 0 ) {
-                               $cleansource .= "<!--\nTidy reports:\n" .
-                                       str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
-                                       "\n-->";
-                       }
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $cleansource;
-       }
-}
index 8a936c6..56eb0a0 100644 (file)
@@ -345,7 +345,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
        public static function getStartupModulesUrl( ResourceLoaderContext $context ) {
                // The core modules:
                $moduleNames = array( 'jquery', 'mediawiki' );
-               wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$moduleNames ), '1.23' );
 
                // Get the latest version
                $loader = $context->getResourceLoader();
index e7a09d7..8a08fd1 100644 (file)
@@ -885,6 +885,45 @@ class RevDelArchivedFileItem extends RevDelFileItem {
                }
                return $link;
        }
+
+       public function getApiData( ApiResult $result ) {
+               $file = $this->file;
+               $user = $this->list->getUser();
+               $ret = array(
+                       'title' => $this->list->title->getPrefixedText(),
+                       'timestamp' => wfTimestamp( TS_ISO_8601, $file->getTimestamp() ),
+                       'width' => $file->getWidth(),
+                       'height' => $file->getHeight(),
+                       'size' => $file->getSize(),
+               );
+               $ret += $file->isDeleted( Revision::DELETED_USER ) ? array( 'userhidden' => '' ) : array();
+               $ret += $file->isDeleted( Revision::DELETED_COMMENT ) ? array( 'commenthidden' => '' ) : array();
+               $ret += $this->isDeleted() ? array( 'contenthidden' => '' ) : array();
+               if ( $this->canViewContent() ) {
+                       $ret += array(
+                               'url' => SpecialPage::getTitleFor( 'Revisiondelete' )->getLinkURL(
+                                       array(
+                                               'target' => $this->list->title->getPrefixedText(),
+                                               'file' => $file->getKey(),
+                                               'token' => $user->getEditToken( $file->getKey() )
+                                       ),
+                                       false, PROTO_RELATIVE
+                               ),
+                       );
+               }
+               if ( $file->userCan( Revision::DELETED_USER, $user ) ) {
+                       $ret += array(
+                               'userid' => $file->getUser( 'id' ),
+                               'user' => $file->getUser( 'text' ),
+                       );
+               }
+               if ( $file->userCan( Revision::DELETED_COMMENT, $user ) ) {
+                       $ret += array(
+                               'comment' => $file->getRawDescription(),
+                       );
+               }
+               return $ret;
+       }
 }
 
 /**
index 8ff2640..581d8bc 100644 (file)
@@ -174,8 +174,6 @@ class SearchMySQL extends SearchDatabase {
        }
 
        protected function searchInternal( $term, $fulltext ) {
-               global $wgCountTotalSearchHits;
-
                // This seems out of place, why is this called with empty term?
                if ( trim( $term ) === '' ) {
                        return null;
@@ -189,19 +187,17 @@ class SearchMySQL extends SearchDatabase {
                );
 
                $total = null;
-               if ( $wgCountTotalSearchHits ) {
-                       $query = $this->getCountQuery( $filteredTerm, $fulltext );
-                       $totalResult = $this->db->select(
-                               $query['tables'], $query['fields'], $query['conds'],
-                               __METHOD__, $query['options'], $query['joins']
-                       );
-
-                       $row = $totalResult->fetchObject();
-                       if ( $row ) {
-                               $total = intval( $row->c );
-                       }
-                       $totalResult->free();
+               $query = $this->getCountQuery( $filteredTerm, $fulltext );
+               $totalResult = $this->db->select(
+                       $query['tables'], $query['fields'], $query['conds'],
+                       __METHOD__, $query['options'], $query['joins']
+               );
+
+               $row = $totalResult->fetchObject();
+               if ( $row ) {
+                       $total = intval( $row->c );
                }
+               $totalResult->free();
 
                return new SqlSearchResultSet( $resultSet, $this->searchTerms, $total );
        }
index f430dd0..698f93c 100644 (file)
@@ -173,7 +173,12 @@ class SqlSearchResultSet extends SearchResultSet {
        }
 
        function getTotalHits() {
-               return $this->totalHits;
+               if ( !is_null( $this->totalHits ) ) {
+                       return $this->totalHits;
+               } else {
+                       // Special:Search expects a number here.
+                       return $this->numRows();
+               }
        }
 }
 
index 8e820f3..05d5ca0 100644 (file)
@@ -164,7 +164,7 @@ class SearchSqlite extends SearchDatabase {
        }
 
        protected function searchInternal( $term, $fulltext ) {
-               global $wgCountTotalSearchHits, $wgContLang;
+               global $wgContLang;
 
                if ( !$this->fulltextSearchSupported() ) {
                        return null;
@@ -174,14 +174,12 @@ class SearchSqlite extends SearchDatabase {
                $resultSet = $this->db->query( $this->getQuery( $filteredTerm, $fulltext ) );
 
                $total = null;
-               if ( $wgCountTotalSearchHits ) {
-                       $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
-                       $row = $totalResult->fetchObject();
-                       if ( $row ) {
-                               $total = intval( $row->c );
-                       }
-                       $totalResult->free();
+               $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
+               $row = $totalResult->fetchObject();
+               if ( $row ) {
+                       $total = intval( $row->c );
                }
+               $totalResult->free();
 
                return new SqlSearchResultSet( $resultSet, $this->searchTerms, $total );
        }
index 8dc4b3c..e25955f 100644 (file)
@@ -410,7 +410,9 @@ class SpecialActiveUsers extends SpecialPage {
                        }
                        foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
                                $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
-                               wfWaitForSlaves();
+                               if ( !$dbw->trxLevel() ) {
+                                       wfWaitForSlaves();
+                               }
                        }
                }
 
index 8e17015..2ce45ac 100644 (file)
@@ -460,6 +460,19 @@ class ImageListPager extends TablePager {
                                        );
                                        $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
 
+                                       // Add delete links if allowed
+                                       // From https://github.com/Wikia/app/pull/3859
+                                       if ( $filePage->userCan( 'delete', $this->getUser() ) ) {
+                                               $deleteMsg = $this->msg( 'listfiles-delete' )->escaped();
+
+                                               $delete = Linker::linkKnown(
+                                                       $filePage, $deleteMsg, array(), array( 'action' => 'delete' )
+                                               );
+                                               $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
+
+                                               return "$link $download $delete";
+                                       }
+
                                        return "$link $download";
                                } else {
                                        return htmlspecialchars( $value );
index a27cf4c..eea1336 100644 (file)
@@ -133,7 +133,7 @@ class SpecialMergeHistory extends SpecialPage {
                if ( !$this->mTargetObj instanceof Title ) {
                        $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
                } elseif ( !$this->mTargetObj->exists() ) {
-                       $errors[] = $this->msg( 'mergehistory-no-source', array( 'parse' ),
+                       $errors[] = $this->msg( 'mergehistory-no-source',
                                wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
                        )->parseAsBlock();
                }
@@ -141,7 +141,7 @@ class SpecialMergeHistory extends SpecialPage {
                if ( !$this->mDestObj instanceof Title ) {
                        $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
                } elseif ( !$this->mDestObj->exists() ) {
-                       $errors[] = $this->msg( 'mergehistory-no-destination', array( 'parse' ),
+                       $errors[] = $this->msg( 'mergehistory-no-destination',
                                wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
                        )->parseAsBlock();
                }
index 0b53a20..15a7b59 100644 (file)
@@ -893,9 +893,7 @@ class SpecialSearch extends SpecialPage {
                        }
 
                        $rows[$subject] .=
-                               Xml::openElement(
-                                       'td', array( 'style' => 'white-space: nowrap' )
-                               ) .
+                               Xml::openElement( 'td' ) .
                                Xml::checkLabel(
                                        $name,
                                        "ns{$namespace}",
@@ -953,10 +951,7 @@ class SpecialSearch extends SpecialPage {
                }
 
                // Return final output
-               return Xml::openElement(
-                       'fieldset',
-                       array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
-               ) .
+               return Xml::openElement( 'fieldset', array( 'id' => 'mw-searchoptions' ) ) .
                        Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
                        Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
                        Html::element( 'div', array( 'id' => 'mw-search-togglebox' ) ) .
@@ -1102,7 +1097,7 @@ class SpecialSearch extends SpecialPage {
                                ->numParams( $resultsShown )
                                ->parse();
                        $out .= Xml::tags( 'div', array( 'class' => 'results-info' ), $top ) .
-                               Xml::element( 'div', array( 'style' => 'clear:both' ) );
+                               Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
                }
 
                return $out . $this->didYouMeanHtml;
index 797543f..96e4dbf 100644 (file)
@@ -83,7 +83,7 @@ class SpecialUnblock extends SpecialPage {
                        'Target' => array(
                                'type' => 'text',
                                'label-message' => 'ipaddressorusername',
-                               'tabindex' => '1',
+                               'autofocus' => true,
                                'size' => '45',
                                'required' => true,
                        ),
@@ -131,6 +131,9 @@ class SpecialUnblock extends SpecialPage {
                                                $fields['Target']['default'] = "#{$this->target}";
                                                break;
                                }
+                               // target is hidden, so the reason is the first element
+                               $fields['Target']['autofocus'] = false;
+                               $fields['Reason']['autofocus'] = true;
                        }
                } else {
                        $fields['Target']['default'] = $this->target;
index 976294b..fc18706 100644 (file)
@@ -788,6 +788,18 @@ class UploadForm extends HTMLForm {
                wfRunHooks( 'UploadFormInitDescriptor', array( &$descriptor ) );
                parent::__construct( $descriptor, $context, 'upload' );
 
+               # Add a link to edit MediaWik:Licenses
+               if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
+                       $licensesLink = Linker::link(
+                               Title::makeTitle( NS_MEDIAWIKI, 'Licenses' ),
+                               $this->msg( 'licenses-edit' )->escaped(),
+                               array(),
+                               array( 'action' => 'edit' )
+                       );
+                       $editLicenses = '<p class="mw-upload-editlicenses">' . $licensesLink . '</p>';
+                       $this->addFooterText( $editLicenses, 'description' );
+               }
+
                # Set some form properties
                $this->setSubmitText( $this->msg( 'uploadbtn' )->text() );
                $this->setSubmitName( 'wpUpload' );
index 576b625..28ad0f4 100644 (file)
@@ -513,7 +513,7 @@ class SpecialVersion extends SpecialPage {
                        );
 
                        array_walk( $tags, function ( &$value ) {
-                               $value = '&lt;' . htmlentities( $value ) . '&gt;';
+                               $value = '&lt;' . htmlspecialchars( $value ) . '&gt;';
                        } );
                        $out .= $this->listToText( $tags );
                } else {
@@ -650,6 +650,7 @@ class SpecialVersion extends SpecialPage {
 
                if ( isset( $extension['path'] ) ) {
                        global $IP;
+                       $extensionPath = dirname( $extension['path'] );
                        if ( $this->coreId == '' ) {
                                wfDebug( 'Looking up core head id' );
                                $coreHeadSHA1 = self::getGitHeadSha1( $IP );
@@ -668,7 +669,6 @@ class SpecialVersion extends SpecialPage {
 
                        if ( !$vcsVersion ) {
                                wfDebug( "Getting VCS info for extension $extensionName" );
-                               $extensionPath = dirname( $extension['path'] );
                                $gitInfo = new GitInfo( $extensionPath );
                                $vcsVersion = $gitInfo->getHeadSHA1();
                                if ( $vcsVersion !== false ) {
index 7d439fa..ea526de 100644 (file)
@@ -49,23 +49,44 @@ class WantedFilesPage extends WantedQueryPage {
                        $category = false;
                }
 
+               $noForeign = '';
+               if ( !$this->likelyToHaveFalsePositives() ) {
+                       // Additional messages for grep:
+                       // wantedfiletext-cat-noforeign, wantedfiletext-nocat
+                       $noForeign = '-noforeign';
+               }
+
                if ( $category ) {
                        return $this
-                               ->msg( 'wantedfiletext-cat' )
+                               ->msg( 'wantedfiletext-cat' . $noForeign )
                                ->params( $category->getFullText() )
                                ->parseAsBlock();
                } else {
                        return $this
-                               ->msg( 'wantedfiletext-nocat' )
+                               ->msg( 'wantedfiletext-nocat' . $noForeign )
                                ->parseAsBlock();
                }
        }
 
+       /**
+        * Whether foreign repos are likely to cause false positives
+        *
+        * In its own function to allow subclasses to override.
+        * @see SpecialWantedFilesGUOverride in GlobalUsage extension.
+        * @since 1.24
+        */
+       protected function likelyToHaveFalsePositives() {
+               return RepoGroup::singleton()->hasForeignRepos();
+       }
+
        /**
         * KLUGE: The results may contain false positives for files
         * that exist e.g. in a shared repo.  Setting this at least
         * keeps them from showing up as redlinks in the output, even
         * if it doesn't fix the real problem (bug 6220).
+        *
+        * @note could also have existing links here from broken file
+        * redirects.
         * @return bool
         */
        function forceExistenceCheck() {
index 19ea20b..bab544b 100644 (file)
@@ -29,6 +29,7 @@ class UserloginTemplate extends BaseTemplate {
                $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
 ?>
 <div class="mw-ui-container">
+       <div id="userloginprompt"><?php $this->msgWiki('loginprompt') ?></div>
        <?php if ( $this->haveData( 'languages' ) ) { ?>
                <div id="languagelinks">
                        <p><?php $this->html( 'languages' ); ?></p>
index bc46111..b35967b 100644 (file)
--- a/index.php
+++ b/index.php
@@ -34,7 +34,7 @@
 # has structures (try/catch, foo()->bar(), etc etc) which throw parse errors in
 # PHP 4. Setup.php and ObjectCache.php have structures invalid in PHP 5.0 and
 # 5.1, respectively.
-if ( !function_exists( 'version_compare' ) || version_compare( phpversion(), '5.3.2' ) < 0 ) {
+if ( !function_exists( 'version_compare' ) || version_compare( PHP_VERSION, '5.3.2' ) < 0 ) {
        // We need to use dirname( __FILE__ ) here cause __DIR__ is PHP5.3+
        require dirname( __FILE__ ) . '/includes/PHPVersionError.php';
        wfPHPVersionError( 'index.php' );
index bf30455..0a19d61 100644 (file)
@@ -443,17 +443,6 @@ class Language {
        function initContLang() {
        }
 
-       /**
-        * Same as getFallbacksFor for current language.
-        * @return array|bool
-        * @deprecated since 1.19
-        */
-       function getFallbackLanguageCode() {
-               wfDeprecated( __METHOD__, '1.19' );
-
-               return self::getFallbackFor( $this->mCode );
-       }
-
        /**
         * @return array
         * @since 1.19
@@ -857,6 +846,11 @@ class Language {
                        include "$IP/languages/Names.php";
                }
 
+               // If passed an invalid language code to use, fallback to en
+               if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
+                       $inLanguage = 'en';
+               }
+
                $names = array();
 
                if ( $inLanguage ) {
index c9e75e0..e1b03b5 100644 (file)
        'tw' => 'Twi',                  # Twi, (FIXME!)
        'ty' => 'reo tahiti',   # Tahitian
        'tyv' => 'тыва дыл',     # Tyvan
+       'tzm' => 'ⵜⴰⵎⴰⵣⵉⵖⵜ',    # Tamazight
        'udm' => 'удмурт',        # Udmurt
        'ug' => 'ئۇيغۇرچە / Uyghurche', # Uyghur (multiple scripts - defaults to Arabic)
        'ug-arab' => 'ئۇيغۇرچە', # Uyghur (Arabic script) (default)
index 4a8a589..04ad974 100644 (file)
        "talkpagelinktext": "نقاش",
        "specialpage": "صفحة خاصة",
        "personaltools": "أدوات شخصية",
-       "postcomment": "قسم جديد",
        "articlepage": "اعرض صفحة المحتوى",
        "talk": "نقاش",
        "views": "معاينة",
        "viewsourcetext": "تمكنك مطالعة و نسخ مصدر هذه الصفحة:",
        "viewyourtext": "يمكنك استعراض و نسخ مصدر ''' تعديلاتك ''' في هذه الصفحة:",
        "protectedinterface": "توفر هذه الصفحة نص الواجهة للبرنامج على هذا الويكي، وهي محمية لمنع سوء أستخدامها.\nلإضافة أو تغيير الترجمات لجميع مشاريع الويكي، رجاءً أستخدم [//translatewiki.net/ translatewiki.net]، مشروع الترجمة الخاص بميدياويكي.",
-       "editinginterface": "'''تحذير:''' أنت تقوم بتحرير صفحة تستخدم في الواجهة النصية للبرنامج.\nسوف تؤثر التغييرات في هذه الصفحة على مظهر واجهة المستخدم للمستخدمين الآخرين على هذا الويكي.\nلإضافة أو تغيير الترجمات في جميع مشاريع الويكي، رجاءً استخدم [//translatewiki.net/ translatewiki.net]، مشروع الترجمة الخاص بميدياويكي.",
+       "editinginterface": "<strong>تنبيه:</strong> تعديل هذه الصفحة سيحفظ في هذا الويكي فقط. لتعميم التعديل على جميع مشاريع ميدياويكي، عدلها في [//translatewiki.net/ مشروع ترجمة الويكي].",
        "cascadeprotected": "تمت حماية هذه الصفحة من التعديل لأنها مدمجة في {{PLURAL:$1||الصفحة التالية، والتي|الصفحتين التاليتين، واللتين|الصفحات التالية، والتي}} تم استعمال خاصية \"حماية الصفحات المدمجة\" {{PLURAL:$1||بها|بهما|بها}}:\n$2",
        "namespaceprotected": "لا تمتلك الصلاحية لتعديل الصفحات في نطاق '''$1'''.",
        "customcssprotected": "أنت لا تمتلك السماح لتعديل صفحة الCSS هذه، لأنها تحتوي على الإعدادات الشخصية لمستخدم آخر.",
        "virus-badscanner": "ضبط سيء: ماسح فيروسات غير معروف: ''$1''",
        "virus-scanfailed": "فشل المسح (كود $1)",
        "virus-unknownscanner": "مضاد فيروسات غير معروف:",
-       "logouttext": "'''أنت الآن غير مسجل الدخول.'''\n\nقد ترى بعض الصفحات كما لو أنك ما زلت مسجل الدخول، وذلك حتى تفرغ التخزين المؤقت في متصفحك.",
+       "logouttext": "<strong>أنت الآن غير مسجل الدخول.</strong> قد ترى بعض الصفحات كما لو أنك ما زلت مسجل الدخول، وذلك حتى تفرغ التخزين المؤقت في متصفحك.",
        "welcomeuser": "أهلاً بك يا $1!",
        "welcomecreation-msg": "تم إنشاء حسابك.\nلا تنس تعديل [[Special:Preferences|تفضيلاتك في {{SITENAME}}]].",
        "yourname": "اسم المستخدم:",
        "externaldberror": "هناك إما خطأ في دخول قاعدة البيانات الخارجية أو أنه غير مسموح لك بتحديث حسابك الخارجي.",
        "login": "تسجيل الدخول",
        "nav-login-createaccount": "دخول / إنشاء حساب",
-       "loginprompt": "يجب أن تكون الكوكيز لديك مفعلة لتسجل الدخول إلى {{SITENAME}}.",
        "userlogin": "دخول / إنشاء حساب",
        "userloginnocreate": "تسجيل الدخول",
        "logout": "تسجيل الخروج",
        "license-nopreview": "(العرض المسبق غير متوفر)",
        "upload_source_url": "  (مسار صحيح، يمكن الوصول إليه)",
        "upload_source_file": " (ملف على حاسوبك)",
+       "listfiles-delete": "أحذف",
        "listfiles-summary": "هذه الصفحة الخاصة تعرض كل الملفات المرفوعة.",
        "listfiles_search_for": "ابحث عن اسم الميديا:",
        "imgfile": "ملف",
        "exbeforeblank": "المحتوى قبل الإفراغ كان: '$1'",
        "delete-confirm": "حذف \"$1\"",
        "delete-legend": "حذف",
-       "historywarning": "'''تحذير:''' الصفحة التي توشك على حذفها لها تاريخ فيه {{PLURAL:$1||مراجعة واحدة|مراجعتان|$1 مراجعات|$1 مراجعة}} تقريبا:",
+       "historywarning": "'''تنبيه:''' الصفحة التي تريد حذفها بها {{PLURAL:$1|نسخة|نسخة واحدة|نسختان|$1 نسخ|$1 نسخة}}. انظر",
        "confirmdeletetext": "أنت على وشك أن تقوم بحذف صفحة بالإضافة إلى كل تاريخها.\nمن فضلك التأكد من عزمك على الحذف، وبأنك مدرك للعواقب، وبأنك تقوم بهذا بالتوافق مع [[{{MediaWiki:Policy-url}}|السياسة]].",
        "actioncomplete": "انتهاء العملية",
        "actionfailed": "الفعل فشل",
        "delete-edit-reasonlist": "عدل أسباب الحذف",
        "delete-toobig": "لهذه الصفحة تاريخ تعديل طويل، أكثر من {{PLURAL:$1||مراجعة واحدة|مراجعتين|$1 مراجعات|$1 مراجعة}}.\nقُيّد محذف مثل هذه الصفحات لمنع الاضطراب المفاجئة في {{SITENAME}}.",
        "delete-warning-toobig": "لهذه الصفحة تاريخ تعديل طويل، أكثر من {{PLURAL:$1||مراجعة واحدة|مراجعتين|$1 مراجعات|$1 مراجعة}}.\nقد يؤدي حذفها إلى اضطراب عمليات قاعدة البيانات في {{SITENAME}}؛\nاستمر مع الحذر.",
-       "deleting-backlinks-warning": "'''تحذير:''' ترتبط [[Special:WhatLinksHere/{{FULLPAGENAME}}|صفحات أخرى]] بالصفحة التي أنت على وشك حذفها.",
+       "deleting-backlinks-warning": "[[Special:WhatLinksHere/{{FULLPAGENAME}}|تتصل صفحات أخرى]] بالصفحة التي تريد حذفها.",
        "rollback": "استرجاع التعديلات",
        "rollback_short": "استرجع",
        "rollbacklink": "استرجع",
        "expand_templates_remove_nowiki": "أخفِ وسوم <nowiki> في الناتج",
        "expand_templates_generate_xml": "اعرض شجرة XML parse",
        "expand_templates_generate_rawhtml": "أظهر خام HTML",
-       "expand_templates_preview": "عرض مسبق"
+       "expand_templates_preview": "عرض مسبق",
+       "pagelang-name": "صفحة"
 }
index c1885cf..34aebd5 100644 (file)
        "tog-hideminor": "خبي الكتيبات الصغيرة في التبديلات التوالا",
        "tog-hidepatrolled": "خبي الكتيبات المعسوسه في التبديلات التوالا",
        "tog-newpageshidepatrolled": "خبي الباجات المعسوسه اللي في ليستت الباجات الجدد",
-       "tog-extendwatchlist": "دÙ\84Ù\8a Ø§Ù\84Ù\84Ù\8aستÙ\87 Ù\86تاع Ø§Ù\84تبÙ\8aعÙ\87 Ø¨Ø§Ø´ ØªØ¹Ø±Ø¶ Ù\83اÙ\85Ù\84 Ø§Ù\84تبدÙ\8aÙ\84ات Ù\88 Ù\85Ø´Ù\8a Ø¨Ø±Ù\83 التوالا",
-       "tog-usenewrc": "اجÙ\85ع Ø§Ù\84Ù\80تبداÙ\84ات Ø¨Ù\84 ØµÙ\81حات Ù\81Ù\84 ØªØ¨Ø¯Ø§Ù\84ات Ø§Ù\84Ù\80تاÙ\84Ù\8aØ© Ù\88 Ø§Ù\84Ù\80Ù\84Ù\8aستة ØªØ§Ø¹ Ø§Ù\84Ù\80Ù\85راÙ\82بة (تستحÙ\82 Ø§Ù\84Ù\80 JavaScript)",
+       "tog-extendwatchlist": "دÙ\84Ù\91Ù\8a Ø§Ù\84Ù\84Ù\8aستة ØªØ§Ø¹ Ø§Ù\84تتباع Ø¨Ø§Ø´ ØªÙ\88رÙ\91Ù\8a Ù\83اÙ\85Ù\84 Ø§Ù\84تبدÙ\8aÙ\84اتØ\8c Ù\85اشÙ\8a Ø¨Ø±Ù\83 ØºÙ\8aر التوالا",
+       "tog-usenewrc": "جÙ\85Ù\91ع Ø§Ù\84Ù\80تبداÙ\84ات Ø¨Ù\84 ØµÙ\81حة Ù\81Ù\84 ØªØ¨Ø¯Ø§Ù\84ات Ø§Ù\84Ù\80جدÙ\8aدة Ù\88 Ø§Ù\84Ù\80Ù\84Ù\8aستة ØªØ§Ø¹ Ø§Ù\84Ù\80عسÙ\91Ø©",
        "tog-numberheadings": "رقم اليا عناوين السكسيو",
-       "tog-showtoolbar": "تبÙ\8aاÙ\86 Ø¨Ø§Ø±Ù\87 Ø§Ù\84Ù\83تÙ\8aبات (Ù\8aÙ\84زÙ\85Ù\87ا Ø¬Ø§Ù\81اسÙ\83رÙ\8aبت)",
-       "tog-editondblclick": "كتيبت الباجات بالزوج دركات (يلزمها جافاسكربت)",
-       "tog-editsectiononrightclick": "اÙ\83تÙ\8aÙ\81Ù\8a Ù\83تÙ\8aبت Ø§Ù\84سÙ\83سÙ\8aÙ\88ات Ø¨Ø§Ù\84درÙ\8aÙ\83 Ø¨Ø§Ù\84Ù\8aÙ\85Ù\8aÙ\86 Ø¹Ù\84Ù\89 Ø§Ù\84عÙ\86اÙ\88Ù\8aÙ\86 Ù\86تاعÙ\87Ù\85\8aتطÙ\84ب Ø¬Ø§Ù\81اسÙ\83رÙ\8aبت)",
+       "tog-showtoolbar": "بÙ\8aÙ\91Ù\86 Ø§Ù\84بارÙ\91Ø© ØªØ§Ø¹ Ø¯Ù\88زاÙ\86â\80\98 Ø§Ù\84Ù\83تبة",
+       "tog-editondblclick": "آكتيفي التبدال تاع الباجات بل زوج ضركات تاع الفارة",
+       "tog-editsectiononrightclick": "Ø¢Ù\83تÙ\8aÙ\81Ù\8a Ø§Ù\84تبداÙ\84 ØªØ§Ø¹ Ø§Ù\84سÙ\83سÙ\8aÙ\88Ù\91ات Ø¨Ù\84 Ø¶Ø±Ù\8aÙ\83 Ø¨Ù\84 Ù\84Ù\8aÙ\85Ù\86Ø© Ø¹Ù\84Ù\89 Ø§Ù\84عÙ\84اÙ\88Ù\8aÙ\86 Ù\86تاعÙ\87Ù\85",
        "tog-watchcreations": "زيد الـصفحات اللي نخلقها و الـفيشيّات فل قايمة تاع التتباع تاعي",
        "tog-watchdefault": "زيد الـصفحات و الـفيشيّات اللي نبدّلها فل قايمة تاع الـتتباع تاعي",
        "tog-watchmoves": "زيد الـصفحات و الـفيشيات اللي نحوّلها فل قايمة تاع الـتباع تاعي",
        "tog-watchdeletion": "زيد الـصفحات اللي نفصيها فل قايمة تاع التتباع تاعي",
        "tog-minordefault": "ماركي كل التبديلات بلي راهي خفيفه",
-       "tog-previewontop": "Ù\88رÙ\8a Ø´Ù\88Ù\81Ù\87\82بÙ\84Ù\8aÙ\87 Ù\84Ù\84Ù\83تبÙ\87 Ù\81Ù\88Ù\82 ØµÙ\86دÙ\88Ù\82 Ø§Ù\84Ù\83تÙ\8aبÙ\87",
+       "tog-previewontop": "Ù\88رÙ\91Ù\8a Ù\86ضرة Ù\82بÙ\84Ù\8aÙ\91Ø© ØªØ§Ø¹ Ù\88اش Ù\8aصراØ\8c Ù\81Ù\88Ù\82 Ø§Ù\84جÙ\8aÙ\87Ø© ØªØ§Ø¹ Ø§Ù\84تبداÙ\84",
        "tog-previewonfirst": "بين شوفه-قبليه مع اول تبديله",
        "tog-enotifwatchlistpages": "ابحت لي إيمال كي تتبدّل صفحة ولا فيشي من الـليستة تاع الـتتباع تاعي",
        "tog-enotifusertalkpages": "ابعثلي بريه كل ما تبدلت باجت نقاش ديالي",
@@ -30,7 +30,7 @@
        "tog-shownumberswatching": "بين شحال كاين من مستعمل يتبع الباجه",
        "tog-oldsig": "خطّ‘لـيدّ اللي كاين",
        "tog-fancysig": "اعتبر التوقيع كي كتيبه ويكي (بلا وصيله توماتيك)",
-       "tog-uselivepreview": "استعمل الـنضرة الـقبلانيّة (تستحق الـ JavaScript) (تجرابيّة)",
+       "tog-uselivepreview": "استعمل الـنضرة الـقبلانيّة الحيّة (عفسة تجرابيّة، تخلّيك تشوف التبدال الّي يصرا فل وقت الّي تكون تكتب)",
        "tog-forceeditsummary": "نبّهني كي تندخل كاش صفحة خاوية",
        "tog-watchlisthideown": "خبّي الـتبدالات تاوعي فل ليستة تاع الـتتباع",
        "tog-watchlisthidebots": "خبّي الـتبدالات تاع الـروبويات فل ليستة تاع التتباع تاعي",
        "newwindow": "(حل في تاقة جديدة)",
        "cancel": "انيلي",
        "moredotdotdot": "كتر...",
-       "morenotlisted": "Ù\83تر Ù\85اشÙ\8a Ù\85Ù\84Ù\8aستÙ\8a...",
+       "morenotlisted": "Ù\87اد Ø§Ù\84Ù\84Ù\8aستة Ù\85ا Ø±Ø§Ù\87Ù\8aØ´ Ù\85Ù\83Ù\85Ù\88Ù\84Ø©",
        "mypage": "باجه",
        "mytalk": "نقاش",
        "anontalk": "تناقش على الـ ip هادي",
        "qbmyoptions": "الباجات نتاوعى",
        "faq": "المسقسية المتعاوده",
        "faqpage": "Project:سؤالات متكرره",
-       "vector-action-addsection": "زيد موضوع",
-       "vector-action-delete": "امحي",
-       "vector-action-move": "حول",
-       "vector-action-protect": "بروجي",
-       "vector-action-undelete": "ردّ كيما كان",
-       "vector-action-unprotect": "بدّل الـحماية",
-       "vector-view-create": "أصنع",
-       "vector-view-edit": "بدل",
-       "vector-view-history": "روح للتاريخي",
-       "vector-view-view": "أقرى",
-       "vector-view-viewsource": "شوف المصدر",
        "actions": "أفعال",
        "namespaces": "بلاصه تع أسموات",
        "variants": "متغيرات",
        "history": "تاريخ الملف",
        "history_short": "تاريخ",
        "updatedmarker": "مبدّل منلي الزيارة تاعي الـتالية",
-       "printableversion": "نسخه نتاع طبيع",
+       "printableversion": "نسخة تاع طبيع",
        "permalink": "وصيل دايم",
        "print": "امبريمي",
        "view": "اقرا",
+       "view-foreign": "شوف على $1",
        "edit": "بدل",
+       "edit-local": "عدّل التوصاف المبلّد",
        "create": "أصنع",
+       "create-local": "زيد توصاف مبلّد",
        "editthispage": "بدّل هاد الـصفحة",
        "create-this-page": "خلّق صفحة ب هاد الـعلوان",
        "delete": "امحي",
        "deletethispage": "امحي هاد الـصفحة",
        "undeletethispage": "ردّ الصفحة الّي محيتها",
+       "undelete_short": "رجّع {{PLURAL:$1||تعديل واحد|$1 تعديل}}",
+       "viewdeleted_short": "شوف {{PLURAL:$1||تعديل واحد|$1 تعديل}}",
        "protect": "حمايه",
        "protect_change": "بدل",
+       "protectthispage": "بروتيجي هاد الباجة",
+       "unprotect": "بدّل الحضية",
+       "unprotectthispage": "بدّل الحضية تاع هاد الباجة",
        "newpage": "باجه جديده",
+       "talkpage": "قرعَج على هاد الباجة",
        "talkpagelinktext": "ناقش",
+       "specialpage": "باجة خوصوصيّة",
        "personaltools": "ادوالت شخصيه",
+       "articlepage": "شوف الباجة تاع المحتاوا",
        "talk": "مناقشه",
        "views": "شوفات",
-       "toolbox": "صندوق الادوات",
+       "toolbox": "صندوق تاع الدوزان",
+       "userpage": "شوف الباجة تاع المستعملي",
+       "projectpage": "شوف الباجة تاع البروجي",
+       "imagepage": "شوف الباجة تاع الفيشي",
+       "mediawikipage": "شوف الباجة تاع الميساج",
+       "templatepage": "شوف الباجة تاع القالب",
+       "viewhelppage": "شوف الباجة تاع المعاونة",
+       "categorypage": "شوف الباجة تاع الصنيف",
+       "viewtalkpage": "شوف التقرعيج",
        "otherlanguages": "بلوغات وحد اوخره",
        "redirectedfrom": "(محول من $1)",
+       "redirectpagesub": "باجة تاع التحوال",
        "lastmodifiedat": "هاد الباجه راهي تبدّلت نهار الـ $1, على الـساعة $2.",
+       "viewcount": "هاد الباجة نشافت {{PLURAL:$1|خطرة وحدة|$1 خطرة}}.",
+       "protectedpage": "باجة محضيّة",
        "jumpto": "اقفز ل:",
        "jumptonavigation": "تجوال",
        "jumptosearch": "تفتاش",
+       "view-pool-error": "اعدرونا، السربايات راهم مغبّنين ف هاد الوقيتة.\nبزّاف المستعمليّين راهم باغيين يشوفو هاد الباجة.\nاصبرو شي وقيتة قبل ما تحاولو تلحقو لها عاود.\n\n$1",
+       "generic-pool-error": "اعدرونا، السربايات راهم مغبّنين ف هاد الوقيتة.\nبزّاف المستعمليّين راهم باغيين يشوفو هاد الباجة.\nاصبرو شي وقيتة قبل ما تحاولو تلحقو لها عاود.",
+       "pool-timeout": "المهلة تاع المقارعة راهي فاتت",
+       "pool-queuefull": "السنسلة تاع المقارعة راهي عامرة",
+       "pool-errorunknown": "خلطة ماشي معروفة",
+       "pool-servererror": "السربيس تاع العدّان راه حابس ( $1 ).",
        "aboutsite": "على{{SITENAME}}",
        "aboutpage": "Project:على",
+       "copyright": "المحتاوا راه تحت النسخة $1 تاع الليسانس، غير يلا كان مكتوب حاجاخرة.",
        "copyrightpage": "{{ns:project}}:حقوق النسخ",
        "currentevents": "الخبورات",
        "currentevents-url": "Project:خبورات",
        "disclaimers": "تنبيهات",
        "disclaimerpage": "Project:التحذيرات العامه",
        "edithelp": "معونة",
-       "mainpage": "الباجة اللوله",
+       "mainpage": "الباجة اللولانيّة",
        "mainpage-description": "الباجة اللوله",
+       "policy-url": "Project:المقاون",
        "portal": "المجتمع",
        "portal-url": "Project:بورطاي المجتمع",
        "privacy": "السياسة تاع الخصوصيات (الدين الضيّق)",
        "privacypage": "Project:خصوصيه",
+       "badaccess": "مشكل فل مسموحات",
+       "badaccess-group0": "ماشي مقبول ليك تدير الشي الّي راك تسيّي تديرهُ.",
+       "badaccess-groups": "الفعلة الّي راك سيّيت تديرها مسموحة برك لل مستعملّين {{PLURAL:$2||الّي هوما منل جماعة|الّي هوما من وحدة من هاد الجمايع}}: $1.",
+       "versionrequired": "النسخة $1 تاع ميدياويكي ملزومة",
+       "versionrequiredtext": "النسخة $1 تاع ميدياويكي راهي ملزومة باش تنجم تستعمل هاد الباجة.\nشوف [[Special:Version|الباجة تاع النسخات باش تفهم كتَر على هاد الشي]]",
+       "ok": "قابل",
        "retrievedfrom": "جايبينه من \"$1\"",
        "youhavenewmessages": "عندك $1 ($2).",
+       "youhavenewmessagesfromusers": "{{PLURAL:$4|عندك}} $1 من عند {PLURAL:$3|مستعملي واحد|زوج تاع المستعمليّين|$3 مستعملي}} ($2).",
+       "youhavenewmessagesmanyusers": "عندك $1 من عند شحال من مستعملي ($2).",
+       "newmessageslinkplural": "{{PLURAL:$1|بريّة جديدة وحدة|999=بريّة جديدة}}",
+       "newmessagesdifflinkplural": "{{PLURAL:$1التبديلة التالية|التبديلات التاليين}}",
+       "youhavenewmessagesmulti": "عندك بريّة جديدة في $1.",
        "editsection": "بدل",
        "editold": "بدل",
        "viewsourceold": "شوف الاصل",
        "viewsourcelink": "شوف العين",
        "editsectionhint": "إيديتي الصنف:$1",
        "toc": "محتويات",
+       "showtoc": "ورّي",
+       "hidetoc": "خبّي",
+       "collapsible-collapse": "خبّي",
+       "collapsible-expand": "ورّي",
+       "thisisdeleted": "راك باغي تشوف ولا ترجّع $1؟",
+       "viewdeleted": "شوف $1؟",
+       "restorelink": "{{PLURAL:$1|تبدال واحد مفاصي|$1 تبدالات مفاصيين|$1 تبدال مفاصي}}",
+       "feedlinks": "السيلان:",
+       "feed-invalid": "النوع تاع التلقيمة ماشي مصلاح.",
+       "feed-unavailable": "التلقيمات ما راهمش موجودين.",
        "site-rss-feed": "تيار آر‌إس‌إس $1",
        "site-atom-feed": "$1 تيار آتوم",
+       "page-rss-feed": "تلقيمة RSS تاع \"$1\"",
        "page-atom-feed": "$1 تيار آتوم",
        "red-link-title": "$1 (الباجه ما كاينش)",
+       "sort-descending": "رتّب بل نازولي",
+       "sort-ascending": "رتّب بل طالوعي",
        "nstab-main": "الباجة",
        "nstab-user": "باجة{{GENDER:{{BASEPAGENAME}}|المستخدم|المستخدمة}}",
+       "nstab-media": "باجة تاع ميديا",
        "nstab-special": "باجه خوصوصيّة",
        "nstab-project": "باجه مشروع",
        "nstab-image": "ملف",
+       "nstab-mediawiki": "بريّة",
        "nstab-template": "مودال",
+       "nstab-help": "باجة تاع معاونة",
        "nstab-category": "تصنيف",
+       "nosuchaction": "الشي الّي طلبتهُ ما كاينش",
+       "nosuchactiontext": "الفعلة الّي مطلوبة فل URL ماشي مقبولة.\nبالاك ما دخّلتوش الـ URL كيما لازم ولا تاني تبّعتو كاش وصيل مغلوط.\nينجم تاني يكون كاين عُلّة فل لوجيسيال الّي مستعمل فـ {{SITENAME}}.",
+       "nosuchspecialpage": "هاد الباجة الخوصوصيّة ما كاينش منها",
+       "nospecialpagetext": "<strong>راك طلبت باجة خوصوصيّة ماشي صحيحة.</strong>\n\nتصيب الليستة تاع الباجات الخوصوصيّة في [[Special:SpecialPages|{{int:specialpages}}]].",
+       "error": "غلطة",
+       "databaseerror": "غلطة فل دخيرة تاع الخبيرات (DB)",
+       "databaseerror-text": "صرات غلطة عند المسقسية تاع الدخيرة تاع الخبيرات. هاد الشي ينجم يكون جاي من غلطة فل برنامج.",
+       "databaseerror-textcl": "صرات غلطة عند المسقسية تاع الدخيرة تاع الخبيرات.",
+       "databaseerror-query": "مسقسية : $1",
+       "databaseerror-function": "دالّة: $1",
+       "databaseerror-error": "غلطة: $1",
+       "laggedslavemode": "<strong>ردّ بالك:</strong> هاد الباجة تنجم تكون ما حاوياش التبدالات التاليين الّي ندارو.",
+       "readonly": "الدخيرة تاع الخبيرات راهي مغلوقة",
+       "enterlockreason": "حطّ السبّة تاع القفيل و المدّة تاعهُ بل ميز.",
+       "readonlytext": "الدخيرة تاع الخبيرات راهي مغلوقة على الدخلات الجديدة ولا التبدالات، بالاك علاجال كاش صيانة عاديّة، مور ماش غادي تعاود ترجع لل طبَع.\n\nالإيداري الّي دار هاد الشي راه يعطي التفسيرات هادي: $1",
        "missing-article": "الداتاباز ما صابتش باجه كان لازم تنصاب، الباجه هي \"$1\" $2.\n\nنورمالمو يصرا هذا مين اتبع فرق بيريمي والا وصيل تأريخ باجة ممحيه.\n\nإذا ما كانش هذا هو الحال همالا راك طحت في علة تاع البرمجية.\nمن فضلك سينياليها لواحد من[[Special:ListUsers/sysop|الإداريين]]، و أعطه مسار هذه الباجه.",
        "missingarticle-rev": "(رقم الفرسيون: $1)",
+       "missingarticle-diff": "(فرق بين: $1، $2)",
+       "readonly_lag": "الدخيرة تاع الخبرات راهي مقفولة بيدما السربايات التوناويّة يلحقو التوخار الّي عندهم معا السرباي اللولاني",
+       "internalerror": "غلطة دخلانيّة",
+       "internalerror_info": "غلطة دخلانيّة: $1",
+       "filecopyerror": "ما قدرش تنساخ الفيشي \"$1\" لل \"$2\"",
+       "filerenameerror": "ما قدرش تبدال السميّة تاع الفيشي \"$1\" لل \"$2\".",
+       "filedeleteerror": "ما قدرش تمحيتٰ الفيشي \"$1\".",
+       "directorycreateerror": "ما قدرش خلقان الدفتار \"$1\".",
+       "filenotfound": "ما قدرش مصيبتٰ الفيشي \"$1\".",
+       "unexpected": "قيمة ما شي مستنية : \"$1\"=\"$2\".",
+       "formerror": "غلطة: ما قدرش ترسال الستيمارة",
        "badtitle": "عنوان عيان",
        "badtitletext": "عنوان الباجه المطلوب إما ماشي صحيح والا فارغ، وبالاك الوصيل بين اللغات والا بين البروجيات ماشي صحيح.\nبالاك فيه حروف ما تصلحش  باس يستعملوها فالعناوين.",
        "viewsource": "شوف الاصل",
        "remembermypassword": "اتفكر الدخول تاعي ب هاذ النافيكاتور (ب مدّة حدها{{PLURAL:$1||يوم واحد|يومين|$1 إيّام|$1 يوم}})",
        "login": "كونكسيون",
        "nav-login-createaccount": "تسجل/ اصنع حساب",
-       "loginprompt": "لازم تكون الكوكيز لديك ماكتيفيه باش تكونيكتي و تدخل ل{{SITENAME}}.",
        "userlogin": "تسجل/ اصنع حساب",
        "userlogout": "سجل خروج",
        "nologin": "ما عندكش حساب مسجل؟ '''$1'''.",
        "link_tip": "وصيلة داخليه",
        "extlink_sample": "http://www.example.com اسم الوصيلة",
        "extlink_tip": "وصيلة برانية (ما تنساش البديةhttp://)",
-       "headline_sample": "كتبة نتاع عنوان كبير",
+       "headline_sample": "كتبة تاع علوان كبير",
        "headline_tip": "عنوان من المستوى الثاني",
        "nowiki_sample": "دخل الكتبة مشي مستفة هنا",
        "nowiki_tip": "اهمل طريقةالويكي",
        "post-expand-template-argument-warning": "'''توليه:''' هذه الباجه فيها عامل قالب واحد على الأقل عندو حجم تمدد كبير بزاف.\nهاذالعوامل اتمحات.",
        "post-expand-template-argument-category": "باجات فيها مدخلات القالب الممحي",
        "viewpagelogs": "بين العمليات على هاذ الباحه",
-       "currentrev-asof": "النسخه نتاع دروك تاريجها $1",
+       "currentrev-asof": "نسخة ضركانية بل تاريخ تاع $1",
        "revisionasof": "معاودة تاع الـ $1",
        "revision-info": "مراجعه $1 بواسطت $2",
        "previousrevision": "← نسخة اللوله",
        "lineno": "سطر$1:",
        "compareselectedversions": "كومباري بين نسختين مخيرين",
        "editundo": "نحي",
-       "searchresults": "ريزيلته نتاع التفتاش",
+       "searchresults": "نتاج تاع التفتيشة",
        "searchresults-title": "ريزيلته تاع التحواس \"$1\"",
        "prevn": "{{PLURAL:$1|précédente|$1 اللولانيين}}",
        "nextn": "{{PLURAL:$1|suivante|$1 التاليين}}",
        "searchmenu-exists": "'''كاين باجه اسمها « [[:$1]] » في هاذ الويكي'''",
        "searchmenu-new": "'''أصنع الباجه « [[:$1|$1]] » في هذ الويكي !'''",
        "searchprofile-articles": "باجه تع محتوى",
-       "searchprofile-project": "باجه تع المعونه و البروجي",
        "searchprofile-images": "ميلتيميديا",
        "searchprofile-everything": "كلش",
        "searchprofile-advanced": "تفتاش متقدم",
        "searchprofile-articles-tooltip": "فتش في $1",
-       "searchprofile-project-tooltip": "فتش في  $1",
        "searchprofile-images-tooltip": "فتش على ملفات ميلتيميديا",
        "searchprofile-everything-tooltip": "فتش في قاع السيت (حتى في باجات المناقشه)",
        "searchprofile-advanced-tooltip": "خير إسباسات الأسامي للتفتاش",
        "booksources-go": "اذهب",
        "log": "ريجيسترات العمليات",
        "allpages": "قاع الباجات",
-       "alphaindexline": "$1 إلى $2",
        "allarticles": "قاع الباجات",
        "allpagessubmit": "روح",
        "categories": "تصنيفات",
        "linksearch-line": "$1 موصولة من $2",
        "listgrouprights-members": "(ليسته الأعضاء)",
        "emailuser": "ابعث بريه لهاذ المستخدم",
-       "watchlist": "ليستة نتاع التابعه",
-       "mywatchlist": "ليستة نتاع التابعه",
+       "watchlist": "ليستة تاع المتابعة",
+       "mywatchlist": "ليستة تاع المتابعة",
        "watchlistfor2": "ل$1 ($2)",
        "watch": "تبع",
        "unwatch": "ما تزيدش تعس",
        "sp-contributions-talk": "نقاش",
        "sp-contributions-search": "تفتاش المشاركات",
        "sp-contributions-username": "عنوان أيبي والال اسم مستخدم:",
-       "sp-contributions-toponly": "ما تبين غير المشاركات التوالا نتاع المقالات",
+       "sp-contributions-toponly": "ما تورّي غير المشاركات التوالا تاع المقالات",
        "sp-contributions-submit": "تفتاش",
        "whatlinkshere": "واش يوصل هنا",
        "whatlinkshere-title": "الباجات اللي تقين في \"$1\"",
        "tooltip-ca-edit": "تنجم تحرر هاذ الباجه ،ماذابيك تستعمل قفله المراجعه قبل ما تحفظ",
        "tooltip-ca-addsection": "ابدأ طرف جديد",
        "tooltip-ca-viewsource": "هاذ الباجه محميه. و شنو تقدرو تشوفو الأصلي نتاعها",
-       "tooltip-ca-history": "المراجعات التوالى نتاع الباجه (مع المساهمين نتاوعها)",
+       "tooltip-ca-history": "المراجعات التوالا تاع الباجة (معا المساهمين تاوعها)",
        "tooltip-ca-protect": "بروتيجي هاذالباجه",
        "tooltip-ca-delete": "امحي هاذ الباجه",
        "tooltip-ca-move": "بدل أسم هذ الباجه",
-       "tooltip-ca-watch": "زيد هذ الباجه لليستتك نتاع التتباع",
-       "tooltip-ca-unwatch": "اÙ\82Ù\84ع Ù\87اذ Ø§Ù\84باجÙ\87 Ù\85Ù\86 Ø§Ù\84Ù\84Ù\8aستÙ\87 Ù\86تاع التتباع",
+       "tooltip-ca-watch": "زيد هذ الباجة لل ليستة تاعك تاع التتباع",
+       "tooltip-ca-unwatch": "اÙ\82Ù\84ع Ù\87اد Ø§Ù\84باجة Ù\85Ù\86Ù\84 Ù\84Ù\8aستة ØªØ§Ø¹Ù\83 تاع التتباع",
        "tooltip-search": " فتّش في {{SITENAME}}",
        "tooltip-search-go": "روح الباجه عندها نفس الآسم إذا كانت كاينه",
        "tooltip-search-fulltext": "فتّش على باجه بهاد الكتبة",
        "tooltip-n-mainpage": "زور الباجه اللوله",
        "tooltip-n-mainpage-description": "زور الباجه لوله",
        "tooltip-n-portal": "على الپروجي،واش تنجم تدير، وين تصيب واش تحتاج",
-       "tooltip-n-currentevents": "تحÙ\88اس Ø¹Ù\84Ù\89 Ù\85عÙ\84Ù\88Ù\85ات Ø£Ø³Ø§Ø³Ù\8aØ© Ù\84صÙ\88اÙ\84Ø­ ØµØ±Ø§Ù\88 Ø°Ø±Ù\88Ù\83",
+       "tooltip-n-currentevents": "صÙ\8aب Ø®Ø¨Ø§Ø±Ø§Øª Ù\85ستÙ\91رÙ\8aÙ\86 Ø¹Ù\84Ù\89 Ø§Ù\84صÙ\88اÙ\84Ø­ Ø§Ù\84Ù\91Ù\8a Ø±Ø§Ù\87Ù\85 Ù\8aصراÙ\88 Ø¶Ø±Ù\83ا",
        "tooltip-n-recentchanges": "الليستة تاع التبديلات التوالا فل ويكي",
        "tooltip-n-randompage": "طلّع باجه على الزهر",
        "tooltip-n-help": "بلاصة المعونة",
-       "tooltip-t-whatlinkshere": "ليسته نتاع قاع باجات المحتوى الي توصل هنا",
-       "tooltip-t-recentchangeslinked": "ليسته نتاع التبديلات التواله نتاع الباجات الي عندهم علاقه بهاذي",
-       "tooltip-feed-atom": "سيلان آتوم نتاع الباجه",
+       "tooltip-t-whatlinkshere": "ليستة تاع كاع الباجات تاع المحتاوا الي توصّل لهنا",
+       "tooltip-t-recentchangeslinked": "ليستة تاع التبديلات التوالا تاع الباجات الّي عندهم رباط معا هادي",
+       "tooltip-feed-atom": "سيلان آتوم تاع هاد الباجة",
        "tooltip-t-contributions": "شوفان ليسته مساهمات هاذا المستخدم",
        "tooltip-t-emailuser": "أرسل بريه لهاذ المستخدم",
        "tooltip-t-upload": "أرسل تصويرة و إلا أي ملف ميديا للسرفر",
        "tooltip-t-specialpages": "ليستة تاع كامل الباجات الخصوصيّة",
        "tooltip-t-print": "نسخه لهاذ الباجه قابله للطبيع",
-       "tooltip-t-permalink": "توصيله دايمه رايحه لهاذ النسخة نتاع الباجة",
+       "tooltip-t-permalink": "وصيل دايم رايح ل هاد النسخة تاع الباجة",
        "tooltip-ca-nstab-main": "شوف باجه المحتوى",
        "tooltip-ca-nstab-user": "شوف باجت المستعمل",
        "tooltip-ca-nstab-special": "هذه الباجه خصوصيه،ما تقدرش تبدل فيها",
        "tooltip-minoredit": "ماركي هاذا تبديل صغير",
        "tooltip-save": "سجل تبديلات نتاعك",
        "tooltip-preview": "بين التغييرات نتاعك، من فضلك استخدم هذا قبل ما تنشر!",
-       "tooltip-diff": "تخلي الشوفان نتاع التبديلات اللي ندارو.",
+       "tooltip-diff": "ورّي التبدالات الّي راك درتهم فل نصّ.",
        "tooltip-compareselectedversions": "شوف الفروق بين نسختين مخيرين من هاذ الباجه.",
-       "tooltip-watch": "زÙ\8aد Ù\87Ø° Ø§Ù\84باجÙ\87 Ù\84Ù\84Ù\8aستتÙ\83 Ù\86تاع التتباع",
-       "tooltip-rollback": "يولي : بدركة وحده تآنيلي التبديله و إلا التبديلات نتاع المساهم التالي",
+       "tooltip-watch": "زÙ\8aد Ù\87اد Ø§Ù\84باجة Ù\84Ù\84 Ù\84Ù\8aستة ØªØ§Ø¹Ù\83 تاع التتباع",
+       "tooltip-rollback": "\"نحّي\" : ب ضركة وحدة تآنيلي التبديلة ولا التبديلات تاع المساهم التالي",
        "tooltip-undo": "\"نحّي\" فاصي هاد الـمعاودة و حلّ تاقة تاع تبدال بشوفه قبلانيّه. تخلّي باش ترجع لل معاوده التاليه و تزيد الـسبّة علاش فل قابسه تاع الـحويصله.",
        "tooltip-summary": "دخل تلخيص صغير",
        "previousdiff": "→ التعديل الي قبل",
        "file-nohires": "ما كانش دقه اكثر من هاك",
        "svg-long-desc": "فيشيي SVG، أبعاده $1 × $2 بكسل، تاي الفيشي : $3",
        "show-big-image": "تصويرة دقة عالية",
-       "bad_image_list": "الفورمه كيما التابعة:\nما كاين غير السطور الّي باديين بل *، الّي يكونو معدودين\nالـوصيل الـلوّل نتاع سطر لازم تكون تاع تصويرة ضايعة.\nكامل الوصيلات لخرين الّي فل سطر، يكونو معدودين كلّي تتنيّات، بل كي الباجات وين الـفيشي يكون باين.",
+       "bad_image_list": "الفورمة راهي كيما واش يتبع:\nما كاين غير السطور الّي باديين بل *، الّي يكونو معدودين\nالـوصيل الـلوّل تاع سطر لازم كون تاع تصويرة ضايعة.\nكامل الوصيلات لخرين الّي فل سطر، يكونو معدودين كلّي تتنيّات، بل متال باجات وين الـتصويرة تنجم تبان.",
        "metadata": "بايان ميتا",
-       "metadata-help": "هذا الملف راه فيه معلومات زيادة، بالاك تكون انزادت من عند صواره نيميريك ولا سكانر مين صنع الملف.\nالأصلي، شي تفاصيل بالاك ما تعبرش على الملف المعدل.",
-       "metadata-fields": "غادÙ\8a Ù\8aÙ\86عرض Ø§Ù\84Ø­Ù\82Ù\84 Ù\86تاع Ù\85عطÙ\8aات Ø§Ù\84Ù\85Ù\8aتا Ø§Ù\84Ù\83اÙ\8aÙ\86Ù\87 Ù\81Ù\8a Ù\87اذ Ø§Ù\84برÙ\8aÙ\87 Ù\81Ù\8a Ø¨Ø§Ø¬Ù\87 Ø§Ù\84تصÙ\88Ù\8aرة Ù\85Ù\86Ù\8aÙ\86 Ù\8aÙ\83Ù\88Ù\86 Ø¬Ø¯Ù\88Ù\84 Ù\85عطÙ\8aات Ø§Ù\84Ù\85Ù\8aتا Ù\85Ø·Ù\88Ù\8aاÙ\8b.\nاÙ\84Ø­Ù\82Ù\88Ù\84 Ù\84خرÙ\87 ØªÙ\83Ù\88Ù\86 Ù\85خبÙ\8aØ© Ø¨Ø§Ø± ديفو.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
+       "metadata-help": "هذا الملف راه فيه خبيرات زايدين، بالاك تكون انزادت من عند صواره نيميريك ولا سكانر مين صنع الملف.\nالأصلي، شي تفاصيل بالاك ما تعبرش على الملف المعدل.",
+       "metadata-fields": "اÙ\84Ø­Ù\82Ù\88Ù\84 ØªØ§Ø¹ Ø§Ù\84Ù\85Ù\8aتا Ù\85عطÙ\8aÙ\91ات ØªØ§Ø¹ ØªØµØ§Ù\88ر Ø§Ù\84Ù\91Ù\8a Ù\8aÙ\83Ù\88Ù\86Ù\88 Ù\81 Ù\87اد Ø§Ù\84برÙ\8aÙ\91Ø© ØºØ§Ø¯Ù\8a Ù\8aÙ\86حطÙ\91Ù\88 Ù\81Ù\84 Ø¨Ø§Ø¬Ø© ØªØ§Ø¹ Ø§Ù\84تÙ\88صاÙ\81 ØªØ§Ø¹ Ø§Ù\84تصÙ\88Ù\8aرة Ù\85Ù\86Ù\8aÙ\86 Ù\8aÙ\83Ù\88Ù\86 Ø§Ù\84جدÙ\88Ù\84 ØªØ§Ø¹  Ø§Ù\84Ù\85Ù\8aتااÙ\84Ù\85عطÙ\8aات Ù\85Ø·Ù\88Ù\8a.\nاÙ\84Ø­Ù\82Ù\88Ù\84 Ù\84خرة Ù\8aÙ\83Ù\88Ù\86Ù\88 Ù\85خبÙ\8aÙ\8aÙ\86 Ø¨Ø§Ø±ديفو.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "watchlistall2": "لكل",
        "namespacesall": "لكل",
        "monthsall": "لكل",
index e04983e..63a7d9f 100644 (file)
@@ -8,7 +8,8 @@
                        "Meno25",
                        "Ouda",
                        "Ramsis II",
-                       "아라"
+                       "아라",
+                       "Oldstoneage"
                ]
        },
        "tog-underline": "حط خط تحت اللينكات:",
        "talkpagelinktext": "مناقشه",
        "specialpage": "صفحة مخصوصة",
        "personaltools": "ادوات شخصيه",
-       "postcomment": "قسم جديد",
        "articlepage": "بين صفحة المحتوى",
        "talk": "مناقشه",
        "views": "مناظر",
        "externaldberror": "يا إما فى حاجة غلط فى الدخول على قاعدة البيانات الخارجية أو انت مش مسموح لك تعمل تحديث لحسابك الخارجي.",
        "login": "دخول",
        "nav-login-createaccount": "تسجيل دخول / فتح حساب",
-       "loginprompt": "لازم تكون الكوكيز عندك مفعله علشان تقدر تدخل ل {{SITENAME}}.",
        "userlogin": "دخول / فتح حساب",
        "userloginnocreate": "دخول",
        "logout": "خروج",
        "timezone-utc": "يو تى سى",
        "unknown_extension_tag": "تاج بتاع امتداد مش معروف \"$1\"",
        "duplicate-defaultsort": "تحزير: زرار الترتيب الاوتوماتيكي\"$2\" بيوقف زرار الترتيب الاوتوماتيكي\"$1\" القديم.",
-       "version": "نسخه",
+       "version": "نسخة",
        "version-extensions": "الامتدادات المتثبتة",
        "version-specialpages": "صفحات مخصوصة",
        "version-parserhooks": "خطاطيف البريزر",
index c7d66d8..1669b9d 100644 (file)
        "talkpagelinktext": "Müzakirə",
        "specialpage": "Xüsusi səhifə",
        "personaltools": "Şəxsi alətlər",
-       "postcomment": "Yeni bölmə",
        "articlepage": "Məqaləni nəzərdən keçir",
        "talk": "Müzakirə",
        "views": "Görünüş",
index d2f090e..52c7e41 100644 (file)
@@ -24,7 +24,7 @@
        "tog-extendwatchlist": "Daweiterde Beówochtungslisten",
        "tog-usenewrc": "Endarunga vo \"Lezde Endarunga\" und vo \"Mei Beobochtd\" noch Seitn gruppian",
        "tog-numberheadings": "Ywerschriften autómaatisch nummerrirn",
-       "tog-showtoolbar": "Beorweiten-Werkzeigleisten åzoang (JavaScript werd braucht)",
+       "tog-showtoolbar": "Zoag de Edit Toolbar (JavaScript nedig)",
        "tog-editondblclick": "Seiten mid am Dóppedrucker beorweiden (JavaScript werd braucht)",
        "tog-editsectiononrightclick": "Oahzelne Obschnitt mid am Rechtsdrucker beorweiten (JavaScript werd braucht)",
        "tog-watchcreations": "Voh mir söwer eihgstöde Seiten autómaatisch beówochten",
        "talkpagelinktext": "Dischkrian",
        "specialpage": "Speziaalseiten",
        "personaltools": "Mei Weakzeig",
-       "postcomment": "Neicher Obschnit",
        "articlepage": "Seiteninhoid åzoang",
        "talk": "Dischkrian",
        "views": "Osichtn",
        "externaldberror": "Entweder es ligt a Feeler bai da externen Authentifiziarung vur oder du derfst dai externs Benytzerkonto ned aktualisirn.",
        "login": "Eilogga",
        "nav-login-createaccount": "Eilogga / Konto olegn",
-       "loginprompt": "Zua Omejdung miassen Cookies aktiviat sei.",
        "userlogin": "Eilogga / Konto olegn",
        "userloginnocreate": "Åmöden",
        "logout": "Obmöden",
        "linkstoimage-more": "Es {{PLURAL:$1|valinkt|valinkn}} mea wia {{PLURAL:$1|oa Seitn |$1 Seitn}} auf de Datei.\nDe foignde Listn zaagt netta {{PLURAL:$1|in easten Link|de easten $1 Links}} auf de Datei.\nA [[Special:WhatLinksHere/$2|voiständige Listn]] gibt's aa.",
        "nolinkstoimage": "De Datei wead vo koana Seitn gnutzt.",
        "morelinkstoimage": "[[Special:WhatLinksHere/$1|Weidare Links]] fia de Datei.",
+       "linkstoimage-redirect": "$1 (Dateiweidaloatung) $2",
        "duplicatesoffile": "{{PLURAL:$1|D'foignde Datei is a Duplikat|De foigndn $1 Datein han Duplikate}} vu dea Datei ([[Special:FileDuplicateSearch/$2|weidare Deteus]]):",
        "sharedupload": "De Datei stãmmt aus $1 und deaf bei ãndare Projekte vawendt wean.",
        "sharedupload-desc-there": "De Datei stãmmt aus $1 und deaf bei ãndera Projekte vawendt wean. Schau auf'd [$2 Dateibeschreibungsseitn] fia weidare Infoamazionen.",
        "filedelete-intro": "Du léschst dé Daatei '''„[[Media:$1|$1]]“'''.",
        "mimesearch-summary": "Auf dieser Spezialseite können die Dateien nach dem MIME-Typ gefiltert werden. Die Eingabe muss immer den Medien- und Subtyp beinhalten: <code>image/jpeg</code> (siehe Bildbeschreibungsseite).",
        "download": "Owerlooden",
+       "listredirects": "Weidaloatunga",
        "unusedtemplates": "Net benutzte Vorlagen",
        "unusedtemplateswlh": "Aundre Links",
        "randompage": "Zuafoisseitn",
+       "randomredirect": "Zuafällige Weidaloatung",
        "statistics": "Statistik",
        "statistics-articles": "Inhoidsseiten",
        "statistics-pages": "Seiten",
        "statistics-edits-average": "Beorweitungen pró Seiten im Durchschnit",
        "statistics-views-total": "Seitenaufruaff gsåmmt",
        "statistics-mostpopular": "Dé am moastbsuachten Seiten",
-       "doubleredirects": "Doppede Weiderloatungen",
+       "doubleredirects": "Doppede Weidaloatunga",
+       "doubleredirectstext": "Af dea Seitn stengan Weidaloatunga, de wo af Weidaloatunga zoang.\n\nA <del>duachgstrichane</del> Eidrog is scho repariad worn.",
+       "double-redirect-fixed-maintenance": "Doppede Weidaloatunga vo [[$1]] af [[$2]] mid oana Aktion reparian.",
+       "brokenredirectstext": "Af dera Spezialseitn stengan Weidaloatunga af Artiken, de wo s ned gibt.",
        "brokenredirects-edit": "werkeln",
        "brokenredirects-delete": "léschen",
        "withoutinterwiki": "Seitn ohne Sprochlinks",
        "allpagesprefix": "Seiten zoang mid Präfix:",
        "allpagesbadtitle": "Da eihgeewerne Seitennaum is néd gütig: Er hod éntwéder a vurauhgstöds Sprooch-, a Interwiki-Kyrzel óder enthoitt oah óder mererne Zeichen, dé in d' Seitennaumen néd vawendt wern derffm.",
        "allpages-bad-ns": "Dén Naumensraum „$1“ gibts in {{SITENAME}} néd.",
+       "allpages-hide-redirects": "Weidaloatunga ausblendn",
        "categories": "Kategorina",
        "special-categories-sort-count": "Sortiarung noch da Auhzoi",
        "special-categories-sort-abc": "Sortiarung noch 'm Alfabet",
        "listgrouprights-addgroup-self-all": "Kauh olle Gruppm zum oagern Kóntó dazuadoah",
        "mailnologin": "Du bist néd auhgmödt",
        "emailuser": "Mail an den Nutza",
-       "emailpage": "E-Mail aun Benutzer",
-       "noemailtitle": "Koah E-Mail-Adress",
+       "emailpage": "E-Mail an Nutza",
+       "noemailtitle": "Koa Mail-Adress",
        "emailfrom": "Voh:",
        "emailto": "Aun:",
        "emailsubject": "Bedreff:",
        "ipbreason-dropdown": "* Oigmoahne Sperrgrynd\n** Eihfyng voh voische Informaziónen\n** Laarn voh Seiten\n** Massenweiss Eihfyng voh externe Links\n** Eihstön voh unsinnige Inhoite auf Seiten\n** néd åbrochts Vahoiden\n** Missbrauch mid mererne Benutzerkontós\n** néd geigneter Benutzernåm",
        "ipb-hardblock": "Auhgmödte Benutzer dodrauh hindern, daas Beorweitungen unter derer IP-Adress vurgnummer wern",
        "ipbcreateaccount": "D' Erstöung voh Benutzerkóntós vahindern",
-       "ipbemailban": "E-Mail-Vasånd sperrn",
+       "ipbemailban": "E-Mail-Vasand spean",
        "ipbenableautoblock": "Sperr dé aktuö voh dém Benutzer gnutzde IP-Adress sówia autómaatisch olle fóiganden, voh dénen aus er Beorweitungen óder 's Auhléng voh Benutzerkóntós vasuacht.",
        "ipbsubmit": "IP-Adress/Benutzer sperrn",
        "ipbother": "Åndre Dauer (auf englisch):",
        "ipusubmit": "Freigem",
        "unblocked": "[[User:$1|$1]] is freigem worn",
        "unblocked-id": "Sperr-ID $1 is fraigeem worn",
+       "blocklist": "Gspeade Nutza",
        "ipblocklist": "Gsperrte Nutza",
        "ipblocklist-legend": "Suach noch am gsperrden Benytzer",
        "createaccountblock": "'s erstön voh Benutzerkóntós is gsperrd",
        "unblocklink": "Freigebm",
        "change-blocklink": "Sperr endan",
        "contribslink": "Beidreg",
-       "emaillink": "E-Póst schicker",
+       "emaillink": "E-Mail vaschicka",
        "autoblocker": "Autómaatische Sperr, wei du a gmoahsaume IP-Adress mim [[User:$1|$1]] bnutzd. Grund voh da Benutzersperrn: „$2“.",
        "blocklogpage": "Sperrlogbuach",
        "blocklog-showlog": "{{GENDER:$1|Der Benutzer|Dé Benutzerrin|Der Benutzer}} do is schoh friarer gsperrd worn. Es fóigt a Eihtrog aus'm Benutzersperrlogbiaché:",
        "block-log-flags-anononly": "netter Anónyme",
        "block-log-flags-nocreate": "Es Olegn vo Nutzakontn is gsperrt",
        "block-log-flags-noautoblock": "Autóblóck deaktivierd",
-       "block-log-flags-noemail": "E-Post vaschicka gspead",
+       "block-log-flags-noemail": "Mail vaschicka gspead",
        "unlockdb": "Daatenbaunk freigeem",
        "unlockconfirm": "Ja, i mecht de Datenbank freigem.",
        "unlockbtn": "Datenbank freigem",
        "movepagebtn": "Seitn vaschiam",
        "pagemovedsub": "s'Vaschiam håd highaud",
        "movepage-moved": "'''D'Seitn „$1“ is nåch „$2“ vaschom woan.'''",
-       "movepage-moved-redirect": "Es is a Weiderloatung erstöd worn.",
+       "movepage-moved-redirect": "A Weidaloatung eigricht worn.",
+       "movepage-moved-noredirect": "De Eirichtung vo oana Weidaloatung is vahindat worn.",
        "articleexists": "Unter dém Naum existierd schoh a Seiten. Bittscheh nimm an aundern Naumen her.",
        "movetalk": "Waunns geet, d' Dischkrierseiten aa midvaschiam",
        "movelogpage": "Vaschiabungs-Logbuach",
        "lastmodifiedatby": "Dé Seiten is zletzt am $1 um $2 voh $3 gänderd worn.",
        "othercontribs": "Basiard auf da Orweid voh $1",
        "creditspage": "Seiteninformaziónen",
+       "pageinfo-redirects-name": "Ozoi vo de Weidaloatunga zua dea Seitn",
+       "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|Weidaloatung|Weidaloatunga}}; $3 {{PLURAL:$3|Untaseitn|Untaseitn}})",
+       "pageinfo-redirectsto": "Weidaloatunga af",
        "markedaspatrollederrortext": "Du muasst a Seitenänderrung auswön",
        "deletedrevision": "Oide Version $1 glöscht.",
        "filedelete-missing": "De Datei „$1“ ko net glöscht wern, weils es net gibt.",
        "htmlform-submit": "Speichern",
        "htmlform-reset": "Änderrungen ryckgängég mochen",
        "htmlform-selectorother-other": "Åndre",
+       "logentry-move-move_redir": "$1 hod de Seitn $3 af $4 {{GENDER:$2|verschom}} und hod dabei a Weidaloatung ibaschriem",
+       "logentry-move-move_redir-noredirect": "$1 hod de Seitn $3 af $4 {{GENDER:$2|verschom}} und dabei a Weidaloatung ibaschriem, ohne a neiche ozlegn",
        "searchsuggest-search": "Suach",
        "searchsuggest-containing": "Voitextsuach noch ..."
 }
index dec808c..8f0e7d1 100644 (file)
@@ -8,7 +8,8 @@
                        "Reedy",
                        "ZxxZxxZ",
                        "아라",
-                       "RigiMahnoor"
+                       "RigiMahnoor",
+                       "Oldstoneage"
                ]
        },
        "tog-underline": ":لینکانآ خط کش",
        "talkpagelinktext": "گپ کن",
        "specialpage": "حاصین صفحه",
        "personaltools": "شخصی وسایل",
-       "postcomment": "نوکین بخش",
        "articlepage": "محتوا صفحه به گند",
        "talk": "بحث",
        "views": "چارگان",
        "externaldberror": "یک حطا دیتابیس تصدیق هویت دراییگی هست یا شما را اجازت نیست وتی حساب درایی په روچ کنیت.",
        "login": "ورود",
        "nav-login-createaccount": "ورود/شرکتن حساب",
-       "loginprompt": "شما بایدن په وارد بیگ ته {{SITENAME}} کوکی فعال کنیت",
        "userlogin": "ورود/شرکتن حساب",
        "userloginnocreate": "لاگین",
        "logout": "در بیگ",
        "hebrew-calendar-m12-gen": "الول",
        "unknown_extension_tag": "ناشناس برجسب الحاق  \"$1\"",
        "duplicate-defaultsort": "هژاری: ترتیب پیش فرض «$2» ترتیب پیش فرض پیشگین «$1» را باطل کنت.",
-       "version": "نسخه",
+       "version": "نسخة",
        "version-extensions": "نصب بوتگیت الحاق آن",
        "version-specialpages": "حاصین صفحات",
        "version-parserhooks": "تجزیه کنوک گیر کت",
index 7edefda..5030c0d 100644 (file)
@@ -12,9 +12,9 @@
                ]
        },
        "tog-underline": "Linyahan an kilyawan:",
-       "tog-hideminor": "Tagóon an mga saradít na paghirá sa nakakaági pa sanáng pagbabàgo",
+       "tog-hideminor": "Tagoon an saradít na paghira sa nakakaagi pa sanang pagbabàgo",
        "tog-hidepatrolled": "Tagóa an patrolyadong mga paghirá sa nakakaági pa sanáng pagbabàgo",
-       "tog-newpageshidepatrolled": "Tagóa an patrolyadong mga pahina gikan sa listahan kan bàgong pahina",
+       "tog-newpageshidepatrolled": "Tagoon an patrolyadong mga pahina gikan sa listahan kan bàgong pahina",
        "tog-extendwatchlist": "Palakbanga an bantay-listahan (watchlist) na maipahiling an gabos na pinagbago, bako sana an pinakahurihang binago",
        "tog-usenewrc": "Pangrupong mga kaliwatan sa kada pahina kan mga dae pa sana nahaloy na mga kaliwatan asin bantay-listahan",
        "tog-numberheadings": "Tolos-bilang na mga pamayohán",
@@ -27,7 +27,7 @@
        "tog-watchdeletion": "Idagdag an mga pahina asin mga sagunson na ako an nagpura sa sakong bantay-listahan",
        "tog-minordefault": "Markahán gabos na saradit na pagliwat sa paaging panugmad",
        "tog-previewontop": "Ipahilíng an patànaw bàgo an kahon nin paghirá",
-       "tog-previewonfirst": "Ipahilíng an patànaw sa enot na paghirá",
+       "tog-previewonfirst": "Ipahiling an patànaw sa inot na paghira",
        "tog-enotifwatchlistpages": "E-suratan mo ako kunsoarin an sarong pahina o sagunson na yaon sa sakong bantay-listahan pinagliwat",
        "tog-enotifusertalkpages": "E-koreohan ako pag pigribáyan an pahina kan sakóng olay",
        "tog-enotifminoredits": "E-suratan man ako para sa saraditon na mga pagliwat kan mga pahina asin mga sagunson",
@@ -49,8 +49,8 @@
        "tog-norollbackdiff": "Omidohon an diff matapos himoon an pagbalikot",
        "tog-useeditwarning": "Patanidan ako kunsoarin na ako nagbaya sa pahinang pigliliwat na dae naitatagama an mga kaliwatan",
        "tog-prefershttps": "Pirmeng gumamit nin sarong seguradong koneksyon kunsoarin na ika nakalaog na",
-       "underline-always": "Pirmi",
-       "underline-never": "Nungka",
+       "underline-always": "Parati",
+       "underline-never": "Dai lamang",
        "underline-default": "Kublit o kilyaw na panugmad",
        "editfont-style": "Baguhon an estilo nin kalwig sa sinasakupan",
        "editfont-default": "Kilyawang tugmad",
        "june-date": "Hunyo $1",
        "july-date": "Hulyo $1",
        "august-date": "Agosto $1",
-       "september-date": "Septiyembre $1",
-       "october-date": "Oktubre $1",
+       "september-date": "Setyembre $1",
+       "october-date": "Oktobre $1",
        "november-date": "Nobyembre $1",
-       "december-date": "Disyembre $1",
+       "december-date": "Desyembre $1",
        "pagecategories": "{{PLURAL:$1|Kategorya|Mga kategorya}}",
        "category_header": "Mga pahina sa kategoryang \"$1\"",
        "subcategories": "Mga sub-kategorya",
        "category-media-header": "Media sa kategoryang \"$1\"",
-       "category-empty": "''Ining kategorya sa presente mayong laog na mga pahina o media.\"",
+       "category-empty": "''Ining kategorya mayong laog na mga pahina o media sa ngunyan.''",
        "hidden-categories": "{{PLURAL:$1|Nakatagong kategorya|Mga nakatagong kategorya}}",
        "hidden-category-category": "Mga nakatagong kategorya",
        "category-subcat-count": "{{PLURAL:$2|Ining kategorya igwa sana kan minasunod na sub-kategorya.|Ining kategorya igwa kan minasunod {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
        "category-article-count-limited": "An minasunod na {{PLURAL:$1|pahina|$1 mga pahina}} yaon sa presenteng kategorya.",
        "category-file-count": "{{PLURAL:$2|Ining kategorya naglalaman sana kan minasunod na sagunson.|An minasunod {{PLURAL:$1|sagunson iyo|$1 na mga sagunson iyo}} sa kategoryang ini, na ginahi sa $2 sa kabilogan.}}",
        "category-file-count-limited": "An minasunod {{PLURAL:$1|na sagunson|$1 na mga sagunson}} yaon sa presenteng kategorya.",
-       "listingcontinuesabbrev": "sunód",
-       "index-category": "Hinukdoang mga pahina",
-       "noindex-category": "Bakong hinukdoang mga pahina",
+       "listingcontinuesabbrev": "sunod",
+       "index-category": "Panhinukdong mga pahina",
+       "noindex-category": "Bakong panhinukdong mga pahina",
        "broken-file-category": "Mga pahina na igwang nagkaparasa na sagunsong kilyawan",
        "about": "Manonongod",
        "article": "Laog na pahina",
        "newwindow": "(minabukas sa bàgong bintanà)",
-       "cancel": "Kanselaron",
+       "cancel": "Pondohon",
        "moredotdotdot": "Kadagdagan...",
-       "morenotlisted": "Ining listahan bako pang kumpleto.",
-       "mypage": "An Pahina",
-       "mytalk": "Orolayan",
-       "anontalk": "Olay para kaining IP address",
-       "navigation": "Nabigasyon",
+       "morenotlisted": "Kulang ining listahan.",
+       "mypage": "Pahina",
+       "mytalk": "Mag-ulay",
+       "anontalk": "Urulay para kaining IP address",
+       "navigation": "Paglibotlibot",
        "and": "&#32;asin",
        "qbfind": "Maghanap",
-       "qbbrowse": "Halungkáta",
-       "qbedit": "Liwata",
+       "qbbrowse": "Maghalungkat",
+       "qbedit": "Liwaton",
        "qbpageoptions": "Ining pahina",
        "qbmyoptions": "Sakong mga pahina",
-       "faq": "PPK (Pirmihang Pighahapot na mga kahaputan)",
-       "faqpage": "Project:PPK (Pirmihang Pighahapot na mga Kahaputan)",
-       "actions": "Mga aksyon",
+       "faq": "PH (Parating Hapot)",
+       "faqpage": "Proyekto:PH (Parating Hapot)",
+       "actions": "Mga paghiro",
        "namespaces": "Mga espasyong ngaran",
-       "variants": "Mga pinalaen",
-       "navigation-heading": "Listahan sa Nabigasyon",
+       "variants": "Mga lain pa",
+       "navigation-heading": "Hihilngan nin paglibotlibot",
        "errorpagetitle": "Salâ",
        "returnto": "Magbalik sa $1.",
        "tagline": "Gikan sa {{SITENAME}}",
-       "help": "Katabangan",
+       "help": "Tabang",
        "search": "Maghanap",
        "searchbutton": "Maghanap",
        "go": "Dumani",
        "searcharticle": "Lakaw",
-       "history": "Historiya kan pahina",
-       "history_short": "Historiya",
+       "history": "Uusipon kan pahina",
+       "history_short": "Uusipon",
        "updatedmarker": "dinagdagan poon kan sakong huring pagbisita",
-       "printableversion": "Puwede maimprintang bersyon",
+       "printableversion": "Nalilimbag na bersyon",
        "permalink": "Permanenteng kilyawan",
-       "print": "Imprintaron",
-       "view": "Tanawon",
-       "view-foreign": "Hilngon sa $1",
-       "edit": "Liwatón",
+       "print": "Ilimbag",
+       "view": "Tànawon",
+       "view-foreign": "Hilingon sa $1",
+       "edit": "Liwaton",
        "edit-local": "Liwaton an lokal na deskripsyon",
        "create": "Muknaon",
        "create-local": "Idugang an lokal na deskripsyon",
-       "editthispage": "Liwata ining pahina",
+       "editthispage": "Liwaton ining pahina",
        "create-this-page": "Muknaon ining pahina",
        "delete": "Puraon",
        "deletethispage": "Puraon ining pahina",
        "undeletethispage": "Balikon sa pagkapura ining pahina",
-       "undelete_short": "Bawia an pagpurà kan {{PLURAL:$1|sarong pagliwat|$1 mga pagliwat}}",
-       "viewdeleted_short": "Hilngon {{PLURAL:$1|sarong pinara na pagliwat|$1 mga pinara na pagliwat}}",
+       "undelete_short": "Bawion an {{PLURAL:$1|sarong pagliwat|$1 mga pagliwat}}",
+       "viewdeleted_short": "Hilingon {{PLURAL:$1|sarong pinara na pagliwat|$1 mga pinara na pagliwat}}",
        "protect": "Protektari",
        "protect_change": "Ribayan",
        "protectthispage": "Protektaran ining pahina",
-       "unprotect": "Ribayi an proteksyon",
-       "unprotectthispage": "Ribayi an proteksyon kaining pahina",
-       "newpage": "Bàguhong pahina",
-       "talkpage": "Orolayan ining pahina",
-       "talkpagelinktext": "Olay",
-       "specialpage": "Espesyal na Pahina",
-       "personaltools": "Personal na mga kagamitan",
-       "postcomment": "Baguhong seksyon",
-       "articlepage": "Tanawon an laog kan pahina",
-       "talk": "Orolayan",
-       "views": "Mga Tanawon",
-       "toolbox": "Mga gagamiton:",
-       "userpage": "Tanawon an pahina kan parágamit",
-       "projectpage": "Tanawon an pahina kan proyekto",
-       "imagepage": "Hilngón an pahina nin sagunson (file)",
-       "mediawikipage": "Tanawon an pahina kan mensahe",
-       "templatepage": "Tanawon an pahina kan panguyog",
-       "viewhelppage": "Tanawon an pahina nin katabangan",
-       "categorypage": "Tanawon an pahina nin kategorya",
-       "viewtalkpage": "Tanawon an orolayan",
-       "otherlanguages": "Sa ibang mga lengguwahe",
-       "redirectedfrom": "(Redirektado gikan sa $1)",
-       "redirectpagesub": "Redirektang pahina",
+       "unprotect": "Ribayan an proteksyon",
+       "unprotectthispage": "Ribayan an proteksyon kaining pahina",
+       "newpage": "Bàgong pahina",
+       "talkpage": "Urulayan ining pahina",
+       "talkpagelinktext": "Mag-ulay",
+       "specialpage": "Sadyang Pahina",
+       "personaltools": "Pansadiring mga kagamitan",
+       "articlepage": "Tànawon an laog kan pahina",
+       "talk": "Urulay",
+       "views": "Mga tànaw",
+       "toolbox": "Mga gamit:",
+       "userpage": "Tànawon an pahina kan paragamit",
+       "projectpage": "Tànawon an pahina kan proyekto",
+       "imagepage": "Hilingon an pahina nin sagunson (file)",
+       "mediawikipage": "Tànawon an pahina kan mensahe",
+       "templatepage": "Tànawon an pahina kan panguyog",
+       "viewhelppage": "Tànawon an pahina nin pagtabang",
+       "categorypage": "Tànawon an pahina nin kategorya",
+       "viewtalkpage": "Tànawon an urulay",
+       "otherlanguages": "Sa ibang mga tataramon",
+       "redirectedfrom": "(Inilikay gikan sa $1)",
+       "redirectpagesub": "Likay na pahina",
        "lastmodifiedat": "Ining pahina huring pinagbago kan $1, mga alas $2.",
        "viewcount": "Ining pahina pinaglaog nin {{PLURAL:$1|sarong beses|nin $1 beses}}.",
        "protectedpage": "Protektadong pahina",
-       "jumpto": "Magluksó sa:",
-       "jumptonavigation": "nabigasyon",
+       "jumpto": "Maglukso sa:",
+       "jumptonavigation": "paglibotlibot",
        "jumptosearch": "hanapon",
        "view-pool-error": "Sori tabi, an mga server kargado sa oras na ini.\nGrabe kadakol an mga paragamit na pinagprubaran mahiling an pahinang ini.\nMakihalat tabi nin kadikit na panahon bago ka magprubara na makapaglaog sa pahinang ini.\n\n$1",
        "generic-pool-error": "Sori tabi, an mga serbidor grabe kakargado sa oras na ini. Kadakulon na gayo an mga paragamit na minaprubar na hilngon ining kaggikanan. Tabi pakihalat kadikit bago ka magprubar otro na makapaglaog sa kaggikanang ini.",
        "pool-queuefull": "An grupong panproseso panoon",
        "pool-errorunknown": "Bakong bistadong sala",
        "aboutsite": "Dapít sa {{SITENAME}}",
-       "aboutpage": "Project:Mapanonongód",
+       "aboutpage": "Project:Mapanonongod",
        "copyright": "An kalamnan manunumpungan sa laog kan $1 o baya notado na ining laen.",
        "copyrightpage": "{{ns:project}}:Mga Katanosang pansurat",
        "currentevents": "Sa ngunyan na mga pangyayari",
        "newmessageslinkplural": "{{PLURAL:$1|sarong baguhong mensahe|999=baguhong mga mensahe}}",
        "newmessagesdifflinkplural": "kahurihan na {{PLURAL:$1|kaliwatan|999=mga kaliwatan}}",
        "youhavenewmessagesmulti": "Igwa ka nin mga bàgong mensahe sa $1",
-       "editsection": "liwatón",
-       "editold": "Liwatón",
-       "viewsourceold": "hilingón an ginikánan",
-       "editlink": "liwatón",
-       "viewsourcelink": "tanawon an ginikanan",
-       "editsectionhint": "Liwatón an seksyon: $1",
-       "toc": "Mga laóg",
-       "showtoc": "ipahilíng",
-       "hidetoc": "tagóon",
-       "collapsible-collapse": "Pinahalipot",
-       "collapsible-expand": "Pinahalawig",
-       "thisisdeleted": "Hilingón o isulít an $1?",
-       "viewdeleted": "Hilingón an $1?",
+       "editsection": "liwaton",
+       "editold": "liwaton",
+       "viewsourceold": "hilingon an ginikanan",
+       "editlink": "liwaton",
+       "viewsourcelink": "tànawon an ginikanan",
+       "editsectionhint": "Liwaton an seksyon: $1",
+       "toc": "Mga laog",
+       "showtoc": "ipahiling",
+       "hidetoc": "tagoon",
+       "collapsible-collapse": "Ibagsak",
+       "collapsible-expand": "Ibuka",
+       "thisisdeleted": "Hilingon o isulit an $1?",
+       "viewdeleted": "Hilingon an $1?",
        "restorelink": "{{PLURAL:$1|sarong pinagpurang pagliwat|$1 na pinagpurang mga pagliwat}}",
        "feedlinks": "Hungit:",
        "feed-invalid": "Imbalidong tipo nin hungit sa subkripsyon.",
        "filereadonlyerror": "Dae kinayang baguhon an sagunson (file) \"$1$ nin huli ta an repositoryo kan sagunson \"$2\" yaon sa kamugtakan na basahon sana.\n\nAn administrador na iyo an nagkandado kaini nagpahayag kaining kapaliwanagan: \"$3\".",
        "invalidtitle-knownnamespace": "Imbalidong titulo na igwang espasyadong ngaran na \"$2\" asin teksto na \"$3\"",
        "invalidtitle-unknownnamespace": "Imbalidong titulo na igwang nin bakong bistado na bilang kan espasyadong ngaran na $1 asin teksto na \"$2\"",
-       "exception-nologin": "Dae ka nakalaog",
+       "exception-nologin": "Dai ka nakalaog",
        "exception-nologin-text": "Tabi man [[Special:Userlogin|maglaog]]na tanganing makalangkay sa pahinang ini o aksyon.",
        "exception-nologin-text-manual": "Tabi man $1 na tanganing makalangkay sa pahinang ini o aksyon.",
        "virus-badscanner": "Raot na kasalansanan: Bakong bistadong virus scanner: ''$1''",
        "userlogin-yourname": "Paragamit-na-Ngaran",
        "userlogin-yourname-ph": "Ikaag an saimong paragamit-na-ngaran",
        "createacct-another-username-ph": "Ikaag an paragamit-na-ngaran",
-       "yourpassword": "Pasa-taramon:",
-       "userlogin-yourpassword": "Sikretong panlaog",
+       "yourpassword": "Sekretong Panlaog",
+       "userlogin-yourpassword": "Sekretong Panlaog",
        "userlogin-yourpassword-ph": "Ikaag an saimong sekretong panlaog",
        "createacct-yourpassword-ph": "Ikaag an sekretong panlaog",
        "yourpasswordagain": "Pakilaog giraray kan sekretong panlaog:",
        "externaldberror": "Igwa gayod sala sa arinman kan patunay sa datos-sarayan o ika dae pinagtugutan na bâgohon an saimong panluwas na panindog.",
        "login": "Maglaog",
        "nav-login-createaccount": "Maglaog / magmukna nin panindog",
-       "loginprompt": "Ika kaipong paganahon an mga cookies tanganing makalaog sa {{SITENAME}}.",
        "userlogin": "Maglaog / magmukna nin panindog",
        "userloginnocreate": "Maglaog ka",
        "logout": "Magluwas",
-       "userlogout": "Magluwás",
+       "userlogout": "Magluwas",
        "notloggedin": "Dae ka nakalaog",
        "userlogin-noaccount": "Mayo ka nin panindog?",
        "userlogin-joinproject": "Mag-ayon{{SITENAME}}",
        "resettokens-resetbutton": "Pakibaguha an pinagpiling mga paduos",
        "bold_sample": "Mahìbog na teksto",
        "bold_tip": "Mahìbog na teksto",
-       "italic_sample": "Itálikong teksto",
-       "italic_tip": "Tekstong Itáliko",
+       "italic_sample": "Italikong teksto",
+       "italic_tip": "Tekstong Italiko",
        "link_sample": "Titulo nin sugpon",
        "link_tip": "Panlaog na sugpon",
        "extlink_sample": "http://www.example.com títulong sugpon",
        "editpage-notsupportedcontentformat-text": "An pormat nin kalamnan na $1 bakong suportado kan modelong kalamnan na $2.",
        "content-model-wikitext": "wiki-teksto",
        "content-model-text": "yanong-teksto",
-       "content-model-javascript": "Java-Kurit",
+       "content-model-javascript": "JavaScript",
        "content-model-css": "CSS",
        "expensive-parserfunction-warning": "'''Patanid tabi:''' Ining pahina naglalaman nin grabe kadakulon na ekspensibong programang pambaranga sa punksyon nin mga pag-aapod.\n\nIni dapat magkaigwa nin menos sanang $2 {{PLURAL:$2|apod|mga apod}}, igwa na {{PLURAL:$1|ngunyan nin $1 apod|ngunyan nin $1 mga apod}}.",
        "expensive-parserfunction-category": "Mga pahina na igwa nin grabe kadakulon na mga ekspensibong programang pambaranga sa punksyon nin mga pag-aapod",
        "cantcreateaccount-text": "An pagbukas nin account halì sa IP na ('''$1''') binágat ni [[User:$3|$3]].\n\n''$2'' an rason na pigtao ni $3",
        "viewpagelogs": "\nHilingon an mga katalaanan para sa pahinang ini",
        "nohistory": "Mayong paghirá nin uusipón sa pahinang ini.",
-       "currentrev": "Sa ngonyan na pagpakarháy",
-       "currentrev-asof": "Pinakahuring pagbabago kan $1",
-       "revisionasof": "Rebisyon poon kan $1",
+       "currentrev": "Ppagpakarhay sa ngunyan",
+       "currentrev-asof": "Pinakahuring pagpakarhay kan $1",
+       "revisionasof": "Pagpakarhay poon kan $1",
        "revision-info": "Rebisyon poon kan $1 ni $2",
-       "previousrevision": "←Lumàon na rebisyon",
-       "nextrevision": "Mas bàguhon na rebisyon→",
+       "previousrevision": "← Dating pagpakarhay",
+       "nextrevision": "Bagong pagpakarhay →",
        "currentrevisionlink": "Sa ngunyan na rebisyon",
-       "cur": "sa ngunyán",
+       "cur": "sa ngunyan",
        "next": "sunod",
-       "last": "sa nakaagi",
-       "page_first": "enot",
+       "last": "dati",
+       "page_first": "inot",
        "page_last": "huri",
-       "histlegend": "Kalaenan sa pilian: Markahan an mga kahon nin radyo kan mga rebisyon tanganing komapararon asin pinduta an \"enter\" o an pindutan na yaon sa irarom.<br />\nKabalaynan: '''({{int:cur}})''' = kalaenan sa pinakahuring rebisyon, '''({{int:last}})''' = kalaenan sa sinundan na rebisyon, '''{{int:minoreditletter}}''' = dikiton na pagliwat.",
-       "history-fieldset-title": "Historiya nin kinilyawan",
+       "histlegend": "Lain sa pilian: Markahan an mga kahon nin radyo kan mga pagpakarhay tanganing komapararon asin pinduta an \"enter\" o an pindutan na yaon sa irarom.<br />\nKabalaynan: '''({{int:cur}})''' = kalaenan sa pinakahuring rebisyon, '''({{int:last}})''' = kalaenan sa sinundan na rebisyon, '''{{int:minoreditletter}}''' = dikiton na pagliwat.",
+       "history-fieldset-title": "Uusipon nin paghalungkat",
        "history-show-deleted": "Pinagpura sana",
        "histfirst": "pinakalumaon",
        "histlast": "pinakabaguhon",
        "historysize": "({{PLURAL:$1|sarong byte|$1 mga bytes}})",
        "historyempty": "(mayong laog)",
-       "history-feed-title": "Uusipón kan pagpakaraháy",
-       "history-feed-description": "Uusipón kan pagpakaraháy para sa pahinang ini sa wiki",
+       "history-feed-title": "Uusipon kan pagpakarhay",
+       "history-feed-description": "Uusipon kan pagpakarahay para sa pahinang ini sa wiki",
        "history-feed-item-nocomment": "$1 sa $2",
-       "history-feed-empty": "Mayò man an hinágad na pahina.\nPwedeng pigparà na ini sa wiki, o tinàwan nin bàgong pangaran.\nProbaran tabì an [[Special:Search|pighahanap sa wiki]] para sa mga pahinang dapít.",
+       "history-feed-empty": "Mayò man an hinagad na pahina.\nPwedeng pigparà na ini sa wiki, o tinàwan nin bàgong pangaran.\nProbaran tabì an [[Special:Search|pighahanap sa wiki]] para sa mga pahinang dapít.",
        "rev-deleted-comment": "(pagliwat na sumaryo pinaghale)",
        "rev-deleted-user": "(hinalì an parágamit)",
        "rev-deleted-event": "(talaan kan aksyon pinaghale)",
        "rclistfrom": "Ipahiling an baguhon na mga kaliwatan magpoon kan $3 $2",
        "rcshowhideminor": "$1 saradit na mga pagliwat",
        "rcshowhideminor-show": "Ipatanaw",
-       "rcshowhideminor-hide": "Tagoa",
+       "rcshowhideminor-hide": "Itago",
        "rcshowhidebots": "$1 mga gantaw",
        "rcshowhidebots-show": "Ipatanaw",
-       "rcshowhidebots-hide": "Tagoa",
+       "rcshowhidebots-hide": "Itago",
        "rcshowhideliu": "$1 rehistradong mga paragamit",
        "rcshowhideliu-show": "Ipatanaw",
-       "rcshowhideliu-hide": "Tagoa",
+       "rcshowhideliu-hide": "Itago",
        "rcshowhideanons": "$1 mga dae bistong paragamit",
        "rcshowhideanons-show": "Ipatanaw",
-       "rcshowhideanons-hide": "Tagoa",
+       "rcshowhideanons-hide": "Itago",
        "rcshowhidepatr": "$1 patrolyadong mga pagliwat",
        "rcshowhidepatr-show": "Ipatanaw",
        "rcshowhidepatr-hide": "Tagoa",
        "rcshowhidemine": "$1 sakong mga pagliliwat",
        "rcshowhidemine-show": "Ipatanaw",
-       "rcshowhidemine-hide": "Tagoa",
+       "rcshowhidemine-hide": "Itago",
        "rclinks": "Ipahilíng an $1 huring mga kaliwatan sa laog nin huring $2 na mga aldaw<br />$3",
        "diff": "kalaenan",
        "hist": "sagaysay",
        "duplicate-defaultsort": "'''Patanid tabi:''' An susing panugmad kan salansan na \"$2\" minasalimbaw sa dating susing panugmad kan salansan na \"$1\".",
        "version": "Bersyon",
        "version-extensions": "Instaladong mga ekstensyon",
+       "version-skins": "Mga kublit",
        "version-specialpages": "Espesyal na mga pahina",
        "version-parserhooks": "Mga pangawil kan parser",
        "version-variables": "Mga kabalanggayahan",
        "version-antispam": "Pan-spam na pangataman",
-       "version-skins": "Mga kublit",
        "version-other": "An iba pa",
        "version-mediahandlers": "Mga Midyang Tagakapot",
        "version-hooks": "Mga pangawil",
index 238e1b5..682264a 100644 (file)
        "talkpagelinktext": "гутаркі",
        "specialpage": "Спэцыяльная старонка",
        "personaltools": "Асабістыя прылады",
-       "postcomment": "Новая сэкцыя",
        "articlepage": "Паказаць старонку зьместу",
        "talk": "Абмеркаваньне",
        "views": "Рэжымы",
        "externaldberror": "Адбылася памылка аўтэнтыфікацыі з дапамогай вонкавай базы зьвестак, ці Вам не дазволена абнаўляць свой рахунак.",
        "login": "Увайсьці",
        "nav-login-createaccount": "Уваход / стварэньне рахунку",
-       "loginprompt": "Вы павінны дазволіць cookie для ўваходу ў {{GRAMMAR:вінавальны|{{SITENAME}}}}.",
        "userlogin": "Увайсьці ў сыстэму",
        "userloginnocreate": "Увайсьці",
        "logout": "Выйсьці",
        "license": "Ліцэнзія:",
        "license-header": "Ліцэнзія",
        "nolicense": "Ня выбраная",
+       "licenses-edit": "Рэдагаваць парамэтры ліцэнзіі",
        "license-nopreview": "(Прагляд недаступны)",
        "upload_source_url": " (слушны, агульнадаступны URL-адрас)",
        "upload_source_file": " (файл на Вашым кампутары)",
+       "listfiles-delete": "выдаліць",
        "listfiles-summary": "На гэтай спэцыяльнай старонцы паказаныя ўсе загружаныя файлы.",
        "listfiles_search_for": "Пошук па назьве файла:",
        "imgfile": "файл",
        "wantedpages-badtitle": "Няслушная назва сярод вынікаў: $1",
        "wantedfiles": "Запатрабаваныя файлы",
        "wantedfiletext-cat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са зьнешніх сховішчаў могуць знаходзіцца ў сьпісе без уліку іх існаваньня. Любыя такія няслушныя ўваходжаньні будуць <del>выкрасьленыя</del>. Дадаткова, старонкі, якія ўбудоўваюць неіснуючыя файлы прыведзеныя на [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Наступныя файлы ўжваюцца, але не існуюць. Дадаткова, старонкі, у якія ўключаныя няісныя файлы, прыведзеныя ў [[:$1]].",
        "wantedfiletext-nocat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са зьнешніх сховішчаў могуць знаходзіцца ў сьпісе без уліку іх існаваньня. Любыя такія няслушныя ўваходжаньні будуць <del>выкрасьленыя</del>.",
+       "wantedfiletext-nocat-noforeign": "Наступныя файлы выкарыстоўваюцца, але іх няма.",
        "wantedtemplates": "Запатрабаваныя шаблёны",
        "mostlinked": "Старонкі, на якія найчасьцей спасылаюцца",
        "mostlinkedcategories": "Катэгорыі з найбольшай колькасьцю старонак",
        "timezone-utc": "UTC",
        "unknown_extension_tag": "Невядомы тэг пашырэньня «$1»",
        "duplicate-defaultsort": "Папярэджаньне: Ключ сартыроўкі па змоўчваньні «$2» замяняе папярэдні ключ сартыроўкі па змоўчваньні «$1».",
+       "duplicate-displaytitle": "<strong>Папярэджаньне:</strong> назва для адлюстраваньня «$2» перапісвае ранейшую назву для адлюстраваньня «$1».",
        "version": "Вэрсія",
        "version-extensions": "Усталяваныя пашырэньні",
        "version-skins": "Усталяваныя тэмы афармленьня",
        "pagelang-language": "Мова",
        "pagelang-use-default": "Ужываць мову па змоўчаньні",
        "pagelang-select-lang": "Абярыце мову",
-       "right-pagelang": "Зьмяніць мову старонкі"
+       "right-pagelang": "Зьмяніць мову старонкі",
+       "action-pagelang": "зьмену мовы старонкі",
+       "log-name-pagelang": "Журнал зьменаў мовы"
 }
index e64d9d8..094e669 100644 (file)
        "talkpagelinktext": "Размовы",
        "specialpage": "Адмысловая старонка",
        "personaltools": "Асабістыя прылады",
-       "postcomment": "Новы раздзел",
        "articlepage": "Паказаць старонку змесціва",
        "talk": "Размовы",
        "views": "Віды",
        "pool-timeout": "Выйшаў час чакання блакіроўкі",
        "pool-queuefull": "Чарга запытаў перапоўнена",
        "pool-errorunknown": "Невядомая памылка",
+       "pool-servererror": "Служба лічыльніка пулу недаступная ($1).",
        "aboutsite": "Пра {{GRAMMAR:вінавальны|{{SITENAME}}}}",
        "aboutpage": "Project:Пра {{GRAMMAR:вінавальны|{{SITENAME}}}}",
        "copyright": "Матэрыял даступны на ўмовах $1 (калі не пазначана іншае).",
        "nospecialpagetext": "<strong>Вы звярнуліся па няправільную адмысловую старонку.</strong>\n\nПералік правільных адмысловых старонак ёсць на [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Памылка",
        "databaseerror": "Памылка базы дадзеных",
+       "databaseerror-text": "Здарылася памылка запыту да базы звестак.\nГэта можа ўказваць на няспраўнасць у праграме.",
+       "databaseerror-textcl": "Здарылася памылка запыту да базы звестак.",
        "databaseerror-query": "Запыт: $1",
        "databaseerror-function": "Функцыя: $1",
        "databaseerror-error": "Памылка: $1",
        "externaldberror": "Або памылка вонкавай аўтэнтыкацыі ў базе дадзеных, або вам не дазволена абнаўляць свой вонкавы рахунак.",
        "login": "Увайсці ў сістэму",
        "nav-login-createaccount": "Увайсці ў сістэму / стварыць рахунак",
-       "loginprompt": "Каб уваходзіць у сістэму {{SITENAME}}, трэба дазволіць у браўзеры квіткі (кукі).",
        "userlogin": "Увайсці ў сістэму / стварыць рахунак",
        "userloginnocreate": "Увайсці",
        "logout": "Выйсці з сістэмы",
        "changeemail-submit": "Змяніць адрас электроннай пошты:",
        "changeemail-cancel": "Адмена",
        "changeemail-throttled": "Надта штмат спробаў увайсці пад гэтым рахункам. Пачакайце $1 перад тым, як спрабаваць ізноў.",
+       "resettokens": "Скінуць токены",
+       "resettokens-text": "Вы можаце пераўстанавіць токены, якія дазваляюць атрымліваць доступ да пэўных прыватных звестак, звязаных з вашым уліковым запісам.\n\nВы мусіце скінуць токены, калі выпадкова падзяліліся імі з кім-небудзь, ці ваш уліковы запіс быў скампраметаваны.",
+       "resettokens-no-tokens": "Няма чаго скідваць.",
+       "resettokens-legend": "Скідванне токенаў",
+       "resettokens-tokens": "Токены:",
        "resettokens-token-label": "$1 (актуальнае значэнне: $2)",
+       "resettokens-watchlist-token": "Токен струменя (Atom/RSS) [[Special:Watchlist|зменаў старонак у вашым спісе назірання]]",
+       "resettokens-done": "Токены скінуты.",
+       "resettokens-resetbutton": "Скінуць выбраныя токены",
        "bold_sample": "Цёмны тэкст",
        "bold_tip": "Цёмны тэкст",
        "italic_sample": "Курсіўны тэкст",
        "right-userrights-interwiki": "Правіць дазволы ўдзельнікаў на іншых вікі",
        "right-siteadmin": "Замыкаць і адмыкаць базу даных",
        "right-override-export-depth": "Экспартаваць старонкі, у тым ліку звязаныя, да глыбіні спасылак 5.",
-       "right-sendemail": "Адправіць па электроннай пошце іншым карыстальнікам",
-       "right-passwordreset": "пÑ\80аглÑ\8fд Ñ\8dлекÑ\82Ñ\80оннÑ\8bÑ\85 Ð»Ñ\96Ñ\81Ñ\82оÑ\9e Ñ\81а Ð·Ð¼Ñ\8fненнем пароля",
+       "right-sendemail": "Адпраўляць электронныя лісты іншым удзельнікам",
+       "right-passwordreset": "Ð\91аÑ\87Ñ\8bÑ\86Ñ\8c Ñ\8dлекÑ\82Ñ\80оннÑ\8bÑ\8f Ð»Ñ\96Ñ\81Ñ\82Ñ\8b Ð°Ð± Ð·Ð¼Ñ\8fненнÑ\96 пароля",
        "newuserlogpage": "Журнал рэгістрацыі ўдзельнікаў",
        "newuserlogpagetext": "Гэта журнал рэгістрацыі новых удзельнікаў.",
        "rightslog": "Журнал правоў удзельнікаў",
        "license": "Ліцэнзіяванне:",
        "license-header": "Ліцэнзіяванне",
        "nolicense": "Нішто не выбрана",
+       "licenses-edit": "Правіць параметры ліцэнзіі",
        "license-nopreview": "(без перадпаказу)",
        "upload_source_url": " (сапраўдны, публічна дасягальны URL)",
        "upload_source_file": " (файл на вашай машыне)",
+       "listfiles-delete": "сцерці",
        "listfiles-summary": "Гэтая службовая старонка паказвае ўсе загружаныя файлы.",
        "listfiles_search_for": "Знайсці назву выявы:",
        "imgfile": "файл",
        "wantedpages-badtitle": "Недапушчальная назва ў выніках: $1",
        "wantedfiles": "Патрэбныя файлы",
        "wantedfiletext-cat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са знешніх сховішчаў могуць знаходзіцца ў спісе без уліку іх існавання. Любыя такія няслушныя ўваходжанні будуць <del>выкрасленыя</del>. Дадаткова, старонкі, якія ўбудоўваюць неіснуючыя файлы прыведзеныя на [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Наступныя файлы выкарыстоўваюцца, хаця іх няма. Дадаткова, старонкі, што ўключаюць файлы, каторых няма, пералічаны ў [[:$1]].",
        "wantedfiletext-nocat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са знешніх сховішчаў могуць знаходзіцца ў спісе без уліку іх існавання. Любыя такія няслушныя ўваходжанні будуць <del>выкрасленыя</del>.",
+       "wantedfiletext-nocat-noforeign": "Наступныя файлы выкарыстоўваюцца, хоць іх няма.",
        "wantedtemplates": "Патрэбныя шаблоны",
        "mostlinked": "Старонкі, на якія найчасцей спасылаюцца",
        "mostlinkedcategories": "Катэгорыі з найбольшай колькасцю складнікаў",
        "protect-badnamespace-title": "Прастора імёнаў без аховы",
        "protect-badnamespace-text": "Старонкі ў гэтай прасторы імёнаў не могуць знаходзіцца пад аховай.",
        "protect-norestrictiontypes-text": "Старонка не можа ахоўвацца, таму што недаступны тыпы абмежавання.",
+       "protect-norestrictiontypes-title": "Неахоўвальная старонка",
        "protect-legend": "Пацверджанне пачатку аховы",
        "protectcomment": "Прычына:",
        "protectexpiry": "Канчаецца:",
        "import-error-interwiki": "Старонка «$1» не была імпартаваная, таму што гэтая назва зарэзерваваная для інтэрвікі.",
        "import-error-special": "Старонка «$1» не была імпартаваная, таму што яна належыць да спецыяльнай прасторы назваў, старонкі ў якой не дазволеныя.",
        "import-error-invalid": "Старонка «$1» не была імпартаваная з-за няслушнасці назвы.",
+       "import-error-unserialize": "Немагчыма дэсерыялізаваць версію $2 старонкі \"$1\". Меркавалася, што версія выкарыстоўвае мадэль змесціва $3, серыялізавана як $4.",
        "import-error-bad-location": "Версія $2, якая выкарыстоўвае мадэль змесціва $3, не можа быць запісана на старонцы \"$1\" гэтай вікі, паколькі такая мадэль не падтрымліваецца на гэтай старонцы.",
+       "import-options-wrong": "{{PLURAL:$2|1=Няправільны параметр|Няправільныя параметры}}: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "Пазначаная назва каранёвай старонкі недапушчальная.",
        "import-rootpage-nosubpage": "У прастора назваў \"$1\" каранёвай старонкі падстаронкі не дазволены.",
        "importlogpage": "Журнал імпартаванняў",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|размовы]])",
        "unknown_extension_tag": "Невядомая метка пашырэння \"$1\"",
        "duplicate-defaultsort": "Увага: прадвызначаная клавіша ўпарадкавання \"$2\" замяніла ранейшую такую клавішу \"$1\".",
+       "duplicate-displaytitle": "<strong>Папярэджанне:</strong> Паказаная назва \"$2\" перасягае ранейшую назву \"$1\".",
        "version": "Версія",
        "version-extensions": "Устаноўленыя прыстаўкі",
        "version-skins": "Устаноўленыя вокладкі",
        "version-license-title": "Ліцэнзія $1",
        "version-license-not-found": "Не знойдзена падрабязнай інфармацыі аб ліцэнзіі для гэтай прыстаўкі.",
        "version-credits-title": "Спіс аўтараў $1",
+       "version-credits-not-found": "Для гэтай прыстаўкі не знойдзена падрабязных звестак пра аўтараў.",
        "version-poweredby-credits": "Пляцоўка працуе на '''[https://www.mediawiki.org/ MediaWiki]''', капірайт © 2001-$1 $2.",
        "version-poweredby-others": "іншыя",
        "version-poweredby-translators": "перакладчыкі translatewiki.net",
+       "version-credits-summary": "Мы хацелі б адзначыць наступных асоб, якія зрабілі ўнёсак у [[Special:Version|MediaWiki]].",
        "version-license-info": "MediaWiki з'яўляецца свабодным праграмным забеспячэннем. Такім чынам, вы можаце паўторна распаўсюджваць прадукт і(або) змяняць яго на ўмовах пагаднення GNU General Public License у тым выглядзе, у якім яно публікуецца фондам Free Software Foundation; сілу мае версія (выпуск) 2 гэтага пагаднення або, на ваш выбар, навейшая версія (выпуск) пагаднення.\n\nMediaWiki распаўсюджваецца, спадзеючыся на прыдатнасць прадукта, але БЕЗ ЯКІХ-НЕБУДЗЬ ГАРАНТЫЙ, у тым ліку, без імплікаваных гарантый СПАЖЫВЕЦКАЙ ВАРТАСЦІ або ПРЫДАТНАСЦІ ДЛЯ ЯКОЙ-НЕБУДЗЬ МЭТЫ. Больш падрабязна гл. пагадненне GNU General Public License.\n\nРазам з гэтым праграмным забеспячэннем вы павінны былі атрымаць [{{SERVER}}{{SCRIPTPATH}}/COPYING копію пагаднення GNU General Public License]. Калі гэта не так, паведамце аб гэтым у фонд Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA або [//www.gnu.org/licenses/old-licenses/gpl-2.0.html атрымайце яе з Інтэрнэту].",
        "version-software": "Устаноўленыя праграмныя прадукты",
        "version-software-product": "Прадукт",
        "version-software-version": "Версія",
+       "version-entrypoints": "Уваходныя адрасы",
        "version-entrypoints-header-entrypoint": "Кропка ўваходу",
        "version-entrypoints-header-url": "URL",
        "redirect": "Перасылка да файла, ID удзельніка, старонкі ці версіі",
        "htmlform-selectorother-other": "Рознае",
        "htmlform-no": "Не",
        "htmlform-yes": "Так",
+       "htmlform-chosen-placeholder": "Выберыце параметр",
        "htmlform-cloner-create": "Дадаць яшчэ",
        "htmlform-cloner-delete": "Сцерці",
        "htmlform-cloner-required": "Неабходна хаця б адно значэнне.",
        "duration-millennia": "$1 {{PLURAL:$1|тысячагоддзе|тысячагоддзі|тысячагоддзяў}}",
        "rotate-comment": "Выява павернута на $1 {{PLURAL:$1|градус|градусы|градусаў}} па гадзіннікавай стрэлцы",
        "limitreport-title": "Звесткі прафілявання парсера:",
+       "limitreport-cputime": "Выкарыстанне часу ЦП",
        "limitreport-cputime-value": "$1 {{PLURAL:$1|секунда|секунды|секундаў}}",
+       "limitreport-walltime": "Выкарыстанне рэальнага часу",
        "limitreport-walltime-value": "$1 {{PLURAL:$1|секунда|секунды|секундаў}}",
+       "limitreport-ppvisitednodes": "Колькасць вузлоў, наведаных прэпрацэсарам",
        "limitreport-ppgeneratednodes": "Колькасць вузлоў, створаных прэпрацэсарам",
        "limitreport-postexpandincludesize": "Памер уключэнняў па разгортванні",
        "limitreport-postexpandincludesize-value": "$1/$2 {{PLURAL:$2|байт|байты|байтаў}}",
        "limitreport-expensivefunctioncount": "Колькасць працаёмкіх зваротаў да функцый парсера",
        "expandtemplates": "Разгортванне шаблонаў",
        "expand_templates_intro": "Гэта адмысловая старонка бярэ тэкст і разгортвае ў ім усе шаблоны рэкурсіўна.\nТаксама разгортвае падтрыманыя функцыі парсера кшталту\n<code><nowiki>{{</nowiki>#language:…}}</code> і зменныя віду\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nФактычна, яна разгортвае ў пэўнай ступені ўсё ў двайных фігурных дужках.",
+       "expand_templates_title": "Загаловак старонкі, для {{FULLPAGENAME}} і г.д.:",
        "expand_templates_input": "Уваходны тэкст:",
        "expand_templates_output": "Вынік",
        "expand_templates_xml_output": "Выніковы XML",
index 2231bd2..8767a1b 100644 (file)
        "tog-hidepatrolled": "हाल के परिवर्तन में मामूली संपादन छुपाईं",
        "tog-newpageshidepatrolled": "नयका पृष्ठ के सूची में से जाँचल पृष्क के छुपाँई",
        "tog-extendwatchlist": "मात्र हाल के परिवर्तन ही नाही,बल्कि सब परिवर्तन के देखावे खातिर ध्यान सूची के विस्तारित करीं",
-       "tog-usenewrc": "तà¥\81रà¤\82त à¤­à¤\88ल à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\86 à¤§à¥\8dयानसà¥\82à¤\9aà¥\80 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतनà¥\8bà¤\82 à¤\95à¥\87 à¤ªà¤¨à¥\8dना à¤\95à¥\87 à¤\85नà¥\81सार à¤¸à¤®à¥\82ह à¤®à¥\87à¤\82 à¤¬à¤¾à¤\81à¤\9fà¥\80 (à¤\9cावासà¥\8dà¤\95à¥\8dरिपà¥\8dà¤\9f à¤\86वशà¥\8dयà¤\95)",
+       "tog-usenewrc": "तà¥\81रà¤\82त à¤­à¤\88ल à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤®à¥\87à¤\82 à¤\86 à¤§à¥\8dयानसà¥\82à¤\9aà¥\80 à¤®à¥\87à¤\82 à¤­à¤\88ल à¤ªà¤°à¤¿à¤µà¤°à¥\8dतनन à¤\95à¥\87 à¤ªà¤¨à¥\8dना à¤\85नà¥\81सार à¤¸à¤®à¥\82ह à¤®à¥\87à¤\82 à¤¬à¤¾à¤\82à¤\9fà¥\80",
        "tog-numberheadings": "स्वयं-सांख्यिकी शिर्षक",
-       "tog-showtoolbar": "समà¥\8dपादन à¤\94à¤\9cारà¥\8d à¤¬à¤\95à¥\8dसा à¤\95à¥\87 à¤¦à¤¿à¤\96ाà¤\87लà¥\8d à¤\9cाà¤\8f",
-       "tog-editondblclick": "दà¥\81à¤\88 à¤\95à¥\8dलिà¤\95 à¤ªà¤° à¤ªà¥\83षà¥\8dठ à¤¸à¤\82पादित à¤\95रà¥\80à¤\82 (à¤\9cावासà¥\8dà¤\95à¥\8dरिपà¥\8dà¤\9f à¤\86वशà¥\8dयà¤\95 à¤¬à¤¾)",
-       "tog-editsectiononrightclick": "अनुभाग शीर्षक पर दायाँ क्लिक कर अनुभाग सम्पादित करीं (जावास्क्रिप्ट आवश्यक बा)",
+       "tog-showtoolbar": "समà¥\8dपादन à¤\94à¤\9cारपà¤\9fà¥\8dà¤\9fà¥\80 à¤\95à¥\87 à¤¦à¤¿à¤\96ावल à¤\9cाव",
+       "tog-editondblclick": "दà¥\81à¤\88 à¤\95à¥\8dलिà¤\95 à¤ªà¤° à¤ªà¤¨à¥\8dना à¤¸à¤\82पादन à¤\95रà¥\80à¤\82",
+       "tog-editsectiononrightclick": "अनुभाग शीर्षक पर दायाँ क्लिक कर अनुभाग सम्पादित करीं",
        "tog-watchcreations": "हमरा द्वारा निर्मित पृष्ठ आ हमरा द्वारा लादल फ़ाइलन के हमार ध्यानसूची में जोड़ी",
        "tog-watchdefault": "हमरा द्वारा निर्मित पृष्ठ आ हमरा द्वारा लादल फ़ाइलन के हमार ध्यानसूची में जोड़ी",
        "tog-watchmoves": "हमरा द्वारा स्थानांतरित पृष्ठ आ लादल फाईल के हमरा ध्यानसूची में जोड़ी",
@@ -31,7 +31,7 @@
        "tog-shownumberswatching": "ध्यान रखे वालन सदस्यन के देखावल जाव",
        "tog-oldsig": "वर्तमान हस्ताक्षर:",
        "tog-fancysig": "हस्ताक्षर के विकी पाठ के रुप में उपयोग करीं (बिना स्वचालित कड़ी के)",
-       "tog-uselivepreview": "लाà¤\88व à¤ªà¥\81रà¥\8dवालà¥\8bà¤\95न à¤\95à¥\87 à¤ªà¥\8dरयà¥\8bà¤\97 à¤\95रà¥\80à¤\82 (à¤\9cावासà¥\8dà¤\95à¥\8dरिपà¥\8dà¤\9f à¤¹à¥\8bà¤\96à¥\87 à¤\95à¥\87 à¤\9aाहà¥\80à¤\82) (पà¥\8dरयà¥\8bà¤\97à¤\95à¥\8dषम)",
+       "tog-uselivepreview": "लाà¤\87व à¤ªà¥\81रà¥\8dवालà¥\8bà¤\95न à¤\95à¥\87 à¤ªà¥\8dरयà¥\8bà¤\97 à¤\95रà¥\80à¤\82 (पà¥\8dरयà¥\8bà¤\97ातà¥\8dमà¤\95)",
        "tog-forceeditsummary": "यदि सम्पादन सारांश ना दिहल होखे त हमके सूचित करब",
        "tog-watchlisthideown": "हमार ध्यान दिहल पन्ना के सूची से हमरा खातिर परिवर्तन छिपाईं",
        "tog-watchlisthidebots": "हमार ध्यान सूची से बोट द्वारा करल गईल परिवर्तन के छिपाईं",
        "category-empty": "''इ श्रेणी में इ समय कउनो पन्ना या मीडिया नइखे।''",
        "hidden-categories": "{{PLURAL:$1|छुपावल गईल श्रेणी|छुपावल गईल श्रेणीं}}",
        "hidden-category-category": "छुपावल गइल श्रेणीं",
-       "category-subcat-count": "{{PLURAL:$2|à¤\87 à¤¶à¥\8dरà¥\87णà¥\80 à¤®à¥\87à¤\82 à¤®à¤¾à¤¤à¥\8dर à¤¨à¤¿à¤®à¥\8dनलिà¤\96ित à¤\89पशà¥\8dरà¥\87णà¥\80 à¤¬à¤¾|à¤\87 à¤¶à¥\8dरà¥\87णà¥\80 à¤®à¥\87à¤\82 à¤¨à¤¿à¤®à¥\8dनलिà¤\96ित {{PLURAL:$1|à¤\89पशà¥\8dरà¥\87णà¥\80|$1 à¤\89पशà¥\8dरà¥\87णà¥\80याà¤\82}} à¤¬à¤¾à¤¡à¤¼à¥\87, à¤\95à¥\81ल à¤\89पशà¥\8dरà¥\87णà¥\80याà¤\82 $2 à¤¬à¤¾à¤¡à¤¼à¥\87।}}",
+       "category-subcat-count": "{{PLURAL:$2|à¤\87 à¤¶à¥\8dरà¥\87णà¥\80 à¤®à¥\87à¤\82 à¤®à¤¾à¤¤à¥\8dर à¤¨à¤¿à¤®à¥\8dनलिà¤\96ित à¤\89पशà¥\8dरà¥\87णà¥\80 à¤¬à¤¾|à¤\87 à¤¶à¥\8dरà¥\87णà¥\80 à¤®à¥\87à¤\82 à¤¨à¤¿à¤®à¥\8dनलिà¤\96ित {{PLURAL:$1|à¤\89पशà¥\8dरà¥\87णà¥\80|$1 à¤\89पशà¥\8dरà¥\87णियाà¤\82}} à¤¬à¤¾à¤¡à¤¼à¥\87, à¤\95à¥\81ल à¤\89पशà¥\8dरà¥\87णियाà¤\81$2}}",
        "category-subcat-count-limited": "इ श्रेणी में निम्नलिखित {{PLURAL:$1|उपश्रेणी बा|$1 उपश्रेणीं बाड़े}}।",
-       "category-article-count": "{{PLURAL:$2|इ श्रेणी में मात्र निम्नलिखित पन्ना बा।|इ श्रेणी में निम्नलिखित {{PLURAL:$1|पन्ना बा|$1 पन्नें}}, कुल पन्नें $2 बाड़े।}}",
+       "category-article-count": "{{PLURAL:$2|इ श्रेणी में मात्र निम्नलिखित पन्न बा।|इ श्रेणी में निम्नलिखित {{PLURAL:$1|पन्ना बा|$1 पन्ना बाड़े}, कुल पन्ना $2}}",
        "category-article-count-limited": "निम्नलिखित {{PLURAL:$1|पन्ना|$1 पन्ना}} इ श्रेणीं में बा।",
        "category-file-count": "{{PLURAL:$2|इ श्रेणी में मात्र निम्नलिखित फ़ाइल बा।|इ श्रेणी में निम्नलिखित {{PLURAL:$1|फ़ाइल|$1 फ़ाइलं}} बाड़े, कुल फ़ाइलं $2}}",
        "category-file-count-limited": "वर्तमान में निम्नलिखित {{PLURAL:$1|पन्ना|$1 पन्नां}} इ श्रेणीं में बाड़े।",
        "newwindow": "(नया विंडो में खोलीं)",
        "cancel": "निरस्त",
        "moredotdotdot": "अउर...",
-       "morenotlisted": "à¤\85धिà¤\95 à¤¸à¥\82à¤\9aà¥\80बदà¥\8dध à¤¨à¤\87à¤\96à¥\87...",
+       "morenotlisted": "à¤\87 à¤¸à¥\82à¤\9aà¥\80 à¤ªà¥\82रà¥\8dण à¤¨à¤\87à¤\96à¥\87।",
        "mypage": "पन्ना",
        "mytalk": "राउर बात",
        "anontalk": "इ आइ॰पी खातिर वार्ता",
        "permalink": "स्थायी लिंक",
        "print": "छापीं",
        "view": "देखीं",
+       "view-foreign": "$1 पर देखीं",
        "edit": "सम्पादन",
+       "edit-local": "क्षेत्रीय विवरण देखीं",
        "create": "बनाईं",
+       "create-local": "क्षेत्रीय विवरण जोड़ीं",
        "editthispage": "ई पन्ना के सम्पादन करीं",
        "create-this-page": "ई पन्ना के निर्माण करीं",
        "delete": "मिटाईं",
        "talkpagelinktext": "बात-चीत",
        "specialpage": "ख़ाश पन्ना",
        "personaltools": "ब्यक्तिगत औजार",
-       "postcomment": "नया खण्ड",
        "articlepage": "सामग्री पन्ना देखीं",
        "talk": "बात-चीत",
        "views": "विचारसूची",
-       "toolbox": "à¤\94à¤\9cार-पà¥\87à¤\9fà¥\80",
+       "toolbox": "à¤\89पà¤\95रण",
        "userpage": "प्रयोगकर्ता पन्ना देखीं",
        "projectpage": "परियोजना पन्ना देखीं",
        "imagepage": "फाईल पन्ना देखीँ",
        "jumptonavigation": "परिभ्रमण",
        "jumptosearch": "खोजीं",
        "view-pool-error": "क्षमा करीं, ई समय सर्वर पर बहुत ज्यादा लोड बढ़ गईल बा।\nई पन्ना के बहुते प्रयोगकर्ता लोग देखे के कोशिश कर रहल बानी।\nई पन्ना के फिर से देखे से पहिले कृपया कुछ देर तक इन्तजार करीं।\n\n$1",
+       "generic-pool-error": "क्षमा करीं, ई समय सर्वर पर बहुत ज्यादा लोड बढ़ गईल बा।\nई संसाधन के बहुते प्रयोगकर्ता लोग देखे के कोशिश कर रहल बानी।\nई संसाधन तक पहुँच बनावे के कोशिश से पहिले कृपया कुछ देर तक इन्तजार करीं।",
        "pool-timeout": "तालाबन्दी खातिर प्रतीक्षा समय समाप्त",
        "pool-queuefull": "पूल पंक्ति भर गइल",
        "pool-errorunknown": "अज्ञात त्रुटि",
+       "pool-servererror": "पूल काउंटर सेवा उपलब्ध नाही बा ($1)।",
        "aboutsite": "{{SITENAME}} के बारे में",
        "aboutpage": "Project:बारे में",
-       "copyright": "सामà¤\97à¥\8dरà¥\80 $1 à¤\95à¥\87 à¤¤à¤¹à¤¤ à¤\89पलबà¥\8dध à¤¬à¤¾।",
+       "copyright": "à¤\89पलबà¥\8dध à¤¸à¤¾à¤®à¤\97à¥\8dरà¥\80 $1 à¤\95à¥\87 à¤\85धà¥\80न à¤\89पलबà¥\8dध à¤¬à¤¾ à¤\9cब à¤¤à¤\95 à¤\95à¥\80 à¤\85लà¤\97 à¤¸à¥\87 à¤\89लà¥\8dलà¥\87à¤\96 à¤¨à¤¾ à¤\95रल à¤\97à¤\88ल à¤¹à¥\8bà¤\96à¥\87 ।",
        "copyrightpage": "{{ns:project}}:लेखाधिकार",
        "currentevents": "हाल के घटना",
        "currentevents-url": "Project:हाल के घटना",
        "youhavenewmessages": "रउआ लगे बा $1 ($2).",
        "youhavenewmessagesfromusers": "रउआ खातिर {{PLURAL:$3|एगो अन्य सदस्य|$3 अन्य सदस्यन}} के $1 बा। ($2)",
        "youhavenewmessagesmanyusers": "रउआ खातिर कई सदस्यन द्वारा $1 बा। ($2)",
-       "newmessageslinkplural": "{{PLURAL:$1|à¤\8fà¤\97à¥\8b à¤¨à¤¯à¤¾ à¤¸à¤¨à¥\8dदà¥\87श à¤¬à¤¾|नया à¤¸à¤¨à¥\8dदà¥\87श à¤¬à¤¾à¤¡à¤¼à¤¨}}",
-       "newmessagesdifflinkplural": "{{PLURAL:$1|पिछला|पिछलका}} बदलाव",
+       "newmessageslinkplural": "{{PLURAL:$1|à¤\8fà¤\95 à¤¨à¤¯à¤¾ à¤¸à¤¨à¥\8dदà¥\87श|999=नयà¤\95ा à¤¸à¤¨à¥\8dदà¥\87श}}",
+       "newmessagesdifflinkplural": "पिछला {{PLURAL:$1|बदलाव|999=बदलावं}}",
        "youhavenewmessagesmulti": "रउआ लगे $1 पर नया सन्देश बा",
        "editsection": "सम्पादन",
        "editold": "सम्पादन",
        "nospecialpagetext": "<strong>रउआ एगो अवैद्य विशेष पन्ना के अनुरोध कईले बानी।</strong>\n\nवैद्य विशेष पन्ना के सूची मिल सकत बा [[Special:SpecialPages|{{int:specialpages}}]] पर।",
        "error": "त्रुटी",
        "databaseerror": "डेटाबेस त्रुटी",
+       "databaseerror-text": "डाटाबेस अनुरोध त्रुटि  भइल बा।\nसंभवतः सॉफ़्टवेयर में गड़बड़ी बा।",
+       "databaseerror-textcl": "डाटाबेस अनुरोध त्रुटि उत्त्पन्न हो गईल बा।",
+       "databaseerror-query": "अनुरोध: $1",
+       "databaseerror-function": "फ़ंक्शन: $1",
+       "databaseerror-error": "त्रुटि: $1",
        "laggedslavemode": "'''चेतावनी:''' इ पन्ना पर हाल के बदलाव ना होखे के आशंका बा।",
        "readonly": "डेटाबेस लॉक बा",
        "enterlockreason": "लॉक करे के कारण दिहीं, साथे लॉक खुले के समय के लगभग आकलन दिहीं।",
        "invalidtitle-knownnamespace": "\"$2\" नामस्थान आ \"$3\" पाठ्य वाला गलत शीर्षक",
        "invalidtitle-unknownnamespace": "अज्ञात नामस्थान संख्या $1 आ नाम \"$2\" वाला गलत शीर्षक",
        "exception-nologin": "खाता में प्रवेश नईखीं भईल",
-       "exception-nologin-text": "इ पन्ना अथवा कार्य खातिर रउआ विकि प्रवेश (लॉग इन) होना आवश्यक है।",
+       "exception-nologin-text": "इ पन्ना अथवा कार्य के सक्षम करे खातिर कृपया [[Special:Userlogin|लॉग इन]] करीं।",
+       "exception-nologin-text-manual": "इ पन्ना अथवा कार्य के सक्षम करे खातिर कृपया $1 करीं।",
        "virus-badscanner": "गलत जमाव: अज्ञात वायरस जाँचक: ''$1''",
        "virus-scanfailed": "जाँच विफल (कोड $1)",
        "virus-unknownscanner": "अज्ञात ऐंटीवायरस:",
        "externaldberror": "या त प्रमाणिकरण डाटाबेस में भइल बा या फिर रउआ के आपन बाह्य खाता अपडेट करे के अनुमति नइखे।",
        "login": "खाता में प्रवेश",
        "nav-login-createaccount": "खाता प्रवेश / खाता बनाईं",
-       "loginprompt": "{{SITENAME}} में प्रवेश खातिर राउर कुकिज चालू होवे के चाहीं",
        "userlogin": "खाता प्रवेश / खाता बनाईं",
        "userloginnocreate": "खाता में प्रवेश",
        "logout": "खाता से बाहर",
        "gotaccount": "का पहिले से एगो खाता बा? $1.",
        "gotaccountlink": "खाता में प्रवेश",
        "userlogin-resetlink": "का रउआ आपन प्रवेश जानकारी भूला गइल बानी?",
-       "userlogin-resetpassword-link": "आपन गुप्तशब्द के फिर से बहाल करीं",
+       "userlogin-resetpassword-link": "आपन गुप्तशब्द भूला गईनी का?",
+       "userlogin-helplink2": "खाता प्रवेश साथ मदद",
+       "userlogin-loggedin": "रउआ {{GENDER:$1|$1}} के रूप में पहिले से लॉग्ड इन बानीं।\nकौनो अन्य सदस्य के रूप में लॉग इन करे खातिर निम्नलिखित फ़ॉर्म के प्रयोग करीं।",
+       "userlogin-createanother": "एगो दोसर खाता बनाईं",
        "createacct-emailrequired": "ई-मेल पता",
        "createacct-emailoptional": "ई-मेल पता (वैकल्पिक)",
        "createacct-email-ph": "आपन ई-मेल पता लिखीं",
        "passwordtooshort": "गुप्त-शब्द कम से कम {{PLURAL:$1|1 अक्षर|$1 अक्षर}} के होवे के चाहीं।",
        "password-name-match": "राउर गुप्त-शब्द राउर प्रयोगकर्ता नाम से अलग होवे के चाहीं।",
        "password-login-forbidden": "इस सदस्यनाम आ गुप्तशब्द के प्रयोग वर्जित बा।",
-       "mailmypassword": "नया à¤\97à¥\81पà¥\8dत-शबà¥\8dद à¤\88-मà¥\87ल à¤ªà¤° à¤­à¥\87à¤\9cीं",
+       "mailmypassword": "à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤°à¤¿à¤¸à¥\87à¤\9f à¤\95रीं",
        "passwordremindertitle": "{{SITENAME}} खातिर नया अस्थायी गुप्त-शब्द",
        "passwordremindertext": "केहु (शायद रउए, $1 आइ॰पी पता से) {{SITENAME}} ($4) पर प्रयोग खातिर नया गुप्तशब्द के निवेदन कईले बानी। सदस्य \"$2\" खातिर एगो अस्थायी गुप्तशब्द बना दिहल गईल बा, आ ई अभी \"$3\" बा। यदि ई राउरे आशय रहल, त अब रउआ खाता प्रवेश खातिर एगो नया गुप्तशब्द चुने के पड़ी।\nराउर अस्थायी गुप्तशब्द के अवधि {{PLURAL:$5|एक दिन|$5 दिनं}} में समाप्त हो जाई।\n\nयदि इ निवेदन केहु अउर कइले रहल, या रउआ आपन पुरान गुप्तशब्द अब नइखी बदले के चाहत काहे कि रउआ राउर पुरनका गुप्तशब्द के स्मरण हो आइल बा, त रउआ इ संदेश के अनदेखा कर सकत बानी, आ आपन पुरान गुप्तशब्द के प्रयोग पहिले हि जइसन कर सकत बानी।",
        "noemail": "\"$1\" सदस्य खातिर कउनो भी ई-मेल पता दर्ज नइखे करल गइल।",
        "noemailcreate": "रउआ एगो जायज ई-मेल पता उपलब्ध करावे के पड़ी।",
        "passwordsent": "\"$1\" के ई-मेल पता पर एगो नया गुप्तशब्द भेज दिहल गइल बा।\nई-मेल पावे के बाद कृपया दुबारा खाता में प्रवेश करब।",
        "blocked-mailpassword": "राउर आइ॰पी पता के सम्पादन करे से वंचित कर दिहल गइल बा, आ गलत प्रयोग रोके खातिर गुप्तशब्द पुनः प्राप्ति के सुविधा इ आइ॰पी पर बंद कर दिहल गइल बा।",
-       "eauthentsent": "दरà¥\8dà¤\9c à¤\95रावल à¤\97à¤\87ल à¤\88-मà¥\87ल à¤ªà¤¤à¤¾ à¤ªà¤° à¤\8fà¤\97à¥\8b à¤ªà¥\81षà¥\8dà¤\9fिà¤\95रण à¤\88-मà¥\87ल à¤­à¥\87à¤\9c à¤¦à¤¿à¤¹à¤² à¤\97à¤\87ल à¤¬à¤¾à¥¤\nरà¤\89à¤\86 à¤\89 à¤\88-मà¥\87ल à¤ªà¤° à¤¦à¤¿à¤¹à¤² à¤\97à¤\87ल à¤¨à¤¿à¤°à¥\8dदà¥\87श à¤\95à¥\87 à¤\85नà¥\81सरण à¤\95र à¤\95à¥\87 à¤\88-मà¥\87ल à¤ªà¤¤à¤¾ à¤\95à¥\87 à¤ªà¥\81षà¥\8dà¤\9fिà¤\95रण à¤\95रावà¥\87 à¤\95à¥\87 à¤ªà¤¡à¤¼à¥\80, à¤\93à¤\95रा à¤¬à¤¾à¤¦à¥\87 à¤\85हिà¤\9cा à¤¸à¥\87 à¤\95à¤\89नà¥\8b à¤¦à¥\82सर à¤\88-मà¥\87ल à¤­à¥\87à¤\9cल à¤\9cाà¤\88।",
+       "eauthentsent": "दरà¥\8dà¤\9c à¤\95रावल à¤\97à¤\87ल à¤\88-मà¥\87ल à¤ªà¤¤à¤¾ à¤ªà¤° à¤\8fà¤\97à¥\8b à¤ªà¥\81षà¥\8dà¤\9fिà¤\95रण à¤\88-मà¥\87ल à¤­à¥\87à¤\9c à¤¦à¤¿à¤¹à¤² à¤\97à¤\87ल à¤¬à¤¾à¥¤\nà¤\89 à¤\96ाता à¤ªà¤° à¤\95à¥\8cनà¥\8b à¤¦à¥\81सर à¤\88मà¥\87ल à¤­à¥\87à¤\9cल à¤\9cाà¤\93 à¤\89 à¤¸à¥\87 à¤ªà¤¹à¤¿à¤²à¥\87, à¤°à¤\89à¤\86 à¤­à¥\87à¤\9cल à¤\97à¤\88ल à¤\88-मà¥\87ल à¤ªà¤° à¤¦à¤¿à¤¹à¤² à¤\97à¤\87ल à¤¨à¤¿à¤°à¥\8dदà¥\87श à¤\95à¥\87 à¤\85नà¥\81सरण à¤\95र à¤\95à¥\87 à¤\88-मà¥\87ल à¤ªà¤¤à¤¾ à¤\95à¥\87 à¤ªà¥\81षà¥\8dà¤\9fिà¤\95रण à¤\95रावà¥\87 à¤\95à¥\87 à¤ªà¤¡à¤¼à¥\80 à¤¤à¤¾à¤\95ि à¤ªà¤¤à¤¾ à¤\9aलà¥\87 à¤\95à¥\80 à¤¸à¤¹à¥\80 à¤®à¥\87à¤\82 à¤\89 à¤°à¤¾à¤\89रà¥\87 à¤\96ाता à¤¹।",
        "throttled-mailpassword": "पिछला {{PLURAL:$1|एक घंटा|$1 घंटा}} के अंदर एगो गुप्तशब्द पुनर्स्थापन ई-मेल भेजल जा चुकल बा।\nदुरुपयोग से बचावे खातिर {{PLURAL:$1|एक घंटा|$1 घंटा}} में सिर्फ एगो गुप्तशब्द पुनर्स्थापन ई-मेल भेजल जाई।",
        "mailerror": "ई-मेल भेजे में त्रुटि: $1",
        "acct_creation_throttle_hit": "राउर आइ॰पी पता से आईल आगंतुक पिछला चौबीस घंटा में इ विकि पर {{PLURAL:$1|एक खाता|$1 खाता}} बना चुकल बानी, इ समयावधि में इहे अधिकतम सीमा बा।\nअतः इ समय इ आइ॰पी पता के प्रयोग करे वाला आगंतुक अउर अधिक खाता नइखन बना सकत।",
-       "emailauthenticated": "राउर ई-मेल पता के पुष्ट दिनांक $2 के $3 बजे हो चुकल रहे।",
-       "emailnotauthenticated": "राउर ई-मेल पता के अभी तक प्रमाणिकरण नइखे भईल।\nनिम्नलिखित कउनो भी सुविधा खातिर रउआ ई-मेल ना भेजल जाई।",
+       "emailauthenticated": "$2 के $3 पर राउर ई-मेल पता के पुष्टीकरण हो चुकल बा।",
+       "emailnotauthenticated": "राà¤\89र à¤\88-मà¥\87ल à¤ªà¤¤à¤¾ à¤\95à¥\87 à¤\85भà¥\80 à¤¤à¤\95 à¤ªà¥\8dरमाणिà¤\95रण à¤¨à¤\87à¤\96à¥\87 à¤­à¤\88ल।\nनिमà¥\8dनलिà¤\96ित à¤\95à¤\89नà¥\8b à¤­à¥\80 à¤¸à¥\81विधा à¤\96ातिर à¤°à¤\89à¤\86 à¤\95à¥\87 à¤\95à¥\8cनà¥\8b à¤­à¥\80 à¤\88-मà¥\87ल à¤¨à¤¾ à¤­à¥\87à¤\9cल à¤\9cाà¤\88।",
        "noemailprefs": "इ सुविधा के प्रयोग करे खातिर आपन वरियता में एगो ई-मेल पता दिहीं।",
        "emailconfirmlink": "अपना ई-मेल पता कन्फर्म करीं",
        "invalidemailaddress": "राउर ई-मेल पता स्वीकार करल नइखे जा सकत काहे कि ई-मेल के जउन रुप दिखाई दे रहल बा उ गलत लागत बा।\nकृपया एगो सहि ई-मेल पता उपलब्ध कराईं या उ जगह के खाली छोड़ दिहीं।",
        "loginlanguagelabel": "भाषा: $1",
        "suspicious-userlogout": "राउर खाता से बाहर जाये के अनुरोध अस्वीकृत कर दिहल गइल बा काहे कि  अइसन लग रहल बा कि इ कउनो खराब ब्राउज़र या कैश करे वाली प्रॉक्सी द्वारा भेजल गईल रहल।",
        "createacct-another-realname-tip": "असली नाम वैकल्पिक बा।\nयदि रउआ इ के उपलब्ध करावे के चुनत बानी त, एकर प्रयोग सदस्य के ओकरा काम के अधिकार देवे खातिर होखी।",
+       "pt-login": "खाता में प्रवेश",
+       "pt-login-button": "खाता में प्रवेश",
+       "pt-createaccount": "खाता बनाईं",
+       "pt-userlogout": "खाता से बाहर",
        "php-mail-error-unknown": "PHP के mail() फ़ंक्शन में अज्ञात त्रुटि बा।",
        "user-mail-no-addy": "बिना कउनो ई-मेल पता के ई-मेल भेजे के प्रयत्न भईल बा।",
        "user-mail-no-body": "एगो खाली अथवा बहुत छोट ई-मेल भेजे के प्रयत्न भईल बा।",
        "changepassword": "गुप्त शब्द बदलीं",
-       "resetpass_announce": "रà¤\89à¤\86 à¤\88-मà¥\87ल à¤¸à¥\87 à¤ªà¥\8dरापà¥\8dत à¤\85सà¥\8dथायà¥\80 à¤\95à¥\8bड à¤¸à¥\87 à¤\96ाता à¤®à¥\87à¤\82 à¤ªà¥\8dरवà¥\87श à¤­à¤\88ल à¤¬à¤¾à¤¨à¥\80।\nà¤\96ाता à¤ªà¥\8dरवà¥\87श à¤\95à¥\87 à¤ªà¥\82रा à¤\95रà¥\87 à¤\96ातिर à¤°à¤\89à¤\86 à¤\87हाà¤\81 à¤\8fà¤\97à¥\8b à¤¨à¤¯à¤¾ à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤¦à¥\87वà¥\87 à¤\95à¥\87 à¤ªà¤¡à¤¼à¥\80:",
+       "resetpass_announce": "लà¥\89à¤\97 à¤\87न à¤¸à¤®à¥\8dपà¥\82रà¥\8dण à¤\95रà¥\87 à¤\96ातिर à¤°à¤\89à¤\86 à¤\8fà¤\97à¥\8b à¤¨à¤¯à¤¾ à¤ªà¤¾à¤¸à¤µà¤°à¥\8dड à¤¦à¥\87वà¥\87 à¤\95à¥\87 à¤¹à¥\8bà¤\88।",
        "resetpass_header": "खाता के गुप्तशब्द बदलीं",
        "oldpassword": "पुराना गुप्त-शब्द:",
        "newpassword": "नया गुप्त-शब्द:",
        "retypenew": "नया गुप्त-शब्द पुन: डालीं:",
        "resetpass_submit": "गुप्तशब्द बनाईं आ खाता में प्रवेश करीं",
        "changepassword-success": "राउर गुप्तशब्द सफलतापुर्वक बदल दिहल गईल बा!",
+       "changepassword-throttled": "रउआ हाले में कईयन बार खाता में प्रवेश करे के कोशिश कर चुकल बानी।\nकृपया $1 प्रतिक्षा करला के बाद फिर से प्रयास करब।",
        "resetpass_forbidden": "गुप्तशब्द बदलल नइखे जा सकत",
        "resetpass-no-info": "इ पन्ना के सिधे प्रयोग करे खातिर रउआ पहिले खाता में प्रवेश करे के पड़ी।",
        "resetpass-submit-loggedin": "गुप्त शब्द बदलीं",
        "resetpass-submit-cancel": "रद्द करीं",
        "resetpass-wrong-oldpass": "अवैद्य अस्थायी या वर्तमान गुप्तशब्द।\nरउआ पहिले हिं सफलतापूर्वक आपन गुप्तशब्द बदल चुकल बानी, या रउआ एगो अस्थायी गुप्तशब्द के अनुरोध कइले होखब।",
+       "resetpass-recycled": "रीसेट करे खातिर नया पासवर्ड में कृपया आपन वर्तमान पासवर्ड के अलावा कौनो अन्य पासवर्ड के प्रयोग करीं।",
+       "resetpass-temp-emailed": "अस्थाई ईमेल कोड के द्वारा रउआ लॉग इन भइल बानी।\nलॉग इन पूरा करे खातिर, रउआ एगो नया पासवर्ड सेट करे के पड़ी:",
        "resetpass-temp-password": "अस्थायी गुप्तशब्द:",
        "resetpass-abort-generic": "कउनो एक्सटेंशन द्वारा गुप्तशब्द में बदलाव रोक दिहल गईल बा।",
+       "resetpass-expired": "राउर पासवर्ड की वैधता अवधि समाप्त हो चुकल बा। कृपया लॉग इन करे खातिर एगो नया पासवर्ड सेट करीं।",
+       "resetpass-validity-soft": "राउर पासवर्ड मान्य नईखे: $1 \n\nकृपया अब एक नया पासवर्ड चुनीं, या उ के बाद में पुनर्स्थापित करे खातिर \"{{int:resetpass-submit-cancel}}\" पर क्लिक करीं।",
        "passwordreset": "गुप्तशब्द रिसेट करीं",
        "passwordreset-text-one": "आपन गुप्तशब्द के पुनर्स्थापित करे खातिर इ फॉर्म भरीं।",
-       "passwordreset-text-many": "{{PLURAL:$1|à¤\86पन à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤ªà¥\81नरà¥\8dसà¥\8dथापित à¤\95रे खातिर निम्न में से कउनो एगो स्थान भरीं।}}",
+       "passwordreset-text-many": "{{PLURAL:$1|à¤\88मà¥\87ल à¤¦à¥\8dवारा à¤\85सà¥\8dथाà¤\88 à¤ªà¤¾à¤¸à¤µà¤°à¥\8dड à¤ªà¤¾à¤µे खातिर निम्न में से कउनो एगो स्थान भरीं।}}",
        "passwordreset-legend": "गुप्तशब्द रिसेट करीं",
        "passwordreset-disabled": "इ विकी पर पासवर्ड पुनर्स्थापन अक्षम बा।",
        "passwordreset-emaildisabled": "इ विकि पर ई-मेल सुविधा अक्षम कर दिहल गईल बा।",
        "changeemail-password": "राउर {{SITENAME}} गुप्तशब्द:",
        "changeemail-submit": "ई-मेल बदलीं",
        "changeemail-cancel": "रद्द करीं",
+       "changeemail-throttled": "रउआ हाले में कईयन बार खाता में प्रवेश करे के कोशिश कर चुकल बानी।\nकृपया $1 प्रतिक्षा करला के बाद फिर से प्रयास करब।",
+       "resettokens": "टोकन रीसेट करीं",
+       "resettokens-text": "जौन टोकन राउर खाता से सम्बद्ध कुछ विशिष्ट व्यक्तिगत जानकारी प्रदान करेला, आप उ के अहिजा रीसेट कर सकत बानी।\n\nयदि रउआ ई के गलती से केहू के दिखा देले बानी या फिर राउर खाता हैक हो गईल बा त रउआ ई के रीसेट कर देवे के चाहीं।",
        "bold_sample": "मोट पाठ्य",
        "bold_tip": "मोट पाठ्य",
        "italic_sample": "इटालिक पाठ्य",
        "permissionserrors": "अनुमति त्रुटी",
        "log-fulllog": "पूरा लॉग देखीं",
        "edit-conflict": "संपादन अंतर्विरोध",
+       "postedit-confirmation-created": "पन्ना बना दिहल गईल।",
        "postedit-confirmation-saved": "राउर सम्पादन सुरक्षित कर दिहल गईल।",
        "invalid-content-data": "अवैध डाटा सामग्री",
        "content-model-wikitext": "विकीपाठ्य",
        "prefs-editing": "संपादन",
        "searchresultshead": "खोज",
        "savedprefs": "राउर वरीयताएँ सुरक्षित कर दिहल गईल।",
+       "timezoneregion-africa": "अफ़्रीका",
+       "timezoneregion-america": "अमेरिका",
+       "timezoneregion-antarctica": "अंटार्कटिका",
+       "timezoneregion-arctic": "आर्कटिक",
        "timezoneregion-asia": "एशिया",
+       "timezoneregion-atlantic": "एटलांटिक महासागर",
        "timezoneregion-australia": "अस्ट्रेलिया",
        "timezoneregion-europe": "यूरोप",
        "timezoneregion-indian": "हिंद महासागर",
        "yourrealname": "असली नाम",
        "yourlanguage": "भाषा:",
        "prefs-preview": "पूर्वावलोकन",
+       "action-move": "ई पन्ना के स्थांतरण करीं",
+       "action-delete": "ई पन्ना के मिटाईं",
        "recentchanges": "तुरंत भईल परिवर्तन",
        "recentchanges-legend": "हाल के परिवर्तन संबंधी विकल्प",
        "recentchanges-label-newpage": "ई सम्पादन से एगो नवका पृष्ठ तैयार हो गइल बा",
        "recentchanges-label-minor": "ई एगो छोटा सम्पाद बा",
+       "recentchanges-legend-heading": "'''कुंजी:'''",
        "rcshowhideminor": "$1 छोट सम्पादन",
+       "rcshowhideminor-show": "दिखाईं",
+       "rcshowhideminor-hide": "छुपाँई",
        "diff": "अन्तर",
        "hist": "इति",
        "hide": "छुपाँई",
        "recentchangeslinked": "सम्बन्धित बदलाव",
        "recentchangeslinked-feed": "सम्बन्धित बदलाव",
        "recentchangeslinked-toolbox": "सम्बन्धित बदलाव",
+       "recentchangeslinked-page": "पन्ना नाम:",
        "upload": "फाईल लादीं",
+       "filedesc": "सारांश",
+       "fileuploadsummary": "सारांश:",
+       "filesource": "स्रोत:",
+       "unknown-error": "अज्ञात त्रुटि उत्पन्न हो गईल बा।",
        "file-anchor-link": "फ़ाइल",
        "filehist": "पन्ना के इतिहास",
        "filehist-deleteall": "सब मिटाईं",
index 434cf28..07a5a29 100644 (file)
        "talkpagelinktext": "আলোচনা",
        "specialpage": "বিশেষ পাতা",
        "personaltools": "নিজস্ব সরঞ্জামসমূহ",
-       "postcomment": "নতুন অনুচ্ছেদ",
        "articlepage": "নিবন্ধ দেখুন",
        "talk": "আলোচনা",
        "views": "দৃষ্টিকোণ",
        "externaldberror": "হয় কোন বহিঃস্থ যাচাইকরণ ডাটাবেজ ত্রুটি ঘটেছে অথবা আপনার বহিঃস্থ অ্যাকাউন্ট হালনাগাদ করার অনুমতি নেই।",
        "login": "প্রবেশ",
        "nav-login-createaccount": "প্রবেশ/নতুন অ্যাকাউন্ট",
-       "loginprompt": "{{SITENAME}}-তে প্রবেশ করতে হলে আপনার ব্রাউজারের কুকি অবশ্যই সক্রিয় করতে হবে।",
        "userlogin": "প্রবেশ/নতুন অ্যাকাউন্ট",
        "userloginnocreate": "প্রবেশ",
        "logout": "প্রস্থান করুন",
        "license-nopreview": "(প্রাকদর্শন লভ্য নয়)",
        "upload_source_url": " (একটি বৈধ, উন্মুক্ত URL)",
        "upload_source_file": " (আপনার কম্পিউটারের একটি ফাইল)",
+       "listfiles-delete": "অপসারণ",
        "listfiles-summary": "এই বিশেষ পাতাটি আপলোড করা সকল ফাইল প্রদর্শন করে।",
        "listfiles_search_for": "ছবির নাম অনুসন্ধান:",
        "imgfile": "ফাইল",
        "expand_templates_remove_nowiki": "ফলাফলে <nowiki> ট্যাগগুলো বাতিল করো",
        "expand_templates_generate_xml": "XML পার্স বৃক্ষ দেখাও",
        "expand_templates_generate_rawhtml": "এইচটিএমএল দেখাও",
-       "expand_templates_preview": "প্রাকদর্শন"
+       "expand_templates_preview": "প্রাকদর্শন",
+       "pagelanguage": "পাতার ভাষা নির্বাচক",
+       "pagelang-name": "পাতা",
+       "pagelang-language": "ভাষা",
+       "pagelang-use-default": "ডিফল্ট ভাষা ব্যবহার করুন",
+       "pagelang-select-lang": "ভাষা নির্বাচন করুন",
+       "right-pagelang": "পাতার ভাষা পরিবর্তন করুন",
+       "action-pagelang": "পাতার ভাষা পরিবর্তন করুন",
+       "log-name-pagelang": "ভাষা পরিবর্তন লগ",
+       "log-description-pagelang": "এটি পাতার ভাষা পরিবর্তনের লগ।",
+       "logentry-pagelang-pagelang": "$1 পাতার ভাষা $3 এর জন্য $4 থেকে $5 এ {{GENDER:$2|পরিবর্তন}} করেছেন।"
 }
index 0c30961..8ec8bb8 100644 (file)
        "talkpagelinktext": "Razgovor",
        "specialpage": "Posebna Stranica",
        "personaltools": "Lični alati",
-       "postcomment": "Nova sekcija",
        "articlepage": "Pogledaj članak",
        "talk": "Razgovor",
        "views": "Pregledi",
        "unexpected": "Neočekivana vrijednost: \"$1\"=\"$2\".",
        "formerror": "Greška: ne može se poslati upitnik",
        "badarticleerror": "Ova akcija ne može biti izvršena na ovoj stranici.",
-       "cannotdelete": "Ne može se obrisati stranica ili datoteka \"$1\".\nMoguće je da ju je neko drugi već obrisao.",
+       "cannotdelete": "Ne može se obrisati stranica ili datoteka \"$1\".\nMoguće je da ju je neko već obrisao.",
        "cannotdelete-title": "Ne mogu izbrisati stranicu \"$1\"",
        "delete-hook-aborted": "Brisanje je prekinuo softverski priključak.\nNije ponuđeno nikakvo objašnjenje.",
        "badtitle": "Loš naslov",
        "externaldberror": "Došlo je do greške pri vanjskoj autorizaciji baze podataka ili vam nije dopušteno osvježavanje Vašeg vanjskog korisničkog računa.",
        "login": "Prijavi se",
        "nav-login-createaccount": "Prijavi se / Registruj se",
-       "loginprompt": "Morate imati kolačiće ('''cookies''') omogućene da biste se prijavili na {{SITENAME}}.",
        "userlogin": "Prijavi se / Registruj se",
        "userloginnocreate": "Prijavi se",
        "logout": "Odjavi me",
        "editingsection": "Uređujete $1 (dio)",
        "editingcomment": "Uređujete $1 (nova sekcija)",
        "editconflict": "Sukobljenje izmjene: $1",
-       "explainconflict": "Neko drugi je promjenio ovu stranicu otkad ste Vi počeli da je mjenjate.\nGornje tekstualno polje sadrži tekst stranice koji trenutno postoji.\nVaše izmjene su prikazane u donjem tekstu.\nMoraćete da unesete svoje promjene u postojeći tekst.\n'''Samo''' tekst u gornjem tekstualnom polju će biti snimljen kad\npritisnete \"{{int:savearticle}}\".",
+       "explainconflict": "Neko drugi je promijenio ovu stranicu otkad ste je Vi počeli mijenjati.\nGornje tekstualno polje sadrži tekst stranice koji trenutno postoji.\nVaše izmjene prikazane su u donjem tekstu.\nMorat ćete unijeti svoje promjene u postojeći tekst.\n'''Samo''' tekst u gornjem tekstualnom polju bit će sačuvan kad\nkliknete \"{{int:savearticle}}\".",
        "yourtext": "Vaš tekst",
        "storedversion": "Uskladištena verzija",
        "nonunicodebrowser": "'''UPOZORENJE: Vaš preglednik ne podržava Unicode zapis znakova.\nMolimo Vas promijenite ga prije sljedećeg uređivanja članaka. Znakovi koji nisu po ASCII standardu će se u prozoru za izmjene pojaviti kao heksadecimalni kodovi.'''",
        "rollbacklinkcount-morethan": "vrati više od $1 {{PLURAL:$1|izmjene|izmjene|izmjena}}",
        "rollbackfailed": "Vraćanje nije uspjelo",
        "cantrollback": "Ne može se vratiti izmjena; posljednji autor je ujedno i jedini.",
-       "alreadyrolled": "Ne može se vratiti posljednja izmjena [[:$1]] od korisnika [[User:$2|$2]] ([[User talk:$2|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]); neko drugi je već izmjenio ili vratio članak.\n\nPosljednja izmjena je bila od korisnika [[User:$3|$3]] ([[User talk:$3|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
+       "alreadyrolled": "Ne može se vratiti posljednja izmjena [[:$1]] od korisnika [[User:$2|$2]] ([[User talk:$2|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]); neko je već izmijenio ili vratio članak na prethodnu provjerenu verziju.\n\nPosljednju izmjenu napravio je korisnik [[User:$3|$3]] ([[User talk:$3|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Sažetak izmjene je bio: \"''$1''\".",
        "revertpage": "Vraćene izmjene korisnika [[Special:Contributions/$2|$2]] ([[User talk:$2|razgovor]]) na posljednju izmjenu koju je napravio [[User:$1|$1]]",
        "revertpage-nouser": "Vraćene izmjene skrivenog korisnika na posljednju reviziju, koju je {{GENDER:$1|napravio|napravila}} [[User:$1|$1]]",
        "anonymous": "{{PLURAL:$1|Anonimni korisnik|$1 anonimna korisnika|$1 anonimnih korisnika}} projekta {{SITENAME}}",
        "siteuser": "{{SITENAME}} korisnik $1",
        "anonuser": "{{SITENAME}} anonimni korisnik $1",
-       "lastmodifiedatby": "Ovu stranicu je posljednji put promjenio $3, u $2, $1",
+       "lastmodifiedatby": "Ovu stranicu posljednji je put promijenio $3, u $2, $1",
        "othercontribs": "Bazirano na radu od strane korisnika $1.",
        "others": "ostali",
        "siteusers": "{{SITENAME}} {{PLURAL:$2|korisnik|korisnika}} $1",
index a01a3c1..25ce027 100644 (file)
        "talkpagelinktext": "Discussió",
        "specialpage": "Pàgina especial",
        "personaltools": "Eines de l'usuari",
-       "postcomment": "Nova secció",
        "articlepage": "Mostra la pàgina",
        "talk": "Discussió",
        "views": "Vistes",
        "externaldberror": "Hi ha hagut una fallida en el servidor d'autenticació externa de la base de dades i no teniu permís per a actualitzar el vostre compte d'accès extern.",
        "login": "Inici de sessió",
        "nav-login-createaccount": "Inicia una sessió / crea un compte",
-       "loginprompt": "Heu de tenir les galetes habilitades per a poder iniciar una sessió a {{SITENAME}}.",
        "userlogin": "Inicia una sessió / crea un compte",
        "userloginnocreate": "Inici de sessió",
        "logout": "Finalitza la sessió",
        "currentrev": "Revisió actual",
        "currentrev-asof": "Revisió de $1",
        "revisionasof": "Revisió de $1",
-       "revision-info": "Revisió de $1; $2",
+       "revision-info": "La revisió el $1 per {{GENDER:$6|$2}}$7",
        "previousrevision": "←Versió més antiga",
        "nextrevision": "Versió més nova→",
        "currentrevisionlink": "Versió actual",
        "mergehistory-empty": "No pot fusionar-se cap revisió.",
        "mergehistory-success": "$3 {{PLURAL:$3|revisió|revisions}} de [[:$1]] s'han fusionat amb èxit a [[:$2]].",
        "mergehistory-fail": "No s'ha pogut realitzar la fusió de l'historial, comproveu la pàgina i els paràmetres horaris.",
+       "mergehistory-fail-toobig": "No s'ha pogut realitzar la fusió de l'historial perquè es mourien més del limit de $1 {{PLURAL:$1|revisió|revisions}}.",
        "mergehistory-no-source": "La pàgina d'origen $1 no existeix.",
        "mergehistory-no-destination": "La pàgina de destinació $1 no existeix.",
        "mergehistory-invalid-source": "La pàgina d'origen ha de tenir un títol vàlid.",
        "largefileserver": "Aquest fitxer és més gran del que el servidor permet.",
        "emptyfile": "El fitxer que heu carregat sembla estar buit.\nAçò por ser degut a un mal caràcter en el nom del fitxer.\nComproveu si realment voleu carregar aquest fitxer.",
        "windows-nonascii-filename": "Aquest wiki no permet noms de fitxer amb caràcters especials.",
-       "fileexists": "Ja hi existeix un fitxer amb aquest nom, si us plau, verifiqueu <strong>[[:$1]]</strong> si no esteu segurs de voler substituir-lo.\n[[$1|thumb]]",
+       "fileexists": "Ja existeix un fitxer amb aquest nom. Comproveu <strong>[[:$1]]</strong> si no esteu {{GENDER:|segur|segura}} de voler substituir-lo.\n[[$1|thumb]]",
        "filepageexists": "La pàgina de descripció d'aquest fitxer ja ha estat creada (<strong>[[:$1]]</strong>), però de moment no hi ha cap fitxer amb aquest nom. La descripció que heu posat no apareixerà a la pàgina de descripció. Si voleu que hi aparegui haureu d'editar-la manualment.\n[[$1|thumb]]",
        "fileexists-extension": "Ja existeix un fitxer amb un nom semblant: [[$2|thumb]]\n* Nom del fitxer que es puja: <strong>[[:$1]]</strong>\n* Nom del fitxer existent: <strong>[[:$2]]</strong>\nPotser voleu fer servir un nom més fàcil de distingir?",
        "fileexists-thumbnail-yes": "Aquest fitxer sembla ser una imatge en mida reduïda (<em>miniatura</em>). [[$1|thumb]]\nComproveu si us plau el fitxer <strong>[[:$1]]</strong>.\nSi el fitxer és la mateixa imatge a mida original, no cal carregar cap miniatura més.",
        "license-nopreview": "(Previsualització no disponible)",
        "upload_source_url": " (un URL vàlid i accessible públicament)",
        "upload_source_file": " (un fitxer en el vostre ordinador)",
+       "listfiles-delete": "elimina",
        "listfiles-summary": "Aquesta pàgina especial mostra tots els fitxers carregats.\nSi filtreu per usuari només es mostraran els fitxers la versió més recent dels quals hagi estat carregada per aquell.",
        "listfiles_search_for": "Cerca el nom d'un fitxer de medis:",
        "imgfile": "fitxer",
        "wantedpages-badtitle": "Títol invàlid al conjunt de resultats: $1",
        "wantedfiles": "Fitxers demanats",
        "wantedfiletext-cat": "Els fitxers següents s'utilitzen per no existeixen. Els fitxers de repositoris aliens poden ser llistats encara que existeixin. Aquells que siguin fals positius es <del>tatxaran</del>. A més, les pàgines que tinguin fitxers incrustats que no existeixin es llistaran a [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Els fitxers següents s'utilitzen, però no existeixen. Addicionalment, s'enumeren a [[:$1]] les pàgines que tenen fitxers inserits que no existeixen.",
        "wantedfiletext-nocat": "Els fitxers següents es fan servir però no existeixen. Els fitxers d'un repositori aliè poden ser llistats encara que existeixin. Tots aquells fals positius es <del>tatxaran</del>.",
+       "wantedfiletext-nocat-noforeign": "Els fitxers següents s'utilitzen però no existeixen.",
        "wantedtemplates": "Plantilles demanades",
        "mostlinked": "Pàgines més enllaçades",
        "mostlinkedcategories": "Categories més utilitzades",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussió]])",
        "unknown_extension_tag": "Etiqueta d'extensió desconeguda «$1»",
        "duplicate-defaultsort": "Atenció: La clau d'ordenació per defecte \"$2\" invalida l'anterior clau \"$1\".",
+       "duplicate-displaytitle": "<strong>Avís:</strong> El títol a mostrar «$2» sobreescriu l'anterior títol a mostrar «$1».",
        "version": "Versió",
        "version-extensions": "Extensions instaŀlades",
-       "version-skins": "Aparences",
+       "version-skins": "Temes instal·lats",
        "version-specialpages": "Pàgines especials",
        "version-parserhooks": "Extensions de l'analitzador",
        "version-variables": "Variables",
        "pagelang-use-default": "Utilitza l'idioma per defecte",
        "pagelang-select-lang": "Selecciona un idioma",
        "right-pagelang": "Canvia l'idioma de la pàgina",
-       "action-pagelang": "canvia l'idioma de la pàgina"
+       "action-pagelang": "canvia l'idioma de la pàgina",
+       "log-name-pagelang": "Canvia el registre de llengua",
+       "log-description-pagelang": "Aquest és un registre dels canvis en les llengües de les pàgines.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|ha canviat}} la llengua de la pàgina per a $3 de $4 a $5."
 }
index f2d1f04..1f6960d 100644 (file)
@@ -7,7 +7,8 @@
                        "Mega programmer",
                        "Sasan700",
                        "Умар",
-                       "아라"
+                       "아라",
+                       "Kaganer"
                ]
        },
        "tog-underline": "КӀел сиз хьакха хьажорган:",
        "talkpagelinktext": "Дийцаре",
        "specialpage": "Белха агӀо",
        "personaltools": "Долахь болу гӀирсаш",
-       "postcomment": "Керла дакъа",
        "articlepage": "Хьажа яззаме",
        "talk": "Дийцаре",
        "views": "Хьажарш",
        "externaldberror": "Арахьара хаамийн базан гӀоьнца аутентификаци ечу хенахь гӀалат даьлла я хьа дӀаяздаран хийцам бан бакъонаш яц.",
        "login": "Системин чугӀо",
        "nav-login-createaccount": "Системин чугӀо / дӀаяздар кхолла",
-       "loginprompt": "Ахьа бакъо яла еза оцу «cookies» хьайна системин чохь болхбан лаахь.",
        "userlogin": "Довзийтар я декъашхочун дӀаяздар кхоллар",
        "userloginnocreate": "Довзийта",
        "logout": "Болх дӀаберзор",
        "passwordreset-emailelement": "Декъашхочун цӀе: $1\nХанна йолу пароль: $2",
        "passwordreset-emailsent": "Электронан хаам баийтина кхоьссинчу паролах лаьцна хаам чохь болуш.",
        "passwordreset-emailsent-capture": "Электронан хаам баийтина кхоьссинчу паролах лаьцна хаам чохь болуш. \nцуна йозане хьажа йиш ю лахахь.",
+       "passwordreset-emailerror-capture": "Пароль кхоссаран хаам чохь болуш электронан кехат кхоьллина, цуна йоза хьажа йиш ю лахахь, амма иза {{GENDER:$2|декъашхочунга}} дӀадахьийта тар цаделира бахьнехь: $1",
        "changeemail": "Хийца электронан почта",
        "changeemail-header": "Электронан почтан адрес хийцар",
        "changeemail-text": "Юза хӀара форма хьайн электронан почтан адрес хуьйцуш. Ахьа хийцар бакъдан язъян еза пароль.",
        "nolicense": "Яц",
        "license-nopreview": "(Хьалха муха ю хьажа цало)",
        "upload_source_file": " (файл хьан компьютер чохь ю)",
+       "listfiles-delete": "дӀаяккха",
        "listfiles-summary": "Лахахь гойтуш ю ерриг файлаш.\nДекъашхо къастичи, цун керла файлаш гойту.",
        "listfiles_search_for": "Лаха хIуман цIарца:",
        "imgfile": "файл",
        "mimesearch-summary": "ХӀокху агӀоно йиш хуьлуьйту MIME-тайпан файлаш харжа. Яздеш долу формат: чулацаман тайп/бухара тайп, масала  <code>image/jpeg</code>.",
        "mimetype": "MIME-тайп:",
        "download": "чуяккха",
-       "unwatchedpages": "Цхьамо тергам ца беш йолу агIонаш",
+       "unwatchedpages": "Цхьамо тергам ца беш йолу агӀонаш",
        "listredirects": "ДIасахьажоран могIам",
        "listduplicatedfiles": "Файлийн могӀам дубликатшца",
        "listduplicatedfiles-entry": "Файлан [[:File:$1|$1]] — [[$3|{{PLURAL:$2|дубликат ю}}]].",
        "unusedtemplateswlh": "кхин хьажоргаш",
        "randompage": "Цахууш нисъелла агӀо",
        "randomincategory": "Категори чу цахууш нисъелла  агӀо",
+       "randomincategory-nopages": "[[:Category:$1]] категори чохь агӀонаш яц.",
        "randomincategory-selectcategory": "Категори чу цахууш нийса елла агӀона чу гӀо: $1 $2.",
        "randomincategory-selectcategory-submit": "Дехьа гӀо",
        "randomredirect": "Цахууш нисделла дIасахьажор",
+       "randomredirect-nopages": "«$1» цӀерийн меттиган чохь дӀасахьажораш яц.",
        "statistics": "Статистика",
        "statistics-header-pages": "АгӀонийн жамӀ",
        "statistics-header-edits": "Нисдаран жамӀ",
        "pageswithprop-text": "Кхузахь гойтуш ю агӀонаш цхьадолу къастамаш куьйга юху билгал даьхнарш.",
        "pageswithprop-prop": "Къастаман цӀе:",
        "pageswithprop-submit": "Лаха",
+       "pageswithprop-prophidden-long": "деха йозан хӀуман маьӀна хьулйина ($1)",
+       "pageswithprop-prophidden-binary": "шалха маьӀна долу хӀума хьулйина ($1)",
        "doubleredirects": "Шалха дIасахьажийнарш",
        "doubleredirectstext": "ХӀокху агӀонехь ю дӀасахьажорашан тӀе хьажийна йолу дӀасахьажораш.\n<del>ТӀехула сиз хаькхна </del>нисйина чарна.",
        "double-redirect-fixed-move": "АгӀон [[$1]] цӀе хийцина, хӀинца иза дӀахьажийна оцу [[$2]]",
        "withoutinterwiki": "Юкъарвики-хьажоргаш йоцу агӀонаш",
        "withoutinterwiki-summary": "Лахара агӀонийн юкъарвики-хьажоргаш яц:",
        "withoutinterwiki-submit": "Гайта",
-       "fewestrevisions": "ЧIогIа кIезиг башхонаш йолу агIонаш",
+       "fewestrevisions": "ЧӀогӀа кӀезиг версеш йолу агӀонаш",
        "nbytes": "$1 {{PLURAL:$1|байт}}",
        "ncategories": "$1 {{PLURAL:$1|категори|категореш}}",
        "ninterwikis": "$1 {{PLURAL:$1|1=юкъарвики-хьажораг|юкъарвики-хьажоргаш}}",
        "nimagelinks": "Лелош ю $1 {{PLURAL:$1|агӀонгахь|агӀонашкахь}}",
        "ntransclusions": "лелош ю $1 {{PLURAL:$1|агӀонгахь|агӀонашкахь}}",
        "specialpage-empty": "Дехаро хӀумма ца елла.",
-       "lonelypages": "Байлахь йисина агIонаш",
+       "lonelypages": "Байлахь йисина агӀонаш",
        "lonelypagestext": "Кхузахь ю {{grammar:genitive|{{SITENAME}}}} кхечу агӀонашкахь тӀе хьажийна хьажоргаш йоцу агӀонаш.",
-       "uncategorizedpages": "Категореш йоцу агIонаш",
+       "uncategorizedpages": "Категореш йоцу агӀонаш",
        "uncategorizedcategories": "Категореш йоцу категореш",
        "uncategorizedimages": "Категореш йоцу файлаш",
        "uncategorizedtemplates": "Категореш йоцу кепаш",
        "unusedimages": "Лелош йоцу файлаш",
        "popularpages": "ГӀараяьлла агӀонаш",
        "wantedcategories": "Оьшуш йолу категореш",
-       "wantedpages": "Оьшуш йолу агIонаш",
+       "wantedpages": "Оьшуш йолу агӀонаш",
        "wantedfiles": "Оьшуш йолу файлаш",
        "wantedfiletext-cat": "Лахара йоцу файлаш лело гӀерта. Оцу могӀам юкъа ца хууш файлаш кхета там бу, кхечу проекташ чохь йолу. Ишта ца хууш юкъа нийса елачарна тӀехула <del>сиз</del> хира ду.\nКхин йоцу файлаш гойту [[:$1]] чохь",
        "wantedfiletext-nocat": "Лахара йоцу файлаш лело гӀерта. Оцу могӀам юкъа ца хууш файлаш кхета там бу, кхечу проекташ чохь йолу. Ишта ца хууш юкъа нийса елачарна тӀехула <del>сиз</del> хира ду.",
        "mostcategories": "Дуккха категореш тӀе тоьхна йолу агӀонаш",
        "mostimages": "Массарел дуккха лелайо файлаш",
        "mostinterwikis": "Дуккха юкъарвики хьажоргаш тӀе тоьхна йолу агӀонаш",
-       "mostrevisions": "Сих сиха нисйина йолу агIонаш",
+       "mostrevisions": "Сих сиха нисйина йолу агӀонаш",
        "prefixindex": "Хьалха агӀонашан цӀераш хӀотто йеза",
        "prefixindex-namespace": "Хьалха агӀонашан цӀераш хӀотто еза («{{ns:$1}}»)",
        "prefixindex-strip": "Хиламийн могӀам чура префикс къайлаяккха",
        "shortpages": "Боца яззамаш",
        "longpages": "Беха яззамаш",
-       "deadendpages": "Дика йоцу агIонаш",
-       "protectedpages": "ГIаролла дина агIонаш",
+       "deadendpages": "Дика йоцу агӀонаш",
+       "protectedpages": "ГIаролла дина агӀонаш",
        "protectedpages-indef": "Хан йоцуш гӀоралла динарш бен",
        "protectedpages-cascade": "Чахчарин гӀоралла бен",
        "protectedpages-noredirect": "Къайлаяха дӀасахьажийнарш",
        "ipaddressorusername": "IP-адрес я декъашхочун цӀе:",
        "ipbexpiry": "Хан чекхйолу:",
        "ipbreason": "Бахьна:",
-       "ipbreason-dropdown": "* Белхан некъ дӀакъовлар бахьанаш:\n** Харца хаам бар\n** АгӀонан чураниг дӀаяккхар\n** Спам-хьажоргаш арахьара сайташна\n** МаьӀна доцу текст тӀетохар\n** Декъашхой хьийзабар, кхерамаш тийсар\n** Масийтта лараман яздар зуламан лелаяр\n** Магийтина йоцу декъашхочун цӀе",
+       "ipbreason-dropdown": "* Белхан некъ дӀакъовлар бахьанаш:\n** Харца хаам бар\n** АгӀонан чураниг дӀаяккхар\n** Спам-хьажоргаш арахьара сайташна\n** МаьӀна доцу йоза тӀетохар\n** Декъашхой хьийзабар, кхерамаш тийсар\n** Масийтта лараман яздар зуламан лелаяр\n** Магийтина йоцу декъашхочун цӀе",
        "ipb-hardblock": "Шаш довзийтина болу декъашхошна бехкам бе хӀокху IP-адресца тадарш дан",
        "ipbcreateaccount": "Цамаго керла декъашхочун дӀаяздарш кхолла",
        "ipbemailban": "Цамагдо декъашхошка хааман кехаташ кхехьийта",
        "blocklogentry": "блоктоьхна [[$1]] цхьана ханна $2 $3",
        "reblock-logentry": "Хийцина  блоктоьхна хан [[$1]] $2 $3",
        "blocklogtext": "Блоктохаршна а блокдӀаякхаршна а тептар. Ша блоккхеташ долу IP-адресаш кхузахь гойтуш дац. Кхин. [[Special:BlockList|хӀийнца блоктоьха берш]].",
-       "unblocklogentry": "дӀаякхинаблок $1",
+       "unblocklogentry": "дӀаяькхинаблок $1",
        "block-log-flags-anononly": "Къайлаха берш",
        "block-log-flags-nocreate": "цамагдо керла дӏаяздарш кхоллар",
        "block-log-flags-noautoblock": "ша блоктухарг дӏаяйина",
        "ilsubmit": "Лаха",
        "bydate": "терахьашца",
        "sp-newimages-showfrom": "Гайта керла файлаш $2, $1 тӀера дуьйна",
-       "seconds-abbrev": "$1оцу",
+       "seconds-abbrev": "$1 оцу",
        "minutes-abbrev": "$1 мин",
        "hours-abbrev": "$1 сахь.",
-       "seconds": "{{PLURAL:$1|$1 секунд}}",
-       "minutes": "$1 минут",
-       "hours": "{{PLURAL:$1|сахьат}}",
-       "days": "{{PLURAL:$1|$1 де}}",
+       "seconds": "{{PLURAL:$1|$1 секунд|$1 секунд}}",
+       "minutes": "{{PLURAL:$1|$1 минут|$1 минут}}",
+       "hours": "{{PLURAL:$1|$1 сахьт|$1 сахьт}}",
+       "days": "{{PLURAL:$1|$1 де|$1 де}}",
        "weeks": "{{PLURAL:$1|$1 кӀира}}",
-       "months": "$1 {{PLURAL:$1|бутт}}",
-       "years": "$1 {{PLURAL:$1|шо}}",
+       "months": "{{PLURAL:$1|$1 бутт|$1 бутт}}",
+       "years": "{{PLURAL:$1|$1 шо|$1 шо}}",
        "ago": "$1 хьалха",
        "just-now": "хӀинца",
-       "hours-ago": "$1 сахьт хьалха",
-       "minutes-ago": "$1 минут хьалха",
+       "hours-ago": "$1 {{PLURAL:$1|сахьт}}",
+       "minutes-ago": "$1 {{PLURAL:$1|минут}} хьалха",
        "seconds-ago": "$1 {{PLURAL:$1|секунд}} хьалха",
        "monday-at": "оршотан дийнахь $1",
        "tuesday-at": "шинара дийнахь $1",
        "exif-iimcategory-fin": "Экономика а бизнес а",
        "exif-iimcategory-edu": "Дешна хилар",
        "exif-iimcategory-lab": "Къинхьегам",
+       "exif-iimcategory-rel": "Дин а тешар а",
+       "exif-iimcategory-sci": "Ӏилма а техника а",
+       "exif-iimcategory-soi": "Социалан хаттарш",
+       "exif-iimcategory-wea": "Хенан хӀоттам",
        "exif-urgency-normal": "Диканиг ($1)",
        "exif-urgency-low": "Лахара ($1)",
        "exif-urgency-high": "Лакхара ($1)",
+       "exif-urgency-other": "Декъашхочо билгалйина приоритет ($1)",
        "watchlistall2": "массо",
        "namespacesall": "массо",
        "monthsall": "массо",
+       "confirmemail": "Электронан почтан адрес бакъдар",
+       "confirmemail_noemail": "Ахьа нийса электронан почтан адрес яздина дац [[Special:Preferences|гӀирсан чохь]].",
        "confirmrecreate": "Декъашхочо [[User:$1|$1]] ([[User talk:$1|дийцаре]]) хӀара агӀо дӀаяьккхина, ахьа иза тая йолийча, дӀаяккхарна бахьна:\n: ''$2''\nДехар до, тешал де, хьо иза агӀо меттахӀотто лууш ву/ю але.",
        "confirmrecreate-noreason": "Декъашхочо [[User:$1|$1]] ([[User talk:$1|дийцаре]]) хӀара агӀо дӀаяьккхина, ахьа иза тая йолийча. Дехар до, тешал де, хьо иза агӀо меттахӀотто лууш ву/ю але.",
        "recreate": "Юха кхолла",
        "watchlistedit-clear-submit": "Тергаман могӀам дӀацӀанбан (иза сацадан лурдац)",
        "watchlistedit-clear-done": "Хьан тергаман могӀам дӀацӀанбина",
        "watchlistedit-clear-removed": "{{PLURAL:$1|ДӀаяьккхина|ДӀаяьхна}} $1 {{PLURAL:$1|дӀаяздар|дӀаяздарш}}:",
-       "watchlistedit-too-many": "Ð\9aÑ\85Ñ\83ззаÑ\85Ñ\8c Ð³Ð°Ð¹Ñ\82а Ñ\82Ó\80еÑ\85Ñ\8c Ð´Ñ\83кÑ\85а Ð°Ð³Ó\80онаÑ\88 Ñ\8e.",
+       "watchlistedit-too-many": "Кхузахь гайта тӀехь дукха агӀонаш ю.",
        "watchlisttools-clear": "Тергаман могӀам дӀацӀанбан",
        "watchlisttools-view": "МогӀам чура агӀонашан хийцамаш",
        "watchlisttools-edit": "Хьажа/нисбé могӀам",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|дийцаре]])",
        "version": "Верси MediaWiki",
        "version-extensions": "ДӀахӀоттийна шордарш",
-       "version-skins": "Ð\9aечяран темаш",
+       "version-skins": "Ð\94Ó\80аÑ\85Ó\80оÑ\82Ñ\82ийна Ðºечяран темаш",
        "version-specialpages": "Белхан агӀонаш",
        "version-parserhooks": "Cинтаксисан къастор схьалоцурш",
        "version-variables": "Хийцаме",
        "version-other": "Кхин",
        "version-mediahandlers": "Медиа кеч ерраш",
+       "version-hooks": "Схьалуьцарш",
        "version-parser-extensiontags": "Cинтаксисан къасторан шораллин тегаш",
        "version-parser-function-hooks": "Cинтаксисан къасторан функци схьалоцурш",
+       "version-hook-name": "Схьалуьцачун цӀе",
+       "version-hook-subscribedby": "ДӀабазбелла тӀе",
        "version-version": "(Верси $1)",
-       "version-license": "Бакъо",
+       "version-no-ext-name": "[цӀе йоцуш]",
+       "version-license": "MediaWiki Лицензи",
        "version-ext-license": "Лицензи",
        "version-ext-colheader-name": "Шордарш",
+       "version-skin-colheader-name": "Кечяран тема",
        "version-ext-colheader-version": "Верси",
        "version-ext-colheader-license": "Лицензи",
        "version-ext-colheader-description": "Цуьнах лаьцна",
        "version-ext-colheader-credits": "Автораш",
+       "version-license-title": "Лицензи цу $1",
+       "version-credits-title": "Авторийн могӀам цу $1",
        "version-poweredby-credits": "ХӀара вики болх беш ю '''[https://www.mediawiki.org/ MediaWiki]''' движок тӀехь, copyright © 2001-$1 $2.",
        "version-poweredby-others": "кхин",
        "version-poweredby-translators": "гочдархой translatewiki.net",
        "version-license-info": "MediaWiki ю маьрша программин латораг, шу йиш ю фондас арахецна йолу GNU General Public License лицензица и яржо я хийца а.\n\nMediaWiki яржош ю и шуна пайдане хир яц те аьлла, амма  ЦХЬА ЮКЪАРАХИЛАР ДОЦУШ. Хь. кхин. лицензи мадарра GNU General Public License .\n\nШоьга кхача езаш яра [{{SERVER}}{{SCRIPTPATH}}/COPYING копи GNU General Public License] хӀокху программица, кхаьчна яцахь язъе Free Software Foundation, Inc., адрес тӀе: 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA я [//www.gnu.org/licenses/old-licenses/gpl-2.0.html еша и онлайнехь].",
        "version-software": "ДӀахӀоттийна программин латтор",
+       "version-software-product": "Сурсат",
        "version-software-version": "Верси",
        "version-entrypoints": "ЧугӀо адресин тӀадамаш",
        "version-entrypoints-header-entrypoint": "Яздаран тӀадам",
        "api-error-duplicate": "Иштта чулацам болу {{PLURAL:$1|1=[$2 кхин файл]|[$2 кхин файлаш]}} йолуш ю",
        "api-error-duplicate-popup-title": "{{PLURAL:$1|1=Файлан|Файлийн}} дубликат.",
        "api-error-empty-file": "Ахьа яхьийтина файл еса ю.",
+       "api-error-mustbeposted": "Чоьхьара гӀалат: дехаро хьехам схьабоьху HTTP POST.",
        "api-error-noimageinfo": "Кхиамца чуяьккхина, амма серверо файлахь лаьцна цхьаа хаам битина бац.",
        "api-error-nomodule": "Чоьхьара гӀалат: чуйокху модуль нисйина яц.",
        "api-error-ok-but-empty": "Чоьхьара гӀалат: серверара жоп дац.",
        "expand_templates_generate_xml": "Гойту дитта цу XML",
        "expand_templates_generate_rawhtml": "Гайта HTML",
        "expand_templates_preview": "Хьалха муха ю хьажа",
+       "pagelanguage": "АгӀона мотт харжар",
        "pagelang-name": "АгӀо",
        "pagelang-language": "Мотт",
        "pagelang-use-default": "Ӏад битарца мотт",
        "right-pagelang": "АгӀона мотт хийца",
        "action-pagelang": "агӀона мотт хийца",
        "log-name-pagelang": "Мотт хийцаран тептар",
-       "log-description-pagelang": "ХӀара агӀонашкахь мотт хийцаран тептар ду."
+       "log-description-pagelang": "ХӀара агӀонашкахь мотт хийцаран тептар ду.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|хийцина}} агӀона мотт $3 $4 → $5."
 }
index 0e298a5..69dddfb 100644 (file)
        "talkpagelinktext": "diskuse",
        "specialpage": "Speciální stránka",
        "personaltools": "Osobní nástroje",
-       "postcomment": "Nová sekce",
        "articlepage": "Prohlédnout si stránku",
        "talk": "Diskuse",
        "views": "Zobrazení",
        "externaldberror": "Buď nastala chyba externí autentizační databáze, nebo nemáte dovoleno měnit svůj externí účet.",
        "login": "Přihlaste se",
        "nav-login-createaccount": "Přihlášení / vytvoření účtu",
-       "loginprompt": "K přihlášení do {{grammar:2sg|{{SITENAME}}}} musíte mít povoleny cookies.",
        "userlogin": "Přihlášení / vytvoření účtu",
        "userloginnocreate": "Přihlášení",
        "logout": "Odhlásit se",
        "license": "Licence:",
        "license-header": "Licence",
        "nolicense": "Bez udání licence",
+       "licenses-edit": "Editovat nabídku licencí",
        "license-nopreview": "(Náhled není dostupný)",
        "upload_source_url": " (platné, veřejně přístupné URL)",
        "upload_source_file": " (soubor ve vašem počítači)",
+       "listfiles-delete": "smazat",
        "listfiles-summary": "Tato speciální stránka zobrazuje všechny načtené soubory.",
        "listfiles_search_for": "Hledat soubor podle názvu:",
        "imgfile": "soubor",
        "wantedpages-badtitle": "Výsledky obsahují neplatný název: $1",
        "wantedfiles": "Chybějící soubory",
        "wantedfiletext-cat": "Následující soubory se používají, ale neexistují. Soubory ze vzdálených úložišť zde mohou být uvedeny, přestože existují. Taková falešná pozitiva budou zobrazena <del>přeškrtnutě</del>. Stránky, které vkládají neexistující soubory, jsou navíc uvedeny v [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Následující soubory se používají, ale neexistují. Stránky, které používají neexistující soubory, jsou navíc uvedeny v [[:$1]].",
        "wantedfiletext-nocat": "Následující soubory se používají, ale neexistují. Soubory ze vzdálených úložišť zde mohou být uvedeny, přestože existují. Taková falešná pozitiva budou zobrazena <del>přeškrtnutě</del>.",
+       "wantedfiletext-nocat-noforeign": "Následující soubory se používají, ale neexistují.",
        "wantedtemplates": "Chybějící šablony",
        "mostlinked": "Nejodkazovanější stránky",
        "mostlinkedcategories": "Nejpoužívanější kategorie",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|diskuse]])",
        "unknown_extension_tag": "Neznámá značka rozšíření: „$1“",
        "duplicate-defaultsort": "Upozornění: Implicitní klíč řazení (DEFAULTSORTKEY) „$2“ přepisuje dříve nastavenou hodnotu „$1“.",
+       "duplicate-displaytitle": "<strong>Upozornění:</strong> Předchozí zobrazovaný název „$1“ je nahrazen zobrazovaným názvem „$2“.",
        "version": "Verze",
        "version-extensions": "Nainstalovaná rozšíření",
        "version-skins": "Nainstalované vzhledy",
index e66b267..fe284ae 100644 (file)
        "talkpagelinktext": "Sgwrs",
        "specialpage": "Tudalen Arbennig",
        "personaltools": "Offer personol",
-       "postcomment": "Adran newydd",
        "articlepage": "Dangos tudalen bwnc",
        "talk": "Sgwrs",
        "views": "Golygon",
index 797b8b6..b505ce4 100644 (file)
        "talkpagelinktext": "diskussion",
        "specialpage": "Speciel side",
        "personaltools": "Personlige værktøjer",
-       "postcomment": "Nyt afsnit",
        "articlepage": "Se artiklen",
        "talk": "Diskussion",
        "views": "Visninger",
        "externaldberror": "Der er opstået en fejl i en ekstern adgangsdatabase, eller du har ikke rettigheder til at opdatere denne.",
        "login": "Log på",
        "nav-login-createaccount": "Opret en konto eller log på",
-       "loginprompt": "Du skal have cookies slået til for at kunne logge på {{SITENAME}}.",
        "userlogin": "Opret en konto eller log på",
        "userloginnocreate": "Log på",
        "logout": "Log af",
        "watchlistedit-raw-done": "Din overvågningsliste blev opdateret.",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 side|$1 sider}} er tilføjet:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|1 side|$1 sider}} er fjernet:",
+       "watchlisttools-clear": "Ryd overvågningsliste",
        "watchlisttools-view": "Se ændrede sider i overvågningslisten",
        "watchlisttools-edit": "Rediger overvågningsliste",
        "watchlisttools-raw": "Rediger rå overvågningsliste",
index f1bd8f8..c2efafd 100644 (file)
        "talkpagelinktext": "Diskussion",
        "specialpage": "Spezialseite",
        "personaltools": "Meine Werkzeuge",
-       "postcomment": "Neuer Abschnitt",
        "articlepage": "Inhaltsseite anzeigen",
        "talk": "Diskussion",
        "views": "Ansichten",
        "externaldberror": "Entweder liegt ein Fehler bei der externen Authentifizierung vor oder du darfst dein externes Benutzerkonto nicht aktualisieren.",
        "login": "Anmelden",
        "nav-login-createaccount": "Anmelden / Benutzerkonto erstellen",
-       "loginprompt": "Zur Anmeldung müssen Cookies aktiviert sein.",
        "userlogin": "Anmelden / Benutzerkonto anlegen",
        "userloginnocreate": "Anmelden",
        "logout": "Abmelden",
        "license": "Lizenz:",
        "license-header": "Lizenz",
        "nolicense": "Keine Vorauswahl",
+       "licenses-edit": "Lizenzoptionen bearbeiten",
        "license-nopreview": "(es ist keine Vorschau verfügbar)",
        "upload_source_url": " (gültige, öffentlich zugängliche URL)",
        "upload_source_file": " (eine Datei auf deinem Computer)",
+       "listfiles-delete": "löschen",
        "listfiles-summary": "Diese Spezialseite listet alle hochgeladenen Dateien auf.",
        "listfiles_search_for": "Suche nach Datei:",
        "imgfile": "Datei",
        "wantedpages-badtitle": "Ungültiger Titel im Ergebnis: $1",
        "wantedfiles": "Gewünschte Dateien",
        "wantedfiletext-cat": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden. Vorhandene Dateien aus fremden Repositorien können dennoch hier aufgelistet sein und werden <del>durchgestrichen</del> dargestellt. Zusätzlich werden Seiten, die nicht vorhandene Dateien enthalten, in die [[:$1]] eingeordnet.",
+       "wantedfiletext-cat-noforeign": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden. Zusätzlich werden Seiten auf [[:$1]] gelistet, die nicht vorhandene Dateien einbetten.",
        "wantedfiletext-nocat": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden. Vorhandene Dateien aus fremden Repositorien können dennoch hier aufgelistet sein und werden <del>durchgestrichen</del> dargestellt.",
+       "wantedfiletext-nocat-noforeign": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden.",
        "wantedtemplates": "Gewünschte Vorlagen",
        "mostlinked": "Seiten mit den meisten Links",
        "mostlinkedcategories": "Meistbenutzte Kategorien",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|Diskussion]])",
        "unknown_extension_tag": "Unbekanntes Parsertag „$1“",
        "duplicate-defaultsort": "Achtung: Der Sortierungsschlüssel „$2“ überschreibt den vorher verwendeten Schlüssel „$1“.",
+       "duplicate-displaytitle": "<strong>Warnung:</strong> Der Anzeigetitel „$2“ überschreibt den früheren Anzeigetitel „$1“.",
        "version": "Version",
        "version-extensions": "Installierte Erweiterungen",
        "version-skins": "Installierte Benutzeroberflächen",
index 4250606..0ed4a54 100644 (file)
        "talkpagelinktext": "Vatenayış",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê şexsiy",
-       "postcomment": "Qısımo newe",
        "articlepage": "Pela zerreki bıvêne",
        "talk": "Werênayış",
        "views": "Asayışi",
        "versionrequired": "No $1 MediaWiki lazımo",
        "versionrequiredtext": "Seba gurenayışê na pele versiyonê MediaWiki $1 lazımo. \n[[Special:Version|Versiyonê pele]] bıvêne.",
        "ok": "Temam",
-       "pagetitle": "\"$1\" adres ra gerya.",
+       "pagetitle": "$1 – {{SITENAME}}",
        "pagetitle-view-mainpage": "{{SITENAME}}",
        "backlinksubtitle": "← $1",
        "retrievedfrom": "\"$1\" ra ard",
        "externaldberror": "Ya database de xeta esta ya zi heqê şıma çino şıma no hesab bıvurni.",
        "login": "Cı kewe",
        "nav-login-createaccount": "Dekew de / hesab vıraze",
-       "loginprompt": "{{SITENAME}} dı ronıştış akerdışi rê ''çerezan'' aktiv kerdış icab keno.",
        "userlogin": "Cı kewe / hesab vıraze",
        "userloginnocreate": "Cı kewe",
        "logout": "Bıveciye",
        "pagemerge-logentry": "[[$1]] u [[$2]] yew kerd (revizyonî heta $3)",
        "revertmerge": "Abırnê",
        "mergelogpagetext": "Cêr de yew liste esta ke mocnena ra, raya tewr peyêne kamci pela tarixi be a bine ra şanawa pê.",
-       "history-title": "Rewizyonê $1:",
+       "history-title": "Tarixê çımraviyarnayışê \"$1\"",
        "difference-title": "Pela \"$1\" ferqê çım ra viyarnayışan",
        "difference-title-multipage": "Ferkê pelan dê \"$1\" u \"$2\"",
        "difference-multipage": "(Ferqê pelan)",
index c7bd536..5aec3ec 100644 (file)
        "talkpagelinktext": "Συζήτηση",
        "specialpage": "Ειδική σελίδα",
        "personaltools": "Προσωπικά εργαλεία",
-       "postcomment": "Νέα ενότητα",
        "articlepage": "Εμφάνιση σελίδας περιεχομένου",
        "talk": "Συζήτηση",
        "views": "Προβολές",
        "externaldberror": "Είτε συνέβη κάποιο σφάλμα εξωτερικής πιστοποίησης της βάσης δεδομένων είτε δεν σας έχει επιτραπεί να ενημερώσετε τον εξωτερικό σας λογαριασμό.",
        "login": "Είσοδος",
        "nav-login-createaccount": "Είσοδος / δημιουργία λογαριασμού",
-       "loginprompt": "Πρέπει να έχετε ενεργοποιήσει τα cookies για να συνδεθείτε στον ιστοχώρο {{SITENAME}}.",
        "userlogin": "Είσοδος / δημιουργία λογαριασμού",
        "userloginnocreate": "Είσοδος",
        "logout": "Έξοδος",
        "largefileserver": "Το μέγεθος αυτού του αρχείο είναι μεγαλύτερο από το μέγιστο μέγεθος που ο εξυπηρετητής είναι ρυθμισμένος να επιτρέπει.",
        "emptyfile": "Το αρχείο που φορτώσατε φαίνεται να είναι κενό. Αυτό μπορεί να οφείλεται σε λάθος πληκτρολόγησης του ονόματος του αρχείου. Παρακαλούμε ελέγξτε εαν αυτό είναι πραγματικά το αρχείο που θέλετε να φορτώσετε.",
        "windows-nonascii-filename": "Αυτό το wiki δεν υποστηρίζει ονόματα αρχείων με ειδικούς χαρακτήρες.",
-       "fileexists": "Υπάρχει ήδη αρχείο με αυτό το όνομα, παρακαλούμε ελέγξτε το <strong>[[:$1]]</strong> εάν δεν είστε σίγουρος/η αν θέλετε να το αλλάξετε.\n[[$1|thumb]]",
+       "fileexists": "Υπάρχει ήδη αρχείο με αυτό το όνομα, παρακαλούμε ελέγξτε το <strong>[[:$1]]</strong> εάν δεν είστε {{GENDER:|σίγουρος|σίγουρη}} αν θέλετε να το αλλάξετε.\n[[$1|thumb]]",
        "filepageexists": "Η σελίδα περιγραφής για αυτό το αρχείο δημιουργήθηκε ήδη στο <strong>[[:$1]]</strong>, αλλά κανένα αρχείο με αυτό το όνομα δεν υπάρχει αυτή τη στιγμή.\nΗ περιγραφἠ που θα εισάγετε δεν θα εμφανιστεί στη σελίδα περιγραφής.\nΓια να εμφανιστεί η περιγραφή σας εκεί, θα πρέπει να την επεξεργαστείτε χειροκίνητα.\n[[$1|thumb]]",
        "fileexists-extension": "Ένα αρχείο με παρόμοιο όνομα υπάρχει: [[$2|thumb]]\n* Όνομα του προς επιφόρτωση αρχείου: <strong>[[:$1]]</strong>\n* Όνομα υπάρχοντος αρχείου: <strong>[[:$2]]</strong>\nΠαρακαλώ διαλέξτε ένα διαφορετικό όνομα.",
        "fileexists-thumbnail-yes": "Το αρχείο φαίνεται ότι είναι μια εικόνα μειωμένου μεγέθους ''(μικρογραφία)''. [[$1|thumb]]\nΠαρακαλώ ελέγξτε το αρχείο <strong>[[:$1]]</strong>.\nΑν το ελεγμένο αρχείο είναι η ίδια εικόνα στο αρχικό μέγεθος δεν είναι απαραίτητο να επιφορτώσετε μια επιπλέον μικρογραφία.",
index 1be50b4..a707f2b 100644 (file)
        "talkpagelinktext": "Talk",
        "specialpage": "Special page",
        "personaltools": "Personal tools",
-       "postcomment": "New section",
        "addsection": "+",
        "articlepage": "View content page",
        "talk": "Discussion",
        "externaldberror": "There was either an authentication database error or you are not allowed to update your external account.",
        "login": "Log in",
        "nav-login-createaccount": "Log in / create account",
-       "loginprompt": "You must have cookies enabled to log in to {{SITENAME}}.",
+       "loginprompt": "",
        "userlogin": "Log in / create account",
        "userloginnocreate": "Log in",
        "logout": "Log out",
        "license-header": "Licensing",
        "nolicense": "None selected",
        "licenses": "-",
+       "licenses-edit": "Edit license options",
        "license-nopreview": "(Preview not available)",
        "upload_source_url": "(a valid, publicly accessible URL)",
        "upload_source_file": "(a file on your computer)",
+       "listfiles-delete": "delete",
        "listfiles-summary": "This special page shows all uploaded files.",
        "listfiles_search_for": "Search for media name:",
        "imgfile": "file",
        "wantedfiles": "Wanted files",
        "wantedfiles-summary": "",
        "wantedfiletext-cat": "The following files are used but do not exist. Files from foreign repositories may be listed despite existing. Any such false positives will be <del>struck out</del>. Additionally, pages that embed files that do not exist are listed in [[:$1]].",
+       "wantedfiletext-cat-noforeign": "The following files are used but do not exist. Additionally, pages that embed files that do not exist are listed in [[:$1]].",
        "wantedfiletext-nocat": "The following files are used but do not exist. Files from foreign repositories may be listed despite existing. Any such false positives will be <del>struck out</del>.",
+       "wantedfiletext-nocat-noforeign": "The following files are used but do not exist.",
        "wantedtemplates": "Wanted templates",
        "wantedtemplates-summary": "",
        "mostlinked": "Most linked-to pages",
        "timezone-utc": "UTC",
        "unknown_extension_tag": "Unknown extension tag \"$1\"",
        "duplicate-defaultsort": "<strong>Warning:</strong> Default sort key \"$2\" overrides earlier default sort key \"$1\".",
+       "duplicate-displaytitle": "<strong>Warning:</strong> Display title \"$2\" overrides earlier display title \"$1\".",
        "version": "Version",
        "version-summary": "",
        "version-extensions": "Installed extensions",
index 45dc6b7..590d8c2 100644 (file)
        "talkpagelinktext": "Diskuto",
        "specialpage": "Speciala Paĝo",
        "personaltools": "Personaj iloj",
-       "postcomment": "Nova sekcio",
        "articlepage": "Rigardi artikolon",
        "talk": "Diskuto",
        "views": "Vidoj",
        "externaldberror": "Aŭ estis datenbaza eraro rilate al ekstera aŭtentikigado, aŭ vi ne rajtas ĝisdatigi vian eksteran konton.",
        "login": "Ensaluti",
        "nav-login-createaccount": "Ensaluti / Krei novan konton",
-       "loginprompt": "Via foliumilo nepre permesu kuketojn por ensaluti en la {{SITENAME}}.",
        "userlogin": "Ensaluti / Krei novan konton",
        "userloginnocreate": "Ensaluti",
        "logout": "Elsaluti",
        "editundo": "malfari",
        "diff-empty": "(Neniu diferenco)",
        "diff-multi-sameuser": "({{PLURAL:$1|Unu meza versio|$1 mezaj versioj}} de la sama uzanto ne montriĝas)",
-       "diff-multi-otherusers": "({{PLURAL:$1|Unu meza versio|$1 mezaj versioj}} de {{PLURAL:$2|alia uzanto|$2 uzoj}} ne montriĝas)",
+       "diff-multi-otherusers": "({{PLURAL:$1|Unu meza versio|$1 mezaj versioj}} de {{PLURAL:$2|alia uzanto|$2 uzantoj}} ne montriĝas)",
        "diff-multi-manyusers": "({{PLURAL:$1|Unu intermeza versio|$1 intermezaj versioj}} de pli ol {{PLURAL:$2|unu uzanto|$2 uzantoj}} ne estas {{PLURAL:$1|montrata|montrataj}}.)",
        "difference-missing-revision": "{{PLURAL:$2|Unu revizio|$2 revizioj}} de ĉi tiu malsameco ($1) ne {{PLURAL:$2|estis|estis}} trovebla.\n\nLa kutima kaŭzo estas sekvi malaktualan malsamo-ligilon al paĝo forviŝita.\nDetaloj troveblos en la [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de forviŝoj].",
        "searchresults": "Serĉrezultoj",
        "license-nopreview": "(Antaŭvido ne montrebla)",
        "upload_source_url": " (valida, publike atingebla URL-o)",
        "upload_source_file": " (dosiero en via komputilo)",
+       "listfiles-delete": "forigi",
        "listfiles-summary": "Ĉi tiu speciala paĝo montras ĉiujn alŝutitajn dosierojn.\nKiam oni filtras ĝin laŭ uzanto, nur la aktuala versio de la dosiero estos montrita.",
        "listfiles_search_for": "Serĉi dosieran nomon:",
        "imgfile": "dosiero",
        "addedwatchtext": "La paĝo \"[[:$1]]\" aldoniĝis al via [[Special:Watchlist|atentaro]]. Estontaj ŝanĝoj de tiu paĝo kaj de ĝia rilata diskutpaĝo aperos tie.",
        "removewatch": "Forigi el atentaro",
        "removedwatchtext": "La paĝo \"[[:$1]]\" estas forigita el via [[Special:Watchlist|atentaro]].",
+       "removedwatchtext-short": "La paĝo \"$1\" estis forigita el via atento-listo.",
        "watch": "Atenti",
        "watchthispage": "Priatenti paĝon",
        "unwatch": "Malatenti",
        "newimages-summary": "Ĉi tiu speciala paĝo montras la lastajn alŝutitajn dosierojn.",
        "newimages-legend": "Dosiernomo",
        "newimages-label": "Dosiernomo (aŭ parto de ĝi):",
+       "newimages-showbots": "Montri alŝutojn per robotoj",
        "noimages": "Nenio videbla.",
        "ilsubmit": "Serĉi",
        "bydate": "laŭ dato",
        "version-hook-name": "Nomo de hoko",
        "version-hook-subscribedby": "Abonita de",
        "version-version": "($1)",
+       "version-no-ext-name": "[sen nomo]",
        "version-license": "Permesilo de MediaWiki",
        "version-ext-license": "Permesilo",
        "version-ext-colheader-name": "Etendilo",
+       "version-skin-colheader-name": "Etoso",
        "version-ext-colheader-version": "Versio",
        "version-ext-colheader-license": "Permesilo",
        "version-ext-colheader-description": "Priskribo",
        "expand_templates_remove_nowiki": "Nuligi <nowiki> etikedojn en rezulto",
        "expand_templates_generate_xml": "Montri XML-sintaksarbon",
        "expand_templates_generate_rawhtml": "Montri krudan HTML-n",
-       "expand_templates_preview": "Antaŭrigardo"
+       "expand_templates_preview": "Antaŭrigardo",
+       "pagelang-name": "Paĝo",
+       "pagelang-language": "Lingvo",
+       "pagelang-use-default": "Uzi defaŭltan lingvon",
+       "pagelang-select-lang": "Elekti la lingvon",
+       "right-pagelang": "Ŝanĝi paĝan lingvon",
+       "action-pagelang": "ŝanĝi la lingvon de la paĝo",
+       "log-name-pagelang": "Ŝanĝi la lingvan protokolon",
+       "log-description-pagelang": "Jen protokolo pri ŝanĝoj de paĝaj lingvoj.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|ŝanĝis}} la paĝan lingvon por $3 de $4 al $5."
 }
index b0d7a2a..635f15c 100644 (file)
                        "Mcervera",
                        "Wifidel",
                        "Macofe",
-                       "Koavf"
+                       "Koavf",
+                       "Themasterriot"
                ]
        },
        "tog-underline": "Subrayar los enlaces:",
        "talkpagelinktext": "Discusión",
        "specialpage": "Página especial",
        "personaltools": "Herramientas personales",
-       "postcomment": "Sección nueva",
        "articlepage": "Ver artículo",
        "talk": "Discusión",
        "views": "Vistas",
        "externaldberror": "Hubo un error de autenticación de la base de datos o bien no tienes autorización para actualizar tu cuenta externa.",
        "login": "Iniciar sesión",
        "nav-login-createaccount": "Acceder/crear cuenta",
-       "loginprompt": "Hay que activar las ''cookies'' en el navegador para iniciar sesión en {{SITENAME}}.",
        "userlogin": "Acceder/crear cuenta",
        "userloginnocreate": "Acceder",
        "logout": "Salir",
        "recentchanges-summary": "Sigue los cambios más recientes de la wiki en esta página.",
        "recentchanges-noresult": "No hubo cambios durante el período seleccionado que respondan a esos criterios.",
        "recentchanges-feed-description": "Realiza un seguimiento de los cambios más recientes en el wiki en este canal.",
-       "recentchanges-label-newpage": "Esta edición inició una página",
+       "recentchanges-label-newpage": "Esta edición creó una página",
        "recentchanges-label-minor": "Esta es una edición menor",
        "recentchanges-label-bot": "Esta edición fue realizada por un robot",
        "recentchanges-label-unpatrolled": "Esta edición todavía no se ha patrullado",
index 05c0e32..fb3bd43 100644 (file)
        "category-file-count-limited": "{{PLURAL:$1|Järgmine fail|Järgmised $1 faili}} on selles kategoorias.",
        "listingcontinuesabbrev": "jätk",
        "index-category": "Indeksiga leheküljed",
-       "noindex-category": "Indeksita leheküljed",
+       "noindex-category": "Indekseerimata leheküljed",
        "broken-file-category": "Katkiste pildilinkidega leheküljed",
        "about": "Tiitelandmed",
        "article": "artikkel",
        "talkpagelinktext": "arutelu",
        "specialpage": "Erilehekülg",
        "personaltools": "Personaalsed tööriistad",
-       "postcomment": "Uus alaosa",
        "articlepage": "Artiklilehekülg",
        "talk": "Arutelu",
        "views": "vaatamisi",
        "externaldberror": "Esines autentimistõrge või sul pole õigust konto andmeid muuta.",
        "login": "Logi sisse",
        "nav-login-createaccount": "Logi sisse või registreeru kasutajaks",
-       "loginprompt": "Sisselogimiseks peavad küpsised lubatud olema.",
        "userlogin": "Sisselogimine või kasutajakonto loomine",
        "userloginnocreate": "Sisselogimine",
        "logout": "Logi välja",
        "prefs-watchlist-edits-max": "Ülemmäär: 1000",
        "prefs-watchlist-token": "Jälgimisloendi luba:",
        "prefs-misc": "Muu",
-       "prefs-resetpass": "Muuda parooli",
+       "prefs-resetpass": "Muuda parool",
        "prefs-changeemail": "Muuda e-posti aadressi",
        "prefs-setemail": "Määra e-posti aadress",
        "prefs-email": "E-posti sätted",
        "license-nopreview": "(Eelvaade ei ole saadaval)",
        "upload_source_url": "(avalikult ligipääsetav URL)",
        "upload_source_file": "(fail sinu arvutis)",
+       "listfiles-delete": "kustuta",
        "listfiles-summary": "Sellel erileheküljel näidatakse kõiki üles laaditud faile.",
        "listfiles_search_for": "Nimeotsing:",
        "imgfile": "fail",
        "wantedpages-badtitle": "Tulemuste seas on vigane pealkiri: $1",
        "wantedfiles": "Kõige oodatumad failid",
        "wantedfiletext-cat": "Järgmised failid puuduvad, aga on lehekülgedel kasutuses. Siin võivad olla loetletud ka välistes hoidlates asuvad failid, hoolimata sellest, et nad tegelikult olemas on. Loendi sellised valeliikmed on <del>läbi kriipsutatud</del>. Lisaks on puuduvaid faile sisaldavad leheküljed loetletud asukohas [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Järgmised failid puuduvad, aga on kasutuses. Peale selle, leheküljel [[:$1]] on loetletud leheküljed, kus kasutatakse puuduvaid faile.",
        "wantedfiletext-nocat": "Järgmised failid puuduvad, aga on lehekülgedel kasutuses. Siin võivad olla loetletud ka välistes hoidlates asuvad failid, hoolimata sellest, et nad tegelikult olemas on. Loendi sellised valeliikmed on <del>läbi kriipsutatud</del>.",
+       "wantedfiletext-nocat-noforeign": "Järgmised failid puuduvad, aga on kasutuses.",
        "wantedtemplates": "Kõige oodatumad mallid",
        "mostlinked": "Kõige viidatumad leheküljed",
        "mostlinkedcategories": "Kõige viidatumad kategooriad",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|arutelu]])",
        "unknown_extension_tag": "Tundmatu lisa silt \"$1\".",
        "duplicate-defaultsort": "'''Hoiatus:''' Järjestamisvõti \"$2\" tühistab eespool oleva järjestamisvõtme \"$1\".",
+       "duplicate-displaytitle": "<strong>Hoiatus:</strong> Kuvatava pealkirjaga \"$2\" kirjutatakse üle varasem kuvatav pealkiri \"$1\".",
        "version": "Versioon",
        "version-extensions": "Paigaldatud lisad",
        "version-skins": "Paigaldatud kujundused",
index 4f10d30..ab0bb1f 100644 (file)
        "talkpagelinktext": "Eztabaida",
        "specialpage": "Aparteko orrialdea",
        "personaltools": "Tresna pertsonalak",
-       "postcomment": "Atal berria",
        "articlepage": "Artikulua ikusi",
        "talk": "Eztabaida",
        "views": "Ikustaldiak",
        "externaldberror": "Kanpoko datu-base autentifikazio errorea gertatu da edo ez duzu zure kanpo kontua eguneratzeko baimenik.",
        "login": "Saioa hasi",
        "nav-login-createaccount": "Saioa hasi / kontua sortu",
-       "loginprompt": "Cookieak gaituta izatea beharrezkoa da {{SITENAME}}(e)n saioa hasteko.",
        "userlogin": "Saioa hasi / kontua sortu",
        "userloginnocreate": "Saioa hasi",
        "logout": "Saioa itxi",
        "limitreport-expansiondepth": "Gehienezko espantsio sakonera",
        "limitreport-expensivefunctioncount": "Parser funtzio kontaketa garestia",
        "expandtemplates": "Txantiloi ordezkatzailea",
-       "expand_templates_intro": "Aparteko orrialde honek modu errekurtsiboan txantiloiak ordezkatu egiten ditu.\nFuntzioak ere ordezkatu egiten ditu, hala nola\n<code><nowiki>{{</nowiki>#language:…}}</code>, eta\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code> bezalako aldagaiak ere.\nKortxete bikoitzarekin hobeto egiten da lan.",
+       "expand_templates_intro": "Orri berezi honek testua hartu eta txantiloi guztiak modu errekurtsiboan zabaltzen ditu.\nOnartutako funtzio sintaktikoak ere ordezkatzen ditu, hala nola\n<code><nowiki>{{</nowiki>#language:…}}</code>; eta aldagaiak ere, hala nola\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nIzan ere, kortxete bikoitzen arteko ia edozer zabaltzen du.",
        "expand_templates_title": "Izenburua ({{FULLPAGENAME}} ordezkatzeko, eta abar):",
        "expand_templates_input": "Sarrerako testua:",
        "expand_templates_output": "Emaitza",
index 77c1b16..308fd3c 100644 (file)
@@ -34,7 +34,9 @@
                        "درفش کاویانی",
                        "محک",
                        "아라",
-                       "Mostafadaneshvar"
+                       "Mostafadaneshvar",
+                       "Pouyana",
+                       "Oldstoneage"
                ]
        },
        "tog-underline": "خط کشیدن زیر پیوندها:",
        "talkpagelinktext": "بحث",
        "specialpage": "صفحهٔ ویژه",
        "personaltools": "ابزارهای شخصی",
-       "postcomment": "بخش جدید",
        "articlepage": "نمایش مقاله",
        "talk": "بحث",
        "views": "بازدیدها",
        "externaldberror": "خطایی در ارتباط با پایگاه داده رخ داده‌است یا اینکه شما اجازهٔ به‌روزرسانی حساب خارجی خود را ندارید.",
        "login": "ورود به سامانه",
        "nav-login-createaccount": "ورود به سامانه / ایجاد حساب کاربری",
-       "loginprompt": "برای ورود به {{SITENAME}} باید کوکی‌ها را فعال کنید.",
        "userlogin": "ورود به سامانه / ایجاد حساب کاربری",
        "userloginnocreate": "ورود به سامانه",
        "logout": "خروج از سامانه",
        "license": "اجازه‌نامه:",
        "license-header": "اجازه‌نامه",
        "nolicense": "هیچ کدام انتخاب نشده‌است",
+       "licenses-edit": "گزینه‌های مجوز ویرایش",
        "license-nopreview": "(پیش‌نمایش وجود ندارد)",
        "upload_source_url": "(یک نشانی اینترنتی معتبر و قابل دسترسی برای عموم)",
        "upload_source_file": "(پرونده‌ای در رایانهٔ شما)",
+       "listfiles-delete": "حذف",
        "listfiles-summary": "این صفحهٔ ویژه تمام پرونده‌های بارگذاری‌شده را نمایش می‌دهد.",
        "listfiles_search_for": "جستجو به دنبال نام پرونده چندرسانه‌ای:",
        "imgfile": "پرونده",
        "wantedpages-badtitle": "عنوان نامجاز در مجموعهٔ نتایج: $1",
        "wantedfiles": "پرونده‌های مورد نیاز",
        "wantedfiletext-cat": "پرونده‌های زیر استفاده می‌شوند اما موجود نیستند. همچنین ممکن است پرونده‌های مخازن خارجی با وجود موجود بودن در اینجا فهرست شوند. هرگونه رتبه مثبت کاذب <del>خط خواهد خورد.</del> علاوه بر این، صفحاتی که پرونده‌هایی ناموجود را در خود جای داده‌اند در [[:$1]] فهرست شده‌اند.",
+       "wantedfiletext-cat-noforeign": "پرونده‌های زیر استفاده می‌شود اما وجود ندارد. علاوه بر این، صفحاتی که پرونده‌ها در آنها وجود دارند فهرست شده‌اند در [[:$1]].",
        "wantedfiletext-nocat": "پرونده‌های زیر استفاده می‌شوند اما موجود نیستند. همچنین ممکن است پرونده‌های مخازن خارجی با وجود موجود بودن در اینجا فهرست شوند. هرگونه رتبهٔ مثبت کاذب <del>خط خواهد خورد.</del>",
+       "wantedfiletext-nocat-noforeign": "پرونده‌های زیر استفاده می‌شوند اما وجود ندارد.",
        "wantedtemplates": "الگوهای مورد نیاز",
        "mostlinked": "صفحه‌هایی که بیشتر از همه به آن‌ها پیوند داده شده‌است",
        "mostlinkedcategories": "رده‌هایی که بیشتر از همه به آن‌ها پیوند داده شده‌است",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|بحث]])",
        "unknown_extension_tag": "برچسب ناشناختهٔ افزونه «$1»",
        "duplicate-defaultsort": "هشدار: ترتیب پیش‌فرض «$2» ترتیب پیش‌فرض قبلی «$1» را باطل می‌کند.",
+       "duplicate-displaytitle": "<strong>هشدار:</strong> نمایش عنوان \" $2 \"باعث ابطال پیش نمایش عنوان\" $1 \" می‌شود.",
        "version": "نسخه",
        "version-extensions": "افزونه‌های نصب‌شده",
        "version-skins": "پوسته‌های نصب شده",
index d554fe6..8cde960 100644 (file)
@@ -39,7 +39,8 @@
                        "ZeiP",
                        "לערי ריינהארט",
                        "아라",
-                       "Syreeni"
+                       "Syreeni",
+                       "MrTapsa"
                ]
        },
        "tog-underline": "Linkkien alleviivaus:",
        "talkpagelinktext": "keskustelu",
        "specialpage": "Toimintosivu",
        "personaltools": "Henkilökohtaiset työkalut",
-       "postcomment": "Uusi osio",
        "articlepage": "Näytä varsinainen sivu",
        "talk": "Keskustelu",
        "views": "Näkymät",
        "externaldberror": "Tapahtui virhe ulkoisen autentikointitietokannan käytössä tai sinulla ei ole lupaa päivittää tunnustasi.",
        "login": "Kirjaudu sisään",
        "nav-login-createaccount": "Kirjaudu sisään tai luo tunnus",
-       "loginprompt": "Sinun täytyy sallia evästeet, jotta voit kirjautua sivustolle {{SITENAME}}.",
        "userlogin": "Kirjaudu sisään tai luo tunnus",
        "userloginnocreate": "Kirjaudu sisään",
        "logout": "Kirjaudu ulos",
        "license": "Lisenssi",
        "license-header": "Lisenssi",
        "nolicense": "Ei lisenssiä",
+       "licenses-edit": "Muokkaa lisenssivaihtoehtoja",
        "license-nopreview": "(esikatselua ei saatavilla)",
        "upload_source_url": " (julkinen verkko-osoite)",
        "upload_source_file": " (tiedosto tietokoneella)",
+       "listfiles-delete": "poista",
        "listfiles-summary": "Tämä toimintosivu näyttää kaikki tallennetut tiedostot.",
        "listfiles_search_for": "Nimihaku",
        "imgfile": "tiedosto",
        "wantedpages-badtitle": "Virheellinen otsikko tuloksissa: $1",
        "wantedfiles": "Halutut tiedostot",
        "wantedfiletext-cat": "Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del>. Lisäksi sellaiset sivut, joihin on sisällytetty tiedostoja, jotka eivät ole olemassa, on luetteloitu [[:$1|täällä]].",
+       "wantedfiletext-cat-noforeign": "Seuraavat tiedostot ovat käytössä vaikka niitä ei ole olemassa. Luettelo sellaisista sivuista, joihin on upotettu olemattomia tiedostoja, on [[:$1]].",
        "wantedfiletext-nocat": "Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del.>",
+       "wantedfiletext-nocat-noforeign": "Seuraavia tiedostoja käytetään vaikka niitä ei ole olemassa.",
        "wantedtemplates": "Halutut mallineet",
        "mostlinked": "Viitatuimmat sivut",
        "mostlinkedcategories": "Viitatuimmat luokat",
        "ipbemailban": "Estä käyttäjää lähettämästä sähköpostia",
        "ipbenableautoblock": "Estä automaattisesti viimeisin IP-osoite, josta käyttäjä on muokannut, sekä ne osoitteet, joista hän jatkossa yrittää muokata.",
        "ipbsubmit": "Estä",
-       "ipbother": "Muu kesto",
+       "ipbother": "Muu aikamääre:",
        "ipboptions": "2 tuntia:2 hours,1 päivä:1 day,3 päivää:3 days,1 viikko:1 week,2 viikkoa:2 weeks,1 kuukausi:1 month,3 kuukautta:3 months,6 kuukautta:6 months,1 vuosi:1 year,ikuinen:infinite",
        "ipbhidename": "Piilota tunnus muokkauksista ja listauksista",
        "ipbwatchuser": "Tarkkaile tämän käyttäjän käyttäjä- ja keskustelusivua",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|keskustelu]])",
        "unknown_extension_tag": "Tuntematon laajennuskoodi ”$1”.",
        "duplicate-defaultsort": "'''Varoitus:''' Oletuslajitteluavain ”$2” korvaa aiemman oletuslajitteluavaimen ”$1”.",
+       "duplicate-displaytitle": "<strong>Varoitus:</strong> Näytettävä otsikko \"$2\" päällekirjoittaa edellisen otsikon \"$1\".",
        "version": "Versio",
        "version-extensions": "Asennetut laajennukset",
        "version-skins": "Asennetut ulkoasut",
        "pagelang-language": "Kieli",
        "pagelang-use-default": "Käytä oletuskieltä",
        "pagelang-select-lang": "Valitse kieli",
-       "right-pagelang": "Vaihda sivun kieli",
+       "right-pagelang": "Vaihtaa sivun kieli",
        "action-pagelang": "muuttaa sivun kieliasetuksia",
        "log-name-pagelang": "Kielenvaihtoloki",
        "log-description-pagelang": "Tämä on loki, johon merkitään muutokset sivujen kieliasetuksissa.",
index 9d3fd76..2b43f7f 100644 (file)
                        "아라",
                        "Scoopfinder",
                        "Akeron",
-                       "Linedwell"
+                       "Linedwell",
+                       "Yona b",
+                       "SnowedEarth"
                ]
        },
        "tog-underline": "Souligner les liens :",
        "tog-minordefault": "Marquer toutes mes modifications comme mineures par défaut",
        "tog-previewontop": "Afficher la prévisualisation au-dessus de la zone de modification",
        "tog-previewonfirst": "Afficher la prévisualisation lors de la première modification",
-       "tog-enotifwatchlistpages": "M'avertir par courriel lorsqu'une page ou un fichier de ma liste de suivi est modifiée",
+       "tog-enotifwatchlistpages": "M'avertir par courriel lorsqu'une page ou un fichier de ma liste de suivi est modifié",
        "tog-enotifusertalkpages": "M'avertir par courriel si ma page de discussion est modifiée",
        "tog-enotifminoredits": "M'avertir par courriel également lors des modifications mineures de pages ou de fichiers",
        "tog-enotifrevealaddr": "Afficher mon adresse de courriel dans les courriels de notification",
        "talkpagelinktext": "discuter",
        "specialpage": "Page spéciale",
        "personaltools": "Outils personnels",
-       "postcomment": "Nouvelle section",
        "articlepage": "Voir la page de contenu",
        "talk": "Discussion",
        "views": "Affichages",
        "externaldberror": "Une erreur s'est produite avec la base de données d'authentification externe, ou bien vous n'êtes pas autorisé{{GENDER:||e|(e)}} à mettre à jour votre compte externe.",
        "login": "Connexion",
        "nav-login-createaccount": "Créer un compte ou se connecter",
-       "loginprompt": "Vous devez activer les cookies pour vous connecter à {{SITENAME}}.",
        "userlogin": "Créer un compte ou se connecter",
        "userloginnocreate": "Connexion",
        "logout": "Se déconnecter",
        "license": "Licence",
        "license-header": "Conditions d'utilisation",
        "nolicense": "Aucune licence sélectionnée",
+       "licenses-edit": "Modifier les options de licence",
        "license-nopreview": "(Prévisualisation non disponible)",
        "upload_source_url": " (une URL valide et accessible publiquement)",
        "upload_source_file": " (un fichier sur votre ordinateur)",
+       "listfiles-delete": "supprimer",
        "listfiles-summary": "Cette page spéciale permet de lister tous les fichiers importés.",
        "listfiles_search_for": "Rechercher un nom de média :",
        "imgfile": "fichier",
        "wantedpages-badtitle": "Titre invalide dans les résultats : $1",
        "wantedfiles": "Fichiers les plus demandés",
        "wantedfiletext-cat": "Les fichiers suivants sont utilisés, mais n'existent pas localement. S'ils se trouvent sur un dépôt partagé, ils peuvent être listés ici, bien qu'ils soient, de fait, déjà disponibles. Tous ces faux positifs seront <del>barrés</del>. En outre, les pages qui intègrent des fichiers qui n'existent pas sont répertoriées dans [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Les fichiers suivants sont utilisés mais n'existent pas. De plus, les pages qui intègrent les fichiers qui n'existent pas sont listés dans [[:$1]].",
        "wantedfiletext-nocat": "Les fichiers suivants sont utilisés, mais n'existent pas localement. S'ils se trouvent sur un dépôt partagé, ils peuvent être listés ici, bien qu'ils soient, de fait, déjà disponibles. Tous ces faux positifs seront <del>barrés</del>.",
+       "wantedfiletext-nocat-noforeign": "Les fichiers suivants sont utilisés mais n'existent pas.",
        "wantedtemplates": "Modèles demandés",
        "mostlinked": "Pages les plus liées",
        "mostlinkedcategories": "Catégories les plus utilisées",
        "tooltip-ca-nstab-help": "Voir la page d'aide",
        "tooltip-ca-nstab-category": "Voir la page de la catégorie",
        "tooltip-minoredit": "Marquer mes modifications comme mineures",
-       "tooltip-save": "Enregister vos modifications",
+       "tooltip-save": "Enregistrer vos modifications",
        "tooltip-preview": "Merci de prévisualiser vos modifications avant de les publier",
        "tooltip-diff": "Affiche les modifications que vous avez apportées au texte",
        "tooltip-compareselectedversions": "Afficher les différences entre deux versions de cette page",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussion]])",
        "unknown_extension_tag": "Balise d’extension « $1 » inconnue",
        "duplicate-defaultsort": "Attention : la clé de tri par défaut « $2 » écrase la précédente clé « $1 ».",
+       "duplicate-displaytitle": "<strong>Attention :</strong> Le titre d'affichage «$2» remplace l'ancien titre d'affichage «$1».",
        "version": "Version",
        "version-extensions": "Extensions installées",
        "version-skins": "Habillages installés",
index b15deea..b343117 100644 (file)
        "talkpagelinktext": "谈詑",
        "specialpage": "特殊页",
        "personaltools": "个人工具",
-       "postcomment": "话滴想法",
        "articlepage": "看吖文章",
        "talk": "谈詑",
        "views": "眵",
index b329eb0..ee29121 100644 (file)
        "help": "Cobhair",
        "search": "Lorg",
        "searchbutton": "Lorg",
-       "go": "Rach",
-       "searcharticle": "Rach",
+       "go": "Siuthad",
+       "searcharticle": "Siuthad",
        "history": "Eachdraidh na duilleige",
        "history_short": "Eachdraidh",
        "updatedmarker": "air ùrachadh on turas mu dheireadh a thadhail mi air",
        "talkpagelinktext": "Deasbaireachd",
        "specialpage": "Duilleag shònraichte",
        "personaltools": "Innealan pearsanta",
-       "postcomment": "Earrann ùr",
        "articlepage": "Seall duilleag na susbainte",
        "talk": "Deasbaireachd",
        "views": "Tadhalan",
        "userlogin-yourpassword-ph": "Cuir a-steach am facal-faire agad",
        "createacct-yourpassword-ph": "Cuir a-steach facal-faire",
        "yourpasswordagain": "Ath-sgrìobh facal-faire",
-       "createacct-yourpasswordagain": "Dearbh am facal-faire",
+       "createacct-yourpasswordagain": "Dearbhaich am facal-faire",
        "createacct-yourpasswordagain-ph": "Cuir a-steach am facal-faire a-rithist",
        "remembermypassword": "Cuimhnich gu bheil mi air logadh a-steach air a' choimpiutair seo (suas gu $1 {{PLURAL:$1|latha|latha|làithean|latha}})",
        "userlogin-remembermypassword": "Cum air logadh a-steach mi",
        "externaldberror": "Thachair mearachd le dearbhadh an stòir-dhàta air neo chan eil cead agad an cunntas agad air an taobh a-muigh ùrachadh.",
        "login": "Log a-steach",
        "nav-login-createaccount": "Log a-steach / cruthaich cunntas",
-       "loginprompt": "Feumaidh briosgaidean a bhith ceadaichte mus dèan thu logadh a-steach do {{SITENAME}}.",
        "userlogin": "Log a-steach / cruthaich cunntas",
        "userloginnocreate": "Log a-steach",
        "logout": "Log a-mach",
        "emailauthenticated": "Chaidh an seòladh puist-d agad a dhearbhadh $2 aig $3.",
        "emailnotauthenticated": "Cha deach am post-d agad a dhearbhadh fhathast.\nCha dèid post-d a chur airson gin dhe na feartan a leanas.",
        "noemailprefs": "Sònraich post-d sna roghainnean agad gus na feartan seo a chur an comas.",
-       "emailconfirmlink": "Dearbh an seòladh puist-dhealain agad",
+       "emailconfirmlink": "Dearbhaich an seòladh puist-dhealain agad",
        "invalidemailaddress": "Chan urrainn dhuinn gabhail ris an t-seòladh seo a chionn 's gu bheil coltas cearr air.\nCuir a-steach seòladh san fhòrmat cheart no falamhaich an raon sin.",
        "cannotchangeemail": "Cha ghabh na puist-d a tha co-cheangailte ri cunntas atharrachadh air an uicipeid seo.",
        "emaildisabled": "Chan urrainn dhut puist-d a chur air an làrach seo.",
        "parser-unstrip-loop-warning": "Mhothaich sinn do lùb unstrip",
        "parser-unstrip-recursion-limit": "Chaidheas thairis air crìoch unstrip recursion ($1)",
        "converter-manual-rule-error": "Mhothaich sinn do mhearachd san riaghailt iompachadh làimhe airson cànan",
-       "undo-success": "Gabhaidh an deasachadh seo a neo-dhèanamh.\nThoir sùil air a' choimeas gu h-ìosal is dearbh gur e sin a tha fa-near dhut agus sàbhail na h-atharraichean gu h-ìosal gus neo-dhèanamh an deasachaidh a choileanadh.",
+       "undo-success": "Gabhaidh an deasachadh seo a neo-dhèanamh.\nThoir sùil air a' choimeas gu h-ìosal is dearbhaich gur e sin a tha fa-near dhut agus sàbhail na h-atharraichean gu h-ìosal gus neo-dhèanamh an deasachaidh a choileanadh.",
        "undo-failure": "Cha b' urrainn dhuinn an deasachadh a neo-dhèanamh air sgàth 's gun robh deasachaidhean eile sa mheadhan.",
        "undo-norev": "Cha b' urrainn dhuinn an deasachadh a neo-dhèanamh a chionn 's nach robh e ann no gun deach a sguabadh às.",
        "undo-nochange": "Tha coltas gun deach am mùthadh seo a neo-dhèanamh mu thràth.",
        "revdelete-text-file": "Nochdaidh tionndaidhean dhen fhaidhle a chaidh a sguabadh às ann an eachdraidh na duilleige fhathast ach chan fhaic buill a' phobaill cuid dhen t-susbaint aca.",
        "logdelete-text": "Nochdaidh tachartasan san loga a chaidh a sguabadh às ann an eachdraidh na duilleige fhathast ach chan fhaic buill a' phobaill cuid dhen t-susbaint aca.",
        "revdelete-text-others": "Gheibh rianairean eile air {{SITENAME}} cothrom air an t-susbaint fhalaichte fhathast agus is urrainn dhaibh an sguabadh às a neo-dhèanamh san dearbh eadar-aghaidh mur an deach cuingeachaidhean a bharrachd a chur orra.",
-       "revdelete-confirm": "Dearbh gu bheil thu airson seo a dhèanamh, gu bheil thu a' tuigsinn na thachras ri linn agus gu bheil thu a' dèanamh seo a-rèir [[{{MediaWiki:Policy-url}}|a' phoileasaidh]].",
+       "revdelete-confirm": "Dearbhaich gu bheil thu airson seo a dhèanamh, gu bheil thu a' tuigsinn na thachras ri linn agus gu bheil thu a' dèanamh seo a-rèir [[{{MediaWiki:Policy-url}}|a' phoileasaidh]].",
        "revdelete-suppress-text": "Cha bu chòir dhut mùchadh a chleachdadh <strong>ach</strong> ann an suidheachaidhean mar seo:\n* Fiosrachadh a dh'fhaodadh a bhith dìteachail\n* Fiosrachadh pearsanta a tha cearr\n*: <em>seòladh taighe, àireamhan fòn, àireamhan NI is msaa.</em>",
        "revdelete-legend": "Suidhich cuingeachaidhean na faicsinneachd",
        "revdelete-hide-text": "Teacsa a' mhùthaidh",
        "recentchangeslinked-feed": "Mùthaidhean buntainneach",
        "recentchangeslinked-toolbox": "Mùthaidhean buntainneach",
        "recentchangeslinked-title": "Mùthaidhean co-cheangailte ri \"$1\"",
-       "recentchangeslinked-summary": "Seo liosta nam mùthaidhean a chaidh a chur air duilleagan a tha a' ceangal o dhuilleag shònraichte (no ri buill ann an roinn-seòrsa sònraichte).\nTha duilleagan air [[Special:Watchlist|do chlàr-faire]] ann an litrichean <strong>troma</strong>.",
+       "recentchangeslinked-summary": "Seo liosta nam mùthaidhean a chaidh a chur air duilleagan a tha a' ceangal o dhuilleag shònraichte (no ri buill ann an roinn-seòrsa sònraichte).\nTha duilleagan air a' [[Special:Watchlist|chlàr-fhaire]] agad ann an litrichean <strong>troma</strong>.",
        "recentchangeslinked-page": "Ainm na duilleige:",
        "recentchangeslinked-to": "Seall mùthaidhean nan duilleagan a tha a' ceangal ris an duilleag sin 'na àite",
        "upload": "Luchdaich suas faidhle",
        "allpagesto": "Seall duilleagan a tha a' crìochnachadh aig:",
        "allarticles": "A h-uile duilleag",
        "allinnamespace": "A h-uile duilleag (ainm-spàs $1)",
-       "allpagessubmit": "Rach",
+       "allpagessubmit": "Siuthad",
        "allpagesprefix": "Seall na duilleagan leis an ro-leasachan:",
        "allpagesbadtitle": "Chaidh tiotal duilleige mì-dhligheach a thoirt seachad no bha ro-leasachan eadar-cànain no eadar-uicidh aige.\nFaodaidh gu bheil aon no barrachd charactaran ann nach urrainn dhut a chleachdadh ann an tiotal.",
        "allpages-bad-ns": "Chan eil an t-ainm-spàs \"$1\" aig {{SITENAME}}.",
        "watchlistanontext": "$1 gus nithean air a' chlàr-fhaire agad a shealltainn no a dheasachadh.",
        "watchnologin": "Chan eil thu air logadh a-steach",
        "addwatch": "Cuir air a' chlàr-fhaire",
-       "addedwatchtext": "Chaidh an duilleag \"[[:$1]]\" a chur ri [[Special:Watchlist|do chlàr-faire]].\nNochdaidh mùthaidhean a nithear air an duilleag seo 's air an duilleag deasbaireachd a tha co-cheangailte ris an-seo san àm ri teachd.",
+       "addedwatchtext": "Chaidh an duilleag \"[[:$1]]\" a chur ris a' [[Special:Watchlist|chlàr-fhaire]] agad.\nNochdaidh mùthaidhean a nithear air an duilleag seo 's air an duilleag deasbaireachd a tha co-cheangailte ris an-seo san àm ri teachd.",
        "addedwatchtext-short": "Chaidh an duilleag \"$1\" a chur ris a' chlàr-fhaire agad.",
        "removewatch": "Thoir air falbh on chlàr-fhaire",
-       "removedwatchtext": "Chaidh an duilleag \"[[:$1]]\" a thoirt air falbh o [[Special:Watchlist|do chlàr-faire]].",
+       "removedwatchtext": "Chaidh an duilleag \"[[:$1]]\" a thoirt air falbh on [[Special:Watchlist|chlàr-fhaire]] agad.",
        "removedwatchtext-short": "Chaidh an duilleag \"$1\" a thoirt ait falbh on chlàr-fhaire agad.",
        "watch": "Cum sùil air",
        "watchthispage": "Cum sùil air an duilleag seo",
        "tooltip-preview": "Ro-sheall na mùthaidhean agad; saoil an cleachd thu seo mus sàbhail thu iad?",
        "tooltip-diff": "Seall na mùthaidhean a chuir mi air an teacs",
        "tooltip-compareselectedversions": "Seall an diofar eadar an dà mhùthadh dhen duilleag seo a thagh thu",
-       "tooltip-watch": "Cuir an duilleag seo air do chlàr-faire",
+       "tooltip-watch": "Cuir an duilleag seo ris a' chlàr-fhaire agad",
        "tooltip-watchlistedit-normal-submit": "Thoir tiotalan air falbh",
        "tooltip-watchlistedit-raw-submit": "Ùraich an clàr-faire",
        "tooltip-recreate": "Ath-chruthaich an duilleag seo ged a chaidh a sguabadh às",
        "watchlistall2": "na h-uile",
        "namespacesall": "na h-uile",
        "monthsall": "na h-uile",
-       "confirmemail": "Dearbh an seòladh puist-dhealain",
+       "confirmemail": "Dearbhaich an seòladh puist-dhealain",
        "confirmemail_noemail": "Cha dug thu seachad seòladh puist-d dligheach ann an [[Special:Preferences|roghainnean a' chleachdaiche]] agad.",
-       "confirmemail_text": "Iarraidh {{SITENAME}} ort gun dearbh thu an seòladh puist-d agad mus cleachd thu feartan puist-d.\nCleachd am putan gu h-ìosal gus post-d dearbhaidh a chur dhan t-seòladh agad.\nBidh ceangal le còd sa phost-d ud;\nluchdaich an ceangal sa bhrabhsair agad airson dearbhadh gu bheil an seòladh puist-d agad dligheach.",
+       "confirmemail_text": "Iarraidh {{SITENAME}} ort gun dearbhaich thu an seòladh puist-d agad mus cleachd thu feartan puist-d.\nCleachd am putan gu h-ìosal gus post-d dearbhaidh a chur dhan t-seòladh agad.\nBidh ceangal le còd sa phost-d ud;\nluchdaich an ceangal sa bhrabhsair agad airson dearbhadh gu bheil an seòladh puist-d agad dligheach.",
        "confirmemail_pending": "Chaidh còd dearbhaidh a chur thugad air a' phost-d mar-thà;\nma tha thu air a' chunntas agad a chruthachadh o chionn goirid, 's math dh'fhaoidte gum b' feairrde thu feitheamh mionaid no dhà ach an ruig e thu mus iarr thu còd ùr.",
        "confirmemail_send": "Cuir còd dearbhaidh thugam",
        "confirmemail_sent": "Chaidh post-d dearbhaidh a chur.",
        "feedback-error3": "Mearachd: Cha d' fhuair sinn freagairt on API",
        "feedback-thanks": "Mòran taing! Chaidh do bheachd a phostadh air an duilleag \"[$2 $1]\".",
        "feedback-close": "Dèanta",
-       "feedback-bugcheck": "Taghta! Dearbh nach eil e air [$1 liosta nam bugaichean air a bheil sinn eòlach] mar-thà.",
+       "feedback-bugcheck": "Taghta! Dearbhaich nach eil e air [$1 liosta nam bugaichean air a bheil sinn eòlach] mar-thà.",
        "feedback-bugnew": "Dhearbh mi seo. Dèan aithris air buga ur",
        "searchsuggest-search": "Lorg",
        "searchsuggest-containing": "anns a bheil...",
index d72e97a..ddda55f 100644 (file)
        "talkpagelinktext": "Conversa",
        "specialpage": "Páxina especial",
        "personaltools": "Ferramentas persoais",
-       "postcomment": "Nova sección",
        "articlepage": "Ver a páxina de contido",
        "talk": "Conversa",
        "views": "Vistas",
        "externaldberror": "Ou ben se produciu un erro da base de datos na autenticación externa ou ben non se lle permite actualizar a súa conta externa.",
        "login": "Acceder ao sistema",
        "nav-login-createaccount": "Rexistro",
-       "loginprompt": "Debe habilitar as cookies para acceder a {{SITENAME}}.",
        "userlogin": "Rexistro",
        "userloginnocreate": "Rexistro",
        "logout": "Saír ao anonimato",
        "revdelete-offender": "Autor da revisión:",
        "suppressionlog": "Rexistro de supresións",
        "suppressionlogtext": "A continuación móstrase unha lista coas eliminacións e cos bloqueos recentes, que inclúen contido oculto dos administradores.\nOlle a [[Special:BlockList|lista de bloqueos]] para comprobar os bloqueos vixentes.",
-       "mergehistory": "Fusionar os historiais das páxinas",
-       "mergehistory-header": "Esta páxina permítelle fusionar revisións dos historiais da páxina de orixe nunha nova páxina.\nAsegúrese de que esta modificación da páxina mantén a continuidade histórica.",
+       "mergehistory": "Fusionar os historiais de páxinas",
+       "mergehistory-header": "Esta páxina permítelle fusionar as revisións dos historiais da páxina de orixe nunha nova páxina.\nAsegúrese de que esta modificación mantén a continuidade histórica da páxina.",
        "mergehistory-box": "Fusionar as revisións de dúas páxinas:",
        "mergehistory-from": "Páxina de orixe:",
        "mergehistory-into": "Páxina de destino:",
-       "mergehistory-list": "Historial de edicións fusionables",
-       "mergehistory-merge": "As revisións seguintes de [[:$1]] pódense fusionar con [[:$2]]. Use a columna de botóns de selección para fusionar só as revisións creadasen e antes da hora indicada. Teña en conta que se usa as ligazóns de navegación a columna limparase.",
+       "mergehistory-list": "Historial de edicións que se pode fusionar",
+       "mergehistory-merge": "As seguintes revisións de \"[[:$1]]\" pódense fusionar con \"[[:$2]]\".\nUtilice a columna de caixas de selección para fusionar só as revisións creadas ata a hora indicada, esta incluída.\nTeña en conta que o uso das ligazóns de navegación ha borrar a selección da columna.",
        "mergehistory-go": "Mostrar as edicións que se poden fusionar",
        "mergehistory-submit": "Fusionar as revisións",
        "mergehistory-empty": "Non hai revisións que se poidan fusionar.",
-       "mergehistory-success": "{{PLURAL:$3|Unha revisión|$3 revisións}} de [[:$1]] {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas en [[:$2]].",
-       "mergehistory-fail": "Non se puido fusionar o historial; comprobe outra vez os parámetros de páxina e hora.",
+       "mergehistory-success": "{{PLURAL:$3|Unha revisión|$3 revisións}} de \"[[:$1]]\" {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con \"[[:$2]]\".",
+       "mergehistory-fail": "Non se puido fusionar o historial; comprobe outra vez os parámetros de páxina e data.",
+       "mergehistory-fail-toobig": "Non se puido fusionar o historial, xa que supón trasladar máis revisións que o límite de $1 {{PLURAL:$1|revisión|revisións}}.",
        "mergehistory-no-source": "Non existe a páxina de orixe \"$1\".",
        "mergehistory-no-destination": "Non existe a páxina de destino \"$1\".",
        "mergehistory-invalid-source": "A páxina de orixe ten que ter un título válido.",
        "mergehistory-invalid-destination": "A páxina de destino ten que ter un título válido.",
-       "mergehistory-autocomment": "\"[[:$1]]\" fusionouse en \"[[:$2]]\"",
-       "mergehistory-comment": "\"[[:$1]]\" fusionouse en \"[[:$2]]\": $3",
-       "mergehistory-same-destination": "A orixe das páxinas e o seu destino non poden ser os mesmos",
+       "mergehistory-autocomment": "\"[[:$1]]\" fusionouse con \"[[:$2]]\"",
+       "mergehistory-comment": "\"[[:$1]]\" fusionouse con \"[[:$2]]\": $3",
+       "mergehistory-same-destination": "A páxina de orixe e a páxina de destino non pode ser a mesma",
        "mergehistory-reason": "Motivo:",
        "mergelog": "Rexistro de fusións",
-       "pagemerge-logentry": "fusionou \"[[$1]]\" con \"[[$2]]\" (revisións até o $3)",
+       "pagemerge-logentry": "fusionou \"[[$1]]\" con \"[[$2]]\" (revisións ata o $3)",
        "revertmerge": "Desfacer a fusión",
        "mergelogpagetext": "A continuación hai unha lista coas fusións máis recentes do historial dunha páxina co doutra.",
        "history-title": "Historial de revisións de \"$1\"",
        "largefileserver": "Este ficheiro é de maior tamaño ca o permitido pola configuración do servidor.",
        "emptyfile": "O ficheiro que cargou semella estar baleiro.\nIsto pode deberse a un erro ortográfico no seu nome.\nPor favor, verifique se realmente quere cargar este ficheiro.",
        "windows-nonascii-filename": "Este wiki non soporta os nomes de ficheiros con caracteres especiais.",
-       "fileexists": "Xa existe un ficheiro con ese nome. Por favor, comprobe \"<strong>[[:$1]]</strong>\" se non está seguro de querer cambialo.\n[[$1|thumb]]",
+       "fileexists": "Xa existe un ficheiro con ese nome. Por favor, comprobe \"<strong>[[:$1]]</strong>\" se non está {{GENDER:|seguro|segura}} de querer cambialo.\n[[$1|thumb]]",
        "filepageexists": "A páxina de descrición deste ficheiro xa foi creada en <strong>[[:$1]]</strong>, pero polo de agora non existe ningún ficheiro con este nome.\nO resumo que escribiu non aparecerá na páxina de descrición.\nPara facer que o resumo apareza alí, necesitará editar a páxina manualmente.\n[[$1|thumb]]",
        "fileexists-extension": "Xa existe un ficheiro cun nome semellante: [[$2|thumb]]\n* Nome do ficheiro que intenta cargar: <strong>[[:$1]]</strong>\n* Nome de ficheiro existente: <strong>[[:$2]]</strong>\nPor favor, escolla un nome diferente.",
        "fileexists-thumbnail-yes": "Semella que o ficheiro é unha imaxe de tamaño reducido ''(miniatura)''.\n[[$1|thumb]]\nPor favor, comprobe o ficheiro <strong>[[:$1]]</strong>.\nSe o ficheiro seleccionado é a mesma imaxe en tamaño orixinal non é preciso enviar unha miniatura adicional.",
        "license-nopreview": "(A vista previa non está dispoñible)",
        "upload_source_url": "  (un URL válido e accesible publicamente)",
        "upload_source_file": "  (un ficheiro no seu ordenador)",
+       "listfiles-delete": "borrar",
        "listfiles-summary": "Esta páxina especial mostra todos os ficheiros cargados.",
        "listfiles_search_for": "Buscar polo nome do ficheiro multimedia:",
        "imgfile": "ficheiro",
        "filedelete-maintenance": "Os borrados e restauracións de ficheiros están desactivados temporalmente durante o mantemento.",
        "filedelete-maintenance-title": "Non se pode borrar o ficheiro",
        "mimesearch": "Busca MIME",
-       "mimesearch-summary": "Esta páxina permite filtrar os ficheiros segundo o seu tipo MIME.\nEntrada: tipodecontido/subtipo, por exemplo <code>image/jpeg</code>.",
+       "mimesearch-summary": "Esta páxina permite filtrar os ficheiros segundo o seu tipo MIME.\nEntrada: tipodecontido/subtipo ou tipodecontido/*; por exemplo, <code>image/jpeg</code>.",
        "mimetype": "Tipo MIME:",
        "download": "descargar",
        "unwatchedpages": "Páxinas non vixiadas",
        "wantedpages-badtitle": "Título inválido fixado nos resultados: $1",
        "wantedfiles": "Ficheiros requiridos",
        "wantedfiletext-cat": "Os seguintes ficheiros están en uso, pero non existen. É posible que aparezan ficheiros de repositoroios externos, malia que existan. Calquera falso positivo estará <del>riscado</del>. Ademais, as páxinas que inclúen ficheiros que non existen están listadas en [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Os seguintes ficheiros están en uso, pero non existen. Ademais, as páxinas que inclúen ficheiros que non existen están listadas en [[:$1]].",
        "wantedfiletext-nocat": "Os seguintes ficheiros están en uso, pero non existen. É posible que aparezan ficheiros de repositoroios externos, malia que existan. Calquera falso positivo estará <del>riscado</del>.",
+       "wantedfiletext-nocat-noforeign": "Os seguintes ficheiros están en uso, pero non existen.",
        "wantedtemplates": "Modelos requiridos",
        "mostlinked": "Páxinas máis ligadas",
        "mostlinkedcategories": "Categorías máis ligadas",
-       "mostlinkedtemplates": "Modelos máis ligados",
+       "mostlinkedtemplates": "Modelos máis transcluídos",
        "mostcategories": "Páxinas con máis categorías",
        "mostimages": "Ficheiros máis usados",
        "mostinterwikis": "Páxinas con máis interwikis",
        "markedaspatrollederror-noautopatrol": "Non está permitido que un mesmo marque as propias edicións como revisadas.",
        "markedaspatrollednotify": "A modificación feita en \"$1\" marcouse como revisada.",
        "markedaspatrollederrornotify": "Erro ao marcar como revisada.",
-       "patrol-log-page": "Rexistro de revisións",
+       "patrol-log-page": "Rexistro de revisións patrulladas",
        "patrol-log-header": "Este é un rexistro das revisións patrulladas.",
        "log-show-hide-patrol": "$1 o rexistro de patrullas",
        "deletedrevision": "A revisión vella $1 foi borrada.",
        "watchlisttools-raw": "Editar a lista de vixilancia simple",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|conversa]])",
        "unknown_extension_tag": "Etiqueta de extensión descoñecida \"$1\"",
-       "duplicate-defaultsort": "'''Aviso:''' A clave de ordenación por defecto \"$2\" anula a clave de ordenación anterior por defecto \"$1\".",
+       "duplicate-defaultsort": "<strong>Aviso:</strong> A clave de ordenación por defecto \"$2\" anula a clave de ordenación anterior por defecto \"$1\".",
+       "duplicate-displaytitle": "'''Aviso:''' O título mostrado \"$2\" anula o título anterior \"$1\".",
        "version": "Versión",
        "version-extensions": "Extensións instaladas",
+       "version-skins": "Aparencias instaladas",
        "version-specialpages": "Páxinas especiais",
        "version-parserhooks": "Asociadores analíticos",
        "version-variables": "Variables",
        "version-antispam": "Prevención contra spam",
-       "version-skins": "Aparencias",
        "version-other": "Outros",
        "version-mediahandlers": "Executadores de multimedia",
        "version-hooks": "Asociadores",
        "version-hook-name": "Nome do asociador",
        "version-hook-subscribedby": "Subscrito por",
        "version-version": "($1)",
+       "version-no-ext-name": "[sen nome]",
        "version-license": "Licenza de MediaWiki",
        "version-ext-license": "Licenza",
        "version-ext-colheader-name": "Extensión",
+       "version-skin-colheader-name": "Aparencia",
        "version-ext-colheader-version": "Versión",
        "version-ext-colheader-license": "Licenza",
        "version-ext-colheader-description": "Descrición",
        "expand_templates_remove_nowiki": "Suprimir as etiquetas <nowiki> no resultado",
        "expand_templates_generate_xml": "Mostrar as árbores de análise XML",
        "expand_templates_generate_rawhtml": "Mostrar o HTML en bruto",
-       "expand_templates_preview": "Vista previa"
+       "expand_templates_preview": "Vista previa",
+       "pagelanguage": "Selector de lingua da páxina",
+       "pagelang-name": "Páxina",
+       "pagelang-language": "Lingua",
+       "pagelang-use-default": "Utilizar a lingua por defecto",
+       "pagelang-select-lang": "Seleccionar a lingua",
+       "right-pagelang": "Cambiar a lingua da páxina",
+       "action-pagelang": "cambiar a lingua da páxina",
+       "log-name-pagelang": "Rexistro de cambios de lingua",
+       "log-description-pagelang": "Este é un rexistro dos cambios na lingua das páxinas.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|cambiou}} a lingua da páxina \"$3\" do $4 ao $5."
 }
index d47eab5..579df7a 100644 (file)
        "permalink": "Bschtändigi URL",
        "print": "Drucke",
        "view": "Aaluege",
+       "view-foreign": "Uf $1 aaluege",
        "edit": "Ändere",
+       "edit-local": "Lokali Bschrybig bearbeite",
        "create": "Erstelle",
+       "create-local": "Lokali Bschrybig zuefiege",
        "editthispage": "Syte bearbeite",
        "create-this-page": "Die Syte afange",
        "delete": "Lesche",
        "talkpagelinktext": "Diskussion",
        "specialpage": "Spezialsyte",
        "personaltools": "Persönlichi Wärkzüg",
-       "postcomment": "Neje Abschnitt",
        "articlepage": "Syte",
        "talk": "Diskussion",
        "views": "Wievylmol agluegt",
        "jumptonavigation": "Navigation",
        "jumptosearch": "Suech",
        "view-pool-error": "Excusez, d Server sin zur Zyt iberlaschtet.\nS versueche grad zvyl Benutzer die Syte aazluege.\nBitte wart e paar Minute, voreb Du s nomol versuechsch.\n\n$1",
+       "generic-pool-error": "Excusez, d Server sin zur Zyt iberlaschtet.\nS versueche grad zvyl Benutzer die Ressource aazluege.\nBitte wart e paar Momänt, voreb Du s nomol versuechsch.",
        "pool-timeout": "\nDi maximal Wartezyt fir e Lock isch umme",
        "pool-queuefull": "D Warteschlang isch voll",
        "pool-errorunknown": "Nit bekannte Fähler",
+       "pool-servererror": "Dr Poolzellerdienscht isch nit verfiegbar ($1).",
        "aboutsite": "Über {{GRAMMAR:akkusativ|{{SITENAME}}}}",
        "aboutpage": "Project:Über {{UCFIRST:{{GRAMMAR:akkusativ|{{SITENAME}}}}}}",
        "copyright": "Dr Inhalt vu dere Syte stoht unter dr Lizänz $1, wänn s nit andersch aagee isch.",
        "externaldberror": "Entwäder s lit e Fähler bi dr externe Authentifizierung vor, oder Du derfsch Dyy extern Benutzerkonto nid aktualisiere.",
        "login": "Aamälde",
        "nav-login-createaccount": "Aamälde / Konto aalege",
-       "loginprompt": "<small>Für di bir {{SITENAME}} aazmälde, muesch Cookies erloube!</small>",
        "userlogin": "Aamälde/Konto aalege",
        "userloginnocreate": "Aamälde",
        "logout": "Abmälde",
        "gotaccountlink": "»Login fir Benutzer, wu scho aagmäldet sin«",
        "userlogin-resetlink": "Hesch Dyy Aamäldedate vergässe?",
        "userlogin-resetpassword-link": "Passwort vergässe?",
+       "userlogin-helplink2": "Hilf bim Aamälde",
        "userlogin-loggedin": "Du bisch scho as {{GENDER:$1|$1}} aagmäldet.\nBruuch s Formular unte go Di unter eme andere Benutzername aamälde.",
        "userlogin-createanother": "En ander Benutzerkonto aalege",
        "createacct-emailrequired": "E-Mail-Adräss",
        "user-mail-no-addy": "Es isch versuecht worde e E-Mail ohni Angab vunere E-Mail-Adräss z verschigge.",
        "user-mail-no-body": "S isch versuecht wore, ne E-Mail mit eme lääre oder z churze Tekscht z verschicke.",
        "changepassword": "Passwort ändere",
-       "resetpass_announce": "Aamäldig mit em Code, wu per Mail zuegschickt woren isch. Zum d Aamäldig abzschliesse, muesch jetz e nej Passwort wehle.",
+       "resetpass_announce": "Go d Aamäldig abzschließe, muesch e nej Passwort feschtlege.",
        "resetpass_text": "<!-- Tue do dr Text ergänze -->",
        "resetpass_header": "Passwort zrucksetze",
        "oldpassword": "Alts Passwort",
        "retypenew": "Nöis Passwort (es zwöits Mal)",
        "resetpass_submit": "Passwort ibermittle un aamälde",
        "changepassword-success": "Dyy Passwort isch erfolgryych gänderet wore.",
+       "changepassword-throttled": "Du hesch z vilmol versuecht Di aazmälde. Bitte wart $1, voreb Du s non emol versuechsch.",
        "resetpass_forbidden": "S Passwort cha nid gänderet wäre.",
        "resetpass-no-info": "Du muesch Di aamälde zum uf die Syte diräkt zuegryfe z chenne.",
        "resetpass-submit-loggedin": "Passwort ändere",
        "resetpass-submit-cancel": "Abbräche",
        "resetpass-wrong-oldpass": "S temporär oder aktuäll Passwort isch nimi giltig.\nVillicht hesch Dyy Passwort scho gänderet oder e nej temporär Passwort aagforderet.",
+       "resetpass-recycled": "Bitte due Dy Passwort ändere.",
+       "resetpass-temp-emailed": "Du hesch Di mit eme temporäre E-Mail-Code aagmäldet.\nGo d Aamäldig abzschließe, muesch e jetz no ne nej Passwort feschtlege:",
        "resetpass-temp-password": "Temporär Passwort:",
        "resetpass-abort-generic": "D Passwortänderig isch dur e Erwyterig abbroche wore.",
+       "resetpass-expired": "Dy Passwort isch abglofe. Bitte leg e nej Passwort fir d Aamäldig fescht.",
        "passwordreset": "Passwort zruggsetze",
        "passwordreset-text-one": "Fill des Formular uus go Dy Passwort zrucksetze.",
        "passwordreset-text-many": "{{PLURAL:$1|Fill eis vu dr Fälder uus go Dy Passwort zrucksetze.}}",
        "duplicate-defaultsort": "Obacht: Dr Sortierigsschlüssel „$2“ iberschrybt dr vorig brucht Schlüssel „$1“.",
        "version": "Version",
        "version-extensions": "Installierti Erwyterige",
+       "version-skins": "Benutzeroberflechine",
        "version-specialpages": "Spezialsyte",
        "version-parserhooks": "Parser-Schnittstelle",
        "version-variables": "Variable",
        "version-antispam": "Spamschutz",
-       "version-skins": "Benutzeroberflechine",
        "version-other": "Anders",
        "version-mediahandlers": "Medie-Handler",
        "version-hooks": "Schnittstelle ''(Hook)''",
index d4e2ecf..f3a899f 100644 (file)
        "talkpagelinktext": "שיחה",
        "specialpage": "דף מיוחד",
        "personaltools": "כלים אישיים",
-       "postcomment": "פסקה חדשה",
        "articlepage": "צפייה בדף התוכן",
        "talk": "שיחה",
        "views": "צפיות",
        "externaldberror": "הייתה שגיאה בבסיס הנתונים של ההזדהות, או שאינכם רשאים לעדכן את חשבונכם החיצוני.",
        "login": "כניסה לחשבון",
        "nav-login-createaccount": "כניסה לחשבון / הרשמה",
-       "loginprompt": "לפני הכניסה לחשבון ב{{grammar:תחילית|{{SITENAME}}}}, עליכם לוודא כי ה\"עוגיות\" (Cookies) מופעלות.",
        "userlogin": "כניסה לחשבון / הרשמה",
        "userloginnocreate": "כניסה לחשבון",
        "logout": "יציאה מהחשבון",
        "license": "רישיון:",
        "license-header": "רישיון",
        "nolicense": "אין",
+       "licenses-edit": "עריכת אפשרויות רישיון",
        "license-nopreview": "(תצוגה מקדימה לא זמינה)",
        "upload_source_url": "(כתובת URL תקפה ונגישה)",
        "upload_source_file": "(קובץ במחשב שלך)",
+       "listfiles-delete": "מחיקה",
        "listfiles-summary": "דף מיוחד זה מציג את כל הקבצים שהועלו.",
        "listfiles_search_for": "חיפוש קובץ מדיה בשם:",
        "imgfile": "קובץ",
        "wantedpages-badtitle": "כותרת בלתי תקינה ברשימת התוצאות: $1",
        "wantedfiles": "קבצים מבוקשים",
        "wantedfiletext-cat": "הקבצים הבאים נמצאים בשימוש, אך אינם קיימים. ייתכן שקבצים ממאגרים חיצוניים יהיו רשומים אף על פי שהם קיימים, אך שגיאות כאלה יהיו <del>מחוקות</del>. בנוסף, דפים שמשתמשים בקבצים שאינם קיימים רשומים בדף [[:$1]].",
+       "wantedfiletext-cat-noforeign": "הקבצים הבאים נמצאים בשימוש, אבל אינם קיימים. כמו־כן, דפים שמשתמשים בקבצים שאינם קיימים רשומים בדף [[:$1]].",
        "wantedfiletext-nocat": "הקבצים הבאים נמצאים בשימוש, אך אינם קיימים. ייתכן שקבצים ממאגרים חיצוניים יהיו רשומים אף על פי שהם קיימים, אך שגיאות כאלה יהיו <del>מחוקות</del>.",
+       "wantedfiletext-nocat-noforeign": "הקבצים הבאים נמצאים בשימוש, אבל אינם קיימים.",
        "wantedtemplates": "תבניות מבוקשות",
        "mostlinked": "הדפים המקושרים ביותר",
        "mostlinkedcategories": "הקטגוריות המקושרות ביותר",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|שיחה]])",
        "unknown_extension_tag": "תגית בלתי ידועה: \"$1\"",
        "duplicate-defaultsort": "'''אזהרה:''' המיון הרגיל \"$2\" דורס את המיון הרגיל המוקדם ממנו \"$1\".",
+       "duplicate-displaytitle": "<strong>אזהרה:</strong> כותרת התצוגה \"$2\" דורסת את כותרת התצוגה הקודמת \"$1\".",
        "version": "גרסת התוכנה",
        "version-extensions": "הרחבות מותקנות",
        "version-skins": "עיצובים מותקנים",
index f2cf581..62fd035 100644 (file)
        "talkpagelinktext": "razgovor",
        "specialpage": "Posebna stranica",
        "personaltools": "Osobni alati",
-       "postcomment": "Novi odlomak",
        "articlepage": "Vidi članak",
        "talk": "Razgovor",
        "views": "Pogledi",
        "externaldberror": "Došlo je do pogreške s vanjskom autorizacijom ili Vam nije dopušteno osvježavanje vanjskog suradničkog računa.",
        "login": "Prijavi se",
        "nav-login-createaccount": "Prijavi se",
-       "loginprompt": "Za prijavu na sustav {{SITENAME}} morate u pregledniku uključiti kolačiće (cookies).",
        "userlogin": "Prijavi se / stvori račun",
        "userloginnocreate": "Prijavi se",
        "logout": "Odjavi se",
        "listfiles_size": "Veličina (u bajtovima)",
        "listfiles_description": "Opis",
        "listfiles_count": "Inačice",
+       "listfiles-show-all": "Uključujući starije inačice slika",
        "listfiles-latestversion-yes": "Da",
        "listfiles-latestversion-no": "Ne",
        "file-anchor-link": "Slika",
index 4567ce3..8203833 100644 (file)
@@ -54,7 +54,7 @@
        "tog-watchdeletion": "Az általam törölt lapok és fájlok felvétele a figyelőlistámra",
        "tog-minordefault": "Alapértelmezetten minden szerkesztésemet jelölje aprónak",
        "tog-previewontop": "Előnézet megjelenítése a szerkesztőablak előtt",
-       "tog-previewonfirst": "Előnézet első szerkesztésnél",
+       "tog-previewonfirst": "Előnézet mutatása az első szerkesztésnél",
        "tog-enotifwatchlistpages": "Kapjak értesítést e-mailben, ha egy általam figyelt lap vagy fájl megváltozik",
        "tog-enotifusertalkpages": "Kapjak értesítést e-mailben, ha megváltozik a vitalapom",
        "tog-enotifminoredits": "Kapjak értesítést e-mailben a lapok és fájlok apró változtatásairól",
        "talkpagelinktext": "vitalap",
        "specialpage": "Speciális lap",
        "personaltools": "Személyes eszközök",
-       "postcomment": "Új szakasz",
        "articlepage": "Szócikk megtekintése",
        "talk": "Vitalap",
        "views": "Nézetek",
        "externaldberror": "Hiba történt a külső adatbázis hitelesítése közben, vagy nem vagy jogosult a külső fiókod frissítésére.",
        "login": "Bejelentkezés",
        "nav-login-createaccount": "Bejelentkezés / fiók létrehozása",
-       "loginprompt": "Engedélyezned kell a sütiket (''cookie''), hogy bejelentkezhess a(z) {{SITENAME}} wikibe.",
        "userlogin": "Bejelentkezés / fiók létrehozása",
        "userloginnocreate": "Bejelentkezés",
        "logout": "Kijelentkezés",
        "sp-contributions-search": "Közreműködések szűrése",
        "sp-contributions-username": "IP-cím vagy felhasználónév:",
        "sp-contributions-toponly": "Csak a jelenleg utolsónak számító változtatásokat mutassa",
+       "sp-contributions-newonly": "Csak az új oldalt létrehozó szerkesztéseket mutassa",
        "sp-contributions-submit": "Keresés",
        "whatlinkshere": "Mi hivatkozik erre",
        "whatlinkshere-title": "A(z) „$1” lapra hivatkozó lapok",
index ca955f2..f6e3c30 100644 (file)
        "talkpagelinktext": "Discussion",
        "specialpage": "Pagina special",
        "personaltools": "Instrumentos personal",
-       "postcomment": "Nove section",
        "articlepage": "Vider pagina de contento",
        "talk": "Discussion",
        "views": "Representationes",
        "externaldberror": "O il occurreva un error in le base de datos de authentication, o tu non ha le autorisation de actualisar tu conto externe.",
        "login": "Aperir session",
        "nav-login-createaccount": "Aperir session / crear conto",
-       "loginprompt": "Tu debe haber activate le cookies pro poter aperir un session in {{SITENAME}}.",
        "userlogin": "Aperir session / crear conto",
        "userloginnocreate": "Aperir session",
        "logout": "Clauder session",
        "license-nopreview": "(Previsualisation non disponibile)",
        "upload_source_url": " (un adresse URL valide e publicamente accessibile)",
        "upload_source_file": " (un file in tu computator)",
+       "listfiles-delete": "deler",
        "listfiles-summary": "Iste pagina special monstra tote le files incargate.",
        "listfiles_search_for": "Cercar un nomine de media:",
        "imgfile": "file",
        "wantedpages-badtitle": "Titulo invalide in le gruppo de resultatos: $1",
        "wantedfiles": "Files desirate",
        "wantedfiletext-cat": "Le sequente files es usate ma non existe. Le files ab repositorios externe pote esser listate malgrado que illos existe. Omne tal false positivos essera <del>cancellate</del>. In addition, paginas que incorpora files que non existe es listate in [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Le sequente files es usate ma non existe. In addition, paginas que incorpora files que non existe es listate in [[:$1]].",
        "wantedfiletext-nocat": "Le sequente files es usate ma non existe. Files ab repositorios externe pote esser listate malgrado que illos existe. Omne tal false positivos essera <del>cancellate</del>.",
+       "wantedfiletext-nocat-noforeign": "Le sequente files es usate ma non existe.",
        "wantedtemplates": "Patronos desirate",
        "mostlinked": "Paginas le plus ligate",
        "mostlinkedcategories": "Categorias le plus ligate",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussion]])",
        "unknown_extension_tag": "Etiquetta de extension incognite \"$1\"",
        "duplicate-defaultsort": "Attention: Le clave de ordination predefinite \"$2\" supplanta le anterior clave de ordination predefinite \"$1\".",
+       "duplicate-displaytitle": "<strong>Attention:</strong> Le titulo a monstrar \"$2\" supplanta le ancian titulo a monstrar \"$1\".",
        "version": "Version",
        "version-extensions": "Extensiones installate",
        "version-skins": "Apparentias installate",
        "pagelang-use-default": "Usar lingua predefinite",
        "pagelang-select-lang": "Selige lingua",
        "right-pagelang": "Cambiar lingua del pagina",
-       "action-pagelang": "Cambiar lingua del pagina",
+       "action-pagelang": "cambiar le lingua del pagina",
        "log-name-pagelang": "Registro de cambios de lingua",
        "log-description-pagelang": "Isto es un registro de cambios de lingua in paginas.",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|cambiava}} le lingua del pagina $3 de $4 a $5."
index b23e77e..87985cc 100644 (file)
        "talkpagelinktext": "bicara",
        "specialpage": "Halaman istimewa",
        "personaltools": "Peralatan pribadi",
-       "postcomment": "Bagian baru",
        "articlepage": "Lihat halaman isi",
        "talk": "Pembicaraan",
        "views": "Tampilan",
        "externaldberror": "Telah terjadi kesalahan otentikasi basis data eksternal atau Anda tidak diizinkan melakukan kemaskini terhadap akun eksternal Anda.",
        "login": "Masuk log",
        "nav-login-createaccount": "Masuk log / buat akun",
-       "loginprompt": "Anda harus mengaktifkan kuki untuk dapat masuk log ke {{SITENAME}}.",
        "userlogin": "Masuk log / buat akun",
        "userloginnocreate": "Masuk log",
        "logout": "Keluar log",
        "mergehistory-empty": "Tidak ada revisi yang dapat digabung.",
        "mergehistory-success": "$3 {{PLURAL:$3|revisi|revisi}} dari [[:$1]] berhasil digabungkan ke [[:$2]].",
        "mergehistory-fail": "Tidak dapat melakukan penggabungan, harap periksa kembali halaman dan parameter waktu.",
+       "mergehistory-fail-toobig": "Tidak dapat melakukan penggabungan sebagai lebih dari batas dari $1 {{PLURAL:$1|revisi|revisi}} akan dipindahkan.",
        "mergehistory-no-source": "Halaman sumber $1 tidak ada.",
        "mergehistory-no-destination": "Halaman tujuan $1 tidak ada.",
        "mergehistory-invalid-source": "Judul halaman sumber haruslah judul yang berlaku.",
        "largefileserver": "Berkas ini lebih besar dari pada yang diizinkan server.",
        "emptyfile": "Berkas yang Anda muatkan kelihatannya kosong. Hal ini mungkin disebabkan karena adanya kesalahan ketik pada nama berkas. Silakan pastikan apakah Anda benar-benar ingin memuatkan berkas ini.",
        "windows-nonascii-filename": "Wiki ini tidak mendukung nama berkas dengan karakter istimewa.",
-       "fileexists": "Suatu berkas dengan nama tersebut telah ada, harap periksa <strong>[[:$1]]</strong> jika Anda tidak yakin untuk mengubahnya.\n[[$1|thumb]]",
+       "fileexists": "Suatu berkas dengan nama tersebut telah ada, harap periksa <strong>[[:$1]]</strong> jika {{GENDER:|Anda}} tidak yakin untuk mengubahnya.\n[[$1|thumb]]",
        "filepageexists": "Halaman deskripsi untuk berkas ini telah dibuat di <strong>[[:$1]]</strong>, tapi saat ini tak ditemukan berkas dengan nama tersebut. Ringkasan yang Anda masukkan tidak akan tampil pada halaman deskripsi. Untuk memunculkannya, Anda perlu untuk menyuntingnya secara manual.\n[[$1|thumb]]",
-       "fileexists-extension": "Berkas dengan nama serupa telah ada: [[$2|thumb]]\n* Nama berkas yang akan dimuat: <strong>[[:$1]]</strong>\n* Nama berkas yang telah ada: <strong>[[:$2]]</strong>\nMohon gunakan nama yang berbeda.",
+       "fileexists-extension": "Berkas dengan nama serupa telah ada: [[$2|thumb]]\n* Nama berkas yang akan dimuat: <strong>[[:$1]]</strong>\n* Nama berkas yang telah ada: <strong>[[:$2]]</strong>\nApakah Anda mungkin ingin menggunakan nama yang lebih khas?",
        "fileexists-thumbnail-yes": "Berkas ini tampaknya merupakan gambar yang ukurannya diperkecil ''(miniatur)''. [[$1|thumb]]\nHarap periksa berkas <strong>[[:$1]]</strong> tersebut.\nJika berkas tersebut memang merupakan gambar dalam ukuran aslinya, Anda tidak perlu untuk memuat kembali miniatur lainnya.",
        "file-thumbnail-no": "Nama berkas dimulai dengan <strong>$1</strong>.\nTampaknya berkas ini merupakan gambar dengan ukuran diperkecil ''(miniatur)''.\nJika Anda memiliki versi resolusi penuh dari gambar ini, harap muatkan berkas tersebut. Jika tidak, harap ubah nama berkas ini.",
        "fileexists-forbidden": "Suatu berkas dengan nama ini telah ada dan tak dapat ditimpa.\nJika Anda masih ingin memuat berkas Anda, silakan kembali dan gunakan nama baru. [[File:$1|thumb|center|$1]]",
        "license-nopreview": "(Pratayang tak tersedia)",
        "upload_source_url": " (suatu URL valid yang dapat diakses publik)",
        "upload_source_file": " (suatu berkas di komputer Anda)",
+       "listfiles-delete": "hapus",
        "listfiles-summary": "Halaman istimewa ini menampilkan semua berkas yang telah diunggah.\nKetika disaring oleh pengguna, hanya versi berkas terbaru dari berkas yang diunggah oleh pengguna tersebut yang ditampilkan.",
        "listfiles_search_for": "Cari nama berkas:",
        "imgfile": "berkas",
        "filedelete-maintenance": "Penghapusan dan pengembalian berkas sementara dinonaktifkan selama perawatan.",
        "filedelete-maintenance-title": "Tidak dapat menghapus berkas",
        "mimesearch": "Pencarian MIME",
-       "mimesearch-summary": "Halaman ini menyediakan fasilitas menyaring berkas berdasarkan tipe MIME-nya. Masukkan: contenttype/subtype, misalnya <code>image/jpeg</code>.",
+       "mimesearch-summary": "Halaman ini menyediakan fasilitas menyaring berkas berdasarkan tipe MIME-nya. Masukkan: contenttype/subtype atau contenttype/*, misalnya <code>image/jpeg</code>.",
        "mimetype": "Tipe MIME:",
        "download": "unduh",
        "unwatchedpages": "Halaman yang tak dipantau",
        "wantedpages-badtitle": "Judul tak valid dalam himpunan hasil: $1",
        "wantedfiles": "Berkas yang diinginkan",
        "wantedfiletext-cat": "Berkas-berkas berikut digunakan tetapi tidak ada. Berkas dari repositori asing mungkin tercantum meskipun ada. Setiap \"false positive\" akan <del>dicoret</del>. Selain itu, halaman yang menggunakan berkas yang tidak ada akan dicantumkan dalam [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Berkas berikut ini digunakan tetapi tidak ada. Selain itu, halaman yang menanamkan berkas yang tidak ada tercantum dalam [[:$1]].",
        "wantedfiletext-nocat": "Berkas-berkas berikut digunakan tetapi tidak ada. Berkas dari repositori asing mungkin tercantum meskipun ada. Setiap \"false positive\" akan <del>dicoret</del>.",
+       "wantedfiletext-nocat-noforeign": "Berkas berikut ini digunakan tetapi tidak ada.",
        "wantedtemplates": "Templat yang diinginkan",
        "mostlinked": "Halaman yang tersering dituju",
        "mostlinkedcategories": "Kategori yang tersering digunakan",
-       "mostlinkedtemplates": "Templat yang tersering digunakan",
+       "mostlinkedtemplates": "Halaman yang paling ditransklusikan",
        "mostcategories": "Halaman dengan kategori terbanyak",
        "mostimages": "Berkas yang tersering digunakan",
        "mostinterwikis": "Halaman dengan interwiki terbanyak",
        "timezone-utc": "UTC",
        "unknown_extension_tag": "Tag ekstensi tidak dikenal \"$1\"",
        "duplicate-defaultsort": "Peringatan: Kunci pengurutan baku \"$2\" mengabaikan kunci pengurutan baku \"$1\" sebelumnya.",
+       "duplicate-displaytitle": "<strong>Peringatan:</strong> Menampilkan judul \"$2\" menimpa judul tampilan \"$1\" sebelumnya.",
        "version": "Versi",
        "version-extensions": "Ekstensi terinstal",
-       "version-skins": "Kulit",
+       "version-skins": "Kulit yang diinstal",
        "version-specialpages": "Halaman istimewa",
        "version-parserhooks": "Kait parser",
        "version-variables": "Variabel",
        "version-hook-name": "Nama kait",
        "version-hook-subscribedby": "Dilanggani oleh",
        "version-version": "(Versi $1)",
+       "version-no-ext-name": "[tanpa nama]",
        "version-svn-revision": "(r$2)",
        "version-license": "Lisensi MediaWiki",
        "version-ext-license": "Lisensi",
        "version-ext-colheader-name": "Ekstensi",
+       "version-skin-colheader-name": "Kulit",
        "version-ext-colheader-version": "Versi",
        "version-ext-colheader-license": "Lisensi",
        "version-ext-colheader-description": "Deskripsi",
        "expand_templates_remove_nowiki": "Tidak menampilkan tag <nowiki> pada hasilnya",
        "expand_templates_generate_xml": "Tampilkan pohon parser XML",
        "expand_templates_generate_rawhtml": "Tampilkan HTML mentah",
-       "expand_templates_preview": "Pratayang"
+       "expand_templates_preview": "Pratayang",
+       "pagelanguage": "Pemilih bahasa halaman",
+       "pagelang-name": "Halaman",
+       "pagelang-language": "Bahasa",
+       "pagelang-use-default": "Gunakan bahasa default",
+       "pagelang-select-lang": "Pilih bahasa",
+       "right-pagelang": "Ubah bahasa halaman",
+       "action-pagelang": "mengubah bahasa halaman",
+       "log-name-pagelang": "Ubah bahasa log",
+       "log-description-pagelang": "Ini adalah log perubahan dalam bahasa halaman.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|mengubah}} bahasa halaman $3 dari $4 menjadi $5."
 }
index 617110a..7bea4ef 100644 (file)
@@ -56,7 +56,7 @@
        "editfont-monospace": "Monospaced a kita ti letra",
        "editfont-sansserif": "Sans-serif a kita ti letra",
        "editfont-serif": "Serif a kita ti letra",
-       "sunday": "Dominggo",
+       "sunday": "Domingo",
        "monday": "Lunes",
        "tuesday": "Martes",
        "wednesday": "Mierkoles",
@@ -66,9 +66,9 @@
        "sun": "Dom",
        "mon": "Lun",
        "tue": "Mar",
-       "wed": "Mie",
+       "wed": "Mier",
        "thu": "Hue",
-       "fri": "Bie",
+       "fri": "Bier",
        "sat": "Sab",
        "january": "Enero",
        "february": "Pebrero",
        "talkpagelinktext": "Tungtungan",
        "specialpage": "Espesial a panid",
        "personaltools": "Bukod a ram-ramit",
-       "postcomment": "Baro a paset",
        "articlepage": "Kitaen ti naglaon a panid",
        "talk": "Pagtungtungan",
        "views": "Dagiti pangkitaan",
        "externaldberror": "Adda biddut idi ti panakapasingked ti database wenno saanmo a mabalin ti agpabaro ti bukodmo nga akin-ruar a pakabilangan.",
        "login": "Sumrek",
        "nav-login-createaccount": "Sumrek / agaramid ti pakabilangan",
-       "loginprompt": "Nasken a napakabaelam dagiti \"galietas\" tapno makastrekka iti {{SITENAME}}.",
        "userlogin": "Sumrek / agaramid ti pakabilangan",
        "userloginnocreate": "Sumrek",
        "logout": "Rummuar",
        "mergehistory-empty": "Awan dagiti mabalin nga itipon ti panagbalbaliw.",
        "mergehistory-success": "$3 {{PLURAL:$3|a binaliwan|dagiti binaliwan}} ti [[:$1]] balligi ti panagitipon idiay [[:$2]].",
        "mergehistory-fail": "Saan a nakaaramid ti panagtipon ti pakasaritaan, pangngaasi ta kitaen ti panid ken parametro ti oras.",
+       "mergehistory-fail-toobig": "Di naaramid ti panagtipon ti pakasaritaan gapu ta ad-adu ti patingga ti $1 {{PLURAL:$1|a rebision|kadagiti rebision}} ti maiyalisto.",
        "mergehistory-no-source": "Awan ti taudan ti panid a $1.",
        "mergehistory-no-destination": "Awan ti papanan ti panid a $1.",
        "mergehistory-invalid-source": "Masapul nga adda ti umisu a titulo ti taudan ti panid.",
        "largefileserver": "Daytoy a papeles ket dakdakel ngem ti naaramid a mabalin para iti server.",
        "emptyfile": "Ti papeles nga ipanmo ket kasla awan ti nagyan na.\nBaka daytoy ket gapu ti kamali ti inkabil a nagan ti papeles.\nPangngaasi ta kitaem no kayatmo latta nga ipapan daytoy a papeles.",
        "windows-nonascii-filename": "Daytoy a wiki ket saanna a suportaran dagiti nagan ti papeles nga addaan kadagiti espesial a karakter.",
-       "fileexists": "Ti papeles nga agnagan ti kastoy ket addan, pangngaasi a kitaem ti <strong>[[:$1]]</strong> no saanka a sigurado no kayatmo a sukatan.\n[[$1|thumb]]",
+       "fileexists": "Ti papeles nga agnagan ti kastoy ket addan, pangngaasi a kitaem ti <strong>[[:$1]]</strong> no {{GENDER:|saanka}} a sigurado no kayatmo a sukatan.\n[[$1|thumb]]",
        "filepageexists": "Ti panangipalpalawag a panid para iti daytoy a papeles ket naaramiden idiay <strong>[[:$1]]</strong>, ngem awan ti agdama nga agnagan ti kastoy a papeles.\nTi pakabuklan nga inkabilmo ket saan nga agparang idiay deskripsion ti panid.\nTapno agparang ti pakabuklan idiay, masapul a manual a baliwam.\n[[$1|thumb]]",
-       "fileexists-extension": "Adda papeles nga agnagan ti kastoy: [[$2|thumb]]\n* Nagan ti naipapan a papeles: <strong>[[:$1]]</strong>\n* Nagan ti adda a papeles: <strong>[[:$2]]</strong>\nPangngaasi nga agpili ti sabali a nagan.",
+       "fileexists-extension": "Adda papeles nga agnagan ti kastoy: [[$2|thumb]]\n* Nagan ti naipapan a papeles: <strong>[[:$1]]</strong>\n* Nagan ti adda a papeles: <strong>[[:$2]]</strong>\nKayatmo kadi ti agusar ti naisangsangayan a nagan?",
        "fileexists-thumbnail-yes": "Daytoy a papeles ket kasla ladawan a napabassit ''(thumbnail)''.\n[[$1|thumb]]\nPangngaasi a kitaem ti papeles a <strong>[[:$1]]</strong>.\nNo ti nakitam a papeles ket isu ti ladawan iti dati a kadakkel saanen a nasken ti agipan ti maysa a napabassit a ladawan.",
        "file-thumbnail-no": "Ti nagan ti papeles ket mangrugi iti <strong>$1</strong>.\nKasla ladawan a napabassit ''(thumbnail)''.\nNo addaanka ti napno a resolusion ipanmo daytoy, no saan pangngaasi a sukatam ti nagan ti papeles.",
        "fileexists-forbidden": "Daytoy a nagan ti papeles ket adda dita, ken saan a mabalin a masuratan manen.\nNo kayatmo pay latta nga ipan ti papeles, pangngaasi nga agsublika ken usarem ti baro a nagan.\n[[File:$1|thumb|center|$1]]",
        "license-nopreview": "(Saan a mabalin nga ipadas)",
        "upload_source_url": " (maysa nga umisu, ken maserrekan ti publiko nga URL)",
        "upload_source_file": "(papeles iti kompiutermo)",
+       "listfiles-delete": "ikkaten",
        "listfiles-summary": "Daytoy nga espesial a panid ket agiparang kadagiti amin a naipan a papeles.",
        "listfiles_search_for": "Agsapul para iti nagan ti midia:",
        "imgfile": "papeles",
        "filedelete-maintenance": "Ti panagikkat ken panagisubli kadagiti papaeles ket nabaldado iti las-ud ti panagtartaripato.",
        "filedelete-maintenance-title": "Saan a maikkat daytoy a papeles",
        "mimesearch": "Pagbiruk ti MIME",
-       "mimesearch-summary": "Daytoy a panid ket pakabaelanna ti panagsagat ti papeles iti MIME a kitada.\nIkabil: kita ti nagyan/subtipo, a kas ti <code>image/jpeg</code>.",
+       "mimesearch-summary": "Daytoy a panid ket pakabaelanna ti panagsagat ti papeles iti MIME a kitada.\nIkabil: kita ti nagyan/subtipo wenno kita ti linaon/*, a kas ti <code>image/jpeg</code>.",
        "mimetype": "Kita ti MIME:",
        "download": "ikarga",
        "unwatchedpages": "Di mabambantayan a pampanid",
        "wantedpages-badtitle": "Saan nga umisu a titulo idiay naikabil a pagbanagan: $1",
        "wantedfiles": "Dagiti makiddaw a papeles",
        "wantedfiletext-cat": "Dagiti sumaganad a papeles ket maus-usar ngem awanda met. Dagiti papeles a naggapu kadagiti ganganaet a repositorio ket mailista uray pay no addaan da. No adda dagiti kasla adda dagitoy ket <del>maikkat</del> to. A maipanayon pay, dagiti pampanid nga agisengngat kadagiti papeles nga awan ket nailista idiay [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Dagiti sumaganad a papeles ket naus-usar ngem awanda met. Iti pay maipatinayon, dagiti panid a mangipenpen kadagiti papeles ket nailista idiay [[:$1]].",
        "wantedfiletext-nocat": "Dagiti sumaganad a papeles ket maus-usar ngem awanda met. Dagiti papeles a naggapu kadagiti ganganaet a repositorio ket mailista uray pay no addaan da. No adda dagiti kasla adda dagitoy ket <del>maikkat</del> to.",
+       "wantedfiletext-nocat-noforeign": "Dagiti sumaganad a papeles ket naus-usar ngem awanda met",
        "wantedtemplates": "Dagiti makiddaw a plantilia",
        "mostlinked": "Dagiti panid a kaaduan iti nakasilpo",
        "mostlinkedcategories": "Dagiti kategoria a kaaduan iti nakasilpo",
-       "mostlinkedtemplates": "Dagiti plantilia a kaaduan iti nakasilpo",
+       "mostlinkedtemplates": "Kaaduan a nailak-am a pampanid",
        "mostcategories": "Dagiti panid a kaaduan kadagiti kategoria",
        "mostimages": "Dagiti papeles a kaaduan iti nakasilpo",
        "mostinterwikis": "Dagiti panid a kaaduan kadagiti interwiki",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|tungtungan]])",
        "unknown_extension_tag": "Di ammo a pagpaatiddog nga etiketa \"$1\"",
        "duplicate-defaultsort": "'''Ballaag:''' Kinasigud a panagilasin ti \"$2\" ket sukatanna ti immuna a kinasigud a panagilasin \"$1\".",
+       "duplicate-displaytitle": "<strong>Ballaag:</strong> Ti maiparang a titulo ti \"$2\" ket tuonanna ti immmuna a maiparang a titulo ti \"$1\".",
        "version": "Bersion",
        "version-extensions": "Dagiti naisaad a pagpaatiddog",
+       "version-skins": "Naisaad a kudkudil",
        "version-specialpages": "Espesial a pampanid",
        "version-parserhooks": "Dagiti parser a kawit",
        "version-variables": "Nadumaduma a kita",
        "version-antispam": "Pawilan ti spam",
-       "version-skins": "Dagiti Kudil",
        "version-other": "Sabali",
        "version-mediahandlers": "Agtengtengngel kadagiti midia",
        "version-hooks": "Dagiti kawit",
        "version-hook-name": "Nagan ti kawit",
        "version-hook-subscribedby": "Umanamong babaen ti",
        "version-version": "($1)",
+       "version-no-ext-name": "[awan nagan]",
        "version-license": "Lisensia ti MediaWiki",
        "version-ext-license": "Lisensia",
        "version-ext-colheader-name": "Pagpaatiddog",
+       "version-skin-colheader-name": "Kudil",
        "version-ext-colheader-version": "Bersion",
        "version-ext-colheader-license": "Lisensia",
        "version-ext-colheader-description": "Deskripsion",
        "expand_templates_remove_nowiki": "Parmeken dagiti <nowiki> nga etiketa kadagiti nagbanagan",
        "expand_templates_generate_xml": "Iparang ti XML parse a kayo",
        "expand_templates_generate_rawhtml": "Ipakita ti naata a HTML",
-       "expand_templates_preview": "Pamadasan"
+       "expand_templates_preview": "Pamadasan",
+       "pagelanguage": "Pagpilian ti pagsasao ti panid",
+       "pagelang-name": "Panid",
+       "pagelang-language": "Pagsasao",
+       "pagelang-use-default": "Usaren ti kasisigud a pagsasao",
+       "pagelang-select-lang": "Agpili iti pagsasao",
+       "right-pagelang": "Baliwan ti pagsasao ti panid",
+       "action-pagelang": "baliwan ti pagsasao ti panid",
+       "log-name-pagelang": "Baliwan ti pagsasao ti listaan",
+       "log-description-pagelang": "Daytoy ket listaan dagiti binaliwan kadagiti pagsasao ti panid.",
+       "logentry-pagelang-pagelang": "NI $1 ket {{GENDER:$2|binaliwanna}} ti pagsasao ti panid para iti $3 manipud ti $4 iti $5."
 }
index 2837738..68d3030 100644 (file)
        "talkpagelinktext": "Spjall",
        "specialpage": "Kerfissíða",
        "personaltools": "Tenglar",
-       "postcomment": "Nýr hluti",
        "articlepage": "Sýna núverandi síðu",
        "talk": "Spjall",
        "views": "Sýn",
        "externaldberror": "Uppfærsla mistókst. Annaðhvort varð villa í gagnasafninu eða að þér sé óheimilt að uppfæra aðra aðganga.",
        "login": "Innskrá",
        "nav-login-createaccount": "Innskrá / Búa til aðgang",
-       "loginprompt": "Þú verður að leyfa vefkökur til þess að geta skráð þig inn á {{SITENAME}}.",
        "userlogin": "Innskrá / Búa til aðgang",
        "userloginnocreate": "Innskrá",
        "logout": "Útskráning",
index 4a8673a..dd285a5 100644 (file)
        "talkpagelinktext": "Discussione",
        "specialpage": "Pagina speciale",
        "personaltools": "Strumenti personali",
-       "postcomment": "Nuova sezione",
        "articlepage": "Visualizza la voce",
        "talk": "Discussione",
        "views": "Visite",
        "externaldberror": "Si è verificato un errore con il server di autenticazione esterno, oppure non si dispone delle autorizzazioni necessarie per aggiornare il proprio accesso esterno.",
        "login": "Entra",
        "nav-login-createaccount": "Entra / registrati",
-       "loginprompt": "Per accedere a {{SITENAME}} è necessario abilitare i cookie.",
        "userlogin": "Entra / registrati",
        "userloginnocreate": "Entra",
        "logout": "Esci",
        "license": "Licenza:",
        "license-header": "Licenza",
        "nolicense": "Nessuna licenza indicata",
+       "licenses-edit": "Modifica opzioni di licenza",
        "license-nopreview": "(Anteprima non disponibile)",
        "upload_source_url": " (una URL corretta e accessibile)",
        "upload_source_file": " (un file sul proprio computer)",
+       "listfiles-delete": "cancella",
        "listfiles-summary": "Questa pagina speciale mostra tutti i file caricati.",
        "listfiles_search_for": "Ricerca immagini per nome:",
        "imgfile": "file",
        "wantedpages": "Pagine più richieste",
        "wantedpages-badtitle": "Titolo non valido nel gruppo di risultati: $1",
        "wantedfiles": "File richiesti",
-       "wantedfiletext-cat": "I seguenti file sono richiamati da wikilink, ma non esistono. I file ospitati su repository esterni potrebbero essere elencati anche se di fatto esistenti. Questi falsi positivi saranno <del>barrati</del>. Le pagine che incorporano i file che non esistono sono elencate in [[:$1]].",
+       "wantedfiletext-cat": "I seguenti file sono utilizzati, ma non esistono. I file ospitati su repository esterni potrebbero essere elencati anche se di fatto esistenti. Questi falsi positivi saranno <del>barrati</del>. Le pagine che incorporano i file che non esistono sono elencate in [[:$1]].",
+       "wantedfiletext-cat-noforeign": "I seguenti file sono utilizzati, ma non esistono. Inoltre, le pagine che incorporano questi file sono elencate nella [[:$1]].",
        "wantedfiletext-nocat": "I seguenti file sono richiamati da wikilink, ma non esistono. I file ospitati su repository esterni potrebbero essere elencati anche se di fatto esistenti. Questi falsi positivi saranno <del>barrati</del>.",
+       "wantedfiletext-nocat-noforeign": "I seguenti file sono utilizzati, ma non esistono.",
        "wantedtemplates": "Template richiesti",
        "mostlinked": "Pagine più richiamate",
        "mostlinkedcategories": "Categorie più richiamate",
-       "mostlinkedtemplates": "Pagine più inlcuse",
+       "mostlinkedtemplates": "Pagine più incluse",
        "mostcategories": "Pagine con più categorie",
        "mostimages": "File più richiamati",
        "mostinterwikis": "Pagine con più interwiki",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussioni]])",
        "unknown_extension_tag": "Tag estensione sconosciuto: \"$1\"",
        "duplicate-defaultsort": "Attenzione: la chiave di ordinamento predefinita \"$2\" sostituisce la precedente \"$1\".",
+       "duplicate-displaytitle": "<strong>Attenzione:</strong> il titolo visualizzato \"$2\" sostituisce il precedente titolo \"$1\".",
        "version": "Versione",
        "version-extensions": "Estensioni installate",
        "version-skins": "Skin installate",
index 4457755..1a563c5 100644 (file)
        "newwindow": "(新しいウィンドウで開きます)",
        "cancel": "中止",
        "moredotdotdot": "続き...",
-       "morenotlisted": "ã\81\93ã\81®ä¸\80覧ã\81®ç¶\9aã\81\8d",
+       "morenotlisted": "ã\81\93ã\81®ä¸\80覧ã\81¯å®\8cå\85¨ã\81§ã\81¯ã\81\82ã\82\8aã\81¾ã\81\9bã\82\93ã\80\82",
        "mypage": "ページ",
        "mytalk": "トーク",
        "anontalk": "このIPアドレスのトーク",
        "talkpagelinktext": "トーク",
        "specialpage": "特別ページ",
        "personaltools": "個人用ツール",
-       "postcomment": "新しい節",
        "articlepage": "本文を表示",
        "talk": "議論",
        "views": "表示",
        "externaldberror": "認証データベースでエラーが発生した、または外部アカウントの更新が許可されていません。",
        "login": "ログイン",
        "nav-login-createaccount": "ログインまたはアカウント作成",
-       "loginprompt": "{{SITENAME}}にログインするにはCookieを有効にする必要があります。",
        "userlogin": "ログインまたはアカウント作成",
        "userloginnocreate": "ログイン",
        "logout": "ログアウト",
        "license-nopreview": "(プレビューはありません)",
        "upload_source_url": "(有効かつ一般に公開されている URL)",
        "upload_source_file": "(あなたのコンピューター上のファイル)",
+       "listfiles-delete": "削除",
        "listfiles-summary": "この特別ページでは、アップロードされたファイルをすべて表示します。",
        "listfiles_search_for": "検索するメディア名:",
        "imgfile": "ファイル",
        "wantedpages-badtitle": "結果が、無効なページ名を含んでいます: $1",
        "wantedfiles": "ファイル情報ページが存在しないファイル",
        "wantedfiletext-cat": "以下のファイルは使用されていますが存在しません。外部リポジトリ由来のファイルは、存在していてもここに列挙される場合があります。その場合は<del>取り消し線</del>が付きます。さらに、存在しないファイルを埋め込んでいるページは[[:$1]]に列挙されます。",
+       "wantedfiletext-cat-noforeign": "以下のファイルは使用されていますが存在しません。さらに、存在しないファイルを埋め込んでいるページは[[:$1]]に列挙されます。",
        "wantedfiletext-nocat": "以下のファイルは使用されていますが存在しません。外部リポジトリ由来のファイルは、存在していてもここに列挙される場合があります。その場合は<del>取り消し線</del>が付きます。",
+       "wantedfiletext-nocat-noforeign": "以下のファイルは使用されていますが存在しません。",
        "wantedtemplates": "呼び出し先が存在しないテンプレート呼び出し",
        "mostlinked": "被リンク数の多いページ",
        "mostlinkedcategories": "被リンク数の多いカテゴリ",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|トーク]])",
        "unknown_extension_tag": "不明な拡張機能タグ「$1」です",
        "duplicate-defaultsort": "<strong>警告:</strong> 既定のソートキー「$2」が、その前に書かれている既定のソートキー「$1」を上書きしています。",
+       "duplicate-displaytitle": "<strong>警告:</strong> 既定のDISPLAYTITLE「$2」が、その前に書かれている既定のDISPLAYTITLE「$1」を上書きしています。",
        "version": "バージョン情報",
        "version-extensions": "インストール済み拡張機能",
        "version-skins": "インストール済み外装",
index c5d401d..7006a1e 100644 (file)
        "talkpagelinktext": "Hurênayis",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê keşi",
-       "postcomment": "Qısımo newe",
        "articlepage": "Pela zerreki bıvêne",
        "talk": "Hurênais",
        "views": "Asaişi",
        "externaldberror": "Cıfeteliyaisê naskerdene de ya xeta esta ya ki tebera vırastena hesabê sıma rê destur çino.",
        "login": "Cı kuye",
        "nav-login-createaccount": "Cı kuye / hesab vıraze",
-       "loginprompt": "Cıkotena {{SITENAME}} rê gunê ''cookies'' akerdey bê.",
        "userlogin": "Cı kuye / hesab vıraze",
        "userloginnocreate": "Cı kuye",
        "logout": "Veciye",
        "mergehistory-into": "Pela hedefi:",
        "mergehistory-reason": "Sebeb:",
        "revertmerge": "Cia ke",
-       "history-title": "Rewizyonê $1:",
+       "history-title": "Tarixê çımraviyarnayişê \"$1\"",
        "lineno": "Rêza $1i:",
        "compareselectedversions": "Varyantunê weçinıtun têver sane",
        "editundo": "peyser bia",
index f962fa6..1bb1d5d 100644 (file)
        "talkpagelinktext": "Талқылауы",
        "specialpage": "Арнайы бет",
        "personaltools": "Жеке құралдар",
-       "postcomment": "Жаңа бөлім",
        "articlepage": "Мәлімет бетін қарау",
        "talk": "Талқылау",
        "views": "Көрініс",
        "externaldberror": "Осы арада не шеттік растау дерекқорында қате болды, немесе шеттік тіркелгіңізді жаңалау рұқсаты жоқ.",
        "login": "Кіру",
        "nav-login-createaccount": "Кіру / Тіркелу",
-       "loginprompt": "{{SITENAME}} торабына кіруіңіз үшін «cookies» қосылуы керек.",
        "userlogin": "Кіру / Тіркелу",
        "userloginnocreate": "Кіру",
        "logout": "Шығу",
index 159b32b..996ae86 100644 (file)
        "talkpagelinktext": "토론",
        "specialpage": "특수 문서",
        "personaltools": "개인 도구",
-       "postcomment": "새 문단",
        "articlepage": "문서 보기",
        "talk": "토론",
        "views": "보기",
        "externaldberror": "바깥 인증 데이터베이스에 오류가 있거나 바깥 계정을 새로 고칠 권한이 없습니다.",
        "login": "로그인",
        "nav-login-createaccount": "로그인 / 계정 만들기",
-       "loginprompt": "{{SITENAME}}에 로그인하려면 쿠키를 사용할 수 있어야 합니다.",
        "userlogin": "로그인 / 계정 만들기",
        "userloginnocreate": "로그인",
        "logout": "로그아웃",
        "nologin": "계정이 없나요? $1.",
        "nologinlink": "계정을 만드세요",
        "createaccount": "계정 만들기",
-       "gotaccount": "계정이 이미 있다면, $1.",
+       "gotaccount": "계정이 이미 있습니까? $1.",
        "gotaccountlink": "로그인하세요",
        "userlogin-resetlink": "로그인 정보를 잊으셨나요?",
        "userlogin-resetpassword-link": "비밀번호를 잊으셨나요?",
        "userlogin-helplink2": "로그인에 대한 도움말",
-       "userlogin-loggedin": "이미 $1로 로그인되어 있습니다. 아래의 양식을 사용하여 다른 계정으로 로그인하세요.",
+       "userlogin-loggedin": "이미 {{GENDER:$1|$1}} 사용자로 로그인되어 있습니다.\n다른 사용자로 로그인하려면 아래의 양식을 사용하세요.",
        "userlogin-createanother": "다른 계정 만들기",
        "createacct-emailrequired": "이메일 주소",
        "createacct-emailoptional": "이메일 주소 (선택 사항)",
        "prefs-dateformat": "날짜 형식",
        "prefs-timeoffset": "시차 설정",
        "prefs-advancedediting": "일반 설정",
-       "prefs-editor": "편집",
+       "prefs-editor": "편집",
        "prefs-preview": "미리 보기",
        "prefs-advancedrc": "고급 설정",
        "prefs-advancedrendering": "고급 설정",
        "rcshowhidemine-show": "보이기",
        "rcshowhidemine-hide": "숨기기",
        "rclinks": "최근 $2일간의 $1개 바뀐 문서 보기<br />$3",
-       "diff": "비교",
+       "diff": "차이",
        "hist": "역사",
        "hide": "숨기기",
        "show": "보이기",
        "license-nopreview": "(미리 보기 불가능)",
        "upload_source_url": "(올바르고, 공개적으로 접근할 수 있는 URL)",
        "upload_source_file": " (당신의 컴퓨터에 있는 파일)",
+       "listfiles-delete": "삭제",
        "listfiles-summary": "이 특수 문서는 모든 올려진 파일을 보여줍니다.",
        "listfiles_search_for": "다음 미디어 이름 검색:",
        "imgfile": "파일",
        "wantedpages-badtitle": "문서 제목이 잘못되었습니다: $1",
        "wantedfiles": "필요한 파일 목록",
        "wantedfiletext-cat": "다음 파일은 쓰이고는 있지만 없는 파일입니다. 바깥 저장소에 있는 파일은 실제로는 있지만 여기 올라 있을 수 있습니다. 그런 오류는 <del>삭제선</del>이 그어질 것입니다. 또한 없는 파일을 포함하고 있는 문서는 [[:$1]]에 올라 있습니다.",
-       "wantedfiletext-nocat": "다음 파일은 쓰이고는 있지만 없는 파일입니다. 바깥 저장소에 있는 파일은 실제로는 있지만 여기 올라 있을 수 있습니다. 그런 오류는 <del>삭제선</del>이 그어질 것입니다.",
+       "wantedfiletext-cat-noforeign": "다음 파일은 쓰이고 있지만 존재하지 않습니다. 또한, 존재하지 않는 파일이 포함된 문서가 [[:$1]]에 나열되어 있습니다.",
+       "wantedfiletext-nocat": "다음 파일은 쓰이고 있지만 존재하지 않습니다. 바깥 저장소에 있는 파일은 실제로는 있지만 여기 올라 있을 수 있습니다. 그런 오류는 <del>삭제선</del>이 그어질 것입니다.",
+       "wantedfiletext-nocat-noforeign": "다음 파일은 쓰이고 있지만 존재하지 않습니다.",
        "wantedtemplates": "필요한 틀 목록",
        "mostlinked": "가장 많이 연결된 문서 목록",
        "mostlinkedcategories": "가장 많이 연결된 분류 목록",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|토론]])",
        "unknown_extension_tag": "알 수 없는 확장 기능 태그 \"$1\"",
        "duplicate-defaultsort": "'''경고:''' 기본 정렬 키 \"$2\"가 이전의 기본 정렬 키 \"$1\"를 덮어쓰고 있습니다.",
+       "duplicate-displaytitle": "<strong>경고:</strong> \"$2\" 제목 표시는 기존의 표시되는 제목 \"$1\"을 덮어씁니다.",
        "version": "버전",
        "version-extensions": "설치된 확장 기능",
        "version-skins": "설치된 스킨",
        "expand_templates_remove_nowiki": "결과에서 <nowiki> 태그를 숨기기",
        "expand_templates_generate_xml": "XML 구문 트리 보기",
        "expand_templates_generate_rawhtml": "원본 HTML 보이기",
-       "expand_templates_preview": "미리 보기"
+       "expand_templates_preview": "미리 보기",
+       "pagelanguage": "문서 언어 선택기",
+       "pagelang-name": "문서",
+       "pagelang-language": "언어",
+       "pagelang-use-default": "기본 언어 사용",
+       "pagelang-select-lang": "언어 선택",
+       "right-pagelang": "문서 언어 바꾸기",
+       "action-pagelang": "문서 언어 바꾸기",
+       "log-name-pagelang": "언어 바꾸기 기록",
+       "log-description-pagelang": "문서 언어를 바꾼 기록입니다.",
+       "logentry-pagelang-pagelang": "$1 사용자가 $3의 문서 언어를 $4에서 $5로 {{GENDER:$2|바꾸었습니다}}."
 }
index db9af3d..c89dcd0 100644 (file)
        "talkpagelinktext": "Diskussioun",
        "specialpage": "Spezialsäit",
        "personaltools": "Perséinlech Tools",
-       "postcomment": "Neien Abschnitt",
        "articlepage": "Säit",
        "talk": "Diskussioun",
        "views": "Affichagen",
        "externaldberror": "Entweder ass e Feeler bei der externer Authentifizéierung geschitt, oder Dir däerft Ären externe Benotzerkont net aktualiséieren.",
        "login": "Aloggen",
        "nav-login-createaccount": "Aloggen / Benotzerkont uleeën",
-       "loginprompt": "Fir sech op {{SITENAME}} aloggen ze kënnen, mussen d'Cookien aktivéiert sinn.",
        "userlogin": "Aloggen / Benotzerkont uleeën",
        "userloginnocreate": "Umellen",
        "logout": "Ofmellen",
        "license-nopreview": "(Kucken ouni ofzespäichere geet net)",
        "upload_source_url": " (gëlteg, ëffentlech zougänglech URL)",
        "upload_source_file": " (e Fichier op Ärem Computer)",
+       "listfiles-delete": "läschen",
        "listfiles-summary": "Op dëser Spezialsäit stinn all déi eropgeluede Fichieren.",
        "listfiles_search_for": "Sicht nom Fichier:",
        "imgfile": "Fichier",
index ccaae87..9ea3ca3 100644 (file)
        "talkpagelinktext": "Dinika",
        "specialpage": "Pejy manokana",
        "personaltools": "Fitaovana manokana",
-       "postcomment": "Hametraka fanamarihana",
        "articlepage": "Hijery ny votoatin'ny pejy",
        "talk": "dinika",
        "views": "Fijerena",
        "externaldberror": "Nisy tsy fetezana angamba teo amin'ny fanamarinana anao tamin'ny sehatra ivelan'ity wiki ity, na tsy manana alalana hanova ny kaontinao ivelany ianao.",
        "login": "Midira",
        "nav-login-createaccount": "Ampidiro ny solonanarana",
-       "loginprompt": "\nMila manaiky cookies ianao raha te hiditra amin'ny {{SITENAME}}.",
        "userlogin": "Hiditra na hanokatra kaonty",
        "userloginnocreate": "hiditra",
        "logout": "Hiala",
        "undo-summary-username-hidden": "Namafa ny famerenana $1 nataom-pikambana afenina",
        "cantcreateaccounttitle": "Tsy afaka manokatra kaonty ianao.",
        "cantcreateaccount-text": "Voasakan'i [[User:$3|$3]] ny fanokafana kaonty avy amin'ity adiresy IP (<b>$1</b>)\n\n''$2'' ny antony.",
+       "cantcreateaccount-range-text": "Nosakanan'i [[User:$3|$3]] ny fanokafana kaonty avy amin'ny adiresy IP ao amin'ny elanelana '''$1''' izay ahitana ny adiresy IP-nao ('''$4''').",
        "viewpagelogs": "Hijery ny fanovan'ity pejy ity",
        "nohistory": "Tsy manana tantaram-panovana io pejy io.",
        "currentrev": "Votoatiny ankehitriny",
        "currentrev-asof": "Endrika tamin'ity $1 ity",
        "revisionasof": "Endrik'io pejy io tamin'ny $1",
-       "revision-info": "Endrika tamin'ny $1 nataon'i $2",
+       "revision-info": "Endrika tamin'ny $1 nataon'i {{GENDER:$6|$2}}$7",
        "previousrevision": "← Endrika tranainy kokoa",
        "nextrevision": "Endrika vaovao kokoa →",
        "currentrevisionlink": "Endrika farany indrindra",
        "right-move": "Manakisaka pejy",
        "right-move-subpages": "Manakisaka pejy miarak'amin'ny zana-pejiny",
        "right-move-rootuserpages": "Mamindra ny renipejin'ny mpikambana",
+       "right-move-categorypages": "Hanetsika ny pejin-tsokajy",
        "right-movefile": "Manova anarana rakitra",
        "right-suppressredirect": "Afaka tsy manometraka redirect avy amin'ny lohateny fiavina",
        "right-upload": "Mampidi-drakitra",
        "action-createpage": "hanao pejy",
        "action-createtalk": "hanao pejin-dresaka",
        "action-createaccount": "amboary io kaontim-pikambana io",
+       "action-history": "hijery ny tantaran'ity pejy ity",
        "action-minoredit": "Mariho ho kely ity fanovana ity",
        "action-move": "hamindra io pejy io",
        "action-move-subpages": "hamindra io pejy io sy ny zanapejiny",
        "action-move-rootuserpages": "hanolo anaran'ny pejin'ny mpikambana",
+       "action-move-categorypages": "hanetsika ny pejin-tsokajy",
        "action-movefile": "manova anaran'ny rakitra iray",
        "action-upload": "hampiditra io rakitra io",
        "action-reupload": "Hanolo io rakitra efa misy io",
        "license-nopreview": "(Tsy misy topi-maso)",
        "upload_source_url": " (URL misy ary azo vangian'ny daholobe)",
        "upload_source_file": " (rakitra eo amin'ny milinao)",
+       "listfiles-delete": "fafao",
        "listfiles-summary": "Ahitana ny rakitra rehetra nampidirina ity pejy manokana ity.",
        "listfiles_search_for": "Hitady anarana media :",
        "imgfile": "rakitra",
        "listfiles_size": "Habe",
        "listfiles_description": "Visavisa",
        "listfiles_count": "Version",
+       "listfiles-show-all": "Hampiditra ny versiona talohan'ny sary",
        "listfiles-latestversion": "Filaza ankehitriny",
        "listfiles-latestversion-yes": "Eny",
        "listfiles-latestversion-no": "Tsia",
index 8f27297..246cdcb 100644 (file)
        "talkpagelinktext": "Разговор",
        "specialpage": "Специјална страница",
        "personaltools": "Лични алатки",
-       "postcomment": "Ново заглавие",
        "articlepage": "Преглед на содржината",
        "talk": "Разговор",
        "views": "Посети",
        "externaldberror": "Настана грешка при надворешното најавување на базата или пак немате дозвола да ја подновите вашата надворешна сметка.",
        "login": "Најава",
        "nav-login-createaccount": "Најава / регистрација",
-       "loginprompt": "За да се најавите на {{SITENAME}} мора да користите колачиња.",
        "userlogin": "Најава / регистрација",
        "userloginnocreate": "Најава",
        "logout": "Одјава",
        "license": "Лиценцирање:",
        "license-header": "Лиценцирање",
        "nolicense": "Нема",
+       "licenses-edit": "Измени лиценцни можности",
        "license-nopreview": "(Прегледот не е достапен)",
        "upload_source_url": " (важечка, јавно достапна URL-адреса)",
        "upload_source_file": "(податотека на вашиот компјутер)",
+       "listfiles-delete": "избриши",
        "listfiles-summary": "Оваа специјална страница ги прикажува сите подигнати податотеки.",
        "listfiles_search_for": "Побарај име на податотека:",
        "imgfile": "податотека",
        "wantedpages-badtitle": "Невалиден наслов во резултатите: $1",
        "wantedfiles": "Потребни податотеки",
        "wantedfiletext-cat": "Следниве податотеки се користат, но не постојат. Податотеките од други складишта може да се наведени дури и ако постојат. Таквите ќе бидат <del>поништени</del> од списокот. Покрај ова, страниците што содржат податотеки кои не постојат се наведени на [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Следниве податотеки се користат, но не постојат. Покрај ова, страниците што ги содржат непостоечките податотеки се наведени во [[:$1]].",
        "wantedfiletext-nocat": "Следниве податотеки се користат, но не постојат. Податотеките од други складишта може да се наведени дури и ако постојат. Таквите ќе бидат <del>поништени</del> од списокот.",
+       "wantedfiletext-nocat-noforeign": "Следниве податотеки се користат, но не постојат.",
        "wantedtemplates": "Потребни шаблони",
        "mostlinked": "Најмногу врски до страници",
        "mostlinkedcategories": "Најмногу врски до категории",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|разговор]])",
        "unknown_extension_tag": "Непозната ознака на додатокот „$1“",
        "duplicate-defaultsort": "Предупредување: Основниот клуч за подредување „$2“ го поништува претходниот основен клуч за подредување „$1“.",
+       "duplicate-displaytitle": "<strong>Предупредување:</strong> Приказниот наслов „$2“ го заменува претходнито приказен наслов „$1“.",
        "version": "Верзија",
        "version-extensions": "Воспоставени додатоци",
        "version-skins": "Воспоставени рува",
index 56c62e6..6c146cf 100644 (file)
        "talkpagelinktext": "സംവാദം",
        "specialpage": "പ്രത്യേക താൾ",
        "personaltools": "സ്വകാര്യതാളുകൾ",
-       "postcomment": "അഭിപ്രായം ചേർക്കുക",
        "articlepage": "ലേഖനം കാണുക",
        "talk": "സംവാദം",
        "views": "ദർശനീയത",
        "externaldberror": "ഒന്നുകിൽ ഡേറ്റാബേസ് സാധൂകരണത്തിൽ പ്രശ്നം ഉണ്ടായിരുന്നു അല്ലെങ്കിൽ നവീകരിക്കുവാൻ താങ്കളുടെ ബാഹ്യ അംഗത്വം താങ്കളെ അനുവദിക്കുന്നില്ല.",
        "login": "പ്രവേശിക്കുക",
        "nav-login-createaccount": "പ്രവേശിക്കുക / അംഗത്വമെടുക്കുക",
-       "loginprompt": "{{SITENAME}} സംരംഭത്തിൽ ലോഗിൻ ചെയ്യാൻ താങ്കൾ കുക്കികൾ (Cookies) സജ്ജമാക്കിയിരിക്കണം.",
        "userlogin": "പ്രവേശിക്കുക / അംഗത്വമെടുക്കുക",
        "userloginnocreate": "പ്രവേശിക്കുക",
        "logout": "ലോഗൗട്ട്",
        "license-nopreview": "(പ്രിവ്യൂ ലഭ്യമല്ല)",
        "upload_source_url": "(സാധുവായ, ആർക്കും ഉപയോഗിക്കാവുന്ന യൂ.ആർ.എൽ.)",
        "upload_source_file": "(താങ്കളുടെ കമ്പ്യൂട്ടറിലുള്ള ഒരു പ്രമാണം)",
+       "listfiles-delete": "മായ്ക്കുക",
        "listfiles-summary": "അപ്‌ലോഡ് ചെയ്തിട്ടുള്ള എല്ലാ പ്രമാണങ്ങളും ഈ പ്രത്യേക താളിൽ കാണാവുന്നതാണ്.",
        "listfiles_search_for": "മീഡിയ പ്രമാണം തിരയുക:",
        "imgfile": "പ്രമാണം",
index d707821..48493dc 100644 (file)
        "talkpagelinktext": "Diskussjoni",
        "specialpage": "Paġna speċjali",
        "personaltools": "Għodda personali",
-       "postcomment": "Sezzjoni ġdida",
        "articlepage": "Ara l-artiklu",
        "talk": "Diskussjoni",
        "views": "Veduti",
        "namespaceprotected": "Inti m'għandhekx il-permess li timodifika paġni fin-''namespace'' '''$1''.",
        "customcssprotected": "M'għandekx il-permessi neċessarji sabiex timmodifika din il-paġna tas-CSS, minħabba li għandha tqegħid personali ta' utent ieħor.",
        "customjsprotected": "M'għandekx il-permessi neċessarji sabiex timmodifika din il-paġna tal-JavaScript, minħabba li għandha tqegħid personali ta' utent ieħor.",
+       "mycustomjsprotected": "Ma għandekx permess li teditja din il-paġna JavaScript.",
+       "myprivateinfoprotected": "Ma għandekx permess li teditja l-informazzjoni privata tiegħek.",
+       "mypreferencesprotected": "Ma għandekx permess li teditja l-preferenzi tiegħek.",
        "ns-specialprotected": "Il-paġni speċjali ma jistgħux jiġu mmodifikati.",
        "titleprotected": "Dan it-titlu ġie protett mill-ħolqien minn [[User:$1|$1]].\nIr-raġuni li ġiet mogħtija kienet ''$2''.",
+       "filereadonlyerror": "L-amministratur li sakkar offra din l-ispjegazzjoni: \"$3\".",
+       "invalidtitle-knownnamespace": "Titolu validu bin-namespace \"$2\" u t-test\"$3\"",
+       "invalidtitle-unknownnamespace": "Titolu validu b'numru tan-namespace mhux magħruf  $1 u t-test \"$2\"",
+       "exception-nologin": "Mhux qiegħed fil-kont",
+       "exception-nologin-text": "Jekk jogħġbok [[Special:Userlogin|idħol fil-kont tiegħek]] biex tkun tista' taċċessa din il-paġna jew din l-azzjoni.",
+       "exception-nologin-text-manual": "Jekk jogħġbok $1 sabiex tkuu tista' taċċessa din il-paġna jew din l-azzjoni.",
        "virus-badscanner": "Problema fil-konfigurazzjoni: antivirus mhux magħruf: ''$1''",
        "virus-scanfailed": "Tfittxija falliet (kodiċi $1)",
        "virus-unknownscanner": "antivirus mhux magħruf:",
-       "logouttext": "'''Bħalissa tinsab barra mill-kont tiegħek.'''\n\nTista' tkompli tuża' {{SITENAME}} bħala utent anonimu, jew tista' terġa <span class='plainlinks'>[$1 tidħol]</span> bħala l-istess utent jew wieħed differenti.\nKun af li ċerti paġni jistgħu jkomplu jidhru bħallikieku l-illogjar 'l barra mill-kont qatt ma seħħ, sakemm ma tħassarx il-cache tal-browser.",
+       "logouttext": "<strong>Bħalissa mhux qiegħed fil-kont tiegħek</strong>\n\nJista' jkun li xi paġni jibqgħu jidhru bħalli kieku qiegħed fil-kont, sakemm ma tneddafx il-cache tan-navigatur tiegħek.",
+       "welcomeuser": "Merħba, $1!",
+       "welcomecreation-msg": "Il-kont tiegħek inħoloq.\nJekk trid tista' tibdel il- [[Special:Preferences|preferenzi tas-]]{{SITENAME}}.",
        "yourname": "Isem l-utent:",
        "userlogin-yourname": "Isem l-utent",
        "userlogin-yourname-ph": "Daħħal isem l-utent tiegħek",
+       "createacct-another-username-ph": "Daħħal l-isem tiegħek ta' utent",
        "yourpassword": "Password:",
        "userlogin-yourpassword": "Password",
        "userlogin-yourpassword-ph": "Daħħal il-password tiegħek",
        "userlogin-remembermypassword": "Żommni fil-kont",
        "userlogin-signwithsecure": "Uża konnessjoni sigura",
        "yourdomainname": "Id-dominju tiegħek:",
+       "password-change-forbidden": "Fuq din il-wiki ma tistax tibdel il-kliem tad-dħul (passwords).",
        "externaldberror": "Kien hemm problema esterna ta' awtentiċitá jew m'għandhekx permess neċċessarju sabiex tagħmel aġġornamenti fuq l-aċċess estern.",
        "login": "Idħol",
        "nav-login-createaccount": "Idħol / Oħloq kont",
-       "loginprompt": "Irid ikollok il-cookies mixgħula biex tkun tista' tidħol fuq {{SITENAME}}.",
        "userlogin": "Idħol jew oħloq kont ġdid",
        "userloginnocreate": "Idħol",
        "logout": "Oħroġ",
        "gotaccount": "Diġa għandhek kont? '''$1'''.",
        "gotaccountlink": "Idħol",
        "userlogin-resetlink": "Insejt kif tidħol fil-kont tiegħek?",
-       "userlogin-resetpassword-link": "Irrisettja l-password",
+       "userlogin-resetpassword-link": "Insejt il-kelma tad-dħul (password)?",
+       "userlogin-helplink2": "Għajnuna biex tidħol fil-kont",
+       "userlogin-loggedin": "Diġà dħalt fil-kont bħala {{GENDER:$1|$1}}.\nUża l-formola t'hawn taħt biex tidħol bħala utent ieħor",
+       "userlogin-createanother": "Oħloq kont ieħor",
        "createacct-emailrequired": "Indirizz elettroniku",
        "createacct-emailoptional": "Indirizz elettroniku (mhux obbligatorju)",
        "createacct-email-ph": "Daħħal l-indirizz elettroniku tiegħek",
-       "createaccountmail": "Uża password każwali temporanja u ibgħatha fuq l-indirizz elettroniku mniżżel hawn taħt",
+       "createacct-another-email-ph": "Daħħal l-indirizz elettroniku",
+       "createaccountmail": "Uża kelma tad-dħul temporanja li tkun u ibgħatha lill-indirizz elettroniku speċifikat",
        "createacct-realname": "Isem proprju (fakultattiv)",
        "createaccountreason": "Raġuni:",
        "createacct-reason": "Raġuni",
        "createacct-captcha": "Kontroll tas-sigurtà",
        "createacct-imgcaptcha-ph": "Daħħal it-test li qed tara hawn fuq",
        "createacct-submit": "Oħloq il-kont",
+       "createacct-another-submit": "Oħloq kont ieħor",
        "createacct-benefit-heading": "{{SITENAME}} hi magħmula minn persuni bħalek.",
        "createacct-benefit-body1": "{{PLURAL:$1|modifika|modifiki}}",
-       "createacct-benefit-body2": "paġna",
+       "createacct-benefit-body2": "{{PLURAL:$1|paġna|paġni}}",
        "createacct-benefit-body3": "{{PLURAL:$1|kontributur|kontributuri}} riċenti",
        "badretype": "Il-passwords li daħħalt ma jaqblux.",
        "userexists": "L-isem l-utent li daħħalt diġà meħud. Jekk jogħġbok, agħżel isem differenti.",
        "passwordtooshort": "Il-password trid tkun mill-inqas {{PLURAL:$1|karattru|$1 karattri}} twila u differenti mill-isem tal-utent.",
        "password-name-match": "Il-password trid tkun differenti mill-isem tal-utent tiegħek.",
        "password-login-forbidden": "L-użu ta' dan l-isem tal-utent u l-password huwa projbit.",
-       "mailmypassword": "Ibgħatli password ġdida",
+       "mailmypassword": "Erġa' waqqaf kelma tad-dħul",
        "passwordremindertitle": "Password temporanju ġdid għal {{SITENAME}}",
        "passwordremindertext": "Xi ħadd (probabbilment int, mill-indirizz IP $1) għamel rikjesta għal password ġdida għal {{SITENAME}} ($4). Password temporanja għall-utent \"$2\" ġiet maħluqa u din hi \"$3\".\nHuwa opportun li inti tidħol issa u tbiddel immedjatament il-password tiegħek. Din il-password il-ġdida se tiskadi fi żmien {{PLURAL:$5|ġurnata|$5 ijiem}}.\n\nJekk xi ħadd ieħor għamel din ir-rikjesta jew jekk int ftakart il-password tiegħek u issa ma tridx tbiddilha, int tista' ma tagħtix każ dan il-messaġġ u tkompli tuża' l-password l-antika.",
        "noemail": "M'hemm l-ebda indirizz ta' posta elettronika għall-utent \"$1\".",
        "noemailcreate": "Huwa neċessarju li tipprovdi indirizz elettroniku validu",
        "passwordsent": "Il-password il-ġdida ntbagħtet fl-indirizz tal-posta elettronika ta' \"$1\".\nJekk jogħġbok, għamel aċċess wara li tasallek.",
        "blocked-mailpassword": "L-indirizz tal-IP tiegħek huwa bblokkjat u miżmum milli jwettaq modifiki. Għaldaqstant, mhuwiex possibli għalik li tuża l-funzjoni sabiex iġġib lura l-password, u dan sabiex ma jkunx hemm abbużi.",
-       "eauthentsent": "Intbagħat messaġġ ta' konferma b'permezz tal-posta elettronika lejn l-indirizz indikat.<br />\nQabel xi posta elettronika oħra tiġi mibgħuta fuq il-kont, trid qabel xejn tesegwixxi l-istruzzjonijiet kif inhuma indikati, sabiex tikkonferma li l-kont huwa tassew tiegħek.",
+       "eauthentsent": "Intbagħtetlek konferma b'permezz ta' messaġġ elettroniku fl-indirizz speċifikat.\nQabel ma tinbagħat xi posta elettronika oħra fuq il-kont, trid issegwi l-istruzzjonijiet indikati fil-messaġġ, sabiex tikkonferma li l-kont huwa tassew tiegħek.",
        "throttled-mailpassword": "Posta elettronika sabiex tfakrek il-password ġiet postjata, fl-aħħar {{PLURAL:$1|siegħa|$1 siegħat}}.\nSabiex jitnaqqas l-abbuż, waħda biss tista' tiġi postjata f'kull {{PLURAL:$1|siegħa|$1 siegħat}}.",
        "mailerror": "Problema bil-postar tal-messaġġ: $1",
        "acct_creation_throttle_hit": "L-utenti ta' din il-wiki li jużaw l-indirizz IP tiegħek ħolqu {{PLURAL:$1|kont|$1 kontijiet}} fl-aħħar ġurnata, li hu n-numru massimu permess f'dan il-perjodu ta' żmien.\nBħala riżultat, il-viżitaturi li jużaw dan l-IP ma jistgħux għall-mument, joħoloqu aktar kontijiet.",
index 0d09d74..17a3128 100644 (file)
        "talkpagelinktext": "Chiàcchiera",
        "specialpage": "Paggena speciàle",
        "personaltools": "Strumiente perzonale",
-       "postcomment": "Nova sezzione",
        "articlepage": "Vere a paggena e contenuto",
        "talk": "Chiàcchiera",
        "views": "Visite",
        "uploadedimage": "ha carecato \"[[$1]]\"",
        "license": "Licenze:",
        "license-header": "Licenza",
+       "licenses-edit": "Càgna opzziune 'e licenza",
        "listfiles_name": "Nomme",
        "file-anchor-link": "Fiùra",
        "filehist": "Cronologgia d\"o file",
index 2aea667..76f2580 100644 (file)
        "talkpagelinktext": "diskusjon",
        "specialpage": "Spesialside",
        "personaltools": "Personlige verktøy",
-       "postcomment": "Ny seksjon",
        "articlepage": "Vis innholdsside",
        "talk": "Diskusjon",
        "views": "Visninger",
        "externaldberror": "Det var en ekstern autentifiseringsfeil, eller du kan ikke oppdatere din eksterne konto.",
        "login": "Logg inn",
        "nav-login-createaccount": "Logg inn eller opprett en konto",
-       "loginprompt": "Du må ha slått på informasjonskapsler for å logge in på {{SITENAME}}.",
        "userlogin": "Logg inn eller opprett en konto",
        "userloginnocreate": "Logg inn",
        "logout": "Logg ut",
        "mergehistory-empty": "Ingen revisjoner kan flettes.",
        "mergehistory-success": "{{PLURAL:$3|Én revisjon|$3 revisjoner}} av [[:$1]] ble flettet til [[:$2]].",
        "mergehistory-fail": "Klarte ikke å utføre historikkfletting; sjekk siden og tidsparameterne igjen.",
+       "mergehistory-fail-toobig": "Det er ikke mulig å utføre historikk-fletting fordi flere enn tillatte $1 {{PLURAL:$1|revisjon|revisjoner}} ville blitt flyttet.",
        "mergehistory-no-source": "Kildesiden $1 finnes ikke.",
        "mergehistory-no-destination": "Målsiden $1 finnes ikke.",
        "mergehistory-invalid-source": "Kildesiden må ha en gyldig tittel.",
        "license-nopreview": "(Forhåndsvisning ikke tilgjengelig)",
        "upload_source_url": " (en gyldig, offentlig tilgjengelig adresse)",
        "upload_source_file": " (en fil på din datamaskin)",
+       "listfiles-delete": "slett",
        "listfiles-summary": "Denne spesialsiden viser alle opplastede filer.",
        "listfiles_search_for": "Søk etter filnavn:",
        "imgfile": "fil",
        "duplicate-defaultsort": "Advarsel: Standardsorteringen «$2» tar over for den tidligere sorteringen «$1».",
        "version": "Versjon",
        "version-extensions": "Installerte utvidelser",
-       "version-skins": "Drakter",
+       "version-skins": "Installerte drakter",
        "version-specialpages": "Spesialsider",
        "version-parserhooks": "Parsertillegg",
        "version-variables": "Variabler",
        "version-hook-name": "Navn",
        "version-hook-subscribedby": "Brukes av",
        "version-version": "(versjon $1)",
+       "version-no-ext-name": "[uten navn]",
        "version-license": "Lisens",
        "version-ext-license": "Lisens",
        "version-ext-colheader-name": "Utvidelse",
+       "version-skin-colheader-name": "Drakt",
        "version-ext-colheader-version": "Versjon",
        "version-ext-colheader-license": "Lisens",
        "version-ext-colheader-description": "Beskrivelse",
        "expand_templates_remove_nowiki": "Ikke vis <nowiki>-merkelapper i resultatet",
        "expand_templates_generate_xml": "Vis parsetre som XML",
        "expand_templates_generate_rawhtml": "Vis ubehandlet HTML",
-       "expand_templates_preview": "Forhåndsvisning"
+       "expand_templates_preview": "Forhåndsvisning",
+       "pagelanguage": "Valg av sidespråk",
+       "pagelang-name": "Side",
+       "pagelang-language": "Språk",
+       "pagelang-use-default": "Bruk standardspråk",
+       "pagelang-select-lang": "Velg språk",
+       "right-pagelang": "Endre sidespråk",
+       "action-pagelang": "endre sidespråket",
+       "log-name-pagelang": "Endre språklogg",
+       "log-description-pagelang": "Dette er en logg som viser endringer i sidespråk",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|endret}} sidespråk for $3 fra $4 til $5."
 }
index af295a3..0ba4a58 100644 (file)
        "talkpagelinktext": "Overleg",
        "specialpage": "Spesiale zied",
        "personaltools": "Persoonlike instellingen",
-       "postcomment": "Niej onderwarp",
        "articlepage": "Artikel",
        "talk": "Overleg",
        "views": "Weergaven",
        "externaldberror": "Der gung iets fout bie de externe authentisering, of je maggen je gebrukersprofiel niet bewarken.",
        "login": "Anmelden",
        "nav-login-createaccount": "Anmelden",
-       "loginprompt": "Je mutten scheumbestaanden (cookies) an hebben staon um an te kunnen melden bie {{SITENAME}}.",
        "userlogin": "Anmelden / inschrieven",
        "userloginnocreate": "Anmelden",
        "logout": "Aofmelden",
        "download": "binnenhaolen",
        "unwatchedpages": "Ziejen die niet evolgd wörden",
        "listredirects": "Lieste van deurverwiezingen",
+       "listduplicatedfiles": "Lieste mit bestaanden mit duplikaoten",
        "unusedtemplates": "Ongebruukten mallen",
        "unusedtemplatestext": "Hieronder staon alle ziejen in de naamruumte \"{{ns:template}}\" die nargens gebruukt wörden.\nVergeet niet de verwiezingen nao te kieken veurda'j de mal vortdoon.",
        "unusedtemplateswlh": "aandere verwiezingen",
        "listgrouprights-removegroup-self": "Kan {{PLURAL:$2|groep|groepen}} vortdoon van eigen gebruker: $1",
        "listgrouprights-addgroup-self-all": "Kan alle groepen bie de eigen gebruker doon",
        "listgrouprights-removegroup-self-all": "Kan alle groepen vortdoon van eigen gebruker",
+       "trackingcategories": "Volgkategorieën",
        "mailnologin": "Niet an-emeld.",
        "mailnologintext": "Je mutten [[Special:UserLogin|an-emeld]] ween en n geldig e-mailadres in \"[[Special:Preferences|mien veurkeuren]]\" invoeren um disse funksie te kunnen gebruken.",
        "emailuser": "n Bericht sturen",
index fe962ef..780e8dc 100644 (file)
@@ -57,7 +57,8 @@
                        "לערי ריינהארט",
                        "아라",
                        "Mar(c)",
-                       "Calak"
+                       "Calak",
+                       "Arg"
                ]
        },
        "tog-underline": "Koppelingen onderstrepen:",
        "talkpagelinktext": "Overleg",
        "specialpage": "Speciale pagina",
        "personaltools": "Persoonlijke instellingen",
-       "postcomment": "Nieuw kopje",
        "articlepage": "Pagina bekijken",
        "talk": "Overleg",
        "views": "Weergaven",
        "externaldberror": "Er is een fout opgetreden bij het aanmelden bij de database of u hebt geen toestemming uw externe account bij te werken.",
        "login": "Aanmelden",
        "nav-login-createaccount": "Aanmelden / registreren",
-       "loginprompt": "U moet cookies ingeschakeld hebben om u te kunnen aanmelden bij {{SITENAME}}.",
        "userlogin": "Aanmelden / registreren",
        "userloginnocreate": "Aanmelden",
        "logout": "Afmelden",
        "license-nopreview": "(Voorvertoning niet beschikbaar)",
        "upload_source_url": " (een geldige, publiek toegankelijke URL)",
        "upload_source_file": " (een bestand op uw computer)",
+       "listfiles-delete": "verwijderen",
        "listfiles-summary": "Op deze speciale pagina zijn alle toegevoegde bestanden te bekijken.",
        "listfiles_search_for": "Zoeken naar bestand:",
        "imgfile": "bestand",
index 40d1bee..b2e931e 100644 (file)
        "talkpagelinktext": "Diskusjon",
        "specialpage": "Spesialside",
        "personaltools": "Personlege verktøy",
-       "postcomment": "Ny bolk",
        "articlepage": "Vis innhaldsside",
        "talk": "Diskusjon",
        "views": "Visningar",
        "externaldberror": "Det var anten ein ekstern databasefeil i tilgjengekontrollen, eller du har ikkje løyve til å oppdatere den eksterne kontoen din.",
        "login": "Logg inn",
        "nav-login-createaccount": "Lag brukarkonto / logg inn",
-       "loginprompt": "Nettlesaren din må godta informasjonskapslar for at du skal kunna logge inn.",
        "userlogin": "Lag brukarkonto / logg inn",
        "userloginnocreate": "Logg inn",
        "logout": "Logg ut",
        "powersearch-togglelabel": "Hak av:",
        "powersearch-toggleall": "Alle",
        "powersearch-togglenone": "Ingen",
+       "powersearch-remember": "Hugs utvalet for framtidige søk",
        "search-external": "Eksternt søk",
        "searchdisabled": "Søkjefunksjonen på {{SITENAME}} er slått av akkurat no.\nI mellomtida kan du søkje gjennom Google.\nVer merksam på at registra deira kan vera utdaterte.",
        "search-error": "Det oppstod ein feil under søket: $1",
index 1d2cd01..800f321 100644 (file)
        "talkpagelinktext": "Discussion",
        "specialpage": "Pagina especiala",
        "personaltools": "Aisinas personalas",
-       "postcomment": "Seccion novèla",
        "articlepage": "Vejatz l'article",
        "talk": "Discussion",
        "views": "Afichatges",
        "externaldberror": "Siá una error s’es producha amb la banca de donadas d’autentificacion extèrna, siá sètz pas autorizat a metre a jorn vòstre compte extèrne.",
        "login": "Identificacion",
        "nav-login-createaccount": "Crear un compte o se connectar",
-       "loginprompt": "Vos cal activar los cookies per vos connectar a {{SITENAME}}.",
        "userlogin": "Crear un compte o se connectar",
        "userloginnocreate": "Connexion",
        "logout": "Se desconnectar",
        "currentrev": "Version actuala",
        "currentrev-asof": "Version actuala en data del $1",
        "revisionasof": "Version del $1",
-       "revision-info": "Version del $1 per $2",
+       "revision-info": "Version del $1 per {{GENDER:$6|$2}}$7",
        "previousrevision": "← Version precedenta",
        "nextrevision": "Version seguenta →",
        "currentrevisionlink": "vejatz la version correnta",
        "largefileserver": "La talha d'aqueste fichièr es superiora al maximum autorizat.",
        "emptyfile": "Lo fichièr que volètz importar sembla void. Aquò pòt èsser degut a una error dins lo nom del fichièr. Verificatz que desiratz vertadièrament copiar aqueste fichièr.",
        "windows-nonascii-filename": "Aqueste wiki supòrta pas los noms de fichièrs amb de caractèrs especials.",
-       "fileexists": "Un fichièr amb aqueste nom existís ja.\nMercé de verificar <strong>[[:$1]]</strong>.\nSètz segur de voler modificar aqueste fichièr ? [[$1|thumb]]",
+       "fileexists": "Un fichièr amb aqueste nom existís ja.\nMercé de verificar <strong>[[:$1]]</strong>\nse sètz pas segur{{GENDER:||a|}} que o volètz remplaçar. [[$1|thumb]]",
        "filepageexists": "La pagina de descripcion per aqueste fichièr ja es estada creada aicí <strong>[[:$1]]</strong>, mas cap de fichièr existís pas actualament jos aqueste nom.\nLo resumit qu'anatz especificar apareisserà pas sus la pagina de descripcion.\nPer o far, vos caldrà modificar la pagina manualament. [[$1|vinheta]]",
-       "fileexists-extension": "Un fichièr amb un nom pròchi existís ja : [[$2|thumb]]\n* Nom del fichièr d'importar : <strong>[[:$1]]</strong>\n* Nom del fichièr existent : <strong>[[:$2]]</strong>\nCausissètz-ne un autre.",
+       "fileexists-extension": "Un fichièr amb un nom pròchi existís ja : [[$2|thumb]]\n* Nom del fichièr d'importar : <strong>[[:$1]]</strong>\n* Nom del fichièr existent : <strong>[[:$2]]</strong>\nBenlèu que podètz utilizar un nom mai explicit ?",
        "fileexists-thumbnail-yes": "Lo fichièr sembla èsser un imatge en talha reducha ''(thumbnail)''. [[$1|thumb]]\nVerificatz lo fichièr <strong>[[:$1]]</strong>.\nSe lo fichièr verificat es lo meteis imatge (dins una resolucion melhora), es pas de besonh d’importar una version reducha.",
        "file-thumbnail-no": "Lo nom del fichièr comença per <strong>$1</strong>.\nEs possible que s’agisca d’una version reducha ''(miniatura)''.\nSe dispausatz del fichièr en resolucion nauta, importatz-lo, si que non cambiatz lo nom del fichièr.",
        "fileexists-forbidden": "Un fichièr amb aqueste nom existís ja e pòt pas èsser espotit.\nSe volètz totjorn importar aquel fichièr, mercé de tornar en arrièr e d'utilizar un nom novèl. [[File:$1|thumb|center|$1]]",
        "filedelete-maintenance": "La supression e lo restabliment de fichièrs es temporàriament desactivada pendent la mantenença.",
        "filedelete-maintenance-title": "Impossible de suprimir lo fichièr",
        "mimesearch": "Recèrca per tipe MIME",
-       "mimesearch-summary": "Aquesta pagina especiala permet de cercar de fichièrs en foncion de lor tipe MIME. Entrada : tipe/sostipe, per exemple <code>image/jpeg</code>.",
+       "mimesearch-summary": "Aquesta pagina vos permet de filtrar los fichièrs en foncion de lor tipe MIME. Entrada : tipe_de_contengut/sostipe o tipe_de_contengut/*, per exemple <code>image/jpeg</code>.",
        "mimetype": "Tipe MIME :",
        "download": "telecargament",
        "unwatchedpages": "Paginas pas seguidas",
        "nmemberschanged": "$1 → $2 {{PLURAL:$2|membre|membres}}",
        "nrevisions": "$1 {{PLURAL:$1|revision|revisions}}",
        "nviews": "$1 {{PLURAL:$1|consultacion|consultacions}}",
-       "nimagelinks": "Utilisat sus $1 {{PLURAL:$1|pagina|paginas}}",
-       "ntransclusions": "Utilisat sus $1 {{PLURAL:$1|pagina|paginas}}",
+       "nimagelinks": "Utilizat sus $1 {{PLURAL:$1|pagina|paginas}}",
+       "ntransclusions": "Utilizat sus $1 {{PLURAL:$1|pagina|paginas}}",
        "specialpage-empty": "Aquesta pagina es voida.",
        "lonelypages": "Paginas orfanèlas",
        "lonelypagestext": "Las paginas seguentas son pas ligadas o enclusas a partir d’autras paginas de {{SITENAME}}.",
        "wantedtemplates": "Modèls demandats",
        "mostlinked": "Paginas mai ligadas",
        "mostlinkedcategories": "Categorias mai utilizadas",
-       "mostlinkedtemplates": "Modèls mai utilizats",
+       "mostlinkedtemplates": "Paginas las mai inclusas",
        "mostcategories": "Articles utilizant mai de categorias",
        "mostimages": "Fichièrs mai utilizats",
        "mostinterwikis": "Paginas amb lo mai d'interwikis",
        "nowatchlist": "Vòstra lista de seguiment conten pas cap d'article.",
        "watchlistanontext": "Per poder afichar o editar los elements de vòstra lista de seguiment, vos cal vos $1.",
        "watchnologin": "Vos sètz pas identificat(ada)",
-       "addwatch": "Ajustar a la lista de seguiment",
+       "addwatch": "Apondre a la lista de seguiment",
        "addedwatchtext": "La pagina « [[:$1]] » es estada aponduda a vòstra [[Special:Watchlist|lista de seguiment]]. Las modificacions venentas d'aquesta pagina e de la pagina de discussion associada i seràn repertoriadas.",
        "removewatch": "Suprimir de la lista de seguiment",
        "removedwatchtext": "La pagina « [[:$1]] » es estada levada de vòstra [[Special:Watchlist|lista de seguiment]].",
        "revertpage": "Anullacion de las modificacions de [[Special:Contributions/$2|$2]] ([[User talk:$2|Discussion]]) cap a la darrièra version de [[User:$1|$1]]",
        "revertpage-nouser": "Revocacion de las modificacions per un utilizaire amagat a la darrièra version per {{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Anullacion de las modificacions de $1 ; retorn a la version de $2.",
-       "sessionfailure-title": "La session capitèt mal",
+       "sessionfailure-title": "La sesilha a fracassat",
        "sessionfailure": "Vòstra sesilha de connexion sembla aver de problèmas ;\naquesta accion es estada anullada en prevencion d’un piratatge de sesilha.\nClicatz sus « Precedent » e tornatz cargar la pagina d’ont venètz, puèi ensajatz tornarmai.",
        "protectlogpage": "Istoric de las proteccions",
        "protectlogtext": "Aquí una lista de las modificacions de las proteccions de paginas.\nConsultatz la [[Special:ProtectedPages|lista de las paginas protegidas]] per la lista de las proteccions actualament operacionalas.",
        "duplicate-defaultsort": "Atencion : La clau de triada per defaut « $2 » espotís la mai recenta « $1 ».",
        "version": "Version",
        "version-extensions": "Extensions installadas",
+       "version-skins": "Abilhatges installats",
        "version-specialpages": "Paginas especialas",
        "version-parserhooks": "Extensions del parser",
        "version-variables": "Variablas",
        "version-antispam": "Prevencion del spam",
-       "version-skins": "Abilhatges",
        "version-other": "Divèrs",
        "version-mediahandlers": "Supòrts mèdia",
        "version-hooks": "Croquets",
index 82754c4..ec41ac6 100644 (file)
@@ -28,7 +28,7 @@
        "tog-hidepatrolled": "ਤਾਜ਼ਾ ਤਬਦੀਲੀਆਂ ਵਿੱਚੋਂ ਜਾਂਚੀਆਂ ਸੋਧਾਂ ਲੁਕਾਓ",
        "tog-newpageshidepatrolled": "ਨਵੀਂ ਸਫ਼ਾ ਸੂਚੀ ਵਿੱਚੋਂ ਜਾਂਚੇ ਸਫ਼ੇ ਲੁਕਾਓ",
        "tog-extendwatchlist": "ਸਿਰਫ਼ ਤਾਜ਼ਾ ਹੀ ਨਹੀਂ, ਸਗੋਂ ਸਾਰੀਆਂ ਤਬਦੀਲੀਆਂ ਨੂੰ ਵਖਾਉਣ ਲਈ ਨਿਗਰਾਨੀ-ਲਿਸਟ ਨੂੰ ਵਧਾਓ",
-       "tog-usenewrc": "ਤਾà¨\9c਼ਾ à¨¤à¨¬à¨¦à©\80ਲà©\80à¨\86à¨\82 à¨\85ਤà©\87 à¨¨à¨¿à¨\97ਰਾਨà©\80-ਲਿਸà¨\9f à¨µà¨¿à©±à¨\9a à¨¸à¨«à¨¼à©\87 à¨®à©\81ਤਾਬà¨\95 à¨¤à¨¬à¨¦à©\80ਲà©\80à¨\86à¨\82 à¨¦à©\87 à¨\97ਰà©\81ੱà¨\95 ਬਣਾਓ (ਜਾਵਾਸਕਰਿਪਟ ਲੋੜੀਂਦੀ ਹੈ)",
+       "tog-usenewrc": "ਹਾਲà©\80à¨\86 à¨¤à¨¬à¨¦à©\80ਲà©\80à¨\86à¨\82 à¨\85ਤà©\87 à¨¨à¨¿à¨\97ਰਾਨà©\80-ਲਿਸà¨\9f à¨µà¨¿à©±à¨\9a à¨¸à¨«à¨¼à©\87 à¨®à©\81ਤਾਬà¨\95 à¨¤à¨¬à¨¦à©\80ਲà©\80à¨\86à¨\82 à¨¦à©\87 à¨\97ਰà©\81ੱਪ ਬਣਾਓ (ਜਾਵਾਸਕਰਿਪਟ ਲੋੜੀਂਦੀ ਹੈ)",
        "tog-numberheadings": "ਆਟੋ-ਨੰਬਰ ਸਿਰਨਾਵੇਂ",
        "tog-showtoolbar": "ਸੋਧ ਸੰਦਬਕਸਾ ਵੇਖੋ",
        "tog-editondblclick": "ਦੋ ਵਾਰ ਕਲਿੱਕ ਕਰਨ 'ਤੇ ਸਫ਼ੇ ਸੋਧੋ",
        "talkpagelinktext": "ਗੱਲ-ਬਾਤ",
        "specialpage": "ਖ਼ਾਸ ਸਫ਼ਾ",
        "personaltools": "ਨਿੱਜੀ ਸੰਦ",
-       "postcomment": "ਨਵਾਂ ਭਾਗ",
        "articlepage": "ਸਮੱਗਰੀ ਸਫ਼ਾ ਵੇਖੋ",
        "talk": "ਚਰਚਾ",
        "views": "ਵਿਊ",
        "mypreferencesprotected": "ਤੁਹਾਨੂੰ ਆਪਣੀਆਂ ਪਸੰਦਾਂ ਵਿੱਚ ਸੋਧ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ।",
        "ns-specialprotected": "ਖ਼ਾਸ ਸਫ਼ਿਆਂ ’ਚ ਫੇਰ-ਬਦਲ ਨਹੀਂ ਹੋ ਸਕਦੇ।",
        "titleprotected": "ਇਹ ਸਿਰਲੇਖ [[User:$1|$1]] ਵੱਲੋਂ ਸੁਰੱਖਿਅਤ ਕੀਤਾ ਗਿਆ ਹੈ ਅਤੇ ਵਰਤਿਆ ਨਹੀਂ ਜਾ ਸਕਦਾ। ਦਿੱਤਾ ਹੋਇਆ ਕਾਰਨ ਹੈ, \"''$2''\"।",
-       "filereadonlyerror": "\"$1\" à¨«à¨¼à¨¾à¨\88ਲ à¨µà¨¿à©±à¨\9a à¨¤à¨¬à¨¦à©\80ਲà©\80 à¨¨à¨¹à©\80à¨\82 à¨¹à©\8b à¨°à¨¹à©\80 à¨\95ਿà¨\89à¨\82à¨\95ਿ à¨«à¨¼à¨¾à¨\88ਲ à¨­à©°à¨¡à¨¾à¨° \"$2\" à¨¸à¨¿à¨°à¨«à¨¼ à¨ªà©\9cà©\8dਹਨਯà©\8bà¨\97 à¨°à©\82ਪ à¨µà¨¿à¨\9a à¨¹à©\88।\n\nà¨\89ਹ à¨ªà©\8dਰਸ਼ਾਸà¨\95, à¨\9cਿਹਨà©\87 à¨\87ਹ à¨°à©\8bà¨\95 à¨²à¨¾à¨\88 à¨¹à©\88, à¨¦à¨¾ à¨\95ਹਿਣਾ à¨¹à©\88:",
+       "filereadonlyerror": "\"$1\" à¨«à¨¼à¨¾à¨\88ਲ à¨µà¨¿à©±à¨\9a à¨¤à¨¬à¨¦à©\80ਲà©\80 à¨¨à¨¹à©\80à¨\82 à¨¹à©\8b à¨¸à¨\95ਦà©\80 à¨\95ਿà¨\89à¨\82à¨\95ਿ à¨«à¨¼à¨¾à¨\88ਲ à¨­à©°à¨¡à¨¾à¨° \"$2\" à¨¸à¨¿à¨°à¨«à¨¼ à¨µà©\87à¨\96ਣਯà©\8bà¨\97 à¨°à©\82ਪ à¨µà¨¿à¨\9a à¨¹à©\88।\n\nà¨\89ਹ à¨ªà©\8dਰਬੰਧà¨\95, à¨\9cਿਹਨà©\87 à¨\87ਹ à¨°à©\8bà¨\95 à¨²à¨¾à¨\88 à¨¹à©\88, à¨¦à¨¾ à¨\95ਹਿਣਾ à¨¹à©\88: ''$3''",
        "invalidtitle-knownnamespace": "ਥਾਂ-ਨਾਮ \"$2\" ਅਤੇ ਲਿਖਤ \"$3\" ਵਾਲ਼ਾ ਗ਼ਲਤ ਸਿਰਲੇਖ",
        "invalidtitle-unknownnamespace": "ਅਣਜਾਣ ਨਾਂ-ਸਥਾਨ ਗਿਣਤੀ $1 ਅਤੇ ਲਿਖਤ $2 ਵਾਲ਼ਾ ਗ਼ਲਤ ਸਿਰਲੇਖ",
        "exception-nologin": "ਲਾਗਇਨ ਨਹੀਂ ਕੀਤਾ",
        "virus-badscanner": "ਮੰਦਾ ਪ੍ਰਬੰਧ: ਅਣਜਾਣ ਵਾਇਰਸ ਸਕੈਨਰ: ''$1''",
        "virus-scanfailed": "ਸਕੈਨ ਫੇਲ੍ਹ ਹੈ (ਕੋਡ $1)",
        "virus-unknownscanner": "ਅਣਪਛਾਤਾ ਐਂਟੀਵਾਇਰਸ:",
-       "logouttext": "'''ਹੁਣ ਤੁਸੀਂ ਵਿਦਾਈ ਲੈ ਚੁੱਕੇ ਹੋ।'''\n\nਤੁਸੀਂ {{SITENAME}} ਦੀ ਵਰਤੋਂ ਗੁਮਨਾਮ ਰਹਿ ਕੇ ਕਰ ਸਕਦੇ ਹੋ ਜਾਂ ਦੁਬਾਰਾ ਇਹੋ ਜਾਂ ਵੱਖਰੇ ਵਰਤੋਂਕਾਰ ਵਜੋਂ ਦਾਖ਼ਲਾ ਲੈ ਸਕਦੇ ਹੋ।\nਧਿਆਨ ਦਿਉ ਕਿ ਜਿੰਨੀ ਦੇਰ ਤੱਕ ਤੁਸੀਂ ਆਪਣੇ ਬਰਾਊਜ਼ਰ ਦਾ ਕੈਸ਼ ਸਾਫ਼ ਨਹੀਂ ਕਰਦੇ, ਕੁਝ ਸਫ਼ੇ ਇੱਦਾਂ ਵਿਖਣਗੇ ਜਿਵੇਂ ਤੁਸੀਂ ਅਜੇ ਵੀ ਦਾਖ਼ਲ ਹੀ ਹੋ।",
+       "logouttext": "<strong>ਹੁਣ ਤੁਸੀਂ ਵਿਦਾਈ ਲੈ ਚੁੱਕੇ ਹੋ।</strong> ਧਿਆਨ ਦਿਉ ਕਿ ਜਦੋਂ ਤੱਕ ਤੁਸੀਂ ਆਪਣੇ ਬਰਾਊਜ਼ਰ ਦਾ ਕੈਸ਼ ਸਾਫ਼ ਨਹੀਂ ਕਰਦੇ, ਕੁਝ ਸਫ਼ੇ ਏਦਾਂ ਵਿਖਾਈ ਦੇ ਸਕਦੇ ਹਨ ਕਿ ਜਿਵੇਂ ਤੁਸੀਂ ਅਜੇ ਵੀ ਲਾਗਇਨ ਹੀ ਹੋ।",
        "welcomeuser": "$1 ਜੀ ਆਇਆਂ ਨੂੰ!",
        "welcomecreation-msg": "ਤੁਹਾਡਾ ਖਾਤਾ ਬਣ ਚੁੱਕਾ ਹੈ। ਆਪਣੀਆਂ [[Special:Preferences|{{SITENAME}} ਪਸੰਦ]] ਬਦਲਣੀ ਨਾ ਭੁੱਲੋ।",
        "yourname": "ਵਰਤੋਂਕਾਰ-ਨਾਂ:",
        "externaldberror": "ਜਾਂ ਤਾਂ ਪ੍ਰਮਾਣਕੀ ਡਾਟਾਬੇਸ ਦੋਸ਼ ਆਇਆ ਹੈ ਜਾਂ ਤੁਹਾਨੂੰ ਆਪਣੇ ਬਾਹਰੀ ਖਾਤੇ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ।",
        "login": "ਲਾਗਇਨ",
        "nav-login-createaccount": "ਲਾਗਇਨ/ਖਾਤਾ ਬਣਾਓ",
-       "loginprompt": "ਤੁਹਾਨੂੰ {{SITENAME}} ’ਤੇ ਲਾਗਇਨ ਕਰਨ ਲਈ ਕੂਕੀਸ ਯੋਗ ਕਰਨੇ ਜ਼ਰੂਰੀ ਹਨ।",
        "userlogin": "ਲਾਗਇਨ/ਖਾਤਾ ਬਣਾਓ",
        "userloginnocreate": "ਲਾਗਇਨ",
        "logout": "ਲਾਗ ਆਉਟ",
        "createacct-benefit-body3": "ਹਾਲੀਆ {{PLURAL:$1|ਯੋਗਦਾਨੀ}}",
        "badretype": "ਤੁਹਾਡੇ ਵਲੋਂ ਦਿੱਤੇ ਪਾਸਵਰਡ ਮਿਲਦੇ ਨਹੀਂ ਹਨ।",
        "userexists": "ਯੂਜ਼ਰ-ਨਾਂ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਵੱਖਰਾ ਨਾਂ ਚੁਣੋ ਜੀ।",
-       "loginerror": "ਲਾਗਇਨ ਗਲਤੀ",
+       "loginerror": "ਲਾà¨\97à¨\87ਨ à¨\97਼ਲਤà©\80",
        "createacct-error": "ਖਾਤਾ ਬਣਾਉਣ ਵਿਚ ਗਲਤੀ",
        "createaccounterror": "ਖਾਤਾ ਬਣਾਇਆ ਨਹੀਂ ਜਾ ਸਕਿਆ: $1",
        "nocookiesnew": "ਯੂਜ਼ਰ ਅਕਾਊਂਟ ਬਣਾਇਆ ਗਿਆ ਹੈ, ਪਰ ਤੁਸੀਂ ਲਾਗਇਨ ਨਹੀਂ ਕੀਤਾ ਹੈ।{{SITENAME}} uses cookies to log in users. You have cookies disabled. Please enable them, then log in with your new username and password.",
        "user-mail-no-addy": "ਬਿਨਾਂ ਈ-ਮੇਲ ਪਤਾ ਦਿੱਤੇ ਈ-ਮੇਲ ਭੇਜਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕੀਤੀ।",
        "user-mail-no-body": "ਖ਼ਾਲੀ ਜਾਂ ਬਹੁਤੀ ਛੋਟੀ ਸਮੱਗਰੀ ਨਾਲ਼ ਈਮੇਲ ਭੇਜਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕੀਤੀ ਗਈ",
        "changepassword": "ਪਾਸਵਰਡ ਬਦਲੋ",
-       "resetpass_announce": "ਤà©\81ਸà©\80à¨\82 à¨\87ੱà¨\95 à¨\86ਰà¨\9c਼à©\80 à¨\88-ਮà©\87ਲ à¨\95à©\80ਤà©\87 à¨\95à©\8bਡ à¨¨à¨¾à¨² à¨²à¨¾à¨\97à¨\87ਨ à¨\95à©\80ਤਾ à¨¹à©\88। à¨²à¨¾à¨\97à¨\87ਨ à¨ªà©\82ਰਾ à¨\95ਰਨ à¨²à¨\88, à¨¤à©\81ਹਾਨà©\82à©° à¨\87ੱਥà©\87 à¨¨à¨µà¨¾à¨\82 à¨ªà¨¾à¨¸à¨µà¨°à¨¡ à¨¦à©\87ਣਾ à¨ªà¨µà©\87à¨\97ਾ:",
+       "resetpass_announce": "ਲਾà¨\97à¨\87ਨ à¨ªà©\82ਰਾ à¨\95ਰਨ à¨²à¨\88 à¨¤à©\81ਹਾਨà©\82à©° à¨¨à¨µà¨¾à¨\82 à¨ªà¨¾à¨¸à¨µà¨°à¨¡ à¨¬à¨£à¨¾à¨\89ਣਾ à¨ªà¨µà©\87à¨\97ਾ।",
        "resetpass_header": "ਅਕਾਊਂਟ ਪਾਸਵਰਡ ਬਦਲੋ",
        "oldpassword": "ਪੁਰਾਣਾ ਪਾਸਵਰਡ:",
        "newpassword": "ਨਵਾਂ ਪਾਸਵਰਡ:",
        "resetpass-abort-generic": "ਇੱਕ ਐਕਸਟੈਂਸ਼ਨ ਵੱਲੋਂ ਪਾਸਵਰਡ ਦੀ ਤਬਦੀਲੀ ਰੱਦ ਕੀਤੀ ਗਈ",
        "passwordreset": "ਪਾਸਵਰਡ ਮੁੜ-ਸੈੱਟ ਕਰੋ",
        "passwordreset-text-one": "ਪਾਸਵਰਡ ਦੁਬਾਰਾ ਬਣਾਉਣ ਲਈ ਇਹ ਫ਼ਾਰਮ ਭਰੋ।",
-       "passwordreset-text-many": "{{PLURAL:$1|à¨\86ਪਣਾ à¨ªà¨¾à¨¸à¨µà¨°à¨¡ à¨®à©\81à©\9c à¨¬à¨£à¨¾à¨\89ਣ ਲਈ ਕੋਈ ਇੱਕ ਥਾਂ ਭਰੋ।}}",
+       "passwordreset-text-many": "{{PLURAL:$1|à¨\88-ਮà©\87ਲ à¨\9c਼ਰà©\80à¨\8f à¨\86ਪਣਾ à¨\86ਰà¨\9c਼à©\80 à¨ªà¨¾à¨¸à¨µà¨°à¨¡ à¨¹à¨¾à¨¸à¨² à¨\95ਰਨ ਲਈ ਕੋਈ ਇੱਕ ਥਾਂ ਭਰੋ।}}",
        "passwordreset-legend": "ਪਾਸਵਰਡ ਮੁੜ-ਸੈੱਟ ਕਰੋ",
        "passwordreset-disabled": "ਇਸ ਵਿਕੀ ਤੇ ਪਾਸਵਰਡ ਰੀਸੈੱਟ ਬੰਦ ਕੀਤੇ ਗਏ ਹਨ।",
        "passwordreset-emaildisabled": "ਇਹ ਵਿਕਿ ਉੱਤੇ ਈਮੇਲ ਫੀਚਰ ਬੰਦ ਕੀਤਾ ਹੋਇਆ ਹੈ।",
        "currentrev": "ਮੌਜੂਦਾ ਰੀਵਿਜ਼ਨ",
        "currentrev-asof": "$1 ਮੁਤਾਬਕ ਸਭ ਤੋਂ ਨਵਾਂ ਰੀਵਿਜਨ",
        "revisionasof": "$1 ਦਾ ਰੀਵਿਜਨ",
-       "revision-info": "$2 ਦਾ ਬਣਾਇਆ $1 ਦਾ ਰੀਵਿਜਨ",
+       "revision-info": "{{GENDER:$6|$2}}$7 ਦਾ ਬਣਾਇਆ $1 ਦਾ ਰੀਵਿਜ੍ਹਨ",
        "previousrevision": "←ਪੁਰਾਣਾ ਰੀਵਿਜਨ",
        "nextrevision": "ਨਵਾਂ ਰੀਵਿਜਨ →",
        "currentrevisionlink": "ਸਭ ਤੋ ਨਵਾਂ ਰੀਵਿਜਨ",
        "searchprofile-images": "ਮਲਟੀਮੀਡੀਆ",
        "searchprofile-everything": "ਸਭ ਕੁਝ",
        "searchprofile-advanced": "ਉੱਨਤ",
-       "searchprofile-articles-tooltip": "$1 ਵਿੱਚ ਖੋਜ",
+       "searchprofile-articles-tooltip": "$1 ਵਿੱਚ ਖੋਜ",
        "searchprofile-images-tooltip": "ਫ਼ਾਈਲਾਂ ਖੋਜੋ",
        "searchprofile-everything-tooltip": "ਸਾਰੀ ਸਮੱਗਰੀ ਵਿੱਚੋਂ ਖੋਜੋ (ਗੱਲ-ਬਾਤ ਸਫ਼ਿਆਂ ਸਮੇਤ)",
        "searchprofile-advanced-tooltip": "ਆਪਣੀਆਂ ਬਣਾਈਆਂ ਨਾਂ-ਥਾਂਵਾਂ ਵਿੱਚ ਖੋਜੋ",
        "watchlisttools-raw": "ਕੱਚੀ ਨਿਗਰਾਨ-ਸੂਚੀ ਸੋਧੋ",
        "duplicate-defaultsort": "ਪੁਰਾਣੀ ਮੂਲ ਕਰਮਾਂਕਨ ਕੁੰਜੀ $1 ਦੇ ਬਜਾਏ ਹੁਣ ਮੂਲ ਕਰਮਾਂਕਨ ਕੁੰਜੀ $2 ਹੋਵੇਗੀ।",
        "version": "ਵਰਜਨ",
-       "version-specialpages": "ਖ਼ਾਸ ਸਫ਼ੇ",
        "version-skins": "ਸਕਿਨਾਂ",
+       "version-specialpages": "ਖ਼ਾਸ ਸਫ਼ੇ",
        "version-other": "ਹੋਰ",
        "version-hooks": "ਹੁੱਕਾਂ",
        "version-hook-name": "ਹੁੱਕ ਦਾ ਨਾਂ",
index 59574f4..a75d85e 100644 (file)
        "talkpagelinktext": "dyskusja",
        "specialpage": "Strona specjalna",
        "personaltools": "Osobiste",
-       "postcomment": "Nowa sekcja",
        "articlepage": "Artykuł",
        "talk": "Dyskusja",
        "views": "Widok",
        "externaldberror": "Wystąpił błąd zewnętrznej bazy autentyfikacyjnej lub nie posiadasz uprawnień koniecznych do aktualizacji zewnętrznego konta.",
        "login": "Zaloguj się",
        "nav-login-createaccount": "Logowanie i rejestracja",
-       "loginprompt": "Musisz mieć włączoną w przeglądarce obsługę ciasteczek, by móc się zalogować do {{GRAMMAR:D.lp|{{SITENAME}}}}.",
        "userlogin": "Logowanie i rejestracja",
        "userloginnocreate": "Zaloguj się",
        "logout": "Wyloguj",
        "right-passwordreset": "Sprawdzanie treści e‐maila o resetowaniu hasła",
        "newuserlogpage": "Nowi użytkownicy",
        "newuserlogpagetext": "To jest rejestr ostatnio utworzonych kont użytkowników",
-       "rightslog": "Rejestr uprawnień",
+       "rightslog": "Uprawnienia",
        "rightslogtext": "Rejestr zmian uprawnień użytkowników.",
        "action-read": "przeglądania tej strony",
        "action-edit": "edytowania tej strony",
        "license-nopreview": "(Podgląd niedostępny)",
        "upload_source_url": " (poprawny, publicznie dostępny adres URL)",
        "upload_source_file": " (plik na twoim komputerze)",
+       "listfiles-delete": "usuń",
        "listfiles-summary": "Na tej stronie specjalnej prezentowane są wszystkie przesłane pliki.",
        "listfiles_search_for": "Szukaj pliku o nazwie",
        "imgfile": "plik",
        "wantedpages-badtitle": "Nieprawidłowy tytuł wśród wyników – $1",
        "wantedfiles": "Potrzebne pliki",
        "wantedfiletext-cat": "Następujące pliki są używane, ale nie istnieją. Pliki z obcych repozytoriów mogą być wymienione pomimo istnienia. Takie fałszywe wyniki zostaną <del>przekreślone</del>. Ponadto strony, które osadzają pliki, które nie istnieją, są wymienione w [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Następujące pliki są używane, ale nie istnieją. Dodatkowo strony, które zawierają nieistniejące pliki, są wymienione w [[:$1]].",
        "wantedfiletext-nocat": "Następujące pliki są używane, ale nie istnieją. Pliki z obcych repozytoriów mogą być wymienione pomimo istnienia. Takie fałszywe wyniki zostaną <del>przekreślone</del>.",
+       "wantedfiletext-nocat-noforeign": "Następujące pliki są używane, ale nie istnieją.",
        "wantedtemplates": "Potrzebne szablony",
        "mostlinked": "Najczęściej linkowane strony",
        "mostlinkedcategories": "Kategorie o największej liczbie stron",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|dyskusja]])",
        "unknown_extension_tag": "Nieznany znacznik rozszerzenia „$1”",
        "duplicate-defaultsort": "Uwaga: Domyślnym kluczem sortowania będzie „$2” i zastąpi on wcześniej wykorzystywany klucz „$1”.",
+       "duplicate-displaytitle": "<strong>Uwaga:</strong> Wyświetlenie tytułu „$2” powoduje nadpisanie wcześniej wyświetlanego tytułu „$1”.",
        "version": "Wersja oprogramowania",
        "version-extensions": "Zainstalowane rozszerzenia",
        "version-skins": "Zainstalowane skórki",
        "specialpages-note": "* Normalne strony specjalne.\n* <span class=\"mw-specialpagerestricted\">Zastrzeżone strony specjalne.</span>",
        "specialpages-group-maintenance": "Raporty konserwacyjne",
        "specialpages-group-other": "Inne strony specjalne",
-       "specialpages-group-login": "Zaloguj się / utwórz konto",
+       "specialpages-group-login": "Logowanie / rejestracja",
        "specialpages-group-changes": "Ostatnie zmiany i rejestry",
        "specialpages-group-media": "Pliki",
        "specialpages-group-users": "Użytkownicy i uprawnienia",
index 2e678a6..1b41713 100644 (file)
        "talkpagelinktext": "discussão",
        "specialpage": "Página especial",
        "personaltools": "Ferramentas pessoais",
-       "postcomment": "Nova seção",
        "articlepage": "Ver página de conteúdo",
        "talk": "Discussão",
        "views": "Visualizações",
        "externaldberror": "Ocorreu ou um erro no banco de dados durante a autenticação ou não lhe é permitido atualizar a sua conta externa.",
        "login": "Autenticar-se",
        "nav-login-createaccount": "Entrar / criar conta",
-       "loginprompt": "É necessário estar com cookies ativados para poder autenticar-se no wiki {{SITENAME}}.",
        "userlogin": "Entrar / criar conta",
        "userloginnocreate": "Entrar",
        "logout": "Sair",
        "revdelete-offender": "Autor da revisão:",
        "suppressionlog": "Registro de supressões",
        "suppressionlogtext": "Abaixo está uma lista das eliminações e bloqueios envolvendo conteúdo ocultado por administradores.\nVeja a [[Special:BlockList|lista de bloqueios]] para uma lista de banimentos e bloqueios em efeito neste momento.",
-       "mergehistory": "Fundir histórico de páginas",
+       "mergehistory": "Fundir históricos das páginas",
        "mergehistory-header": "A partir desta página é possível fundir históricos de edições de uma página em outra.\nCertifique-se de que tal alteração manterá a continuidade das ações.",
        "mergehistory-box": "Fundir revisões de duas páginas:",
        "mergehistory-from": "Página de origem:",
        "mergehistory-list": "Histórico de edições habilitadas para fusão",
        "mergehistory-merge": "As edições de [[:$1]] a seguir poderão ser fundidas em [[:$2]]. Utilize a coluna de botões de opção para fundir apenas as edições feitas entre o intervalo de tempo especificado. Note que ao utilizar os links de navegação esta coluna será retornada a seus valores padrão.",
        "mergehistory-go": "Exibir edições habilitadas a serem fundidas",
-       "mergehistory-submit": "Fundir edições",
+       "mergehistory-submit": "Fundir revisões",
        "mergehistory-empty": "Não existem edições habilitadas a serem fundidas.",
        "mergehistory-success": "$3 {{PLURAL:$3|revisão|revisões}} de [[:$1]] fundidas em [[:$2]] com sucesso.",
        "mergehistory-fail": "Não foi possível fundir os históricos; por gentileza, verifique a página e os parâmetros de tempo.",
        "action-patrol": "marcar as edições de outros usuários como patrulhadas",
        "action-autopatrol": "ter suas edições marcadas como patrulhadas",
        "action-unwatchedpages": "ver a lista de páginas não-vigiadas",
-       "action-mergehistory": "fundir o histórico de edições desta página",
+       "action-mergehistory": "fundir o histórico desta página",
        "action-userrights": "editar todos os privilégios de usuário",
        "action-userrights-interwiki": "editar privilégios de usuários de outros wikis",
        "action-siteadmin": "bloquear ou desbloquear o banco de dados",
        "duplicate-defaultsort": "Aviso: A chave de ordenação padrão \"$2\" sobrepõe-se à anterior chave de ordenação padrão \"$1\".",
        "version": "Versão",
        "version-extensions": "Extensões instaladas",
-       "version-skins": "Temas",
+       "version-skins": "Temas instalados",
        "version-specialpages": "Páginas especiais",
        "version-parserhooks": "Hooks do analisador (parser)",
        "version-variables": "Variáveis",
        "expand_templates_remove_nowiki": "Suprima marcações <nowiki> no resultado",
        "expand_templates_generate_xml": "Mostrar árvore de análise (parse) do XML",
        "expand_templates_generate_rawhtml": "Mostrar HTML puro",
-       "expand_templates_preview": "Pré-visualização"
+       "expand_templates_preview": "Pré-visualização",
+       "pagelang-select-lang": "Selecionar idioma",
+       "right-pagelang": "Mudar idioma da página",
+       "action-pagelang": "mudar idioma da página",
+       "log-name-pagelang": "Mudar idioma do log"
 }
index ac5d18e..d045576 100644 (file)
        "unprotectthispage": "Alterar a proteção desta página",
        "newpage": "Página nova",
        "talkpage": "Discutir esta página",
-       "talkpagelinktext": "discussão",
+       "talkpagelinktext": "Discussão",
        "specialpage": "Página especial",
        "personaltools": "Ferramentas pessoais",
-       "postcomment": "Seção nova",
        "articlepage": "Ver página de conteúdo",
        "talk": "Discussão",
        "views": "Vistas",
        "externaldberror": "Ocorreu um erro externo à base de dados durante a autenticação ou não lhe é permitido atualizar a sua conta externa.",
        "login": "Entrar",
        "nav-login-createaccount": "Entrar / criar conta",
-       "loginprompt": "É necessário ter os ''cookies'' ativados no seu navegador para poder autenticar-se em {{SITENAME}}.",
        "userlogin": "Criar uma conta ou entrar",
        "userloginnocreate": "Entrar",
        "logout": "Sair",
        "license": "Licença:",
        "license-header": "Licenciamento",
        "nolicense": "Nenhuma selecionada",
+       "licenses-edit": "Editar opções de licença",
        "license-nopreview": "(Antevisão indisponível)",
        "upload_source_url": " (uma URL válida, publicamente acessível)",
        "upload_source_file": " (um ficheiro no seu computador)",
+       "listfiles-delete": "eliminar",
        "listfiles-summary": "Esta página especial mostra todos os ficheiros carregados.",
        "listfiles_search_for": "Pesquisar por nome de imagem:",
        "imgfile": "ficheiro",
        "delete-warning-toobig": "Esta página tem um histórico de edições longo, com mais de $1 {{PLURAL:$1|edição|edições}}.\nEliminá-la poderá causar problemas na base de dados da {{SITENAME}};\nprossiga com precaução.",
        "deleting-backlinks-warning": "'''Aviso:''' Há [[Special:WhatLinksHere/{{FULLPAGENAME}}|páginas]] que contêm ligações para a página que está prestes a eliminar ou que a transcluem.",
        "rollback": "Reverter edições",
-       "rollback_short": "Desfazer",
-       "rollbacklink": "desfazer",
-       "rollbacklinkcount": "desfazer $1 {{PLURAL:$1|edição|edições}}",
-       "rollbacklinkcount-morethan": "desfazer mais do que $1 {{PLURAL:$1|edição|edições}}",
+       "rollback_short": "Reverter",
+       "rollbacklink": "reverter",
+       "rollbacklinkcount": "reverter $1 {{PLURAL:$1|edição|edições}}",
+       "rollbacklinkcount-morethan": "reverter mais do que $1 {{PLURAL:$1|edição|edições}}",
        "rollbackfailed": "A reversão falhou",
        "cantrollback": "Não foi possível reverter a edição; o último contribuidor é o único autor desta página",
        "alreadyrolled": "Não foi possível reverter as edições de [[:$1]] por [[User:$2|$2]] ([[User talk:$2|discussão]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\nalguém editou ou já reverteu a página.\n\nA última edição foi de [[User:$3|$3]] ([[User talk:$3|discussão]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
index a10ad5a..62837d2 100644 (file)
        "talkpagelinktext": "Used as name of links going to talk page in some places, like in [[Special:RecentChanges]], [[Special:Allmessages]], [[Special:Logs]], and [[Special:Watchlist/edit]].\n\n{{Identical|Talk}}",
        "specialpage": "{{Identical|Special page}}",
        "personaltools": "Heading for a group of links to your user page, talk page, preferences, watchlist, and contributions. This heading is visible in the sidebar in some skins. For an example, see [{{canonicalurl:Main_Page|useskin=simple}} Main Page using simple skin].",
-       "postcomment": "Used as link text.\n\nThe link points to the talk page and has the parameters \"action=edit&section=new\".",
        "addsection": "{{notranslate}}\nText of the new section tab (the one next to the \"edit\" tab on non-Vector skins).\n\nSee also:\n* {{msg-mw|Addsection}}\n* {{msg-mw|Accesskey-ca-addsection}}\n* {{msg-mw|Tooltip-ca-addsection}}",
        "articlepage": "'Content page' is used for NS_MAIN and any other non-standard namespaces. Only used in the Cologne Blue skin in the bottomLinks part.\n\n{{Identical|Content page}}",
        "talk": "Used as display name for the tab to all {{msg-mw|Talk}} pages. These pages accompany all content pages and can be used for discussing the content page. Example: [[Talk:Example]].\n\nSee also:\n* {{msg-mw|Talk}}\n* {{msg-mw|Accesskey-ca-talk}}\n* {{msg-mw|Tooltip-ca-talk}}\n{{Identical|Discussion}}",
        "externaldberror": "This message is thrown when a valid attempt to change the wiki password for a user fails because of a database error or an error from an external system.",
        "login": "{{Doc-special|UserLogin|unlisted=1}}\n{{Identical|Log in}}",
        "nav-login-createaccount": "Shown to anonymous users in the upper right corner of the page. When you can't create an account, the message {{msg-mw|login}} is shown.\n{{Identical|Log in / create account}}",
-       "loginprompt": "A small notice in the log in form.",
+       "loginprompt": "{{ignored}}",
        "userlogin": "Since 1.22 no longer used in core, but may still be used by extensions. DEPRECATED\n\n{{Identical|Log in / create account}}",
        "userloginnocreate": "Since 1.22 no longer used in core, but may still be used by some extensions. A variant of {{msg-mw|Userlogin}} when the user is not allowed to create a new account. DEPRECATED\n\n{{Identical|Log in}}",
        "logout": "Used as link text in your personal toolbox (upper right side).\n\nSee also:\n* {{msg-mw|Logout}}\n* {{msg-mw|Accesskey-pt-logout}}\n* {{msg-mw|Tooltip-pt-logout}}\n{{Identical|Log out}}",
        "license-header": "Used as section header in [[Special:Upload]].\n\nSee also:\n* {{msg-mw|Filedesc}}\n* {{msg-mw|Filestatus}}\n* {{msg-mw|Filesource}}\n{{Identical|Licensing}}",
        "nolicense": "{{Identical|None selected}}",
        "licenses": "{{notranslate}}",
+       "licenses-edit": "Label text for a link on Special:Upload to edit MediaWiki:Licenses",
        "license-nopreview": "Error message when a certain license does not exist",
        "upload_source_url": "Used in [[Special:Upload]].\n\nSee also:\n* {{msg-mw|Sourcefilename|label}}\n* {{msg-mw|Sourceurl|label}}\n* {{msg-mw|Upload source file}}\n* {{msg-mw|Upload-maxfilesize}}",
        "upload_source_file": "Used in [[Special:Upload]].\n\nSee also:\n* {{msg-mw|Sourcefilename|label}}\n* {{msg-mw|Sourceurl|label}}\n* {{msg-mw|Upload source url}}\n* {{msg-mw|Upload-maxfilesize}}",
+       "listfiles-delete": "Text of the delete links next to the entries on [[Special:ListFiles]], surrounded by parentheses.\n{{Identical|Delete}}",
        "listfiles-summary": "This message is displayed at the top of [[Special:ImageList]] to explain how to use that special page.",
        "listfiles_search_for": "Input label for the form displayed on [[Special:ListFiles]].",
        "imgfile": "{{Identical|File}}",
        "popularpages-summary": "{{doc-specialpagesummary|popularpages}}",
        "wantedcategories": "{{doc-special|WantedCategories}}",
        "wantedcategories-summary": "{{doc-specialpagesummary|wantedcategories}}",
-       "wantedpages": "{{doc-special|WantedPages}}",
+       "wantedpages": "{{doc-special|WantedPages}}\n{{Identical|Wanted page}}",
        "wantedpages-summary": "{{doc-specialpagesummary|wantedpages}}",
        "wantedpages-badtitle": "Error message shown when [[Special:WantedPages]] is listing a page with a title that shouldn't exist.\n\nParameters:\n* $1 - a page title",
        "wantedfiles": "{{doc-special|WantedFiles}}",
        "wantedfiles-summary": "{{doc-specialpagesummary|wantedfiles}}",
-       "wantedfiletext-cat": "Message displayed at top of [[special:WantedFiles]]. $1 contains the name of the tracking category for broken files (Including Category prefix). {{msg-mw|wantedfiletext-nocat}} is used if the tracking category is disabled.",
-       "wantedfiletext-nocat": "Message displayed at top of [[special:WantedFiles]] when broken file tracking category is disabled. See {{msg-mw|wantedfiletext-cat}}.",
+       "wantedfiletext-cat": "Message displayed at top of [[special:WantedFiles]] when false positives from foreign file repositories (like commons) are likely. $1 contains the name of the tracking category for broken files (Including Category prefix). {{msg-mw|wantedfiletext-nocat}} is used if the tracking category is disabled.\n\nSee also: {{msg-mw|wantedfiletext-cat-noforeign}}, {{msg-mw|wantedfiletext-nocat}}",
+       "wantedfiletext-cat-noforeign": "Message displayed at top of [[Special:WantedFiles]] when the wiki has no foreign repositories. See also {{msg-mw|wantedfilestext-cat}}. $1 contains the name of the tracking category for broken files (Including Category prefix). {{msg-mw|wantedfiletext-nocat}} is used if the tracking category is disabled.",
+       "wantedfiletext-nocat": "Message displayed at top of [[special:WantedFiles]] when broken file tracking category is disabled and false positives from foreign file repositories (like commons) are likely. See {{msg-mw|wantedfiletext-cat}}.",
+       "wantedfiletext-nocat-noforeign": "Message displayed at top of [[special:WantedFiles]] when broken file tracking category is disabled and their are no foreign file repositories enabled on the wiki. See {{msg-mw|wantedfiletext-cat}}, {{msg-mw|wantedfiletext-nocat}}.",
        "wantedtemplates": "{{doc-special|WantedTemplates}}",
        "wantedtemplates-summary": "{{doc-specialpagesummary|wantedtemplates}}",
        "mostlinked": "{{doc-special|MostLinked}}",
        "timezone-utc": "{{optional}}",
        "unknown_extension_tag": "This is an error shown when you use an unknown extension tag name.\n\nThis feature allows tags like <code><nowiki><pre></nowiki></code> to be called with a parser like <code><nowiki>{{#tag:pre}}</nowiki></code>.\n\nParameters:\n* $1 - the unknown extension tag name",
        "duplicate-defaultsort": "See definition of [[w:Sorting|sort key]] on Wikipedia. Parameters:\n* $1 - old default sort key\n* $2 - new default sort key",
+       "duplicate-displaytitle": "Warning shown when a page has its display title set multiple times. Parameters:\n* $1 - old display title\n* $2 - new display title",
        "version": "{{doc-special|Version}}\n{{Identical|Version}}",
        "version-summary": "{{doc-specialpagesummary|version}}",
        "version-extensions": "Header on [[Special:Version]].",
index 4271f79..8dbbff0 100644 (file)
        "talkpagelinktext": "Discuție",
        "specialpage": "Pagină specială",
        "personaltools": "Unelte personale",
-       "postcomment": "Secțiune nouă",
        "articlepage": "Vedeți articolul",
        "talk": "Discuție",
        "views": "Vizualizări",
        "externaldberror": "A fost fie o eroare de bază de date pentru o autentificare extenă sau nu aveți permisiunea să actualizați contul extern.",
        "login": "Autentificare",
        "nav-login-createaccount": "Creare cont / Autentificare",
-       "loginprompt": "Trebuie să ai modulele cookie activate pentru a te autentifica la {{SITENAME}}.",
        "userlogin": "Creare cont / Autentificare",
        "userloginnocreate": "Autentificare",
        "logout": "Închidere sesiune",
        "license-nopreview": "(Previzualizare indisponibilă)",
        "upload_source_url": " (un URL valid, accesibil public)",
        "upload_source_file": " (un fișier de pe computerul dv.)",
+       "listfiles-delete": "șterge",
        "listfiles-summary": "Această pagină specială listează toate fișierele încărcate.",
        "listfiles_search_for": "Căutare fișiere după nume:",
        "imgfile": "fișier",
        "wantedpages-badtitle": "Titlu invalid în rezultatele : $1",
        "wantedfiles": "Fișiere dorite",
        "wantedfiletext-cat": "Următoarele fișiere sunt utilizate, dar nu există. Fișierele provenind din depozite externe pot apărea listate, în ciuda faptului că ele nu există. Orice astfel de pozitive false vor fi <del>tăiate</del>. În plus, paginile care încorporează astfel de fișiere inexistente sunt listate la [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Următoarele fișiere sunt utilizate, dar nu există. În plus, paginile care încorporează astfel de fișiere inexistente sunt listate la [[:$1]].",
        "wantedfiletext-nocat": "Următoarele fișiere sunt utilizate, dar nu există. Fișierele provenind din depozite externe pot apărea listate, în ciuda faptului că ele nu există. Orice astfel de pozitive false vor fi <del>tăiate</del>.",
+       "wantedfiletext-nocat-noforeign": "Următoarele fișiere sunt utilizate, dar nu există.",
        "wantedtemplates": "Formate dorite",
        "mostlinked": "Cele mai căutate articole",
        "mostlinkedcategories": "Cele mai căutate categorii",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discuție]])",
        "unknown_extension_tag": "Extensie etichetă necunoscută „$1”",
        "duplicate-defaultsort": "'''Atenție:''' Cheia de sortare implicită („$2”) o înlocuiește pe precedenta („$1”).",
+       "duplicate-displaytitle": "<strong>Atenție:</strong> Titlul afișat „$2” înlocuieşte titlul afișat anterior, „$1”.",
        "version": "Versiune",
        "version-extensions": "Extensii instalate",
        "version-skins": "Aspecte instalate",
index fce8dbb..2051860 100644 (file)
        "talkpagelinktext": "обсуждение",
        "specialpage": "Служебная страница",
        "personaltools": "Персональные инструменты",
-       "postcomment": "Новый раздел",
        "articlepage": "Просмотреть статью",
        "talk": "Обсуждение",
        "views": "Просмотры",
        "externaldberror": "Произошла ошибка при аутентификации с помощью внешней базы данных или у вас недостаточно прав для внесения изменений в свою внешнюю учётную запись.",
        "login": "Представиться системе",
        "nav-login-createaccount": "Представиться / зарегистрироваться",
-       "loginprompt": "Вы должны разрешить «cookies», чтобы представиться системе.",
        "userlogin": "Представиться или зарегистрироваться",
        "userloginnocreate": "Представиться",
        "logout": "Завершение сеанса",
        "license": "Лицензирование:",
        "license-header": "Лицензирование",
        "nolicense": "Отсутствует",
+       "licenses-edit": "Изменить параметры лицензии",
        "license-nopreview": "(Предпросмотр недоступен)",
        "upload_source_url": " (правильный, публично доступный интернет-адрес)",
        "upload_source_file": " (файл на вашем компьютере)",
+       "listfiles-delete": "удалить",
        "listfiles-summary": "Эта служебная страница показывает все загруженные файлы.",
        "listfiles_search_for": "Поиск по имени файла:",
        "imgfile": "файл",
        "wantedpages-badtitle": "Ошибочный заголовок в результатах запроса: $1",
        "wantedfiles": "Требуемые файлы",
        "wantedfiletext-cat": "Следующие файлы пытаются использовать, хотя их не существует. В этот список могут ошибочно попасть файлы, находящиеся во внешних хранилищах. Подобные ложные срабатывания будут отмечены <del>зачёркиванием</del>. Кроме того, страницы, содержащие несуществующие файлы, перечислены в [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Следующие файлы используются, но не существуют. Кроме того, страницы, которые ссылаются на эти файлы, не существуют и перечислены на странице [[:$1]].",
        "wantedfiletext-nocat": "Следующие файлы пытаются использовать, хотя их не существует. В этот список могут ошибочно попасть файлы, находящиеся во внешних хранилищах. Подобные ложные срабатывания будут отмечены <del>зачёркиванием</del>.",
+       "wantedfiletext-nocat-noforeign": "Следующие файлы используются, но не существуют.",
        "wantedtemplates": "Требуемые шаблоны",
        "mostlinked": "Страницы, на которые больше всего ссылок",
        "mostlinkedcategories": "Категории, на которые больше всего ссылок",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|обсуждение]])",
        "unknown_extension_tag": "Неизвестный тег расширения «$1»",
        "duplicate-defaultsort": "Внимание. Ключ сортировки по умолчанию «$2» переопределяет прежний ключ сортировки по умолчанию «$1».",
+       "duplicate-displaytitle": "<strong>Внимание:</strong> Отображаемое название «$2» переопределяет ранее заданное отображаемое название «$1».",
        "version": "Версия",
        "version-extensions": "Установленные расширения",
        "version-skins": "Установленные темы оформления",
index 040772c..2d6a3a4 100644 (file)
        "talkpagelinktext": "Pogovor",
        "specialpage": "Posebna stran",
        "personaltools": "Osebna orodja",
-       "postcomment": "Nov razdelek",
        "articlepage": "Prikaže članek",
        "talk": "Pogovor",
        "views": "Pogled",
        "externaldberror": "Pri potrjevanju istovetnosti je prišlo do notranje napake ali pa za osveževanje zunanjega računa nimate dovoljenja.",
        "login": "Prijava",
        "nav-login-createaccount": "Prijavite se / registrirajte se",
-       "loginprompt": "Za prijavo v {{GRAMMAR:tožilnik|{{SITENAME}}}} morate imeti omogočene piškotke.",
        "userlogin": "Prijavite se / registrirajte se",
        "userloginnocreate": "Prijava",
        "logout": "Odjava",
        "license-nopreview": "(Predogled ni na voljo)",
        "upload_source_url": " (veljaven, javnosti dostopen URL)",
        "upload_source_file": " (datoteka na vašem računalniku)",
+       "listfiles-delete": "izbriši",
        "listfiles-summary": "Ta posebna stran prikazuje vse naložene datoteke.",
        "listfiles_search_for": "Išči po imenu datoteke:",
        "imgfile": "dat.",
        "wantedpages-badtitle": "Neveljaven naslov v končnem nizu: $1",
        "wantedfiles": "Želene datoteke",
        "wantedfiletext-cat": "Naslednje datoteke so uporabljene, vendar ne obstajajo. Navedene so morda tudi datoteke iz zunanjih hramb, čeprav obstajajo. Vsi takšni lažni pozitivi bodo <del>prečrtani</del>. Poleg tega so strani, ki vključujejo neobstoječe datoteke, navedene na [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Spodnje datoteke so nekje uporabljene, vendar ne obstajajo. Dodatno lahko strani, ki vključujejo neobstoječe datoteke, najdete na [[:$1]].",
        "wantedfiletext-nocat": "Naslednje datoteke so uporabljene, vendar ne obstajajo. Navedene so morda tudi datoteke iz zunanjih hramb, čeprav obstajajo. Vsi takšni lažni pozitivi bodo <del>prečrtani</del>.",
+       "wantedfiletext-nocat-noforeign": "Spodnje datoteke so nekje uporabljene, vendar ne obstajajo.",
        "wantedtemplates": "Želene predloge",
        "mostlinked": "Strani, na katere se največ povezuje",
        "mostlinkedcategories": "Kategorije z največ elementi",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|pogovor]])",
        "unknown_extension_tag": "Neznana razširitvena etiketa »$1«",
        "duplicate-defaultsort": "'''Opozorilo:''' Privzeti ključ razvrščanja »$2« prepiše prejšnji privzeti ključ razvrščanja »$1«.",
+       "duplicate-displaytitle": "<strong>Opozorilo:</strong> Prikazni naslov »$2« prepiše prejšnji prikazni naslov »$1«.",
        "version": "Različica",
        "version-extensions": "Nameščene razširitve",
        "version-skins": "Nameščene kože",
index 7aa4ea6..31a2dba 100644 (file)
@@ -24,7 +24,9 @@
                        "Urhixidur",
                        "Vinie007",
                        "לערי ריינהארט",
-                       "아라"
+                       "아라",
+                       "Gertakapllani",
+                       "OrvenBregu"
                ]
        },
        "tog-underline": "Nënvizo lidhjet:",
        "talkpagelinktext": "Diskuto",
        "specialpage": "Faqe speciale",
        "personaltools": "Mjetet e mia",
-       "postcomment": "Seksion i ri",
        "articlepage": "Shiko faqen me përmbajtje",
        "talk": "Diskutimet",
        "views": "Shikime",
        "externaldberror": "Ose kishte një gabim tek regjistri i identifikimit të jashtëm, ose nuk ju lejohet të përtërini llogarinë tuaje të jashtme.",
        "login": "Hyni",
        "nav-login-createaccount": "Hyni ose hapni një llogari",
-       "loginprompt": "Ju duhet të mundësoni lejimin e \"cookies\" për të hyrë brënda në {{SITENAME}}.",
        "userlogin": "Hyni / hapni llogari",
        "userloginnocreate": "Hyni",
        "logout": "Dalje",
        "edit-gone-missing": "Faqja nuk mund t freskohet.\nDuket se është grisur.",
        "edit-conflict": "Konflikt në redaktim.",
        "edit-no-change": "Redaktimi juaj është anashkaluar pasi që asnjë ndryshim nuk u bë në tekst.",
+       "postedit-confirmation-created": "Faqja eshte krijuar",
+       "postedit-confirmation-restored": "Faqja eshte kthyer",
+       "postedit-confirmation-saved": "Redaktimi juaj eshte ruajtur.",
        "edit-already-exists": "Faqja nuk mundej të hapet.\nAjo tanimë ekziston.",
        "defaultmessagetext": "Teksti i porosisë së parazgjedhur",
-       "editwarning-warning": "Lënia e kësaj faqeje mund t'ju shkaktojë humbjen e çdo ndryshimi që keni bërë.\nNëse keni hyrë brenda, ju mund ta hiqni këtë paralajmërim në seksionin \"Redaktimi\" tek preferencat tuaja.",
+       "invalid-content-data": "Të pavlefshme të dhënave e përmbajtjes",
+       "editwarning-warning": "Duke e lënë këtë faqe mund të shkaktojë ju për të humbur të gjitha ndryshimet që keni bërë ju.\nNëse ju jeni regjistruar, ju mund të çaktivizoni këtë paralajmërim në \"{{int:prefs-editing}}\" seksionin e preferencave tuaja.",
        "content-model-text": "tekst i thejshtë",
        "expensive-parserfunction-warning": "Kujdes: Kjo faqe ka shumë kërkesa që kërkojnë analizë gramatikore të kushtueshme për sistemin.\n\nDuhet të ketë më pakë se $2, {{PLURAL:$2|kërkesë|kërkesa}}, kurse tani {{PLURAL:$1|është $1 kërkesë|janë $1 kërkesa}}.",
        "expensive-parserfunction-category": "Faqe me shumë shprehje të kushtueshmë për analizë gramatikore",
        "license-nopreview": "(Nuk ka parapamje)",
        "upload_source_url": " (URL e vlefshme, publikisht e përdorshme)",
        "upload_source_file": " (skeda në kompjuterin tuaj)",
+       "listfiles-delete": "fshije",
        "listfiles-summary": "Kjo faqe speciale tregon tërë skedat e ngarkuara.\nFillimisht skedat e ngarkuara së fundmi jepen më sipër.\nShtypni kolonat e tjera për të ndryshuar radhitjen.",
        "listfiles_search_for": "Kërko për emrin e figurës:",
        "imgfile": "skeda",
index 980ea1a..4da7191 100644 (file)
@@ -24,7 +24,8 @@
                        "Милан Јелисавчић",
                        "Михајло Анђелковић",
                        "לערי ריינהארט",
-                       "아라"
+                       "아라",
+                       "Nemo bis"
                ]
        },
        "tog-underline": "Подвлачење веза:",
        "talkpagelinktext": "разговор",
        "specialpage": "Посебна страница",
        "personaltools": "Личне алатке",
-       "postcomment": "Нови одељак",
        "articlepage": "Погледај страницу са садржајем",
        "talk": "Разговор",
        "views": "Прегледи",
        "externaldberror": "Дошло је до грешке при препознавању базе података или немате овлашћења да ажурирате свој спољни налог.",
        "login": "Пријави ме",
        "nav-login-createaccount": "Пријава/регистрација",
-       "loginprompt": "Омогућите колачиће да бисте се пријавили на овај вики.",
        "userlogin": "Пријава/регистрација",
        "userloginnocreate": "Пријава",
        "logout": "Одјава",
        "license-nopreview": "(преглед није доступан)",
        "upload_source_url": " (исправна и јавно доступна адреса)",
        "upload_source_file": "(датотека на вашем рачунару)",
+       "listfiles-delete": "обриши",
        "listfiles-summary": "Ова посебна страница приказује све послате датотеке.",
        "listfiles_search_for": "Назив датотеке:",
        "imgfile": "датотека",
        "uploadnewversion-linktext": "Пошаљи нову верзију ове датотеке",
        "shared-repo-from": "из $1",
        "shared-repo": "заједничко складиште",
-       "shared-repo-name-wikimediacommons": "{{#SWITCH:{{{1|}}}\n|#default=Викимедијина остава\n|dat=Викимедијиној остави\n}}",
+       "shared-repo-name-wikimediacommons": "Викимедијина остава",
        "filepage.css": "/* CSS који је постављен овде се налази на страницама за опис датотека, као и на страним викијима */",
        "upload-disallowed-here": "Не можете да замените ову датотеку.",
        "filerevert": "Врати $1",
        "wantedfiles": "Тражене датотеке",
        "wantedfiletext-cat": "Следеће датотеке се користе, али не постоје. Датотеке из других ризница могу бити наведене иако не постоје. Такве датотеке ће бити <del>поништене</del> са списка. Поред тога, странице које садрже непостојеће датотеке се налазе [[:$1|овде]].",
        "wantedfiletext-nocat": "Следеће датотеке се користе, али не постоје. Датотеке из других ризница могу бити наведене иако не постоје. Такве датотеке ће бити <del>поништене</del> са списка.",
+       "wantedfiletext-nocat-noforeign": "Следеће датотеке се користе, али не постоје.",
        "wantedtemplates": "Тражени шаблони",
        "mostlinked": "Странице с највише веза",
        "mostlinkedcategories": "Категорије с највише веза",
        "movepage-page-exists": "Страница $1 већ постоји и не може се заменити.",
        "movepage-page-moved": "Страница $1 је премештена на $2.",
        "movepage-page-unmoved": "Страница $1 не може да се премести на $2.",
-       "movepage-max-pages": "Највише $1 {{PLURAL:$1|страница је премештена|странице су премештене|страница је премештено}}, и више не може да буде аутоматски премештено.",
+       "movepage-max-pages": "Највише $1 {{PLURAL:$1|страница је премештена|странице су премештене|страница је премештено}} и више не може да буде аутоматски премештено.",
        "movelogpage": "Дневник премештања",
        "movelogpagetext": "Испод се налази списак премештања страница.",
        "movesubpage": "{{PLURAL:$1|Подстраница|Подстранице}}",
        "duplicate-defaultsort": "'''Упозорење:''' подразумевани кључ сврставања „$2“ мења некадашњи кључ „$1“.",
        "version": "Верзија",
        "version-extensions": "Инсталирана проширења",
-       "version-skins": "Теме",
+       "version-skins": "Ð\98нÑ\81Ñ\82алиÑ\80ане Ñ\82еме",
        "version-specialpages": "Посебне странице",
        "version-parserhooks": "Куке рашчлањивача",
        "version-variables": "Променљиве",
index 799eaea..552272d 100644 (file)
@@ -16,7 +16,8 @@
                        "Жељко Тодоровић",
                        "Михајло Анђелковић",
                        "לערי ריינהארט",
-                       "아라"
+                       "아라",
+                       "Nemo bis"
                ]
        },
        "tog-underline": "Podvlačenje veza:",
        "talkpagelinktext": "razgovor",
        "specialpage": "Posebna stranica",
        "personaltools": "Lične alatke",
-       "postcomment": "Novi odeljak",
        "articlepage": "Pogledaj stranicu sa sadržajem",
        "talk": "Razgovor",
        "views": "Pregledi",
        "externaldberror": "Došlo je do greške pri prepoznavanju baze podataka ili nemate ovlašćenja da ažurirate svoj spoljni nalog.",
        "login": "Prijavi me",
        "nav-login-createaccount": "Prijava/registracija",
-       "loginprompt": "Omogućite kolačiće da biste se prijavili na ovaj viki.",
        "userlogin": "Prijava/registracija",
        "userloginnocreate": "Prijava",
        "logout": "Odjava",
        "uploadnewversion-linktext": "Pošalji novo izdanje ove datoteke",
        "shared-repo-from": "iz $1",
        "shared-repo": "zajedničko skladište",
-       "shared-repo-name-wikimediacommons": "{{#SWITCH:{{{1|}}}\n|#default=Vikimedijina ostava\n|dat=Vikimedijinoj ostavi\n}}",
+       "shared-repo-name-wikimediacommons": "Vikimedijina ostava",
        "filepage.css": "/* CSS koji je postavljen ovde se nalazi na stranicama za opis datoteka, kao i na stranim vikijima */",
        "upload-disallowed-here": "Ne možete da zamenite ovu datoteku.",
        "filerevert": "Vrati $1",
        "movepage-page-exists": "Stranica $1 već postoji i ne može se zameniti.",
        "movepage-page-moved": "Stranica $1 je premeštena na $2.",
        "movepage-page-unmoved": "Stranica $1 ne može da se premesti na $2.",
-       "movepage-max-pages": "Najviše $1 {{PLURAL:$1|stranica je premeštena|stranice su premeštene|stranica je premešteno}}, i više ne može da bude automatski premešteno.",
+       "movepage-max-pages": "Najviše $1 {{PLURAL:$1|stranica je premeštena|stranice su premeštene|stranica je premešteno}} i više ne može da bude automatski premešteno.",
        "movelogpage": "Dnevnik premeštanja",
        "movelogpagetext": "Ispod se nalazi spisak premeštanja stranica.",
        "movesubpage": "{{PLURAL:$1|Podstranica|Podstranice}}",
index 6dc5f90..be3b67e 100644 (file)
        "talkpagelinktext": "Diskussion",
        "specialpage": "Specialsida",
        "personaltools": "Personliga verktyg",
-       "postcomment": "Nytt avsnitt",
        "articlepage": "Visa innehållssida",
        "talk": "Diskussion",
        "views": "Visningar",
        "externaldberror": "Antingen inträffade autentiseringsproblem med en extern databas, eller så får du inte uppdatera ditt externa konto.",
        "login": "Logga in",
        "nav-login-createaccount": "Logga in / skapa konto",
-       "loginprompt": "Du måste tillåta kakor för att logga in på {{SITENAME}}.",
        "userlogin": "Logga in / skapa konto",
        "userloginnocreate": "Logga in",
        "logout": "Logga ut",
        "license": "Licens:",
        "license-header": "Licensiering",
        "nolicense": "Ingen angiven",
+       "licenses-edit": "Redigera licensalternativ",
        "license-nopreview": "(Förhandsvisning är inte tillgänglig)",
        "upload_source_url": " (en giltig URL som är allmänt åtkomlig)",
        "upload_source_file": " (en fil på din dator)",
+       "listfiles-delete": "radera",
        "listfiles-summary": "Den här specialsidan visar alla filer som laddats upp.",
        "listfiles_search_for": "Sök efter filnamn:",
        "imgfile": "fil",
        "wantedpages-badtitle": "Ogiltig titel bland resultaten: $1",
        "wantedfiles": "Önskade filer",
        "wantedfiletext-cat": "Följande filer används men finns inte. Filer från utländska databaser kan vara listade trots att de inte finns. Sådana falska realiteter kommer att <del>tas bort</del>. Sidor som bäddar in filer som inte finns listas upp på [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Följande filer används men finns inte. Sidor som bäddar in filer som inte finns listas upp i [[:$1]].",
        "wantedfiletext-nocat": "Följande filer används men finns inte. Filer från utländska databaser kan vara listade trots att de inte finns. Sådana falska realiteter kommer att <del>tas bort</del>.",
+       "wantedfiletext-nocat-noforeign": "Följande filer används men finns inte.",
        "wantedtemplates": "Önskade mallar",
        "mostlinked": "Sidor med flest länkar till sig",
        "mostlinkedcategories": "Kategorier med flest länkar till sig",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|diskussion]])",
        "unknown_extension_tag": "Okänd tagg \"$1\"",
        "duplicate-defaultsort": "'''Varning:''' Standardsorteringsnyckeln \"$2\" tar över från den tidigare standardsorteringsnyckeln \"$1\".",
+       "duplicate-displaytitle": "<strong>Varning:</strong> Visningstiteln \"$2\" skriver över den tidigare visningstiteln \"$1\".",
        "version": "Version",
        "version-extensions": "Installerade programtillägg",
        "version-skins": "Installerade utseenden",
index 5b9b74a..d8d1b2d 100644 (file)
        "talkpagelinktext": "Mesaj",
        "specialpage": "Özel sayfa",
        "personaltools": "Kişisel araçlar",
-       "postcomment": "Yeni bölüm",
        "articlepage": "İçerik sayfasını gör",
        "talk": "Tartışma",
        "views": "Görünümler",
        "externaldberror": "Ya doğrulama veritabanı hatası var ya da kullanıcı hesabınızı güncellemeye yetkiniz yok.",
        "login": "Oturum aç",
        "nav-login-createaccount": "Oturum aç / hesap oluştur",
-       "loginprompt": "{{SITENAME}} sitesinde oturum açabilmek için çerezleri etkinleştirmeniz gerekmektedir.",
        "userlogin": "Oturum aç / hesap oluştur",
        "userloginnocreate": "Giriş yap",
        "logout": "Oturumu kapat",
        "resetpass-submit-cancel": "İptal",
        "resetpass-wrong-oldpass": "Geçersiz geçici veya güncel şifre.\nŞifrenizi zaten başarıyla değiştirdiniz ya da yeni bir geçici şifre istediniz.",
        "resetpass-recycled": "Lütfen parolanızı eski parolanızdan farklı olarak değiştirin.",
+       "resetpass-temp-emailed": "E-postayla gönderilmiş geçici kodla giriş yaptınız. Oturum açmayı tamamlamak için yeni bir şifre belirlemeniz gerekiyor:",
        "resetpass-temp-password": "Geçici parola:",
        "resetpass-abort-generic": "Parola değişikliği bir uzantı tarafından iptal edildi.",
        "resetpass-expired": "Parolanızın süresi bitti. Lütfen, giriş için yeni bir parola oluşturun.",
        "resetpass-expired-soft": "Parolanızın süresi bitti ve değiştirilmesi gerekiyor. Lütfen, yeni bir parola seçin veya daha sonra oluşturmak için \"{{int:resetpass-submit-cancel}}\" butonuna tıklayın.",
+       "resetpass-validity-soft": "Parolanız geçerli değiş: $1\n\nLütfen yeni bir şifre belirleyin veya daha sonra sıfırlamak için \"{{int:resetpass-submit-cancel}}\" bağlantısını tıklayın.",
        "passwordreset": "Parola sıfırlama",
        "passwordreset-text-one": "Parolanızı sıfırlamak için bu formu doldurun.",
        "passwordreset-text-many": "{{PLURAL:$1|E-posta ile geçici bir parola almak için alanlardan birini doldurun.}}",
        "revdelete-show-file-confirm": "\"<nowiki>$1</nowiki>\" dosyasının $2 $3 tarihli silinmiş bir revizyonunu görmek istediğinize emin misiniz?",
        "revdelete-show-file-submit": "Evet",
        "logdelete-selected": "{{PLURAL:$1|Seçili kayıt olayı|Seçili kayıt olayları}}:",
+       "revdelete-text-text": "Silinen sürümler sayfa geçmişinde yer almaya devam edecek ancak okuyucular tarafından içeriklerine erişilemeyecektir.",
+       "revdelete-text-file": "Silinen dosya sürümleri dosya geçmişinde yer almaya devam edecek ancak okuyucular tarafından içerik bölümlerine erişilemeyecektir.",
        "revdelete-confirm": "Lütfen, bunu yapmak istediğinizi , sonuçlarını anladığınızı, ve bunu [[{{MediaWiki:Policy-url}}|ilkelere]] göre yapıyor olduğunuzu onaylayın.",
        "revdelete-suppress-text": "Saklama '''sadece''' aşağıdaki durumlarda kullanılmalıdır:\n* Muhtemel iftira niteliğindeki bilgi\n* Uygunsuz kişisel bilgi\n*: ''ev adresleri ve telefon numaraları, sosyal güvenlik numaraları, vs.''",
        "revdelete-legend": "Görünürlük kısıtlamaları ayarla",
        "license-nopreview": "(Önizleme etkin değil)",
        "upload_source_url": " (geçerli, herkesin ulaşabileceği bir URL)",
        "upload_source_file": " (bilgisayarınızdaki bir dosya)",
+       "listfiles-delete": "sil",
        "listfiles-summary": "Bu özel sayfa yüklenen tüm dosyaları gösterir.",
        "listfiles_search_for": "Medya adı ara:",
        "imgfile": "dosya",
        "filedelete-maintenance": "Dosyaların silinmesi ve geri getirilmesi bakım süresince geçici olarak devre dışı bırakıldı.",
        "filedelete-maintenance-title": "Dosya silinemiyor",
        "mimesearch": "MIME araması",
-       "mimesearch-summary": "Bu sayfa, MIME türü dosyaların süzülmesini sağlar. Girdi: içeriktürü/alttürü, e.g. <code>resim/jpeg</code>.",
+       "mimesearch-summary": "Bu sayfa, dosyaların MIME türlerine göre filtrelenmesini sağlar. Girdi: içerik_türü/alt_tür veya içerik_türü/*, örn. <code>image/jpeg</code>.",
        "mimetype": "MIME türü:",
        "download": "yükle",
        "unwatchedpages": "İzlenmeyen sayfalar",
        "pageswithprop-prophidden-binary": "ikili özellik değeri gizlendi ($1)",
        "doubleredirects": "Çift yönlendirmeler",
        "doubleredirectstext": "Bu sayfa diğer yönlendirme sayfalarına yönlendirme yapan sayfaları listeler.\nHer satırın içerdiği bağlantılar; birinci ve ikinci yönlendirme, ayrıca ikinci yönlendirmenin hedefi, ki bu genelde birinci yönlendirmenin göstermesi gereken \"gerçek\" hedef sayfasıdır.\n<del>Üstü çizili</del> girdiler çözülmüştür.",
-       "double-redirect-fixed-move": "[[$1]] taşındı, artık [[$2]] sayfasına yönlendiriyor",
+       "double-redirect-fixed-move": "[[$1]] taşındı.\nYönlendirme otomatik olarak güncellendi ve [[$2]] sayfasına yönlendirildi.",
        "double-redirect-fixed-maintenance": "[[$1]] - [[$2]] yapılan çift yönlendirme düzeltiliyor.",
        "double-redirect-fixer": "Yönlendirme tamircisi",
        "brokenredirects": "Boş yönlendirmeler",
        "movenotallowedfile": "Sayfaları taşımaya izniniz yok.",
        "cant-move-user-page": "Kullanıcı sayfalarını taşımaya izniniz yok (altsayfalardan başka).",
        "cant-move-to-user-page": "Bir sayfayı, bir kullanıcı sayfasına taşımaya izniniz yok (bir kullanıcı altsayfası dışında).",
+       "cant-move-category-page": "Kategori sayfalarını taşıma yetkiniz yok.",
        "cant-move-to-category-page": "Bir sayfayı, bir kategoriye taşımaya izniniz yok.",
        "newtitle": "Yeni isim",
        "move-watch": "Bu sayfayı izle",
        "expand_templates_remove_nowiki": "Sonuçlarda <nowiki> etiketlerini bastır",
        "expand_templates_generate_xml": "XML derleyici ağacını göster",
        "expand_templates_generate_rawhtml": "Ham HTML göster",
-       "expand_templates_preview": "Önizleme"
+       "expand_templates_preview": "Önizleme",
+       "pagelang-language": "Dil",
+       "pagelang-use-default": "Varsayılan dili kullan",
+       "pagelang-select-lang": "Dil seçin",
+       "right-pagelang": "Sayfa dilini değiştir",
+       "action-pagelang": "sayfa dilini değiştir"
 }
index 8cc9001..0fa99e0 100644 (file)
        "talkpagelinktext": "Бәхәс",
        "specialpage": "Махсус бит",
        "personaltools": "Шәхси кораллар",
-       "postcomment": "Яңа бүлек",
        "articlepage": "Мәкаләне карау",
        "talk": "Бәхәс",
        "views": "Караулар",
        "externaldberror": "Тышкы мәгълүмат базасы ярдәмендә аутентификация үткәндә хата чыкты, яисә тышкы хисап язмагызга үзгәрешләр кертү хокукыгыз юк.",
        "login": "Керү",
        "nav-login-createaccount": "Керү / теркәлү",
-       "loginprompt": "{{SITENAME}} проектына керү өчен «cookies» рөхсәт ителгән булырга тиеш.",
        "userlogin": "Керү / теркәлү",
        "userloginnocreate": "Керү",
        "logout": "Чыгу",
        "newimages": "Яңа сүрәтләр җыелмасы",
        "newimages-legend": "Фильтр",
        "ilsubmit": "Эзләү",
+       "hours": "{{PLURAL:$1|$1 cәгать|$1 cәгать}}",
+       "hours-ago": "$1 cәгать элек",
+       "minutes-ago": "$1 минут элек",
        "bad_image_list": "Киләчәк рәвеш кирәк:\n\nИсемлек кисәкләре генә (* символыннан башланучы юллар) саналырлар.\nЮлның беренче сылтамасы куйма өчен тыелган рәсемгә сылтама булырга тиеш.\nШул ук юлның киләчәк сылтамалары чыгармалар, рәсемгә тыелмаган битләре, саналырлар.",
        "metadata": "Мета мәгълүматлар",
        "metadata-help": "Бу файлда гадәттә санлы камера яки сканер тарафыннан өстәлгән мәгълүмат бар. Әгәр бу файл төзү вакытыннан соң үзгәртелгән булса, аның кайбер параметрлары дөрес булмаска мөмкин.",
index 95d9f26..b1a48d9 100644 (file)
        "talkpagelinktext": "обговорення",
        "specialpage": "Спеціальна сторінка",
        "personaltools": "Особисті інструменти",
-       "postcomment": "Новий розділ",
        "articlepage": "Переглянути статтю",
        "talk": "Обговорення",
        "views": "Перегляди",
        "externaldberror": "Сталася помилка при автентифікації за допомогою зовнішньої бази даних, або у вас недостатньо прав для внесення змін до свого зовнішнього облікового запису.",
        "login": "Вхід до системи",
        "nav-login-createaccount": "Вхід / реєстрація",
-       "loginprompt": "Ви повинні активувати куки (cookies) для входу до {{GRAMMAR:genitive|{{SITENAME}}}}.",
        "userlogin": "Вхід / реєстрація",
        "userloginnocreate": "Увійти",
        "logout": "Вихід із системи",
index a262641..63be321 100644 (file)
        "talkpagelinktext": "Thảo luận",
        "specialpage": "Trang đặc biệt",
        "personaltools": "Công cụ cá nhân",
-       "postcomment": "Đề mục mới",
        "articlepage": "Xem trang nội dung",
        "talk": "Thảo luận",
        "views": "Các hiển thị",
        "externaldberror": "Có lỗi khi xác nhận cơ sở dữ liệu bên ngoài hoặc bạn không được phép cập nhật tài khoản bên ngoài.",
        "login": "Đăng nhập",
        "nav-login-createaccount": "Đăng nhập / Mở tài khoản",
-       "loginprompt": "Bạn cần bật cookie để đăng nhập vào {{SITENAME}}.",
        "userlogin": "Đăng nhập / Mở tài khoản",
        "userloginnocreate": "Đăng nhập",
        "logout": "Đăng xuất",
        "noemailprefs": "Hãy ghi một địa chỉ thư điện tử trong tùy chọn cá nhân để có thể sử dụng tính năng này.",
        "emailconfirmlink": "Xác nhận địa chỉ thư điện tử",
        "invalidemailaddress": "Địa chỉ thư điện tử không được chấp nhận vì định dạng thư có vẻ sai.\nHãy nhập một địa chỉ có định dạng đúng hoặc bỏ trống ô đó.",
-       "cannotchangeemail": "Không có thể thay đổi địa chỉ thư điện tử của các tài khoản trên wiki này.",
+       "cannotchangeemail": "Không thể thay đổi địa chỉ thư điện tử của các tài khoản trên wiki này.",
        "emaildisabled": "Website này không thể gửi thư điện tử.",
        "accountcreated": "Mở tài khoản thành công",
        "accountcreatedtext": "Tài khoản thành viên cho [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|thảo luận]]) đã được mở.",
        "prefs-custom-css": "sửa CSS",
        "prefs-custom-js": "sửa JS",
        "prefs-common-css-js": "CSS/JS chung cho mọi giao diện:",
-       "prefs-reset-intro": "Có thể mặc định lại toàn bộ tùy chọn dùng trang này.\nKhông có thể lùi lại tác động này.",
+       "prefs-reset-intro": "Có thể mặc định lại toàn bộ tùy chọn dùng trang này. Điều này không thể hoàn tác.",
        "prefs-emailconfirm-label": "Xác nhận thư điện tử:",
        "youremail": "Thư điện tử:",
        "username": "{{GENDER:$1}}Tên người dùng:",
        "ignorewarnings": "Bỏ qua cảnh báo",
        "minlength1": "Tên tập tin phải có ít nhất một ký tự.",
        "illegalfilename": "Tên tập tin “$1” có chứa ký tự không được phép dùng cho tựa trang. Xin hãy đổi tên và tải lên lại.",
-       "filename-toolong": "Tên tập tin không có thể dài quá 240 byte.",
+       "filename-toolong": "Tên tập tin không thể dài quá 240 byte.",
        "badfilename": "Tên tập tin đã được đổi thành “$1”.",
        "filetype-mime-mismatch": "Phần mở rộng của tập tin (“.$1”) không phù hợp kiểu MIME được nhận ra ($2).",
        "filetype-badmime": "Không thể tải lên các tập tin có kiểu MIME “$1”.",
        "license-nopreview": "(Không xem trước được)",
        "upload_source_url": " (địa chỉ URL đúng, có thể truy cập)",
        "upload_source_file": " (tập tin trên máy của bạn)",
+       "listfiles-delete": "xóa",
        "listfiles-summary": "Trang đặc biệt này liệt kê các tập tin được tải lên.",
        "listfiles_search_for": "Tìm kiếm theo tên tập tin:",
        "imgfile": "tập tin",
        "shared-repo-from": "tại $1",
        "shared-repo": "kho lưu trữ dùng chung",
        "filepage.css": "/* Mã CSS tại đây sẽ ảnh hướng đến trang miêu tả tập tin, cũng như các wiki khách bên ngoài dựa trên wiki này */",
-       "upload-disallowed-here": "Bạn không có thể ghi đè lên tập tin này.",
+       "upload-disallowed-here": "Bạn không thể ghi đè lên tập tin này.",
        "filerevert": "Lùi lại phiên bản của $1",
        "filerevert-legend": "Lùi lại tập tin",
        "filerevert-intro": "Bạn đang lùi '''[[Media:$1|$1]]''' về [$4 phiên bản lúc $3, $2].",
        "wantedpages-badtitle": "Tiêu đề không hợp lệ trong tập kết quả: $1",
        "wantedfiles": "Tập tin cần thiết",
        "wantedfiletext-cat": "Các tập tin sau được nhúng nhưng không tồn tại. Các tập tin từ kho dùng chung có thể được liệt kê trong khi tồn tại; các trường hợp này được <del>gạch bỏ</del>. Ngoài ra, các trang nhúng tập tin không tồn tại được liệt kê tại [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Các tập tin bên dưới được sử dụng nhưng không tồn tại. Các trang nhúng những tập tin không tồn tại cũng được xếp trong [[:$1]].",
        "wantedfiletext-nocat": "Các tập tin sau được nhúng nhưng không tồn tại. Các tập tin từ kho dùng chung có thể được liệt kê trong khi tồn tại; các trường hợp này được <del>gạch bỏ</del>.",
+       "wantedfiletext-nocat-noforeign": "Các tập tin bên dưới được sử dụng nhưng không tồn tại.",
        "wantedtemplates": "Bản mẫu cần viết nhất",
        "mostlinked": "Trang được liên kết đến nhiều nhất",
        "mostlinkedcategories": "Thể loại có nhiều trang nhất",
        "imported-log-entries": "Đã nhập {{PLURAL:$1|mục nhật trình|$1 mục nhật trình}}.",
        "importfailed": "Không nhập được: $1",
        "importunknownsource": "Không hiểu nguồn trang để nhập vào",
-       "importcantopen": "Không có thể mở tập tin để nhập vào",
+       "importcantopen": "Không thể mở tập tin để nhập vào",
        "importbadinterwiki": "Liên kết liên wiki sai",
        "importsuccess": "Nhập thành công!",
        "importnosources": "Không có nguồn nhập giữa wiki và việc nhập lịch sử bị tắt.",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|thảo luận]])",
        "unknown_extension_tag": "Không hiểu thẻ mở rộng “$1”",
        "duplicate-defaultsort": "Cảnh báo: Từ khóa xếp mặc định “$2” ghi đè từ khóa trước, “$1”.",
+       "duplicate-displaytitle": "<strong>Cảnh báo:</strong> Tên hiển thị “$2” ghi đè tên hiển thị “$1” bên trên.",
        "version": "Phiên bản",
        "version-extensions": "Các phần mở rộng được cài đặt",
        "version-skins": "Giao diện đã cài đặt",
index 5778823..62b2328 100644 (file)
        "talkpagelinktext": "討論",
        "specialpage": "特別頁",
        "personaltools": "私人家伙",
-       "postcomment": "新段",
        "articlepage": "望內容頁",
        "talk": "探讨",
        "views": "望",
index 816bf32..4376f87 100644 (file)
        "talkpagelinktext": "שמועס",
        "specialpage": "ספעציעלער בלאט",
        "personaltools": "פערזענלעכע געצייג",
-       "postcomment": "נייע אפטיילונג",
        "articlepage": "זען אינהאַלט בלאַט",
        "talk": "שמועס",
        "views": "קוקן",
index a67e3d9..0c53e99 100644 (file)
@@ -45,6 +45,7 @@
        "tog-showhiddencats": "Ṣ'àfihàn àwọn ẹ̀ka pípamọ́",
        "tog-norollbackdiff": "Fo ìyàtọ̀ lẹ́yín síṣe ìyísẹ́yìn",
        "tog-useeditwarning": "Kìlọ̀ fún mi tí mo bá únkúrò ní ojúewé àtúnṣe láì tíì mupamọ́",
+       "tog-prefershttps": "Lo ìjáwọlé oníàbò ní gbogbo ìgbà",
        "underline-always": "Nígbà gbogbo",
        "underline-never": "Rárá",
        "underline-default": "Ti àwọ tàbí ẹrọ́ ìtọ́kùn",
        "permalink": "Ìjápọ̀ tíkòníyípadà",
        "print": "Ìtẹ̀síìwé",
        "view": "Ìwòran",
+       "view-foreign": "Ìgbéwò lórí $1",
        "edit": "Àtúnṣe",
+       "edit-local": "Àtúnṣe ìjúwe ìhàhín",
        "create": "Ṣèdá",
+       "create-local": "Ìfikún ìjúwe ìhàhín",
        "editthispage": "S'àtúnṣe ojúewé yi",
        "create-this-page": "Ṣè'dá ojúewé yìí",
        "delete": "Ìparẹ́",
        "talkpagelinktext": "Ọ̀rọ̀",
        "specialpage": "Ojúewé Pàtàkì",
        "personaltools": "Àwọn irinṣẹ́ àdáni",
-       "postcomment": "Abala tuntun",
        "articlepage": "Ìfihàn àkóónú ojúewé",
        "talk": "Ìfọ̀rọ̀wérọ̀",
        "views": "Àwọn ìwò",
        "externaldberror": "Bóyá àsìṣe ìfidájú ibùdó dátà ló ṣẹlẹ̀ tàbí ẹ kò jẹ́ gbígbà ní ààyè láti sọ àpamọ́ òde yín di ọ̀tun.",
        "login": "Ìjáwọlé",
        "nav-login-createaccount": "Ìwọlé / Ìforúkọ sílẹ̀",
-       "loginprompt": "Ẹ gbọ́dọ̀ jọ̀wọ́ cookies láti wọlé sí {{SITENAME}}.",
        "userlogin": "Ìwọlé / ìforúkọ sílẹ̀",
        "userloginnocreate": "Ìjáwọlé",
        "logout": "Ìjáde",
        "duplicate-defaultsort": "'''Ìkìlọ̀:''' Bọ́tìnì ìtò àkọ́kọ́ṣe \"$2\" dípò Bọ́tìnì ìtò àkọ́kọ́ṣe \"$1\" tẹ́lẹ̀.",
        "version": "Àtẹ̀jáde",
        "version-extensions": "Àwọn ìfàgùn kíkànsínú",
+       "version-skins": "Skin (Àwọ̀)",
        "version-specialpages": "Àwọn ojúewé pàtàkì",
        "version-variables": "Ayàtọ̀",
        "version-antispam": "Ìdínà spam",
-       "version-skins": "Skin (Àwọ̀)",
        "version-other": "Òmíràn",
        "version-hooks": "Àwọn hook",
        "version-hook-name": "Orúkọ hook",
index 966331a..a20b6c3 100644 (file)
        "talkpagelinktext": "讨论",
        "specialpage": "特殊页面",
        "personaltools": "个人工具",
-       "postcomment": "新段落",
        "articlepage": "查看内容页面",
        "talk": "讨论",
        "views": "查看",
        "externaldberror": "验证数据库出错或您被禁止更新您的外部账号。",
        "login": "登录",
        "nav-login-createaccount": "登录/创建账户",
-       "loginprompt": "你必须启用Cookie才能登录{{SITENAME}}。",
        "userlogin": "登录/创建账户",
        "userloginnocreate": "登录",
        "logout": "退出",
        "license": "授权协议:",
        "license-header": "授权协议",
        "nolicense": "未选定",
+       "licenses-edit": "编辑许可选项",
        "license-nopreview": "(无预览可用)",
        "upload_source_url": "(有效、可以公开访问的URL)",
        "upload_source_file": "(您计算机上的一个文件)",
+       "listfiles-delete": "删除",
        "listfiles-summary": "本特殊页面展示所有上传的文件。",
        "listfiles_search_for": "按媒体名称搜索:",
        "imgfile": "文件",
        "wantedpages-badtitle": "在结果组上的无效标题:$1",
        "wantedfiles": "需要的文件",
        "wantedfiletext-cat": "以下文件被使用,但并不存在。来自外部库的文件即使存在也可能被列出。任何这类误报会用<del>删除线</del>标记。另外,插入不存在的文件的页面列于[[:$1]]。",
+       "wantedfiletext-cat-noforeign": "以下文件被使用但尚不存在。此外嵌入不存在文件的页面在[[:$1]]列出。",
        "wantedfiletext-nocat": "以下文件被使用,但并不存在。来自外部库的文件即使存在也可能被列出。任何这类误报会用<del>删除线</del>标记。",
+       "wantedfiletext-nocat-noforeign": "以下文件被使用但尚不存在。",
        "wantedtemplates": "需要的模板",
        "mostlinked": "最多链接页面",
        "mostlinkedcategories": "最多链接分类",
        "signature": "[[{{ns:user}}:$1|$2]]([[{{ns:user_talk}}:$1|讨论]])",
        "unknown_extension_tag": "未知扩展标签“$1”",
        "duplicate-defaultsort": "'''警告:'''默认排序关键词“$2”覆盖了之前的默认排序关键词“$1”。",
+       "duplicate-displaytitle": "<strong>警告:</strong>显示的标题“$2”重写了此前显示的标题“$1”。",
        "version": "版本",
        "version-extensions": "安装的扩展程序",
        "version-skins": "已安装皮肤",
index 2dbb891..d6efb45 100644 (file)
        "history_short": "歷史",
        "updatedmarker": "自我最後一次訪問以後的更新",
        "printableversion": "可列印版",
-       "permalink": "固定連結",
+       "permalink": "靜態連結",
        "print": "列印",
        "view": "檢視",
        "view-foreign": "用 $1 檢視",
        "talkpagelinktext": "對話",
        "specialpage": "特殊頁面",
        "personaltools": "個人工具",
-       "postcomment": "新章節",
        "articlepage": "檢視內容頁面",
        "talk": "討論",
        "views": "檢視",
        "nstab-project": "專案頁面",
        "nstab-image": "檔案",
        "nstab-mediawiki": "訊息",
-       "nstab-template": "模æ\9d¿",
+       "nstab-template": "樣ç\89\88",
        "nstab-help": "說明頁面",
        "nstab-category": "分類",
        "nosuchaction": "無此動作",
        "externaldberror": "這可能是由於資料庫驗證錯誤,或是不允許您更新外部帳號。",
        "login": "登入",
        "nav-login-createaccount": "登入/建立帳號",
-       "loginprompt": "您必須允許瀏覽器紀錄 Cookie 才能成功登入 {{SITENAME}}。",
        "userlogin": "登入/建立帳號",
        "userloginnocreate": "登入",
        "logout": "登出",
        "license-nopreview": "(不可預覽)",
        "upload_source_url": "(有效,可公開存取的 URL)",
        "upload_source_file": "(在您電腦上的檔案)",
+       "listfiles-delete": "刪除",
        "listfiles-summary": "此特殊頁面顯示所有上傳過的檔案。",
        "listfiles_search_for": "搜尋媒體名稱:",
        "imgfile": "檔案",
        "wantedpages-badtitle": "在結果組上的無效標題: $1",
        "wantedfiles": "需要的檔案",
        "wantedfiletext-cat": "以下檔案被使用,但不存在。外部儲存庫的文件儘管現有,但可能會在此列出,任何此類的誤報將被<del>剔除</del>。此外,內嵌了不存在的檔案的網頁將在[[:$1]]列出。",
+       "wantedfiletext-cat-noforeign": "下列檔案已被使用但不存在。 除此之外,頁面已內嵌但不存在的檔案列於 [[:$1]]。",
        "wantedfiletext-nocat": "以下檔案被使用,但不存在。外部儲存庫的文件儘管現有,但可能會在此列出,任何此類的誤報將被<del>剔除</del>。",
+       "wantedfiletext-nocat-noforeign": "下列檔案已被使用但不存在。",
        "wantedtemplates": "需要的樣版",
        "mostlinked": "最多連結頁面",
        "mostlinkedcategories": "最多連結分類",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|對話]])",
        "unknown_extension_tag": "不明的擴充標籤 \"$1\"",
        "duplicate-defaultsort": "<strong>警告:</strong>預設的排序鍵 \"$2\" 會覆蓋先前預設的排序鍵 \"$1\"。",
+       "duplicate-displaytitle": "<strong>警告:</strong> 顯示標題 \"$2\" 覆蓋之前的顯示標題 \"$1\"。",
        "version": "版本",
        "version-extensions": "已安裝的擴充套件",
-       "version-skins": "已外觀",
+       "version-skins": "已安裝的外觀",
        "version-specialpages": "特殊頁面",
        "version-parserhooks": "語法連結(Hook)",
        "version-variables": "變數",
        "version-entrypoints": "入口 URL",
        "version-entrypoints-header-entrypoint": "入口",
        "version-entrypoints-header-url": "URL",
-       "version-entrypoints-articlepath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgArticlePath 條目路徑]",
+       "version-entrypoints-articlepath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgArticlePath 文章路徑]",
+       "version-entrypoints-scriptpath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgScriptPath Script 路徑]",
        "redirect": "重新導向至檔案、使用者、頁面或修訂 ID",
        "redirect-legend": "重新導向至檔案或頁面",
        "redirect-summary": "此特殊頁面可用來重新導向至檔案 (指定檔案名稱)、頁面 (指定修訂 ID 或頁面 ID) 或使用者頁面 (指定使用者 ID)。用法:[[{{#Special:Redirect}}/file/Example.jpg]]、[[{{#Special:Redirect}}/page/64308]]、[[{{#Special:Redirect}}/revision/328429]] 或 [[{{#Special:Redirect}}/user/101]]。",
index 6900aeb..8167b19 100644 (file)
@@ -365,6 +365,8 @@ $magicWords = array(
        'url_query'               => array( 0,    'QUERY' ),
        'defaultsort_noerror'     => array( 0,    'noerror' ),
        'defaultsort_noreplace'   => array( 0,    'noreplace' ),
+       'displaytitle_noerror'    => array( 0,    'noerror' ),
+       'displaytitle_noreplace'  => array( 0,    'noreplace' ),
        'pagesincategory_all'     => array( 0,    'all' ),
        'pagesincategory_pages'   => array( 0,    'pages' ),
        'pagesincategory_subcats' => array( 0,    'subcats' ),
index 1790f48..1dd1a89 100644 (file)
--- a/load.php
+++ b/load.php
@@ -23,7 +23,7 @@
  */
 
 // Bail if PHP is too low
-if ( !function_exists( 'version_compare' ) || version_compare( phpversion(), '5.3.2' ) < 0 ) {
+if ( !function_exists( 'version_compare' ) || version_compare( PHP_VERSION, '5.3.2' ) < 0 ) {
        // We need to use dirname( __FILE__ ) here cause __DIR__ is PHP5.3+
        require dirname( __FILE__ ) . '/includes/PHPVersionError.php';
        wfPHPVersionError( 'load.php' );
index b81f9fd..ce38dad 100644 (file)
@@ -41,7 +41,10 @@ class BenchmarkParse extends Maintenance {
                parent::__construct();
                $this->addDescription( 'Benchmark parse operation' );
                $this->addArg( 'title', 'The name of the page to parse' );
-               $this->addOption( 'cold', 'Don\'t repeat the parse operation to warm the cache' );
+               $this->addOption( 'warmup', 'Repeat the parse operation this number of times to warm the cache',
+                       false, true );
+               $this->addOption( 'loops', 'Number of times to repeat parse operation post-warmup',
+                       false, true );
                $this->addOption( 'page-time',
                        'Use the version of the page which was current at the given time',
                        false, true );
@@ -81,22 +84,30 @@ class BenchmarkParse extends Maintenance {
                        exit( 1 );
                }
 
-               if ( !$this->hasOption( 'cold' ) ) {
+               $warmup = $this->getOption( 'warmup', 1 );
+               for ( $i = 0; $i < $warmup; $i++ ) {
                        $this->runParser( $revision );
                }
 
+               $loops = $this->getOption( 'loops', 1 );
+               if ( $loops < 1 ) {
+                       $this->error( 'Invalid number of loops specified', true );
+               }
                $startUsage = getrusage();
                $startTime = microtime( true );
-               $this->runParser( $revision );
+               for ( $i = 0; $i < $loops; $i++ ) {
+                       $this->runParser( $revision );
+               }
                $endUsage = getrusage();
                $endTime = microtime( true );
 
                printf( "CPU time = %.3f s, wall clock time = %.3f s\n",
                        // CPU time
-                       $endUsage['ru_utime.tv_sec'] + $endUsage['ru_utime.tv_usec'] * 1e-6
-                       - $startUsage['ru_utime.tv_sec'] - $startUsage['ru_utime.tv_usec'] * 1e-6,
+                       $endUsage['ru_utime.tv_sec'] + $endUsage['ru_utime.tv_usec'] * 1e-6
+                       - $startUsage['ru_utime.tv_sec'] - $startUsage['ru_utime.tv_usec'] * 1e-6 ) / $loops,
                        // Wall clock time
-                       $endTime - $startTime );
+                       ( $endTime - $startTime ) / $loops
+               );
        }
 
        /**
index 5a3e00c..3b29452 100644 (file)
@@ -21,7 +21,7 @@
  * @ingroup Maintenance
  */
 
-if ( !function_exists( 'version_compare' ) || ( version_compare( phpversion(), '5.3.2' ) < 0 ) ) {
+if ( !function_exists( 'version_compare' ) || ( version_compare( PHP_VERSION, '5.3.2' ) < 0 ) ) {
        require_once dirname( __FILE__ ) . '/../includes/PHPVersionError.php';
        wfPHPVersionError( 'cli' );
 }
@@ -85,8 +85,8 @@ class CommandLineInstaller extends Maintenance {
                        true
                );
                $this->addOption( 'confpath', "Path to write LocalSettings.php to ($IP)", false, true );
+               $this->addOption( 'dbschema', 'The schema for the MediaWiki DB in PostgreSQL/Microsoft SQL Server (mediawiki)', false, true );
                /*
-               $this->addOption( 'dbschema', 'The schema for the MediaWiki DB in pg (mediawiki)', false, true );
                $this->addOption( 'namespace', 'The project namespace (same as the "name" argument)', false, true );
                */
                $this->addOption( 'env-checks', "Run environment checks only, don't change anything" );
index ca9572f..e1ccb46 100755 (executable)
@@ -72,7 +72,7 @@ NEWCHANGESDISPLAY=$(git log $OLDHASH.. --oneline --no-merges --reverse --color=a
 
 # Copy files
 # - Exclude the default non-svg stylesheet
-rsync --recursive --delete --force --exclude 'oojs-ui.css' ./dist/ $TARGET_REPO/$TARGET_DIR || exit 1
+rsync --recursive --delete --force --exclude 'oojs-ui.css' --exclude 'oojs-ui*.rtl.css' ./dist/ $TARGET_REPO/$TARGET_DIR || exit 1
 
 # Read the new version
 NEWVERSION=$(oojsuiversion)
index 9b86e1b..1383a8a 100644 (file)
@@ -131,8 +131,11 @@ class RunJobs extends Maintenance {
                                        $this->runJobsLog( $job->toString() . " t=$timeMs good" );
                                }
 
-                               // Back off of certain jobs for a while
+                               // Back off of certain jobs for a while (for throttling and for errors)
                                $ttw = $this->getBackoffTimeToWait( $job );
+                               if ( $status === false && mt_rand( 0, 49 ) == 0 ) {
+                                       $ttw = max( $ttw, 30 );
+                               }
                                if ( $ttw > 0 ) {
                                        $jType = $job->getType();
                                        $backoffs[$jType] = isset( $backoffs[$jType] ) ? $backoffs[$jType] : 0;
index a51564a..046d73c 100755 (executable)
@@ -26,7 +26,7 @@
  * @ingroup Maintenance
  */
 
-if ( !function_exists( 'version_compare' ) || ( version_compare( phpversion(), '5.3.2' ) < 0 ) ) {
+if ( !function_exists( 'version_compare' ) || ( version_compare( PHP_VERSION, '5.3.2' ) < 0 ) ) {
        require dirname( __FILE__ ) . '/../includes/PHPVersionError.php';
        wfPHPVersionError( 'cli' );
 }
index 56c1308..be176c7 100644 (file)
  * @file
  */
 
+// Bail if PHP is too low
+if ( !function_exists( 'version_compare' ) || version_compare( PHP_VERSION, '5.3.2' ) < 0 ) {
+       // We need to use dirname( __FILE__ ) here cause __DIR__ is PHP5.3+
+       require dirname( dirname( __FILE__ ) ) . '/includes/PHPVersionError.php';
+       wfPHPVersionError( 'mw-config/index.php' );
+}
+
 define( 'MW_CONFIG_CALLBACK', 'Installer::overrideConfig' );
 define( 'MEDIAWIKI_INSTALL', true );
 
index 4f854fb..7f80832 100644 (file)
@@ -865,6 +865,7 @@ return array(
                'dependencies' => array(
                        'jquery.hidpi',
                ),
+               'skipFunction' => 'resources/src/mediawiki.hidpi-skip.js',
                'targets' => array( 'desktop', 'mobile' ),
        ),
        'mediawiki.hlist' => array(
@@ -1442,18 +1443,16 @@ return array(
        /* MediaWiki UI */
 
        'mediawiki.ui' => array(
-               'skinStyles' => array(
-                       'default' => 'resources/src/mediawiki.ui/default.less',
-                       'vector' => 'resources/src/mediawiki.ui/vector.less',
+               'styles' => array(
+                       'resources/src/mediawiki.ui/default.less',
                ),
                'position' => 'top',
                'targets' => array( 'desktop', 'mobile' ),
        ),
        // Lightweight module for button styles
        'mediawiki.ui.button' => array(
-               'skinStyles' => array(
-                       'default' => 'resources/src/mediawiki.ui/components/default/buttons.less',
-                       'vector' => 'resources/src/mediawiki.ui/components/vector/buttons.less',
+               'styles' => array(
+                       'resources/src/mediawiki.ui/components/buttons.less',
                ),
                'position' => 'top',
                'targets' => array( 'desktop', 'mobile' ),
index 02048c0..4595994 100644 (file)
@@ -366,7 +366,6 @@ var spliceWorksWithEmptyObject = (function () {
     ArrayPrototype.splice.call(obj, 0, 0, 1);
     return obj.length === 1;
 }());
-var omittingSecondSpliceArgIsNoop = [1].splice(0).length === 0;
 defineProperties(ArrayPrototype, {
     splice: function splice(start, deleteCount) {
         if (arguments.length === 0) { return []; }
@@ -375,14 +374,14 @@ defineProperties(ArrayPrototype, {
         if (arguments.length > 0 && typeof deleteCount !== 'number') {
             args = _Array_slice_.call(arguments);
             if (args.length < 2) {
-                args.push(toInteger(deleteCount));
+                args.push(this.length - start);
             } else {
                 args[1] = toInteger(deleteCount);
             }
         }
         return array_splice.apply(this, args);
     }
-}, !omittingSecondSpliceArgIsNoop || !spliceWorksWithEmptyObject);
+}, !spliceWorksWithEmptyObject);
 
 // ES5 15.4.4.12
 // http://es5.github.com/#x15.4.4.13
@@ -674,7 +673,7 @@ defineProperties(ArrayPrototype, {
 // ES5 15.4.4.14
 // http://es5.github.com/#x15.4.4.14
 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
-var hasFirefox2IndexOfBug = [0, 1].indexOf(1, 2) !== -1;
+var hasFirefox2IndexOfBug = Array.prototype.indexOf && [0, 1].indexOf(1, 2) !== -1;
 defineProperties(ArrayPrototype, {
     indexOf: function indexOf(sought /*, fromIndex */ ) {
         var self = splitString && isString(this) ? this.split('') : toObject(this),
@@ -703,7 +702,7 @@ defineProperties(ArrayPrototype, {
 // ES5 15.4.4.15
 // http://es5.github.com/#x15.4.4.15
 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf
-var hasFirefox2LastIndexOfBug = [0, 1].lastIndexOf(0, -3) !== -1;
+var hasFirefox2LastIndexOfBug = Array.prototype.lastIndexOf && [0, 1].lastIndexOf(0, -3) !== -1;
 defineProperties(ArrayPrototype, {
     lastIndexOf: function lastIndexOf(sought /*, fromIndex */) {
         var self = splitString && isString(this) ? this.split('') : toObject(this),
@@ -1082,7 +1081,7 @@ if (!Date.now) {
 // http://es5.github.com/#x15.7.4.5
 var hasToFixedBugs = NumberPrototype.toFixed && (
   (0.00008).toFixed(3) !== '0.000'
-  || (0.9).toFixed(0) === '0'
+  || (0.9).toFixed(0) !== '1'
   || (1.255).toFixed(2) !== '1.25'
   || (1000000000000000128).toFixed(0) !== "1000000000000000128"
 );
index 6d5974a..95001fb 100644 (file)
@@ -1,5 +1,6 @@
+/*jshint eqnull:true */
 /*!
- * jQuery Cookie Plugin
+ * jQuery Cookie Plugin v1.2
  * https://github.com/carhartl/jquery-cookie
  *
  * Copyright 2011, Klaus Hartl
@@ -7,41 +8,65 @@
  * http://www.opensource.org/licenses/mit-license.php
  * http://www.opensource.org/licenses/GPL-2.0
  */
-(function($) {
-    $.cookie = function(key, value, options) {
-
-        // key and at least value given, set cookie...
-        if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value === null || value === undefined)) {
-            options = $.extend({}, options);
-
-            if (value === null || value === undefined) {
-                options.expires = -1;
-            }
-
-            if (typeof options.expires === 'number') {
-                var days = options.expires, t = options.expires = new Date();
-                t.setDate(t.getDate() + days);
-            }
-
-            value = String(value);
-
-            return (document.cookie = [
-                encodeURIComponent(key), '=', options.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(''));
-        }
-
-        // key and possibly options given, get cookie...
-        options = value || {};
-        var decode = options.raw ? function(s) { return s; } : decodeURIComponent;
-
-        var pairs = document.cookie.split('; ');
-        for (var i = 0, pair; pair = pairs[i] && pairs[i].split('='); i++) {
-            if (decode(pair[0]) === key) return decode(pair[1] || ''); // IE saves cookies with empty string as "c; ", e.g. without "=" as opposed to EOMB, thus pair[1] may be undefined
-        }
-        return null;
-    };
-})(jQuery);
+(function ($, document, undefined) {
+
+       var pluses = /\+/g;
+
+       function raw(s) {
+               return s;
+       }
+
+       function decoded(s) {
+               return decodeURIComponent(s.replace(pluses, ' '));
+       }
+
+       $.cookie = function (key, value, options) {
+
+               // key and at least value given, set cookie...
+               if (value !== undefined && !/Object/.test(Object.prototype.toString.call(value))) {
+                       options = $.extend({}, $.cookie.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 = String(value);
+
+                       return (document.cookie = [
+                               encodeURIComponent(key), '=', options.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(''));
+               }
+
+               // key and possibly options given, get cookie...
+               options = value || $.cookie.defaults || {};
+               var decode = options.raw ? raw : decoded;
+               var cookies = document.cookie.split('; ');
+               for (var i = 0, parts; (parts = cookies[i] && cookies[i].split('=')); i++) {
+                       if (decode(parts.shift()) === key) {
+                               return decode(parts.join('='));
+                       }
+               }
+
+               return null;
+       };
+
+       $.cookie.defaults = {};
+
+       $.removeCookie = function (key, options) {
+               if ($.cookie(key, options) !== null) {
+                       $.cookie(key, null, options);
+                       return true;
+               }
+               return false;
+       };
+
+})(jQuery, document);
index 34f8972..eb13f59 100644 (file)
@@ -8,11 +8,12 @@
                        "Mido",
                        "OsamaK",
                        "زكريا",
-                       "مشعل الحربي"
+                       "مشعل الحربي",
+                       "ترجمان05"
                ]
        },
-       "ooui-dialog-action-close": "أغلق",
        "ooui-outline-control-move-down": "انقل العنصر للأسفل",
        "ooui-outline-control-move-up": "انقل العنصر للأعلى",
-       "ooui-toolbar-more": "مزيد"
+       "ooui-toolbar-more": "مزيد",
+       "ooui-dialog-process-retry": "حاول مرة أخرى"
 }
index 3ff9763..4555c11 100644 (file)
@@ -7,11 +7,12 @@
                        "Pginer",
                        "QuimGil",
                        "SMP",
-                       "Vriullop"
+                       "Vriullop",
+                       "Toniher"
                ]
        },
-       "ooui-dialog-action-close": "Tanca",
        "ooui-outline-control-move-down": "Baixa element",
        "ooui-outline-control-move-up": "Puja element",
-       "ooui-toolbar-more": "Més"
+       "ooui-toolbar-more": "Més",
+       "ooui-dialog-process-dismiss": "Descarta"
 }
index ca6d5b4..a75cf0b 100644 (file)
                        "ශ්වෙත"
                ]
        },
-       "ooui-dialog-action-close": "Zavřít",
        "ooui-outline-control-move-down": "Přesunout položku dolů",
        "ooui-outline-control-move-up": "Přesunout položku nahoru",
        "ooui-outline-control-remove": "Odstranit položku",
        "ooui-toolbar-more": "Další",
-       "ooui-dialog-confirm-title": "Potvrzení",
-       "ooui-dialog-confirm-default-prompt": "Opravdu?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Storno"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Storno",
+       "ooui-dialog-process-error": "Něco se pokazilo",
+       "ooui-dialog-process-dismiss": "Zavřít",
+       "ooui-dialog-process-retry": "Zkusit znovu"
 }
index 97ed48c..546689b 100644 (file)
                        "Tomabrafix"
                ]
        },
-       "ooui-dialog-action-close": "Schließen",
        "ooui-outline-control-move-down": "Element nach unten verschieben",
        "ooui-outline-control-move-up": "Element nach oben verschieben",
        "ooui-outline-control-remove": "Element entfernen",
        "ooui-toolbar-more": "Mehr",
-       "ooui-dialog-confirm-title": "Bestätigen",
-       "ooui-dialog-confirm-default-prompt": "Bist du sicher?",
-       "ooui-dialog-confirm-default-ok": "Okay",
-       "ooui-dialog-confirm-default-cancel": "Abbrechen"
+       "ooui-dialog-message-accept": "Okay",
+       "ooui-dialog-message-reject": "Abbrechen",
+       "ooui-dialog-process-error": "Etwas ist schief gelaufen",
+       "ooui-dialog-process-dismiss": "Ausblenden",
+       "ooui-dialog-process-retry": "Erneut versuchen"
 }
index 2498a76..602efc8 100644 (file)
             "Amir E. Aharoni"
         ]
     },
-    "ooui-dialog-action-close": "Close",
     "ooui-outline-control-move-down": "Move item down",
     "ooui-outline-control-move-up": "Move item up",
     "ooui-outline-control-remove": "Remove item",
     "ooui-toolbar-more": "More",
-    "ooui-dialog-confirm-title": "Confirm",
-    "ooui-dialog-confirm-default-prompt": "Are you sure?",
-    "ooui-dialog-confirm-default-ok": "OK",
-    "ooui-dialog-confirm-default-cancel": "Cancel"
+    "ooui-dialog-message-accept": "OK",
+    "ooui-dialog-message-reject": "Cancel",
+    "ooui-dialog-process-error": "Something went wrong",
+    "ooui-dialog-process-dismiss": "Dismiss",
+    "ooui-dialog-process-retry": "Try again"
 }
index 76485ea..805897d 100644 (file)
                        "Gloria sah"
                ]
        },
-       "ooui-dialog-action-close": "Cerrar",
        "ooui-outline-control-move-down": "Bajar elemento",
        "ooui-outline-control-move-up": "Subir elemento",
        "ooui-outline-control-remove": "Eliminar elemento",
        "ooui-toolbar-more": "Más",
-       "ooui-dialog-confirm-title": "Confirmar",
-       "ooui-dialog-confirm-default-prompt": "¿Está seguro?",
-       "ooui-dialog-confirm-default-ok": "Aceptar",
-       "ooui-dialog-confirm-default-cancel": "Cancelar"
+       "ooui-dialog-message-accept": "Aceptar",
+       "ooui-dialog-message-reject": "Cancelar",
+       "ooui-dialog-process-retry": "Intentar de nuevo"
 }
index 164685c..ac3af74 100644 (file)
@@ -5,13 +5,13 @@
                        "Pikne"
                ]
        },
-       "ooui-dialog-action-close": "Sule",
        "ooui-outline-control-move-down": "Liiguta üksust allapoole",
        "ooui-outline-control-move-up": "Liiguta üksust ülespoole",
        "ooui-outline-control-remove": "Eemalda üksus",
        "ooui-toolbar-more": "Veel",
-       "ooui-dialog-confirm-title": "Kinnitus",
-       "ooui-dialog-confirm-default-prompt": "Kas oled kindel?",
-       "ooui-dialog-confirm-default-ok": "Sobib",
-       "ooui-dialog-confirm-default-cancel": "Loobu"
+       "ooui-dialog-message-accept": "Sobib",
+       "ooui-dialog-message-reject": "Loobu",
+       "ooui-dialog-process-error": "Midagi läks valesti",
+       "ooui-dialog-process-dismiss": "Hülga",
+       "ooui-dialog-process-retry": "Proovi uuesti"
 }
index ec051ac..b0ec803 100644 (file)
                        "Armin1392"
                ]
        },
-       "ooui-dialog-action-close": "بستن",
        "ooui-outline-control-move-down": "انتقال مورد به پایین",
        "ooui-outline-control-move-up": "انتقال مورد به بالا",
        "ooui-outline-control-remove": "حذف مورد",
        "ooui-toolbar-more": "بیشتر",
-       "ooui-dialog-confirm-title": "تأیید",
-       "ooui-dialog-confirm-default-prompt": "آیا مطمئن هستید؟",
-       "ooui-dialog-confirm-default-ok": "تأیید",
-       "ooui-dialog-confirm-default-cancel": "لغو"
+       "ooui-dialog-message-accept": "تأیید",
+       "ooui-dialog-message-reject": "لغو",
+       "ooui-dialog-process-error": "مشکلی وجود دارد",
+       "ooui-dialog-process-dismiss": "نپذیرفتن",
+       "ooui-dialog-process-retry": "دوباره امتحان کن"
 }
index 8e8b81e..efaabed 100644 (file)
                        "VezonThunder"
                ]
        },
-       "ooui-dialog-action-close": "Sulje",
        "ooui-outline-control-move-down": "Siirrä kohdetta alaspäin",
        "ooui-outline-control-move-up": "Siirrä kohdetta ylöspäin",
        "ooui-outline-control-remove": "Poista kohde",
        "ooui-toolbar-more": "Lisää",
-       "ooui-dialog-confirm-title": "Vahvista",
-       "ooui-dialog-confirm-default-prompt": "Oletko varma?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Peruuta"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Peruuta",
+       "ooui-dialog-process-error": "Jokin meni pieleen",
+       "ooui-dialog-process-dismiss": "Hylkää",
+       "ooui-dialog-process-retry": "Yritä uudelleen"
 }
index 6b8871a..8ff5475 100644 (file)
                        "Trizek",
                        "Urhixidur",
                        "Verdy p",
-                       "Wyz"
+                       "Wyz",
+                       "SnowedEarth"
                ]
        },
-       "ooui-dialog-action-close": "Fermer",
        "ooui-outline-control-move-down": "Faire descendre l’élément",
        "ooui-outline-control-move-up": "Faire monter l’élément",
        "ooui-outline-control-remove": "Supprimer l’élément",
        "ooui-toolbar-more": "Plus",
-       "ooui-dialog-confirm-title": "Confirmer",
-       "ooui-dialog-confirm-default-prompt": "Êtes-vous sûr ?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Annuler"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Annuler",
+       "ooui-dialog-process-error": "Quelque chose a mal tourné",
+       "ooui-dialog-process-dismiss": "Rejeter",
+       "ooui-dialog-process-retry": "Réessayez"
 }
diff --git a/resources/lib/oojs-ui/i18n/gd.json b/resources/lib/oojs-ui/i18n/gd.json
new file mode 100644 (file)
index 0000000..6a83c9c
--- /dev/null
@@ -0,0 +1,13 @@
+{
+       "@metadata": {
+               "authors": [
+                       "GunChleoc"
+               ]
+       },
+       "ooui-outline-control-move-down": "Gluais nì sìos",
+       "ooui-outline-control-move-up": "Gluais nì suas",
+       "ooui-outline-control-remove": "Thoir air falbh an nì",
+       "ooui-toolbar-more": "Barrachd",
+       "ooui-dialog-message-accept": "Ceart ma-thà",
+       "ooui-dialog-message-reject": "Sguir dheth"
+}
index a4b6787..eac992f 100644 (file)
@@ -6,13 +6,13 @@
                        "Toliño"
                ]
        },
-       "ooui-dialog-action-close": "Pechar",
        "ooui-outline-control-move-down": "Mover o elemento abaixo",
        "ooui-outline-control-move-up": "Mover o elemento arriba",
        "ooui-outline-control-remove": "Eliminar o elemento",
        "ooui-toolbar-more": "Máis",
-       "ooui-dialog-confirm-title": "Confirmar",
-       "ooui-dialog-confirm-default-prompt": "Está seguro?",
-       "ooui-dialog-confirm-default-ok": "Aceptar",
-       "ooui-dialog-confirm-default-cancel": "Cancelar"
+       "ooui-dialog-message-accept": "Aceptar",
+       "ooui-dialog-message-reject": "Cancelar",
+       "ooui-dialog-process-error": "Algo foi mal",
+       "ooui-dialog-process-dismiss": "Agochar",
+       "ooui-dialog-process-retry": "Inténteo de novo"
 }
index 26660f9..bbaf4c1 100644 (file)
                        "קיפודנחש"
                ]
        },
-       "ooui-dialog-action-close": "סגירה",
        "ooui-outline-control-move-down": "להזיז את הפריט מטה",
        "ooui-outline-control-move-up": "להזיז את הפריט מעלה",
        "ooui-outline-control-remove": "להסיר את הפריט",
        "ooui-toolbar-more": "עוד",
-       "ooui-dialog-confirm-title": "אישור",
-       "ooui-dialog-confirm-default-prompt": "באמת?",
-       "ooui-dialog-confirm-default-ok": "אישור",
-       "ooui-dialog-confirm-default-cancel": "ביטול"
+       "ooui-dialog-message-accept": "אישור",
+       "ooui-dialog-message-reject": "ביטול",
+       "ooui-dialog-process-error": "משהו השתבש",
+       "ooui-dialog-process-dismiss": "לוותר",
+       "ooui-dialog-process-retry": "לנסות שוב"
 }
index 0f423b3..6069625 100644 (file)
@@ -5,15 +5,14 @@
                        "Einstein2",
                        "Misibacsi",
                        "ViDam",
-                       "Tacsipacsi"
+                       "Tacsipacsi",
+                       "Csega"
                ]
        },
-       "ooui-dialog-action-close": "Bezár",
        "ooui-outline-control-move-down": "Elem mozgatása lefelé",
        "ooui-outline-control-move-up": "Elem mozgatása felfelé",
        "ooui-outline-control-remove": "Elem eltávolítása",
        "ooui-toolbar-more": "Tovább...",
-       "ooui-dialog-confirm-title": "Megerősítés",
-       "ooui-dialog-confirm-default-prompt": "Biztos vagy benne?",
-       "ooui-dialog-confirm-default-cancel": "Mégse"
+       "ooui-dialog-message-reject": "Mégse",
+       "ooui-dialog-process-retry": "Próbáld újra"
 }
index f1c9ced..b374b6f 100644 (file)
@@ -4,13 +4,13 @@
                        "McDutchie"
                ]
        },
-       "ooui-dialog-action-close": "Clauder",
        "ooui-outline-control-move-down": "Displaciar elemento in basso",
        "ooui-outline-control-move-up": "Displaciar elemento in alto",
        "ooui-outline-control-remove": "Remover elemento",
        "ooui-toolbar-more": "Plus",
-       "ooui-dialog-confirm-title": "Confirmation",
-       "ooui-dialog-confirm-default-prompt": "Es tu secur?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Cancellar"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Cancellar",
+       "ooui-dialog-process-error": "Qualcosa ha vadite mal",
+       "ooui-dialog-process-dismiss": "Clauder",
+       "ooui-dialog-process-retry": "Reprobar"
 }
index 162fa8c..3d4e049 100644 (file)
                        "Ontsed"
                ]
        },
-       "ooui-dialog-action-close": "Chiudi",
        "ooui-outline-control-move-down": "Sposta in basso",
        "ooui-outline-control-move-up": "Sposta in alto",
        "ooui-outline-control-remove": "Rimuovi elemento",
        "ooui-toolbar-more": "Altro",
-       "ooui-dialog-confirm-title": "Conferma",
-       "ooui-dialog-confirm-default-prompt": "Sei sicuro?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Annulla"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Annulla",
+       "ooui-dialog-process-error": "Qualcosa è andato storto",
+       "ooui-dialog-process-dismiss": "Nascondi",
+       "ooui-dialog-process-retry": "Riprova"
 }
index e2e12ab..1cbcb8a 100644 (file)
                        "Викиней"
                ]
        },
-       "ooui-dialog-action-close": "Zoumaachen",
        "ooui-outline-control-move-down": "Element erof réckelen",
        "ooui-outline-control-move-up": "Element erop réckelen",
        "ooui-outline-control-remove": "Element ewechhuelen",
        "ooui-toolbar-more": "Méi",
-       "ooui-dialog-confirm-title": "Confirméieren",
-       "ooui-dialog-confirm-default-prompt": "Sidd Dir sécher?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Ofbriechen"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Ofbriechen",
+       "ooui-dialog-process-error": "Et ass eppes schif gaang",
+       "ooui-dialog-process-dismiss": "Verwerfen",
+       "ooui-dialog-process-retry": "Nach eng Kéier probéieren"
 }
index 7ad74dc..32fc9fe 100644 (file)
@@ -8,12 +8,10 @@
                        "PeterisP"
                ]
        },
-       "ooui-dialog-action-close": "Aizvērt",
        "ooui-outline-control-move-down": "Pārvietot vienumu uz leju",
        "ooui-outline-control-move-up": "Pārvietot vienumu uz augšu",
        "ooui-toolbar-more": "Vairāk",
-       "ooui-dialog-confirm-title": "Apstiprināt",
-       "ooui-dialog-confirm-default-prompt": "Vai esat pārliecināts?",
-       "ooui-dialog-confirm-default-ok": "Labi",
-       "ooui-dialog-confirm-default-cancel": "Atcelt"
+       "ooui-dialog-message-accept": "Labi",
+       "ooui-dialog-message-reject": "Atcelt",
+       "ooui-dialog-process-retry": "Mēģināt vēlreiz"
 }
index 90685ea..d628034 100644 (file)
@@ -6,13 +6,13 @@
                        "Iwan Novirion"
                ]
        },
-       "ooui-dialog-action-close": "Затвори",
        "ooui-outline-control-move-down": "Помести надолу",
        "ooui-outline-control-move-up": "Помести нагоре",
        "ooui-outline-control-remove": "Отстрани ставка",
        "ooui-toolbar-more": "Повеќе",
-       "ooui-dialog-confirm-title": "Потврди",
-       "ooui-dialog-confirm-default-prompt": "Дали сте сигурни?",
-       "ooui-dialog-confirm-default-ok": "ОК",
-       "ooui-dialog-confirm-default-cancel": "Откажи"
+       "ooui-dialog-message-accept": "ОК",
+       "ooui-dialog-message-reject": "Откажи",
+       "ooui-dialog-process-error": "Нешто не е во ред",
+       "ooui-dialog-process-dismiss": "Тргни",
+       "ooui-dialog-process-retry": "Обиди се пак"
 }
index bea0c3a..7978673 100644 (file)
                        "Andrzej aa"
                ]
        },
-       "ooui-dialog-action-close": "Zamknij",
        "ooui-outline-control-move-down": "Przenieś niżej",
        "ooui-outline-control-move-up": "Przenieś wyżej",
        "ooui-outline-control-remove": "Usuń element",
        "ooui-toolbar-more": "Więcej",
-       "ooui-dialog-confirm-title": "Potwierdź",
-       "ooui-dialog-confirm-default-prompt": "Jesteś pewien?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Anuluj"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Anuluj",
+       "ooui-dialog-process-error": "Coś poszło nie tak",
+       "ooui-dialog-process-dismiss": "Ukryj",
+       "ooui-dialog-process-retry": "Spróbuj ponownie"
 }
index e9ad6de..5cb3e3d 100644 (file)
                        "SandroHc"
                ]
        },
-       "ooui-dialog-action-close": "Fechar",
        "ooui-outline-control-move-down": "Mover item para baixo",
        "ooui-outline-control-move-up": "Mover item para cima",
        "ooui-outline-control-remove": "Remover elemento",
        "ooui-toolbar-more": "Mais",
-       "ooui-dialog-confirm-title": "Confirmar",
-       "ooui-dialog-confirm-default-prompt": "Tem a certeza?",
-       "ooui-dialog-confirm-default-ok": "Aceitar",
-       "ooui-dialog-confirm-default-cancel": "Cancelar"
+       "ooui-dialog-message-accept": "Aceitar",
+       "ooui-dialog-message-reject": "Cancelar",
+       "ooui-dialog-process-error": "Algo correu mal",
+       "ooui-dialog-process-dismiss": "Ignorar",
+       "ooui-dialog-process-retry": "Tentar novamente"
 }
index 87198e5..e8ab9f9 100644 (file)
                        "Sayak Sarkar",
                        "Shirayuki",
                        "Siebrand",
-                       "Trevor Parscal"
+                       "Trevor Parscal",
+                       "Liuxinyu970226"
                ]
        },
-       "ooui-dialog-action-close": "Label text for button to exit from dialog.\n\n{{Identical|Close}}",
        "ooui-outline-control-move-down": "Tool tip for a button that moves items in a list down one place",
        "ooui-outline-control-move-up": "Tool tip for a button that moves items in a list up one place",
        "ooui-outline-control-remove": "Tool tip for a button that removes items from a list.\n{{Identical|Remove item}}",
        "ooui-toolbar-more": "Label for the toolbar group that contains a list of all other available tools.\n{{Identical|More}}",
-       "ooui-dialog-confirm-title": "Title of the generic dialog used to confirm things.\n{{Identical|Confirm}}",
-       "ooui-dialog-confirm-default-prompt": "The default prompt of a confirmation dialog.\n{{Identical|Are you sure?}}",
-       "ooui-dialog-confirm-default-ok": "The default OK button text on a confirmation dialog.\n{{Identical|OK}}",
-       "ooui-dialog-confirm-default-cancel": "The default cancel button text on a confirmation dialog.\n{{Identical|Cancel}}"
+       "ooui-dialog-message-accept": "Default label for the accept button of a message dialog",
+       "ooui-dialog-message-reject": "Default label for the reject button of a message dialog",
+       "ooui-dialog-process-error": "Title for process dialog error description",
+       "ooui-dialog-process-dismiss": "Label for process dialog dismiss error button, visible when describing errors\n{{Identical|Dismiss}}",
+       "ooui-dialog-process-retry": "Label for process dialog retry action button, visible when describing recoverable errors\n{{Identical|Try again}}"
 }
index 0181514..06e0f1d 100644 (file)
@@ -8,13 +8,13 @@
                        "Gloria sah"
                ]
        },
-       "ooui-dialog-action-close": "Închide",
        "ooui-outline-control-move-down": "Mută elementul mai jos",
        "ooui-outline-control-move-up": "Mută elementul mai sus",
        "ooui-outline-control-remove": "Elimină elementul",
        "ooui-toolbar-more": "Mai mult",
-       "ooui-dialog-confirm-title": "Confirmare",
-       "ooui-dialog-confirm-default-prompt": "Sunteți sigur(ă)?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Revocare"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Revocare",
+       "ooui-dialog-process-error": "Ceva nu a funcționat",
+       "ooui-dialog-process-dismiss": "Renunțare",
+       "ooui-dialog-process-retry": "Reîncearcă"
 }
index 435f20c..efd1062 100644 (file)
                        "Умар"
                ]
        },
-       "ooui-dialog-action-close": "Закрыть",
        "ooui-outline-control-move-down": "Переместить элемент вниз",
        "ooui-outline-control-move-up": "Переместить элемент вверх",
        "ooui-outline-control-remove": "Удалить пункт",
        "ooui-toolbar-more": "Ещё",
-       "ooui-dialog-confirm-title": "Подтвердить",
-       "ooui-dialog-confirm-default-prompt": "Вы уверены?",
-       "ooui-dialog-confirm-default-ok": "ОК",
-       "ooui-dialog-confirm-default-cancel": "Отмена"
+       "ooui-dialog-message-accept": "ОК",
+       "ooui-dialog-message-reject": "Отмена",
+       "ooui-dialog-process-error": "Что-то пошло не так",
+       "ooui-dialog-process-dismiss": "Закрыть",
+       "ooui-dialog-process-retry": "Попробовать ещё раз"
 }
index 44dfd60..ec18019 100644 (file)
@@ -4,16 +4,16 @@
                        "Euriditi",
                        "Kushtrim",
                        "Elioqoshi",
-                       "GretaDoci"
+                       "GretaDoci",
+                       "Gertakapllani"
                ]
        },
-       "ooui-dialog-action-close": "Mbylle",
        "ooui-outline-control-move-down": "Zhvendose artikullin më poshtë",
        "ooui-outline-control-move-up": "Zhvendose artikullin më lart",
        "ooui-outline-control-remove": "Hiq artikullin",
        "ooui-toolbar-more": "Më tepër...",
-       "ooui-dialog-confirm-title": "Konfirmo",
-       "ooui-dialog-confirm-default-prompt": "A jeni i sigurt?",
-       "ooui-dialog-confirm-default-ok": "Në rregull",
-       "ooui-dialog-confirm-default-cancel": "Anullo"
+       "ooui-dialog-message-accept": "Në rregull",
+       "ooui-dialog-message-reject": "Anullo",
+       "ooui-dialog-process-error": "Diçka shkoi keq",
+       "ooui-dialog-process-retry": "Provo përsëri"
 }
index 308ed84..d653356 100644 (file)
@@ -6,13 +6,13 @@
                        "Милан Јелисавчић"
                ]
        },
-       "ooui-dialog-action-close": "Затвори",
        "ooui-outline-control-move-down": "Премести ставку на доле",
        "ooui-outline-control-move-up": "Премести ставку на горе",
        "ooui-outline-control-remove": "Уклони ставку",
        "ooui-toolbar-more": "Више",
-       "ooui-dialog-confirm-title": "Потврди",
-       "ooui-dialog-confirm-default-prompt": "Јесте ли сигурни?",
-       "ooui-dialog-confirm-default-ok": "У реду",
-       "ooui-dialog-confirm-default-cancel": "Откажи"
+       "ooui-dialog-message-accept": "У реду",
+       "ooui-dialog-message-reject": "Откажи",
+       "ooui-dialog-process-error": "Нешто је пошло наопако",
+       "ooui-dialog-process-dismiss": "Одбаци",
+       "ooui-dialog-process-retry": "Покушај поново"
 }
index fbd03de..40305d0 100644 (file)
                        "Lokal Profil"
                ]
        },
-       "ooui-dialog-action-close": "Stäng",
        "ooui-outline-control-move-down": "Flytta ned objekt",
        "ooui-outline-control-move-up": "Flytta upp objekt",
        "ooui-outline-control-remove": "Ta bort objekt",
        "ooui-toolbar-more": "Mer",
-       "ooui-dialog-confirm-title": "Bekräfta",
-       "ooui-dialog-confirm-default-prompt": "Är du säker?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Avbryt"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Avbryt",
+       "ooui-dialog-process-error": "Något gick fel",
+       "ooui-dialog-process-dismiss": "Stäng",
+       "ooui-dialog-process-retry": "Försök igen"
 }
index 2bdac54..11aeed4 100644 (file)
@@ -21,5 +21,9 @@
        "ooui-outline-control-move-down": "Перемістити елемент униз",
        "ooui-outline-control-move-up": "Перемістити елемент вгору",
        "ooui-outline-control-remove": "Видалити елемент",
-       "ooui-toolbar-more": "Більше"
+       "ooui-toolbar-more": "Більше",
+       "ooui-dialog-confirm-title": "Підтвердити",
+       "ooui-dialog-confirm-default-prompt": "Ви впевнені?",
+       "ooui-dialog-confirm-default-ok": "Готово",
+       "ooui-dialog-confirm-default-cancel": "Скасувати"
 }
index 9cc4543..205cbe8 100644 (file)
@@ -6,13 +6,13 @@
                        "Minh Nguyen"
                ]
        },
-       "ooui-dialog-action-close": "Đóng",
        "ooui-outline-control-move-down": "Chuyển mục xuống",
        "ooui-outline-control-move-up": "Chuyển mục lên",
        "ooui-outline-control-remove": "Xóa khoản",
        "ooui-toolbar-more": "Thêm",
-       "ooui-dialog-confirm-title": "Xác nhận",
-       "ooui-dialog-confirm-default-prompt": "Bạn có chắc chắn?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Hủy bỏ"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Hủy bỏ",
+       "ooui-dialog-process-error": "Đã bị trục trặc",
+       "ooui-dialog-process-dismiss": "Bỏ qua",
+       "ooui-dialog-process-retry": "Thử lại"
 }
index 01a22d1..e26af70 100644 (file)
@@ -6,13 +6,12 @@
                        "十弌"
                ]
        },
-       "ooui-dialog-action-close": "שליסן",
        "ooui-outline-control-move-down": "רוקן עלעמענט אראפ",
        "ooui-outline-control-move-up": "רוקן עלעמענט ארויף",
        "ooui-outline-control-remove": "אַראָפנעמען איינס",
        "ooui-toolbar-more": "נאך",
-       "ooui-dialog-confirm-title": "באַשטעטיקן",
-       "ooui-dialog-confirm-default-prompt": "איר זענט זיכער?",
-       "ooui-dialog-confirm-default-ok": "יאָ",
-       "ooui-dialog-confirm-default-cancel": "אַנולירן"
+       "ooui-dialog-message-accept": "יאָ",
+       "ooui-dialog-message-reject": "אַנולירן",
+       "ooui-dialog-process-error": "עפעס איז דורכגעפאלן",
+       "ooui-dialog-process-retry": "פרובירט נאכאמאל"
 }
index 8d1c09f..50df67a 100644 (file)
                        "乌拉跨氪"
                ]
        },
-       "ooui-dialog-action-close": "关闭",
        "ooui-outline-control-move-down": "下移项",
        "ooui-outline-control-move-up": "上移项",
        "ooui-outline-control-remove": "删除项",
        "ooui-toolbar-more": "更多",
-       "ooui-dialog-confirm-title": "确认",
-       "ooui-dialog-confirm-default-prompt": "您确定吗?",
-       "ooui-dialog-confirm-default-ok": "好",
-       "ooui-dialog-confirm-default-cancel": "取消"
+       "ooui-dialog-message-accept": "好",
+       "ooui-dialog-message-reject": "取消",
+       "ooui-dialog-process-error": "发生一些错误",
+       "ooui-dialog-process-dismiss": "解除",
+       "ooui-dialog-process-retry": "重试"
 }
diff --git a/resources/lib/oojs-ui/images/anchor.svg b/resources/lib/oojs-ui/images/anchor.svg
new file mode 100644 (file)
index 0000000..417bc96
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+        width="15px" height="8px" viewBox="0 0 15 8" style="enable-background:new 0 0 15 8;" xml:space="preserve">
+<g id="anchor">
+       <polygon id="outline" style="fill-rule:evenodd;clip-rule:evenodd;fill:#808080;" points="7.609,2.499 2.096,8 13.125,8"/>
+       <polygon id="fill" style="fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;" points="7.609,3 2.598,8 12.622,8"/>
+</g>
+</svg>
diff --git a/resources/lib/oojs-ui/images/tail.svg b/resources/lib/oojs-ui/images/tail.svg
deleted file mode 100644 (file)
index 4df8bb2..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="iso-8859-1"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-        width="15px" height="8px" viewBox="0 0 15 8" style="enable-background:new 0 0 15 8;" xml:space="preserve">
-<g id="tail">
-       <polygon id="outline" style="fill-rule:evenodd;clip-rule:evenodd;fill:#808080;" points="7.609,2.499 2.096,8 13.125,8"/>
-       <polygon id="fill" style="fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;" points="7.609,3 2.598,8 12.622,8"/>
-</g>
-</svg>
index dc999cd..0dfa042 100644 (file)
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (97dbb50137)
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2014-07-03T02:33:09Z
+ * Date: 2014-07-16T22:59:48Z
  */
-.oo-ui-dialog-content .oo-ui-window-closeButton {
+.oo-ui-dialog-content > .oo-ui-window-head,
+.oo-ui-dialog-content > .oo-ui-window-body,
+.oo-ui-dialog-content > .oo-ui-window-foot {
   position: absolute;
-  top: 0;
+  right: 0;
   left: 0;
+  overflow: hidden;
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
 }
 
-.oo-ui-dialog-content .oo-ui-window-icon {
-  margin-left: 3.35em;
+.oo-ui-dialog-content > .oo-ui-window-head {
+  top: 0;
+  z-index: 1;
 }
 
-.oo-ui-dialog-content .oo-ui-window-body {
-  position: absolute;
-  top: 3.35em;
-  right: 0;
+.oo-ui-dialog-content > .oo-ui-window-body {
+  top: 0;
   bottom: 0;
-  left: 0;
-  overflow-y: auto;
+  z-index: 2;
 }
 
-.oo-ui-dialog-content .oo-ui-window-foot {
-  position: absolute;
-  top: 0;
-  right: 0;
-  height: 3.35em;
+.oo-ui-dialog-content > .oo-ui-window-foot {
+  bottom: 0;
+  z-index: 1;
 }
 
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-button {
-  height: 100%;
+.oo-ui-dialog-content > .oo-ui-window-overlay {
+  z-index: 3;
 }
 
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-button .oo-ui-labeledElement-label {
-  display: inline-block;
-  width: 0;
-  text-indent: -9999px;
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+  background-color: rgba(255, 255, 255, 0.5);
+  opacity: 0;
+  -webkit-transition: opacity 250ms ease-in-out;
+     -moz-transition: opacity 250ms ease-in-out;
+      -ms-transition: opacity 250ms ease-in-out;
+       -o-transition: opacity 250ms ease-in-out;
+          transition: opacity 250ms ease-in-out;
 }
 
-.oo-ui-dialog-medium .oo-ui-window-frame {
-  top: 0;
-  bottom: 0;
-  background-color: white;
+.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame {
+  top: 1em;
+  bottom: 1em;
+  background-color: #fff;
+  -webkit-transform: translate3d(0, -200%, 0);
+     -moz-transform: translate3d(0, -200%, 0);
+      -ms-transform: translate3d(0, -200%, 0);
+       -o-transform: translate3d(0, -200%, 0);
+          transform: translate3d(0, -200%, 0);
+  -webkit-transition: transform 250ms ease-in-out;
+     -moz-transition: transform 250ms ease-in-out;
+      -ms-transition: transform 250ms ease-in-out;
+       -o-transition: transform 250ms ease-in-out;
+          transition: transform 250ms ease-in-out;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready {
+  opacity: 1;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
+  -webkit-transform: translate3d(0, 0, 0);
+     -moz-transform: translate3d(0, 0, 0);
+      -ms-transform: translate3d(0, 0, 0);
+       -o-transform: translate3d(0, 0, 0);
+          transform: translate3d(0, 0, 0);
+}
+
+.oo-ui-windowManager-modal.oo-ui-windowManager-floating > .oo-ui-dialog > .oo-ui-window-frame {
+  border: solid 1px #ccc;
+  border-radius: 0.5em;
+  box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
+}
+
+.oo-ui-messageDialog-title,
+.oo-ui-messageDialog-message {
+  display: block;
+  padding-top: 0.5em;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-title {
+  font-size: 1.5em;
+  line-height: 1em;
+  color: #000;
+}
+
+.oo-ui-messageDialog-message {
+  font-size: 0.9em;
+  line-height: 1.25em;
+  color: #666;
+}
+
+.oo-ui-messageDialog-message-verbose {
+  font-size: 1.1em;
+  line-height: 1.5em;
+  text-align: left;
+}
+
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+  border-right: solid 1px #e5e5e5;
+}
+
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child {
+  border-right-width: 0;
+}
+
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+  border-bottom: solid 1px #e5e5e5;
+}
+
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child {
+  border-bottom-width: 0;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labeledElement-label {
+  padding: 0 2em;
+  line-height: 3.4em;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:hover {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:active {
+  background-color: rgba(0, 0, 0, 0.1);
 }
 
-.oo-ui-window-head {
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover {
+  background-color: rgba(8, 126, 204, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active {
+  background-color: rgba(8, 126, 204, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label {
+  font-weight: bold;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover {
+  background-color: rgba(118, 171, 54, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active {
+  background-color: rgba(118, 171, 54, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover {
+  background-color: rgba(212, 83, 83, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active {
+  background-color: rgba(212, 83, 83, 0.1);
+}
+
+.oo-ui-processDialog-content .oo-ui-window-head {
   height: 3.35em;
   border-bottom: 1px solid #dddddd;
   -webkit-box-sizing: border-box;
           box-sizing: border-box;
 }
 
-.oo-ui-window-body {
-  padding: 2em 3.35em;
+.oo-ui-processDialog-content .oo-ui-window-body {
+  top: 3.35em;
+  padding: 2em 0;
 }
 
-.oo-ui-window-icon {
-  width: 3.35em;
+.oo-ui-processDialog-navigation {
+  position: relative;
   height: 3.35em;
-  background-size: 2em auto;
-  border-left: 1px solid #dddddd;
+  padding: 0 1em;
+}
+
+.oo-ui-processDialog-location {
+  height: 3.35em;
+  padding: 0.25em 0;
+  text-align: center;
+  cursor: default;
+}
+
+.oo-ui-processDialog-title {
+  font-weight: bold;
+  line-height: 1.85em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-buttonedElement-button {
+  min-width: 1.85em;
+  min-height: 1.85em;
+  padding-top: 0.75em;
+  padding-bottom: 0.75em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-labeledElement-label {
+  padding: 0 1em;
+  line-height: 1.85em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-iconedElement-icon,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-iconedElement-icon,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-iconedElement-icon {
+  position: absolute;
+  margin-top: -0.125em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+  padding: 0;
+  vertical-align: middle;
+}
+
+.oo-ui-processDialog-actions-safe.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+  margin: 0.75em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:hover {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:active {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover {
+  background-color: rgba(8, 126, 204, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active {
+  background-color: rgba(8, 126, 204, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label {
+  font-weight: bold;
 }
 
-.oo-ui-window-title {
-  line-height: 3.35em;
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover {
+  background-color: rgba(118, 171, 54, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active {
+  background-color: rgba(118, 171, 54, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover {
+  background-color: rgba(212, 83, 83, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active {
+  background-color: rgba(212, 83, 83, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon {
+  left: 0.5em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label {
+  padding-left: 2.25em;
+}
+
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon {
+  right: 0.5em;
+}
+
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label {
+  padding-right: 2.25em;
+}
+
+.oo-ui-processDialog-actions-other:not(:empty) {
+  padding: 0.75em;
+}
+
+.oo-ui-processDialog-actions-other:not(:empty) .oo-ui-actionWidget {
+  margin: 0 0.75em 0 0;
+  border: solid 1px #ccc;
+  border-radius: 0.25em;
+}
+
+.oo-ui-processDialog > .oo-ui-window-frame {
+  min-height: 5em;
+}
+
+.oo-ui-processDialog-errors {
+  padding: 3em 3em 1.5em 3em;
+  text-align: center;
+  background-color: rgba(255, 255, 255, 0.9);
+}
+
+.oo-ui-processDialog-errors .oo-ui-buttonWidget {
+  margin: 2em 1em 2em 1em;
+}
+
+.oo-ui-processDialog-errors-title {
+  margin-bottom: 2em;
+  font-size: 1.5em;
+  color: #000;
+}
+
+.oo-ui-processDialog-error {
+  padding: 1em;
+  margin: 1em;
+  text-align: left;
+  background-color: #fff7f7;
+  border: solid 1px #ff9e9e;
+  border-radius: 0.25em;
 }
 
 .oo-ui-buttonedElement.oo-ui-indicatedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator,
   padding: 0;
 }
 
+.oo-ui-lookupWidget-menu {
+  background-color: #fff;
+}
+
 .oo-ui-menuItemWidget.oo-ui-optionWidget-selected {
   color: #ffffff;
   background: #347bff;
index 7018b52..7bc9be8 100644 (file)
@@ -1,53 +1,16 @@
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (97dbb50137)
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2014-07-03T02:33:09Z
+ * Date: 2014-07-16T22:59:48Z
  */
-.oo-ui-dialog {
-  background-color: #fff;
-  background-color: rgba(255, 255, 255, 0.5);
-  /* Opening and closing animation */
-
-  opacity: 0;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame {
-  -webkit-transform: scale(0.5);
-     -moz-transform: scale(0.5);
-      -ms-transform: scale(0.5);
-       -o-transform: scale(0.5);
-          transform: scale(0.5);
-}
-
-.oo-ui-dialog.oo-ui-window-setup,
-.oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
-  -webkit-transition: all 250ms ease-in-out;
-     -moz-transition: all 250ms ease-in-out;
-      -ms-transition: all 250ms ease-in-out;
-       -o-transition: all 250ms ease-in-out;
-          transition: all 250ms ease-in-out;
-}
-
-.oo-ui-dialog.oo-ui-window-ready {
-  opacity: 1;
-}
-
-.oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
-  -webkit-transform: scale(1);
-     -moz-transform: scale(1);
-      -ms-transform: scale(1);
-       -o-transform: scale(1);
-          transform: scale(1);
-}
-
-.oo-ui-dialog-content .oo-ui-window-head,
-.oo-ui-dialog-content .oo-ui-window-body,
-.oo-ui-dialog-content .oo-ui-window-foot {
+.oo-ui-dialog-content > .oo-ui-window-head,
+.oo-ui-dialog-content > .oo-ui-window-body,
+.oo-ui-dialog-content > .oo-ui-window-foot {
   position: absolute;
   right: 0;
   left: 0;
           box-sizing: border-box;
 }
 
-.oo-ui-dialog-content .oo-ui-window-head {
+.oo-ui-dialog-content .oo-ui-window-head {
   top: 0;
-  height: 3.8em;
-  padding: 0.5em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-title {
-  line-height: 2.8em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-icon {
-  width: 2.4em;
-  height: 2.8em;
-  line-height: 2.8em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-closeButton {
-  float: right;
-  margin: 0.25em 0.25em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-body {
-  top: 3.8em;
-  bottom: 4.8em;
-}
-
-.oo-ui-dialog-content-footless .oo-ui-window-body {
-  bottom: 0;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame {
-  top: 1em;
-  bottom: 1em;
-  background-color: #fff;
-  border: solid 1px #ccc;
-  border-radius: 0.5em;
-  box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
-}
-
-.oo-ui-dialog-small > .oo-ui-window-frame {
-  width: 400px;
-  max-height: 230px;
-}
-
-.oo-ui-dialog-medium > .oo-ui-window-frame {
-  width: 600px;
-  max-height: 460px;
-}
-
-.oo-ui-dialog-large > .oo-ui-window-frame {
-  width: 800px;
-  max-height: 690px;
-}
-
-.oo-ui-dialog-content .oo-ui-window-head,
-.oo-ui-dialog-content .oo-ui-window-foot {
   z-index: 1;
 }
 
-.oo-ui-dialog-content .oo-ui-window-body {
+.oo-ui-dialog-content > .oo-ui-window-body {
+  top: 0;
+  bottom: 0;
   z-index: 2;
   box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25);
 }
 
-.oo-ui-dialog-content .oo-ui-window-foot {
+.oo-ui-dialog-content .oo-ui-window-foot {
   bottom: 0;
-  height: 4.8em;
-  padding: 1em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed {
-  margin: 0.125em 0.25em;
+  z-index: 1;
 }
 
-.oo-ui-dialog-content .oo-ui-window-overlay {
+.oo-ui-dialog-content .oo-ui-window-overlay {
   z-index: 3;
 }
 
   color: #000;
 }
 
-.oo-ui-window-body {
-  padding: 0 0.75em;
+.oo-ui-window-content {
+  background: transparent;
 }
 
-.oo-ui-window-icon {
-  width: 2em;
-  height: 2em;
-  margin-right: 0.5em;
-  line-height: 2em;
+.oo-ui-window-overlay {
+  font-family: sans-serif;
+  font-size: 1em;
+  line-height: 1.5em;
 }
 
-.oo-ui-window-title {
-  line-height: 2em;
-  color: #333;
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+  background-color: rgba(255, 255, 255, 0.5);
+  opacity: 0;
+  -webkit-transition: opacity 250ms ease-in-out;
+     -moz-transition: opacity 250ms ease-in-out;
+      -ms-transition: opacity 250ms ease-in-out;
+       -o-transition: opacity 250ms ease-in-out;
+          transition: opacity 250ms ease-in-out;
 }
 
-.oo-ui-window-overlay {
-  font-family: sans-serif;
-  font-size: 1em;
+.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame {
+  top: 1em;
+  bottom: 1em;
+  background-color: #fff;
+  -webkit-transform: scale(0.5);
+     -moz-transform: scale(0.5);
+      -ms-transform: scale(0.5);
+       -o-transform: scale(0.5);
+          transform: scale(0.5);
+  -webkit-transition: all 250ms ease-in-out;
+     -moz-transition: all 250ms ease-in-out;
+      -ms-transition: all 250ms ease-in-out;
+       -o-transition: all 250ms ease-in-out;
+          transition: all 250ms ease-in-out;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready {
+  opacity: 1;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
+  -webkit-transform: scale(1);
+     -moz-transform: scale(1);
+      -ms-transform: scale(1);
+       -o-transform: scale(1);
+          transform: scale(1);
+}
+
+.oo-ui-windowManager-modal.oo-ui-windowManager-floating > .oo-ui-dialog > .oo-ui-window-frame {
+  border: solid 1px #ccc;
+  border-radius: 0.5em;
+  box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
+}
+
+.oo-ui-messageDialog-content .oo-ui-window-body {
+  box-shadow: 0 0 0.33em rgba(0, 0, 0, 0.33);
+}
+
+.oo-ui-messageDialog-title,
+.oo-ui-messageDialog-message {
+  display: block;
+  padding-top: 0.5em;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-title {
+  font-size: 1.5em;
+  line-height: 1em;
+  color: #000;
+}
+
+.oo-ui-messageDialog-message {
+  font-size: 0.9em;
+  line-height: 1.25em;
+  color: #666;
+}
+
+.oo-ui-messageDialog-message-verbose {
+  font-size: 1.1em;
   line-height: 1.5em;
+  text-align: left;
+}
+
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+  border-right: solid 1px #e5e5e5;
+}
+
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child {
+  border-right-width: 0;
+}
+
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+  border-bottom: solid 1px #e5e5e5;
+}
+
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child {
+  border-bottom-width: 0;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labeledElement-label {
+  padding: 0 2em;
+  line-height: 3.4em;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:hover {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:active {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover {
+  background-color: rgba(8, 126, 204, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active {
+  background-color: rgba(8, 126, 204, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label {
+  font-weight: bold;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover {
+  background-color: rgba(118, 171, 54, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active {
+  background-color: rgba(118, 171, 54, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover {
+  background-color: rgba(212, 83, 83, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active {
+  background-color: rgba(212, 83, 83, 0.1);
+}
+
+.oo-ui-processDialog-content .oo-ui-window-head {
+  height: 3.4em;
+}
+
+.oo-ui-processDialog-content .oo-ui-window-body {
+  top: 3.4em;
+  box-shadow: 0 0 0.33em rgba(0, 0, 0, 0.33);
+}
+
+.oo-ui-processDialog-navigation {
+  position: relative;
+  height: 3.4em;
+  padding: 0 1em;
+}
+
+.oo-ui-processDialog-location {
+  height: 1.9em;
+  padding: 0.75em 0;
+  text-align: center;
+  cursor: default;
+}
+
+.oo-ui-processDialog-title {
+  font-weight: bold;
+  line-height: 1.9em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-buttonedElement-button {
+  min-width: 1.9em;
+  min-height: 1.9em;
+  padding-top: 0.75em;
+  padding-bottom: 0.75em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-labeledElement-label {
+  padding: 0 1em;
+  line-height: 1.9em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-iconedElement-icon,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-iconedElement-icon,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-iconedElement-icon {
+  position: absolute;
+  margin-top: -0.125em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+  padding: 0;
+  vertical-align: middle;
+}
+
+.oo-ui-processDialog-actions-safe.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+  margin: 0.75em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:hover {
+  background-color: rgba(0, 0, 0, 0.05);
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button {
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:active {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover {
+  background-color: rgba(8, 126, 204, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active {
+  background-color: rgba(8, 126, 204, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label {
+  font-weight: bold;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover {
+  background-color: rgba(118, 171, 54, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active {
+  background-color: rgba(118, 171, 54, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover {
+  background-color: rgba(212, 83, 83, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active {
+  background-color: rgba(212, 83, 83, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon {
+  left: 0.5em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label {
+  padding-left: 2.25em;
+}
+
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon {
+  right: 0.5em;
+}
+
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label {
+  padding-right: 2.25em;
+}
+
+.oo-ui-processDialog-actions-other:not(:empty) {
+  padding: 0.75em;
+}
+
+.oo-ui-processDialog-actions-other:not(:empty) .oo-ui-actionWidget {
+  margin: 0 0.75em 0 0;
+}
+
+.oo-ui-processDialog > .oo-ui-window-frame {
+  min-height: 5em;
+}
+
+.oo-ui-processDialog-errors {
+  padding: 3em 3em 1.5em 3em;
+  text-align: center;
+  background-color: rgba(255, 255, 255, 0.9);
+}
+
+.oo-ui-processDialog-errors .oo-ui-buttonWidget {
+  margin: 2em 1em 2em 1em;
+}
+
+.oo-ui-processDialog-errors-title {
+  margin-bottom: 2em;
+  font-size: 1.5em;
+  color: #000;
+}
+
+.oo-ui-processDialog-error {
+  padding: 1em;
+  margin: 1em;
+  text-align: left;
+  background-color: #fff7f7;
+  border: solid 1px #ff9e9e;
+  border-radius: 0.25em;
+}
+
+.oo-ui-buttonedElement > .oo-ui-buttonedElement-button {
   color: #333;
 }
 
-.oo-ui-buttonedElement.oo-ui-indicatedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator,
-.oo-ui-buttonedElement.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement.oo-ui-indicatedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator,
+.oo-ui-buttonedElement.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   width: 1.9em;
   height: 1.9em;
   opacity: 0.8;
 }
 
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   /* Don't animate opacities for now, causes wiggling in Chrome (bug 63020) */
 
   /*.oo-ui-transition(opacity 200ms);*/
 
 }
 
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-iconedElement-icon,
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-iconedElement-icon,
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-iconedElement-icon {
   opacity: 1;
 }
 
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-labeledElement-label,
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-labeledElement-label,
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-labeledElement-label {
   color: #000;
 }
 
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   color: #333;
 }
 
-.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+  color: #087ecc;
+}
+
+.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+  color: #76ab36;
+}
+
+.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-destructive > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+  color: #d45353;
+}
+
+.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   opacity: 0.2;
 }
 
-.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   color: #ccc;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
   padding: 0.2em 0.8em;
   margin: 0.1em 0;
   text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
           transition: border-color 100ms ease-in-out;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:hover,
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:focus {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:focus {
   border-color: #aaa;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   color: black;
   background: #eeeeee;
   background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #dddddd), color-stop(100%, #ffffff));
   box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-framed.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   margin-right: -0.5em;
   margin-left: -0.5em;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-iconedElement.oo-ui-labeledElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-framed.oo-ui-iconedElement.oo-ui-labeledElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   margin-right: 0.3em;
   margin-left: -0.5em;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button {
   background: #cde7f4;
   background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #eaf4fa), color-stop(100%, #b0d9ee));
   background-image: -webkit-linear-gradient(top, #eaf4fa 0%, #b0d9ee 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#eaf4fa', endColorstr='#b0d9ee');
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:hover,
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:focus {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:focus {
   border-color: #9dc2d4;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   background: #cde7f4;
   background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #b0d9ee), color-stop(100%, #eaf4fa));
   background-image: -webkit-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#b0d9ee', endColorstr='#eaf4fa');
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button {
   background: #daf0be;
   background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #f0fbe1), color-stop(100%, #c3e59a));
   background-image: -webkit-linear-gradient(top, #f0fbe1 0%, #c3e59a 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f0fbe1', endColorstr='#c3e59a');
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:hover,
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:focus {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:focus {
   border-color: #adcb89;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   background: #daf0be;
   background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #c3e59a), color-stop(100%, #f0fbe1));
   background-image: -webkit-linear-gradient(top, #c3e59a 0%, #f0fbe1 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#c3e59a', endColorstr='#f0fbe1');
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive .oo-ui-buttonedElement-button {
   color: #d45353;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   color: #333;
   background: #eee;
   border-color: #ccc;
   box-shadow: none;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:hover,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:hover,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:hover,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:focus,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:focus,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:focus {
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:hover,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:hover,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:focus,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:focus,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:focus {
   border-color: #ccc;
   box-shadow: none;
 }
   font-size: 1.5em;
 }
 
-.oo-ui-panelLayout {
-  position: absolute;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-}
-
 .oo-ui-panelLayout-padded {
-  padding: 2em;
+  padding: 1.25em;
 }
 
 .oo-ui-barToolGroup .oo-ui-tool {
   box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2);
 }
 
-.oo-ui-popupWidget-tailed .oo-ui-popupWidget-tail {
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
   width: 15px;
   height: 8px;
   margin-left: -7px;
-  background-image: /* @embed */ url(images/tail.svg);
+  background-image: /* @embed */ url(images/anchor.svg);
 }
 
 .oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
index f2e3202..389fb9f 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (97dbb50137)
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2014-07-03T02:33:09Z
+ * Date: 2014-07-16T22:59:48Z
  */
 ( function ( OO ) {
 
@@ -94,7 +94,6 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
 };
 
 ( function () {
-
        /**
         * Message store for the default implementation of OO.ui.msg
         *
@@ -104,8 +103,6 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
         * @private
         */
        var messages = {
-               // Label text for button to exit from dialog
-               'ooui-dialog-action-close': 'Close',
                // Tool tip for a button that moves items in a list down one place
                'ooui-outline-control-move-down': 'Move item down',
                // Tool tip for a button that moves items in a list up one place
@@ -114,15 +111,16 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
                'ooui-outline-control-remove': 'Remove item',
                // Label for the toolbar group that contains a list of all other available tools
                'ooui-toolbar-more': 'More',
-
-               // Label for the generic dialog used to confirm things
-               'ooui-dialog-confirm-title': 'Confirm',
-               // The default prompt of a confirmation dialog
-               'ooui-dialog-confirm-default-prompt': 'Are you sure?',
-               // The default OK button text on a confirmation dialog
-               'ooui-dialog-confirm-default-ok': 'OK',
-               // The default cancel button text on a confirmation dialog
-               'ooui-dialog-confirm-default-cancel': 'Cancel'
+               // Default label for the accept button of a confirmation dialog
+               'ooui-dialog-message-accept': 'OK',
+               // Default label for the reject button of a confirmation dialog
+               'ooui-dialog-message-reject': 'Cancel',
+               // Title for process dialog error description
+               'ooui-dialog-process-error': 'Something went wrong',
+               // Label for process dialog dismiss error button, visible when describing errors
+               'ooui-dialog-process-dismiss': 'Dismiss',
+               // Label for process dialog retry action button, visible when describing recoverable errors
+               'ooui-dialog-process-retry': 'Try again'
        };
 
        /**
@@ -157,14 +155,30 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
                return message;
        };
 
-       /** */
-       OO.ui.deferMsg = function ( key ) {
+       /**
+        * Package a message and arguments for deferred resolution.
+        *
+        * Use this when you are statically specifying a message and the message may not yet be present.
+        *
+        * @param {string} key Message key
+        * @param {Mixed...} [params] Message parameters
+        * @return {Function} Function that returns the resolved message when executed
+        */
+       OO.ui.deferMsg = function () {
+               var args = arguments;
                return function () {
-                       return OO.ui.msg( key );
+                       return OO.ui.msg.apply( OO.ui, args );
                };
        };
 
-       /** */
+       /**
+        * Resolve a message.
+        *
+        * If the message is a function it will be executed, otherwise it will pass through directly.
+        *
+        * @param {Function|string} msg Deferred message, or message text
+        * @return {string} Resolved message
+        */
        OO.ui.resolveMsg = function ( msg ) {
                if ( $.isFunction( msg ) ) {
                        return msg();
@@ -174,6 +188,414 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
 
 } )();
 
+/**
+ * List of actions.
+ *
+ * @abstract
+ * @class
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ActionSet = function OoUiActionSet( config ) {
+       // Configuration intialization
+       config = config || {};
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.list = [];
+       this.categories = {
+               'actions': 'getAction',
+               'flags': 'getFlags',
+               'modes': 'getModes'
+       };
+       this.categorized = {};
+       this.special = {};
+       this.others = [];
+       this.organized = false;
+       this.changing = false;
+       this.changed = false;
+};
+
+/* Setup */
+
+OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
+
+/* Static Properties */
+
+/**
+ * Symbolic name of dialog.
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
+
+/* Events */
+
+/**
+ * @event click
+ * @param {OO.ui.ActionWidget} action Action that was clicked
+ */
+
+/**
+ * @event resize
+ * @param {OO.ui.ActionWidget} action Action that was resized
+ */
+
+/**
+ * @event add
+ * @param {OO.ui.ActionWidget[]} added Actions added
+ */
+
+/**
+ * @event remove
+ * @param {OO.ui.ActionWidget[]} added Actions removed
+ */
+
+/**
+ * @event change
+ */
+
+/* Methods */
+
+/**
+ * Handle action change events.
+ *
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.onActionChange = function () {
+       this.organized = false;
+       if ( this.changing ) {
+               this.changed = true;
+       } else {
+               this.emit( 'change' );
+       }
+};
+
+/**
+ * Check if a action is one of the special actions.
+ *
+ * @param {OO.ui.ActionWidget} action Action to check
+ * @return {boolean} Action is special
+ */
+OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
+       var flag;
+
+       for ( flag in this.special ) {
+               if ( action === this.special[flag] ) {
+                       return true;
+               }
+       }
+
+       return false;
+};
+
+/**
+ * Get actions.
+ *
+ * @param {Object} [filters] Filters to use, omit to get all actions
+ * @param {string|string[]} [filters.actions] Actions that actions must have
+ * @param {string|string[]} [filters.flags] Flags that actions must have
+ * @param {string|string[]} [filters.modes] Modes that actions must have
+ * @param {boolean} [filters.visible] Actions must be visible
+ * @param {boolean} [filters.disabled] Actions must be disabled
+ * @return {OO.ui.ActionWidget[]} Actions matching all criteria
+ */
+OO.ui.ActionSet.prototype.get = function ( filters ) {
+       var i, len, list, category, actions, index, match, matches;
+
+       if ( filters ) {
+               this.organize();
+
+               // Collect category candidates
+               matches = [];
+               for ( category in this.categorized ) {
+                       list = filters[category];
+                       if ( list ) {
+                               if ( !Array.isArray( list ) ) {
+                                       list = [ list ];
+                               }
+                               for ( i = 0, len = list.length; i < len; i++ ) {
+                                       actions = this.categorized[category][list[i]];
+                                       if ( Array.isArray( actions ) ) {
+                                               matches.push.apply( matches, actions );
+                                       }
+                               }
+                       }
+               }
+               // Remove by boolean filters
+               for ( i = 0, len = matches.length; i < len; i++ ) {
+                       match = matches[i];
+                       if (
+                               ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
+                               ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
+                       ) {
+                               matches.splice( i, 1 );
+                               len--;
+                               i--;
+                       }
+               }
+               // Remove duplicates
+               for ( i = 0, len = matches.length; i < len; i++ ) {
+                       match = matches[i];
+                       index = matches.lastIndexOf( match );
+                       while ( index !== i ) {
+                               matches.splice( index, 1 );
+                               len--;
+                               index = matches.lastIndexOf( match );
+                       }
+               }
+               return matches;
+       }
+       return this.list.slice();
+};
+
+/**
+ * Get special actions.
+ *
+ * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
+ * Special flags can be configured by changing #static-specialFlags in a subclass.
+ *
+ * @return {OO.ui.ActionWidget|null} Safe action
+ */
+OO.ui.ActionSet.prototype.getSpecial = function () {
+       this.organize();
+       return $.extend( {}, this.special );
+};
+
+/**
+ * Get other actions.
+ *
+ * Other actions include all non-special visible actions.
+ *
+ * @return {OO.ui.ActionWidget[]} Other actions
+ */
+OO.ui.ActionSet.prototype.getOthers = function () {
+       this.organize();
+       return this.others.slice();
+};
+
+/**
+ * Toggle actions based on their modes.
+ *
+ * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
+ * visibility; matching actions will be shown, non-matching actions will be hidden.
+ *
+ * @param {string} mode Mode actions must have
+ * @chainable
+ * @fires toggle
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.setMode = function ( mode ) {
+       var i, len, action;
+
+       this.changing = true;
+       for ( i = 0, len = this.list.length; i < len; i++ ) {
+               action = this.list[i];
+               action.toggle( action.hasMode( mode ) );
+       }
+
+       this.organized = false;
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Change which actions are able to be performed.
+ *
+ * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
+ *
+ * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
+ *   indicate actions are able to be performed
+ * @chainable
+ */
+OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
+       var i, len, action, item;
+
+       for ( i = 0, len = this.list.length; i < len; i++ ) {
+               item = this.list[i];
+               action = item.getAction();
+               if ( actions[action] !== undefined ) {
+                       item.setDisabled( !actions[action] );
+               }
+       }
+
+       return this;
+};
+
+/**
+ * Executes a function once per action.
+ *
+ * When making changes to multiple actions, use this method instead of iterating over the actions
+ * manually to defer emitting a change event until after all actions have been changed.
+ *
+ * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
+ * @param {Function} callback Callback to run for each action; callback is invoked with three
+ *   arguments: the action, the action's index, the list of actions being iterated over
+ * @chainable
+ */
+OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
+       this.changed = false;
+       this.changing = true;
+       this.get( filter ).forEach( callback );
+       this.changing = false;
+       if ( this.changed ) {
+               this.emit( 'change' );
+       }
+
+       return this;
+};
+
+/**
+ * Add actions.
+ *
+ * @param {OO.ui.ActionWidget[]} actions Actions to add
+ * @chainable
+ * @fires add
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.add = function ( actions ) {
+       var i, len, action;
+
+       this.changing = true;
+       for ( i = 0, len = actions.length; i < len; i++ ) {
+               action = actions[i];
+               action.connect( this, {
+                       'click': [ 'emit', 'click', action ],
+                       'resize': [ 'emit', 'resize', action ],
+                       'toggle': [ 'onActionChange' ]
+               } );
+               this.list.push( action );
+       }
+       this.organized = false;
+       this.emit( 'add', actions );
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Remove actions.
+ *
+ * @param {OO.ui.ActionWidget[]} actions Actions to remove
+ * @chainable
+ * @fires remove
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.remove = function ( actions ) {
+       var i, len, index, action;
+
+       this.changing = true;
+       for ( i = 0, len = actions.length; i < len; i++ ) {
+               action = actions[i];
+               index = this.list.indexOf( action );
+               if ( index !== -1 ) {
+                       action.disconnect( this );
+                       this.list.splice( index, 1 );
+               }
+       }
+       this.organized = false;
+       this.emit( 'remove', actions );
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Remove all actions.
+ *
+ * @chainable
+ * @fires remove
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.clear = function () {
+       var i, len, action,
+               removed = this.list.slice();
+
+       this.changing = true;
+       for ( i = 0, len = this.list.length; i < len; i++ ) {
+               action = this.list[i];
+               action.disconnect( this );
+       }
+
+       this.list = [];
+
+       this.organized = false;
+       this.emit( 'remove', removed );
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Organize actions.
+ *
+ * This is called whenver organized information is requested. It will only reorganize the actions
+ * if something has changed since the last time it ran.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.ActionSet.prototype.organize = function () {
+       var i, iLen, j, jLen, flag, action, category, list, item, special,
+               specialFlags = this.constructor.static.specialFlags;
+
+       if ( !this.organized ) {
+               this.categorized = {};
+               this.special = {};
+               this.others = [];
+               for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
+                       action = this.list[i];
+                       if ( action.isVisible() ) {
+                               // Populate catgeories
+                               for ( category in this.categories ) {
+                                       if ( !this.categorized[category] ) {
+                                               this.categorized[category] = {};
+                                       }
+                                       list = action[this.categories[category]]();
+                                       if ( !Array.isArray( list ) ) {
+                                               list = [ list ];
+                                       }
+                                       for ( j = 0, jLen = list.length; j < jLen; j++ ) {
+                                               item = list[j];
+                                               if ( !this.categorized[category][item] ) {
+                                                       this.categorized[category][item] = [];
+                                               }
+                                               this.categorized[category][item].push( action );
+                                       }
+                               }
+                               // Populate special/others
+                               special = false;
+                               for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
+                                       flag = specialFlags[j];
+                                       if ( !this.special[flag] && action.hasFlag( flag ) ) {
+                                               this.special[flag] = action;
+                                               special = true;
+                                               break;
+                                       }
+                               }
+                               if ( !special ) {
+                                       this.others.push( action );
+                               }
+                       }
+               }
+               this.organized = true;
+       }
+
+       return this;
+};
+
 /**
  * DOM element abstraction.
  *
@@ -851,7 +1273,8 @@ OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout )
  * @fires load
  */
 OO.ui.Frame.prototype.load = function () {
-       var win, doc;
+       var win, doc,
+               frame = this;
 
        // Return existing promise if already loading or loaded
        if ( this.loading ) {
@@ -872,8 +1295,7 @@ OO.ui.Frame.prototype.load = function () {
        doc.write(
                '<!doctype html>' +
                '<html>' +
-                       '<body class="oo-ui-frame-body oo-ui-' + this.dir + '" style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
-                               '<div class="oo-ui-frame-content"></div>' +
+                       '<body class="oo-ui-frame-content oo-ui-' + this.dir + '" style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
                        '</body>' +
                '</html>'
        );
@@ -886,10 +1308,10 @@ OO.ui.Frame.prototype.load = function () {
 
        // Initialization
        this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
-               .always( OO.ui.bind( function () {
-                       this.emit( 'load' );
-                       this.loading.resolve();
-               }, this ) );
+               .always( function () {
+                       frame.emit( 'load' );
+                       frame.loading.resolve();
+               } );
 
        return this.loading.promise();
 };
@@ -907,10 +1329,7 @@ OO.ui.Frame.prototype.setSize = function ( width, height ) {
 };
 
 /**
- * Container for elements in a child frame.
- *
- * There are two ways to specify a title: set the static `title` property or provide a `title`
- * property in the configuration options. The latter will override the former.
+ * Container for elements.
  *
  * @abstract
  * @class
@@ -919,305 +1338,369 @@ OO.ui.Frame.prototype.setSize = function ( width, height ) {
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {string|Function} [title] Title string or function that returns a string
- * @cfg {string} [icon] Symbolic name of icon
- * @fires initialize
  */
-OO.ui.Window = function OoUiWindow( config ) {
-       var element = this;
+OO.ui.Layout = function OoUiLayout( config ) {
+       // Initialize config
+       config = config || {};
+
        // Parent constructor
-       OO.ui.Window.super.call( this, config );
+       OO.ui.Layout.super.call( this, config );
 
        // Mixin constructors
        OO.EventEmitter.call( this );
 
-       // Properties
-       this.visible = false;
-       this.opening = null;
-       this.closing = null;
-       this.opened = null;
-       this.title = OO.ui.resolveMsg( config.title || this.constructor.static.title );
-       this.icon = config.icon || this.constructor.static.icon;
-       this.frame = new OO.ui.Frame( { '$': this.$ } );
-       this.$frame = this.$( '<div>' );
-       this.$ = function () {
-               throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
-       };
-
        // Initialization
-       this.$element
-               .addClass( 'oo-ui-window' )
-               // Hide the window using visibility: hidden; while the iframe is still loading
-               // Can't use display: none; because that prevents the iframe from loading in Firefox
-               .css( 'visibility', 'hidden' )
-               .append( this.$frame );
-       this.$frame
-               .addClass( 'oo-ui-window-frame' )
-               .append( this.frame.$element );
-
-       // Events
-       this.frame.on( 'load', function () {
-               element.initialize();
-               // Undo the visibility: hidden; hack and apply display: none;
-               // We can do this safely now that the iframe has initialized
-               // (don't do this from within #initialize because it has to happen
-               // after the all subclasses have been handled as well).
-               element.$element.hide().css( 'visibility', '' );
-       } );
+       this.$element.addClass( 'oo-ui-layout' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Window, OO.ui.Element );
-OO.mixinClass( OO.ui.Window, OO.EventEmitter );
-
-/* Events */
+OO.inheritClass( OO.ui.Layout, OO.ui.Element );
+OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
 
 /**
- * Window is setup.
+ * User interface control.
  *
- * Fired after the setup process has been executed.
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
  *
- * @event setup
- * @param {Object} data Window opening data
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [disabled=false] Disable
  */
+OO.ui.Widget = function OoUiWidget( config ) {
+       // Initialize config
+       config = $.extend( { 'disabled': false }, config );
+
+       // Parent constructor
+       OO.ui.Widget.super.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.visible = true;
+       this.disabled = null;
+       this.wasDisabled = null;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-widget' );
+       this.setDisabled( !!config.disabled );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.Widget, OO.ui.Element );
+OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
+
+/* Events */
 
 /**
- * Window is ready.
- *
- * Fired after the ready process has been executed.
- *
- * @event ready
- * @param {Object} data Window opening data
+ * @event disable
+ * @param {boolean} disabled Widget is disabled
  */
 
 /**
- * Window is torn down
- *
- * Fired after the teardown process has been executed.
- *
- * @event teardown
- * @param {Object} data Window closing data
+ * @event toggle
+ * @param {boolean} visible Widget is visible
  */
 
-/* Static Properties */
+/* Methods */
 
 /**
- * Symbolic name of icon.
+ * Check if the widget is disabled.
  *
- * @static
- * @inheritable
- * @property {string}
+ * @param {boolean} Button is disabled
  */
-OO.ui.Window.static.icon = 'window';
+OO.ui.Widget.prototype.isDisabled = function () {
+       return this.disabled;
+};
 
 /**
- * Window title.
+ * Check if widget is visible.
  *
- * Subclasses must implement this property before instantiating the window.
- * Alternatively, override #getTitle with an alternative implementation.
+ * @return {boolean} Widget is visible
+ */
+OO.ui.Widget.prototype.isVisible = function () {
+       return this.visible;
+};
+
+/**
+ * Set the disabled state of the widget.
  *
- * @static
- * @abstract
- * @inheritable
- * @property {string|Function} Title string or function that returns a string
+ * This should probably change the widgets' appearance and prevent it from being used.
+ *
+ * @param {boolean} disabled Disable widget
+ * @chainable
  */
-OO.ui.Window.static.title = null;
+OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
+       var isDisabled;
 
-/* Methods */
+       this.disabled = !!disabled;
+       isDisabled = this.isDisabled();
+       if ( isDisabled !== this.wasDisabled ) {
+               this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
+               this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
+               this.emit( 'disable', isDisabled );
+       }
+       this.wasDisabled = isDisabled;
+
+       return this;
+};
 
 /**
- * Check if window is visible.
+ * Toggle visibility of widget.
  *
- * @return {boolean} Window is visible
+ * @param {boolean} [show] Make widget visible, omit to toggle visibility
+ * @fires visible
+ * @chainable
  */
-OO.ui.Window.prototype.isVisible = function () {
-       return this.visible;
+OO.ui.Widget.prototype.toggle = function ( show ) {
+       show = show === undefined ? !this.visible : !!show;
+
+       if ( show !== this.isVisible() ) {
+               this.visible = show;
+               this.$element.toggle( show );
+               this.emit( 'toggle', show );
+       }
+
+       return this;
 };
 
 /**
- * Check if window is opening.
+ * Update the disabled state, in case of changes in parent widget.
  *
- * @return {boolean} Window is opening
+ * @chainable
  */
-OO.ui.Window.prototype.isOpening = function () {
-       return !!this.opening && this.opening.state() === 'pending';
+OO.ui.Widget.prototype.updateDisabled = function () {
+       this.setDisabled( this.disabled );
+       return this;
 };
 
 /**
- * Check if window is closing.
+ * Container for elements in a child frame.
  *
- * @return {boolean} Window is closing
+ * Use together with OO.ui.WindowManager.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
+ * teardown processes are executed when the window is closed.
+ *
+ * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
+ * - Window manager begins opening window
+ * - {@link #getSetupProcess} method is called and its result executed
+ * - {@link #getReadyProcess} method is called and its result executed
+ * - Window is now open
+ *
+ * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
+ * - Window manager begins closing window
+ * - {@link #getHoldProcess} method is called and its result executed
+ * - {@link #getTeardownProcess} method is called and its result executed
+ * - Window is now closed
+ *
+ * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
+ * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
+ * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous
+ * processing can complete. Always assume window processes are executed asychronously. See
+ * OO.ui.Process for more details about how to work with processes. Some events, as well as the
+ * #open and #close methods, provide promises which are resolved when the window enters a new state.
+ *
+ * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
+ * If the requested size is not recognized, the window manager will choose a sensible fallback.
+ *
+ * @constructor
+ * @param {OO.ui.WindowManager} manager Manager of window
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
+ *   use #static-size
+ * @fires initialize
  */
-OO.ui.Window.prototype.isClosing = function () {
-       return !!this.closing && this.closing.state() === 'pending';
+OO.ui.Window = function OoUiWindow( manager, config ) {
+       var win = this;
+
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Window.super.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       if ( !( manager instanceof OO.ui.WindowManager ) ) {
+               throw new Error( 'Cannot construct window: window must have a manager' );
+       }
+
+       // Properties
+       this.manager = manager;
+       this.initialized = false;
+       this.visible = false;
+       this.opening = null;
+       this.closing = null;
+       this.opened = null;
+       this.timing = null;
+       this.size = config.size || this.constructor.static.size;
+       this.frame = new OO.ui.Frame( { '$': this.$ } );
+       this.$frame = this.$( '<div>' );
+       this.$ = function () {
+               throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
+       };
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-window' )
+               // Hide the window using visibility: hidden; while the iframe is still loading
+               // Can't use display: none; because that prevents the iframe from loading in Firefox
+               .css( 'visibility', 'hidden' )
+               .append( this.$frame );
+       this.$frame
+               .addClass( 'oo-ui-window-frame' )
+               .append( this.frame.$element );
+
+       // Events
+       this.frame.on( 'load', function () {
+               win.initialize();
+               win.initialized = true;
+               // Undo the visibility: hidden; hack and apply display: none;
+               // We can do this safely now that the iframe has initialized
+               // (don't do this from within #initialize because it has to happen
+               // after the all subclasses have been handled as well).
+               win.$element.hide().css( 'visibility', '' );
+       } );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.Window, OO.ui.Element );
+OO.mixinClass( OO.ui.Window, OO.EventEmitter );
+
+/* Events */
+
 /**
- * Check if window is opened.
+ * @event resize
+ * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
+ */
+
+/* Static Properties */
+
+/**
+ * Symbolic name of size.
  *
- * @return {boolean} Window is opened
+ * Size is used if no size is configured during construction.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
  */
-OO.ui.Window.prototype.isOpened = function () {
-       return !!this.opened && this.opened.state() === 'pending';
-};
+OO.ui.Window.static.size = 'medium';
+
+/* Methods */
 
 /**
- * Get the window frame.
+ * Check if window has been initialized.
  *
- * @return {OO.ui.Frame} Frame of window
+ * @return {boolean} Window has been initialized
  */
-OO.ui.Window.prototype.getFrame = function () {
-       return this.frame;
+OO.ui.Window.prototype.isInitialized = function () {
+       return this.initialized;
 };
 
 /**
- * Get the title of the window.
+ * Check if window is visible.
  *
- * @return {string} Title text
+ * @return {boolean} Window is visible
  */
-OO.ui.Window.prototype.getTitle = function () {
-       return this.title;
+OO.ui.Window.prototype.isVisible = function () {
+       return this.visible;
 };
 
 /**
- * Get the window icon.
+ * Check if window is opening.
+ *
+ * This is a wrapper around OO.ui.WindowManager#isOpening.
  *
- * @return {string} Symbolic name of icon
+ * @return {boolean} Window is opening
  */
-OO.ui.Window.prototype.getIcon = function () {
-       return this.icon;
+OO.ui.Window.prototype.isOpening = function () {
+       return this.manager.isOpening( this );
 };
 
 /**
- * Set the size of window frame.
+ * Check if window is closing.
  *
- * @param {number} [width=auto] Custom width
- * @param {number} [height=auto] Custom height
- * @chainable
+ * This is a wrapper around OO.ui.WindowManager#isClosing.
+ *
+ * @return {boolean} Window is closing
  */
-OO.ui.Window.prototype.setSize = function ( width, height ) {
-       if ( !this.frame.$content ) {
-               return;
-       }
-
-       this.frame.$element.css( {
-               'width': width === undefined ? 'auto' : width,
-               'height': height === undefined ? 'auto' : height
-       } );
-
-       return this;
+OO.ui.Window.prototype.isClosing = function () {
+       return this.manager.isClosing( this );
 };
 
 /**
- * Set the title of the window.
+ * Check if window is opened.
  *
- * @param {string|Function} title Title text or a function that returns text
- * @chainable
+ * This is a wrapper around OO.ui.WindowManager#isOpened.
+ *
+ * @return {boolean} Window is opened
  */
-OO.ui.Window.prototype.setTitle = function ( title ) {
-       this.title = OO.ui.resolveMsg( title );
-       if ( this.$title ) {
-               this.$title.text( title );
-       }
-       return this;
+OO.ui.Window.prototype.isOpened = function () {
+       return this.manager.isOpened( this );
 };
 
 /**
- * Set the icon of the window.
+ * Get the window manager.
  *
- * @param {string} icon Symbolic name of icon
- * @chainable
+ * @return {OO.ui.WindowManager} Manager of window
  */
-OO.ui.Window.prototype.setIcon = function ( icon ) {
-       if ( this.$icon ) {
-               this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
-       }
-       this.icon = icon;
-       if ( this.$icon ) {
-               this.$icon.addClass( 'oo-ui-icon-' + this.icon );
-       }
-
-       return this;
+OO.ui.Window.prototype.getManager = function () {
+       return this.manager;
 };
 
 /**
- * Set the position of window to fit with contents.
+ * Get the window frame.
  *
- * @param {string} left Left offset
- * @param {string} top Top offset
- * @chainable
+ * @return {OO.ui.Frame} Frame of window
  */
-OO.ui.Window.prototype.setPosition = function ( left, top ) {
-       this.$element.css( { 'left': left, 'top': top } );
-       return this;
+OO.ui.Window.prototype.getFrame = function () {
+       return this.frame;
 };
 
 /**
- * Set the height of window to fit with contents.
+ * Get the window size.
  *
- * @param {number} [min=0] Min height
- * @param {number} [max] Max height (defaults to content's outer height)
- * @chainable
+ * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
  */
-OO.ui.Window.prototype.fitHeightToContents = function ( min, max ) {
-       var height = this.frame.$content.outerHeight();
-
-       this.frame.$element.css(
-               'height', Math.max( min || 0, max === undefined ? height : Math.min( max, height ) )
-       );
-
-       return this;
+OO.ui.Window.prototype.getSize = function () {
+       return this.size;
 };
 
 /**
- * Set the width of window to fit with contents.
+ * Get the height of the dialog contents.
  *
- * @param {number} [min=0] Min height
- * @param {number} [max] Max height (defaults to content's outer width)
- * @chainable
+ * @return {number} Content height
  */
-OO.ui.Window.prototype.fitWidthToContents = function ( min, max ) {
-       var width = this.frame.$content.outerWidth();
-
-       this.frame.$element.css(
-               'width', Math.max( min || 0, max === undefined ? width : Math.min( max, width ) )
+OO.ui.Window.prototype.getContentHeight = function () {
+       return Math.round(
+               // Add buffer for border
+               ( ( this.$frame.outerHeight() - this.$frame.innerHeight() ) * 2 ) +
+               // Height of contents
+               ( this.$head.outerHeight( true ) + this.getBodyHeight() + this.$foot.outerHeight( true ) )
        );
-
-       return this;
 };
 
 /**
- * Initialize window contents.
- *
- * The first time the window is opened, #initialize is called when it's safe to begin populating
- * its contents. See #setup for a way to make changes each time the window opens.
- *
- * Once this method is called, this.$$ can be used to create elements within the frame.
+ * Get the height of the dialog contents.
  *
- * @chainable
+ * @return {number} Height of content
  */
-OO.ui.Window.prototype.initialize = function () {
-       // Properties
-       this.$ = this.frame.$;
-       this.$title = this.$( '<div class="oo-ui-window-title"></div>' )
-               .text( this.title );
-       this.$icon = this.$( '<div class="oo-ui-window-icon"></div>' )
-               .addClass( 'oo-ui-icon-' + this.icon );
-       this.$head = this.$( '<div class="oo-ui-window-head"></div>' );
-       this.$body = this.$( '<div class="oo-ui-window-body"></div>' );
-       this.$foot = this.$( '<div class="oo-ui-window-foot"></div>' );
-       this.$overlay = this.$( '<div class="oo-ui-window-overlay"></div>' );
-
-       // Initialization
-       this.frame.$content.append(
-               this.$head.append( this.$icon, this.$title ),
-               this.$body,
-               this.$foot,
-               this.$overlay
-       );
-
-       return this;
+OO.ui.Window.prototype.getBodyHeight = function () {
+       return this.$body[0].scrollHeight;
 };
 
 /**
@@ -1255,322 +1738,298 @@ OO.ui.Window.prototype.getReadyProcess = function () {
 };
 
 /**
- * Get a process for tearing down a window after use.
+ * Get a process for holding a window from use.
  *
- * Each time the window is closed this process will tear it down and do something with the user's
- * interactions within the window, based on the `data` argument.
+ * Each time the window is closed, this process will hold it from use in a particular context, based
+ * on the `data` argument.
  *
- * When you override this method, you can add additional teardown steps to the process the parent
+ * When you override this method, you can add additional setup steps to the process the parent
  * method provides using the 'first' and 'next' methods.
  *
  * @abstract
  * @param {Object} [data] Window closing data
- * @return {OO.ui.Process} Teardown process
+ * @return {OO.ui.Process} Hold process
  */
-OO.ui.Window.prototype.getTeardownProcess = function () {
+OO.ui.Window.prototype.getHoldProcess = function () {
        return new OO.ui.Process();
 };
 
 /**
- * Open window.
+ * Get a process for tearing down a window after use.
  *
- * Do not override this method. Use #getSetupProcess to do something each time the window closes.
+ * Each time the window is closed this process will tear it down and do something with the user's
+ * interactions within the window, based on the `data` argument.
  *
- * @param {Object} [data] Window opening data
- * @fires initialize
- * @fires opening
- * @fires open
- * @fires ready
- * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
- *   first argument will be a promise which will be resolved when the window begins closing
+ * When you override this method, you can add additional teardown steps to the process the parent
+ * method provides using the 'first' and 'next' methods.
+ *
+ * @abstract
+ * @param {Object} [data] Window closing data
+ * @return {OO.ui.Process} Teardown process
  */
-OO.ui.Window.prototype.open = function ( data ) {
-       // Return existing promise if already opening or open
-       if ( this.opening ) {
-               return this.opening.promise();
-       }
-
-       // Open the window
-       this.opening = $.Deferred();
-
-       this.$ariaHidden = $( 'body' ).children().not( this.$element.parentsUntil( 'body' ).last() )
-               .attr( 'aria-hidden', '' );
-
-       this.frame.load().done( OO.ui.bind( function () {
-               this.$element.show();
-               this.visible = true;
-               this.getSetupProcess( data ).execute().done( OO.ui.bind( function () {
-                       this.$element.addClass( 'oo-ui-window-setup' );
-                       this.emit( 'setup', data );
-                       setTimeout( OO.ui.bind( function () {
-                               this.frame.$content.focus();
-                               this.getReadyProcess( data ).execute().done( OO.ui.bind( function () {
-                                       this.$element.addClass( 'oo-ui-window-ready' );
-                                       this.emit( 'ready', data );
-                                       this.opened = $.Deferred();
-                                       // Now that we are totally done opening, it's safe to allow closing
-                                       this.closing = null;
-                                       this.opening.resolve( this.opened.promise() );
-                               }, this ) );
-                       }, this ) );
-               }, this ) );
-       }, this ) );
-
-       return this.opening.promise();
+OO.ui.Window.prototype.getTeardownProcess = function () {
+       return new OO.ui.Process();
 };
 
 /**
- * Close window.
- *
- * Do not override this method. Use #getTeardownProcess to do something each time the window closes.
+ * Set the window size.
  *
- * @param {Object} [data] Window closing data
- * @fires closing
- * @fires close
- * @return {jQuery.Promise} Promise resolved when window is closed
+ * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
+ * @chainable
  */
-OO.ui.Window.prototype.close = function ( data ) {
-       var close;
-
-       // Return existing promise if already closing or closed
-       if ( this.closing ) {
-               return this.closing.promise();
-       }
-
-       // Close after opening is done if opening is in progress
-       if ( this.opening && this.opening.state() === 'pending' ) {
-               close = OO.ui.bind( function () {
-                       return this.close( data );
-               }, this );
-               return this.opening.then( close, close );
-       }
-
-       // Close the window
-       // This.closing needs to exist before we emit the closing event so that handlers can call
-       // window.close() and trigger the safety check above
-       this.closing = $.Deferred();
-       this.frame.$content.find( ':focus' ).blur();
-       this.$element.removeClass( 'oo-ui-window-ready' );
-       this.getTeardownProcess( data ).execute().done( OO.ui.bind( function () {
-               this.$element.removeClass( 'oo-ui-window-setup' );
-               this.emit( 'teardown', data );
-               // To do something different with #opened, resolve/reject #opened in the teardown process
-               if ( this.opened && this.opened.state() === 'pending' ) {
-                       this.opened.resolve();
-               }
-               this.$element.hide();
-               if ( this.$ariaHidden ) {
-                       this.$ariaHidden.removeAttr( 'aria-hidden' );
-                       this.$ariaHidden = undefined;
-               }
-               this.visible = false;
-               this.closing.resolve();
-               // Now that we are totally done closing, it's safe to allow opening
-               this.opening = null;
-       }, this ) );
-
-       return this.closing.promise();
+OO.ui.Window.prototype.setSize = function ( size ) {
+       this.size = size;
+       this.manager.updateWindowSize( this );
+       return this;
 };
 
 /**
- * Set of mutually exclusive windows.
+ * Set window dimensions.
  *
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
+ * Properties are applied to the frame container.
  *
- * @constructor
- * @param {OO.Factory} factory Window factory
- * @param {Object} [config] Configuration options
+ * @param {Object} dim CSS dimension properties
+ * @param {string|number} [dim.width] Width
+ * @param {string|number} [dim.minWidth] Minimum width
+ * @param {string|number} [dim.maxWidth] Maximum width
+ * @param {string|number} [dim.width] Height, omit to set based on height of contents
+ * @param {string|number} [dim.minWidth] Minimum height
+ * @param {string|number} [dim.maxWidth] Maximum height
+ * @chainable
  */
-OO.ui.WindowSet = function OoUiWindowSet( factory, config ) {
-       // Parent constructor
-       OO.ui.WindowSet.super.call( this, config );
-
-       // Mixin constructors
-       OO.EventEmitter.call( this );
-
-       // Properties
-       this.factory = factory;
-
-       /**
-        * List of all windows associated with this window set.
-        *
-        * @property {OO.ui.Window[]}
-        */
-       this.windowList = [];
-
-       /**
-        * Mapping of OO.ui.Window objects created by name from the #factory.
-        *
-        * @property {Object}
-        */
-       this.windows = {};
-       this.currentWindow = null;
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-windowSet' );
+OO.ui.Window.prototype.setDimensions = function ( dim ) {
+       // Apply width before height so height is not based on wrapping content using the wrong width
+       this.$frame.css( {
+               'width': dim.width || '',
+               'min-width': dim.minWidth || '',
+               'max-width': dim.maxWidth || ''
+       } );
+       this.$frame.css( {
+               'height': ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '',
+               'min-height': dim.minHeight || '',
+               'max-height': dim.maxHeight || ''
+       } );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.WindowSet, OO.ui.Element );
-OO.mixinClass( OO.ui.WindowSet, OO.EventEmitter );
-
-/* Events */
-
-/**
- * @event setup
- * @param {OO.ui.Window} win Window that's been setup
- * @param {Object} config Window opening information
- */
-
 /**
- * @event ready
- * @param {OO.ui.Window} win Window that's ready
- * @param {Object} config Window opening information
+ * Initialize window contents.
+ *
+ * The first time the window is opened, #initialize is called when it's safe to begin populating
+ * its contents. See #getSetupProcess for a way to make changes each time the window opens.
+ *
+ * Once this method is called, this.$ can be used to create elements within the frame.
+ *
+ * @chainable
  */
+OO.ui.Window.prototype.initialize = function () {
+       // Properties
+       this.$ = this.frame.$;
+       this.$head = this.$( '<div>' );
+       this.$body = this.$( '<div>' );
+       this.$foot = this.$( '<div>' );
+       this.$overlay = this.$( '<div>' );
 
-/**
- * @event teardown
- * @param {OO.ui.Window} win Window that's been torn down
- * @param {Object} config Window closing information
- */
+       // Initialization
+       this.$head.addClass( 'oo-ui-window-head' );
+       this.$body.addClass( 'oo-ui-window-body' );
+       this.$foot.addClass( 'oo-ui-window-foot' );
+       this.$overlay.addClass( 'oo-ui-window-overlay' );
+       this.frame.$content
+               .addClass( 'oo-ui-window-content' )
+               .append( this.$head, this.$body, this.$foot, this.$overlay );
 
-/* Methods */
+       return this;
+};
 
 /**
- * Handle a window setup event.
+ * Open window.
  *
- * @param {OO.ui.Window} win Window that's been setup
- * @param {Object} [config] Window opening information
- * @fires setup
+ * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
+ * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
+ *
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
+ *   first argument will be a promise which will be resolved when the window begins closing
  */
-OO.ui.WindowSet.prototype.onWindowSetup = function ( win, config ) {
-       if ( this.currentWindow && this.currentWindow !== win ) {
-               this.currentWindow.close();
-       }
-       this.currentWindow = win;
-       this.emit( 'setup', win, config );
+OO.ui.Window.prototype.open = function ( data ) {
+       return this.manager.openWindow( this, data );
 };
 
 /**
- * Handle a window ready event.
+ * Close window.
+ *
+ * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
+ * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
  *
- * @param {OO.ui.Window} win Window that's ready
- * @param {Object} [config] Window opening information
- * @fires ready
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is closed
  */
-OO.ui.WindowSet.prototype.onWindowReady = function ( win, config ) {
-       this.emit( 'ready', win, config );
+OO.ui.Window.prototype.close = function ( data ) {
+       return this.manager.closeWindow( this, data );
 };
 
 /**
- * Handle a window teardown event.
+ * Load window.
+ *
+ * This is called by OO.ui.WindowManager durring window adding, and should not be called directly
+ * by other systems.
  *
- * @param {OO.ui.Window} win Window that's been torn down
- * @param {Object} [config] Window closing information
- * @fires teardown
+ * @return {jQuery.Promise} Promise resolved when window is loaded
  */
-OO.ui.WindowSet.prototype.onWindowTeardown = function ( win, config ) {
-       this.currentWindow = null;
-       this.emit( 'teardown', win, config );
+OO.ui.Window.prototype.load = function () {
+       return this.frame.load();
 };
 
 /**
- * Get the current window.
+ * Setup window.
+ *
+ * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * by other systems.
  *
- * @return {OO.ui.Window|null} Current window or null if none open
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is setup
  */
-OO.ui.WindowSet.prototype.getCurrentWindow = function () {
-       return this.currentWindow;
+OO.ui.Window.prototype.setup = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
+
+       this.$element.show();
+       this.visible = true;
+       this.getSetupProcess( data ).execute().done( function () {
+               win.manager.updateWindowSize( win );
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.addClass( 'oo-ui-window-setup' ).width();
+               win.frame.$content.addClass( 'oo-ui-window-content-setup' ).width();
+               deferred.resolve();
+       } );
+
+       return deferred.promise();
 };
 
 /**
- * Return a given window.
+ * Ready window.
  *
- * @param {string} name Symbolic name of window
- * @return {OO.ui.Window} Window with specified name
+ * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * by other systems.
+ *
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is ready
  */
-OO.ui.WindowSet.prototype.getWindow = function ( name ) {
-       var win;
+OO.ui.Window.prototype.ready = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
 
-       if ( !this.factory.lookup( name ) ) {
-               throw new Error( 'Unknown window: ' + name );
-       }
-       if ( !( name in this.windows ) ) {
-               win = this.windows[name] = this.createWindow( name );
-               this.addWindow( win );
-       }
-       return this.windows[name];
+       this.frame.$content.focus();
+       this.getReadyProcess( data ).execute().done( function () {
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.addClass( 'oo-ui-window-ready' ).width();
+               win.frame.$content.addClass( 'oo-ui-window-content-ready' ).width();
+               deferred.resolve();
+       } );
+
+       return deferred.promise();
 };
 
 /**
- * Create a window for use in this window set.
+ * Hold window.
+ *
+ * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * by other systems.
  *
- * @param {string} name Symbolic name of window
- * @return {OO.ui.Window} Window with specified name
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is held
  */
-OO.ui.WindowSet.prototype.createWindow = function ( name ) {
-       return this.factory.create( name, { '$': this.$ } );
+OO.ui.Window.prototype.hold = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
+
+       this.getHoldProcess( data ).execute().done( function () {
+               win.frame.$content.find( ':focus' ).blur();
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.removeClass( 'oo-ui-window-ready' ).width();
+               win.frame.$content.removeClass( 'oo-ui-window-content-ready' ).width();
+               deferred.resolve();
+       } );
+
+       return deferred.promise();
 };
 
 /**
- * Add a given window to this window set.
+ * Teardown window.
  *
- * Connects event handlers and attaches it to the DOM. Calling
- * OO.ui.Window#open will not work until the window is added to the set.
+ * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * by other systems.
  *
- * @param {OO.ui.Window} win Window to add
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is torn down
  */
-OO.ui.WindowSet.prototype.addWindow = function ( win ) {
-       if ( this.windowList.indexOf( win ) !== -1 ) {
-               // Already set up
-               return;
-       }
-       this.windowList.push( win );
+OO.ui.Window.prototype.teardown = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
 
-       win.connect( this, {
-               'setup': [ 'onWindowSetup', win ],
-               'ready': [ 'onWindowReady', win ],
-               'teardown': [ 'onWindowTeardown', win ]
+       this.getTeardownProcess( data ).execute().done( function () {
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.removeClass( 'oo-ui-window-setup' ).width();
+               win.frame.$content.removeClass( 'oo-ui-window-content-setup' ).width();
+               win.$element.hide();
+               win.visible = false;
+               deferred.resolve();
        } );
-       this.$element.append( win.$element );
+
+       return deferred.promise();
 };
 
 /**
- * Modal dialog window.
+ * Base class for all dialogs.
+ *
+ * Logic:
+ * - Manage the window (open and close, etc.).
+ * - Store the internal name and display title.
+ * - A stack to track one or more pending actions.
+ * - Manage a set of actions that can be performed.
+ * - Configure and create action widgets.
+ *
+ * User interface:
+ * - Close the dialog with Escape key.
+ * - Visually lock the dialog while an action is in
+ *   progress (aka "pending").
+ *
+ * Subclass responsibilities:
+ * - Display the title somewhere.
+ * - Add content to the dialog.
+ * - Provide a UI to close the dialog.
+ * - Display the action widgets somewhere.
  *
  * @abstract
  * @class
  * @extends OO.ui.Window
+ * @mixins OO.ui.LabeledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [footless] Hide foot
- * @cfg {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large`
  */
-OO.ui.Dialog = function OoUiDialog( config ) {
-       // Configuration initialization
-       config = $.extend( { 'size': 'large' }, config );
-
+OO.ui.Dialog = function OoUiDialog( manager, config ) {
        // Parent constructor
-       OO.ui.Dialog.super.call( this, config );
+       OO.ui.Dialog.super.call( this, manager, config );
 
        // Properties
-       this.visible = false;
-       this.footless = !!config.footless;
-       this.size = null;
+       this.actions = new OO.ui.ActionSet();
+       this.attachedActions = [];
+       this.currentAction = null;
        this.pending = 0;
-       this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
-       this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
 
        // Events
-       this.$element.on( 'mousedown', false );
+       this.actions.connect( this, {
+               'click': 'onActionClick',
+               'resize': 'onActionResize',
+               'change': 'onActionsChange'
+       } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-dialog' ).attr( 'role', 'dialog' );
-       this.setSize( config.size );
+       this.$element
+               .addClass( 'oo-ui-dialog' )
+               .attr( 'role', 'dialog' );
 };
 
 /* Setup */
@@ -1590,55 +2049,35 @@ OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
 OO.ui.Dialog.static.name = '';
 
 /**
- * Map of symbolic size names and CSS classes.
+ * Dialog title.
  *
+ * @abstract
  * @static
  * @inheritable
- * @property {Object}
- */
-OO.ui.Dialog.static.sizeCssClasses = {
-       'small': 'oo-ui-dialog-small',
-       'medium': 'oo-ui-dialog-medium',
-       'large': 'oo-ui-dialog-large'
-};
-
-/* Methods */
-
-/**
- * Handle close button click events.
+ * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
  */
-OO.ui.Dialog.prototype.onCloseButtonClick = function () {
-       this.close( { 'action': 'cancel' } );
-};
+OO.ui.Dialog.static.title = '';
 
 /**
- * Handle window mouse wheel events.
+ * List of OO.ui.ActionWidget configuration options.
  *
- * @param {jQuery.Event} e Mouse wheel event
+ * @static
+ * inheritable
+ * @property {Object[]}
  */
-OO.ui.Dialog.prototype.onWindowMouseWheel = function () {
-       return false;
-};
+OO.ui.Dialog.static.actions = [];
 
 /**
- * Handle document key down events.
+ * Close dialog when the escape key is pressed.
  *
- * @param {jQuery.Event} e Key down event
+ * @static
+ * @abstract
+ * @inheritable
+ * @property {boolean}
  */
-OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
-       switch ( e.which ) {
-               case OO.ui.Keys.PAGEUP:
-               case OO.ui.Keys.PAGEDOWN:
-               case OO.ui.Keys.END:
-               case OO.ui.Keys.HOME:
-               case OO.ui.Keys.LEFT:
-               case OO.ui.Keys.UP:
-               case OO.ui.Keys.RIGHT:
-               case OO.ui.Keys.DOWN:
-                       // Prevent any key events that might cause scrolling
-                       return false;
-       }
-};
+OO.ui.Dialog.static.escapable = true;
+
+/* Methods */
 
 /**
  * Handle frame document key down events.
@@ -1647,68 +2086,109 @@ OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
  */
 OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) {
        if ( e.which === OO.ui.Keys.ESCAPE ) {
-               this.close( { 'action': 'cancel' } );
+               this.close();
                return false;
        }
 };
 
 /**
- * Set dialog size.
+ * Handle action resized events.
  *
- * @param {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large`
+ * @param {OO.ui.ActionWidget} action Action that was resized
  */
-OO.ui.Dialog.prototype.setSize = function ( size ) {
-       var name, state, cssClass,
-               sizeCssClasses = OO.ui.Dialog.static.sizeCssClasses;
+OO.ui.Dialog.prototype.onActionResize = function () {
+       // Override in subclass
+};
 
-       if ( !sizeCssClasses[size] ) {
-               size = 'large';
-       }
-       this.size = size;
-       for ( name in sizeCssClasses ) {
-               state = name === size;
-               cssClass = sizeCssClasses[name];
-               this.$element.toggleClass( cssClass, state );
+/**
+ * Handle action click events.
+ *
+ * @param {OO.ui.ActionWidget} action Action that was clicked
+ */
+OO.ui.Dialog.prototype.onActionClick = function ( action ) {
+       if ( !this.isPending() ) {
+               this.currentAction = action;
+               this.executeAction( action.getAction() );
        }
 };
 
 /**
- * @inheritdoc
+ * Handle actions change event.
  */
-OO.ui.Dialog.prototype.initialize = function () {
-       // Parent method
-       OO.ui.Dialog.super.prototype.initialize.call( this );
+OO.ui.Dialog.prototype.onActionsChange = function () {
+       this.detachActions();
+       if ( !this.isClosing() ) {
+               this.attachActions();
+       }
+};
 
-       // Properties
-       this.closeButton = new OO.ui.ButtonWidget( {
-               '$': this.$,
-               'frameless': true,
-               'icon': 'close',
-               'title': OO.ui.msg( 'ooui-dialog-action-close' )
-       } );
+/**
+ * Check if input is pending.
+ *
+ * @return {boolean}
+ */
+OO.ui.Dialog.prototype.isPending = function () {
+       return !!this.pending;
+};
 
-       // Events
-       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
-       this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
+/**
+ * Get set of actions.
+ *
+ * @return {OO.ui.ActionSet}
+ */
+OO.ui.Dialog.prototype.getActions = function () {
+       return this.actions;
+};
 
-       // Initialization
-       this.frame.$content.addClass( 'oo-ui-dialog-content' );
-       if ( this.footless ) {
-               this.frame.$content.addClass( 'oo-ui-dialog-content-footless' );
-       }
-       this.closeButton.$element.addClass( 'oo-ui-window-closeButton' );
-       this.$head.append( this.closeButton.$element );
+/**
+ * Get a process for taking action.
+ *
+ * When you override this method, you can add additional accept steps to the process the parent
+ * method provides using the 'first' and 'next' methods.
+ *
+ * @abstract
+ * @param {string} [action] Symbolic name of action
+ * @return {OO.ui.Process} Action process
+ */
+OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
+       return new OO.ui.Process()
+               .next( function () {
+                       if ( !action ) {
+                               // An empty action always closes the dialog without data, which should always be
+                               // safe and make no changes
+                               this.close();
+                       }
+               }, this );
 };
 
 /**
  * @inheritdoc
+ *
+ * @param {Object} [data] Dialog opening data
+ * @param {jQuery|string|Function|null} [data.label] Dialog label, omit to use #static-label
+ * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
+ *   action item, omit to use #static-actions
  */
 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
+       data = data || {};
+
+       // Parent method
        return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
                .next( function () {
-                       // Prevent scrolling in top-level window
-                       this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler );
-                       this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler );
+                       var i, len,
+                               items = [],
+                               config = this.constructor.static,
+                               actions = data.actions !== undefined ? data.actions : config.actions;
+
+                       this.title.setLabel(
+                               data.title !== undefined ? data.title : this.constructor.static.title
+                       );
+                       for ( i = 0, len = actions.length; i < len; i++ ) {
+                               items.push(
+                                       new OO.ui.ActionWidget( $.extend( { '$': this.$ }, actions[i] ) )
+                               );
+                       }
+                       this.actions.add( items );
                }, this );
 };
 
@@ -1716,37 +2196,77 @@ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
  * @inheritdoc
  */
 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
+       // Parent method
        return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
                .first( function () {
-                       // Wait for closing transition
-                       return OO.ui.Process.static.delay( 250 );
-               }, this )
-               .next( function () {
-                       // Allow scrolling in top-level window
-                       this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler );
-                       this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler );
+                       this.actions.clear();
+                       this.currentAction = null;
                }, this );
 };
 
 /**
- * Check if input is pending.
- *
- * @return {boolean}
+ * @inheritdoc
  */
-OO.ui.Dialog.prototype.isPending = function () {
-       return !!this.pending;
+OO.ui.Dialog.prototype.initialize = function () {
+       // Parent method
+       OO.ui.Dialog.super.prototype.initialize.call( this );
+
+       // Properties
+       this.title = new OO.ui.LabelWidget( { '$': this.$ } );
+
+       // Events
+       if ( this.constructor.static.escapable ) {
+               this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
+       }
+
+       // Initialization
+       this.frame.$content.addClass( 'oo-ui-dialog-content' );
 };
 
 /**
- * Increase the pending stack.
+ * Attach action actions.
+ */
+OO.ui.Dialog.prototype.attachActions = function () {
+       // Remember the list of potentially attached actions
+       this.attachedActions = this.actions.get();
+};
+
+/**
+ * Detach action actions.
+ *
+ * @chainable
+ */
+OO.ui.Dialog.prototype.detachActions = function () {
+       var i, len;
+
+       // Detach all actions that may have been previously attached
+       for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
+               this.attachedActions[i].$element.detach();
+       }
+       this.attachedActions = [];
+};
+
+/**
+ * Execute an action.
+ *
+ * @param {string} action Symbolic name of action to execute
+ * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
+ */
+OO.ui.Dialog.prototype.executeAction = function ( action ) {
+       this.pushPending();
+       return this.getActionProcess( action ).execute()
+               .always( OO.ui.bind( this.popPending, this ) );
+};
+
+/**
+ * Increase the pending stack.
  *
  * @chainable
  */
 OO.ui.Dialog.prototype.pushPending = function () {
        if ( this.pending === 0 ) {
-               this.frame.$content.addClass( 'oo-ui-dialog-pending' );
+               this.frame.$content.addClass( 'oo-ui-actionDialog-content-pending' );
                this.$head.addClass( 'oo-ui-texture-pending' );
-               this.$foot.addClass( 'oo-ui-texture-pending' );
        }
        this.pending++;
 
@@ -1762,9 +2282,8 @@ OO.ui.Dialog.prototype.pushPending = function () {
  */
 OO.ui.Dialog.prototype.popPending = function () {
        if ( this.pending === 1 ) {
-               this.frame.$content.removeClass( 'oo-ui-dialog-pending' );
+               this.frame.$content.removeClass( 'oo-ui-actionDialog-content-pending' );
                this.$head.removeClass( 'oo-ui-texture-pending' );
-               this.$foot.removeClass( 'oo-ui-texture-pending' );
        }
        this.pending = Math.max( 0, this.pending - 1 );
 
@@ -1772,2030 +2291,2630 @@ OO.ui.Dialog.prototype.popPending = function () {
 };
 
 /**
- * Container for elements.
+ * Collection of windows.
  *
- * @abstract
  * @class
  * @extends OO.ui.Element
  * @mixins OO.EventEmitter
  *
+ * Managed windows are mutually exclusive. If a window is opened while there is a current window
+ * already opening or opened, the current window will be closed without data. Empty closing data
+ * should always result in the window being closed without causing constructive or destructive
+ * action.
+ *
+ * As a window is opened and closed, it passes through several stages and the manager emits several
+ * corresponding events.
+ *
+ * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
+ * - {@link #event-opening} is emitted with `opening` promise
+ * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
+ * - `setup` progress notification is emitted from opening promise
+ * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
+ * - `ready` progress notification is emitted from opening promise
+ * - `opening` promise is resolved with `opened` promise
+ * - Window is now open
+ *
+ * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
+ * - `opened` promise is resolved with `closing` promise
+ * - {@link #event-opening} is emitted with `closing` promise
+ * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
+ * - `hold` progress notification is emitted from opening promise
+ * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
+ * - `teardown` progress notification is emitted from opening promise
+ * - Closing promise is resolved
+ * - Window is now closed
+ *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
+ * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
  */
-OO.ui.Layout = function OoUiLayout( config ) {
-       // Initialize config
+OO.ui.WindowManager = function OoUiWindowManager( config ) {
+       // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.Layout.super.call( this, config );
+       OO.ui.WindowManager.super.call( this, config );
 
        // Mixin constructors
        OO.EventEmitter.call( this );
 
+       // Properties
+       this.factory = config.factory;
+       this.modal = config.modal === undefined || !!config.modal;
+       this.windows = {};
+       this.opening = null;
+       this.opened = null;
+       this.closing = null;
+       this.size = null;
+       this.currentWindow = null;
+       this.$ariaHidden = null;
+       this.requestedSize = null;
+       this.onWindowResizeTimeout = null;
+       this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this );
+       this.afterWindowResizeHandler = OO.ui.bind( this.afterWindowResize, this );
+       this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
+       this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
+
+       // Events
+       this.$element.on( 'mousedown', false );
+
        // Initialization
-       this.$element.addClass( 'oo-ui-layout' );
+       this.$element
+               .addClass( 'oo-ui-windowManager' )
+               .toggleClass( 'oo-ui-windowManager-modal', this.modal );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Layout, OO.ui.Element );
-OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
+OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
+OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
+
+/* Events */
 
 /**
- * User interface control.
+ * Window is opening.
  *
- * @abstract
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
+ * Fired when the window begins to be opened.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [disabled=false] Disable
+ * @event opening
+ * @param {OO.ui.Window} win Window that's being opened
+ * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
+ *   resolved the first argument will be a promise which will be resolved when the window begins
+ *   closing, the second argument will be the opening data; progress notifications will be fired on
+ *   the promise for `setup` and `ready` when those processes are completed respectively.
+ * @param {Object} data Window opening data
  */
-OO.ui.Widget = function OoUiWidget( config ) {
-       // Initialize config
-       config = $.extend( { 'disabled': false }, config );
-
-       // Parent constructor
-       OO.ui.Widget.super.call( this, config );
 
-       // Mixin constructors
-       OO.EventEmitter.call( this );
+/**
+ * Window is closing.
+ *
+ * Fired when the window begins to be closed.
+ *
+ * @event closing
+ * @param {OO.ui.Window} win Window that's being closed
+ * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
+ *   is resolved the first argument will be a the closing data; progress notifications will be fired
+ *   on the promise for `hold` and `teardown` when those processes are completed respectively.
+ * @param {Object} data Window closing data
+ */
 
-       // Properties
-       this.disabled = null;
-       this.wasDisabled = null;
+/* Static Properties */
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-widget' );
-       this.setDisabled( !!config.disabled );
+/**
+ * Map of symbolic size names and CSS properties.
+ *
+ * @static
+ * @inheritable
+ * @property {Object}
+ */
+OO.ui.WindowManager.static.sizes = {
+       'small': {
+               'width': 300
+       },
+       'medium': {
+               'width': 500
+       },
+       'large': {
+               'width': 700
+       },
+       'full': {
+               // These can be non-numeric because they are never used in calculations
+               'width': '100%',
+               'height': '100%'
+       }
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.Widget, OO.ui.Element );
-OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
-
-/* Events */
-
 /**
- * @event disable
- * @param {boolean} disabled Widget is disabled
+ * Symbolic name of default size.
+ *
+ * Default size is used if the window's requested size is not recognized.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
  */
+OO.ui.WindowManager.static.defaultSize = 'medium';
 
 /* Methods */
 
 /**
- * Check if the widget is disabled.
+ * Handle window resize events.
  *
- * @param {boolean} Button is disabled
+ * @param {jQuery.Event} e Window resize event
  */
-OO.ui.Widget.prototype.isDisabled = function () {
-       return this.disabled;
+OO.ui.WindowManager.prototype.onWindowResize = function () {
+       clearTimeout( this.onWindowResizeTimeout );
+       this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
 };
 
 /**
- * Update the disabled state, in case of changes in parent widget.
+ * Handle window resize events.
  *
- * @chainable
+ * @param {jQuery.Event} e Window resize event
  */
-OO.ui.Widget.prototype.updateDisabled = function () {
-       this.setDisabled( this.disabled );
-       return this;
+OO.ui.WindowManager.prototype.afterWindowResize = function () {
+       if ( this.currentWindow ) {
+               this.updateWindowSize( this.currentWindow );
+       }
 };
 
 /**
- * Set the disabled state of the widget.
- *
- * This should probably change the widgets' appearance and prevent it from being used.
+ * Handle window mouse wheel events.
  *
- * @param {boolean} disabled Disable widget
- * @chainable
+ * @param {jQuery.Event} e Mouse wheel event
  */
-OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
-       var isDisabled;
+OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
+       return false;
+};
 
-       this.disabled = !!disabled;
-       isDisabled = this.isDisabled();
-       if ( isDisabled !== this.wasDisabled ) {
-               this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
-               this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
-               this.emit( 'disable', isDisabled );
+/**
+ * Handle document key down events.
+ *
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) {
+       switch ( e.which ) {
+               case OO.ui.Keys.PAGEUP:
+               case OO.ui.Keys.PAGEDOWN:
+               case OO.ui.Keys.END:
+               case OO.ui.Keys.HOME:
+               case OO.ui.Keys.LEFT:
+               case OO.ui.Keys.UP:
+               case OO.ui.Keys.RIGHT:
+               case OO.ui.Keys.DOWN:
+                       // Prevent any key events that might cause scrolling
+                       return false;
        }
-       this.wasDisabled = isDisabled;
-       return this;
 };
 
 /**
- * A list of functions, called in sequence.
+ * Check if window is opening.
  *
- * If a function added to a process returns boolean false the process will stop; if it returns an
- * object with a `promise` method the process will use the promise to either continue to the next
- * step when the promise is resolved or stop when the promise is rejected.
+ * @return {boolean} Window is opening
+ */
+OO.ui.WindowManager.prototype.isOpening = function ( win ) {
+       return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
+};
+
+/**
+ * Check if window is closing.
  *
- * @class
+ * @return {boolean} Window is closing
+ */
+OO.ui.WindowManager.prototype.isClosing = function ( win ) {
+       return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
+};
+
+/**
+ * Check if window is opened.
  *
- * @constructor
+ * @return {boolean} Window is opened
  */
-OO.ui.Process = function () {
-       // Properties
-       this.steps = [];
+OO.ui.WindowManager.prototype.isOpened = function ( win ) {
+       return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
 };
 
-/* Setup */
+/**
+ * Check if a window is being managed.
+ *
+ * @param {OO.ui.Window} win Window to check
+ * @return {boolean} Window is being managed
+ */
+OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
+       var name;
 
-OO.initClass( OO.ui.Process );
+       for ( name in this.windows ) {
+               if ( this.windows[name] === win ) {
+                       return true;
+               }
+       }
 
-/* Static Methods */
+       return false;
+};
 
 /**
- * Generate a promise which is resolved after a set amount of time.
+ * Get the number of milliseconds to wait between beginning opening and executing setup process.
  *
- * @param {number} length Number of milliseconds before resolving the promise
- * @return {jQuery.Promise} Promise that will be resolved after a set amount of time
+ * @param {OO.ui.Window} win Window being opened
+ * @param {Object} [data] Window opening data
+ * @return {number} Milliseconds to wait
  */
-OO.ui.Process.static.delay = function ( length ) {
-       var deferred = $.Deferred();
-
-       setTimeout( function () {
-               deferred.resolve();
-       }, length );
+OO.ui.WindowManager.prototype.getSetupDelay = function () {
+       return 0;
+};
 
-       return deferred.promise();
+/**
+ * Get the number of milliseconds to wait between finishing setup and executing ready process.
+ *
+ * @param {OO.ui.Window} win Window being opened
+ * @param {Object} [data] Window opening data
+ * @return {number} Milliseconds to wait
+ */
+OO.ui.WindowManager.prototype.getReadyDelay = function () {
+       return 0;
 };
 
-/* Methods */
+/**
+ * Get the number of milliseconds to wait between beginning closing and executing hold process.
+ *
+ * @param {OO.ui.Window} win Window being closed
+ * @param {Object} [data] Window closing data
+ * @return {number} Milliseconds to wait
+ */
+OO.ui.WindowManager.prototype.getHoldDelay = function () {
+       return 0;
+};
 
 /**
- * Start the process.
+ * Get the number of milliseconds to wait between finishing hold and executing teardown process.
  *
- * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
- *   any of the steps return boolean false or a promise which gets rejected; upon stopping the
- *   process, the remaining steps will not be taken
+ * @param {OO.ui.Window} win Window being closed
+ * @param {Object} [data] Window closing data
+ * @return {number} Milliseconds to wait
  */
-OO.ui.Process.prototype.execute = function () {
-       var i, len, promise;
+OO.ui.WindowManager.prototype.getTeardownDelay = function () {
+       return this.modal ? 250 : 0;
+};
 
-       /**
-        * Continue execution.
-        *
-        * @ignore
-        * @param {Array} step A function and the context it should be called in
-        * @return {Function} Function that continues the process
-        */
-       function proceed( step ) {
-               return function () {
-                       // Execute step in the correct context
-                       var result = step[0].call( step[1] );
+/**
+ * Get managed window by symbolic name.
+ *
+ * If window is not yet instantiated, it will be instantiated and added automatically.
+ *
+ * @param {string} name Symbolic window name
+ * @return {jQuery.Promise} Promise resolved when window is ready to be accessed; when resolved the
+ *   first argument is an OO.ui.Window; when rejected the first argument is an OO.ui.Error
+ * @throws {Error} If the symbolic name is unrecognized by the factory
+ * @throws {Error} If the symbolic name unrecognized as a managed window
+ */
+OO.ui.WindowManager.prototype.getWindow = function ( name ) {
+       var deferred = $.Deferred(),
+               win = this.windows[name];
 
-                       if ( result === false ) {
-                               // Use rejected promise for boolean false results
-                               return $.Deferred().reject().promise();
-                       }
-                       // Duck-type the object to see if it can produce a promise
-                       if ( result && $.isFunction( result.promise ) ) {
-                               // Use a promise generated from the result
-                               return result.promise();
+       if ( !( win instanceof OO.ui.Window ) ) {
+               if ( this.factory ) {
+                       if ( !this.factory.lookup( name ) ) {
+                               deferred.reject( new OO.ui.Error(
+                                       'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
+                               ) );
+                       } else {
+                               win = this.factory.create( name, this, { '$': this.$ } );
+                               this.addWindows( [ win ] ).then(
+                                       OO.ui.bind( deferred.resolve, deferred, win ),
+                                       deferred.reject
+                               );
                        }
-                       // Use resolved promise for other results
-                       return $.Deferred().resolve().promise();
-               };
-       }
-
-       if ( this.steps.length ) {
-               // Generate a chain reaction of promises
-               promise = proceed( this.steps[0] )();
-               for ( i = 1, len = this.steps.length; i < len; i++ ) {
-                       promise = promise.then( proceed( this.steps[i] ) );
+               } else {
+                       deferred.reject( new OO.ui.Error(
+                               'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
+                       ) );
                }
        } else {
-               promise = $.Deferred().resolve().promise();
+               deferred.resolve( win );
        }
 
-       return promise;
+       return deferred.promise();
 };
 
 /**
- * Add step to the beginning of the process.
+ * Get current window.
  *
- * @param {Function} step Function to execute; if it returns boolean false the process will stop; if
- *   it returns an object with a `promise` method the process will use the promise to either
- *   continue to the next step when the promise is resolved or stop when the promise is rejected
- * @param {Object} [context=null] Context to call the step function in
- * @chainable
+ * @return {OO.ui.Window|null} Currently opening/opened/closing window
  */
-OO.ui.Process.prototype.first = function ( step, context ) {
-       this.steps.unshift( [ step, context || null ] );
-       return this;
+OO.ui.WindowManager.prototype.getCurrentWindow = function () {
+       return this.currentWindow;
 };
 
 /**
- * Add step to the end of the process.
+ * Open a window.
  *
- * @param {Function} step Function to execute; if it returns boolean false the process will stop; if
- *   it returns an object with a `promise` method the process will use the promise to either
- *   continue to the next step when the promise is resolved or stop when the promise is rejected
- * @param {Object} [context=null] Context to call the step function in
- * @chainable
+ * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
+ *   for more details about the `opening` promise
+ * @fires opening
  */
-OO.ui.Process.prototype.next = function ( step, context ) {
-       this.steps.push( [ step, context || null ] );
-       return this;
+OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
+       var manager = this,
+               preparing = [],
+               opening = $.Deferred();
+
+       // Argument handling
+       if ( typeof win === 'string' ) {
+               return this.getWindow( win ).then( function ( win ) {
+                       return manager.openWindow( win, data );
+               } );
+       }
+
+       // Error handling
+       if ( !this.hasWindow( win ) ) {
+               opening.reject( new OO.ui.Error(
+                       'Cannot open window: window is not attached to manager'
+               ) );
+       }
+
+       // Window opening
+       if ( opening.state() !== 'rejected' ) {
+               // Begin loading the window if it's not loaded already - may take noticable time and we want
+               // too do this in paralell with any preparatory actions
+               preparing.push( win.load() );
+
+               if ( this.opening || this.opened ) {
+                       // If a window is currently opening or opened, close it first
+                       preparing.push( this.closeWindow( this.currentWindow ) );
+               } else if ( this.closing ) {
+                       // If a window is currently closing, wait for it to complete
+                       preparing.push( this.closing );
+               }
+
+               $.when.apply( $, preparing ).done( function () {
+                       if ( manager.modal ) {
+                               manager.$( manager.getElementDocument() ).on( {
+                                       // Prevent scrolling by keys in top-level window
+                                       'keydown': manager.onDocumentKeyDownHandler
+                               } );
+                               manager.$( manager.getElementWindow() ).on( {
+                                       // Prevent scrolling by wheel in top-level window
+                                       'mousewheel': manager.onWindowMouseWheelHandler,
+                                       // Start listening for top-level window dimension changes
+                                       'orientationchange resize': manager.onWindowResizeHandler
+                               } );
+                               // Hide other content from screen readers
+                               manager.$ariaHidden = $( 'body' )
+                                       .children()
+                                       .not( manager.$element.parentsUntil( 'body' ).last() )
+                                       .attr( 'aria-hidden', '' );
+                       }
+                       manager.currentWindow = win;
+                       manager.opening = opening;
+                       manager.emit( 'opening', win, opening, data );
+                       manager.updateWindowSize( win );
+                       setTimeout( function () {
+                               win.setup( data ).then( function () {
+                                       manager.opening.notify( { 'state': 'setup' } );
+                                       setTimeout( function () {
+                                               win.ready( data ).then( function () {
+                                                       manager.opening.notify( { 'state': 'ready' } );
+                                                       manager.opening = null;
+                                                       manager.opened = $.Deferred();
+                                                       opening.resolve( manager.opened.promise(), data );
+                                               } );
+                                       }, manager.getReadyDelay() );
+                               } );
+                       }, manager.getSetupDelay() );
+               } );
+       }
+
+       return opening;
 };
 
 /**
- * Dialog for showing a confirmation/warning message.
+ * Close a window.
  *
- * @class
- * @extends OO.ui.Dialog
- *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-closing}
+ *   for more details about the `closing` promise
+ * @throws {Error} If no window by that name is being managed
+ * @fires closing
  */
-OO.ui.ConfirmationDialog = function OoUiConfirmationDialog( config ) {
-       // Configuration initialization
-       config = $.extend( { 'size': 'small' }, config );
-
-       // Parent constructor
-       OO.ui.Dialog.call( this, config );
-};
+OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
+       var manager = this,
+               preparing = [],
+               closing = $.Deferred(),
+               opened = this.opened;
+
+       // Argument handling
+       if ( typeof win === 'string' ) {
+               win = this.windows[win];
+       } else if ( !this.hasWindow( win ) ) {
+               win = null;
+       }
+
+       // Error handling
+       if ( !win ) {
+               closing.reject( new OO.ui.Error(
+                       'Cannot close window: window is not attached to manager'
+               ) );
+       } else if ( win !== this.currentWindow ) {
+               closing.reject( new OO.ui.Error(
+                       'Cannot close window: window already closed with different data'
+               ) );
+       } else if ( this.closing ) {
+               closing.reject( new OO.ui.Error(
+                       'Cannot close window: window already closing with different data'
+               ) );
+       }
+
+       // Window closing
+       if ( closing.state() !== 'rejected' ) {
+               if ( this.opening ) {
+                       // If the window is currently opening, close it when it's done
+                       preparing.push( this.opening );
+               }
 
-/* Inheritance */
+               // Close the window
+               $.when.apply( $, preparing ).done( function () {
+                       manager.closing = closing;
+                       manager.emit( 'closing', win, closing, data );
+                       manager.opened = null;
+                       opened.resolve( closing.promise(), data );
+                       setTimeout( function () {
+                               win.hold( data ).then( function () {
+                                       closing.notify( { 'state': 'hold' } );
+                                       setTimeout( function () {
+                                               win.teardown( data ).then( function () {
+                                                       closing.notify( { 'state': 'teardown' } );
+                                                       if ( manager.modal ) {
+                                                               manager.$( manager.getElementDocument() ).off( {
+                                                                       // Allow scrolling by keys in top-level window
+                                                                       'keydown': manager.onDocumentKeyDownHandler
+                                                               } );
+                                                               manager.$( manager.getElementWindow() ).off( {
+                                                                       // Allow scrolling by wheel in top-level window
+                                                                       'mousewheel': manager.onWindowMouseWheelHandler,
+                                                                       // Stop listening for top-level window dimension changes
+                                                                       'orientationchange resize': manager.onWindowResizeHandler
+                                                               } );
+                                                       }
+                                                       // Restore screen reader visiblity
+                                                       if ( manager.$ariaHidden ) {
+                                                               manager.$ariaHidden.removeAttr( 'aria-hidden' );
+                                                               manager.$ariaHidden = null;
+                                                       }
+                                                       manager.closing = null;
+                                                       manager.currentWindow = null;
+                                                       closing.resolve( data );
+                                               } );
+                                       }, manager.getTeardownDelay() );
+                               } );
+                       }, manager.getHoldDelay() );
+               } );
+       }
 
-OO.inheritClass( OO.ui.ConfirmationDialog, OO.ui.Dialog );
+       return closing;
+};
 
-/* Static Properties */
+/**
+ * Add windows.
+ *
+ * If the window manager is attached to the DOM then windows will be automatically loaded as they
+ * are added.
+ *
+ * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
+ * @return {jQuery.Promise} Promise resolved when all windows are added
+ * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
+ *   a statically configured symbolic name
+ */
+OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
+       var i, len, win, name, list,
+               promises = [];
 
-OO.ui.ConfirmationDialog.static.name = 'confirm';
+       if ( $.isArray( windows ) ) {
+               // Convert to map of windows by looking up symbolic names from static configuration
+               list = {};
+               for ( i = 0, len = windows.length; i < len; i++ ) {
+                       name = windows[i].constructor.static.name;
+                       if ( typeof name !== 'string' ) {
+                               throw new Error( 'Cannot add window' );
+                       }
+                       list[name] = windows[i];
+               }
+       } else if ( $.isPlainObject( windows ) ) {
+               list = windows;
+       }
 
-OO.ui.ConfirmationDialog.static.icon = 'help';
+       // Add windows
+       for ( name in list ) {
+               win = list[name];
+               this.windows[name] = win;
+               this.$element.append( win.$element );
 
-OO.ui.ConfirmationDialog.static.title = OO.ui.deferMsg( 'ooui-dialog-confirm-title' );
+               if ( this.isElementAttached() ) {
+                       promises.push( win.load() );
+               }
+       }
 
-/* Methods */
+       return $.when.apply( $, promises );
+};
 
 /**
- * @inheritdoc
+ * Remove windows.
+ *
+ * Windows will be closed before they are removed.
+ *
+ * @param {string} name Symbolic name of window to remove
+ * @return {jQuery.Promise} Promise resolved when window is closed and removed
+ * @throws {Error} If windows being removed are not being managed
  */
-OO.ui.ConfirmationDialog.prototype.initialize = function () {
-       // Parent method
-       OO.ui.Dialog.prototype.initialize.call( this );
-
-       // Set up the layout
-       var contentLayout = new OO.ui.PanelLayout( {
-               '$': this.$,
-               'padded': true
-       } );
-
-       this.$promptContainer = this.$( '<div>' ).addClass( 'oo-ui-dialog-confirm-promptContainer' );
-
-       this.cancelButton = new OO.ui.ButtonWidget();
-       this.cancelButton.connect( this, { 'click': [ 'close', 'cancel' ] } );
-
-       this.okButton = new OO.ui.ButtonWidget();
-       this.okButton.connect( this, { 'click': [ 'close', 'ok' ] } );
+OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
+       var i, len, win, name,
+               manager = this,
+               promises = [],
+               cleanup = function ( name, win ) {
+                       delete manager.windows[name];
+                       win.$element.detach();
+               };
 
-       // Make the buttons
-       contentLayout.$element.append( this.$promptContainer );
-       this.$body.append( contentLayout.$element );
+       for ( i = 0, len = names.length; i < len; i++ ) {
+               name = names[i];
+               win = this.windows[name];
+               if ( !win ) {
+                       throw new Error( 'Cannot remove window' );
+               }
+               promises.push( this.closeWindow( name ).then( OO.ui.bind( cleanup, null, name, win ) ) );
+       }
 
-       this.$foot.append(
-               this.okButton.$element,
-               this.cancelButton.$element
-       );
+       return $.when.apply( $, promises );
 };
 
-/*
- * Setup a confirmation dialog.
+/**
+ * Remove all windows.
  *
- * @param {Object} [data] Window opening data including text of the dialog and text for the buttons
- * @param {jQuery|string} [data.prompt] Text to display or list of nodes to use as content of the dialog.
- * @param {jQuery|string|Function|null} [data.okLabel] Label of the OK button
- * @param {jQuery|string|Function|null} [data.cancelLabel] Label of the cancel button
- * @param {string|string[]} [data.okFlags="constructive"] Flags for the OK button
- * @param {string|string[]} [data.cancelFlags="destructive"] Flags for the cancel button
- * @return {OO.ui.Process} Setup process
+ * Windows will be closed before they are removed.
+ *
+ * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
  */
-OO.ui.ConfirmationDialog.prototype.getSetupProcess = function ( data ) {
-       // Parent method
-       return OO.ui.ConfirmationDialog.super.prototype.getSetupProcess.call( this, data )
-               .next( function () {
-                       var prompt = data.prompt || OO.ui.deferMsg( 'ooui-dialog-confirm-default-prompt' ),
-                               okLabel = data.okLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-ok' ),
-                               cancelLabel = data.cancelLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-cancel' ),
-                               okFlags = data.okFlags || 'constructive',
-                               cancelFlags = data.cancelFlags || 'destructive';
-
-                       if ( typeof prompt === 'string' ) {
-                               this.$promptContainer.text( prompt );
-                       } else {
-                               this.$promptContainer.empty().append( prompt );
-                       }
-
-                       this.okButton.setLabel( okLabel ).clearFlags().setFlags( okFlags );
-                       this.cancelButton.setLabel( cancelLabel ).clearFlags().setFlags( cancelFlags );
-               }, this );
+OO.ui.WindowManager.prototype.clearWindows = function () {
+       return this.removeWindows( Object.keys( this.windows ) );
 };
 
 /**
- * @inheritdoc
+ * Set dialog size.
+ *
+ * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
+ *
+ * @chainable
  */
-OO.ui.ConfirmationDialog.prototype.getTeardownProcess = function ( data ) {
-       // Parent method
-       return OO.ui.ConfirmationDialog.super.prototype.getTeardownProcess.call( this, data )
-               .first( function () {
-                       if ( data === 'ok' ) {
-                               this.opened.resolve();
-                       } else { // data === 'cancel', or no data
-                               this.opened.reject();
-                       }
-               }, this );
+OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
+       // Bypass for non-current, and thus invisible, windows
+       if ( win !== this.currentWindow ) {
+               return;
+       }
+
+       var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ),
+               sizes = this.constructor.static.sizes,
+               size = win.getSize();
+
+       if ( !sizes[size] ) {
+               size = this.constructor.static.defaultSize;
+       }
+       if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) {
+               size = 'full';
+       }
+
+       this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
+       this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
+       win.setDimensions( sizes[size] );
+
+       return this;
 };
 
 /**
- * Element with a button.
- *
  * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $button Button node, assigned to #$button
+ * @param {string|jQuery} message Description of error
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [frameless] Render button without a frame
- * @cfg {number} [tabIndex=0] Button's tab index, use -1 to prevent tab focusing
+ * @cfg {boolean} [recoverable=true] Error is recoverable
  */
-OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
+OO.ui.Error = function OoUiElement( message, config ) {
        // Configuration initialization
        config = config || {};
 
        // Properties
-       this.$button = $button;
-       this.tabIndex = null;
-       this.active = false;
-       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
-
-       // Events
-       this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
-
-       // Initialization
-       this.$element
-               .addClass( 'oo-ui-buttonedElement' )
-               .prop( 'tabIndex', config.tabIndex || 0 );
-       this.$button
-               .addClass( 'oo-ui-buttonedElement-button' )
-               .attr( 'role', 'button' );
-       if ( config.frameless ) {
-               this.$element.addClass( 'oo-ui-buttonedElement-frameless' );
-       } else {
-               this.$element.addClass( 'oo-ui-buttonedElement-framed' );
-       }
+       this.message = message instanceof jQuery ? message : String( message );
+       this.recoverable = config.recoverable === undefined || !!config.recoverable;
 };
 
 /* Setup */
 
-OO.initClass( OO.ui.ButtonedElement );
-
-/* Static Properties */
-
-/**
- * Cancel mouse down events.
- *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true;
+OO.initClass( OO.ui.Error );
 
 /* Methods */
 
 /**
- * Handles mouse down events.
+ * Check if error can be recovered from.
  *
- * @param {jQuery.Event} e Mouse down event
+ * @return {boolean} Error is recoverable
  */
-OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
-               return false;
-       }
-       // tabIndex should generally be interacted with via the property, but it's not possible to
-       // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute
-       this.tabIndex = this.$button.attr( 'tabindex' );
-       this.$button
-               // Remove the tab-index while the button is down to prevent the button from stealing focus
-               .removeAttr( 'tabindex' )
-               .addClass( 'oo-ui-buttonedElement-pressed' );
-       // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
-       // reliably reapply the tabindex and remove the pressed class
-       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
-       // Prevent change of focus unless specifically configured otherwise
-       if ( this.constructor.static.cancelButtonMouseDownEvents ) {
-               return false;
-       }
+OO.ui.Error.prototype.isRecoverable = function () {
+       return this.recoverable;
 };
 
 /**
- * Handles mouse up events.
+ * Get error message as DOM nodes.
  *
- * @param {jQuery.Event} e Mouse up event
+ * @return {jQuery} Error message in DOM nodes
  */
-OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
-               return false;
-       }
-       this.$button
-               // Restore the tab-index after the button is up to restore the button's accesssibility
-               .attr( 'tabindex', this.tabIndex )
-               .removeClass( 'oo-ui-buttonedElement-pressed' );
-       // Stop listening for mouseup, since we only needed this once
-       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+OO.ui.Error.prototype.getMessage = function () {
+       return this.message instanceof jQuery ?
+               this.message.clone() :
+               $( '<div>' ).text( this.message ).contents();
 };
 
 /**
- * Set active state.
+ * Get error message as text.
  *
- * @param {boolean} [value] Make button active
- * @chainable
+ * @return {string} Error message
  */
-OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
-       this.$button.toggleClass( 'oo-ui-buttonedElement-active', !!value );
-       return this;
+OO.ui.Error.prototype.getMessageText = function () {
+       return this.message instanceof jQuery ? this.message.text() : this.message;
 };
 
 /**
- * Element that can be automatically clipped to visible boundaies.
+ * A list of functions, called in sequence.
+ *
+ * If a function added to a process returns boolean false the process will stop; if it returns an
+ * object with a `promise` method the process will use the promise to either continue to the next
+ * step when the promise is resolved or stop when the promise is rejected.
  *
- * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
- * @param {Object} [config] Configuration options
+ * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
+ *   call, see #createStep for more information
+ * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
+ *   or a promise
+ * @return {Object} Step object, with `callback` and `context` properties
  */
-OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) {
-       // Configuration initialization
-       config = config || {};
-
+OO.ui.Process = function ( step, context ) {
        // Properties
-       this.$clippable = $clippable;
-       this.clipping = false;
-       this.clipped = false;
-       this.$clippableContainer = null;
-       this.$clippableScroller = null;
-       this.$clippableWindow = null;
-       this.idealWidth = null;
-       this.idealHeight = null;
-       this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
-       this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
+       this.steps = [];
 
        // Initialization
-       this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
+       if ( step !== undefined ) {
+               this.next( step, context );
+       }
 };
 
+/* Setup */
+
+OO.initClass( OO.ui.Process );
+
 /* Methods */
 
 /**
- * Set clipping.
+ * Start the process.
  *
- * @param {boolean} value Enable clipping
- * @chainable
+ * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
+ *   any of the steps return boolean false or a promise which gets rejected; upon stopping the
+ *   process, the remaining steps will not be taken
  */
-OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
-       value = !!value;
+OO.ui.Process.prototype.execute = function () {
+       var i, len, promise;
 
-       if ( this.clipping !== value ) {
-               this.clipping = value;
-               if ( this.clipping ) {
-                       this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
-                       // If the clippable container is the body, we have to listen to scroll events and check
-                       // jQuery.scrollTop on the window because of browser inconsistencies
-                       this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
-                               this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
-                               this.$clippableContainer;
-                       this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
-                       this.$clippableWindow = this.$( this.getElementWindow() )
-                               .on( 'resize', this.onClippableWindowResizeHandler );
-                       // Initial clip after visible
-                       setTimeout( OO.ui.bind( this.clip, this ) );
-               } else {
-                       this.$clippableContainer = null;
-                       this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
-                       this.$clippableScroller = null;
-                       this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
-                       this.$clippableWindow = null;
+       /**
+        * Continue execution.
+        *
+        * @ignore
+        * @param {Array} step A function and the context it should be called in
+        * @return {Function} Function that continues the process
+        */
+       function proceed( step ) {
+               return function () {
+                       // Execute step in the correct context
+                       var deferred,
+                               result = step.callback.call( step.context );
+
+                       if ( result === false ) {
+                               // Use rejected promise for boolean false results
+                               return $.Deferred().reject( [] ).promise();
+                       }
+                       if ( typeof result === 'number' ) {
+                               if ( result < 0 ) {
+                                       throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
+                               }
+                               // Use a delayed promise for numbers, expecting them to be in milliseconds
+                               deferred = $.Deferred();
+                               setTimeout( deferred.resolve, result );
+                               return deferred.promise();
+                       }
+                       if ( result instanceof OO.ui.Error ) {
+                               // Use rejected promise for error
+                               return $.Deferred().reject( [ result ] ).promise();
+                       }
+                       if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) {
+                               // Use rejected promise for list of errors
+                               return $.Deferred().reject( result ).promise();
+                       }
+                       // Duck-type the object to see if it can produce a promise
+                       if ( result && $.isFunction( result.promise ) ) {
+                               // Use a promise generated from the result
+                               return result.promise();
+                       }
+                       // Use resolved promise for other results
+                       return $.Deferred().resolve().promise();
+               };
+       }
+
+       if ( this.steps.length ) {
+               // Generate a chain reaction of promises
+               promise = proceed( this.steps[0] )();
+               for ( i = 1, len = this.steps.length; i < len; i++ ) {
+                       promise = promise.then( proceed( this.steps[i] ) );
                }
+       } else {
+               promise = $.Deferred().resolve().promise();
        }
 
-       return this;
+       return promise;
 };
 
 /**
- * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ * Create a process step.
  *
- * @return {boolean} Element will be clipped to the visible area
- */
-OO.ui.ClippableElement.prototype.isClipping = function () {
-       return this.clipping;
+ * @private
+ * @param {number|jQuery.Promise|Function} step
+ *
+ * - Number of milliseconds to wait; or
+ * - Promise to wait to be resolved; or
+ * - Function to execute
+ *   - If it returns boolean false the process will stop
+ *   - If it returns an object with a `promise` method the process will use the promise to either
+ *     continue to the next step when the promise is resolved or stop when the promise is rejected
+ *   - If it returns a number, the process will wait for that number of milliseconds before
+ *     proceeding
+ * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
+ *   or a promise
+ * @return {Object} Step object, with `callback` and `context` properties
+ */
+OO.ui.Process.prototype.createStep = function ( step, context ) {
+       if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
+               return {
+                       'callback': function () {
+                               return step;
+                       },
+                       'context': null
+               };
+       }
+       if ( $.isFunction( step ) ) {
+               return {
+                       'callback': step,
+                       'context': context
+               };
+       }
+       throw new Error( 'Cannot create process step: number, promise or function expected' );
 };
 
 /**
- * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ * Add step to the beginning of the process.
  *
- * @return {boolean} Part of the element is being clipped
+ * @inheritdoc #createStep
+ * @return {OO.ui.Process} this
+ * @chainable
  */
-OO.ui.ClippableElement.prototype.isClipped = function () {
-       return this.clipped;
-};
-
-/**
- * Set the ideal size.
- *
- * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
- * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
- */
-OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
-       this.idealWidth = width;
-       this.idealHeight = height;
+OO.ui.Process.prototype.first = function ( step, context ) {
+       this.steps.unshift( this.createStep( step, context ) );
+       return this;
 };
 
 /**
- * Clip element to visible boundaries and allow scrolling when needed.
- *
- * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
- * overlapped by, the visible area of the nearest scrollable container.
+ * Add step to the end of the process.
  *
+ * @inheritdoc #createStep
+ * @return {OO.ui.Process} this
  * @chainable
  */
-OO.ui.ClippableElement.prototype.clip = function () {
-       if ( !this.clipping ) {
-               // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
-               return this;
-       }
-
-       var buffer = 10,
-               cOffset = this.$clippable.offset(),
-               ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 },
-               ccHeight = this.$clippableContainer.innerHeight() - buffer,
-               ccWidth = this.$clippableContainer.innerWidth() - buffer,
-               scrollTop = this.$clippableScroller.scrollTop(),
-               scrollLeft = this.$clippableScroller.scrollLeft(),
-               desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
-               desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
-               naturalWidth = this.$clippable.prop( 'scrollWidth' ),
-               naturalHeight = this.$clippable.prop( 'scrollHeight' ),
-               clipWidth = desiredWidth < naturalWidth,
-               clipHeight = desiredHeight < naturalHeight;
-
-       if ( clipWidth ) {
-               this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } );
-       } else {
-               this.$clippable.css( 'width', this.idealWidth || '' );
-               this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
-               this.$clippable.css( 'overflow-x', '' );
-       }
-       if ( clipHeight ) {
-               this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
-       } else {
-               this.$clippable.css( 'height', this.idealHeight || '' );
-               this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
-               this.$clippable.css( 'overflow-y', '' );
-       }
-
-       this.clipped = clipWidth || clipHeight;
-
+OO.ui.Process.prototype.next = function ( step, context ) {
+       this.steps.push( this.createStep( step, context ) );
        return this;
 };
 
 /**
- * Element with named flags that can be added, removed, listed and checked.
- *
- * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with
- * the flag name. Flags are primarily useful for styling.
+ * Factory for tools.
  *
- * @abstract
  * @class
- *
+ * @extends OO.Factory
  * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
  */
-OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) {
-       // Config initialization
-       config = config || {};
+OO.ui.ToolFactory = function OoUiToolFactory() {
+       // Parent constructor
+       OO.ui.ToolFactory.super.call( this );
+};
 
-       // Properties
-       this.flags = {};
+/* Setup */
 
-       // Initialization
-       this.setFlags( config.flags );
-};
+OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
 
 /* Methods */
 
-/**
- * Check if a flag is set.
- *
- * @param {string} flag Name of flag
- * @return {boolean} Has flag
- */
-OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
-       return flag in this.flags;
-};
+/** */
+OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
+       var i, len, included, promoted, demoted,
+               auto = [],
+               used = {};
 
-/**
- * Get the names of all flags set.
- *
- * @return {string[]} flags Flag names
- */
-OO.ui.FlaggableElement.prototype.getFlags = function () {
-       return Object.keys( this.flags );
-};
+       // Collect included and not excluded tools
+       included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
 
-/**
- * Clear all flags.
- *
- * @chainable
- */
-OO.ui.FlaggableElement.prototype.clearFlags = function () {
-       var flag,
-               classPrefix = 'oo-ui-flaggableElement-';
+       // Promotion
+       promoted = this.extract( promote, used );
+       demoted = this.extract( demote, used );
 
-       for ( flag in this.flags ) {
-               delete this.flags[flag];
-               this.$element.removeClass( classPrefix + flag );
+       // Auto
+       for ( i = 0, len = included.length; i < len; i++ ) {
+               if ( !used[included[i]] ) {
+                       auto.push( included[i] );
+               }
        }
 
-       return this;
+       return promoted.concat( auto ).concat( demoted );
 };
 
 /**
- * Add one or more flags.
+ * Get a flat list of names from a list of names or groups.
  *
- * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
- *  keyed by flag name containing boolean set/remove instructions.
- * @chainable
+ * Tools can be specified in the following ways:
+ *
+ * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
+ * - All tools in a group: `{ 'group': 'group-name' }`
+ * - All tools: `'*'`
+ *
+ * @private
+ * @param {Array|string} collection List of tools
+ * @param {Object} [used] Object with names that should be skipped as properties; extracted
+ *  names will be added as properties
+ * @return {string[]} List of extracted names
  */
-OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) {
-       var i, len, flag,
-               classPrefix = 'oo-ui-flaggableElement-';
+OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
+       var i, len, item, name, tool,
+               names = [];
 
-       if ( typeof flags === 'string' ) {
-               // Set
-               this.flags[flags] = true;
-               this.$element.addClass( classPrefix + flags );
-       } else if ( $.isArray( flags ) ) {
-               for ( i = 0, len = flags.length; i < len; i++ ) {
-                       flag = flags[i];
-                       // Set
-                       this.flags[flag] = true;
-                       this.$element.addClass( classPrefix + flag );
+       if ( collection === '*' ) {
+               for ( name in this.registry ) {
+                       tool = this.registry[name];
+                       if (
+                               // Only add tools by group name when auto-add is enabled
+                               tool.static.autoAddToCatchall &&
+                               // Exclude already used tools
+                               ( !used || !used[name] )
+                       ) {
+                               names.push( name );
+                               if ( used ) {
+                                       used[name] = true;
+                               }
+                       }
                }
-       } else if ( OO.isPlainObject( flags ) ) {
-               for ( flag in flags ) {
-                       if ( flags[flag] ) {
-                               // Set
-                               this.flags[flag] = true;
-                               this.$element.addClass( classPrefix + flag );
-                       } else {
-                               // Remove
-                               delete this.flags[flag];
-                               this.$element.removeClass( classPrefix + flag );
+       } else if ( $.isArray( collection ) ) {
+               for ( i = 0, len = collection.length; i < len; i++ ) {
+                       item = collection[i];
+                       // Allow plain strings as shorthand for named tools
+                       if ( typeof item === 'string' ) {
+                               item = { 'name': item };
+                       }
+                       if ( OO.isPlainObject( item ) ) {
+                               if ( item.group ) {
+                                       for ( name in this.registry ) {
+                                               tool = this.registry[name];
+                                               if (
+                                                       // Include tools with matching group
+                                                       tool.static.group === item.group &&
+                                                       // Only add tools by group name when auto-add is enabled
+                                                       tool.static.autoAddToGroup &&
+                                                       // Exclude already used tools
+                                                       ( !used || !used[name] )
+                                               ) {
+                                                       names.push( name );
+                                                       if ( used ) {
+                                                               used[name] = true;
+                                                       }
+                                               }
+                                       }
+                               // Include tools with matching name and exclude already used tools
+                               } else if ( item.name && ( !used || !used[item.name] ) ) {
+                                       names.push( item.name );
+                                       if ( used ) {
+                                               used[item.name] = true;
+                                       }
+                               }
                        }
                }
        }
-       return this;
+       return names;
 };
 
 /**
- * Element containing a sequence of child elements.
+ * Factory for tool groups.
  *
- * @abstract
  * @class
- *
+ * @extends OO.Factory
  * @constructor
- * @param {jQuery} $group Container node, assigned to #$group
- * @param {Object} [config] Configuration options
  */
-OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
-       // Configuration
-       config = config || {};
+OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
+       // Parent constructor
+       OO.Factory.call( this );
 
-       // Properties
-       this.$group = $group;
-       this.items = [];
-       this.aggregateItemEvents = {};
+       var i, l,
+               defaultClasses = this.constructor.static.getDefaultClasses();
+
+       // Register default toolgroups
+       for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
+               this.register( defaultClasses[i] );
+       }
 };
 
-/* Methods */
+/* Setup */
 
-/**
- * Get items.
- *
- * @return {OO.ui.Element[]} Items
- */
-OO.ui.GroupElement.prototype.getItems = function () {
-       return this.items.slice( 0 );
-};
+OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
+
+/* Static Methods */
 
 /**
- * Add an aggregate item event.
+ * Get a default set of classes to be registered on construction
  *
- * Aggregated events are listened to on each item and then emitted by the group under a new name,
- * and with an additional leading parameter containing the item that emitted the original event.
- * Other arguments that were emitted from the original event are passed through.
+ * @return {Function[]} Default classes
+ */
+OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
+       return [
+               OO.ui.BarToolGroup,
+               OO.ui.ListToolGroup,
+               OO.ui.MenuToolGroup
+       ];
+};
+
+/**
+ * Element with a button.
  *
- * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
- *   event, use null value to remove aggregation
- * @throws {Error} If aggregation already exists
+ * Buttons are used for controls which can be clicked. They can be configured to use tab indexing
+ * and access keys for accessibility purposes.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {jQuery} $button Button node, assigned to #$button
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [framed=true] Render button with a frame
+ * @cfg {number} [tabIndex=0] Button's tab index, use null to have no tabIndex
+ * @cfg {string} [accessKey] Button's access key
  */
-OO.ui.GroupElement.prototype.aggregate = function ( events ) {
-       var i, len, item, add, remove, itemEvent, groupEvent;
+OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
+       // Configuration initialization
+       config = config || {};
 
-       for ( itemEvent in events ) {
-               groupEvent = events[itemEvent];
+       // Properties
+       this.$button = $button;
+       this.tabIndex = null;
+       this.framed = null;
+       this.active = false;
+       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
 
-               // Remove existing aggregated event
-               if ( itemEvent in this.aggregateItemEvents ) {
-                       // Don't allow duplicate aggregations
-                       if ( groupEvent ) {
-                               throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
-                       }
-                       // Remove event aggregation from existing items
-                       for ( i = 0, len = this.items.length; i < len; i++ ) {
-                               item = this.items[i];
-                               if ( item.connect && item.disconnect ) {
-                                       remove = {};
-                                       remove[itemEvent] = [ 'emit', groupEvent, item ];
-                                       item.disconnect( this, remove );
-                               }
-                       }
-                       // Prevent future items from aggregating event
-                       delete this.aggregateItemEvents[itemEvent];
-               }
+       // Events
+       this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
 
-               // Add new aggregate event
-               if ( groupEvent ) {
-                       // Make future items aggregate event
-                       this.aggregateItemEvents[itemEvent] = groupEvent;
-                       // Add event aggregation to existing items
-                       for ( i = 0, len = this.items.length; i < len; i++ ) {
-                               item = this.items[i];
-                               if ( item.connect && item.disconnect ) {
-                                       add = {};
-                                       add[itemEvent] = [ 'emit', groupEvent, item ];
-                                       item.connect( this, add );
-                               }
-                       }
-               }
-       }
+       // Initialization
+       this.$element.addClass( 'oo-ui-buttonedElement' );
+       this.$button
+               .addClass( 'oo-ui-buttonedElement-button' )
+               .attr( 'role', 'button' );
+       this.setTabIndex( config.tabIndex || 0 );
+       this.setAccessKey( config.accessKey );
+       this.toggleFramed( config.framed === undefined || config.framed );
 };
 
+/* Setup */
+
+OO.initClass( OO.ui.ButtonedElement );
+
+/* Static Properties */
+
 /**
- * Add items.
+ * Cancel mouse down events.
  *
- * @param {OO.ui.Element[]} items Item
- * @param {number} [index] Index to insert items at
- * @chainable
+ * @static
+ * @inheritable
+ * @property {boolean}
  */
-OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
-       var i, len, item, event, events, currentIndex,
-               itemElements = [];
+OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true;
 
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
+/* Methods */
 
-               // Check if item exists then remove it first, effectively "moving" it
-               currentIndex = $.inArray( item, this.items );
-               if ( currentIndex >= 0 ) {
-                       this.removeItems( [ item ] );
-                       // Adjust index to compensate for removal
-                       if ( currentIndex < index ) {
-                               index--;
-                       }
-               }
-               // Add the item
-               if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
-                       events = {};
-                       for ( event in this.aggregateItemEvents ) {
-                               events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
-                       }
-                       item.connect( this, events );
-               }
-               item.setElementGroup( this );
-               itemElements.push( item.$element.get( 0 ) );
+/**
+ * Handles mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
+       if ( this.isDisabled() || e.which !== 1 ) {
+               return false;
        }
-
-       if ( index === undefined || index < 0 || index >= this.items.length ) {
-               this.$group.append( itemElements );
-               this.items.push.apply( this.items, items );
-       } else if ( index === 0 ) {
-               this.$group.prepend( itemElements );
-               this.items.unshift.apply( this.items, items );
-       } else {
-               this.items[index].$element.before( itemElements );
-               this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
+       // tabIndex should generally be interacted with via the property, but it's not possible to
+       // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute
+       this.tabIndex = this.$button.attr( 'tabindex' );
+       this.$button
+               // Remove the tab-index while the button is down to prevent the button from stealing focus
+               .removeAttr( 'tabindex' )
+               .addClass( 'oo-ui-buttonedElement-pressed' );
+       // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
+       // reliably reapply the tabindex and remove the pressed class
+       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
+       // Prevent change of focus unless specifically configured otherwise
+       if ( this.constructor.static.cancelButtonMouseDownEvents ) {
+               return false;
        }
-
-       return this;
 };
 
 /**
- * Remove items.
+ * Handles mouse up events.
  *
- * Items will be detached, not removed, so they can be used later.
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
+       if ( this.isDisabled() || e.which !== 1 ) {
+               return false;
+       }
+       this.$button
+               // Restore the tab-index after the button is up to restore the button's accesssibility
+               .attr( 'tabindex', this.tabIndex )
+               .removeClass( 'oo-ui-buttonedElement-pressed' );
+       // Stop listening for mouseup, since we only needed this once
+       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+};
+
+/**
+ * Toggle frame.
  *
- * @param {OO.ui.Element[]} items Items to remove
+ * @param {boolean} [framed] Make button framed, omit to toggle
  * @chainable
  */
-OO.ui.GroupElement.prototype.removeItems = function ( items ) {
-       var i, len, item, index, remove, itemEvent;
-
-       // Remove specific items
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
-               index = $.inArray( item, this.items );
-               if ( index !== -1 ) {
-                       if (
-                               item.connect && item.disconnect &&
-                               !$.isEmptyObject( this.aggregateItemEvents )
-                       ) {
-                               remove = {};
-                               if ( itemEvent in this.aggregateItemEvents ) {
-                                       remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
-                               }
-                               item.disconnect( this, remove );
-                       }
-                       item.setElementGroup( null );
-                       this.items.splice( index, 1 );
-                       item.$element.detach();
-               }
+OO.ui.ButtonedElement.prototype.toggleFramed = function ( framed ) {
+       framed = framed === undefined ? !this.framed : !!framed;
+       if ( framed !== this.framed ) {
+               this.framed = framed;
+               this.$element
+                       .toggleClass( 'oo-ui-buttonedElement-frameless', !framed )
+                       .toggleClass( 'oo-ui-buttonedElement-framed', framed );
        }
 
        return this;
 };
 
 /**
- * Clear all items.
- *
- * Items will be detached, not removed, so they can be used later.
+ * Set tab index.
  *
+ * @param {number|null} tabIndex Button's tab index, use null to remove
  * @chainable
  */
-OO.ui.GroupElement.prototype.clearItems = function () {
-       var i, len, item, remove, itemEvent;
+OO.ui.ButtonedElement.prototype.setTabIndex = function ( tabIndex ) {
+       if ( typeof tabIndex === 'number' && tabIndex >= 0 ) {
+               this.$button.attr( 'tabindex', tabIndex );
+       } else {
+               this.$button.removeAttr( 'tabindex' );
+       }
+       return this;
+};
 
-       // Remove all items
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               item = this.items[i];
-               if (
-                       item.connect && item.disconnect &&
-                       !$.isEmptyObject( this.aggregateItemEvents )
-               ) {
-                       remove = {};
-                       if ( itemEvent in this.aggregateItemEvents ) {
-                               remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
-                       }
-                       item.disconnect( this, remove );
-               }
-               item.setElementGroup( null );
-               item.$element.detach();
+/**
+ * Set access key
+ *
+ * @param {string} accessKey Button's access key, use empty string to remove
+ * @chainable
+ */
+OO.ui.ButtonedElement.prototype.setAccessKey = function ( accessKey ) {
+       if ( typeof accessKey === 'string' && accessKey.length ) {
+               this.$button.attr( 'accesskey', accessKey );
+       } else {
+               this.$button.removeAttr( 'accesskey' );
        }
+       return this;
+};
 
-       this.items = [];
+/**
+ * Set active state.
+ *
+ * @param {boolean} [value] Make button active
+ * @chainable
+ */
+OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
+       this.$button.toggleClass( 'oo-ui-buttonedElement-active', !!value );
        return this;
 };
 
 /**
- * Element containing an icon.
+ * Element that can be automatically clipped to visible boundaies.
  *
  * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $icon Icon node, assigned to #$icon
+ * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
  * @param {Object} [config] Configuration options
- * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
- *  use the 'default' key to specify the icon to be used when there is no icon in the user's
- *  language
  */
-OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) {
-       // Config intialization
+OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) {
+       // Configuration initialization
        config = config || {};
 
        // Properties
-       this.$icon = $icon;
-       this.icon = null;
+       this.$clippable = $clippable;
+       this.clipping = false;
+       this.clipped = false;
+       this.$clippableContainer = null;
+       this.$clippableScroller = null;
+       this.$clippableWindow = null;
+       this.idealWidth = null;
+       this.idealHeight = null;
+       this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
+       this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
 
        // Initialization
-       this.$icon.addClass( 'oo-ui-iconedElement-icon' );
-       this.setIcon( config.icon || this.constructor.static.icon );
+       this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
 };
 
-/* Setup */
+/* Methods */
 
-OO.initClass( OO.ui.IconedElement );
+/**
+ * Set clipping.
+ *
+ * @param {boolean} value Enable clipping
+ * @chainable
+ */
+OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
+       value = !!value;
 
-/* Static Properties */
+       if ( this.clipping !== value ) {
+               this.clipping = value;
+               if ( this.clipping ) {
+                       this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
+                       // If the clippable container is the body, we have to listen to scroll events and check
+                       // jQuery.scrollTop on the window because of browser inconsistencies
+                       this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
+                               this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
+                               this.$clippableContainer;
+                       this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
+                       this.$clippableWindow = this.$( this.getElementWindow() )
+                               .on( 'resize', this.onClippableWindowResizeHandler );
+                       // Initial clip after visible
+                       setTimeout( OO.ui.bind( this.clip, this ) );
+               } else {
+                       this.$clippableContainer = null;
+                       this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
+                       this.$clippableScroller = null;
+                       this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
+                       this.$clippableWindow = null;
+               }
+       }
+
+       return this;
+};
 
 /**
- * Icon.
- *
- * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
- *
- * For i18n purposes, this property can be an object containing a `default` icon name property and
- * additional icon names keyed by language code.
+ * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
  *
- * Example of i18n icon definition:
- *     { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' }
+ * @return {boolean} Element will be clipped to the visible area
+ */
+OO.ui.ClippableElement.prototype.isClipping = function () {
+       return this.clipping;
+};
+
+/**
+ * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
  *
- * @static
- * @inheritable
- * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
- *  use the 'default' key to specify the icon to be used when there is no icon in the user's
- *  language
+ * @return {boolean} Part of the element is being clipped
  */
-OO.ui.IconedElement.static.icon = null;
+OO.ui.ClippableElement.prototype.isClipped = function () {
+       return this.clipped;
+};
 
-/* Methods */
+/**
+ * Set the ideal size.
+ *
+ * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
+ * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
+ */
+OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
+       this.idealWidth = width;
+       this.idealHeight = height;
+};
 
 /**
- * Set icon.
+ * Clip element to visible boundaries and allow scrolling when needed.
+ *
+ * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
+ * overlapped by, the visible area of the nearest scrollable container.
  *
- * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID;
- *  use the 'default' key to specify the icon to be used when there is no icon in the user's
- *  language
  * @chainable
  */
-OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
-       icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
+OO.ui.ClippableElement.prototype.clip = function () {
+       if ( !this.clipping ) {
+               // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
+               return this;
+       }
 
-       if ( this.icon ) {
-               this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+       var buffer = 10,
+               cOffset = this.$clippable.offset(),
+               ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 },
+               ccHeight = this.$clippableContainer.innerHeight() - buffer,
+               ccWidth = this.$clippableContainer.innerWidth() - buffer,
+               scrollTop = this.$clippableScroller.scrollTop(),
+               scrollLeft = this.$clippableScroller.scrollLeft(),
+               desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
+               desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
+               naturalWidth = this.$clippable.prop( 'scrollWidth' ),
+               naturalHeight = this.$clippable.prop( 'scrollHeight' ),
+               clipWidth = desiredWidth < naturalWidth,
+               clipHeight = desiredHeight < naturalHeight;
+
+       if ( clipWidth ) {
+               this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } );
+       } else {
+               this.$clippable.css( 'width', this.idealWidth || '' );
+               this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
+               this.$clippable.css( 'overflow-x', '' );
        }
-       if ( typeof icon === 'string' ) {
-               icon = icon.trim();
-               if ( icon.length ) {
-                       this.$icon.addClass( 'oo-ui-icon-' + icon );
-                       this.icon = icon;
-               }
+       if ( clipHeight ) {
+               this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
+       } else {
+               this.$clippable.css( 'height', this.idealHeight || '' );
+               this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
+               this.$clippable.css( 'overflow-y', '' );
        }
-       this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon );
+
+       this.clipped = clipWidth || clipHeight;
 
        return this;
 };
 
 /**
- * Get icon.
+ * Element with named flags that can be added, removed, listed and checked.
  *
- * @return {string} Icon
- */
-OO.ui.IconedElement.prototype.getIcon = function () {
-       return this.icon;
-};
-
-/**
- * Element containing an indicator.
+ * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with
+ * the flag name. Flags are primarily useful for styling.
  *
  * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $indicator Indicator node, assigned to #$indicator
  * @param {Object} [config] Configuration options
- * @cfg {string} [indicator] Symbolic indicator name
- * @cfg {string} [indicatorTitle] Indicator title text or a function that return text
+ * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
  */
-OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
-       // Config intialization
+OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) {
+       // Config initialization
        config = config || {};
 
        // Properties
-       this.$indicator = $indicator;
-       this.indicator = null;
-       this.indicatorLabel = null;
+       this.flags = {};
 
        // Initialization
-       this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
-       this.setIndicator( config.indicator || this.constructor.static.indicator );
-       this.setIndicatorTitle( config.indicatorTitle  || this.constructor.static.indicatorTitle );
+       this.setFlags( config.flags );
 };
 
-/* Setup */
+/* Events */
 
-OO.initClass( OO.ui.IndicatedElement );
+/**
+ * @event flag
+ * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
+ *   added/removed properties
+ */
 
-/* Static Properties */
+/* Methods */
 
 /**
- * indicator.
+ * Check if a flag is set.
  *
- * @static
- * @inheritable
- * @property {string|null} Symbolic indicator name or null for no indicator
+ * @param {string} flag Name of flag
+ * @return {boolean} Has flag
  */
-OO.ui.IndicatedElement.static.indicator = null;
+OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
+       return flag in this.flags;
+};
 
 /**
- * Indicator title.
+ * Get the names of all flags set.
  *
- * @static
- * @inheritable
- * @property {string|Function|null} Indicator title text, a function that return text or null for no
- *  indicator title
+ * @return {string[]} flags Flag names
  */
-OO.ui.IndicatedElement.static.indicatorTitle = null;
-
-/* Methods */
+OO.ui.FlaggableElement.prototype.getFlags = function () {
+       return Object.keys( this.flags );
+};
 
 /**
- * Set indicator.
+ * Clear all flags.
  *
- * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
  * @chainable
+ * @fires flag
  */
-OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) {
-       if ( this.indicator ) {
-               this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
-               this.indicator = null;
-       }
-       if ( typeof indicator === 'string' ) {
-               indicator = indicator.trim();
-               if ( indicator.length ) {
-                       this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
-                       this.indicator = indicator;
-               }
+OO.ui.FlaggableElement.prototype.clearFlags = function () {
+       var flag,
+               changes = {},
+               classPrefix = 'oo-ui-flaggableElement-';
+
+       for ( flag in this.flags ) {
+               changes[flag] = false;
+               delete this.flags[flag];
+               this.$element.removeClass( classPrefix + flag );
        }
-       this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator );
+
+       this.emit( 'flag', changes );
 
        return this;
 };
 
 /**
- * Set indicator label.
+ * Add one or more flags.
  *
- * @param {string|Function|null} indicator Indicator title text, a function that return text or null
- *  for no indicator title
+ * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
+ *  keyed by flag name containing boolean set/remove instructions.
  * @chainable
+ * @fires flag
  */
-OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
-       this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
+OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) {
+       var i, len, flag,
+               changes = {},
+               classPrefix = 'oo-ui-flaggableElement-';
 
-       if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
-               this.$indicator.attr( 'title', indicatorTitle );
-       } else {
-               this.$indicator.removeAttr( 'title' );
+       if ( typeof flags === 'string' ) {
+               // Set
+               this.flags[flags] = true;
+               this.$element.addClass( classPrefix + flags );
+       } else if ( $.isArray( flags ) ) {
+               for ( i = 0, len = flags.length; i < len; i++ ) {
+                       flag = flags[i];
+                       // Set
+                       changes[flag] = true;
+                       this.flags[flag] = true;
+                       this.$element.addClass( classPrefix + flag );
+               }
+       } else if ( OO.isPlainObject( flags ) ) {
+               for ( flag in flags ) {
+                       if ( flags[flag] ) {
+                               // Set
+                               changes[flag] = true;
+                               this.flags[flag] = true;
+                               this.$element.addClass( classPrefix + flag );
+                       } else {
+                               // Remove
+                               changes[flag] = false;
+                               delete this.flags[flag];
+                               this.$element.removeClass( classPrefix + flag );
+                       }
+               }
        }
 
+       this.emit( 'flag', changes );
+
        return this;
 };
 
 /**
- * Get indicator.
- *
- * @return {string} title Symbolic name of indicator
- */
-OO.ui.IndicatedElement.prototype.getIndicator = function () {
-       return this.indicator;
-};
-
-/**
- * Get indicator title.
- *
- * @return {string} Indicator title text
- */
-OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
-       return this.indicatorTitle;
-};
-
-/**
- * Element containing a label.
+ * Element containing a sequence of child elements.
  *
  * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $label Label node, assigned to #$label
+ * @param {jQuery} $group Container node, assigned to #$group
  * @param {Object} [config] Configuration options
- * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
- * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
  */
-OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) {
-       // Config intialization
+OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
+       // Configuration
        config = config || {};
 
        // Properties
-       this.$label = $label;
-       this.label = null;
-
-       // Initialization
-       this.$label.addClass( 'oo-ui-labeledElement-label' );
-       this.setLabel( config.label || this.constructor.static.label );
-       this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
+       this.$group = $group;
+       this.items = [];
+       this.aggregateItemEvents = {};
 };
 
-/* Setup */
-
-OO.initClass( OO.ui.LabeledElement );
+/* Methods */
 
-/* Static Properties */
+/**
+ * Get items.
+ *
+ * @return {OO.ui.Element[]} Items
+ */
+OO.ui.GroupElement.prototype.getItems = function () {
+       return this.items.slice( 0 );
+};
 
 /**
- * Label.
+ * Add an aggregate item event.
  *
- * @static
- * @inheritable
- * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for
- *  no label
+ * Aggregated events are listened to on each item and then emitted by the group under a new name,
+ * and with an additional leading parameter containing the item that emitted the original event.
+ * Other arguments that were emitted from the original event are passed through.
+ *
+ * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
+ *   event, use null value to remove aggregation
+ * @throws {Error} If aggregation already exists
  */
-OO.ui.LabeledElement.static.label = null;
+OO.ui.GroupElement.prototype.aggregate = function ( events ) {
+       var i, len, item, add, remove, itemEvent, groupEvent;
 
-/* Methods */
+       for ( itemEvent in events ) {
+               groupEvent = events[itemEvent];
+
+               // Remove existing aggregated event
+               if ( itemEvent in this.aggregateItemEvents ) {
+                       // Don't allow duplicate aggregations
+                       if ( groupEvent ) {
+                               throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
+                       }
+                       // Remove event aggregation from existing items
+                       for ( i = 0, len = this.items.length; i < len; i++ ) {
+                               item = this.items[i];
+                               if ( item.connect && item.disconnect ) {
+                                       remove = {};
+                                       remove[itemEvent] = [ 'emit', groupEvent, item ];
+                                       item.disconnect( this, remove );
+                               }
+                       }
+                       // Prevent future items from aggregating event
+                       delete this.aggregateItemEvents[itemEvent];
+               }
+
+               // Add new aggregate event
+               if ( groupEvent ) {
+                       // Make future items aggregate event
+                       this.aggregateItemEvents[itemEvent] = groupEvent;
+                       // Add event aggregation to existing items
+                       for ( i = 0, len = this.items.length; i < len; i++ ) {
+                               item = this.items[i];
+                               if ( item.connect && item.disconnect ) {
+                                       add = {};
+                                       add[itemEvent] = [ 'emit', groupEvent, item ];
+                                       item.connect( this, add );
+                               }
+                       }
+               }
+       }
+};
 
 /**
- * Set the label.
- *
- * An empty string will result in the label being hidden. A string containing only whitespace will
- * be converted to a single &nbsp;
+ * Add items.
  *
- * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
- *  text; or null for no label
+ * @param {OO.ui.Element[]} items Item
+ * @param {number} [index] Index to insert items at
  * @chainable
  */
-OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
-       var empty = false;
+OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
+       var i, len, item, event, events, currentIndex,
+               itemElements = [];
 
-       this.label = label = OO.ui.resolveMsg( label ) || null;
-       if ( typeof label === 'string' && label.length ) {
-               if ( label.match( /^\s*$/ ) ) {
-                       // Convert whitespace only string to a single non-breaking space
-                       this.$label.html( '&nbsp;' );
-               } else {
-                       this.$label.text( label );
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+
+               // Check if item exists then remove it first, effectively "moving" it
+               currentIndex = $.inArray( item, this.items );
+               if ( currentIndex >= 0 ) {
+                       this.removeItems( [ item ] );
+                       // Adjust index to compensate for removal
+                       if ( currentIndex < index ) {
+                               index--;
+                       }
                }
-       } else if ( label instanceof jQuery ) {
-               this.$label.empty().append( label );
+               // Add the item
+               if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
+                       events = {};
+                       for ( event in this.aggregateItemEvents ) {
+                               events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
+                       }
+                       item.connect( this, events );
+               }
+               item.setElementGroup( this );
+               itemElements.push( item.$element.get( 0 ) );
+       }
+
+       if ( index === undefined || index < 0 || index >= this.items.length ) {
+               this.$group.append( itemElements );
+               this.items.push.apply( this.items, items );
+       } else if ( index === 0 ) {
+               this.$group.prepend( itemElements );
+               this.items.unshift.apply( this.items, items );
        } else {
-               this.$label.empty();
-               empty = true;
+               this.items[index].$element.before( itemElements );
+               this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
        }
-       this.$element.toggleClass( 'oo-ui-labeledElement', !empty );
-       this.$label.css( 'display', empty ? 'none' : '' );
 
        return this;
 };
 
 /**
- * Get the label.
+ * Remove items.
  *
- * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
- *  text; or null for no label
+ * Items will be detached, not removed, so they can be used later.
+ *
+ * @param {OO.ui.Element[]} items Items to remove
+ * @chainable
  */
-OO.ui.LabeledElement.prototype.getLabel = function () {
-       return this.label;
+OO.ui.GroupElement.prototype.removeItems = function ( items ) {
+       var i, len, item, index, remove, itemEvent;
+
+       // Remove specific items
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               index = $.inArray( item, this.items );
+               if ( index !== -1 ) {
+                       if (
+                               item.connect && item.disconnect &&
+                               !$.isEmptyObject( this.aggregateItemEvents )
+                       ) {
+                               remove = {};
+                               if ( itemEvent in this.aggregateItemEvents ) {
+                                       remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+                               }
+                               item.disconnect( this, remove );
+                       }
+                       item.setElementGroup( null );
+                       this.items.splice( index, 1 );
+                       item.$element.detach();
+               }
+       }
+
+       return this;
 };
 
 /**
- * Fit the label.
+ * Clear all items.
+ *
+ * Items will be detached, not removed, so they can be used later.
  *
  * @chainable
  */
-OO.ui.LabeledElement.prototype.fitLabel = function () {
-       if ( this.$label.autoEllipsis && this.autoFitLabel ) {
-               this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } );
+OO.ui.GroupElement.prototype.clearItems = function () {
+       var i, len, item, remove, itemEvent;
+
+       // Remove all items
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               item = this.items[i];
+               if (
+                       item.connect && item.disconnect &&
+                       !$.isEmptyObject( this.aggregateItemEvents )
+               ) {
+                       remove = {};
+                       if ( itemEvent in this.aggregateItemEvents ) {
+                               remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+                       }
+                       item.disconnect( this, remove );
+               }
+               item.setElementGroup( null );
+               item.$element.detach();
        }
+
+       this.items = [];
        return this;
 };
 
 /**
- * Popuppable element.
+ * Element containing an icon.
+ *
+ * Icons are graphics, about the size of normal text. They can be used to aid the user in locating
+ * a control or convey information in a more space efficient way. Icons should rarely be used
+ * without labels; such as in a toolbar where space is at a premium or within a context where the
+ * meaning is very clear to the user.
  *
  * @abstract
  * @class
  *
  * @constructor
+ * @param {jQuery} $icon Icon node, assigned to #$icon
  * @param {Object} [config] Configuration options
- * @cfg {number} [popupWidth=320] Width of popup
- * @cfg {number} [popupHeight] Height of popup
- * @cfg {Object} [popup] Configuration to pass to popup
+ * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
+ *  use the 'default' key to specify the icon to be used when there is no icon in the user's
+ *  language
  */
-OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
-       // Configuration initialization
-       config = $.extend( { 'popupWidth': 320 }, config );
+OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) {
+       // Config intialization
+       config = config || {};
 
        // Properties
-       this.popup = new OO.ui.PopupWidget( $.extend(
-               { 'align': 'center', 'autoClose': true },
-               config.popup,
-               { '$': this.$, '$autoCloseIgnore': this.$element }
-       ) );
-       this.popupWidth = config.popupWidth;
-       this.popupHeight = config.popupHeight;
+       this.$icon = $icon;
+       this.icon = null;
+
+       // Initialization
+       this.$icon.addClass( 'oo-ui-iconedElement-icon' );
+       this.setIcon( config.icon || this.constructor.static.icon );
 };
 
-/* Methods */
+/* Setup */
 
-/**
- * Get popup.
- *
- * @return {OO.ui.PopupWidget} Popup widget
- */
-OO.ui.PopuppableElement.prototype.getPopup = function () {
-       return this.popup;
-};
+OO.initClass( OO.ui.IconedElement );
 
-/**
- * Show popup.
- */
-OO.ui.PopuppableElement.prototype.showPopup = function () {
-       this.popup.show().display( this.popupWidth, this.popupHeight );
-};
+/* Static Properties */
 
 /**
- * Hide popup.
- */
-OO.ui.PopuppableElement.prototype.hidePopup = function () {
-       this.popup.hide();
-};
-
-/**
- * Element with a title.
+ * Icon.
  *
- * @abstract
- * @class
+ * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
  *
- * @constructor
- * @param {jQuery} $label Titled node, assigned to #$titled
- * @param {Object} [config] Configuration options
- * @cfg {string|Function} [title] Title text or a function that returns text
- */
-OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
-       // Config intialization
-       config = config || {};
-
-       // Properties
-       this.$titled = $titled;
-       this.title = null;
-
-       // Initialization
-       this.setTitle( config.title || this.constructor.static.title );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.TitledElement );
-
-/* Static Properties */
-
-/**
- * Title.
+ * For i18n purposes, this property can be an object containing a `default` icon name property and
+ * additional icon names keyed by language code.
+ *
+ * Example of i18n icon definition:
+ *     { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' }
  *
  * @static
  * @inheritable
- * @property {string|Function} Title text or a function that returns text
+ * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
+ *  use the 'default' key to specify the icon to be used when there is no icon in the user's
+ *  language
  */
-OO.ui.TitledElement.static.title = null;
+OO.ui.IconedElement.static.icon = null;
 
 /* Methods */
 
 /**
- * Set title.
+ * Set icon.
  *
- * @param {string|Function|null} title Title text, a function that returns text or null for no title
+ * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID;
+ *  use the 'default' key to specify the icon to be used when there is no icon in the user's
+ *  language
  * @chainable
  */
-OO.ui.TitledElement.prototype.setTitle = function ( title ) {
-       this.title = title = OO.ui.resolveMsg( title ) || null;
+OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
+       icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
 
-       if ( typeof title === 'string' && title.length ) {
-               this.$titled.attr( 'title', title );
-       } else {
-               this.$titled.removeAttr( 'title' );
+       if ( this.icon ) {
+               this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+       }
+       if ( typeof icon === 'string' ) {
+               icon = icon.trim();
+               if ( icon.length ) {
+                       this.$icon.addClass( 'oo-ui-icon-' + icon );
+                       this.icon = icon;
+               }
        }
+       this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon );
 
        return this;
 };
 
 /**
- * Get title.
+ * Get icon.
  *
- * @return {string} Title string
+ * @return {string} Icon
  */
-OO.ui.TitledElement.prototype.getTitle = function () {
-       return this.title;
+OO.ui.IconedElement.prototype.getIcon = function () {
+       return this.icon;
 };
 
 /**
- * Generic toolbar tool.
+ * Element containing an indicator.
+ *
+ * Indicators are graphics, smaller than normal text. They can be used to describe unique status or
+ * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu
+ * instead of performing an action directly, or an item in a list which has errors that need to be
+ * resolved.
  *
  * @abstract
  * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
  *
  * @constructor
- * @param {OO.ui.ToolGroup} toolGroup
+ * @param {jQuery} $indicator Indicator node, assigned to #$indicator
  * @param {Object} [config] Configuration options
- * @cfg {string|Function} [title] Title text or a function that returns text
+ * @cfg {string} [indicator] Symbolic indicator name
+ * @cfg {string} [indicatorTitle] Indicator title text or a function that return text
  */
-OO.ui.Tool = function OoUiTool( toolGroup, config ) {
+OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
        // Config intialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.Tool.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
-
        // Properties
-       this.toolGroup = toolGroup;
-       this.toolbar = this.toolGroup.getToolbar();
-       this.active = false;
-       this.$title = this.$( '<span>' );
-       this.$link = this.$( '<a>' );
-       this.title = null;
-
-       // Events
-       this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+       this.$indicator = $indicator;
+       this.indicator = null;
+       this.indicatorLabel = null;
 
        // Initialization
-       this.$title.addClass( 'oo-ui-tool-title' );
-       this.$link
-               .addClass( 'oo-ui-tool-link' )
-               .append( this.$icon, this.$title )
-               .prop( 'tabIndex', 0 )
-               .attr( 'role', 'button' );
-       this.$element
-               .data( 'oo-ui-tool', this )
-               .addClass(
-                       'oo-ui-tool ' + 'oo-ui-tool-name-' +
-                       this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
-               )
-               .append( this.$link );
-       this.setTitle( config.title || this.constructor.static.title );
+       this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
+       this.setIndicator( config.indicator || this.constructor.static.indicator );
+       this.setIndicatorTitle( config.indicatorTitle  || this.constructor.static.indicatorTitle );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
-OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
-
-/* Events */
-
-/**
- * @event select
- */
+OO.initClass( OO.ui.IndicatedElement );
 
 /* Static Properties */
 
 /**
- * @static
- * @inheritdoc
- */
-OO.ui.Tool.static.tagName = 'span';
-
-/**
- * Symbolic name of tool.
+ * indicator.
  *
- * @abstract
  * @static
  * @inheritable
- * @property {string}
+ * @property {string|null} Symbolic indicator name or null for no indicator
  */
-OO.ui.Tool.static.name = '';
+OO.ui.IndicatedElement.static.indicator = null;
 
 /**
- * Tool group.
+ * Indicator title.
  *
- * @abstract
  * @static
  * @inheritable
- * @property {string}
+ * @property {string|Function|null} Indicator title text, a function that return text or null for no
+ *  indicator title
  */
-OO.ui.Tool.static.group = '';
+OO.ui.IndicatedElement.static.indicatorTitle = null;
 
-/**
- * Tool title.
- *
- * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
- * is part of a list or menu tool group. If a trigger is associated with an action by the same name
- * as the tool, a description of its keyboard shortcut for the appropriate platform will be
- * appended to the title if the tool is part of a bar tool group.
- *
- * @abstract
- * @static
- * @inheritable
- * @property {string|Function} Title text or a function that returns text
- */
-OO.ui.Tool.static.title = '';
+/* Methods */
 
 /**
- * Tool can be automatically added to catch-all groups.
+ * Set indicator.
  *
- * @static
- * @inheritable
- * @property {boolean}
+ * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
+ * @chainable
  */
-OO.ui.Tool.static.autoAddToCatchall = true;
+OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) {
+       if ( this.indicator ) {
+               this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
+               this.indicator = null;
+       }
+       if ( typeof indicator === 'string' ) {
+               indicator = indicator.trim();
+               if ( indicator.length ) {
+                       this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
+                       this.indicator = indicator;
+               }
+       }
+       this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator );
 
-/**
- * Tool can be automatically added to named groups.
- *
- * @static
- * @property {boolean}
- * @inheritable
- */
-OO.ui.Tool.static.autoAddToGroup = true;
+       return this;
+};
 
 /**
- * Check if this tool is compatible with given data.
+ * Set indicator label.
  *
- * @static
- * @inheritable
- * @param {Mixed} data Data to check
- * @return {boolean} Tool can be used with data
+ * @param {string|Function|null} indicator Indicator title text, a function that return text or null
+ *  for no indicator title
+ * @chainable
  */
-OO.ui.Tool.static.isCompatibleWith = function () {
-       return false;
-};
+OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
+       this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
 
-/* Methods */
+       if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
+               this.$indicator.attr( 'title', indicatorTitle );
+       } else {
+               this.$indicator.removeAttr( 'title' );
+       }
 
-/**
- * Handle the toolbar state being updated.
- *
- * This is an abstract method that must be overridden in a concrete subclass.
- *
- * @abstract
- */
-OO.ui.Tool.prototype.onUpdateState = function () {
-       throw new Error(
-               'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
-       );
+       return this;
 };
 
 /**
- * Handle the tool being selected.
- *
- * This is an abstract method that must be overridden in a concrete subclass.
+ * Get indicator.
  *
- * @abstract
+ * @return {string} title Symbolic name of indicator
  */
-OO.ui.Tool.prototype.onSelect = function () {
-       throw new Error(
-               'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
-       );
+OO.ui.IndicatedElement.prototype.getIndicator = function () {
+       return this.indicator;
 };
 
 /**
- * Check if the button is active.
+ * Get indicator title.
  *
- * @param {boolean} Button is active
+ * @return {string} Indicator title text
  */
-OO.ui.Tool.prototype.isActive = function () {
-       return this.active;
+OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
+       return this.indicatorTitle;
 };
 
 /**
- * Make the button appear active or inactive.
+ * Element containing a label.
  *
- * @param {boolean} state Make button appear active
- */
-OO.ui.Tool.prototype.setActive = function ( state ) {
-       this.active = !!state;
-       if ( this.active ) {
-               this.$element.addClass( 'oo-ui-tool-active' );
-       } else {
-               this.$element.removeClass( 'oo-ui-tool-active' );
-       }
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {jQuery} $label Label node, assigned to #$label
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
+ * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
+ */
+OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.$label = $label;
+       this.label = null;
+
+       // Initialization
+       this.$label.addClass( 'oo-ui-labeledElement-label' );
+       this.setLabel( config.label || this.constructor.static.label );
+       this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
 };
 
+/* Setup */
+
+OO.initClass( OO.ui.LabeledElement );
+
+/* Static Properties */
+
 /**
- * Get the tool title.
+ * Label.
  *
- * @param {string|Function} title Title text or a function that returns text
+ * @static
+ * @inheritable
+ * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for
+ *  no label
+ */
+OO.ui.LabeledElement.static.label = null;
+
+/* Methods */
+
+/**
+ * Set the label.
+ *
+ * An empty string will result in the label being hidden. A string containing only whitespace will
+ * be converted to a single &nbsp;
+ *
+ * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
+ *  text; or null for no label
  * @chainable
  */
-OO.ui.Tool.prototype.setTitle = function ( title ) {
-       this.title = OO.ui.resolveMsg( title );
-       this.updateTitle();
+OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
+       var empty = false;
+
+       this.label = label = OO.ui.resolveMsg( label ) || null;
+       if ( typeof label === 'string' && label.length ) {
+               if ( label.match( /^\s*$/ ) ) {
+                       // Convert whitespace only string to a single non-breaking space
+                       this.$label.html( '&nbsp;' );
+               } else {
+                       this.$label.text( label );
+               }
+       } else if ( label instanceof jQuery ) {
+               this.$label.empty().append( label );
+       } else {
+               this.$label.empty();
+               empty = true;
+       }
+       this.$element.toggleClass( 'oo-ui-labeledElement', !empty );
+       this.$label.css( 'display', empty ? 'none' : '' );
+
        return this;
 };
 
 /**
- * Get the tool title.
+ * Get the label.
  *
- * @return {string} Title text
+ * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ *  text; or null for no label
  */
-OO.ui.Tool.prototype.getTitle = function () {
-       return this.title;
+OO.ui.LabeledElement.prototype.getLabel = function () {
+       return this.label;
 };
 
 /**
- * Get the tool's symbolic name.
+ * Fit the label.
  *
- * @return {string} Symbolic name of tool
+ * @chainable
  */
-OO.ui.Tool.prototype.getName = function () {
-       return this.constructor.static.name;
+OO.ui.LabeledElement.prototype.fitLabel = function () {
+       if ( this.$label.autoEllipsis && this.autoFitLabel ) {
+               this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } );
+       }
+       return this;
 };
 
 /**
- * Update the title.
+ * Element containing an OO.ui.PopupWidget object.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [popup] Configuration to pass to popup
+ * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
  */
-OO.ui.Tool.prototype.updateTitle = function () {
-       var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
-               accelTooltips = this.toolGroup.constructor.static.accelTooltips,
-               accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
-               tooltipParts = [];
-
-       this.$title.empty()
-               .text( this.title )
-               .append(
-                       this.$( '<span>' )
-                               .addClass( 'oo-ui-tool-accel' )
-                               .text( accel )
-               );
+OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
+       // Configuration initialization
+       config = config || {};
 
-       if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
-               tooltipParts.push( this.title );
-       }
-       if ( accelTooltips && typeof accel === 'string' && accel.length ) {
-               tooltipParts.push( accel );
-       }
-       if ( tooltipParts.length ) {
-               this.$link.attr( 'title', tooltipParts.join( ' ' ) );
-       } else {
-               this.$link.removeAttr( 'title' );
-       }
+       // Properties
+       this.popup = new OO.ui.PopupWidget( $.extend(
+               { 'autoClose': true },
+               config.popup,
+               { '$': this.$, '$autoCloseIgnore': this.$element }
+       ) );
 };
 
+/* Methods */
+
 /**
- * Destroy tool.
+ * Get popup.
+ *
+ * @return {OO.ui.PopupWidget} Popup widget
  */
-OO.ui.Tool.prototype.destroy = function () {
-       this.toolbar.disconnect( this );
-       this.$element.remove();
+OO.ui.PopuppableElement.prototype.getPopup = function () {
+       return this.popup;
 };
 
 /**
- * Collection of tool groups.
+ * Element with a title.
  *
+ * Titles are rendered by the browser and are made visible when hovering the element. Titles are
+ * not visible on touch devices.
+ *
+ * @abstract
  * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- * @mixins OO.ui.GroupElement
  *
  * @constructor
- * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
- * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
+ * @param {jQuery} $label Titled node, assigned to #$titled
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [actions] Add an actions section opposite to the tools
- * @cfg {boolean} [shadow] Add a shadow below the toolbar
+ * @cfg {string|Function} [title] Title text or a function that returns text
  */
-OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
-       // Configuration initialization
+OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
+       // Config intialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.Toolbar.super.call( this, config );
-
-       // Mixin constructors
-       OO.EventEmitter.call( this );
-       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
-
        // Properties
-       this.toolFactory = toolFactory;
-       this.toolGroupFactory = toolGroupFactory;
-       this.groups = [];
-       this.tools = {};
-       this.$bar = this.$( '<div>' );
-       this.$actions = this.$( '<div>' );
-       this.initialized = false;
-
-       // Events
-       this.$element
-               .add( this.$bar ).add( this.$group ).add( this.$actions )
-               .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
+       this.$titled = $titled;
+       this.title = null;
 
        // Initialization
-       this.$group.addClass( 'oo-ui-toolbar-tools' );
-       this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
-       if ( config.actions ) {
-               this.$actions.addClass( 'oo-ui-toolbar-actions' );
-               this.$bar.append( this.$actions );
-       }
-       this.$bar.append( '<div style="clear:both"></div>' );
-       if ( config.shadow ) {
-               this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
-       }
-       this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
+       this.setTitle( config.title || this.constructor.static.title );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
-OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
-OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
+OO.initClass( OO.ui.TitledElement );
 
-/* Methods */
+/* Static Properties */
 
 /**
- * Get the tool factory.
+ * Title.
  *
- * @return {OO.ui.ToolFactory} Tool factory
+ * @static
+ * @inheritable
+ * @property {string|Function} Title text or a function that returns text
  */
-OO.ui.Toolbar.prototype.getToolFactory = function () {
-       return this.toolFactory;
-};
+OO.ui.TitledElement.static.title = null;
 
-/**
- * Get the tool group factory.
- *
- * @return {OO.Factory} Tool group factory
- */
-OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
-       return this.toolGroupFactory;
-};
+/* Methods */
 
 /**
- * Handles mouse down events.
+ * Set title.
  *
- * @param {jQuery.Event} e Mouse down event
+ * @param {string|Function|null} title Title text, a function that returns text or null for no title
+ * @chainable
  */
-OO.ui.Toolbar.prototype.onMouseDown = function ( e ) {
-       var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
-               $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
-       if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
-               return false;
+OO.ui.TitledElement.prototype.setTitle = function ( title ) {
+       this.title = title = OO.ui.resolveMsg( title ) || null;
+
+       if ( typeof title === 'string' && title.length ) {
+               this.$titled.attr( 'title', title );
+       } else {
+               this.$titled.removeAttr( 'title' );
        }
+
+       return this;
 };
 
 /**
- * Sets up handles and preloads required information for the toolbar to work.
- * This must be called immediately after it is attached to a visible document.
+ * Get title.
+ *
+ * @return {string} Title string
  */
-OO.ui.Toolbar.prototype.initialize = function () {
-       this.initialized = true;
+OO.ui.TitledElement.prototype.getTitle = function () {
+       return this.title;
 };
 
 /**
- * Setup toolbar.
- *
- * Tools can be specified in the following ways:
+ * Generic toolbar tool.
  *
- * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
- * - All tools in a group: `{ 'group': 'group-name' }`
- * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconedElement
  *
- * @param {Object.<string,Array>} groups List of tool group configurations
- * @param {Array|string} [groups.include] Tools to include
- * @param {Array|string} [groups.exclude] Tools to exclude
- * @param {Array|string} [groups.promote] Tools to promote to the beginning
- * @param {Array|string} [groups.demote] Tools to demote to the end
+ * @constructor
+ * @param {OO.ui.ToolGroup} toolGroup
+ * @param {Object} [config] Configuration options
+ * @cfg {string|Function} [title] Title text or a function that returns text
  */
-OO.ui.Toolbar.prototype.setup = function ( groups ) {
-       var i, len, type, group,
-               items = [],
-               defaultType = 'bar';
+OO.ui.Tool = function OoUiTool( toolGroup, config ) {
+       // Config intialization
+       config = config || {};
 
-       // Cleanup previous groups
-       this.reset();
+       // Parent constructor
+       OO.ui.Tool.super.call( this, config );
 
-       // Build out new groups
-       for ( i = 0, len = groups.length; i < len; i++ ) {
-               group = groups[i];
-               if ( group.include === '*' ) {
-                       // Apply defaults to catch-all groups
-                       if ( group.type === undefined ) {
-                               group.type = 'list';
-                       }
-                       if ( group.label === undefined ) {
-                               group.label = 'ooui-toolbar-more';
-                       }
-               }
-               // Check type has been registered
-               type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
-               items.push(
-                       this.getToolGroupFactory().create( type, this, $.extend( { '$': this.$ }, group ) )
-               );
-       }
-       this.addItems( items );
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
+
+       // Properties
+       this.toolGroup = toolGroup;
+       this.toolbar = this.toolGroup.getToolbar();
+       this.active = false;
+       this.$title = this.$( '<span>' );
+       this.$link = this.$( '<a>' );
+       this.title = null;
+
+       // Events
+       this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+
+       // Initialization
+       this.$title.addClass( 'oo-ui-tool-title' );
+       this.$link
+               .addClass( 'oo-ui-tool-link' )
+               .append( this.$icon, this.$title )
+               .prop( 'tabIndex', 0 )
+               .attr( 'role', 'button' );
+       this.$element
+               .data( 'oo-ui-tool', this )
+               .addClass(
+                       'oo-ui-tool ' + 'oo-ui-tool-name-' +
+                       this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
+               )
+               .append( this.$link );
+       this.setTitle( config.title || this.constructor.static.title );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
+OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
+
+/* Events */
+
 /**
- * Remove all tools and groups from the toolbar.
+ * @event select
  */
-OO.ui.Toolbar.prototype.reset = function () {
-       var i, len;
 
-       this.groups = [];
-       this.tools = {};
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               this.items[i].destroy();
-       }
-       this.clearItems();
-};
+/* Static Properties */
 
 /**
- * Destroys toolbar, removing event handlers and DOM elements.
- *
- * Call this whenever you are done using a toolbar.
+ * @static
+ * @inheritdoc
  */
-OO.ui.Toolbar.prototype.destroy = function () {
-       this.reset();
-       this.$element.remove();
-};
+OO.ui.Tool.static.tagName = 'span';
 
 /**
- * Check if tool has not been used yet.
+ * Symbolic name of tool.
  *
- * @param {string} name Symbolic name of tool
- * @return {boolean} Tool is available
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
  */
-OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
-       return !this.tools[name];
-};
+OO.ui.Tool.static.name = '';
 
 /**
- * Prevent tool from being used again.
+ * Tool group.
  *
- * @param {OO.ui.Tool} tool Tool to reserve
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
  */
-OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
-       this.tools[tool.getName()] = tool;
-};
+OO.ui.Tool.static.group = '';
 
 /**
- * Allow tool to be used again.
+ * Tool title.
  *
- * @param {OO.ui.Tool} tool Tool to release
+ * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
+ * is part of a list or menu tool group. If a trigger is associated with an action by the same name
+ * as the tool, a description of its keyboard shortcut for the appropriate platform will be
+ * appended to the title if the tool is part of a bar tool group.
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string|Function} Title text or a function that returns text
  */
-OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
-       delete this.tools[tool.getName()];
-};
+OO.ui.Tool.static.title = '';
 
 /**
- * Get accelerator label for tool.
+ * Tool can be automatically added to catch-all groups.
  *
- * This is a stub that should be overridden to provide access to accelerator information.
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.Tool.static.autoAddToCatchall = true;
+
+/**
+ * Tool can be automatically added to named groups.
  *
- * @param {string} name Symbolic name of tool
- * @return {string|undefined} Tool accelerator label if available
+ * @static
+ * @property {boolean}
+ * @inheritable
  */
-OO.ui.Toolbar.prototype.getToolAccelerator = function () {
-       return undefined;
-};
+OO.ui.Tool.static.autoAddToGroup = true;
 
 /**
- * Factory for tools.
+ * Check if this tool is compatible with given data.
  *
- * @class
- * @extends OO.Factory
- * @constructor
+ * @static
+ * @inheritable
+ * @param {Mixed} data Data to check
+ * @return {boolean} Tool can be used with data
  */
-OO.ui.ToolFactory = function OoUiToolFactory() {
-       // Parent constructor
-       OO.ui.ToolFactory.super.call( this );
+OO.ui.Tool.static.isCompatibleWith = function () {
+       return false;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
-
 /* Methods */
 
-/** */
-OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
-       var i, len, included, promoted, demoted,
-               auto = [],
-               used = {};
+/**
+ * Handle the toolbar state being updated.
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @abstract
+ */
+OO.ui.Tool.prototype.onUpdateState = function () {
+       throw new Error(
+               'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
+       );
+};
 
-       // Collect included and not excluded tools
-       included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
+/**
+ * Handle the tool being selected.
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @abstract
+ */
+OO.ui.Tool.prototype.onSelect = function () {
+       throw new Error(
+               'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
+       );
+};
 
-       // Promotion
-       promoted = this.extract( promote, used );
-       demoted = this.extract( demote, used );
+/**
+ * Check if the button is active.
+ *
+ * @param {boolean} Button is active
+ */
+OO.ui.Tool.prototype.isActive = function () {
+       return this.active;
+};
 
-       // Auto
-       for ( i = 0, len = included.length; i < len; i++ ) {
-               if ( !used[included[i]] ) {
-                       auto.push( included[i] );
-               }
+/**
+ * Make the button appear active or inactive.
+ *
+ * @param {boolean} state Make button appear active
+ */
+OO.ui.Tool.prototype.setActive = function ( state ) {
+       this.active = !!state;
+       if ( this.active ) {
+               this.$element.addClass( 'oo-ui-tool-active' );
+       } else {
+               this.$element.removeClass( 'oo-ui-tool-active' );
        }
-
-       return promoted.concat( auto ).concat( demoted );
 };
 
 /**
- * Get a flat list of names from a list of names or groups.
- *
- * Tools can be specified in the following ways:
- *
- * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
- * - All tools in a group: `{ 'group': 'group-name' }`
- * - All tools: `'*'`
+ * Get the tool title.
  *
- * @private
- * @param {Array|string} collection List of tools
- * @param {Object} [used] Object with names that should be skipped as properties; extracted
- *  names will be added as properties
- * @return {string[]} List of extracted names
+ * @param {string|Function} title Title text or a function that returns text
+ * @chainable
  */
-OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
-       var i, len, item, name, tool,
-               names = [];
-
-       if ( collection === '*' ) {
-               for ( name in this.registry ) {
-                       tool = this.registry[name];
-                       if (
-                               // Only add tools by group name when auto-add is enabled
-                               tool.static.autoAddToCatchall &&
-                               // Exclude already used tools
-                               ( !used || !used[name] )
-                       ) {
-                               names.push( name );
-                               if ( used ) {
-                                       used[name] = true;
-                               }
-                       }
-               }
-       } else if ( $.isArray( collection ) ) {
-               for ( i = 0, len = collection.length; i < len; i++ ) {
-                       item = collection[i];
-                       // Allow plain strings as shorthand for named tools
-                       if ( typeof item === 'string' ) {
-                               item = { 'name': item };
-                       }
-                       if ( OO.isPlainObject( item ) ) {
-                               if ( item.group ) {
-                                       for ( name in this.registry ) {
-                                               tool = this.registry[name];
-                                               if (
-                                                       // Include tools with matching group
-                                                       tool.static.group === item.group &&
-                                                       // Only add tools by group name when auto-add is enabled
-                                                       tool.static.autoAddToGroup &&
-                                                       // Exclude already used tools
-                                                       ( !used || !used[name] )
-                                               ) {
-                                                       names.push( name );
-                                                       if ( used ) {
-                                                               used[name] = true;
-                                                       }
-                                               }
-                                       }
-                               // Include tools with matching name and exclude already used tools
-                               } else if ( item.name && ( !used || !used[item.name] ) ) {
-                                       names.push( item.name );
-                                       if ( used ) {
-                                               used[item.name] = true;
-                                       }
-                               }
-                       }
-               }
-       }
-       return names;
+OO.ui.Tool.prototype.setTitle = function ( title ) {
+       this.title = OO.ui.resolveMsg( title );
+       this.updateTitle();
+       return this;
 };
 
 /**
- * Collection of tools.
+ * Get the tool title.
  *
- * Tools can be specified in the following ways:
+ * @return {string} Title text
+ */
+OO.ui.Tool.prototype.getTitle = function () {
+       return this.title;
+};
+
+/**
+ * Get the tool's symbolic name.
  *
- * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
- * - All tools in a group: `{ 'group': 'group-name' }`
- * - All tools: `'*'`
+ * @return {string} Symbolic name of tool
+ */
+OO.ui.Tool.prototype.getName = function () {
+       return this.constructor.static.name;
+};
+
+/**
+ * Update the title.
+ */
+OO.ui.Tool.prototype.updateTitle = function () {
+       var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
+               accelTooltips = this.toolGroup.constructor.static.accelTooltips,
+               accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
+               tooltipParts = [];
+
+       this.$title.empty()
+               .text( this.title )
+               .append(
+                       this.$( '<span>' )
+                               .addClass( 'oo-ui-tool-accel' )
+                               .text( accel )
+               );
+
+       if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
+               tooltipParts.push( this.title );
+       }
+       if ( accelTooltips && typeof accel === 'string' && accel.length ) {
+               tooltipParts.push( accel );
+       }
+       if ( tooltipParts.length ) {
+               this.$link.attr( 'title', tooltipParts.join( ' ' ) );
+       } else {
+               this.$link.removeAttr( 'title' );
+       }
+};
+
+/**
+ * Destroy tool.
+ */
+OO.ui.Tool.prototype.destroy = function () {
+       this.toolbar.disconnect( this );
+       this.$element.remove();
+};
+
+/**
+ * Collection of tool groups.
  *
- * @abstract
  * @class
- * @extends OO.ui.Widget
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
  * @mixins OO.ui.GroupElement
  *
  * @constructor
- * @param {OO.ui.Toolbar} toolbar
+ * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
+ * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
  * @param {Object} [config] Configuration options
- * @cfg {Array|string} [include=[]] List of tools to include
- * @cfg {Array|string} [exclude=[]] List of tools to exclude
- * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
- * @cfg {Array|string} [demote=[]] List of tools to demote to the end
+ * @cfg {boolean} [actions] Add an actions section opposite to the tools
+ * @cfg {boolean} [shadow] Add a shadow below the toolbar
  */
-OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
+OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
        // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.ToolGroup.super.call( this, config );
+       OO.ui.Toolbar.super.call( this, config );
 
        // Mixin constructors
+       OO.EventEmitter.call( this );
        OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
 
        // Properties
-       this.toolbar = toolbar;
+       this.toolFactory = toolFactory;
+       this.toolGroupFactory = toolGroupFactory;
+       this.groups = [];
        this.tools = {};
-       this.pressed = null;
-       this.autoDisabled = false;
-       this.include = config.include || [];
-       this.exclude = config.exclude || [];
-       this.promote = config.promote || [];
-       this.demote = config.demote || [];
-       this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this );
+       this.$bar = this.$( '<div>' );
+       this.$actions = this.$( '<div>' );
+       this.initialized = false;
 
        // Events
-       this.$element.on( {
-               'mousedown': OO.ui.bind( this.onMouseDown, this ),
-               'mouseup': OO.ui.bind( this.onMouseUp, this ),
-               'mouseover': OO.ui.bind( this.onMouseOver, this ),
-               'mouseout': OO.ui.bind( this.onMouseOut, this )
-       } );
-       this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } );
-       this.aggregate( { 'disable': 'itemDisable' } );
-       this.connect( this, { 'itemDisable': 'updateDisabled' } );
+       this.$element
+               .add( this.$bar ).add( this.$group ).add( this.$actions )
+               .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
 
        // Initialization
-       this.$group.addClass( 'oo-ui-toolGroup-tools' );
-       this.$element
-               .addClass( 'oo-ui-toolGroup' )
-               .append( this.$group );
-       this.populate();
+       this.$group.addClass( 'oo-ui-toolbar-tools' );
+       this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
+       if ( config.actions ) {
+               this.$actions.addClass( 'oo-ui-toolbar-actions' );
+               this.$bar.append( this.$actions );
+       }
+       this.$bar.append( '<div style="clear:both"></div>' );
+       if ( config.shadow ) {
+               this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
+       }
+       this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
-OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
-
-/* Events */
-
-/**
- * @event update
- */
+OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
+OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
+OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
 
-/* Static Properties */
+/* Methods */
 
 /**
- * Show labels in tooltips.
+ * Get the tool factory.
  *
- * @static
- * @inheritable
- * @property {boolean}
+ * @return {OO.ui.ToolFactory} Tool factory
  */
-OO.ui.ToolGroup.static.titleTooltips = false;
+OO.ui.Toolbar.prototype.getToolFactory = function () {
+       return this.toolFactory;
+};
 
 /**
- * Show acceleration labels in tooltips.
+ * Get the tool group factory.
  *
- * @static
- * @inheritable
- * @property {boolean}
+ * @return {OO.Factory} Tool group factory
  */
-OO.ui.ToolGroup.static.accelTooltips = false;
+OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
+       return this.toolGroupFactory;
+};
 
 /**
- * Automatically disable the toolgroup when all tools are disabled
+ * Handles mouse down events.
  *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.ToolGroup.static.autoDisable = true;
-
-/* Methods */
-
-/**
- * @inheritdoc
+ * @param {jQuery.Event} e Mouse down event
  */
-OO.ui.ToolGroup.prototype.isDisabled = function () {
-       return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
+OO.ui.Toolbar.prototype.onMouseDown = function ( e ) {
+       var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
+               $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
+       if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
+               return false;
+       }
 };
 
 /**
- * @inheritdoc
+ * Sets up handles and preloads required information for the toolbar to work.
+ * This must be called immediately after it is attached to a visible document.
  */
-OO.ui.ToolGroup.prototype.updateDisabled = function () {
-       var i, item, allDisabled = true;
-
-       if ( this.constructor.static.autoDisable ) {
-               for ( i = this.items.length - 1; i >= 0; i-- ) {
-                       item = this.items[i];
-                       if ( !item.isDisabled() ) {
-                               allDisabled = false;
-                               break;
-                       }
-               }
-               this.autoDisabled = allDisabled;
-       }
-       OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
+OO.ui.Toolbar.prototype.initialize = function () {
+       this.initialized = true;
 };
 
 /**
- * Handle mouse down events.
+ * Setup toolbar.
  *
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
+ * Tools can be specified in the following ways:
+ *
+ * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
+ * - All tools in a group: `{ 'group': 'group-name' }`
+ * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
+ *
+ * @param {Object.<string,Array>} groups List of tool group configurations
+ * @param {Array|string} [groups.include] Tools to include
+ * @param {Array|string} [groups.exclude] Tools to exclude
+ * @param {Array|string} [groups.promote] Tools to promote to the beginning
+ * @param {Array|string} [groups.demote] Tools to demote to the end
+ */
+OO.ui.Toolbar.prototype.setup = function ( groups ) {
+       var i, len, type, group,
+               items = [],
+               defaultType = 'bar';
+
+       // Cleanup previous groups
+       this.reset();
+
+       // Build out new groups
+       for ( i = 0, len = groups.length; i < len; i++ ) {
+               group = groups[i];
+               if ( group.include === '*' ) {
+                       // Apply defaults to catch-all groups
+                       if ( group.type === undefined ) {
+                               group.type = 'list';
+                       }
+                       if ( group.label === undefined ) {
+                               group.label = 'ooui-toolbar-more';
+                       }
+               }
+               // Check type has been registered
+               type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
+               items.push(
+                       this.getToolGroupFactory().create( type, this, $.extend( { '$': this.$ }, group ) )
+               );
+       }
+       this.addItems( items );
+};
+
+/**
+ * Remove all tools and groups from the toolbar.
+ */
+OO.ui.Toolbar.prototype.reset = function () {
+       var i, len;
+
+       this.groups = [];
+       this.tools = {};
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               this.items[i].destroy();
+       }
+       this.clearItems();
+};
+
+/**
+ * Destroys toolbar, removing event handlers and DOM elements.
+ *
+ * Call this whenever you are done using a toolbar.
+ */
+OO.ui.Toolbar.prototype.destroy = function () {
+       this.reset();
+       this.$element.remove();
+};
+
+/**
+ * Check if tool has not been used yet.
+ *
+ * @param {string} name Symbolic name of tool
+ * @return {boolean} Tool is available
+ */
+OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
+       return !this.tools[name];
+};
+
+/**
+ * Prevent tool from being used again.
+ *
+ * @param {OO.ui.Tool} tool Tool to reserve
+ */
+OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
+       this.tools[tool.getName()] = tool;
+};
+
+/**
+ * Allow tool to be used again.
+ *
+ * @param {OO.ui.Tool} tool Tool to release
+ */
+OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
+       delete this.tools[tool.getName()];
+};
+
+/**
+ * Get accelerator label for tool.
+ *
+ * This is a stub that should be overridden to provide access to accelerator information.
+ *
+ * @param {string} name Symbolic name of tool
+ * @return {string|undefined} Tool accelerator label if available
+ */
+OO.ui.Toolbar.prototype.getToolAccelerator = function () {
+       return undefined;
+};
+
+/**
+ * Collection of tools.
+ *
+ * Tools can be specified in the following ways:
+ *
+ * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
+ * - All tools in a group: `{ 'group': 'group-name' }`
+ * - All tools: `'*'`
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ * @cfg {Array|string} [include=[]] List of tools to include
+ * @cfg {Array|string} [exclude=[]] List of tools to exclude
+ * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
+ * @cfg {Array|string} [demote=[]] List of tools to demote to the end
+ */
+OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.ToolGroup.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+
+       // Properties
+       this.toolbar = toolbar;
+       this.tools = {};
+       this.pressed = null;
+       this.autoDisabled = false;
+       this.include = config.include || [];
+       this.exclude = config.exclude || [];
+       this.promote = config.promote || [];
+       this.demote = config.demote || [];
+       this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this );
+
+       // Events
+       this.$element.on( {
+               'mousedown': OO.ui.bind( this.onMouseDown, this ),
+               'mouseup': OO.ui.bind( this.onMouseUp, this ),
+               'mouseover': OO.ui.bind( this.onMouseOver, this ),
+               'mouseout': OO.ui.bind( this.onMouseOut, this )
+       } );
+       this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } );
+       this.aggregate( { 'disable': 'itemDisable' } );
+       this.connect( this, { 'itemDisable': 'updateDisabled' } );
+
+       // Initialization
+       this.$group.addClass( 'oo-ui-toolGroup-tools' );
+       this.$element
+               .addClass( 'oo-ui-toolGroup' )
+               .append( this.$group );
+       this.populate();
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
+OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * @event update
+ */
+
+/* Static Properties */
+
+/**
+ * Show labels in tooltips.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.ToolGroup.static.titleTooltips = false;
+
+/**
+ * Show acceleration labels in tooltips.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.ToolGroup.static.accelTooltips = false;
+
+/**
+ * Automatically disable the toolgroup when all tools are disabled
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.ToolGroup.static.autoDisable = true;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToolGroup.prototype.isDisabled = function () {
+       return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToolGroup.prototype.updateDisabled = function () {
+       var i, item, allDisabled = true;
+
+       if ( this.constructor.static.autoDisable ) {
+               for ( i = this.items.length - 1; i >= 0; i-- ) {
+                       item = this.items[i];
+                       if ( !item.isDisabled() ) {
+                               allDisabled = false;
+                               break;
+                       }
+               }
+               this.autoDisabled = allDisabled;
+       }
+       OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
+};
+
+/**
+ * Handle mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
        if ( !this.isDisabled() && e.which === 1 ) {
                this.pressed = this.getTargetTool( e );
                if ( this.pressed ) {
@@ -3803,9 +4922,9 @@ OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
                        this.getElementDocument().addEventListener(
                                'mouseup', this.onCapturedMouseUpHandler, true
                        );
-                       return false;
                }
        }
+       return false;
 };
 
 /**
@@ -3980,367 +5099,440 @@ OO.ui.ToolGroup.prototype.destroy = function () {
 };
 
 /**
- * Factory for tool groups.
+ * Dialog for showing a message.
+ *
+ * User interface:
+ * - Registers two actions by default (safe and primary).
+ * - Renders action widgets in the footer.
  *
  * @class
- * @extends OO.Factory
+ * @extends OO.ui.Dialog
+ *
  * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
+OO.ui.MessageDialog = function OoUiMessageDialog( manager, config ) {
        // Parent constructor
-       OO.Factory.call( this );
+       OO.ui.MessageDialog.super.call( this, manager, config );
 
-       var i, l,
-               defaultClasses = this.constructor.static.getDefaultClasses();
+       // Properties
+       this.verticalActionLayout = null;
 
-       // Register default toolgroups
-       for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
-               this.register( defaultClasses[i] );
-       }
+       // Initialization
+       this.$element.addClass( 'oo-ui-messageDialog' );
 };
 
-/* Setup */
+/* Inheritance */
 
-OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
+OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
 
-/* Static Methods */
+/* Static Properties */
+
+OO.ui.MessageDialog.static.name = 'message';
+
+OO.ui.MessageDialog.static.size = 'small';
+
+OO.ui.MessageDialog.static.verbose = false;
 
 /**
- * Get a default set of classes to be registered on construction
+ * Dialog title.
  *
- * @return {Function[]} Default classes
+ * A confirmation dialog's title should describe what the progressive action will do. An alert
+ * dialog's title should describe what event occured.
+ *
+ * @static
+ * inheritable
+ * @property {jQuery|string|Function|null}
  */
-OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
-       return [
-               OO.ui.BarToolGroup,
-               OO.ui.ListToolGroup,
-               OO.ui.MenuToolGroup
-       ];
-};
+OO.ui.MessageDialog.static.title = null;
 
 /**
- * Layout made of a fieldset and optional legend.
- *
- * Just add OO.ui.FieldLayout items.
- *
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.LabeledElement
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.GroupElement
+ * A confirmation dialog's message should describe the consequences of the progressive action. An
+ * alert dialog's message should describe why the event occured.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [icon] Symbolic icon name
- * @cfg {OO.ui.FieldLayout[]} [items] Items to add
+ * @static
+ * inheritable
+ * @property {jQuery|string|Function|null}
  */
-OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
-       // Config initialization
-       config = config || {};
+OO.ui.MessageDialog.static.message = null;
 
-       // Parent constructor
-       OO.ui.FieldsetLayout.super.call( this, config );
+OO.ui.MessageDialog.static.actions = [
+       { 'label': OO.ui.deferMsg( 'ooui-dialog-message-accept' ), 'flags': 'primary' },
+       { 'label': OO.ui.deferMsg( 'ooui-dialog-message-reject' ), 'flags': 'safe' }
+];
 
-       // Mixin constructors
-       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
-       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
-       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+/* Methods */
 
-       // Initialization
-       this.$element
-               .addClass( 'oo-ui-fieldsetLayout' )
-               .prepend( this.$icon, this.$label, this.$group );
-       if ( $.isArray( config.items ) ) {
-               this.addItems( config.items );
-       }
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
+       this.fitActions();
+       return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
-
-/* Static Properties */
-
-OO.ui.FieldsetLayout.static.tagName = 'div';
-
 /**
- * Layout made of a field and optional label.
- *
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.LabeledElement
- *
- * Available label alignment modes include:
- *  - 'left': Label is before the field and aligned away from it, best for when the user will be
- *    scanning for a specific label in a form with many fields
- *  - 'right': Label is before the field and aligned toward it, best for forms the user is very
- *    familiar with and will tab through field checking quickly to verify which field they are in
- *  - 'top': Label is before the field and above it, best for when the use will need to fill out all
- *    fields from top to bottom in a form with few fields
- *  - 'inline': Label is after the field and aligned toward it, best for small boolean fields like
- *    checkboxes or radio buttons
+ * Toggle action layout between vertical and horizontal.
  *
- * @constructor
- * @param {OO.ui.Widget} field Field widget
- * @param {Object} [config] Configuration options
- * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @param {boolean} [value] Layout actions vertically, omit to toggle
+ * @chainable
  */
-OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
-       // Config initialization
-       config = $.extend( { 'align': 'left' }, config );
-
-       // Parent constructor
-       OO.ui.FieldLayout.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.LabeledElement.call( this, this.$( '<label>' ), config );
+OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
+       value = value === undefined ? !this.verticalActionLayout : !!value;
 
-       // Properties
-       this.$field = this.$( '<div>' );
-       this.field = field;
-       this.align = null;
-
-       // Events
-       if ( this.field instanceof OO.ui.InputWidget ) {
-               this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) );
+       if ( value !== this.verticalActionLayout ) {
+               this.verticalActionLayout = value;
+               this.$actions
+                       .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
+                       .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
        }
-       this.field.connect( this, { 'disable': 'onFieldDisable' } );
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-fieldLayout' );
-       this.$field
-               .addClass( 'oo-ui-fieldLayout-field' )
-               .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() )
-               .append( this.field.$element );
-       this.setAlignment( config.align );
+       return this;
 };
 
-/* Setup */
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
+       if ( action ) {
+               return new OO.ui.Process( function () {
+                       this.close( { 'action': action } );
+               }, this );
+       }
+       return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
+};
 
-OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabeledElement );
+/**
+ * @inheritdoc
+ *
+ * @param {Object} [data] Dialog opening data
+ * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
+ * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
+ * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
+ * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
+ *   action item
+ */
+OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
+       data = data || {};
 
-/* Methods */
+       // Parent method
+       return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
+               .next( function () {
+                       this.title.setLabel(
+                               data.title !== undefined ? data.title : this.constructor.static.title
+                       );
+                       this.message.setLabel(
+                               data.message !== undefined ? data.message : this.constructor.static.message
+                       );
+                       this.message.$element.toggleClass(
+                               'oo-ui-messageDialog-message-verbose',
+                               data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
+                       );
+               }, this );
+};
 
 /**
- * Handle field disable events.
- *
- * @param {boolean} value Field is disabled
+ * @inheritdoc
  */
-OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
-       this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
+OO.ui.MessageDialog.prototype.getBodyHeight = function () {
+       return Math.round( this.text.$element.outerHeight( true ) );
 };
 
 /**
- * Handle label mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
+ * @inheritdoc
  */
-OO.ui.FieldLayout.prototype.onLabelClick = function () {
-       this.field.simulateLabelClick();
-       return false;
+OO.ui.MessageDialog.prototype.initialize = function () {
+       // Parent method
+       OO.ui.MessageDialog.super.prototype.initialize.call( this );
+
+       // Properties
+       this.$actions = this.$( '<div>' );
+       this.container = new OO.ui.PanelLayout( {
+               '$': this.$, 'scrollable': true, 'classes': [ 'oo-ui-messageDialog-container' ]
+       } );
+       this.text = new OO.ui.PanelLayout( {
+               '$': this.$, 'padded': true, 'expanded': false, 'classes': [ 'oo-ui-messageDialog-text' ]
+       } );
+       this.message = new OO.ui.LabelWidget( {
+               '$': this.$, 'classes': [ 'oo-ui-messageDialog-message' ]
+       } );
+
+       // Initialization
+       this.title.$element.addClass( 'oo-ui-messageDialog-title' );
+       this.frame.$content.addClass( 'oo-ui-messageDialog-content' );
+       this.container.$element.append( this.text.$element );
+       this.text.$element.append( this.title.$element, this.message.$element );
+       this.$body.append( this.container.$element );
+       this.$actions.addClass( 'oo-ui-messageDialog-actions' );
+       this.$foot.append( this.$actions );
 };
 
 /**
- * Get the field.
- *
- * @return {OO.ui.Widget} Field widget
+ * @inheritdoc
  */
-OO.ui.FieldLayout.prototype.getField = function () {
-       return this.field;
+OO.ui.MessageDialog.prototype.attachActions = function () {
+       var i, len, other, special, others;
+
+       // Parent method
+       OO.ui.MessageDialog.super.prototype.attachActions.call( this );
+
+       special = this.actions.getSpecial();
+       others = this.actions.getOthers();
+       if ( special.safe ) {
+               this.$actions.append( special.safe.$element );
+               special.safe.toggleFramed( false );
+       }
+       if ( others.length ) {
+               for ( i = 0, len = others.length; i < len; i++ ) {
+                       other = others[i];
+                       this.$actions.append( other.$element );
+                       other.toggleFramed( false );
+               }
+       }
+       if ( special.primary ) {
+               this.$actions.append( special.primary.$element );
+               special.primary.toggleFramed( false );
+       }
+
+       this.fitActions();
+       if ( !this.isOpening() ) {
+               this.manager.updateWindowSize( this );
+       }
+       this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
 };
 
 /**
- * Set the field alignment mode.
+ * Fit action actions into columns or rows.
  *
- * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
- * @chainable
+ * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
  */
-OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
-       if ( value !== this.align ) {
-               // Default to 'left'
-               if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
-                       value = 'left';
-               }
-               // Reorder elements
-               if ( value === 'inline' ) {
-                       this.$element.append( this.$field, this.$label );
-               } else {
-                       this.$element.append( this.$label, this.$field );
-               }
-               // Set classes
-               if ( this.align ) {
-                       this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
+OO.ui.MessageDialog.prototype.fitActions = function () {
+       var i, len, action,
+               actions = this.actions.get();
+
+       // Detect clipping
+       this.toggleVerticalActionLayout( false );
+       for ( i = 0, len = actions.length; i < len; i++ ) {
+               action = actions[i];
+               if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
+                       this.toggleVerticalActionLayout( true );
+                       break;
                }
-               this.align = value;
-               this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align );
        }
-
-       return this;
 };
 
 /**
- * Layout made of proportionally sized columns and rows.
+ * Navigation dialog window.
  *
+ * Logic:
+ * - Show and hide errors.
+ * - Retry an action.
+ *
+ * User interface:
+ * - Renders header with dialog title and one action widget on either side
+ *   (a 'safe' button on the left, and a 'primary' button on the right, both of
+ *   which close the dialog).
+ * - Displays any action widgets in the footer (none by default).
+ * - Ability to dismiss errors.
+ *
+ * Subclass responsibilities:
+ * - Register a 'safe' action.
+ * - Register a 'primary' action.
+ * - Add content to the dialog.
+ *
+ * @abstract
  * @class
- * @extends OO.ui.Layout
+ * @extends OO.ui.Dialog
  *
  * @constructor
- * @param {OO.ui.PanelLayout[]} panels Panels in the grid
  * @param {Object} [config] Configuration options
- * @cfg {number[]} [widths] Widths of columns as ratios
- * @cfg {number[]} [heights] Heights of columns as ratios
  */
-OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
-       var i, len, widths;
-
-       // Config initialization
-       config = config || {};
-
+OO.ui.ProcessDialog = function OoUiProcessDialog( manager, config ) {
        // Parent constructor
-       OO.ui.GridLayout.super.call( this, config );
-
-       // Properties
-       this.panels = [];
-       this.widths = [];
-       this.heights = [];
+       OO.ui.ProcessDialog.super.call( this, manager, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-gridLayout' );
-       for ( i = 0, len = panels.length; i < len; i++ ) {
-               this.panels.push( panels[i] );
-               this.$element.append( panels[i].$element );
-       }
-       if ( config.widths || config.heights ) {
-               this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
-       } else {
-               // Arrange in columns by default
-               widths = [];
-               for ( i = 0, len = this.panels.length; i < len; i++ ) {
-                       widths[i] = 1;
-               }
-               this.layout( widths, [ 1 ] );
-       }
+       this.$element.addClass( 'oo-ui-processDialog' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
+OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
 
-/* Events */
+/* Methods */
 
 /**
- * @event layout
+ * Handle dismiss button click events.
+ *
+ * Hides errors.
  */
+OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
+       this.hideErrors();
+};
 
 /**
- * @event update
+ * Handle retry button click events.
+ *
+ * Hides errors and then tries again.
  */
+OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
+       this.hideErrors();
+       this.executeAction( this.currentAction.getAction() );
+};
 
-/* Static Properties */
-
-OO.ui.GridLayout.static.tagName = 'div';
-
-/* Methods */
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
+       if ( this.actions.isSpecial( action ) ) {
+               this.fitLabel();
+       }
+       return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
+};
 
 /**
- * Set grid dimensions.
- *
- * @param {number[]} widths Widths of columns as ratios
- * @param {number[]} heights Heights of rows as ratios
- * @fires layout
- * @throws {Error} If grid is not large enough to fit all panels
+ * @inheritdoc
  */
-OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
-       var x, y,
-               xd = 0,
-               yd = 0,
-               cols = widths.length,
-               rows = heights.length;
+OO.ui.ProcessDialog.prototype.initialize = function () {
+       // Parent method
+       OO.ui.ProcessDialog.super.prototype.initialize.call( this );
 
-       // Verify grid is big enough to fit panels
-       if ( cols * rows < this.panels.length ) {
-               throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
-       }
+       // Properties
+       this.$navigation = this.$( '<div>' );
+       this.$location = this.$( '<div>' );
+       this.$safeActions = this.$( '<div>' );
+       this.$primaryActions = this.$( '<div>' );
+       this.$otherActions = this.$( '<div>' );
+       this.dismissButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'label': OO.ui.msg( 'ooui-dialog-process-dismiss' )
+       } );
+       this.retryButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'label': OO.ui.msg( 'ooui-dialog-process-retry' )
+       } );
+       this.$errors = this.$( '<div>' );
+       this.$errorsTitle = this.$( '<div>' );
 
-       // Sum up denominators
-       for ( x = 0; x < cols; x++ ) {
-               xd += widths[x];
-       }
-       for ( y = 0; y < rows; y++ ) {
-               yd += heights[y];
-       }
-       // Store factors
-       this.widths = [];
-       this.heights = [];
-       for ( x = 0; x < cols; x++ ) {
-               this.widths[x] = widths[x] / xd;
-       }
-       for ( y = 0; y < rows; y++ ) {
-               this.heights[y] = heights[y] / yd;
-       }
-       // Synchronize view
-       this.update();
-       this.emit( 'layout' );
+       // Events
+       this.dismissButton.connect( this, { 'click': 'onDismissErrorButtonClick' } );
+       this.retryButton.connect( this, { 'click': 'onRetryButtonClick' } );
+
+       // Initialization
+       this.title.$element.addClass( 'oo-ui-processDialog-title' );
+       this.$location
+               .append( this.title.$element )
+               .addClass( 'oo-ui-processDialog-location' );
+       this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
+       this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
+       this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
+       this.$errorsTitle
+               .addClass( 'oo-ui-processDialog-errors-title' )
+               .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
+       this.$errors
+               .addClass( 'oo-ui-processDialog-errors' )
+               .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
+       this.frame.$content
+               .addClass( 'oo-ui-processDialog-content' )
+               .append( this.$errors );
+       this.$navigation
+               .addClass( 'oo-ui-processDialog-navigation' )
+               .append( this.$safeActions, this.$location, this.$primaryActions );
+       this.$head.append( this.$navigation );
+       this.$foot.append( this.$otherActions );
 };
 
 /**
- * Update panel positions and sizes.
- *
- * @fires update
+ * @inheritdoc
  */
-OO.ui.GridLayout.prototype.update = function () {
-       var x, y, panel,
-               i = 0,
-               left = 0,
-               top = 0,
-               dimensions,
-               width = 0,
-               height = 0,
-               cols = this.widths.length,
-               rows = this.heights.length;
+OO.ui.ProcessDialog.prototype.attachActions = function () {
+       var i, len, other, special, others;
 
-       for ( y = 0; y < rows; y++ ) {
-               for ( x = 0; x < cols; x++ ) {
-                       panel = this.panels[i];
-                       width = this.widths[x];
-                       height = this.heights[y];
-                       dimensions = {
-                               'width': Math.round( width * 100 ) + '%',
-                               'height': Math.round( height * 100 ) + '%',
-                               'top': Math.round( top * 100 ) + '%'
-                       };
-                       // If RTL, reverse:
-                       if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
-                               dimensions.right = Math.round( left * 100 ) + '%';
-                       } else {
-                               dimensions.left = Math.round( left * 100 ) + '%';
-                       }
-                       panel.$element.css( dimensions );
-                       i++;
-                       left += width;
+       // Parent method
+       OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
+
+       special = this.actions.getSpecial();
+       others = this.actions.getOthers();
+       if ( special.primary ) {
+               this.$primaryActions.append( special.primary.$element );
+               special.primary.toggleFramed( false );
+       }
+       if ( others.length ) {
+               for ( i = 0, len = others.length; i < len; i++ ) {
+                       other = others[i];
+                       this.$otherActions.append( other.$element );
+                       other.toggleFramed( true );
                }
-               top += height;
-               left = 0;
+       }
+       if ( special.safe ) {
+               this.$safeActions.append( special.safe.$element );
+               special.safe.toggleFramed( false );
        }
 
-       this.emit( 'update' );
+       this.fitLabel();
+       this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
 };
 
 /**
- * Get a panel at a given position.
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
+       OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
+               .fail( OO.ui.bind( this.showErrors, this ) );
+};
+
+/**
+ * Fit label between actions.
  *
- * The x and y position is affected by the current grid layout.
+ * @chainable
+ */
+OO.ui.ProcessDialog.prototype.fitLabel = function () {
+       var width = Math.max(
+               this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
+               this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
+       );
+       this.$location.css( { 'padding-left': width, 'padding-right': width } );
+
+       return this;
+};
+
+/**
+ * Handle errors that occured durring accept or reject processes.
  *
- * @param {number} x Horizontal position
- * @param {number} y Vertical position
- * @return {OO.ui.PanelLayout} The panel at the given postion
+ * @param {OO.ui.Error[]} errors Errors to be handled
  */
-OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
-       return this.panels[( x * this.widths.length ) + y];
+OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
+       var i, len, $item,
+               items = [],
+               recoverable = true;
+
+       for ( i = 0, len = errors.length; i < len; i++ ) {
+               if ( !errors[i].isRecoverable() ) {
+                       recoverable = false;
+               }
+               $item = this.$( '<div>' )
+                       .addClass( 'oo-ui-processDialog-error' )
+                       .append( errors[i].getMessage() );
+               items.push( $item[0] );
+       }
+       this.$errorItems = this.$( items );
+       if ( recoverable ) {
+               this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
+       } else {
+               this.currentAction.setDisabled( true );
+       }
+       this.retryButton.toggle( recoverable );
+       this.$errorsTitle.after( this.$errorItems );
+       this.$errors.show().scrollTop( 0 );
+};
+
+/**
+ * Hide errors.
+ */
+OO.ui.ProcessDialog.prototype.hideErrors = function () {
+       this.$errors.hide();
+       this.$errorItems.remove();
+       this.$errorItems = null;
 };
 
 /**
@@ -4368,7 +5560,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
        this.pages = {};
        this.ignoreFocus = false;
        this.stackLayout = new OO.ui.StackLayout( { '$': this.$, 'continuous': !!config.continuous } );
-       this.autoFocus = config.autoFocus === undefined ? true : !!config.autoFocus;
+       this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
        this.outlineVisible = false;
        this.outlined = !!config.outlined;
        if ( this.outlined ) {
@@ -4465,15 +5657,16 @@ OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
  * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
  */
 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
+       var layout = this;
        if ( page ) {
-               page.scrollElementIntoView( { 'complete': OO.ui.bind( function () {
-                       if ( this.autoFocus ) {
+               page.scrollElementIntoView( { 'complete': function () {
+                       if ( layout.autoFocus ) {
                                // Set focus to the first input if nothing on the page is focused yet
                                if ( !page.$element.find( ':focus' ).length ) {
                                        page.$element.find( ':input:first' ).focus();
                                }
                        }
-               }, this ) } );
+               } } );
        }
 };
 
@@ -4708,58 +5901,452 @@ OO.ui.BookletLayout.prototype.clearPages = function () {
        }
        this.stackLayout.clearItems();
 
-       this.emit( 'remove', pages );
+       this.emit( 'remove', pages );
+
+       return this;
+};
+
+/**
+ * Set the current page by name.
+ *
+ * @fires set
+ * @param {string} name Symbolic name of page
+ */
+OO.ui.BookletLayout.prototype.setPage = function ( name ) {
+       var selectedItem,
+               page = this.pages[name];
+
+       if ( name !== this.currentPageName ) {
+               if ( this.outlined ) {
+                       selectedItem = this.outlineWidget.getSelectedItem();
+                       if ( selectedItem && selectedItem.getData() !== name ) {
+                               this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
+                       }
+               }
+               if ( page ) {
+                       if ( this.currentPageName && this.pages[this.currentPageName] ) {
+                               this.pages[this.currentPageName].setActive( false );
+                               // Blur anything focused if the next page doesn't have anything focusable - this
+                               // is not needed if the next page has something focusable because once it is focused
+                               // this blur happens automatically
+                               if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
+                                       this.pages[this.currentPageName].$element.find( ':focus' ).blur();
+                               }
+                       }
+                       this.currentPageName = name;
+                       this.stackLayout.setItem( page );
+                       page.setActive( true );
+                       this.emit( 'set', page );
+               }
+       }
+};
+
+/**
+ * Call this after adding or removing items from the OutlineWidget.
+ *
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
+       // Auto-select first item when nothing is selected anymore
+       if ( !this.outlineWidget.getSelectedItem() ) {
+               this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
+       }
+
+       return this;
+};
+
+/**
+ * Layout made of a field and optional label.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.LabeledElement
+ *
+ * Available label alignment modes include:
+ *  - 'left': Label is before the field and aligned away from it, best for when the user will be
+ *    scanning for a specific label in a form with many fields
+ *  - 'right': Label is before the field and aligned toward it, best for forms the user is very
+ *    familiar with and will tab through field checking quickly to verify which field they are in
+ *  - 'top': Label is before the field and above it, best for when the use will need to fill out all
+ *    fields from top to bottom in a form with few fields
+ *  - 'inline': Label is after the field and aligned toward it, best for small boolean fields like
+ *    checkboxes or radio buttons
+ *
+ * @constructor
+ * @param {OO.ui.Widget} field Field widget
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @cfg {string} [help] Explanatory text shown as a '?' icon.
+ */
+OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
+       var popupButtonWidget;
+       // Config initialization
+       config = $.extend( { 'align': 'left' }, config );
+
+       // Parent constructor
+       OO.ui.FieldLayout.super.call( this, config );
+
+       // Mixin constructors
+       this.$help = this.$( '<div>' );
+       OO.ui.LabeledElement.call( this, this.$( '<label>' ), config );
+       if ( config.help ) {
+               popupButtonWidget = new OO.ui.PopupButtonWidget( $.extend(
+                       {
+                               '$': this.$,
+                               'frameless': true,
+                               'icon': 'info',
+                               'title': config.help
+                       },
+                       config,
+                       { label: null }
+               ) );
+               popupButtonWidget.getPopup().$body.append( this.getElementDocument().createTextNode( config.help ) );
+               this.$help = popupButtonWidget.$element;
+       }
+
+       // Properties
+       this.$field = this.$( '<div>' );
+       this.field = field;
+       this.align = null;
+
+       // Events
+       if ( this.field instanceof OO.ui.InputWidget ) {
+               this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) );
+       }
+       this.field.connect( this, { 'disable': 'onFieldDisable' } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-fieldLayout' );
+       this.$field
+               .addClass( 'oo-ui-fieldLayout-field' )
+               .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() )
+               .append( this.field.$element );
+       this.setAlignment( config.align );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabeledElement );
+
+/* Methods */
+
+/**
+ * Handle field disable events.
+ *
+ * @param {boolean} value Field is disabled
+ */
+OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
+       this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
+};
+
+/**
+ * Handle label mouse click events.
+ *
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.FieldLayout.prototype.onLabelClick = function () {
+       this.field.simulateLabelClick();
+       return false;
+};
+
+/**
+ * Get the field.
+ *
+ * @return {OO.ui.Widget} Field widget
+ */
+OO.ui.FieldLayout.prototype.getField = function () {
+       return this.field;
+};
+
+/**
+ * Set the field alignment mode.
+ *
+ * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @chainable
+ */
+OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
+       if ( value !== this.align ) {
+               // Default to 'left'
+               if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
+                       value = 'left';
+               }
+               // Reorder elements
+               if ( value === 'inline' ) {
+                       this.$element.append( this.$field, this.$label, this.$help );
+               } else {
+                       this.$element.append( this.$help, this.$label, this.$field );
+               }
+               // Set classes
+               if ( this.align ) {
+                       this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
+               }
+               this.align = value;
+               this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align );
+       }
+
+       return this;
+};
+
+/**
+ * Layout made of a fieldset and optional legend.
+ *
+ * Just add OO.ui.FieldLayout items.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [icon] Symbolic icon name
+ * @cfg {OO.ui.FieldLayout[]} [items] Items to add
+ */
+OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
+       // Config initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.FieldsetLayout.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
+       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
+       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-fieldsetLayout' )
+               .prepend( this.$icon, this.$label, this.$group );
+       if ( $.isArray( config.items ) ) {
+               this.addItems( config.items );
+       }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
+
+/* Static Properties */
+
+OO.ui.FieldsetLayout.static.tagName = 'div';
+
+/**
+ * Layout with an HTML form.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.FormLayout = function OoUiFormLayout( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.FormLayout.super.call( this, config );
+
+       // Events
+       this.$element.on( 'submit', OO.ui.bind( this.onFormSubmit, this ) );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-formLayout' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
+
+/* Events */
+
+/**
+ * @event submit
+ */
+
+/* Static Properties */
+
+OO.ui.FormLayout.static.tagName = 'form';
+
+/* Methods */
+
+/**
+ * Handle form submit events.
+ *
+ * @param {jQuery.Event} e Submit event
+ * @fires submit
+ */
+OO.ui.FormLayout.prototype.onFormSubmit = function () {
+       this.emit( 'submit' );
+       return false;
+};
+
+/**
+ * Layout made of proportionally sized columns and rows.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {OO.ui.PanelLayout[]} panels Panels in the grid
+ * @param {Object} [config] Configuration options
+ * @cfg {number[]} [widths] Widths of columns as ratios
+ * @cfg {number[]} [heights] Heights of columns as ratios
+ */
+OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
+       var i, len, widths;
+
+       // Config initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.GridLayout.super.call( this, config );
+
+       // Properties
+       this.panels = [];
+       this.widths = [];
+       this.heights = [];
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-gridLayout' );
+       for ( i = 0, len = panels.length; i < len; i++ ) {
+               this.panels.push( panels[i] );
+               this.$element.append( panels[i].$element );
+       }
+       if ( config.widths || config.heights ) {
+               this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
+       } else {
+               // Arrange in columns by default
+               widths = [];
+               for ( i = 0, len = this.panels.length; i < len; i++ ) {
+                       widths[i] = 1;
+               }
+               this.layout( widths, [ 1 ] );
+       }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
+
+/* Events */
+
+/**
+ * @event layout
+ */
+
+/**
+ * @event update
+ */
+
+/* Static Properties */
+
+OO.ui.GridLayout.static.tagName = 'div';
+
+/* Methods */
+
+/**
+ * Set grid dimensions.
+ *
+ * @param {number[]} widths Widths of columns as ratios
+ * @param {number[]} heights Heights of rows as ratios
+ * @fires layout
+ * @throws {Error} If grid is not large enough to fit all panels
+ */
+OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
+       var x, y,
+               xd = 0,
+               yd = 0,
+               cols = widths.length,
+               rows = heights.length;
+
+       // Verify grid is big enough to fit panels
+       if ( cols * rows < this.panels.length ) {
+               throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
+       }
 
-       return this;
+       // Sum up denominators
+       for ( x = 0; x < cols; x++ ) {
+               xd += widths[x];
+       }
+       for ( y = 0; y < rows; y++ ) {
+               yd += heights[y];
+       }
+       // Store factors
+       this.widths = [];
+       this.heights = [];
+       for ( x = 0; x < cols; x++ ) {
+               this.widths[x] = widths[x] / xd;
+       }
+       for ( y = 0; y < rows; y++ ) {
+               this.heights[y] = heights[y] / yd;
+       }
+       // Synchronize view
+       this.update();
+       this.emit( 'layout' );
 };
 
 /**
- * Set the current page by name.
+ * Update panel positions and sizes.
  *
- * @fires set
- * @param {string} name Symbolic name of page
+ * @fires update
  */
-OO.ui.BookletLayout.prototype.setPage = function ( name ) {
-       var selectedItem,
-               page = this.pages[name];
+OO.ui.GridLayout.prototype.update = function () {
+       var x, y, panel,
+               i = 0,
+               left = 0,
+               top = 0,
+               dimensions,
+               width = 0,
+               height = 0,
+               cols = this.widths.length,
+               rows = this.heights.length;
 
-       if ( name !== this.currentPageName ) {
-               if ( this.outlined ) {
-                       selectedItem = this.outlineWidget.getSelectedItem();
-                       if ( selectedItem && selectedItem.getData() !== name ) {
-                               this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
-                       }
-               }
-               if ( page ) {
-                       if ( this.currentPageName && this.pages[this.currentPageName] ) {
-                               this.pages[this.currentPageName].setActive( false );
-                               // Blur anything focused if the next page doesn't have anything focusable - this
-                               // is not needed if the next page has something focusable because once it is focused
-                               // this blur happens automatically
-                               if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
-                                       this.pages[this.currentPageName].$element.find( ':focus' ).blur();
-                               }
+       for ( y = 0; y < rows; y++ ) {
+               height = this.heights[y];
+               for ( x = 0; x < cols; x++ ) {
+                       panel = this.panels[i];
+                       width = this.widths[x];
+                       dimensions = {
+                               'width': Math.round( width * 100 ) + '%',
+                               'height': Math.round( height * 100 ) + '%',
+                               'top': Math.round( top * 100 ) + '%',
+                               // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
+                               'visibility': width === 0 || height === 0 ? 'hidden' : ''
+                       };
+                       // If RTL, reverse:
+                       if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
+                               dimensions.right = Math.round( left * 100 ) + '%';
+                       } else {
+                               dimensions.left = Math.round( left * 100 ) + '%';
                        }
-                       this.currentPageName = name;
-                       this.stackLayout.setItem( page );
-                       page.setActive( true );
-                       this.emit( 'set', page );
+                       panel.$element.css( dimensions );
+                       i++;
+                       left += width;
                }
+               top += height;
+               left = 0;
        }
+
+       this.emit( 'update' );
 };
 
 /**
- * Call this after adding or removing items from the OutlineWidget.
+ * Get a panel at a given position.
  *
- * @chainable
+ * The x and y position is affected by the current grid layout.
+ *
+ * @param {number} x Horizontal position
+ * @param {number} y Vertical position
+ * @return {OO.ui.PanelLayout} The panel at the given postion
  */
-OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
-       // Auto-select first item when nothing is selected anymore
-       if ( !this.outlineWidget.getSelectedItem() ) {
-               this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
-       }
-
-       return this;
+OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
+       return this.panels[( x * this.widths.length ) + y];
 };
 
 /**
@@ -4770,8 +6357,9 @@ OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [scrollable] Allow vertical scrolling
- * @cfg {boolean} [padded] Pad the content from the edges
+ * @cfg {boolean} [scrollable=false] Allow vertical scrolling
+ * @cfg {boolean} [padded=false] Pad the content from the edges
+ * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
  */
 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
        // Config initialization
@@ -4789,6 +6377,10 @@ OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
        if ( config.padded ) {
                this.$element.addClass( 'oo-ui-panelLayout-padded' );
        }
+
+       if ( config.expanded === undefined || config.expanded ) {
+               this.$element.addClass( 'oo-ui-panelLayout-expanded' );
+       }
 };
 
 /* Setup */
@@ -4863,13 +6455,33 @@ OO.ui.PageLayout.prototype.getOutlineItem = function () {
 };
 
 /**
- * Get outline item.
+ * Set outline item.
+ *
+ * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
+ *   outline item as desired; this method is called for setting (with an object) and unsetting
+ *   (with null) and overriding methods would have to check the value of `outlineItem` to avoid
+ *   operating on null instead of an OO.ui.OutlineItemWidget object.
  *
  * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
  * @chainable
  */
 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
-       this.outlineItem = outlineItem;
+       this.outlineItem = outlineItem || null;
+       if ( outlineItem ) {
+               this.setupOutlineItem();
+       }
+       return this;
+};
+
+/**
+ * Setup outline item.
+ *
+ * @localdoc Subclasses should override this method to adjust the outline item as desired.
+ *
+ * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup
+ * @chainable
+ */
+OO.ui.PageLayout.prototype.setupOutlineItem = function () {
        return this;
 };
 
@@ -5172,388 +6784,724 @@ OO.ui.PopupToolGroup.prototype.setDisabled = function () {
        // Parent method
        OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
 
-       if ( this.isDisabled() && this.isElementAttached() ) {
-               this.setActive( false );
+       if ( this.isDisabled() && this.isElementAttached() ) {
+               this.setActive( false );
+       }
+};
+
+/**
+ * Handle focus being lost.
+ *
+ * The event is actually generated from a mouseup, so it is not a normal blur event object.
+ *
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
+       // Only deactivate when clicking outside the dropdown element
+       if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
+               this.setActive( false );
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupToolGroup.prototype.onMouseUp = function ( e ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.setActive( false );
+       }
+       return OO.ui.PopupToolGroup.super.prototype.onMouseUp.call( this, e );
+};
+
+/**
+ * Handle mouse up events.
+ *
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
+       return false;
+};
+
+/**
+ * Handle mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.setActive( !this.active );
+       }
+       return false;
+};
+
+/**
+ * Switch into active mode.
+ *
+ * When active, mouseup events anywhere in the document will trigger deactivation.
+ */
+OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
+       value = !!value;
+       if ( this.active !== value ) {
+               this.active = value;
+               if ( value ) {
+                       this.setClipping( true );
+                       this.$element.addClass( 'oo-ui-popupToolGroup-active' );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
+               } else {
+                       this.setClipping( false );
+                       this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
+                       this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
+               }
+       }
+};
+
+/**
+ * Drop down list layout of tools as labeled icon buttons.
+ *
+ * @class
+ * @extends OO.ui.PopupToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
+       // Parent constructor
+       OO.ui.ListToolGroup.super.call( this, toolbar, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-listToolGroup' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
+
+/* Static Properties */
+
+OO.ui.ListToolGroup.static.accelTooltips = true;
+
+OO.ui.ListToolGroup.static.name = 'list';
+
+/**
+ * Drop down menu layout of tools as selectable menu items.
+ *
+ * @class
+ * @extends OO.ui.PopupToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.MenuToolGroup.super.call( this, toolbar, config );
+
+       // Events
+       this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuToolGroup' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
+
+/* Static Properties */
+
+OO.ui.MenuToolGroup.static.accelTooltips = true;
+
+OO.ui.MenuToolGroup.static.name = 'menu';
+
+/* Methods */
+
+/**
+ * Handle the toolbar state being updated.
+ *
+ * When the state changes, the title of each active item in the menu will be joined together and
+ * used as a label for the group. The label will be empty if none of the items are active.
+ */
+OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
+       var name,
+               labelTexts = [];
+
+       for ( name in this.tools ) {
+               if ( this.tools[name].isActive() ) {
+                       labelTexts.push( this.tools[name].getTitle() );
+               }
        }
+
+       this.setLabel( labelTexts.join( ', ' ) || ' ' );
 };
 
 /**
- * Handle focus being lost.
+ * Tool that shows a popup when selected.
  *
- * The event is actually generated from a mouseup, so it is not a normal blur event object.
+ * @abstract
+ * @class
+ * @extends OO.ui.Tool
+ * @mixins OO.ui.PopuppableElement
  *
- * @param {jQuery.Event} e Mouse up event
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
  */
-OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
-       // Only deactivate when clicking outside the dropdown element
-       if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
-               this.setActive( false );
-       }
+OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
+       // Parent constructor
+       OO.ui.PopupTool.super.call( this, toolbar, config );
+
+       // Mixin constructors
+       OO.ui.PopuppableElement.call( this, config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-popupTool' )
+               .append( this.popup.$element );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
+OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
+
+/* Methods */
+
 /**
+ * Handle the tool being selected.
+ *
  * @inheritdoc
  */
-OO.ui.PopupToolGroup.prototype.onMouseUp = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
-               this.setActive( false );
+OO.ui.PopupTool.prototype.onSelect = function () {
+       if ( !this.isDisabled() ) {
+               this.popup.toggle();
        }
-       return OO.ui.PopupToolGroup.super.prototype.onMouseUp.call( this, e );
+       this.setActive( false );
+       return false;
 };
 
 /**
- * Handle mouse up events.
+ * Handle the toolbar state being updated.
  *
- * @param {jQuery.Event} e Mouse up event
+ * @inheritdoc
  */
-OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
-       return false;
+OO.ui.PopupTool.prototype.onUpdateState = function () {
+       this.setActive( false );
 };
 
 /**
- * Handle mouse down events.
+ * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
  *
- * @param {jQuery.Event} e Mouse down event
+ * Use together with OO.ui.ItemWidget to make disabled state inheritable.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {jQuery} $group Container node, assigned to #$group
+ * @param {Object} [config] Configuration options
  */
-OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
-               this.setActive( !this.active );
-       }
-       return false;
+OO.ui.GroupWidget = function OoUiGroupWidget( $element, config ) {
+       // Parent constructor
+       OO.ui.GroupWidget.super.call( this, $element, config );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
+
+/* Methods */
+
 /**
- * Switch into active mode.
+ * Set the disabled state of the widget.
  *
- * When active, mouseup events anywhere in the document will trigger deactivation.
+ * This will also update the disabled state of child widgets.
+ *
+ * @param {boolean} disabled Disable widget
+ * @chainable
  */
-OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
-       value = !!value;
-       if ( this.active !== value ) {
-               this.active = value;
-               if ( value ) {
-                       this.setClipping( true );
-                       this.$element.addClass( 'oo-ui-popupToolGroup-active' );
-                       this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
-               } else {
-                       this.setClipping( false );
-                       this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
-                       this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
+OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
+       var i, len;
+
+       // Parent method
+       // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
+       OO.ui.Widget.prototype.setDisabled.call( this, disabled );
+
+       // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
+       if ( this.items ) {
+               for ( i = 0, len = this.items.length; i < len; i++ ) {
+                       this.items[i].updateDisabled();
                }
        }
+
+       return this;
 };
 
 /**
- * Drop down list layout of tools as labeled icon buttons.
+ * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
  *
+ * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
+ * allows bidrectional communication.
+ *
+ * Use together with OO.ui.GroupWidget to make disabled state inheritable.
+ *
+ * @abstract
  * @class
- * @extends OO.ui.PopupToolGroup
  *
  * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
  */
-OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
-       // Parent constructor
-       OO.ui.ListToolGroup.super.call( this, toolbar, config );
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-listToolGroup' );
+OO.ui.ItemWidget = function OoUiItemWidget() {
+       //
 };
 
-/* Setup */
+/* Methods */
 
-OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
+/**
+ * Check if widget is disabled.
+ *
+ * Checks parent if present, making disabled state inheritable.
+ *
+ * @return {boolean} Widget is disabled
+ */
+OO.ui.ItemWidget.prototype.isDisabled = function () {
+       return this.disabled ||
+               ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
+};
 
-/* Static Properties */
+/**
+ * Set group element is in.
+ *
+ * @param {OO.ui.GroupElement|null} group Group element, null if none
+ * @chainable
+ */
+OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
+       // Parent method
+       // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
+       OO.ui.Element.prototype.setElementGroup.call( this, group );
 
-OO.ui.ListToolGroup.static.accelTooltips = true;
+       // Initialize item disabled states
+       this.updateDisabled();
 
-OO.ui.ListToolGroup.static.name = 'list';
+       return this;
+};
 
 /**
- * Drop down menu layout of tools as selectable menu items.
+ * Mixin that adds a menu showing suggested values for a text input.
+ *
+ * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
  *
  * @class
- * @extends OO.ui.PopupToolGroup
+ * @abstract
  *
  * @constructor
- * @param {OO.ui.Toolbar} toolbar
+ * @param {OO.ui.TextInputWidget} input Input widget
  * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
  */
-OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
-       // Configuration initialization
+OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
+       // Config intialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.MenuToolGroup.super.call( this, toolbar, config );
+       // Properties
+       this.lookupInput = input;
+       this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last();
+       this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
+               '$': OO.ui.Element.getJQuery( this.$overlay ),
+               'input': this.lookupInput,
+               '$container': config.$container
+       } );
+       this.lookupCache = {};
+       this.lookupQuery = null;
+       this.lookupRequest = null;
+       this.populating = false;
 
        // Events
-       this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+       this.$overlay.append( this.lookupMenu.$element );
+
+       this.lookupInput.$input.on( {
+               'focus': OO.ui.bind( this.onLookupInputFocus, this ),
+               'blur': OO.ui.bind( this.onLookupInputBlur, this ),
+               'mousedown': OO.ui.bind( this.onLookupInputMouseDown, this )
+       } );
+       this.lookupInput.connect( this, { 'change': 'onLookupInputChange' } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-menuToolGroup' );
+       this.$element.addClass( 'oo-ui-lookupWidget' );
+       this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
 };
 
-/* Setup */
+/* Methods */
 
-OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
+/**
+ * Handle input focus event.
+ *
+ * @param {jQuery.Event} e Input focus event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
+       this.openLookupMenu();
+};
 
-/* Static Properties */
+/**
+ * Handle input blur event.
+ *
+ * @param {jQuery.Event} e Input blur event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
+       this.lookupMenu.toggle( false );
+};
 
-OO.ui.MenuToolGroup.static.accelTooltips = true;
+/**
+ * Handle input mouse down event.
+ *
+ * @param {jQuery.Event} e Input mouse down event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Handle input change event.
+ *
+ * @param {string} value New input value
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Get lookup menu.
+ *
+ * @return {OO.ui.TextInputMenuWidget}
+ */
+OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
+       return this.lookupMenu;
+};
+
+/**
+ * Open the menu.
+ *
+ * @chainable
+ */
+OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
+       var value = this.lookupInput.getValue();
 
-OO.ui.MenuToolGroup.static.name = 'menu';
+       if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
+               this.populateLookupMenu();
+               this.lookupMenu.toggle( true );
+       } else {
+               this.lookupMenu
+                       .clearItems()
+                       .toggle( false );
+       }
 
-/* Methods */
+       return this;
+};
 
 /**
- * Handle the toolbar state being updated.
+ * Populate lookup menu with current information.
  *
- * When the state changes, the title of each active item in the menu will be joined together and
- * used as a label for the group. The label will be empty if none of the items are active.
+ * @chainable
  */
-OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
-       var name,
-               labelTexts = [];
+OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
+       var widget = this;
 
-       for ( name in this.tools ) {
-               if ( this.tools[name].isActive() ) {
-                       labelTexts.push( this.tools[name].getTitle() );
-               }
+       if ( !this.populating ) {
+               this.populating = true;
+               this.getLookupMenuItems()
+                       .done( function ( items ) {
+                               widget.lookupMenu.clearItems();
+                               if ( items.length ) {
+                                       widget.lookupMenu
+                                               .addItems( items )
+                                               .toggle( true );
+                                       widget.initializeLookupMenuSelection();
+                                       widget.openLookupMenu();
+                               } else {
+                                       widget.lookupMenu.toggle( true );
+                               }
+                               widget.populating = false;
+                       } )
+                       .fail( function () {
+                               widget.lookupMenu.clearItems();
+                               widget.populating = false;
+                       } );
        }
 
-       this.setLabel( labelTexts.join( ', ' ) || ' ' );
+       return this;
 };
 
 /**
- * Tool that shows a popup when selected.
- *
- * @abstract
- * @class
- * @extends OO.ui.Tool
- * @mixins OO.ui.PopuppableElement
+ * Set selection in the lookup menu with current information.
  *
- * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
+ * @chainable
  */
-OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
-       // Parent constructor
-       OO.ui.PopupTool.super.call( this, toolbar, config );
-
-       // Mixin constructors
-       OO.ui.PopuppableElement.call( this, config );
-
-       // Initialization
-       this.$element
-               .addClass( 'oo-ui-popupTool' )
-               .append( this.popup.$element );
+OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
+       if ( !this.lookupMenu.getSelectedItem() ) {
+               this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
+       }
+       this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
-OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
-
-/* Methods */
-
 /**
- * Handle the tool being selected.
+ * Get lookup menu items for the current query.
  *
- * @inheritdoc
+ * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
+ * of the done event
  */
-OO.ui.PopupTool.prototype.onSelect = function () {
-       if ( !this.isDisabled() ) {
-               if ( this.popup.isVisible() ) {
-                       this.hidePopup();
+OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
+       var widget = this,
+               value = this.lookupInput.getValue(),
+               deferred = $.Deferred();
+
+       if ( value && value !== this.lookupQuery ) {
+               // Abort current request if query has changed
+               if ( this.lookupRequest ) {
+                       this.lookupRequest.abort();
+                       this.lookupQuery = null;
+                       this.lookupRequest = null;
+               }
+               if ( value in this.lookupCache ) {
+                       deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
                } else {
-                       this.showPopup();
+                       this.lookupQuery = value;
+                       this.lookupRequest = this.getLookupRequest()
+                               .always( function () {
+                                       widget.lookupQuery = null;
+                                       widget.lookupRequest = null;
+                               } )
+                               .done( function ( data ) {
+                                       widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
+                                       deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
+                               } )
+                               .fail( function () {
+                                       deferred.reject();
+                               } );
+                       this.pushPending();
+                       this.lookupRequest.always( function () {
+                               widget.popPending();
+                       } );
                }
        }
-       this.setActive( false );
-       return false;
+       return deferred.promise();
 };
 
 /**
- * Handle the toolbar state being updated.
+ * Get a new request object of the current lookup query value.
  *
- * @inheritdoc
+ * @abstract
+ * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
  */
-OO.ui.PopupTool.prototype.onUpdateState = function () {
-       this.setActive( false );
+OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
+       // Stub, implemented in subclass
+       return null;
 };
 
 /**
- * Group widget.
- *
- * Mixin for OO.ui.Widget subclasses.
+ * Handle successful lookup request.
  *
- * Use together with OO.ui.ItemWidget to make disabled state inheritable.
+ * Overriding methods should call #populateLookupMenu when results are available and cache results
+ * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
  *
  * @abstract
- * @class
- * @extends OO.ui.GroupElement
- *
- * @constructor
- * @param {jQuery} $group Container node, assigned to #$group
- * @param {Object} [config] Configuration options
+ * @param {Mixed} data Response from server
  */
-OO.ui.GroupWidget = function OoUiGroupWidget( $element, config ) {
-       // Parent constructor
-       OO.ui.GroupWidget.super.call( this, $element, config );
+OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
+       // Stub, implemented in subclass
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
-
-/* Methods */
-
 /**
- * Set the disabled state of the widget.
- *
- * This will also update the disabled state of child widgets.
+ * Get a list of menu item widgets from the data stored by the lookup request's done handler.
  *
- * @param {boolean} disabled Disable widget
- * @chainable
+ * @abstract
+ * @param {Mixed} data Cached result data, usually an array
+ * @return {OO.ui.MenuItemWidget[]} Menu items
  */
-OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
-       var i, len;
-
-       // Parent method
-       // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
-       OO.ui.Widget.prototype.setDisabled.call( this, disabled );
-
-       // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
-       if ( this.items ) {
-               for ( i = 0, len = this.items.length; i < len; i++ ) {
-                       this.items[i].updateDisabled();
-               }
-       }
-
-       return this;
+OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
+       // Stub, implemented in subclass
+       return [];
 };
 
 /**
- * Item widget.
+ * Set of controls for an OO.ui.OutlineWidget.
  *
- * Use together with OO.ui.GroupWidget to make disabled state inheritable.
+ * Controls include moving items up and down, removing items, and adding different kinds of items.
  *
- * @abstract
  * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.GroupElement
+ * @mixins OO.ui.IconedElement
  *
  * @constructor
+ * @param {OO.ui.OutlineWidget} outline Outline to control
+ * @param {Object} [config] Configuration options
  */
-OO.ui.ItemWidget = function OoUiItemWidget() {
-       //
+OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
+       // Configuration initialization
+       config = $.extend( { 'icon': 'add-item' }, config );
+
+       // Parent constructor
+       OO.ui.OutlineControlsWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
+
+       // Properties
+       this.outline = outline;
+       this.$movers = this.$( '<div>' );
+       this.upButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'framed': false,
+               'icon': 'collapse',
+               'title': OO.ui.msg( 'ooui-outline-control-move-up' )
+       } );
+       this.downButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'framed': false,
+               'icon': 'expand',
+               'title': OO.ui.msg( 'ooui-outline-control-move-down' )
+       } );
+       this.removeButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'framed': false,
+               'icon': 'remove',
+               'title': OO.ui.msg( 'ooui-outline-control-remove' )
+       } );
+
+       // Events
+       outline.connect( this, {
+               'select': 'onOutlineChange',
+               'add': 'onOutlineChange',
+               'remove': 'onOutlineChange'
+       } );
+       this.upButton.connect( this, { 'click': [ 'emit', 'move', -1 ] } );
+       this.downButton.connect( this, { 'click': [ 'emit', 'move', 1 ] } );
+       this.removeButton.connect( this, { 'click': [ 'emit', 'remove' ] } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-outlineControlsWidget' );
+       this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
+       this.$movers
+               .addClass( 'oo-ui-outlineControlsWidget-movers' )
+               .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
+       this.$element.append( this.$icon, this.$group, this.$movers );
 };
 
-/* Methods */
+/* Setup */
+
+OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
+OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconedElement );
+
+/* Events */
 
 /**
- * Check if widget is disabled.
- *
- * Checks parent if present, making disabled state inheritable.
- *
- * @return {boolean} Widget is disabled
+ * @event move
+ * @param {number} places Number of places to move
  */
-OO.ui.ItemWidget.prototype.isDisabled = function () {
-       return this.disabled ||
-               ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
-};
 
 /**
- * Set group element is in.
- *
- * @param {OO.ui.GroupElement|null} group Group element, null if none
- * @chainable
+ * @event remove
  */
-OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
-       // Parent method
-       // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
-       OO.ui.Element.prototype.setElementGroup.call( this, group );
 
-       // Initialize item disabled states
-       this.updateDisabled();
+/* Methods */
 
-       return this;
+/**
+ * Handle outline change events.
+ */
+OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
+       var i, len, firstMovable, lastMovable,
+               items = this.outline.getItems(),
+               selectedItem = this.outline.getSelectedItem(),
+               movable = selectedItem && selectedItem.isMovable(),
+               removable = selectedItem && selectedItem.isRemovable();
+
+       if ( movable ) {
+               i = -1;
+               len = items.length;
+               while ( ++i < len ) {
+                       if ( items[i].isMovable() ) {
+                               firstMovable = items[i];
+                               break;
+                       }
+               }
+               i = len;
+               while ( i-- ) {
+                       if ( items[i].isMovable() ) {
+                               lastMovable = items[i];
+                               break;
+                       }
+               }
+       }
+       this.upButton.setDisabled( !movable || selectedItem === firstMovable );
+       this.downButton.setDisabled( !movable || selectedItem === lastMovable );
+       this.removeButton.setDisabled( !removable );
 };
 
 /**
- * Icon widget.
+ * Mixin for widgets with a boolean on/off state.
  *
+ * @abstract
  * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.TitledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] Initial value
  */
-OO.ui.IconWidget = function OoUiIconWidget( config ) {
-       // Config intialization
+OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
+       // Configuration initialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.IconWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.IconedElement.call( this, this.$element, config );
-       OO.ui.TitledElement.call( this, this.$element, config );
+       // Properties
+       this.value = null;
 
        // Initialization
-       this.$element.addClass( 'oo-ui-iconWidget' );
+       this.$element.addClass( 'oo-ui-toggleWidget' );
+       this.setValue( !!config.value );
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
+/* Events */
 
-/* Static Properties */
+/**
+ * @event change
+ * @param {boolean} value Changed value
+ */
 
-OO.ui.IconWidget.static.tagName = 'span';
+/* Methods */
 
 /**
- * Indicator widget.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.TitledElement
+ * Get the value of the toggle.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @return {boolean}
  */
-OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
-       // Config intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.IndicatorWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.IndicatedElement.call( this, this.$element, config );
-       OO.ui.TitledElement.call( this, this.$element, config );
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-indicatorWidget' );
+OO.ui.ToggleWidget.prototype.getValue = function () {
+       return this.value;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
-
-/* Static Properties */
-
-OO.ui.IndicatorWidget.static.tagName = 'span';
+/**
+ * Set the value of the toggle.
+ *
+ * @param {boolean} value New value
+ * @fires change
+ * @chainable
+ */
+OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
+       value = !!value;
+       if ( this.value !== value ) {
+               this.value = value;
+               this.emit( 'change', value );
+               this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
+               this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
+       }
+       return this;
+};
 
 /**
- * Container for multiple related buttons.
+ * Group widget for multiple related buttons.
  *
  * Use together with OO.ui.ButtonWidget.
  *
@@ -5585,7 +7533,7 @@ OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
 
 /**
- * Button widget.
+ * Generic widget for buttons.
  *
  * @class
  * @extends OO.ui.Widget
@@ -5598,7 +7546,6 @@ OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {string} [title=''] Title text
  * @cfg {string} [href] Hyperlink to visit when clicked
  * @cfg {string} [target] Target to open hyperlink in
  */
@@ -5618,7 +7565,9 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        OO.ui.FlaggableElement.call( this, config );
 
        // Properties
-       this.isHyperlink = typeof config.href === 'string';
+       this.href = null;
+       this.target = null;
+       this.isHyperlink = false;
 
        // Events
        this.$button.on( {
@@ -5627,12 +7576,12 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        } );
 
        // Initialization
-       this.$button
-               .append( this.$icon, this.$label, this.$indicator )
-               .attr( { 'href': config.href, 'target': config.target } );
+       this.$button.append( this.$icon, this.$label, this.$indicator );
        this.$element
                .addClass( 'oo-ui-buttonWidget' )
                .append( this.$button );
+       this.setHref( config.href );
+       this.setTarget( config.target );
 };
 
 /* Setup */
@@ -5655,2751 +7604,2771 @@ OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement );
 
 /**
  * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
- * @fires click
- */
-OO.ui.ButtonWidget.prototype.onClick = function () {
-       if ( !this.isDisabled() ) {
-               this.emit( 'click' );
-               if ( this.isHyperlink ) {
-                       return true;
-               }
-       }
-       return false;
-};
-
-/**
- * Handles keypress events.
- *
- * @param {jQuery.Event} e Keypress event
- * @fires click
- */
-OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
-       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
-               this.onClick();
-               if ( this.isHyperlink ) {
-                       return true;
-               }
-       }
-       return false;
-};
-
-/**
- * Input widget.
- *
- * @abstract
- * @class
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [name=''] HTML input name
- * @cfg {string} [value=''] Input value
- * @cfg {boolean} [readOnly=false] Prevent changes
- * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
- */
-OO.ui.InputWidget = function OoUiInputWidget( config ) {
-       // Config intialization
-       config = $.extend( { 'readOnly': false }, config );
-
-       // Parent constructor
-       OO.ui.InputWidget.super.call( this, config );
-
-       // Properties
-       this.$input = this.getInputElement( config );
-       this.value = '';
-       this.readOnly = false;
-       this.inputFilter = config.inputFilter;
-
-       // Events
-       this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) );
-
-       // Initialization
-       this.$input
-               .attr( 'name', config.name )
-               .prop( 'disabled', this.isDisabled() );
-       this.setReadOnly( config.readOnly );
-       this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
-       this.setValue( config.value );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
-
-/* Events */
-
-/**
- * @event change
- * @param value
- */
-
-/* Methods */
-
-/**
- * Get input element.
- *
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
- */
-OO.ui.InputWidget.prototype.getInputElement = function () {
-       return this.$( '<input>' );
-};
-
-/**
- * Handle potentially value-changing events.
- *
- * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
- */
-OO.ui.InputWidget.prototype.onEdit = function () {
-       if ( !this.isDisabled() ) {
-               // Allow the stack to clear so the value will be updated
-               setTimeout( OO.ui.bind( function () {
-                       this.setValue( this.$input.val() );
-               }, this ) );
-       }
-};
-
-/**
- * Get the value of the input.
- *
- * @return {string} Input value
- */
-OO.ui.InputWidget.prototype.getValue = function () {
-       return this.value;
-};
-
-/**
- * Sets the direction of the current input, either RTL or LTR
- *
- * @param {boolean} isRTL
- */
-OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
-       if ( isRTL ) {
-               this.$input.removeClass( 'oo-ui-ltr' );
-               this.$input.addClass( 'oo-ui-rtl' );
-       } else {
-               this.$input.removeClass( 'oo-ui-rtl' );
-               this.$input.addClass( 'oo-ui-ltr' );
-       }
-};
-
-/**
- * Set the value of the input.
- *
- * @param {string} value New value
- * @fires change
- * @chainable
- */
-OO.ui.InputWidget.prototype.setValue = function ( value ) {
-       value = this.sanitizeValue( value );
-       if ( this.value !== value ) {
-               this.value = value;
-               this.emit( 'change', this.value );
-       }
-       // Update the DOM if it has changed. Note that with sanitizeValue, it
-       // is possible for the DOM value to change without this.value changing.
-       if ( this.$input.val() !== this.value ) {
-               this.$input.val( this.value );
-       }
-       return this;
-};
-
-/**
- * Sanitize incoming value.
- *
- * Ensures value is a string, and converts undefined and null to empty strings.
- *
- * @param {string} value Original value
- * @return {string} Sanitized value
+ *
+ * @param {jQuery.Event} e Mouse click event
+ * @fires click
  */
-OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
-       if ( value === undefined || value === null ) {
-               return '';
-       } else if ( this.inputFilter ) {
-               return this.inputFilter( String( value ) );
-       } else {
-               return String( value );
+OO.ui.ButtonWidget.prototype.onClick = function () {
+       if ( !this.isDisabled() ) {
+               this.emit( 'click' );
+               if ( this.isHyperlink ) {
+                       return true;
+               }
        }
+       return false;
 };
 
 /**
- * Simulate the behavior of clicking on a label bound to this input.
+ * Handles keypress events.
+ *
+ * @param {jQuery.Event} e Keypress event
+ * @fires click
  */
-OO.ui.InputWidget.prototype.simulateLabelClick = function () {
-       if ( !this.isDisabled() ) {
-               if ( this.$input.is( ':checkbox,:radio' ) ) {
-                       this.$input.click();
-               } else if ( this.$input.is( ':input' ) ) {
-                       this.$input.focus();
+OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+               this.onClick();
+               if ( this.isHyperlink ) {
+                       return true;
                }
        }
+       return false;
 };
 
 /**
- * Check if the widget is read-only.
+ * Get hyperlink location.
  *
- * @return {boolean}
+ * @return {string} Hyperlink location
  */
-OO.ui.InputWidget.prototype.isReadOnly = function () {
-       return this.readOnly;
+OO.ui.ButtonWidget.prototype.getHref = function () {
+       return this.href;
 };
 
 /**
- * Set the read-only state of the widget.
- *
- * This should probably change the widgets's appearance and prevent it from being used.
+ * Get hyperlink target.
  *
- * @param {boolean} state Make input read-only
- * @chainable
+ * @return {string} Hyperlink target
  */
-OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
-       this.readOnly = !!state;
-       this.$input.prop( 'readOnly', this.readOnly );
-       return this;
+OO.ui.ButtonWidget.prototype.getTarget = function () {
+       return this.target;
 };
 
 /**
- * @inheritdoc
+ * Set hyperlink location.
+ *
+ * @param {string|null} href Hyperlink location, null to remove
  */
-OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
-       OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
-       if ( this.$input ) {
-               this.$input.prop( 'disabled', this.isDisabled() );
+OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
+       href = typeof href === 'string' ? href : null;
+
+       if ( href !== this.href ) {
+               this.href = href;
+               if ( href !== null ) {
+                       this.$button.attr( 'href', href );
+                       this.isHyperlink = true;
+               } else {
+                       this.$button.removeAttr( 'href' );
+                       this.isHyperlink = false;
+               }
        }
+
        return this;
 };
 
 /**
- * Focus the input.
+ * Set hyperlink target.
  *
- * @chainable
+ * @param {string|null} target Hyperlink target, null to remove
  */
-OO.ui.InputWidget.prototype.focus = function () {
-       this.$input.focus();
+OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
+       target = typeof target === 'string' ? target : null;
+
+       if ( target !== this.target ) {
+               this.target = target;
+               if ( target !== null ) {
+                       this.$button.attr( 'target', target );
+               } else {
+                       this.$button.removeAttr( 'target' );
+               }
+       }
+
        return this;
 };
 
 /**
- * Checkbox widget.
+ * Button widget that executes an action and is managed by an OO.ui.ActionSet.
  *
  * @class
- * @extends OO.ui.InputWidget
+ * @extends OO.ui.ButtonWidget
  *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {string} [action] Symbolic action name
+ * @cfg {string[]} [modes] Symbolic mode names
  */
-OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+OO.ui.ActionWidget = function OoUiActionWidget( config ) {
+       // Config intialization
+       config = $.extend( { 'framed': false }, config );
+
        // Parent constructor
-       OO.ui.CheckboxInputWidget.super.call( this, config );
+       OO.ui.ActionWidget.super.call( this, config );
+
+       // Properties
+       this.action = config.action || '';
+       this.modes = config.modes || [];
+       this.width = 0;
+       this.height = 0;
 
        // Initialization
-       this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+       this.$element.addClass( 'oo-ui-actionWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
+OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
 
 /* Events */
 
-/* Methods */
-
 /**
- * Get input element.
- *
- * @return {jQuery} Input element
+ * @event resize
  */
-OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
-       return this.$( '<input type="checkbox" />' );
-};
+
+/* Methods */
 
 /**
- * Get checked state of the checkbox
+ * Check if action is available in a certain mode.
  *
- * @return {boolean} If the checkbox is checked
+ * @param {string} mode Name of mode
+ * @return {boolean} Has mode
  */
-OO.ui.CheckboxInputWidget.prototype.getValue = function () {
-       return this.value;
+OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
+       return this.modes.indexOf( mode ) !== -1;
 };
 
 /**
- * Set value
+ * Get symbolic action name.
+ *
+ * @return {string}
  */
-OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
-       value = !!value;
-       if ( this.value !== value ) {
-               this.value = value;
-               this.$input.prop( 'checked', this.value );
-               this.emit( 'change', this.value );
-       }
+OO.ui.ActionWidget.prototype.getAction = function () {
+       return this.action;
 };
 
 /**
- * @inheritdoc
+ * Get symbolic action name.
+ *
+ * @return {string}
  */
-OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
-       if ( !this.isDisabled() ) {
-               // Allow the stack to clear so the value will be updated
-               setTimeout( OO.ui.bind( function () {
-                       this.setValue( this.$input.prop( 'checked' ) );
-               }, this ) );
-       }
+OO.ui.ActionWidget.prototype.getModes = function () {
+       return this.modes.slice();
 };
 
 /**
- * Label widget.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
+ * Emit a resize event if the size has changed.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @chainable
  */
-OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
-       // Config intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.LabelWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.LabeledElement.call( this, this.$element, config );
+OO.ui.ActionWidget.prototype.propagateResize = function () {
+       var width, height;
 
-       // Properties
-       this.input = config.input;
+       if ( this.isElementAttached() ) {
+               width = this.$element.width();
+               height = this.$element.height();
 
-       // Events
-       if ( this.input instanceof OO.ui.InputWidget ) {
-               this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
+               if ( width !== this.width || height !== this.height ) {
+                       this.width = width;
+                       this.height = height;
+                       this.emit( 'resize' );
+               }
        }
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-labelWidget' );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabeledElement );
-
-/* Static Properties */
-
-OO.ui.LabelWidget.static.tagName = 'label';
-
-/* Methods */
-
 /**
- * Handles label mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
+ * @inheritdoc
  */
-OO.ui.LabelWidget.prototype.onClick = function () {
-       this.input.simulateLabelClick();
-       return false;
+OO.ui.ActionWidget.prototype.setIcon = function () {
+       // Mixin method
+       OO.ui.IconedElement.prototype.setIcon.apply( this, arguments );
+       this.propagateResize();
+
+       return this;
 };
 
 /**
- * Lookup input widget.
- *
- * Mixin that adds a menu showing suggested values to a text input. Subclasses must handle `select`
- * and `choose` events on #lookupMenu to make use of selections.
- *
- * @class
- * @abstract
- *
- * @constructor
- * @param {OO.ui.TextInputWidget} input Input widget
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
+ * @inheritdoc
  */
-OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
-       // Config intialization
-       config = config || {};
-
-       // Properties
-       this.lookupInput = input;
-       this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last();
-       this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
-               '$': OO.ui.Element.getJQuery( this.$overlay ),
-               'input': this.lookupInput,
-               '$container': config.$container
-       } );
-       this.lookupCache = {};
-       this.lookupQuery = null;
-       this.lookupRequest = null;
-       this.populating = false;
-
-       // Events
-       this.$overlay.append( this.lookupMenu.$element );
-
-       this.lookupInput.$input.on( {
-               'focus': OO.ui.bind( this.onLookupInputFocus, this ),
-               'blur': OO.ui.bind( this.onLookupInputBlur, this ),
-               'mousedown': OO.ui.bind( this.onLookupInputMouseDown, this )
-       } );
-       this.lookupInput.connect( this, { 'change': 'onLookupInputChange' } );
+OO.ui.ActionWidget.prototype.setLabel = function () {
+       // Mixin method
+       OO.ui.LabeledElement.prototype.setLabel.apply( this, arguments );
+       this.propagateResize();
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-lookupWidget' );
-       this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
+       return this;
 };
 
-/* Methods */
-
 /**
- * Handle input focus event.
- *
- * @param {jQuery.Event} e Input focus event
+ * @inheritdoc
  */
-OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
-       this.openLookupMenu();
-};
+OO.ui.ActionWidget.prototype.setFlags = function () {
+       // Mixin method
+       OO.ui.FlaggableElement.prototype.setFlags.apply( this, arguments );
+       this.propagateResize();
 
-/**
- * Handle input blur event.
- *
- * @param {jQuery.Event} e Input blur event
- */
-OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
-       this.lookupMenu.hide();
+       return this;
 };
 
 /**
- * Handle input mouse down event.
- *
- * @param {jQuery.Event} e Input mouse down event
+ * @inheritdoc
  */
-OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
-       this.openLookupMenu();
+OO.ui.ActionWidget.prototype.clearFlags = function () {
+       // Mixin method
+       OO.ui.FlaggableElement.prototype.clearFlags.apply( this, arguments );
+       this.propagateResize();
+
+       return this;
 };
 
 /**
- * Handle input change event.
+ * Toggle visibility of button.
  *
- * @param {string} value New input value
+ * @param {boolean} [show] Show button, omit to toggle visibility
+ * @chainable
  */
-OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
-       this.openLookupMenu();
+OO.ui.ActionWidget.prototype.toggle = function () {
+       // Parent method
+       OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
+       this.propagateResize();
+
+       return this;
 };
 
 /**
- * Get lookup menu.
+ * Button that shows and hides a popup.
  *
- * @return {OO.ui.TextInputMenuWidget}
+ * @class
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.PopuppableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
-       return this.lookupMenu;
+OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
+       // Parent constructor
+       OO.ui.PopupButtonWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.PopuppableElement.call( this, config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-popupButtonWidget' )
+               .append( this.popup.$element );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
+OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
+
+/* Methods */
+
 /**
- * Open the menu.
+ * Handles mouse click events.
  *
- * @chainable
+ * @param {jQuery.Event} e Mouse click event
  */
-OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
-       var value = this.lookupInput.getValue();
-
-       if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
-               this.populateLookupMenu();
-               if ( !this.lookupMenu.isVisible() ) {
-                       this.lookupMenu.show();
-               }
-       } else {
-               this.lookupMenu.clearItems();
-               this.lookupMenu.hide();
+OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
+       // Skip clicks within the popup
+       if ( $.contains( this.popup.$element[0], e.target ) ) {
+               return;
        }
 
-       return this;
+       if ( !this.isDisabled() ) {
+               this.popup.toggle();
+               // Parent method
+               OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
+       }
+       return false;
 };
 
 /**
- * Populate lookup menu with current information.
+ * Button that toggles on and off.
  *
- * @chainable
+ * @class
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.ToggleWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] Initial value
  */
-OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
-       if ( !this.populating ) {
-               this.populating = true;
-               this.getLookupMenuItems()
-                       .done( OO.ui.bind( function ( items ) {
-                               this.lookupMenu.clearItems();
-                               if ( items.length ) {
-                                       this.lookupMenu.show();
-                                       this.lookupMenu.addItems( items );
-                                       this.initializeLookupMenuSelection();
-                                       this.openLookupMenu();
-                               } else {
-                                       this.lookupMenu.hide();
-                               }
-                               this.populating = false;
-                       }, this ) )
-                       .fail( OO.ui.bind( function () {
-                               this.lookupMenu.clearItems();
-                               this.populating = false;
-                       }, this ) );
-       }
+OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
+       // Configuration initialization
+       config = config || {};
 
-       return this;
+       // Parent constructor
+       OO.ui.ToggleButtonWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.ToggleWidget.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-toggleButtonWidget' );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
+
+/* Methods */
+
 /**
- * Set selection in the lookup menu with current information.
- *
- * @chainable
+ * @inheritdoc
  */
-OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
-       if ( !this.lookupMenu.getSelectedItem() ) {
-               this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
+OO.ui.ToggleButtonWidget.prototype.onClick = function () {
+       if ( !this.isDisabled() ) {
+               this.setValue( !this.value );
        }
-       this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
+
+       // Parent method
+       return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
 };
 
 /**
- * Get lookup menu items for the current query.
- *
- * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
- * of the done event
+ * @inheritdoc
  */
-OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
-       var value = this.lookupInput.getValue(),
-               deferred = $.Deferred();
-
-       if ( value && value !== this.lookupQuery ) {
-               // Abort current request if query has changed
-               if ( this.lookupRequest ) {
-                       this.lookupRequest.abort();
-                       this.lookupQuery = null;
-                       this.lookupRequest = null;
-               }
-               if ( value in this.lookupCache ) {
-                       deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
-               } else {
-                       this.lookupQuery = value;
-                       this.lookupRequest = this.getLookupRequest()
-                               .always( OO.ui.bind( function () {
-                                       this.lookupQuery = null;
-                                       this.lookupRequest = null;
-                               }, this ) )
-                               .done( OO.ui.bind( function ( data ) {
-                                       this.lookupCache[value] = this.getLookupCacheItemFromData( data );
-                                       deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
-                               }, this ) )
-                               .fail( function () {
-                                       deferred.reject();
-                               } );
-                       this.pushPending();
-                       this.lookupRequest.always( OO.ui.bind( function () {
-                               this.popPending();
-                       }, this ) );
-               }
+OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
+       value = !!value;
+       if ( value !== this.value ) {
+               this.setActive( value );
        }
-       return deferred.promise();
-};
 
-/**
- * Get a new request object of the current lookup query value.
- *
- * @abstract
- * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
- */
-OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
-       // Stub, implemented in subclass
-       return null;
+       // Parent method (from mixin)
+       OO.ui.ToggleWidget.prototype.setValue.call( this, value );
+
+       return this;
 };
 
 /**
- * Handle successful lookup request.
+ * Icon widget.
  *
- * Overriding methods should call #populateLookupMenu when results are available and cache results
- * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.TitledElement
  *
- * @abstract
- * @param {Mixed} data Response from server
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
-       // Stub, implemented in subclass
+OO.ui.IconWidget = function OoUiIconWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.IconWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$element, config );
+       OO.ui.TitledElement.call( this, this.$element, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-iconWidget' );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IconWidget.static.tagName = 'span';
+
 /**
- * Get a list of menu item widgets from the data stored by the lookup request's done handler.
+ * Indicator widget.
  *
- * @abstract
- * @param {Mixed} data Cached result data, usually an array
- * @return {OO.ui.MenuItemWidget[]} Menu items
+ * See OO.ui.IndicatedElement for more information.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IndicatedElement
+ * @mixins OO.ui.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
-       // Stub, implemented in subclass
-       return [];
+OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.IndicatorWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IndicatedElement.call( this, this.$element, config );
+       OO.ui.TitledElement.call( this, this.$element, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-indicatorWidget' );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IndicatorWidget.static.tagName = 'span';
+
 /**
- * Option widget.
+ * Inline menu of options.
  *
- * Use with OO.ui.SelectWidget.
+ * Inline menus provide a control for accessing a menu and compose a menu within the widget, which
+ * can be accessed using the #getMenu method.
+ *
+ * Use with OO.ui.MenuOptionWidget.
  *
  * @class
  * @extends OO.ui.Widget
  * @mixins OO.ui.IconedElement
- * @mixins OO.ui.LabeledElement
  * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.FlaggableElement
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.TitledElement
  *
  * @constructor
- * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
- * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
+ * @cfg {Object} [menu] Configuration options to pass to menu widget
  */
-OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
-       // Config intialization
-       config = config || {};
+OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
+       // Configuration initialization
+       config = $.extend( { 'indicator': 'down' }, config );
 
        // Parent constructor
-       OO.ui.OptionWidget.super.call( this, config );
+       OO.ui.InlineMenuWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.ItemWidget.call( this );
        OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
-       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
        OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
-       OO.ui.FlaggableElement.call( this, config );
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
+       OO.ui.TitledElement.call( this, this.$label, config );
 
        // Properties
-       this.data = data;
-       this.selected = false;
-       this.highlighted = false;
-       this.pressed = false;
+       this.menu = new OO.ui.MenuWidget( $.extend( { '$': this.$, 'widget': this }, config.menu ) );
+       this.$handle = this.$( '<span>' );
+
+       // Events
+       this.$element.on( { 'click': OO.ui.bind( this.onClick, this ) } );
+       this.menu.connect( this, { 'select': 'onMenuSelect' } );
 
        // Initialization
+       this.$handle
+               .addClass( 'oo-ui-inlineMenuWidget-handle' )
+               .append( this.$icon, this.$label, this.$indicator );
        this.$element
-               .data( 'oo-ui-optionWidget', this )
-               .attr( 'rel', config.rel )
-               .addClass( 'oo-ui-optionWidget' )
-               .append( this.$label );
-       this.$element
-               .prepend( this.$icon )
-               .append( this.$indicator );
+               .addClass( 'oo-ui-inlineMenuWidget' )
+               .append( this.$handle, this.menu.$element );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggableElement );
-
-/* Static Properties */
-
-OO.ui.OptionWidget.static.tagName = 'li';
-
-OO.ui.OptionWidget.static.selectable = true;
-
-OO.ui.OptionWidget.static.highlightable = true;
-
-OO.ui.OptionWidget.static.pressable = true;
-
-OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
+OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
 
 /* Methods */
 
 /**
- * Check if option can be selected.
+ * Get the menu.
  *
- * @return {boolean} Item is selectable
+ * @return {OO.ui.MenuWidget} Menu of widget
  */
-OO.ui.OptionWidget.prototype.isSelectable = function () {
-       return this.constructor.static.selectable && !this.isDisabled();
+OO.ui.InlineMenuWidget.prototype.getMenu = function () {
+       return this.menu;
 };
 
 /**
- * Check if option can be highlighted.
+ * Handles menu select events.
  *
- * @return {boolean} Item is highlightable
+ * @param {OO.ui.MenuItemWidget} item Selected menu item
  */
-OO.ui.OptionWidget.prototype.isHighlightable = function () {
-       return this.constructor.static.highlightable && !this.isDisabled();
-};
+OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
+       var selectedLabel;
 
-/**
- * Check if option can be pressed.
- *
- * @return {boolean} Item is pressable
- */
-OO.ui.OptionWidget.prototype.isPressable = function () {
-       return this.constructor.static.pressable && !this.isDisabled();
+       if ( !item ) {
+               return;
+       }
+
+       selectedLabel = item.getLabel();
+
+       // If the label is a DOM element, clone it, because setLabel will append() it
+       if ( selectedLabel instanceof jQuery ) {
+               selectedLabel = selectedLabel.clone();
+       }
+
+       this.setLabel( selectedLabel );
 };
 
 /**
- * Check if option is selected.
+ * Handles mouse click events.
  *
- * @return {boolean} Item is selected
+ * @param {jQuery.Event} e Mouse click event
  */
-OO.ui.OptionWidget.prototype.isSelected = function () {
-       return this.selected;
+OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
+       // Skip clicks within the menu
+       if ( $.contains( this.menu.$element[0], e.target ) ) {
+               return;
+       }
+
+       if ( !this.isDisabled() ) {
+               if ( this.menu.isVisible() ) {
+                       this.menu.toggle( false );
+               } else {
+                       this.menu.toggle( true );
+               }
+       }
+       return false;
 };
 
 /**
- * Check if option is highlighted.
+ * Base class for input widgets.
  *
- * @return {boolean} Item is highlighted
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [name=''] HTML input name
+ * @cfg {string} [value=''] Input value
+ * @cfg {boolean} [readOnly=false] Prevent changes
+ * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
  */
-OO.ui.OptionWidget.prototype.isHighlighted = function () {
-       return this.highlighted;
+OO.ui.InputWidget = function OoUiInputWidget( config ) {
+       // Config intialization
+       config = $.extend( { 'readOnly': false }, config );
+
+       // Parent constructor
+       OO.ui.InputWidget.super.call( this, config );
+
+       // Properties
+       this.$input = this.getInputElement( config );
+       this.value = '';
+       this.readOnly = false;
+       this.inputFilter = config.inputFilter;
+
+       // Events
+       this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) );
+
+       // Initialization
+       this.$input
+               .attr( 'name', config.name )
+               .prop( 'disabled', this.isDisabled() );
+       this.setReadOnly( config.readOnly );
+       this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
+       this.setValue( config.value );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
+
+/* Events */
+
 /**
- * Check if option is pressed.
- *
- * @return {boolean} Item is pressed
+ * @event change
+ * @param value
  */
-OO.ui.OptionWidget.prototype.isPressed = function () {
-       return this.pressed;
-};
+
+/* Methods */
 
 /**
- * Set selected state.
+ * Get input element.
  *
- * @param {boolean} [state=false] Select option
- * @chainable
+ * @param {Object} [config] Configuration options
+ * @return {jQuery} Input element
  */
-OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
-       if ( this.constructor.static.selectable ) {
-               this.selected = !!state;
-               if ( this.selected ) {
-                       this.$element.addClass( 'oo-ui-optionWidget-selected' );
-                       if ( this.constructor.static.scrollIntoViewOnSelect ) {
-                               this.scrollElementIntoView();
-                       }
-               } else {
-                       this.$element.removeClass( 'oo-ui-optionWidget-selected' );
-               }
-       }
-       return this;
+OO.ui.InputWidget.prototype.getInputElement = function () {
+       return this.$( '<input>' );
 };
 
 /**
- * Set highlighted state.
+ * Handle potentially value-changing events.
  *
- * @param {boolean} [state=false] Highlight option
- * @chainable
+ * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
  */
-OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
-       if ( this.constructor.static.highlightable ) {
-               this.highlighted = !!state;
-               if ( this.highlighted ) {
-                       this.$element.addClass( 'oo-ui-optionWidget-highlighted' );
-               } else {
-                       this.$element.removeClass( 'oo-ui-optionWidget-highlighted' );
-               }
+OO.ui.InputWidget.prototype.onEdit = function () {
+       var widget = this;
+       if ( !this.isDisabled() ) {
+               // Allow the stack to clear so the value will be updated
+               setTimeout( function () {
+                       widget.setValue( widget.$input.val() );
+               } );
        }
-       return this;
 };
 
 /**
- * Set pressed state.
+ * Get the value of the input.
  *
- * @param {boolean} [state=false] Press option
- * @chainable
+ * @return {string} Input value
  */
-OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
-       if ( this.constructor.static.pressable ) {
-               this.pressed = !!state;
-               if ( this.pressed ) {
-                       this.$element.addClass( 'oo-ui-optionWidget-pressed' );
-               } else {
-                       this.$element.removeClass( 'oo-ui-optionWidget-pressed' );
-               }
-       }
-       return this;
+OO.ui.InputWidget.prototype.getValue = function () {
+       return this.value;
 };
 
 /**
- * Make the option's highlight flash.
- *
- * While flashing, the visual style of the pressed state is removed if present.
+ * Sets the direction of the current input, either RTL or LTR
  *
- * @return {jQuery.Promise} Promise resolved when flashing is done
+ * @param {boolean} isRTL
  */
-OO.ui.OptionWidget.prototype.flash = function () {
-       var $this = this.$element,
-               deferred = $.Deferred();
-
-       if ( !this.isDisabled() && this.constructor.static.pressable ) {
-               $this.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
-               setTimeout( OO.ui.bind( function () {
-                       // Restore original classes
-                       $this
-                               .toggleClass( 'oo-ui-optionWidget-highlighted', this.highlighted )
-                               .toggleClass( 'oo-ui-optionWidget-pressed', this.pressed );
-                       setTimeout( function () {
-                               deferred.resolve();
-                       }, 100 );
-               }, this ), 100 );
+OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
+       if ( isRTL ) {
+               this.$input.removeClass( 'oo-ui-ltr' );
+               this.$input.addClass( 'oo-ui-rtl' );
+       } else {
+               this.$input.removeClass( 'oo-ui-rtl' );
+               this.$input.addClass( 'oo-ui-ltr' );
        }
-
-       return deferred.promise();
 };
 
 /**
- * Get option data.
+ * Set the value of the input.
  *
- * @return {Mixed} Option data
+ * @param {string} value New value
+ * @fires change
+ * @chainable
  */
-OO.ui.OptionWidget.prototype.getData = function () {
-       return this.data;
+OO.ui.InputWidget.prototype.setValue = function ( value ) {
+       value = this.sanitizeValue( value );
+       if ( this.value !== value ) {
+               this.value = value;
+               this.emit( 'change', this.value );
+       }
+       // Update the DOM if it has changed. Note that with sanitizeValue, it
+       // is possible for the DOM value to change without this.value changing.
+       if ( this.$input.val() !== this.value ) {
+               this.$input.val( this.value );
+       }
+       return this;
 };
 
 /**
- * Selection of options.
- *
- * Use together with OO.ui.OptionWidget.
+ * Sanitize incoming value.
  *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.GroupElement
+ * Ensures value is a string, and converts undefined and null to empty strings.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.OptionWidget[]} [items] Options to add
+ * @param {string} value Original value
+ * @return {string} Sanitized value
  */
-OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
-       // Config intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.SelectWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.GroupWidget.call( this, this.$element, config );
-
-       // Properties
-       this.pressed = false;
-       this.selecting = null;
-       this.hashes = {};
-       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
-       this.onMouseMoveHandler = OO.ui.bind( this.onMouseMove, this );
-
-       // Events
-       this.$element.on( {
-               'mousedown': OO.ui.bind( this.onMouseDown, this ),
-               'mouseover': OO.ui.bind( this.onMouseOver, this ),
-               'mouseleave': OO.ui.bind( this.onMouseLeave, this )
-       } );
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
-       if ( $.isArray( config.items ) ) {
-               this.addItems( config.items );
-       }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
-
-// Need to mixin base class as well
-OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
-OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
-
-/* Events */
+OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
+       if ( value === undefined || value === null ) {
+               return '';
+       } else if ( this.inputFilter ) {
+               return this.inputFilter( String( value ) );
+       } else {
+               return String( value );
+       }
+};
 
 /**
- * @event highlight
- * @param {OO.ui.OptionWidget|null} item Highlighted item
+ * Simulate the behavior of clicking on a label bound to this input.
  */
+OO.ui.InputWidget.prototype.simulateLabelClick = function () {
+       if ( !this.isDisabled() ) {
+               if ( this.$input.is( ':checkbox,:radio' ) ) {
+                       this.$input.click();
+               } else if ( this.$input.is( ':input' ) ) {
+                       this.$input.focus();
+               }
+       }
+};
 
 /**
- * @event press
- * @param {OO.ui.OptionWidget|null} item Pressed item
+ * Check if the widget is read-only.
+ *
+ * @return {boolean}
  */
+OO.ui.InputWidget.prototype.isReadOnly = function () {
+       return this.readOnly;
+};
 
 /**
- * @event select
- * @param {OO.ui.OptionWidget|null} item Selected item
+ * Set the read-only state of the widget.
+ *
+ * This should probably change the widgets's appearance and prevent it from being used.
+ *
+ * @param {boolean} state Make input read-only
+ * @chainable
  */
+OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
+       this.readOnly = !!state;
+       this.$input.prop( 'readOnly', this.readOnly );
+       return this;
+};
 
 /**
- * @event choose
- * @param {OO.ui.OptionWidget|null} item Chosen item
+ * @inheritdoc
  */
+OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
+       OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
+       if ( this.$input ) {
+               this.$input.prop( 'disabled', this.isDisabled() );
+       }
+       return this;
+};
 
 /**
- * @event add
- * @param {OO.ui.OptionWidget[]} items Added items
- * @param {number} index Index items were added at
+ * Focus the input.
+ *
+ * @chainable
  */
+OO.ui.InputWidget.prototype.focus = function () {
+       this.$input.focus();
+       return this;
+};
 
 /**
- * @event remove
- * @param {OO.ui.OptionWidget[]} items Removed items
+ * Blur the input.
+ *
+ * @chainable
  */
-
-/* Static Properties */
-
-OO.ui.SelectWidget.static.tagName = 'ul';
-
-/* Methods */
+OO.ui.InputWidget.prototype.blur = function () {
+       this.$input.blur();
+       return this;
+};
 
 /**
- * Handle mouse down events.
+ * Checkbox input widget.
  *
- * @private
- * @param {jQuery.Event} e Mouse down event
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
-       var item;
+OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+       // Parent constructor
+       OO.ui.CheckboxInputWidget.super.call( this, config );
 
-       if ( !this.isDisabled() && e.which === 1 ) {
-               this.togglePressed( true );
-               item = this.getTargetItem( e );
-               if ( item && item.isSelectable() ) {
-                       this.pressItem( item );
-                       this.selecting = item;
-                       this.getElementDocument().addEventListener(
-                               'mouseup', this.onMouseUpHandler, true
-                       );
-                       this.getElementDocument().addEventListener(
-                               'mousemove', this.onMouseMoveHandler, true
-                       );
-               }
-       }
-       return false;
+       // Initialization
+       this.$element.addClass( 'oo-ui-checkboxInputWidget' );
 };
 
-/**
- * Handle mouse up events.
- *
- * @private
- * @param {jQuery.Event} e Mouse up event
- */
-OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
-       var item;
+/* Setup */
 
-       this.togglePressed( false );
-       if ( !this.selecting ) {
-               item = this.getTargetItem( e );
-               if ( item && item.isSelectable() ) {
-                       this.selecting = item;
-               }
-       }
-       if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
-               this.pressItem( null );
-               this.chooseItem( this.selecting );
-               this.selecting = null;
-       }
+OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
 
-       this.getElementDocument().removeEventListener(
-               'mouseup', this.onMouseUpHandler, true
-       );
-       this.getElementDocument().removeEventListener(
-               'mousemove', this.onMouseMoveHandler, true
-       );
+/* Events */
 
-       return false;
-};
+/* Methods */
 
 /**
- * Handle mouse move events.
+ * Get input element.
  *
- * @private
- * @param {jQuery.Event} e Mouse move event
+ * @return {jQuery} Input element
  */
-OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
-       var item;
-
-       if ( !this.isDisabled() && this.pressed ) {
-               item = this.getTargetItem( e );
-               if ( item && item !== this.selecting && item.isSelectable() ) {
-                       this.pressItem( item );
-                       this.selecting = item;
-               }
-       }
-       return false;
+OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
+       return this.$( '<input type="checkbox" />' );
 };
 
 /**
- * Handle mouse over events.
+ * Get checked state of the checkbox
  *
- * @private
- * @param {jQuery.Event} e Mouse over event
+ * @return {boolean} If the checkbox is checked
  */
-OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
-       var item;
-
-       if ( !this.isDisabled() ) {
-               item = this.getTargetItem( e );
-               this.highlightItem( item && item.isHighlightable() ? item : null );
-       }
-       return false;
+OO.ui.CheckboxInputWidget.prototype.getValue = function () {
+       return this.value;
 };
 
 /**
- * Handle mouse leave events.
- *
- * @private
- * @param {jQuery.Event} e Mouse over event
+ * Set value
  */
-OO.ui.SelectWidget.prototype.onMouseLeave = function () {
-       if ( !this.isDisabled() ) {
-               this.highlightItem( null );
+OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
+       value = !!value;
+       if ( this.value !== value ) {
+               this.value = value;
+               this.$input.prop( 'checked', this.value );
+               this.emit( 'change', this.value );
        }
-       return false;
 };
 
 /**
- * Get the closest item to a jQuery.Event.
- *
- * @private
- * @param {jQuery.Event} e
- * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
+ * @inheritdoc
  */
-OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
-       var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
-       if ( $item.length ) {
-               return $item.data( 'oo-ui-optionWidget' );
+OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
+       var widget = this;
+       if ( !this.isDisabled() ) {
+               // Allow the stack to clear so the value will be updated
+               setTimeout( function () {
+                       widget.setValue( widget.$input.prop( 'checked' ) );
+               } );
        }
-       return null;
 };
 
 /**
- * Get selected item.
+ * Input widget with a text field.
  *
- * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [placeholder] Placeholder text
+ * @cfg {string} [icon] Symbolic name of icon
+ * @cfg {boolean} [multiline=false] Allow multiple lines of text
+ * @cfg {boolean} [autosize=false] Automatically resize to fit content
+ * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
  */
-OO.ui.SelectWidget.prototype.getSelectedItem = function () {
-       var i, len;
+OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
+       var widget = this;
+       config = $.extend( { 'maxRows': 10 }, config );
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               if ( this.items[i].isSelected() ) {
-                       return this.items[i];
-               }
+       // Parent constructor
+       OO.ui.TextInputWidget.super.call( this, config );
+
+       // Properties
+       this.pending = 0;
+       this.multiline = !!config.multiline;
+       this.autosize = !!config.autosize;
+       this.maxRows = config.maxRows;
+
+       // Events
+       this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
+       this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-textInputWidget' );
+       if ( config.icon ) {
+               this.$element.addClass( 'oo-ui-textInputWidget-decorated' );
+               this.$element.append(
+                       this.$( '<span>' )
+                               .addClass( 'oo-ui-textInputWidget-icon oo-ui-icon-' + config.icon )
+                               .mousedown( function () {
+                                       widget.$input.focus();
+                                       return false;
+                               } )
+               );
+       }
+       if ( config.placeholder ) {
+               this.$input.attr( 'placeholder', config.placeholder );
        }
-       return null;
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
+
+/* Events */
+
 /**
- * Get highlighted item.
+ * User presses enter inside the text box.
  *
- * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
+ * Not called if input is multiline.
+ *
+ * @event enter
  */
-OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
-       var i, len;
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               if ( this.items[i].isHighlighted() ) {
-                       return this.items[i];
-               }
-       }
-       return null;
-};
+/* Methods */
 
 /**
- * Get an existing item with equivilant data.
+ * Handle key press events.
  *
- * @param {Object} data Item data to search for
- * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
+ * @param {jQuery.Event} e Key press event
+ * @fires enter If enter key is pressed and input is not multiline
  */
-OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
-       var hash = OO.getHash( data );
-
-       if ( hash in this.hashes ) {
-               return this.hashes[hash];
+OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
+       if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
+               this.emit( 'enter' );
        }
-
-       return null;
 };
 
 /**
- * Toggle pressed state.
+ * Handle element attach events.
  *
- * @param {boolean} pressed An option is being pressed
+ * @param {jQuery.Event} e Element attach event
  */
-OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
-       if ( pressed === undefined ) {
-               pressed = !this.pressed;
-       }
-       if ( pressed !== this.pressed ) {
-               this.$element.toggleClass( 'oo-ui-selectWidget-pressed', pressed );
-               this.$element.toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
-               this.pressed = pressed;
-       }
+OO.ui.TextInputWidget.prototype.onElementAttach = function () {
+       this.adjustSize();
 };
 
 /**
- * Highlight an item.
- *
- * Highlighting is mutually exclusive.
- *
- * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
- * @fires highlight
- * @chainable
+ * @inheritdoc
  */
-OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
-       var i, len, highlighted,
-               changed = false;
-
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               highlighted = this.items[i] === item;
-               if ( this.items[i].isHighlighted() !== highlighted ) {
-                       this.items[i].setHighlighted( highlighted );
-                       changed = true;
-               }
-       }
-       if ( changed ) {
-               this.emit( 'highlight', item );
-       }
+OO.ui.TextInputWidget.prototype.onEdit = function () {
+       this.adjustSize();
 
-       return this;
+       // Parent method
+       return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
 };
 
 /**
- * Select an item.
+ * Automatically adjust the size of the text input.
+ *
+ * This only affects multi-line inputs that are auto-sized.
  *
- * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
- * @fires select
  * @chainable
  */
-OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
-       var i, len, selected,
-               changed = false;
+OO.ui.TextInputWidget.prototype.adjustSize = function () {
+       var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               selected = this.items[i] === item;
-               if ( this.items[i].isSelected() !== selected ) {
-                       this.items[i].setSelected( selected );
-                       changed = true;
-               }
-       }
-       if ( changed ) {
-               this.emit( 'select', item );
+       if ( this.multiline && this.autosize ) {
+               $clone = this.$input.clone()
+                       .val( this.$input.val() )
+                       .css( { 'height': 0 } )
+                       .insertAfter( this.$input );
+               // Set inline height property to 0 to measure scroll height
+               scrollHeight = $clone[0].scrollHeight;
+               // Remove inline height property to measure natural heights
+               $clone.css( 'height', '' );
+               innerHeight = $clone.innerHeight();
+               outerHeight = $clone.outerHeight();
+               // Measure max rows height
+               $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' );
+               maxInnerHeight = $clone.innerHeight();
+               $clone.removeAttr( 'rows' ).css( 'height', '' );
+               $clone.remove();
+               idealHeight = Math.min( maxInnerHeight, scrollHeight );
+               // Only apply inline height when expansion beyond natural height is needed
+               this.$input.css(
+                       'height',
+                       // Use the difference between the inner and outer height as a buffer
+                       idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : ''
+               );
        }
-
        return this;
 };
 
 /**
- * Press an item.
+ * Get input element.
  *
- * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
- * @fires press
- * @chainable
+ * @param {Object} [config] Configuration options
+ * @return {jQuery} Input element
  */
-OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
-       var i, len, pressed,
-               changed = false;
-
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               pressed = this.items[i] === item;
-               if ( this.items[i].isPressed() !== pressed ) {
-                       this.items[i].setPressed( pressed );
-                       changed = true;
-               }
-       }
-       if ( changed ) {
-               this.emit( 'press', item );
-       }
-
-       return this;
+OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
+       return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
 };
 
+/* Methods */
+
 /**
- * Choose an item.
- *
- * Identical to #selectItem, but may vary in subclasses that want to take additional action when
- * an item is selected using the keyboard or mouse.
+ * Check if input supports multiple lines.
  *
- * @param {OO.ui.OptionWidget} item Item to choose
- * @fires choose
- * @chainable
+ * @return {boolean}
  */
-OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
-       this.selectItem( item );
-       this.emit( 'choose', item );
-
-       return this;
+OO.ui.TextInputWidget.prototype.isMultiline = function () {
+       return !!this.multiline;
 };
 
 /**
- * Get an item relative to another one.
+ * Check if input automatically adjusts its size.
  *
- * @param {OO.ui.OptionWidget} item Item to start at
- * @param {number} direction Direction to move in
- * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
+ * @return {boolean}
  */
-OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
-       var inc = direction > 0 ? 1 : -1,
-               len = this.items.length,
-               index = item instanceof OO.ui.OptionWidget ?
-                       $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ),
-               stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
-               i = inc > 0 ?
-                       // Default to 0 instead of -1, if nothing is selected let's start at the beginning
-                       Math.max( index, -1 ) :
-                       // Default to n-1 instead of -1, if nothing is selected let's start at the end
-                       Math.min( index, len );
-
-       while ( true ) {
-               i = ( i + inc + len ) % len;
-               item = this.items[i];
-               if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
-                       return item;
-               }
-               // Stop iterating when we've looped all the way around
-               if ( i === stopAt ) {
-                       break;
-               }
-       }
-       return null;
+OO.ui.TextInputWidget.prototype.isAutosizing = function () {
+       return !!this.autosize;
 };
 
 /**
- * Get the next selectable item.
+ * Check if input is pending.
  *
- * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
+ * @return {boolean}
  */
-OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
-       var i, len, item;
-
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               item = this.items[i];
-               if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
-                       return item;
-               }
-       }
-
-       return null;
+OO.ui.TextInputWidget.prototype.isPending = function () {
+       return !!this.pending;
 };
 
 /**
- * Add items.
- *
- * When items are added with the same values as existing items, the existing items will be
- * automatically removed before the new items are added.
+ * Increase the pending stack.
  *
- * @param {OO.ui.OptionWidget[]} items Items to add
- * @param {number} [index] Index to insert items after
- * @fires add
  * @chainable
  */
-OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
-       var i, len, item, hash,
-               remove = [];
-
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
-               hash = OO.getHash( item.getData() );
-               if ( hash in this.hashes ) {
-                       // Remove item with same value
-                       remove.push( this.hashes[hash] );
-               }
-               this.hashes[hash] = item;
-       }
-       if ( remove.length ) {
-               this.removeItems( remove );
+OO.ui.TextInputWidget.prototype.pushPending = function () {
+       if ( this.pending === 0 ) {
+               this.$element.addClass( 'oo-ui-textInputWidget-pending' );
+               this.$input.addClass( 'oo-ui-texture-pending' );
        }
-
-       // Mixin method
-       OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
-
-       // Always provide an index, even if it was omitted
-       this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
+       this.pending++;
 
        return this;
 };
 
 /**
- * Remove items.
+ * Reduce the pending stack.
  *
- * Items will be detached, not removed, so they can be used later.
+ * Clamped at zero.
  *
- * @param {OO.ui.OptionWidget[]} items Items to remove
- * @fires remove
  * @chainable
  */
-OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
-       var i, len, item, hash;
-
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
-               hash = OO.getHash( item.getData() );
-               if ( hash in this.hashes ) {
-                       // Remove existing item
-                       delete this.hashes[hash];
-               }
-               if ( item.isSelected() ) {
-                       this.selectItem( null );
-               }
-       }
-
-       // Mixin method
-       OO.ui.GroupWidget.prototype.removeItems.call( this, items );
-
-       this.emit( 'remove', items );
+OO.ui.TextInputWidget.prototype.popPending = function () {
+       if ( this.pending === 1 ) {
+               this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
+               this.$input.removeClass( 'oo-ui-texture-pending' );
+       }
+       this.pending = Math.max( 0, this.pending - 1 );
 
        return this;
 };
 
 /**
- * Clear all items.
- *
- * Items will be detached, not removed, so they can be used later.
+ * Select the contents of the input.
  *
- * @fires remove
  * @chainable
  */
-OO.ui.SelectWidget.prototype.clearItems = function () {
-       var items = this.items.slice();
-
-       // Clear all items
-       this.hashes = {};
-       // Mixin method
-       OO.ui.GroupWidget.prototype.clearItems.call( this );
-       this.selectItem( null );
-
-       this.emit( 'remove', items );
-
+OO.ui.TextInputWidget.prototype.select = function () {
+       this.$input.select();
        return this;
 };
 
 /**
- * Menu item widget.
- *
- * Use with OO.ui.MenuWidget.
+ * Label widget.
  *
  * @class
- * @extends OO.ui.OptionWidget
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
  *
  * @constructor
- * @param {Mixed} data Item data
  * @param {Object} [config] Configuration options
  */
-OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
-       // Configuration initialization
-       config = $.extend( { 'icon': 'check' }, config );
+OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
+       // Config intialization
+       config = config || {};
 
        // Parent constructor
-       OO.ui.MenuItemWidget.super.call( this, data, config );
+       OO.ui.LabelWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$element, config );
+
+       // Properties
+       this.input = config.input;
+
+       // Events
+       if ( this.input instanceof OO.ui.InputWidget ) {
+               this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
+       }
 
        // Initialization
-       this.$element.addClass( 'oo-ui-menuItemWidget' );
+       this.$element.addClass( 'oo-ui-labelWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.OptionWidget );
+OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabeledElement );
+
+/* Static Properties */
+
+OO.ui.LabelWidget.static.tagName = 'label';
+
+/* Methods */
 
 /**
- * Menu widget.
+ * Handles label mouse click events.
  *
- * Use together with OO.ui.MenuItemWidget.
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.LabelWidget.prototype.onClick = function () {
+       this.input.simulateLabelClick();
+       return false;
+};
+
+/**
+ * Generic option widget for use with OO.ui.SelectWidget.
  *
  * @class
- * @extends OO.ui.SelectWidget
- * @mixins OO.ui.ClippableElement
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.FlaggableElement
  *
  * @constructor
+ * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
- * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
- * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
+ * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
  */
-OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
+OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
        // Config intialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.MenuWidget.super.call( this, config );
+       OO.ui.OptionWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.ClippableElement.call( this, this.$group, config );
+       OO.ui.ItemWidget.call( this );
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
+       OO.ui.FlaggableElement.call( this, config );
 
        // Properties
-       this.autoHide = config.autoHide === undefined || !!config.autoHide;
-       this.newItems = null;
-       this.$input = config.input ? config.input.$input : null;
-       this.$previousFocus = null;
-       this.isolated = !config.input;
-       this.visible = false;
-       this.flashing = false;
-       this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
-       this.onDocumentMouseDownHandler = OO.ui.bind( this.onDocumentMouseDown, this );
+       this.data = data;
+       this.selected = false;
+       this.highlighted = false;
+       this.pressed = false;
 
        // Initialization
-       this.$element.hide().addClass( 'oo-ui-menuWidget' );
+       this.$element
+               .data( 'oo-ui-optionWidget', this )
+               .attr( 'rel', config.rel )
+               .addClass( 'oo-ui-optionWidget' )
+               .append( this.$label );
+       this.$element
+               .prepend( this.$icon )
+               .append( this.$indicator );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
+OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggableElement );
+
+/* Static Properties */
+
+OO.ui.OptionWidget.static.tagName = 'li';
+
+OO.ui.OptionWidget.static.selectable = true;
+
+OO.ui.OptionWidget.static.highlightable = true;
+
+OO.ui.OptionWidget.static.pressable = true;
+
+OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
 
 /* Methods */
 
 /**
- * Handles document mouse down events.
+ * Check if option can be selected.
  *
- * @param {jQuery.Event} e Key down event
+ * @return {boolean} Item is selectable
  */
-OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
-       if ( !$.contains( this.$element[0], e.target ) ) {
-               this.hide();
-       }
+OO.ui.OptionWidget.prototype.isSelectable = function () {
+       return this.constructor.static.selectable && !this.isDisabled();
 };
 
 /**
- * Handles key down events.
+ * Check if option can be highlighted.
  *
- * @param {jQuery.Event} e Key down event
+ * @return {boolean} Item is highlightable
  */
-OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
-       var nextItem,
-               handled = false,
-               highlightItem = this.getHighlightedItem();
+OO.ui.OptionWidget.prototype.isHighlightable = function () {
+       return this.constructor.static.highlightable && !this.isDisabled();
+};
 
-       if ( !this.isDisabled() && this.visible ) {
-               if ( !highlightItem ) {
-                       highlightItem = this.getSelectedItem();
-               }
-               switch ( e.keyCode ) {
-                       case OO.ui.Keys.ENTER:
-                               this.chooseItem( highlightItem );
-                               handled = true;
-                               break;
-                       case OO.ui.Keys.UP:
-                               nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
-                               handled = true;
-                               break;
-                       case OO.ui.Keys.DOWN:
-                               nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
-                               handled = true;
-                               break;
-                       case OO.ui.Keys.ESCAPE:
-                               if ( highlightItem ) {
-                                       highlightItem.setHighlighted( false );
-                               }
-                               this.hide();
-                               handled = true;
-                               break;
-               }
+/**
+ * Check if option can be pressed.
+ *
+ * @return {boolean} Item is pressable
+ */
+OO.ui.OptionWidget.prototype.isPressable = function () {
+       return this.constructor.static.pressable && !this.isDisabled();
+};
 
-               if ( nextItem ) {
-                       this.highlightItem( nextItem );
-                       nextItem.scrollElementIntoView();
-               }
+/**
+ * Check if option is selected.
+ *
+ * @return {boolean} Item is selected
+ */
+OO.ui.OptionWidget.prototype.isSelected = function () {
+       return this.selected;
+};
 
-               if ( handled ) {
-                       e.preventDefault();
-                       e.stopPropagation();
-                       return false;
+/**
+ * Check if option is highlighted.
+ *
+ * @return {boolean} Item is highlighted
+ */
+OO.ui.OptionWidget.prototype.isHighlighted = function () {
+       return this.highlighted;
+};
+
+/**
+ * Check if option is pressed.
+ *
+ * @return {boolean} Item is pressed
+ */
+OO.ui.OptionWidget.prototype.isPressed = function () {
+       return this.pressed;
+};
+
+/**
+ * Set selected state.
+ *
+ * @param {boolean} [state=false] Select option
+ * @chainable
+ */
+OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
+       if ( this.constructor.static.selectable ) {
+               this.selected = !!state;
+               this.$element.toggleClass( 'oo-ui-optionWidget-selected', state );
+               if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
+                       this.scrollElementIntoView();
                }
        }
+       return this;
 };
 
 /**
- * Check if the menu is visible.
+ * Set highlighted state.
  *
- * @return {boolean} Menu is visible
+ * @param {boolean} [state=false] Highlight option
+ * @chainable
  */
-OO.ui.MenuWidget.prototype.isVisible = function () {
-       return this.visible;
+OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
+       if ( this.constructor.static.highlightable ) {
+               this.highlighted = !!state;
+               this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
+       }
+       return this;
 };
 
 /**
- * Bind key down listener.
+ * Set pressed state.
+ *
+ * @param {boolean} [state=false] Press option
+ * @chainable
  */
-OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
-       if ( this.$input ) {
-               this.$input.on( 'keydown', this.onKeyDownHandler );
-       } else {
-               // Capture menu navigation keys
-               this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
+       if ( this.constructor.static.pressable ) {
+               this.pressed = !!state;
+               this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
+       }
+       return this;
+};
+
+/**
+ * Make the option's highlight flash.
+ *
+ * While flashing, the visual style of the pressed state is removed if present.
+ *
+ * @return {jQuery.Promise} Promise resolved when flashing is done
+ */
+OO.ui.OptionWidget.prototype.flash = function () {
+       var widget = this,
+               $element = this.$element,
+               deferred = $.Deferred();
+
+       if ( !this.isDisabled() && this.constructor.static.pressable ) {
+               $element.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
+               setTimeout( function () {
+                       // Restore original classes
+                       $element
+                               .toggleClass( 'oo-ui-optionWidget-highlighted', widget.highlighted )
+                               .toggleClass( 'oo-ui-optionWidget-pressed', widget.pressed );
+
+                       setTimeout( function () {
+                               deferred.resolve();
+                       }, 100 );
+
+               }, 100 );
        }
+
+       return deferred.promise();
 };
 
 /**
- * Unbind key down listener.
+ * Get option data.
+ *
+ * @return {Mixed} Option data
  */
-OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
-       if ( this.$input ) {
-               this.$input.off( 'keydown' );
-       } else {
-               this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
-       }
+OO.ui.OptionWidget.prototype.getData = function () {
+       return this.data;
 };
 
 /**
- * Choose an item.
+ * Option widget with an option icon and indicator.
  *
- * This will close the menu when done, unlike selectItem which only changes selection.
+ * Use together with OO.ui.SelectWidget.
  *
- * @param {OO.ui.OptionWidget} item Item to choose
- * @chainable
+ * @class
+ * @extends OO.ui.OptionWidget
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.IndicatedElement
+ *
+ * @constructor
+ * @param {Mixed} data Option data
+ * @param {Object} [config] Configuration options
  */
-OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
-       // Parent method
-       OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
+OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config ) {
+       // Parent constructor
+       OO.ui.DecoratedOptionWidget.super.call( this, data, config );
 
-       if ( item && !this.flashing ) {
-               this.flashing = true;
-               item.flash().done( OO.ui.bind( function () {
-                       this.hide();
-                       this.flashing = false;
-               }, this ) );
-       } else {
-               this.hide();
-       }
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
+       OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
 
-       return this;
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-decoratedOptionWidget' )
+               .prepend( this.$icon )
+               .append( this.$indicator );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
+
 /**
- * Add items.
+ * Option widget that looks like a button.
  *
- * Adding an existing item (by value) will move it.
+ * Use together with OO.ui.ButtonSelectWidget.
  *
- * @param {OO.ui.MenuItemWidget[]} items Items to add
- * @param {number} [index] Index to insert items after
- * @chainable
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ * @mixins OO.ui.ButtonedElement
+ *
+ * @constructor
+ * @param {Mixed} data Option data
+ * @param {Object} [config] Configuration options
  */
-OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
-       var i, len, item;
+OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
+       // Parent constructor
+       OO.ui.ButtonOptionWidget.super.call( this, data, config );
 
-       // Parent method
-       OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
+       // Mixin constructors
+       OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
 
-       // Auto-initialize
-       if ( !this.newItems ) {
-               this.newItems = [];
-       }
+       // Initialization
+       this.$element.addClass( 'oo-ui-buttonOptionWidget' );
+       this.$button.append( this.$element.contents() );
+       this.$element.append( this.$button );
+};
 
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
-               if ( this.visible ) {
-                       // Defer fitting label until
-                       item.fitLabel();
-               } else {
-                       this.newItems.push( item );
-               }
+/* Setup */
+
+OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
+
+/* Static Properties */
+
+// Allow button mouse down events to pass through so they can be handled by the parent select widget
+OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
+       OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
+
+       if ( this.constructor.static.selectable ) {
+               this.setActive( state );
        }
 
        return this;
 };
 
 /**
- * Show the menu.
+ * Item of an OO.ui.MenuWidget.
  *
- * @chainable
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ *
+ * @constructor
+ * @param {Mixed} data Item data
+ * @param {Object} [config] Configuration options
  */
-OO.ui.MenuWidget.prototype.show = function () {
-       var i, len;
-
-       if ( this.items.length ) {
-               this.$element.show();
-               this.visible = true;
-               this.bindKeyDownListener();
+OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
+       // Configuration initialization
+       config = $.extend( { 'icon': 'check' }, config );
 
-               // Change focus to enable keyboard navigation
-               if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
-                       this.$previousFocus = this.$( ':focus' );
-                       this.$input.focus();
-               }
-               if ( this.newItems && this.newItems.length ) {
-                       for ( i = 0, len = this.newItems.length; i < len; i++ ) {
-                               this.newItems[i].fitLabel();
-                       }
-                       this.newItems = null;
-               }
+       // Parent constructor
+       OO.ui.MenuItemWidget.super.call( this, data, config );
 
-               this.setClipping( true );
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuItemWidget' );
+};
 
-               // Auto-hide
-               if ( this.autoHide ) {
-                       this.getElementDocument().addEventListener(
-                               'mousedown', this.onDocumentMouseDownHandler, true
-                       );
-               }
-       }
+/* Setup */
 
-       return this;
-};
+OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.DecoratedOptionWidget );
 
 /**
- * Hide the menu.
+ * Section to group one or more items in a OO.ui.MenuWidget.
  *
- * @chainable
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ *
+ * @constructor
+ * @param {Mixed} data Item data
+ * @param {Object} [config] Configuration options
  */
-OO.ui.MenuWidget.prototype.hide = function () {
-       this.$element.hide();
-       this.visible = false;
-       this.unbindKeyDownListener();
+OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
+       // Parent constructor
+       OO.ui.MenuSectionItemWidget.super.call( this, data, config );
 
-       if ( this.isolated && this.$previousFocus ) {
-               this.$previousFocus.focus();
-               this.$previousFocus = null;
-       }
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
+};
 
-       this.getElementDocument().removeEventListener(
-               'mousedown', this.onDocumentMouseDownHandler, true
-       );
+/* Setup */
 
-       this.setClipping( false );
+OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.DecoratedOptionWidget );
 
-       return this;
-};
+/* Static Properties */
+
+OO.ui.MenuSectionItemWidget.static.selectable = false;
+
+OO.ui.MenuSectionItemWidget.static.highlightable = false;
 
 /**
- * Inline menu of options.
- *
- * Use with OO.ui.MenuOptionWidget.
+ * Items for an OO.ui.OutlineWidget.
  *
  * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.LabeledElement
- * @mixins OO.ui.TitledElement
+ * @extends OO.ui.DecoratedOptionWidget
  *
  * @constructor
+ * @param {Mixed} data Item data
  * @param {Object} [config] Configuration options
- * @cfg {Object} [menu] Configuration options to pass to menu widget
+ * @cfg {number} [level] Indentation level
+ * @cfg {boolean} [movable] Allow modification from outline controls
  */
-OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
-       // Configuration initialization
-       config = $.extend( { 'indicator': 'down' }, config );
+OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
+       // Config intialization
+       config = config || {};
 
        // Parent constructor
-       OO.ui.InlineMenuWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
-       OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
-       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
-       OO.ui.TitledElement.call( this, this.$label, config );
+       OO.ui.OutlineItemWidget.super.call( this, data, config );
 
        // Properties
-       this.menu = new OO.ui.MenuWidget( $.extend( { '$': this.$ }, config.menu ) );
-       this.$handle = this.$( '<span>' );
-
-       // Events
-       this.$element.on( { 'click': OO.ui.bind( this.onClick, this ) } );
-       this.menu.connect( this, { 'select': 'onMenuSelect' } );
+       this.level = 0;
+       this.movable = !!config.movable;
+       this.removable = !!config.removable;
 
        // Initialization
-       this.$handle
-               .addClass( 'oo-ui-inlineMenuWidget-handle' )
-               .append( this.$icon, this.$label, this.$indicator );
-       this.$element
-               .addClass( 'oo-ui-inlineMenuWidget' )
-               .append( this.$handle, this.menu.$element );
+       this.$element.addClass( 'oo-ui-outlineItemWidget' );
+       this.setLevel( config.level );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
+OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.DecoratedOptionWidget );
+
+/* Static Properties */
+
+OO.ui.OutlineItemWidget.static.highlightable = false;
+
+OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
+
+OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
+
+OO.ui.OutlineItemWidget.static.levels = 3;
 
 /* Methods */
 
 /**
- * Get the menu.
+ * Check if item is movable.
  *
- * @return {OO.ui.MenuWidget} Menu of widget
+ * Movablilty is used by outline controls.
+ *
+ * @return {boolean} Item is movable
  */
-OO.ui.InlineMenuWidget.prototype.getMenu = function () {
-       return this.menu;
+OO.ui.OutlineItemWidget.prototype.isMovable = function () {
+       return this.movable;
 };
 
 /**
- * Handles menu select events.
+ * Check if item is removable.
  *
- * @param {OO.ui.MenuItemWidget} item Selected menu item
+ * Removablilty is used by outline controls.
+ *
+ * @return {boolean} Item is removable
  */
-OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
-       var selectedLabel;
-
-       if ( !item ) {
-               return;
-       }
-
-       selectedLabel = item.getLabel();
-
-       // If the label is a DOM element, clone it, because setLabel will append() it
-       if ( selectedLabel instanceof jQuery ) {
-               selectedLabel = selectedLabel.clone();
-       }
+OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
+       return this.removable;
+};
 
-       this.setLabel( selectedLabel );
+/**
+ * Get indentation level.
+ *
+ * @return {number} Indentation level
+ */
+OO.ui.OutlineItemWidget.prototype.getLevel = function () {
+       return this.level;
 };
 
 /**
- * Handles mouse click events.
+ * Set movability.
  *
- * @param {jQuery.Event} e Mouse click event
+ * Movablilty is used by outline controls.
+ *
+ * @param {boolean} movable Item is movable
+ * @chainable
  */
-OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
-       // Skip clicks within the menu
-       if ( $.contains( this.menu.$element[0], e.target ) ) {
-               return;
-       }
-
-       if ( !this.isDisabled() ) {
-               if ( this.menu.isVisible() ) {
-                       this.menu.hide();
-               } else {
-                       this.menu.show();
-               }
-       }
-       return false;
+OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
+       this.movable = !!movable;
+       return this;
 };
 
 /**
- * Menu section item widget.
- *
- * Use with OO.ui.MenuWidget.
+ * Set removability.
  *
- * @class
- * @extends OO.ui.OptionWidget
+ * Removablilty is used by outline controls.
  *
- * @constructor
- * @param {Mixed} data Item data
- * @param {Object} [config] Configuration options
+ * @param {boolean} movable Item is removable
+ * @chainable
  */
-OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
-       // Parent constructor
-       OO.ui.MenuSectionItemWidget.super.call( this, data, config );
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
+OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
+       this.removable = !!removable;
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.OptionWidget );
-
-/* Static Properties */
-
-OO.ui.MenuSectionItemWidget.static.selectable = false;
-
-OO.ui.MenuSectionItemWidget.static.highlightable = false;
-
 /**
- * Create an OO.ui.OutlineWidget object.
- *
- * Use with OO.ui.OutlineItemWidget.
- *
- * @class
- * @extends OO.ui.SelectWidget
+ * Set indentation level.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
+ * @chainable
  */
-OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
-       // Config intialization
-       config = config || {};
+OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
+       var levels = this.constructor.static.levels,
+               levelClass = this.constructor.static.levelClass,
+               i = levels;
 
-       // Parent constructor
-       OO.ui.OutlineWidget.super.call( this, config );
+       this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
+       while ( i-- ) {
+               if ( this.level === i ) {
+                       this.$element.addClass( levelClass + i );
+               } else {
+                       this.$element.removeClass( levelClass + i );
+               }
+       }
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-outlineWidget' );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
-
 /**
- * Creates an OO.ui.OutlineControlsWidget object.
- *
- * Use together with OO.ui.OutlineWidget.js
+ * Container for content that is overlaid and positioned absolutely.
  *
  * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
  *
  * @constructor
- * @param {OO.ui.OutlineWidget} outline Outline to control
  * @param {Object} [config] Configuration options
+ * @cfg {number} [width=320] Width of popup in pixels
+ * @cfg {number} [height] Height of popup, omit to use automatic height
+ * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
+ * @cfg {string} [align='center'] Alignment of popup to origin
+ * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
+ * @cfg {jQuery} [$content] Content to append to the popup's body
+ * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
+ * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
+ * @cfg {boolean} [head] Show label and close button at the top
+ * @cfg {boolean} [padded] Add padding to the body
  */
-OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
-       // Configuration initialization
-       config = $.extend( { 'icon': 'add-item' }, config );
+OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
+       // Config intialization
+       config = config || {};
 
        // Parent constructor
-       OO.ui.OutlineControlsWidget.super.call( this, config );
+       OO.ui.PopupWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
-       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
+       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
+       OO.ui.ClippableElement.call( this, this.$( '<div>' ), config );
 
        // Properties
-       this.outline = outline;
-       this.$movers = this.$( '<div>' );
-       this.upButton = new OO.ui.ButtonWidget( {
-               '$': this.$,
-               'frameless': true,
-               'icon': 'collapse',
-               'title': OO.ui.msg( 'ooui-outline-control-move-up' )
-       } );
-       this.downButton = new OO.ui.ButtonWidget( {
-               '$': this.$,
-               'frameless': true,
-               'icon': 'expand',
-               'title': OO.ui.msg( 'ooui-outline-control-move-down' )
-       } );
-       this.removeButton = new OO.ui.ButtonWidget( {
-               '$': this.$,
-               'frameless': true,
-               'icon': 'remove',
-               'title': OO.ui.msg( 'ooui-outline-control-remove' )
-       } );
+       this.visible = false;
+       this.$popup = this.$( '<div>' );
+       this.$head = this.$( '<div>' );
+       this.$body = this.$clippable;
+       this.$anchor = this.$( '<div>' );
+       this.$container = config.$container || this.$( 'body' );
+       this.autoClose = !!config.autoClose;
+       this.$autoCloseIgnore = config.$autoCloseIgnore;
+       this.transitionTimeout = null;
+       this.anchor = null;
+       this.width = config.width !== undefined ? config.width : 320;
+       this.height = config.height !== undefined ? config.height : null;
+       this.align = config.align || 'center';
+       this.closeButton = new OO.ui.ButtonWidget( { '$': this.$, 'framed': false, 'icon': 'close' } );
+       this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
 
        // Events
-       outline.connect( this, {
-               'select': 'onOutlineChange',
-               'add': 'onOutlineChange',
-               'remove': 'onOutlineChange'
-       } );
-       this.upButton.connect( this, { 'click': [ 'emit', 'move', -1 ] } );
-       this.downButton.connect( this, { 'click': [ 'emit', 'move', 1 ] } );
-       this.removeButton.connect( this, { 'click': [ 'emit', 'remove' ] } );
+       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-outlineControlsWidget' );
-       this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
-       this.$movers
-               .addClass( 'oo-ui-outlineControlsWidget-movers' )
-               .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
-       this.$element.append( this.$icon, this.$group, this.$movers );
+       this.toggleAnchor( config.anchor === undefined || config.anchor );
+       this.$body.addClass( 'oo-ui-popupWidget-body' );
+       this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
+       this.$head
+               .addClass( 'oo-ui-popupWidget-head' )
+               .append( this.$label, this.closeButton.$element );
+       if ( !config.head ) {
+               this.$head.hide();
+       }
+       this.$popup
+               .addClass( 'oo-ui-popupWidget-popup' )
+               .append( this.$head, this.$body );
+       this.$element
+               .hide()
+               .addClass( 'oo-ui-popupWidget' )
+               .append( this.$popup, this.$anchor );
+       // Move content, which was added to #$element by OO.ui.Widget, to the body
+       if ( config.$content instanceof jQuery ) {
+               this.$body.append( config.$content );
+       }
+       if ( config.padded ) {
+               this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
+       }
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
-OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconedElement );
+OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
 
 /* Events */
 
 /**
- * @event move
- * @param {number} places Number of places to move
+ * @event hide
  */
 
 /**
- * @event remove
+ * @event show
  */
 
 /* Methods */
 
 /**
- * Handle outline change events.
+ * Handles mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
  */
-OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
-       var i, len, firstMovable, lastMovable,
-               items = this.outline.getItems(),
-               selectedItem = this.outline.getSelectedItem(),
-               movable = selectedItem && selectedItem.isMovable(),
-               removable = selectedItem && selectedItem.isRemovable();
-
-       if ( movable ) {
-               i = -1;
-               len = items.length;
-               while ( ++i < len ) {
-                       if ( items[i].isMovable() ) {
-                               firstMovable = items[i];
-                               break;
-                       }
-               }
-               i = len;
-               while ( i-- ) {
-                       if ( items[i].isMovable() ) {
-                               lastMovable = items[i];
-                               break;
-                       }
-               }
+OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
+       if (
+               this.isVisible() &&
+               !$.contains( this.$element[0], e.target ) &&
+               ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
+       ) {
+               this.toggle( false );
        }
-       this.upButton.setDisabled( !movable || selectedItem === firstMovable );
-       this.downButton.setDisabled( !movable || selectedItem === lastMovable );
-       this.removeButton.setDisabled( !removable );
 };
 
 /**
- * Creates an OO.ui.OutlineItemWidget object.
- *
- * Use with OO.ui.OutlineWidget.
- *
- * @class
- * @extends OO.ui.OptionWidget
- *
- * @constructor
- * @param {Mixed} data Item data
- * @param {Object} [config] Configuration options
- * @cfg {number} [level] Indentation level
- * @cfg {boolean} [movable] Allow modification from outline controls
+ * Bind mouse down listener.
  */
-OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
-       // Config intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.OutlineItemWidget.super.call( this, data, config );
-
-       // Properties
-       this.level = 0;
-       this.movable = !!config.movable;
-       this.removable = !!config.removable;
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-outlineItemWidget' );
-       this.setLevel( config.level );
+OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
+       // Capture clicks outside popup
+       this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.OptionWidget );
-
-/* Static Properties */
-
-OO.ui.OutlineItemWidget.static.highlightable = false;
-
-OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
-
-OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
-
-OO.ui.OutlineItemWidget.static.levels = 3;
-
-/* Methods */
+/**
+ * Handles close button click events.
+ */
+OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
+       if ( this.isVisible() ) {
+               this.toggle( false );
+       }
+};
 
 /**
- * Check if item is movable.
- *
- * Movablilty is used by outline controls.
- *
- * @return {boolean} Item is movable
+ * Unbind mouse down listener.
  */
-OO.ui.OutlineItemWidget.prototype.isMovable = function () {
-       return this.movable;
+OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
+       this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
 };
 
 /**
- * Check if item is removable.
- *
- * Removablilty is used by outline controls.
+ * Set whether to show a anchor.
  *
- * @return {boolean} Item is removable
+ * @param {boolean} [show] Show anchor, omit to toggle
  */
-OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
-       return this.removable;
+OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
+       show = show === undefined ? !this.anchored : !!show;
+
+       if ( this.anchored !== show ) {
+               if ( show ) {
+                       this.$element.addClass( 'oo-ui-popupWidget-anchored' );
+               } else {
+                       this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
+               }
+               this.anchored = show;
+       }
 };
 
 /**
- * Get indentation level.
+ * Check if showing a anchor.
  *
- * @return {number} Indentation level
+ * @return {boolean} anchor is visible
  */
-OO.ui.OutlineItemWidget.prototype.getLevel = function () {
-       return this.level;
+OO.ui.PopupWidget.prototype.hasAnchor = function () {
+       return this.anchor;
 };
 
 /**
- * Set movability.
- *
- * Movablilty is used by outline controls.
- *
- * @param {boolean} movable Item is movable
- * @chainable
+ * @inheritdoc
  */
-OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
-       this.movable = !!movable;
+OO.ui.PopupWidget.prototype.toggle = function ( show ) {
+       show = show === undefined ? !this.isVisible() : !!show;
+
+       var change = show !== this.isVisible();
+
+       // Parent method
+       OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
+
+       if ( change ) {
+               if ( show ) {
+                       this.setClipping( true );
+                       if ( this.autoClose ) {
+                               this.bindMouseDownListener();
+                       }
+                       this.updateDimensions();
+               } else {
+                       this.setClipping( false );
+                       if ( this.autoClose ) {
+                               this.unbindMouseDownListener();
+                       }
+               }
+       }
+
        return this;
 };
 
 /**
- * Set removability.
+ * Set the size of the popup.
  *
- * Removablilty is used by outline controls.
+ * Changing the size may also change the popup's position depending on the alignment.
  *
- * @param {boolean} movable Item is removable
+ * @param {number} width Width
+ * @param {number} height Height
+ * @param {boolean} [transition=false] Use a smooth transition
  * @chainable
  */
-OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
-       this.removable = !!removable;
-       return this;
+OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
+       this.width = width;
+       this.height = height !== undefined ? height : null;
+       if ( this.isVisible() ) {
+               this.updateDimensions( transition );
+       }
 };
 
 /**
- * Set indentation level.
+ * Update the size and position.
  *
- * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
+ * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
+ * be called automatically.
+ *
+ * @param {boolean} [transition=false] Use a smooth transition
  * @chainable
  */
-OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
-       var levels = this.constructor.static.levels,
-               levelClass = this.constructor.static.levelClass,
-               i = levels;
+OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
+       var widget = this,
+               padding = 10,
+               originOffset = Math.round( this.$element.offset().left ),
+               containerLeft = Math.round( this.$container.offset().left ),
+               containerWidth = this.$container.innerWidth(),
+               containerRight = containerLeft + containerWidth,
+               popupOffset = this.width * ( { 'left': 0, 'center': -0.5, 'right': -1 } )[this.align],
+               anchorWidth = this.$anchor.width(),
+               popupLeft = popupOffset - padding,
+               popupRight = popupOffset + padding + this.width + padding,
+               overlapLeft = ( originOffset + popupLeft ) - containerLeft,
+               overlapRight = containerRight - ( originOffset + popupRight );
 
-       this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
-       while ( i-- ) {
-               if ( this.level === i ) {
-                       this.$element.addClass( levelClass + i );
-               } else {
-                       this.$element.removeClass( levelClass + i );
-               }
+       // Prevent transition from being interrupted
+       clearTimeout( this.transitionTimeout );
+       if ( transition ) {
+               // Enable transition
+               this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
+       }
+
+       if ( overlapRight < 0 ) {
+               popupOffset += overlapRight;
+       } else if ( overlapLeft < 0 ) {
+               popupOffset -= overlapLeft;
+       }
+
+       // Adjust offset to avoid anchor being rendered too close to the edge
+       if ( this.align === 'right' ) {
+               popupOffset += anchorWidth;
+       } else if ( this.align === 'left' ) {
+               popupOffset -= anchorWidth;
+       }
+
+       // Position body relative to anchor and resize
+       this.$popup.css( {
+               'left': popupOffset,
+               'width': this.width,
+               'height': this.height !== null ? this.height : 'auto'
+       } );
+
+       if ( transition ) {
+               // Prevent transitioning after transition is complete
+               this.transitionTimeout = setTimeout( function () {
+                       widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
+               }, 200 );
+       } else {
+               // Prevent transitioning immediately
+               this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
        }
 
        return this;
 };
 
 /**
- * Option widget that looks like a button.
+ * Search widget.
  *
- * Use together with OO.ui.ButtonSelectWidget.
+ * Search widgets combine a query input, placed above, and a results selection widget, placed below.
+ * Results are cleared and populated each time the query is changed.
  *
  * @class
- * @extends OO.ui.OptionWidget
- * @mixins OO.ui.ButtonedElement
- * @mixins OO.ui.FlaggableElement
+ * @extends OO.ui.Widget
  *
  * @constructor
- * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
+ * @cfg {string|jQuery} [placeholder] Placeholder text for query input
+ * @cfg {string} [value] Initial query value
  */
-OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
+OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
+       // Configuration intialization
+       config = config || {};
+
        // Parent constructor
-       OO.ui.ButtonOptionWidget.super.call( this, data, config );
+       OO.ui.SearchWidget.super.call( this, config );
 
-       // Mixin constructors
-       OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
-       OO.ui.FlaggableElement.call( this, config );
+       // Properties
+       this.query = new OO.ui.TextInputWidget( {
+               '$': this.$,
+               'icon': 'search',
+               'placeholder': config.placeholder,
+               'value': config.value
+       } );
+       this.results = new OO.ui.SelectWidget( { '$': this.$ } );
+       this.$query = this.$( '<div>' );
+       this.$results = this.$( '<div>' );
+
+       // Events
+       this.query.connect( this, {
+               'change': 'onQueryChange',
+               'enter': 'onQueryEnter'
+       } );
+       this.results.connect( this, {
+               'highlight': 'onResultsHighlight',
+               'select': 'onResultsSelect'
+       } );
+       this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-buttonOptionWidget' );
-       this.$button.append( this.$element.contents() );
-       this.$element.append( this.$button );
+       this.$query
+               .addClass( 'oo-ui-searchWidget-query' )
+               .append( this.query.$element );
+       this.$results
+               .addClass( 'oo-ui-searchWidget-results' )
+               .append( this.results.$element );
+       this.$element
+               .addClass( 'oo-ui-searchWidget' )
+               .append( this.$results, this.$query );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.OptionWidget );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.FlaggableElement );
+OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
 
-/* Static Properties */
+/* Events */
 
-// Allow button mouse down events to pass through so they can be handled by the parent select widget
-OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
+/**
+ * @event highlight
+ * @param {Object|null} item Item data or null if no item is highlighted
+ */
+
+/**
+ * @event select
+ * @param {Object|null} item Item data or null if no item is selected
+ */
 
 /* Methods */
 
 /**
- * @inheritdoc
+ * Handle query key down events.
+ *
+ * @param {jQuery.Event} e Key down event
  */
-OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
-       OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
+OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
+       var highlightedItem, nextItem,
+               dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
 
-       if ( this.constructor.static.selectable ) {
-               this.setActive( state );
+       if ( dir ) {
+               highlightedItem = this.results.getHighlightedItem();
+               if ( !highlightedItem ) {
+                       highlightedItem = this.results.getSelectedItem();
+               }
+               nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
+               this.results.highlightItem( nextItem );
+               nextItem.scrollElementIntoView();
        }
+};
 
-       return this;
+/**
+ * Handle select widget select events.
+ *
+ * Clears existing results. Subclasses should repopulate items according to new query.
+ *
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryChange = function () {
+       // Reset
+       this.results.clearItems();
 };
 
 /**
- * Select widget containing button options.
+ * Handle select widget enter key events.
  *
- * Use together with OO.ui.ButtonOptionWidget.
+ * Selects highlighted item.
  *
- * @class
- * @extends OO.ui.SelectWidget
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryEnter = function () {
+       // Reset
+       this.results.selectItem( this.results.getHighlightedItem() );
+};
+
+/**
+ * Handle select widget highlight events.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @param {OO.ui.OptionWidget} item Highlighted item
+ * @fires highlight
  */
-OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
-       // Parent constructor
-       OO.ui.ButtonSelectWidget.super.call( this, config );
+OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
+       this.emit( 'highlight', item ? item.getData() : null );
+};
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-buttonSelectWidget' );
+/**
+ * Handle select widget select events.
+ *
+ * @param {OO.ui.OptionWidget} item Selected item
+ * @fires select
+ */
+OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
+       this.emit( 'select', item ? item.getData() : null );
+};
+
+/**
+ * Get the query input.
+ *
+ * @return {OO.ui.TextInputWidget} Query input
+ */
+OO.ui.SearchWidget.prototype.getQuery = function () {
+       return this.query;
+};
+
+/**
+ * Get the results list.
+ *
+ * @return {OO.ui.SelectWidget} Select list
+ */
+OO.ui.SearchWidget.prototype.getResults = function () {
+       return this.results;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
-
 /**
- * Container for content that is overlaid and positioned absolutely.
+ * Generic selection of options.
+ *
+ * Items can contain any rendering, and are uniquely identified by a has of thier data. Any widget
+ * that provides options, from which the user must choose one, should be built on this class.
+ *
+ * Use together with OO.ui.OptionWidget.
  *
  * @class
  * @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.GroupElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [tail=true] Show tail pointing to origin of popup
- * @cfg {string} [align='center'] Alignment of popup to origin
- * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
- * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
- * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
- * @cfg {boolean} [head] Show label and close button at the top
+ * @cfg {OO.ui.OptionWidget[]} [items] Options to add
  */
-OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
+OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        // Config intialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.PopupWidget.super.call( this, config );
+       OO.ui.SelectWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
-       OO.ui.ClippableElement.call( this, this.$( '<div>' ), config );
+       OO.ui.GroupWidget.call( this, this.$element, config );
 
        // Properties
-       this.visible = false;
-       this.$popup = this.$( '<div>' );
-       this.$head = this.$( '<div>' );
-       this.$body = this.$clippable;
-       this.$tail = this.$( '<div>' );
-       this.$container = config.$container || this.$( 'body' );
-       this.autoClose = !!config.autoClose;
-       this.$autoCloseIgnore = config.$autoCloseIgnore;
-       this.transitionTimeout = null;
-       this.tail = false;
-       this.align = config.align || 'center';
-       this.closeButton = new OO.ui.ButtonWidget( { '$': this.$, 'frameless': true, 'icon': 'close' } );
-       this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
+       this.pressed = false;
+       this.selecting = null;
+       this.hashes = {};
+       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
+       this.onMouseMoveHandler = OO.ui.bind( this.onMouseMove, this );
 
        // Events
-       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
+       this.$element.on( {
+               'mousedown': OO.ui.bind( this.onMouseDown, this ),
+               'mouseover': OO.ui.bind( this.onMouseOver, this ),
+               'mouseleave': OO.ui.bind( this.onMouseLeave, this )
+       } );
 
        // Initialization
-       this.useTail( config.tail !== undefined ? !!config.tail : true );
-       this.$body.addClass( 'oo-ui-popupWidget-body' );
-       this.$tail.addClass( 'oo-ui-popupWidget-tail' );
-       this.$head
-               .addClass( 'oo-ui-popupWidget-head' )
-               .append( this.$label, this.closeButton.$element );
-       if ( !config.head ) {
-               this.$head.hide();
+       this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
+       if ( $.isArray( config.items ) ) {
+               this.addItems( config.items );
        }
-       this.$popup
-               .addClass( 'oo-ui-popupWidget-popup' )
-               .append( this.$head, this.$body );
-       this.$element.hide()
-               .addClass( 'oo-ui-popupWidget' )
-               .append( this.$popup, this.$tail );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
+OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
+
+// Need to mixin base class as well
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
 
 /* Events */
 
 /**
- * @event hide
+ * @event highlight
+ * @param {OO.ui.OptionWidget|null} item Highlighted item
  */
 
 /**
- * @event show
+ * @event press
+ * @param {OO.ui.OptionWidget|null} item Pressed item
+ */
+
+/**
+ * @event select
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+
+/**
+ * @event choose
+ * @param {OO.ui.OptionWidget|null} item Chosen item
+ */
+
+/**
+ * @event add
+ * @param {OO.ui.OptionWidget[]} items Added items
+ * @param {number} index Index items were added at
  */
 
+/**
+ * @event remove
+ * @param {OO.ui.OptionWidget[]} items Removed items
+ */
+
+/* Static Properties */
+
+OO.ui.SelectWidget.static.tagName = 'ul';
+
 /* Methods */
 
 /**
- * Handles mouse down events.
+ * Handle mouse down events.
  *
+ * @private
  * @param {jQuery.Event} e Mouse down event
  */
-OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
-       if (
-               this.visible &&
-               !$.contains( this.$element[0], e.target ) &&
-               ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
-       ) {
-               this.hide();
+OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
+       var item;
+
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.togglePressed( true );
+               item = this.getTargetItem( e );
+               if ( item && item.isSelectable() ) {
+                       this.pressItem( item );
+                       this.selecting = item;
+                       this.getElementDocument().addEventListener(
+                               'mouseup',
+                               this.onMouseUpHandler,
+                               true
+                       );
+                       this.getElementDocument().addEventListener(
+                               'mousemove',
+                               this.onMouseMoveHandler,
+                               true
+                       );
+               }
        }
+       return false;
 };
 
 /**
- * Bind mouse down listener.
+ * Handle mouse up events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse up event
  */
-OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
-       // Capture clicks outside popup
-       this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
+OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
+       var item;
+
+       this.togglePressed( false );
+       if ( !this.selecting ) {
+               item = this.getTargetItem( e );
+               if ( item && item.isSelectable() ) {
+                       this.selecting = item;
+               }
+       }
+       if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
+               this.pressItem( null );
+               this.chooseItem( this.selecting );
+               this.selecting = null;
+       }
+
+       this.getElementDocument().removeEventListener(
+               'mouseup',
+               this.onMouseUpHandler,
+               true
+       );
+       this.getElementDocument().removeEventListener(
+               'mousemove',
+               this.onMouseMoveHandler,
+               true
+       );
+
+       return false;
 };
 
 /**
- * Handles close button click events.
+ * Handle mouse move events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse move event
  */
-OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
-       if ( this.visible ) {
-               this.hide();
+OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
+       var item;
+
+       if ( !this.isDisabled() && this.pressed ) {
+               item = this.getTargetItem( e );
+               if ( item && item !== this.selecting && item.isSelectable() ) {
+                       this.pressItem( item );
+                       this.selecting = item;
+               }
        }
+       return false;
 };
 
 /**
- * Unbind mouse down listener.
+ * Handle mouse over events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse over event
  */
-OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
-       this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
+OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
+       var item;
+
+       if ( !this.isDisabled() ) {
+               item = this.getTargetItem( e );
+               this.highlightItem( item && item.isHighlightable() ? item : null );
+       }
+       return false;
 };
 
 /**
- * Check if the popup is visible.
+ * Handle mouse leave events.
  *
- * @return {boolean} Popup is visible
+ * @private
+ * @param {jQuery.Event} e Mouse over event
  */
-OO.ui.PopupWidget.prototype.isVisible = function () {
-       return this.visible;
+OO.ui.SelectWidget.prototype.onMouseLeave = function () {
+       if ( !this.isDisabled() ) {
+               this.highlightItem( null );
+       }
+       return false;
+};
+
+/**
+ * Get the closest item to a jQuery.Event.
+ *
+ * @private
+ * @param {jQuery.Event} e
+ * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
+ */
+OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
+       var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
+       if ( $item.length ) {
+               return $item.data( 'oo-ui-optionWidget' );
+       }
+       return null;
 };
 
 /**
- * Set whether to show a tail.
+ * Get selected item.
  *
- * @return {boolean} Make tail visible
+ * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
  */
-OO.ui.PopupWidget.prototype.useTail = function ( value ) {
-       value = !!value;
-       if ( this.tail !== value ) {
-               this.tail = value;
-               if ( value ) {
-                       this.$element.addClass( 'oo-ui-popupWidget-tailed' );
-               } else {
-                       this.$element.removeClass( 'oo-ui-popupWidget-tailed' );
+OO.ui.SelectWidget.prototype.getSelectedItem = function () {
+       var i, len;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               if ( this.items[i].isSelected() ) {
+                       return this.items[i];
+               }
+       }
+       return null;
+};
+
+/**
+ * Get highlighted item.
+ *
+ * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
+ */
+OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
+       var i, len;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               if ( this.items[i].isHighlighted() ) {
+                       return this.items[i];
                }
        }
+       return null;
+};
+
+/**
+ * Get an existing item with equivilant data.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
+ */
+OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
+       var hash = OO.getHash( data );
+
+       if ( hash in this.hashes ) {
+               return this.hashes[hash];
+       }
+
+       return null;
 };
 
 /**
- * Check if showing a tail.
+ * Toggle pressed state.
  *
- * @return {boolean} tail is visible
+ * @param {boolean} pressed An option is being pressed
  */
-OO.ui.PopupWidget.prototype.hasTail = function () {
-       return this.tail;
+OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
+       if ( pressed === undefined ) {
+               pressed = !this.pressed;
+       }
+       if ( pressed !== this.pressed ) {
+               this.$element
+                       .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
+                       .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
+               this.pressed = pressed;
+       }
 };
 
 /**
- * Show the context.
+ * Highlight an item.
+ *
+ * Highlighting is mutually exclusive.
  *
- * @fires show
+ * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
+ * @fires highlight
  * @chainable
  */
-OO.ui.PopupWidget.prototype.show = function () {
-       if ( !this.visible ) {
-               this.setClipping( true );
-               this.$element.show();
-               this.visible = true;
-               this.emit( 'show' );
-               if ( this.autoClose ) {
-                       this.bindMouseDownListener();
+OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
+       var i, len, highlighted,
+               changed = false;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               highlighted = this.items[i] === item;
+               if ( this.items[i].isHighlighted() !== highlighted ) {
+                       this.items[i].setHighlighted( highlighted );
+                       changed = true;
                }
        }
+       if ( changed ) {
+               this.emit( 'highlight', item );
+       }
+
        return this;
 };
 
 /**
- * Hide the context.
+ * Select an item.
  *
- * @fires hide
+ * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
+ * @fires select
  * @chainable
  */
-OO.ui.PopupWidget.prototype.hide = function () {
-       if ( this.visible ) {
-               this.setClipping( false );
-               this.$element.hide();
-               this.visible = false;
-               this.emit( 'hide' );
-               if ( this.autoClose ) {
-                       this.unbindMouseDownListener();
+OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
+       var i, len, selected,
+               changed = false;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               selected = this.items[i] === item;
+               if ( this.items[i].isSelected() !== selected ) {
+                       this.items[i].setSelected( selected );
+                       changed = true;
                }
        }
+       if ( changed ) {
+               this.emit( 'select', item );
+       }
+
        return this;
 };
 
 /**
- * Updates the position and size.
+ * Press an item.
  *
- * @param {number} width Width
- * @param {number} height Height
- * @param {boolean} [transition=false] Use a smooth transition
+ * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
+ * @fires press
  * @chainable
  */
-OO.ui.PopupWidget.prototype.display = function ( width, height, transition ) {
-       var padding = 10,
-               originOffset = Math.round( this.$element.offset().left ),
-               containerLeft = Math.round( this.$container.offset().left ),
-               containerWidth = this.$container.innerWidth(),
-               containerRight = containerLeft + containerWidth,
-               popupOffset = width * ( { 'left': 0, 'center': -0.5, 'right': -1 } )[this.align],
-               popupLeft = popupOffset - padding,
-               popupRight = popupOffset + padding + width + padding,
-               overlapLeft = ( originOffset + popupLeft ) - containerLeft,
-               overlapRight = containerRight - ( originOffset + popupRight );
-
-       // Prevent transition from being interrupted
-       clearTimeout( this.transitionTimeout );
-       if ( transition ) {
-               // Enable transition
-               this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
-       }
+OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
+       var i, len, pressed,
+               changed = false;
 
-       if ( overlapRight < 0 ) {
-               popupOffset += overlapRight;
-       } else if ( overlapLeft < 0 ) {
-               popupOffset -= overlapLeft;
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               pressed = this.items[i] === item;
+               if ( this.items[i].isPressed() !== pressed ) {
+                       this.items[i].setPressed( pressed );
+                       changed = true;
+               }
        }
-
-       // Position body relative to anchor and resize
-       this.$popup.css( {
-               'left': popupOffset,
-               'width': width,
-               'height': height === undefined ? 'auto' : height
-       } );
-
-       if ( transition ) {
-               // Prevent transitioning after transition is complete
-               this.transitionTimeout = setTimeout( OO.ui.bind( function () {
-                       this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
-               }, this ), 200 );
-       } else {
-               // Prevent transitioning immediately
-               this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
+       if ( changed ) {
+               this.emit( 'press', item );
        }
 
        return this;
 };
 
 /**
- * Button that shows and hides a popup.
+ * Choose an item.
  *
- * @class
- * @extends OO.ui.ButtonWidget
- * @mixins OO.ui.PopuppableElement
+ * Identical to #selectItem, but may vary in subclasses that want to take additional action when
+ * an item is selected using the keyboard or mouse.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @param {OO.ui.OptionWidget} item Item to choose
+ * @fires choose
+ * @chainable
  */
-OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
-       // Parent constructor
-       OO.ui.PopupButtonWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.PopuppableElement.call( this, config );
+OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
+       this.selectItem( item );
+       this.emit( 'choose', item );
 
-       // Initialization
-       this.$element
-               .addClass( 'oo-ui-popupButtonWidget' )
-               .append( this.popup.$element );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
-OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
-
-/* Methods */
-
 /**
- * Handles mouse click events.
+ * Get an item relative to another one.
  *
- * @param {jQuery.Event} e Mouse click event
+ * @param {OO.ui.OptionWidget} item Item to start at
+ * @param {number} direction Direction to move in
+ * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
  */
-OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
-       // Skip clicks within the popup
-       if ( $.contains( this.popup.$element[0], e.target ) ) {
-               return;
-       }
+OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
+       var inc = direction > 0 ? 1 : -1,
+               len = this.items.length,
+               index = item instanceof OO.ui.OptionWidget ?
+                       $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ),
+               stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
+               i = inc > 0 ?
+                       // Default to 0 instead of -1, if nothing is selected let's start at the beginning
+                       Math.max( index, -1 ) :
+                       // Default to n-1 instead of -1, if nothing is selected let's start at the end
+                       Math.min( index, len );
 
-       if ( !this.isDisabled() ) {
-               if ( this.popup.isVisible() ) {
-                       this.hidePopup();
-               } else {
-                       this.showPopup();
+       while ( true ) {
+               i = ( i + inc + len ) % len;
+               item = this.items[i];
+               if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+                       return item;
+               }
+               // Stop iterating when we've looped all the way around
+               if ( i === stopAt ) {
+                       break;
                }
-               OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
        }
-       return false;
+       return null;
 };
 
 /**
- * Search widget.
- *
- * Combines query and results selection widgets.
- *
- * @class
- * @extends OO.ui.Widget
+ * Get the next selectable item.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string|jQuery} [placeholder] Placeholder text for query input
- * @cfg {string} [value] Initial query value
+ * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
  */
-OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
-       // Configuration intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.SearchWidget.super.call( this, config );
-
-       // Properties
-       this.query = new OO.ui.TextInputWidget( {
-               '$': this.$,
-               'icon': 'search',
-               'placeholder': config.placeholder,
-               'value': config.value
-       } );
-       this.results = new OO.ui.SelectWidget( { '$': this.$ } );
-       this.$query = this.$( '<div>' );
-       this.$results = this.$( '<div>' );
+OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
+       var i, len, item;
 
-       // Events
-       this.query.connect( this, {
-               'change': 'onQueryChange',
-               'enter': 'onQueryEnter'
-       } );
-       this.results.connect( this, {
-               'highlight': 'onResultsHighlight',
-               'select': 'onResultsSelect'
-       } );
-       this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) );
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               item = this.items[i];
+               if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+                       return item;
+               }
+       }
 
-       // Initialization
-       this.$query
-               .addClass( 'oo-ui-searchWidget-query' )
-               .append( this.query.$element );
-       this.$results
-               .addClass( 'oo-ui-searchWidget-results' )
-               .append( this.results.$element );
-       this.$element
-               .addClass( 'oo-ui-searchWidget' )
-               .append( this.$results, this.$query );
+       return null;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
-
-/* Events */
-
-/**
- * @event highlight
- * @param {Object|null} item Item data or null if no item is highlighted
- */
-
-/**
- * @event select
- * @param {Object|null} item Item data or null if no item is selected
- */
-
-/* Methods */
-
 /**
- * Handle query key down events.
+ * Add items.
  *
- * @param {jQuery.Event} e Key down event
+ * When items are added with the same values as existing items, the existing items will be
+ * automatically removed before the new items are added.
+ *
+ * @param {OO.ui.OptionWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
+ * @fires add
+ * @chainable
  */
-OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
-       var highlightedItem, nextItem,
-               dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
+OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
+       var i, len, item, hash,
+               remove = [];
 
-       if ( dir ) {
-               highlightedItem = this.results.getHighlightedItem();
-               if ( !highlightedItem ) {
-                       highlightedItem = this.results.getSelectedItem();
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               hash = OO.getHash( item.getData() );
+               if ( hash in this.hashes ) {
+                       // Remove item with same value
+                       remove.push( this.hashes[hash] );
                }
-               nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
-               this.results.highlightItem( nextItem );
-               nextItem.scrollElementIntoView();
+               this.hashes[hash] = item;
        }
+       if ( remove.length ) {
+               this.removeItems( remove );
+       }
+
+       // Mixin method
+       OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
+
+       // Always provide an index, even if it was omitted
+       this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
+
+       return this;
 };
 
 /**
- * Handle select widget select events.
+ * Remove items.
  *
- * Clears existing results. Subclasses should repopulate items according to new query.
+ * Items will be detached, not removed, so they can be used later.
  *
- * @param {string} value New value
+ * @param {OO.ui.OptionWidget[]} items Items to remove
+ * @fires remove
+ * @chainable
  */
-OO.ui.SearchWidget.prototype.onQueryChange = function () {
-       // Reset
-       this.results.clearItems();
+OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
+       var i, len, item, hash;
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               hash = OO.getHash( item.getData() );
+               if ( hash in this.hashes ) {
+                       // Remove existing item
+                       delete this.hashes[hash];
+               }
+               if ( item.isSelected() ) {
+                       this.selectItem( null );
+               }
+       }
+
+       // Mixin method
+       OO.ui.GroupWidget.prototype.removeItems.call( this, items );
+
+       this.emit( 'remove', items );
+
+       return this;
 };
 
 /**
- * Handle select widget enter key events.
+ * Clear all items.
  *
- * Selects highlighted item.
+ * Items will be detached, not removed, so they can be used later.
  *
- * @param {string} value New value
+ * @fires remove
+ * @chainable
  */
-OO.ui.SearchWidget.prototype.onQueryEnter = function () {
-       // Reset
-       this.results.selectItem( this.results.getHighlightedItem() );
-};
+OO.ui.SelectWidget.prototype.clearItems = function () {
+       var items = this.items.slice();
 
-/**
- * Handle select widget highlight events.
- *
- * @param {OO.ui.OptionWidget} item Highlighted item
- * @fires highlight
- */
-OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
-       this.emit( 'highlight', item ? item.getData() : null );
-};
+       // Clear all items
+       this.hashes = {};
+       // Mixin method
+       OO.ui.GroupWidget.prototype.clearItems.call( this );
+       this.selectItem( null );
 
-/**
- * Handle select widget select events.
- *
- * @param {OO.ui.OptionWidget} item Selected item
- * @fires select
- */
-OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
-       this.emit( 'select', item ? item.getData() : null );
-};
+       this.emit( 'remove', items );
 
-/**
- * Get the query input.
- *
- * @return {OO.ui.TextInputWidget} Query input
- */
-OO.ui.SearchWidget.prototype.getQuery = function () {
-       return this.query;
+       return this;
 };
 
 /**
- * Get the results list.
+ * Select widget containing button options.
  *
- * @return {OO.ui.SelectWidget} Select list
- */
-OO.ui.SearchWidget.prototype.getResults = function () {
-       return this.results;
-};
-
-/**
- * Text input widget.
+ * Use together with OO.ui.ButtonOptionWidget.
  *
  * @class
- * @extends OO.ui.InputWidget
+ * @extends OO.ui.SelectWidget
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {string} [placeholder] Placeholder text
- * @cfg {string} [icon] Symbolic name of icon
- * @cfg {boolean} [multiline=false] Allow multiple lines of text
- * @cfg {boolean} [autosize=false] Automatically resize to fit content
- * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
  */
-OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
-       config = $.extend( { 'maxRows': 10 }, config );
-
+OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
        // Parent constructor
-       OO.ui.TextInputWidget.super.call( this, config );
-
-       // Properties
-       this.pending = 0;
-       this.multiline = !!config.multiline;
-       this.autosize = !!config.autosize;
-       this.maxRows = config.maxRows;
-
-       // Events
-       this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
-       this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) );
+       OO.ui.ButtonSelectWidget.super.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-textInputWidget' );
-       if ( config.icon ) {
-               this.$element.addClass( 'oo-ui-textInputWidget-decorated' );
-               this.$element.append(
-                       this.$( '<span>' )
-                               .addClass( 'oo-ui-textInputWidget-icon oo-ui-icon-' + config.icon )
-                               .mousedown( OO.ui.bind( function () {
-                                       this.$input.focus();
-                                       return false;
-                               }, this ) )
-               );
-       }
-       if ( config.placeholder ) {
-               this.$input.attr( 'placeholder', config.placeholder );
-       }
+       this.$element.addClass( 'oo-ui-buttonSelectWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
-
-/* Events */
+OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
 
 /**
- * User presses enter inside the text box.
+ * Overlaid menu of options.
  *
- * Not called if input is multiline.
+ * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
+ * the menu.
  *
- * @event enter
+ * Use together with OO.ui.MenuItemWidget.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.ClippableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
+ * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
+ * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
  */
+OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
+       // Config intialization
+       config = config || {};
 
-/* Methods */
+       // Parent constructor
+       OO.ui.MenuWidget.super.call( this, config );
 
-/**
- * Handle key press events.
- *
- * @param {jQuery.Event} e Key press event
- * @fires enter If enter key is pressed and input is not multiline
- */
-OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
-       if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
-               this.emit( 'enter' );
-       }
-};
+       // Mixin constructors
+       OO.ui.ClippableElement.call( this, this.$group, config );
 
-/**
- * Handle element attach events.
- *
- * @param {jQuery.Event} e Element attach event
- */
-OO.ui.TextInputWidget.prototype.onElementAttach = function () {
-       this.adjustSize();
+       // Properties
+       this.flashing = false;
+       this.visible = false;
+       this.newItems = null;
+       this.autoHide = config.autoHide === undefined || !!config.autoHide;
+       this.$input = config.input ? config.input.$input : null;
+       this.$widget = config.widget ? config.widget.$element : null;
+       this.$previousFocus = null;
+       this.isolated = !config.input;
+       this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
+       this.onDocumentMouseDownHandler = OO.ui.bind( this.onDocumentMouseDown, this );
+
+       // Initialization
+       this.$element
+               .hide()
+               .addClass( 'oo-ui-menuWidget' );
 };
 
-/**
- * @inheritdoc
- */
-OO.ui.TextInputWidget.prototype.onEdit = function () {
-       this.adjustSize();
+/* Setup */
 
-       // Parent method
-       return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
-};
+OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
+
+/* Methods */
 
 /**
- * Automatically adjust the size of the text input.
- *
- * This only affects multi-line inputs that are auto-sized.
+ * Handles document mouse down events.
  *
- * @chainable
+ * @param {jQuery.Event} e Key down event
  */
-OO.ui.TextInputWidget.prototype.adjustSize = function () {
-       var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
-
-       if ( this.multiline && this.autosize ) {
-               $clone = this.$input.clone()
-                       .val( this.$input.val() )
-                       .css( { 'height': 0 } )
-                       .insertAfter( this.$input );
-               // Set inline height property to 0 to measure scroll height
-               scrollHeight = $clone[0].scrollHeight;
-               // Remove inline height property to measure natural heights
-               $clone.css( 'height', '' );
-               innerHeight = $clone.innerHeight();
-               outerHeight = $clone.outerHeight();
-               // Measure max rows height
-               $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' );
-               maxInnerHeight = $clone.innerHeight();
-               $clone.removeAttr( 'rows' ).css( 'height', '' );
-               $clone.remove();
-               idealHeight = Math.min( maxInnerHeight, scrollHeight );
-               // Only apply inline height when expansion beyond natural height is needed
-               this.$input.css(
-                       'height',
-                       // Use the difference between the inner and outer height as a buffer
-                       idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : ''
-               );
+OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
+       if ( !$.contains( this.$element[0], e.target ) && ( !this.$widget || !$.contains( this.$widget[0], e.target ) ) ) {
+               this.toggle( false );
        }
-       return this;
 };
 
 /**
- * Get input element.
+ * Handles key down events.
  *
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
+ * @param {jQuery.Event} e Key down event
  */
-OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
-       return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
-};
+OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
+       var nextItem,
+               handled = false,
+               highlightItem = this.getHighlightedItem();
+
+       if ( !this.isDisabled() && this.isVisible() ) {
+               if ( !highlightItem ) {
+                       highlightItem = this.getSelectedItem();
+               }
+               switch ( e.keyCode ) {
+                       case OO.ui.Keys.ENTER:
+                               this.chooseItem( highlightItem );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.UP:
+                               nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.DOWN:
+                               nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.ESCAPE:
+                               if ( highlightItem ) {
+                                       highlightItem.setHighlighted( false );
+                               }
+                               this.toggle( false );
+                               handled = true;
+                               break;
+               }
 
-/* Methods */
+               if ( nextItem ) {
+                       this.highlightItem( nextItem );
+                       nextItem.scrollElementIntoView();
+               }
 
-/**
- * Check if input supports multiple lines.
- *
- * @return {boolean}
- */
-OO.ui.TextInputWidget.prototype.isMultiline = function () {
-       return !!this.multiline;
+               if ( handled ) {
+                       e.preventDefault();
+                       e.stopPropagation();
+                       return false;
+               }
+       }
 };
 
 /**
- * Check if input automatically adjusts its size.
- *
- * @return {boolean}
+ * Bind key down listener.
  */
-OO.ui.TextInputWidget.prototype.isAutosizing = function () {
-       return !!this.autosize;
+OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
+       if ( this.$input ) {
+               this.$input.on( 'keydown', this.onKeyDownHandler );
+       } else {
+               // Capture menu navigation keys
+               this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+       }
 };
 
 /**
- * Check if input is pending.
- *
- * @return {boolean}
+ * Unbind key down listener.
  */
-OO.ui.TextInputWidget.prototype.isPending = function () {
-       return !!this.pending;
+OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
+       if ( this.$input ) {
+               this.$input.off( 'keydown' );
+       } else {
+               this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+       }
 };
 
 /**
- * Increase the pending stack.
+ * Choose an item.
+ *
+ * This will close the menu when done, unlike selectItem which only changes selection.
  *
+ * @param {OO.ui.OptionWidget} item Item to choose
  * @chainable
  */
-OO.ui.TextInputWidget.prototype.pushPending = function () {
-       if ( this.pending === 0 ) {
-               this.$element.addClass( 'oo-ui-textInputWidget-pending' );
-               this.$input.addClass( 'oo-ui-texture-pending' );
+OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
+       var widget = this;
+
+       // Parent method
+       OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
+
+       if ( item && !this.flashing ) {
+               this.flashing = true;
+               item.flash().done( function () {
+                       widget.toggle( false );
+                       widget.flashing = false;
+               } );
+       } else {
+               this.toggle( false );
        }
-       this.pending++;
 
        return this;
 };
 
 /**
- * Reduce the pending stack.
+ * Add items.
  *
- * Clamped at zero.
+ * Adding an existing item (by value) will move it.
  *
+ * @param {OO.ui.MenuItemWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
  * @chainable
  */
-OO.ui.TextInputWidget.prototype.popPending = function () {
-       if ( this.pending === 1 ) {
-               this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
-               this.$input.removeClass( 'oo-ui-texture-pending' );
+OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
+       var i, len, item;
+
+       // Parent method
+       OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
+
+       // Auto-initialize
+       if ( !this.newItems ) {
+               this.newItems = [];
+       }
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               if ( this.isVisible() ) {
+                       // Defer fitting label until
+                       item.fitLabel();
+               } else {
+                       this.newItems.push( item );
+               }
        }
-       this.pending = Math.max( 0, this.pending - 1 );
 
        return this;
 };
 
 /**
- * Select the contents of the input.
- *
- * @chainable
+ * @inheritdoc
  */
-OO.ui.TextInputWidget.prototype.select = function () {
-       this.$input.select();
+OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
+       visible = !!visible && !!this.items.length;
+
+       var i, len,
+               change = visible !== this.isVisible();
+
+       // Parent method
+       OO.ui.MenuWidget.super.prototype.toggle.call( this, visible );
+
+       if ( change ) {
+               if ( visible ) {
+                       this.bindKeyDownListener();
+
+                       // Change focus to enable keyboard navigation
+                       if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
+                               this.$previousFocus = this.$( ':focus' );
+                               this.$input.focus();
+                       }
+                       if ( this.newItems && this.newItems.length ) {
+                               for ( i = 0, len = this.newItems.length; i < len; i++ ) {
+                                       this.newItems[i].fitLabel();
+                               }
+                               this.newItems = null;
+                       }
+                       this.setClipping( true );
+
+                       // Auto-hide
+                       if ( this.autoHide ) {
+                               this.getElementDocument().addEventListener(
+                                       'mousedown', this.onDocumentMouseDownHandler, true
+                               );
+                       }
+               } else {
+                       this.unbindKeyDownListener();
+                       if ( this.isolated && this.$previousFocus ) {
+                               this.$previousFocus.focus();
+                               this.$previousFocus = null;
+                       }
+                       this.getElementDocument().removeEventListener(
+                               'mousedown', this.onDocumentMouseDownHandler, true
+                       );
+                       this.setClipping( false );
+               }
+       }
+
        return this;
 };
 
 /**
  * Menu for a text input widget.
  *
+ * This menu is specially designed to be positioned beneeth the text input widget. Even if the input
+ * is in a different frame, the menu's position is automatically calulated and maintained when the
+ * menu is toggled or the window is resized.
+ *
  * @class
  * @extends OO.ui.MenuWidget
  *
@@ -8437,29 +10406,24 @@ OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
 };
 
 /**
- * Show the menu.
- *
- * @chainable
+ * @inheritdoc
  */
-OO.ui.TextInputMenuWidget.prototype.show = function () {
-       // Parent method
-       OO.ui.TextInputMenuWidget.super.prototype.show.call( this );
+OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
+       visible = !!visible;
 
-       this.position();
-       this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
-       return this;
-};
+       var change = visible !== this.isVisible();
 
-/**
- * Hide the menu.
- *
- * @chainable
- */
-OO.ui.TextInputMenuWidget.prototype.hide = function () {
        // Parent method
-       OO.ui.TextInputMenuWidget.super.prototype.hide.call( this );
+       OO.ui.TextInputMenuWidget.super.prototype.toggle.call( this, visible );
 
-       this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+       if ( change ) {
+               if ( this.isVisible() ) {
+                       this.position();
+                       this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
+               } else {
+                       this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+               }
+       }
        return this;
 };
 
@@ -8492,130 +10456,37 @@ OO.ui.TextInputMenuWidget.prototype.position = function () {
                        delete dimensions.left;
                }
        }
-
        this.$element.css( dimensions );
        this.setIdealSize( $container.width() );
-       return this;
-};
-
-/**
- * Width with on and off states.
- *
- * Mixin for widgets with a boolean state.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [value=false] Initial value
- */
-OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
-       // Configuration initialization
-       config = config || {};
-
-       // Properties
-       this.value = null;
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-toggleWidget' );
-       this.setValue( !!config.value );
-};
-
-/* Events */
-
-/**
- * @event change
- * @param {boolean} value Changed value
- */
-
-/* Methods */
-
-/**
- * Get the value of the toggle.
- *
- * @return {boolean}
- */
-OO.ui.ToggleWidget.prototype.getValue = function () {
-       return this.value;
-};
 
-/**
- * Set the value of the toggle.
- *
- * @param {boolean} value New value
- * @fires change
- * @chainable
- */
-OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
-       value = !!value;
-       if ( this.value !== value ) {
-               this.value = value;
-               this.emit( 'change', value );
-               this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
-               this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
-       }
        return this;
 };
 
 /**
- * Button that toggles on and off.
+ * Structured list of items.
+ *
+ * Use with OO.ui.OutlineItemWidget.
  *
  * @class
- * @extends OO.ui.ButtonWidget
- * @mixins OO.ui.ToggleWidget
+ * @extends OO.ui.SelectWidget
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [value=false] Initial value
  */
-OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
-       // Configuration initialization
+OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
+       // Config intialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.ToggleButtonWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.ToggleWidget.call( this, config );
+       OO.ui.OutlineWidget.super.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-toggleButtonWidget' );
+       this.$element.addClass( 'oo-ui-outlineWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
-
-/* Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.ToggleButtonWidget.prototype.onClick = function () {
-       if ( !this.isDisabled() ) {
-               this.setValue( !this.value );
-       }
-
-       // Parent method
-       return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
-       value = !!value;
-       if ( value !== this.value ) {
-               this.setActive( value );
-       }
-
-       // Parent method (from mixin)
-       OO.ui.ToggleWidget.prototype.setValue.call( this, value );
-
-       return this;
-};
+OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
 
 /**
  * Switch that slides on and off.
index a249314..08dffc7 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (97dbb50137)
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2014-07-03T02:33:09Z
+ * Date: 2014-07-16T22:59:48Z
  */
 /* Textures */
 
   direction: ltr;
 }
 
-.oo-ui-dialog {
-  position: fixed;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-  padding: 1em;
-  line-height: 1em;
-  /* Fix for strange opacity-related rendering issues.
-          CAUTION: -webkit-backface-visibility: hidden; is EXTREMELY DANGEROUS.
-          If applied to a VE surface directly, it will break selection of
-          FocusableNodes, and in the past it's caused transparent PNGs to
-          render as opaque black images. For some reason applying it to the dialog
-          wrapper in the main document fixes opacity-related behavior in the iframe
-          document, but doesn't break the surface inside the iframe. */
-
-  -webkit-backface-visibility: hidden;
-          backface-visibility: hidden;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame {
-  position: fixed;
-  right: 0;
-  left: 0;
-  min-height: 12em;
-  margin: auto;
-  overflow: hidden;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame .oo-ui-frame {
-  width: 100%;
-  height: 100%;
-}
-
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed {
-  float: left;
-}
-
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-flaggableElement-primary,
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-flaggableElement-constructive,
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-flaggableElement-destructive {
-  float: right;
-}
-
-.oo-ui-dialog-content-footless .oo-ui-window-foot {
-  display: none;
-}
-
 .oo-ui-frame {
   padding: 0;
   margin: 0;
   background-repeat: no-repeat;
 }
 
-.oo-ui-window-head {
+.oo-ui-window {
+  line-height: 1em;
+}
+
+> .oo-ui-window-frame {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+
+> .oo-ui-window-frame > .oo-ui-frame {
+  width: 100%;
+  height: 100%;
+}
+
+.oo-ui-window-head,
+.oo-ui-window-foot {
   -webkit-user-select: none;
      -moz-user-select: none;
       -ms-user-select: none;
   -webkit-touch-callout: none;
 }
 
-.oo-ui-window-icon {
-  float: left;
-  background-position: center center;
-  background-repeat: no-repeat;
+.oo-ui-window-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
 }
 
-.oo-ui-window-title {
-  float: left;
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+  position: fixed;
+  width: 0;
+  height: 0;
+  overflow: hidden;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup {
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  width: auto;
+  height: auto;
+  padding: 1em;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
+  position: fixed;
+  right: 0;
+  left: 0;
+  max-width: 100%;
+  max-height: 100%;
+  margin: auto;
+  overflow: hidden;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame .oo-ui-frame {
+  width: 100%;
+  height: 100%;
+}
+
+.oo-ui-windowManager-fullscreen > .oo-ui-dialog > .oo-ui-window-frame {
+  top: 0;
+  bottom: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.oo-ui-messageDialog-actions-horizontal {
+  display: table;
+  width: 100%;
+  table-layout: fixed;
+}
+
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+  display: table-cell;
+  width: 1%;
+}
+
+.oo-ui-messageDialog-actions-vertical {
+  display: block;
+}
+
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget {
+  position: relative;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-buttonedElement-button {
+  display: block;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labeledElement-label {
+  position: relative;
+  top: auto;
+  bottom: auto;
+  display: inline;
   white-space: nowrap;
-  cursor: default;
 }
 
-.oo-ui-window-overlay {
+.oo-ui-processDialog-location {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.oo-ui-processDialog-title {
+  display: inline;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget {
+  white-space: nowrap;
+}
+
+.oo-ui-processDialog-actions-safe,
+.oo-ui-processDialog-actions-primary {
   position: absolute;
   top: 0;
+  bottom: 0;
+}
+
+.oo-ui-processDialog-actions-safe {
   left: 0;
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button {
+.oo-ui-processDialog-actions-primary {
+  right: 0;
+}
+
+.oo-ui-processDialog-errors {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+  display: none;
+  padding: 3em 3em 1.5em 3em;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.oo-ui-buttonedElement > .oo-ui-buttonedElement-button {
   display: inline-block;
   vertical-align: middle;
   cursor: pointer;
   -webkit-touch-callout: none;
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   display: none;
   margin-left: 0;
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator {
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator {
   display: none;
   margin-right: -0.75em;
 }
 
-.oo-ui-buttonedElement.oo-ui-widget-disabled .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement.oo-ui-widget-disabled .oo-ui-buttonedElement-button {
   cursor: default;
 }
 
-.oo-ui-buttonedElement.oo-ui-indicatedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator,
-.oo-ui-buttonedElement.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement.oo-ui-indicatedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator,
+.oo-ui-buttonedElement.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   display: inline-block;
   vertical-align: middle;
   background-position: center center;
   display: inline-block;
 }
 
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   display: inline-block;
   margin-left: 0.25em;
   vertical-align: middle;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
   display: inline-block;
   text-align: center;
   vertical-align: top;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   display: inline-block;
   line-height: 1.9em;
   vertical-align: middle;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   cursor: default;
 }
 
 }
 
 .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-labeledElement-label {
+  display: inline-block;
   padding: 0.5em 0;
 }
 
+.oo-ui-fieldLayout > .oo-ui-popupButtonWidget > .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+  margin-top: 0.25em;
+}
+
+.oo-ui-fieldLayout > .oo-ui-popupButtonWidget > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
+  z-index: 1;
+}
+
 .oo-ui-fieldsetLayout {
   position: relative;
   padding: 0;
 }
 
 .oo-ui-labelWidget {
+  display: inline-block;
   padding: 0.5em 0;
 }
 
+.oo-ui-panelLayout {
+  position: relative;
+}
+
 .oo-ui-panelLayout-scrollable {
   overflow-y: auto;
 }
 
+.oo-ui-panelLayout-expanded {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
 .oo-ui-stackLayout > .oo-ui-panelLayout {
   display: none;
 }
 }
 
 .oo-ui-menuToolGroup .oo-ui-tool-active .oo-ui-tool-link .oo-ui-iconedElement-icon {
-  background-image: /* @embed */ url(images/icons/check.png);
+  background-image: /* @embed */ url(images/icons/check.svg);
 }
 
 .oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
 }
 
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
-.oo-ui-popupTool .oo-ui-popupWidget-tail {
+.oo-ui-popupTool .oo-ui-popupWidget-anchor {
   z-index: 4;
 }
 
   white-space: nowrap;
 }
 
-.oo-ui-optionWidget .oo-ui-iconedElement-icon,
-.oo-ui-optionWidget .oo-ui-indicatedElement-indicator {
+.oo-ui-decoratedOptionWidget .oo-ui-iconedElement-icon,
+.oo-ui-decoratedOptionWidget .oo-ui-indicatedElement-indicator {
   position: absolute;
   top: 50%;
   width: 2em;
   background-repeat: no-repeat;
 }
 
-.oo-ui-optionWidget .oo-ui-iconedElement-icon {
+.oo-ui-decoratedOptionWidget .oo-ui-iconedElement-icon {
   left: 0.5em;
 }
 
-.oo-ui-optionWidget .oo-ui-indicatedElement-indicator {
+.oo-ui-decoratedOptionWidget .oo-ui-indicatedElement-indicator {
   right: 0.5em;
 }
 
 
 .oo-ui-popupWidget-popup {
   position: absolute;
+  z-index: 1;
   overflow: hidden;
 }
 
-.oo-ui-popupWidget-tail {
+.oo-ui-popupWidget-anchor {
+  z-index: 1;
   display: none;
 }
 
-.oo-ui-popupWidget-tailed .oo-ui-popupWidget-popup {
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-popup {
   margin-top: 7px;
 }
 
-.oo-ui-popupWidget-tailed .oo-ui-popupWidget-tail {
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
   position: absolute;
   display: block;
   background-repeat: no-repeat;
 }
 
 .oo-ui-popupWidget-body {
+  overflow: hidden;
   clear: both;
 }
 
+.oo-ui-popupWidget-body-padded {
+  padding: 0 1em;
+}
+
 .oo-ui-buttonGroupWidget {
   border-radius: 0.3em;
 }
diff --git a/resources/lib/sinonjs/sinon-1.10.3.js b/resources/lib/sinonjs/sinon-1.10.3.js
new file mode 100644 (file)
index 0000000..703414d
--- /dev/null
@@ -0,0 +1,5073 @@
+/**
+ * Sinon.JS 1.10.3, 2014/07/11
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ *     * Redistributions of source code must retain the above copyright notice,
+ *       this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright notice,
+ *       this list of conditions and the following disclaimer in the documentation
+ *       and/or other materials provided with the distribution.
+ *     * Neither the name of Christian Johansen nor the names of his contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+this.sinon = (function () {
+var samsam, formatio;
+function define(mod, deps, fn) { if (mod == "samsam") { samsam = deps(); } else if (typeof fn === "function") { formatio = fn(samsam); } }
+define.amd = {};
+((typeof define === "function" && define.amd && function (m) { define("samsam", m); }) ||
+ (typeof module === "object" &&
+      function (m) { module.exports = m(); }) || // Node
+ function (m) { this.samsam = m(); } // Browser globals
+)(function () {
+    var o = Object.prototype;
+    var div = typeof document !== "undefined" && document.createElement("div");
+
+    function isNaN(value) {
+        // Unlike global isNaN, this avoids type coercion
+        // typeof check avoids IE host object issues, hat tip to
+        // lodash
+        var val = value; // JsLint thinks value !== value is "weird"
+        return typeof value === "number" && value !== val;
+    }
+
+    function getClass(value) {
+        // Returns the internal [[Class]] by calling Object.prototype.toString
+        // with the provided value as this. Return value is a string, naming the
+        // internal class, e.g. "Array"
+        return o.toString.call(value).split(/[ \]]/)[1];
+    }
+
+    /**
+     * @name samsam.isArguments
+     * @param Object object
+     *
+     * Returns ``true`` if ``object`` is an ``arguments`` object,
+     * ``false`` otherwise.
+     */
+    function isArguments(object) {
+        if (typeof object !== "object" || typeof object.length !== "number" ||
+                getClass(object) === "Array") {
+            return false;
+        }
+        if (typeof object.callee == "function") { return true; }
+        try {
+            object[object.length] = 6;
+            delete object[object.length];
+        } catch (e) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @name samsam.isElement
+     * @param Object object
+     *
+     * Returns ``true`` if ``object`` is a DOM element node. Unlike
+     * Underscore.js/lodash, this function will return ``false`` if ``object``
+     * is an *element-like* object, i.e. a regular object with a ``nodeType``
+     * property that holds the value ``1``.
+     */
+    function isElement(object) {
+        if (!object || object.nodeType !== 1 || !div) { return false; }
+        try {
+            object.appendChild(div);
+            object.removeChild(div);
+        } catch (e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @name samsam.keys
+     * @param Object object
+     *
+     * Return an array of own property names.
+     */
+    function keys(object) {
+        var ks = [], prop;
+        for (prop in object) {
+            if (o.hasOwnProperty.call(object, prop)) { ks.push(prop); }
+        }
+        return ks;
+    }
+
+    /**
+     * @name samsam.isDate
+     * @param Object value
+     *
+     * Returns true if the object is a ``Date``, or *date-like*. Duck typing
+     * of date objects work by checking that the object has a ``getTime``
+     * function whose return value equals the return value from the object's
+     * ``valueOf``.
+     */
+    function isDate(value) {
+        return typeof value.getTime == "function" &&
+            value.getTime() == value.valueOf();
+    }
+
+    /**
+     * @name samsam.isNegZero
+     * @param Object value
+     *
+     * Returns ``true`` if ``value`` is ``-0``.
+     */
+    function isNegZero(value) {
+        return value === 0 && 1 / value === -Infinity;
+    }
+
+    /**
+     * @name samsam.equal
+     * @param Object obj1
+     * @param Object obj2
+     *
+     * Returns ``true`` if two objects are strictly equal. Compared to
+     * ``===`` there are two exceptions:
+     *
+     *   - NaN is considered equal to NaN
+     *   - -0 and +0 are not considered equal
+     */
+    function identical(obj1, obj2) {
+        if (obj1 === obj2 || (isNaN(obj1) && isNaN(obj2))) {
+            return obj1 !== 0 || isNegZero(obj1) === isNegZero(obj2);
+        }
+    }
+
+
+    /**
+     * @name samsam.deepEqual
+     * @param Object obj1
+     * @param Object obj2
+     *
+     * Deep equal comparison. Two values are "deep equal" if:
+     *
+     *   - They are equal, according to samsam.identical
+     *   - They are both date objects representing the same time
+     *   - They are both arrays containing elements that are all deepEqual
+     *   - They are objects with the same set of properties, and each property
+     *     in ``obj1`` is deepEqual to the corresponding property in ``obj2``
+     *
+     * Supports cyclic objects.
+     */
+    function deepEqualCyclic(obj1, obj2) {
+
+        // used for cyclic comparison
+        // contain already visited objects
+        var objects1 = [],
+            objects2 = [],
+        // contain pathes (position in the object structure)
+        // of the already visited objects
+        // indexes same as in objects arrays
+            paths1 = [],
+            paths2 = [],
+        // contains combinations of already compared objects
+        // in the manner: { "$1['ref']$2['ref']": true }
+            compared = {};
+
+        /**
+         * used to check, if the value of a property is an object
+         * (cyclic logic is only needed for objects)
+         * only needed for cyclic logic
+         */
+        function isObject(value) {
+
+            if (typeof value === 'object' && value !== null &&
+                    !(value instanceof Boolean) &&
+                    !(value instanceof Date)    &&
+                    !(value instanceof Number)  &&
+                    !(value instanceof RegExp)  &&
+                    !(value instanceof String)) {
+
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * returns the index of the given object in the
+         * given objects array, -1 if not contained
+         * only needed for cyclic logic
+         */
+        function getIndex(objects, obj) {
+
+            var i;
+            for (i = 0; i < objects.length; i++) {
+                if (objects[i] === obj) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        // does the recursion for the deep equal check
+        return (function deepEqual(obj1, obj2, path1, path2) {
+            var type1 = typeof obj1;
+            var type2 = typeof obj2;
+
+            // == null also matches undefined
+            if (obj1 === obj2 ||
+                    isNaN(obj1) || isNaN(obj2) ||
+                    obj1 == null || obj2 == null ||
+                    type1 !== "object" || type2 !== "object") {
+
+                return identical(obj1, obj2);
+            }
+
+            // Elements are only equal if identical(expected, actual)
+            if (isElement(obj1) || isElement(obj2)) { return false; }
+
+            var isDate1 = isDate(obj1), isDate2 = isDate(obj2);
+            if (isDate1 || isDate2) {
+                if (!isDate1 || !isDate2 || obj1.getTime() !== obj2.getTime()) {
+                    return false;
+                }
+            }
+
+            if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
+                if (obj1.toString() !== obj2.toString()) { return false; }
+            }
+
+            var class1 = getClass(obj1);
+            var class2 = getClass(obj2);
+            var keys1 = keys(obj1);
+            var keys2 = keys(obj2);
+
+            if (isArguments(obj1) || isArguments(obj2)) {
+                if (obj1.length !== obj2.length) { return false; }
+            } else {
+                if (type1 !== type2 || class1 !== class2 ||
+                        keys1.length !== keys2.length) {
+                    return false;
+                }
+            }
+
+            var key, i, l,
+                // following vars are used for the cyclic logic
+                value1, value2,
+                isObject1, isObject2,
+                index1, index2,
+                newPath1, newPath2;
+
+            for (i = 0, l = keys1.length; i < l; i++) {
+                key = keys1[i];
+                if (!o.hasOwnProperty.call(obj2, key)) {
+                    return false;
+                }
+
+                // Start of the cyclic logic
+
+                value1 = obj1[key];
+                value2 = obj2[key];
+
+                isObject1 = isObject(value1);
+                isObject2 = isObject(value2);
+
+                // determine, if the objects were already visited
+                // (it's faster to check for isObject first, than to
+                // get -1 from getIndex for non objects)
+                index1 = isObject1 ? getIndex(objects1, value1) : -1;
+                index2 = isObject2 ? getIndex(objects2, value2) : -1;
+
+                // determine the new pathes of the objects
+                // - for non cyclic objects the current path will be extended
+                //   by current property name
+                // - for cyclic objects the stored path is taken
+                newPath1 = index1 !== -1
+                    ? paths1[index1]
+                    : path1 + '[' + JSON.stringify(key) + ']';
+                newPath2 = index2 !== -1
+                    ? paths2[index2]
+                    : path2 + '[' + JSON.stringify(key) + ']';
+
+                // stop recursion if current objects are already compared
+                if (compared[newPath1 + newPath2]) {
+                    return true;
+                }
+
+                // remember the current objects and their pathes
+                if (index1 === -1 && isObject1) {
+                    objects1.push(value1);
+                    paths1.push(newPath1);
+                }
+                if (index2 === -1 && isObject2) {
+                    objects2.push(value2);
+                    paths2.push(newPath2);
+                }
+
+                // remember that the current objects are already compared
+                if (isObject1 && isObject2) {
+                    compared[newPath1 + newPath2] = true;
+                }
+
+                // End of cyclic logic
+
+                // neither value1 nor value2 is a cycle
+                // continue with next level
+                if (!deepEqual(value1, value2, newPath1, newPath2)) {
+                    return false;
+                }
+            }
+
+            return true;
+
+        }(obj1, obj2, '$1', '$2'));
+    }
+
+    var match;
+
+    function arrayContains(array, subset) {
+        if (subset.length === 0) { return true; }
+        var i, l, j, k;
+        for (i = 0, l = array.length; i < l; ++i) {
+            if (match(array[i], subset[0])) {
+                for (j = 0, k = subset.length; j < k; ++j) {
+                    if (!match(array[i + j], subset[j])) { return false; }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @name samsam.match
+     * @param Object object
+     * @param Object matcher
+     *
+     * Compare arbitrary value ``object`` with matcher.
+     */
+    match = function match(object, matcher) {
+        if (matcher && typeof matcher.test === "function") {
+            return matcher.test(object);
+        }
+
+        if (typeof matcher === "function") {
+            return matcher(object) === true;
+        }
+
+        if (typeof matcher === "string") {
+            matcher = matcher.toLowerCase();
+            var notNull = typeof object === "string" || !!object;
+            return notNull &&
+                (String(object)).toLowerCase().indexOf(matcher) >= 0;
+        }
+
+        if (typeof matcher === "number") {
+            return matcher === object;
+        }
+
+        if (typeof matcher === "boolean") {
+            return matcher === object;
+        }
+
+        if (getClass(object) === "Array" && getClass(matcher) === "Array") {
+            return arrayContains(object, matcher);
+        }
+
+        if (matcher && typeof matcher === "object") {
+            var prop;
+            for (prop in matcher) {
+                if (!match(object[prop], matcher[prop])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        throw new Error("Matcher was not a string, a number, a " +
+                        "function, a boolean or an object");
+    };
+
+    return {
+        isArguments: isArguments,
+        isElement: isElement,
+        isDate: isDate,
+        isNegZero: isNegZero,
+        identical: identical,
+        deepEqual: deepEqualCyclic,
+        match: match,
+        keys: keys
+    };
+});
+((typeof define === "function" && define.amd && function (m) {
+    define("formatio", ["samsam"], m);
+}) || (typeof module === "object" && function (m) {
+    module.exports = m(require("samsam"));
+}) || function (m) { this.formatio = m(this.samsam); }
+)(function (samsam) {
+    
+    var formatio = {
+        excludeConstructors: ["Object", /^.$/],
+        quoteStrings: true
+    };
+
+    var hasOwn = Object.prototype.hasOwnProperty;
+
+    var specialObjects = [];
+    if (typeof global !== "undefined") {
+        specialObjects.push({ object: global, value: "[object global]" });
+    }
+    if (typeof document !== "undefined") {
+        specialObjects.push({
+            object: document,
+            value: "[object HTMLDocument]"
+        });
+    }
+    if (typeof window !== "undefined") {
+        specialObjects.push({ object: window, value: "[object Window]" });
+    }
+
+    function functionName(func) {
+        if (!func) { return ""; }
+        if (func.displayName) { return func.displayName; }
+        if (func.name) { return func.name; }
+        var matches = func.toString().match(/function\s+([^\(]+)/m);
+        return (matches && matches[1]) || "";
+    }
+
+    function constructorName(f, object) {
+        var name = functionName(object && object.constructor);
+        var excludes = f.excludeConstructors ||
+                formatio.excludeConstructors || [];
+
+        var i, l;
+        for (i = 0, l = excludes.length; i < l; ++i) {
+            if (typeof excludes[i] === "string" && excludes[i] === name) {
+                return "";
+            } else if (excludes[i].test && excludes[i].test(name)) {
+                return "";
+            }
+        }
+
+        return name;
+    }
+
+    function isCircular(object, objects) {
+        if (typeof object !== "object") { return false; }
+        var i, l;
+        for (i = 0, l = objects.length; i < l; ++i) {
+            if (objects[i] === object) { return true; }
+        }
+        return false;
+    }
+
+    function ascii(f, object, processed, indent) {
+        if (typeof object === "string") {
+            var qs = f.quoteStrings;
+            var quote = typeof qs !== "boolean" || qs;
+            return processed || quote ? '"' + object + '"' : object;
+        }
+
+        if (typeof object === "function" && !(object instanceof RegExp)) {
+            return ascii.func(object);
+        }
+
+        processed = processed || [];
+
+        if (isCircular(object, processed)) { return "[Circular]"; }
+
+        if (Object.prototype.toString.call(object) === "[object Array]") {
+            return ascii.array.call(f, object, processed);
+        }
+
+        if (!object) { return String((1/object) === -Infinity ? "-0" : object); }
+        if (samsam.isElement(object)) { return ascii.element(object); }
+
+        if (typeof object.toString === "function" &&
+                object.toString !== Object.prototype.toString) {
+            return object.toString();
+        }
+
+        var i, l;
+        for (i = 0, l = specialObjects.length; i < l; i++) {
+            if (object === specialObjects[i].object) {
+                return specialObjects[i].value;
+            }
+        }
+
+        return ascii.object.call(f, object, processed, indent);
+    }
+
+    ascii.func = function (func) {
+        return "function " + functionName(func) + "() {}";
+    };
+
+    ascii.array = function (array, processed) {
+        processed = processed || [];
+        processed.push(array);
+        var i, l, pieces = [];
+        for (i = 0, l = array.length; i < l; ++i) {
+            pieces.push(ascii(this, array[i], processed));
+        }
+        return "[" + pieces.join(", ") + "]";
+    };
+
+    ascii.object = function (object, processed, indent) {
+        processed = processed || [];
+        processed.push(object);
+        indent = indent || 0;
+        var pieces = [], properties = samsam.keys(object).sort();
+        var length = 3;
+        var prop, str, obj, i, l;
+
+        for (i = 0, l = properties.length; i < l; ++i) {
+            prop = properties[i];
+            obj = object[prop];
+
+            if (isCircular(obj, processed)) {
+                str = "[Circular]";
+            } else {
+                str = ascii(this, obj, processed, indent + 2);
+            }
+
+            str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str;
+            length += str.length;
+            pieces.push(str);
+        }
+
+        var cons = constructorName(this, object);
+        var prefix = cons ? "[" + cons + "] " : "";
+        var is = "";
+        for (i = 0, l = indent; i < l; ++i) { is += " "; }
+
+        if (length + indent > 80) {
+            return prefix + "{\n  " + is + pieces.join(",\n  " + is) + "\n" +
+                is + "}";
+        }
+        return prefix + "{ " + pieces.join(", ") + " }";
+    };
+
+    ascii.element = function (element) {
+        var tagName = element.tagName.toLowerCase();
+        var attrs = element.attributes, attr, pairs = [], attrName, i, l, val;
+
+        for (i = 0, l = attrs.length; i < l; ++i) {
+            attr = attrs.item(i);
+            attrName = attr.nodeName.toLowerCase().replace("html:", "");
+            val = attr.nodeValue;
+            if (attrName !== "contenteditable" || val !== "inherit") {
+                if (!!val) { pairs.push(attrName + "=\"" + val + "\""); }
+            }
+        }
+
+        var formatted = "<" + tagName + (pairs.length > 0 ? " " : "");
+        var content = element.innerHTML;
+
+        if (content.length > 20) {
+            content = content.substr(0, 20) + "[...]";
+        }
+
+        var res = formatted + pairs.join(" ") + ">" + content +
+                "</" + tagName + ">";
+
+        return res.replace(/ contentEditable="inherit"/, "");
+    };
+
+    function Formatio(options) {
+        for (var opt in options) {
+            this[opt] = options[opt];
+        }
+    }
+
+    Formatio.prototype = {
+        functionName: functionName,
+
+        configure: function (options) {
+            return new Formatio(options);
+        },
+
+        constructorName: function (object) {
+            return constructorName(this, object);
+        },
+
+        ascii: function (object, processed, indent) {
+            return ascii(this, object, processed, indent);
+        }
+    };
+
+    return Formatio.prototype;
+});
+/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/
+/*global module, require, __dirname, document*/
+/**
+ * Sinon core utilities. For internal use only.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+var sinon = (function (formatio) {
+    var div = typeof document != "undefined" && document.createElement("div");
+    var hasOwn = Object.prototype.hasOwnProperty;
+
+    function isDOMNode(obj) {
+        var success = false;
+
+        try {
+            obj.appendChild(div);
+            success = div.parentNode == obj;
+        } catch (e) {
+            return false;
+        } finally {
+            try {
+                obj.removeChild(div);
+            } catch (e) {
+                // Remove failed, not much we can do about that
+            }
+        }
+
+        return success;
+    }
+
+    function isElement(obj) {
+        return div && obj && obj.nodeType === 1 && isDOMNode(obj);
+    }
+
+    function isFunction(obj) {
+        return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply);
+    }
+
+    function isReallyNaN(val) {
+        return typeof val === 'number' && isNaN(val);
+    }
+
+    function mirrorProperties(target, source) {
+        for (var prop in source) {
+            if (!hasOwn.call(target, prop)) {
+                target[prop] = source[prop];
+            }
+        }
+    }
+
+    function isRestorable (obj) {
+        return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon;
+    }
+
+    var sinon = {
+        wrapMethod: function wrapMethod(object, property, method) {
+            if (!object) {
+                throw new TypeError("Should wrap property of object");
+            }
+
+            if (typeof method != "function") {
+                throw new TypeError("Method wrapper should be function");
+            }
+
+            var wrappedMethod = object[property],
+                error;
+
+            if (!isFunction(wrappedMethod)) {
+                error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " +
+                                    property + " as function");
+            } else if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
+                error = new TypeError("Attempted to wrap " + property + " which is already wrapped");
+            } else if (wrappedMethod.calledBefore) {
+                var verb = !!wrappedMethod.returns ? "stubbed" : "spied on";
+                error = new TypeError("Attempted to wrap " + property + " which is already " + verb);
+            }
+
+            if (error) {
+                if (wrappedMethod && wrappedMethod._stack) {
+                    error.stack += '\n--------------\n' + wrappedMethod._stack;
+                }
+                throw error;
+            }
+
+            // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem
+            // when using hasOwn.call on objects from other frames.
+            var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property);
+            object[property] = method;
+            method.displayName = property;
+            // Set up a stack trace which can be used later to find what line of
+            // code the original method was created on.
+            method._stack = (new Error('Stack Trace for original')).stack;
+
+            method.restore = function () {
+                // For prototype properties try to reset by delete first.
+                // If this fails (ex: localStorage on mobile safari) then force a reset
+                // via direct assignment.
+                if (!owned) {
+                    delete object[property];
+                }
+                if (object[property] === method) {
+                    object[property] = wrappedMethod;
+                }
+            };
+
+            method.restore.sinon = true;
+            mirrorProperties(method, wrappedMethod);
+
+            return method;
+        },
+
+        extend: function extend(target) {
+            for (var i = 1, l = arguments.length; i < l; i += 1) {
+                for (var prop in arguments[i]) {
+                    if (arguments[i].hasOwnProperty(prop)) {
+                        target[prop] = arguments[i][prop];
+                    }
+
+                    // DONT ENUM bug, only care about toString
+                    if (arguments[i].hasOwnProperty("toString") &&
+                        arguments[i].toString != target.toString) {
+                        target.toString = arguments[i].toString;
+                    }
+                }
+            }
+
+            return target;
+        },
+
+        create: function create(proto) {
+            var F = function () {};
+            F.prototype = proto;
+            return new F();
+        },
+
+        deepEqual: function deepEqual(a, b) {
+            if (sinon.match && sinon.match.isMatcher(a)) {
+                return a.test(b);
+            }
+
+            if (typeof a != 'object' || typeof b != 'object') {
+                if (isReallyNaN(a) && isReallyNaN(b)) {
+                    return true;
+                } else {
+                    return a === b;
+                }
+            }
+
+            if (isElement(a) || isElement(b)) {
+                return a === b;
+            }
+
+            if (a === b) {
+                return true;
+            }
+
+            if ((a === null && b !== null) || (a !== null && b === null)) {
+                return false;
+            }
+
+            if (a instanceof RegExp && b instanceof RegExp) {
+              return (a.source === b.source) && (a.global === b.global) &&
+                (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline);
+            }
+
+            var aString = Object.prototype.toString.call(a);
+            if (aString != Object.prototype.toString.call(b)) {
+                return false;
+            }
+
+            if (aString == "[object Date]") {
+                return a.valueOf() === b.valueOf();
+            }
+
+            var prop, aLength = 0, bLength = 0;
+
+            if (aString == "[object Array]" && a.length !== b.length) {
+                return false;
+            }
+
+            for (prop in a) {
+                aLength += 1;
+
+                if (!(prop in b)) {
+                    return false;
+                }
+
+                if (!deepEqual(a[prop], b[prop])) {
+                    return false;
+                }
+            }
+
+            for (prop in b) {
+                bLength += 1;
+            }
+
+            return aLength == bLength;
+        },
+
+        functionName: function functionName(func) {
+            var name = func.displayName || func.name;
+
+            // Use function decomposition as a last resort to get function
+            // name. Does not rely on function decomposition to work - if it
+            // doesn't debugging will be slightly less informative
+            // (i.e. toString will say 'spy' rather than 'myFunc').
+            if (!name) {
+                var matches = func.toString().match(/function ([^\s\(]+)/);
+                name = matches && matches[1];
+            }
+
+            return name;
+        },
+
+        functionToString: function toString() {
+            if (this.getCall && this.callCount) {
+                var thisValue, prop, i = this.callCount;
+
+                while (i--) {
+                    thisValue = this.getCall(i).thisValue;
+
+                    for (prop in thisValue) {
+                        if (thisValue[prop] === this) {
+                            return prop;
+                        }
+                    }
+                }
+            }
+
+            return this.displayName || "sinon fake";
+        },
+
+        getConfig: function (custom) {
+            var config = {};
+            custom = custom || {};
+            var defaults = sinon.defaultConfig;
+
+            for (var prop in defaults) {
+                if (defaults.hasOwnProperty(prop)) {
+                    config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop];
+                }
+            }
+
+            return config;
+        },
+
+        format: function (val) {
+            return "" + val;
+        },
+
+        defaultConfig: {
+            injectIntoThis: true,
+            injectInto: null,
+            properties: ["spy", "stub", "mock", "clock", "server", "requests"],
+            useFakeTimers: true,
+            useFakeServer: true
+        },
+
+        timesInWords: function timesInWords(count) {
+            return count == 1 && "once" ||
+                count == 2 && "twice" ||
+                count == 3 && "thrice" ||
+                (count || 0) + " times";
+        },
+
+        calledInOrder: function (spies) {
+            for (var i = 1, l = spies.length; i < l; i++) {
+                if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) {
+                    return false;
+                }
+            }
+
+            return true;
+        },
+
+        orderByFirstCall: function (spies) {
+            return spies.sort(function (a, b) {
+                // uuid, won't ever be equal
+                var aCall = a.getCall(0);
+                var bCall = b.getCall(0);
+                var aId = aCall && aCall.callId || -1;
+                var bId = bCall && bCall.callId || -1;
+
+                return aId < bId ? -1 : 1;
+            });
+        },
+
+        log: function () {},
+
+        logError: function (label, err) {
+            var msg = label + " threw exception: ";
+            sinon.log(msg + "[" + err.name + "] " + err.message);
+            if (err.stack) { sinon.log(err.stack); }
+
+            setTimeout(function () {
+                err.message = msg + err.message;
+                throw err;
+            }, 0);
+        },
+
+        typeOf: function (value) {
+            if (value === null) {
+                return "null";
+            }
+            else if (value === undefined) {
+                return "undefined";
+            }
+            var string = Object.prototype.toString.call(value);
+            return string.substring(8, string.length - 1).toLowerCase();
+        },
+
+        createStubInstance: function (constructor) {
+            if (typeof constructor !== "function") {
+                throw new TypeError("The constructor should be a function.");
+            }
+            return sinon.stub(sinon.create(constructor.prototype));
+        },
+
+        restore: function (object) {
+            if (object !== null && typeof object === "object") {
+                for (var prop in object) {
+                    if (isRestorable(object[prop])) {
+                        object[prop].restore();
+                    }
+                }
+            }
+            else if (isRestorable(object)) {
+                object.restore();
+            }
+        }
+    };
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd;
+
+    function makePublicAPI(require, exports, module) {
+        module.exports = sinon;
+        sinon.spy = require("./sinon/spy");
+        sinon.spyCall = require("./sinon/call");
+        sinon.behavior = require("./sinon/behavior");
+        sinon.stub = require("./sinon/stub");
+        sinon.mock = require("./sinon/mock");
+        sinon.collection = require("./sinon/collection");
+        sinon.assert = require("./sinon/assert");
+        sinon.sandbox = require("./sinon/sandbox");
+        sinon.test = require("./sinon/test");
+        sinon.testCase = require("./sinon/test_case");
+        sinon.match = require("./sinon/match");
+    }
+
+    if (isAMD) {
+        define(makePublicAPI);
+    } else if (isNode) {
+        try {
+            formatio = require("formatio");
+        } catch (e) {}
+        makePublicAPI(require, exports, module);
+    }
+
+    if (formatio) {
+        var formatter = formatio.configure({ quoteStrings: false });
+        sinon.format = function () {
+            return formatter.ascii.apply(formatter, arguments);
+        };
+    } else if (isNode) {
+        try {
+            var util = require("util");
+            sinon.format = function (value) {
+                return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value;
+            };
+        } catch (e) {
+            /* Node, but no util module - would be very old, but better safe than
+             sorry */
+        }
+    }
+
+    return sinon;
+}(typeof formatio == "object" && formatio));
+
+/* @depend ../sinon.js */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+ * Match functions
+ *
+ * @author Maximilian Antoni (mail@maxantoni.de)
+ * @license BSD
+ *
+ * Copyright (c) 2012 Maximilian Antoni
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function assertType(value, type, name) {
+        var actual = sinon.typeOf(value);
+        if (actual !== type) {
+            throw new TypeError("Expected type of " + name + " to be " +
+                type + ", but was " + actual);
+        }
+    }
+
+    var matcher = {
+        toString: function () {
+            return this.message;
+        }
+    };
+
+    function isMatcher(object) {
+        return matcher.isPrototypeOf(object);
+    }
+
+    function matchObject(expectation, actual) {
+        if (actual === null || actual === undefined) {
+            return false;
+        }
+        for (var key in expectation) {
+            if (expectation.hasOwnProperty(key)) {
+                var exp = expectation[key];
+                var act = actual[key];
+                if (match.isMatcher(exp)) {
+                    if (!exp.test(act)) {
+                        return false;
+                    }
+                } else if (sinon.typeOf(exp) === "object") {
+                    if (!matchObject(exp, act)) {
+                        return false;
+                    }
+                } else if (!sinon.deepEqual(exp, act)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    matcher.or = function (m2) {
+        if (!arguments.length) {
+            throw new TypeError("Matcher expected");
+        } else if (!isMatcher(m2)) {
+            m2 = match(m2);
+        }
+        var m1 = this;
+        var or = sinon.create(matcher);
+        or.test = function (actual) {
+            return m1.test(actual) || m2.test(actual);
+        };
+        or.message = m1.message + ".or(" + m2.message + ")";
+        return or;
+    };
+
+    matcher.and = function (m2) {
+        if (!arguments.length) {
+            throw new TypeError("Matcher expected");
+        } else if (!isMatcher(m2)) {
+            m2 = match(m2);
+        }
+        var m1 = this;
+        var and = sinon.create(matcher);
+        and.test = function (actual) {
+            return m1.test(actual) && m2.test(actual);
+        };
+        and.message = m1.message + ".and(" + m2.message + ")";
+        return and;
+    };
+
+    var match = function (expectation, message) {
+        var m = sinon.create(matcher);
+        var type = sinon.typeOf(expectation);
+        switch (type) {
+        case "object":
+            if (typeof expectation.test === "function") {
+                m.test = function (actual) {
+                    return expectation.test(actual) === true;
+                };
+                m.message = "match(" + sinon.functionName(expectation.test) + ")";
+                return m;
+            }
+            var str = [];
+            for (var key in expectation) {
+                if (expectation.hasOwnProperty(key)) {
+                    str.push(key + ": " + expectation[key]);
+                }
+            }
+            m.test = function (actual) {
+                return matchObject(expectation, actual);
+            };
+            m.message = "match(" + str.join(", ") + ")";
+            break;
+        case "number":
+            m.test = function (actual) {
+                return expectation == actual;
+            };
+            break;
+        case "string":
+            m.test = function (actual) {
+                if (typeof actual !== "string") {
+                    return false;
+                }
+                return actual.indexOf(expectation) !== -1;
+            };
+            m.message = "match(\"" + expectation + "\")";
+            break;
+        case "regexp":
+            m.test = function (actual) {
+                if (typeof actual !== "string") {
+                    return false;
+                }
+                return expectation.test(actual);
+            };
+            break;
+        case "function":
+            m.test = expectation;
+            if (message) {
+                m.message = message;
+            } else {
+                m.message = "match(" + sinon.functionName(expectation) + ")";
+            }
+            break;
+        default:
+            m.test = function (actual) {
+              return sinon.deepEqual(expectation, actual);
+            };
+        }
+        if (!m.message) {
+            m.message = "match(" + expectation + ")";
+        }
+        return m;
+    };
+
+    match.isMatcher = isMatcher;
+
+    match.any = match(function () {
+        return true;
+    }, "any");
+
+    match.defined = match(function (actual) {
+        return actual !== null && actual !== undefined;
+    }, "defined");
+
+    match.truthy = match(function (actual) {
+        return !!actual;
+    }, "truthy");
+
+    match.falsy = match(function (actual) {
+        return !actual;
+    }, "falsy");
+
+    match.same = function (expectation) {
+        return match(function (actual) {
+            return expectation === actual;
+        }, "same(" + expectation + ")");
+    };
+
+    match.typeOf = function (type) {
+        assertType(type, "string", "type");
+        return match(function (actual) {
+            return sinon.typeOf(actual) === type;
+        }, "typeOf(\"" + type + "\")");
+    };
+
+    match.instanceOf = function (type) {
+        assertType(type, "function", "type");
+        return match(function (actual) {
+            return actual instanceof type;
+        }, "instanceOf(" + sinon.functionName(type) + ")");
+    };
+
+    function createPropertyMatcher(propertyTest, messagePrefix) {
+        return function (property, value) {
+            assertType(property, "string", "property");
+            var onlyProperty = arguments.length === 1;
+            var message = messagePrefix + "(\"" + property + "\"";
+            if (!onlyProperty) {
+                message += ", " + value;
+            }
+            message += ")";
+            return match(function (actual) {
+                if (actual === undefined || actual === null ||
+                        !propertyTest(actual, property)) {
+                    return false;
+                }
+                return onlyProperty || sinon.deepEqual(value, actual[property]);
+            }, message);
+        };
+    }
+
+    match.has = createPropertyMatcher(function (actual, property) {
+        if (typeof actual === "object") {
+            return property in actual;
+        }
+        return actual[property] !== undefined;
+    }, "has");
+
+    match.hasOwn = createPropertyMatcher(function (actual, property) {
+        return actual.hasOwnProperty(property);
+    }, "hasOwn");
+
+    match.bool = match.typeOf("boolean");
+    match.number = match.typeOf("number");
+    match.string = match.typeOf("string");
+    match.object = match.typeOf("object");
+    match.func = match.typeOf("function");
+    match.array = match.typeOf("array");
+    match.regexp = match.typeOf("regexp");
+    match.date = match.typeOf("date");
+
+    sinon.match = match;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = match; });
+    } else if (commonJSModule) {
+        module.exports = match;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+  * @depend ../sinon.js
+  * @depend match.js
+  */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+  * Spy calls
+  *
+  * @author Christian Johansen (christian@cjohansen.no)
+  * @author Maximilian Antoni (mail@maxantoni.de)
+  * @license BSD
+  *
+  * Copyright (c) 2010-2013 Christian Johansen
+  * Copyright (c) 2013 Maximilian Antoni
+  */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function throwYieldError(proxy, text, args) {
+        var msg = sinon.functionName(proxy) + text;
+        if (args.length) {
+            msg += " Received [" + slice.call(args).join(", ") + "]";
+        }
+        throw new Error(msg);
+    }
+
+    var slice = Array.prototype.slice;
+
+    var callProto = {
+        calledOn: function calledOn(thisValue) {
+            if (sinon.match && sinon.match.isMatcher(thisValue)) {
+                return thisValue.test(this.thisValue);
+            }
+            return this.thisValue === thisValue;
+        },
+
+        calledWith: function calledWith() {
+            for (var i = 0, l = arguments.length; i < l; i += 1) {
+                if (!sinon.deepEqual(arguments[i], this.args[i])) {
+                    return false;
+                }
+            }
+
+            return true;
+        },
+
+        calledWithMatch: function calledWithMatch() {
+            for (var i = 0, l = arguments.length; i < l; i += 1) {
+                var actual = this.args[i];
+                var expectation = arguments[i];
+                if (!sinon.match || !sinon.match(expectation).test(actual)) {
+                    return false;
+                }
+            }
+            return true;
+        },
+
+        calledWithExactly: function calledWithExactly() {
+            return arguments.length == this.args.length &&
+                this.calledWith.apply(this, arguments);
+        },
+
+        notCalledWith: function notCalledWith() {
+            return !this.calledWith.apply(this, arguments);
+        },
+
+        notCalledWithMatch: function notCalledWithMatch() {
+            return !this.calledWithMatch.apply(this, arguments);
+        },
+
+        returned: function returned(value) {
+            return sinon.deepEqual(value, this.returnValue);
+        },
+
+        threw: function threw(error) {
+            if (typeof error === "undefined" || !this.exception) {
+                return !!this.exception;
+            }
+
+            return this.exception === error || this.exception.name === error;
+        },
+
+        calledWithNew: function calledWithNew() {
+            return this.proxy.prototype && this.thisValue instanceof this.proxy;
+        },
+
+        calledBefore: function (other) {
+            return this.callId < other.callId;
+        },
+
+        calledAfter: function (other) {
+            return this.callId > other.callId;
+        },
+
+        callArg: function (pos) {
+            this.args[pos]();
+        },
+
+        callArgOn: function (pos, thisValue) {
+            this.args[pos].apply(thisValue);
+        },
+
+        callArgWith: function (pos) {
+            this.callArgOnWith.apply(this, [pos, null].concat(slice.call(arguments, 1)));
+        },
+
+        callArgOnWith: function (pos, thisValue) {
+            var args = slice.call(arguments, 2);
+            this.args[pos].apply(thisValue, args);
+        },
+
+        "yield": function () {
+            this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0)));
+        },
+
+        yieldOn: function (thisValue) {
+            var args = this.args;
+            for (var i = 0, l = args.length; i < l; ++i) {
+                if (typeof args[i] === "function") {
+                    args[i].apply(thisValue, slice.call(arguments, 1));
+                    return;
+                }
+            }
+            throwYieldError(this.proxy, " cannot yield since no callback was passed.", args);
+        },
+
+        yieldTo: function (prop) {
+            this.yieldToOn.apply(this, [prop, null].concat(slice.call(arguments, 1)));
+        },
+
+        yieldToOn: function (prop, thisValue) {
+            var args = this.args;
+            for (var i = 0, l = args.length; i < l; ++i) {
+                if (args[i] && typeof args[i][prop] === "function") {
+                    args[i][prop].apply(thisValue, slice.call(arguments, 2));
+                    return;
+                }
+            }
+            throwYieldError(this.proxy, " cannot yield to '" + prop +
+                "' since no callback was passed.", args);
+        },
+
+        toString: function () {
+            var callStr = this.proxy.toString() + "(";
+            var args = [];
+
+            for (var i = 0, l = this.args.length; i < l; ++i) {
+                args.push(sinon.format(this.args[i]));
+            }
+
+            callStr = callStr + args.join(", ") + ")";
+
+            if (typeof this.returnValue != "undefined") {
+                callStr += " => " + sinon.format(this.returnValue);
+            }
+
+            if (this.exception) {
+                callStr += " !" + this.exception.name;
+
+                if (this.exception.message) {
+                    callStr += "(" + this.exception.message + ")";
+                }
+            }
+
+            return callStr;
+        }
+    };
+
+    callProto.invokeCallback = callProto.yield;
+
+    function createSpyCall(spy, thisValue, args, returnValue, exception, id) {
+        if (typeof id !== "number") {
+            throw new TypeError("Call id is not a number");
+        }
+        var proxyCall = sinon.create(callProto);
+        proxyCall.proxy = spy;
+        proxyCall.thisValue = thisValue;
+        proxyCall.args = args;
+        proxyCall.returnValue = returnValue;
+        proxyCall.exception = exception;
+        proxyCall.callId = id;
+
+        return proxyCall;
+    }
+    createSpyCall.toString = callProto.toString; // used by mocks
+
+    sinon.spyCall = createSpyCall;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = createSpyCall; });
+    } else if (commonJSModule) {
+        module.exports = createSpyCall;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+
+/**
+  * @depend ../sinon.js
+  * @depend call.js
+  */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+  * Spy functions
+  *
+  * @author Christian Johansen (christian@cjohansen.no)
+  * @license BSD
+  *
+  * Copyright (c) 2010-2013 Christian Johansen
+  */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var push = Array.prototype.push;
+    var slice = Array.prototype.slice;
+    var callId = 0;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function spy(object, property) {
+        if (!property && typeof object == "function") {
+            return spy.create(object);
+        }
+
+        if (!object && !property) {
+            return spy.create(function () { });
+        }
+
+        var method = object[property];
+        return sinon.wrapMethod(object, property, spy.create(method));
+    }
+
+    function matchingFake(fakes, args, strict) {
+        if (!fakes) {
+            return;
+        }
+
+        for (var i = 0, l = fakes.length; i < l; i++) {
+            if (fakes[i].matches(args, strict)) {
+                return fakes[i];
+            }
+        }
+    }
+
+    function incrementCallCount() {
+        this.called = true;
+        this.callCount += 1;
+        this.notCalled = false;
+        this.calledOnce = this.callCount == 1;
+        this.calledTwice = this.callCount == 2;
+        this.calledThrice = this.callCount == 3;
+    }
+
+    function createCallProperties() {
+        this.firstCall = this.getCall(0);
+        this.secondCall = this.getCall(1);
+        this.thirdCall = this.getCall(2);
+        this.lastCall = this.getCall(this.callCount - 1);
+    }
+
+    var vars = "a,b,c,d,e,f,g,h,i,j,k,l";
+    function createProxy(func) {
+        // Retain the function length:
+        var p;
+        if (func.length) {
+            eval("p = (function proxy(" + vars.substring(0, func.length * 2 - 1) +
+                ") { return p.invoke(func, this, slice.call(arguments)); });");
+        }
+        else {
+            p = function proxy() {
+                return p.invoke(func, this, slice.call(arguments));
+            };
+        }
+        return p;
+    }
+
+    var uuid = 0;
+
+    // Public API
+    var spyApi = {
+        reset: function () {
+            this.called = false;
+            this.notCalled = true;
+            this.calledOnce = false;
+            this.calledTwice = false;
+            this.calledThrice = false;
+            this.callCount = 0;
+            this.firstCall = null;
+            this.secondCall = null;
+            this.thirdCall = null;
+            this.lastCall = null;
+            this.args = [];
+            this.returnValues = [];
+            this.thisValues = [];
+            this.exceptions = [];
+            this.callIds = [];
+            if (this.fakes) {
+                for (var i = 0; i < this.fakes.length; i++) {
+                    this.fakes[i].reset();
+                }
+            }
+        },
+
+        create: function create(func) {
+            var name;
+
+            if (typeof func != "function") {
+                func = function () { };
+            } else {
+                name = sinon.functionName(func);
+            }
+
+            var proxy = createProxy(func);
+
+            sinon.extend(proxy, spy);
+            delete proxy.create;
+            sinon.extend(proxy, func);
+
+            proxy.reset();
+            proxy.prototype = func.prototype;
+            proxy.displayName = name || "spy";
+            proxy.toString = sinon.functionToString;
+            proxy._create = sinon.spy.create;
+            proxy.id = "spy#" + uuid++;
+
+            return proxy;
+        },
+
+        invoke: function invoke(func, thisValue, args) {
+            var matching = matchingFake(this.fakes, args);
+            var exception, returnValue;
+
+            incrementCallCount.call(this);
+            push.call(this.thisValues, thisValue);
+            push.call(this.args, args);
+            push.call(this.callIds, callId++);
+
+            // Make call properties available from within the spied function:
+            createCallProperties.call(this);
+
+            try {
+                if (matching) {
+                    returnValue = matching.invoke(func, thisValue, args);
+                } else {
+                    returnValue = (this.func || func).apply(thisValue, args);
+                }
+
+                var thisCall = this.getCall(this.callCount - 1);
+                if (thisCall.calledWithNew() && typeof returnValue !== 'object') {
+                    returnValue = thisValue;
+                }
+            } catch (e) {
+                exception = e;
+            }
+
+            push.call(this.exceptions, exception);
+            push.call(this.returnValues, returnValue);
+
+            // Make return value and exception available in the calls:
+            createCallProperties.call(this);
+
+            if (exception !== undefined) {
+                throw exception;
+            }
+
+            return returnValue;
+        },
+
+        named: function named(name) {
+            this.displayName = name;
+            return this;
+        },
+
+        getCall: function getCall(i) {
+            if (i < 0 || i >= this.callCount) {
+                return null;
+            }
+
+            return sinon.spyCall(this, this.thisValues[i], this.args[i],
+                                    this.returnValues[i], this.exceptions[i],
+                                    this.callIds[i]);
+        },
+
+        getCalls: function () {
+            var calls = [];
+            var i;
+
+            for (i = 0; i < this.callCount; i++) {
+                calls.push(this.getCall(i));
+            }
+
+            return calls;
+        },
+
+        calledBefore: function calledBefore(spyFn) {
+            if (!this.called) {
+                return false;
+            }
+
+            if (!spyFn.called) {
+                return true;
+            }
+
+            return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1];
+        },
+
+        calledAfter: function calledAfter(spyFn) {
+            if (!this.called || !spyFn.called) {
+                return false;
+            }
+
+            return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1];
+        },
+
+        withArgs: function () {
+            var args = slice.call(arguments);
+
+            if (this.fakes) {
+                var match = matchingFake(this.fakes, args, true);
+
+                if (match) {
+                    return match;
+                }
+            } else {
+                this.fakes = [];
+            }
+
+            var original = this;
+            var fake = this._create();
+            fake.matchingAguments = args;
+            fake.parent = this;
+            push.call(this.fakes, fake);
+
+            fake.withArgs = function () {
+                return original.withArgs.apply(original, arguments);
+            };
+
+            for (var i = 0; i < this.args.length; i++) {
+                if (fake.matches(this.args[i])) {
+                    incrementCallCount.call(fake);
+                    push.call(fake.thisValues, this.thisValues[i]);
+                    push.call(fake.args, this.args[i]);
+                    push.call(fake.returnValues, this.returnValues[i]);
+                    push.call(fake.exceptions, this.exceptions[i]);
+                    push.call(fake.callIds, this.callIds[i]);
+                }
+            }
+            createCallProperties.call(fake);
+
+            return fake;
+        },
+
+        matches: function (args, strict) {
+            var margs = this.matchingAguments;
+
+            if (margs.length <= args.length &&
+                sinon.deepEqual(margs, args.slice(0, margs.length))) {
+                return !strict || margs.length == args.length;
+            }
+        },
+
+        printf: function (format) {
+            var spy = this;
+            var args = slice.call(arguments, 1);
+            var formatter;
+
+            return (format || "").replace(/%(.)/g, function (match, specifyer) {
+                formatter = spyApi.formatters[specifyer];
+
+                if (typeof formatter == "function") {
+                    return formatter.call(null, spy, args);
+                } else if (!isNaN(parseInt(specifyer, 10))) {
+                    return sinon.format(args[specifyer - 1]);
+                }
+
+                return "%" + specifyer;
+            });
+        }
+    };
+
+    function delegateToCalls(method, matchAny, actual, notCalled) {
+        spyApi[method] = function () {
+            if (!this.called) {
+                if (notCalled) {
+                    return notCalled.apply(this, arguments);
+                }
+                return false;
+            }
+
+            var currentCall;
+            var matches = 0;
+
+            for (var i = 0, l = this.callCount; i < l; i += 1) {
+                currentCall = this.getCall(i);
+
+                if (currentCall[actual || method].apply(currentCall, arguments)) {
+                    matches += 1;
+
+                    if (matchAny) {
+                        return true;
+                    }
+                }
+            }
+
+            return matches === this.callCount;
+        };
+    }
+
+    delegateToCalls("calledOn", true);
+    delegateToCalls("alwaysCalledOn", false, "calledOn");
+    delegateToCalls("calledWith", true);
+    delegateToCalls("calledWithMatch", true);
+    delegateToCalls("alwaysCalledWith", false, "calledWith");
+    delegateToCalls("alwaysCalledWithMatch", false, "calledWithMatch");
+    delegateToCalls("calledWithExactly", true);
+    delegateToCalls("alwaysCalledWithExactly", false, "calledWithExactly");
+    delegateToCalls("neverCalledWith", false, "notCalledWith",
+        function () { return true; });
+    delegateToCalls("neverCalledWithMatch", false, "notCalledWithMatch",
+        function () { return true; });
+    delegateToCalls("threw", true);
+    delegateToCalls("alwaysThrew", false, "threw");
+    delegateToCalls("returned", true);
+    delegateToCalls("alwaysReturned", false, "returned");
+    delegateToCalls("calledWithNew", true);
+    delegateToCalls("alwaysCalledWithNew", false, "calledWithNew");
+    delegateToCalls("callArg", false, "callArgWith", function () {
+        throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
+    });
+    spyApi.callArgWith = spyApi.callArg;
+    delegateToCalls("callArgOn", false, "callArgOnWith", function () {
+        throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
+    });
+    spyApi.callArgOnWith = spyApi.callArgOn;
+    delegateToCalls("yield", false, "yield", function () {
+        throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
+    });
+    // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode.
+    spyApi.invokeCallback = spyApi.yield;
+    delegateToCalls("yieldOn", false, "yieldOn", function () {
+        throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
+    });
+    delegateToCalls("yieldTo", false, "yieldTo", function (property) {
+        throw new Error(this.toString() + " cannot yield to '" + property +
+            "' since it was not yet invoked.");
+    });
+    delegateToCalls("yieldToOn", false, "yieldToOn", function (property) {
+        throw new Error(this.toString() + " cannot yield to '" + property +
+            "' since it was not yet invoked.");
+    });
+
+    spyApi.formatters = {
+        "c": function (spy) {
+            return sinon.timesInWords(spy.callCount);
+        },
+
+        "n": function (spy) {
+            return spy.toString();
+        },
+
+        "C": function (spy) {
+            var calls = [];
+
+            for (var i = 0, l = spy.callCount; i < l; ++i) {
+                var stringifiedCall = "    " + spy.getCall(i).toString();
+                if (/\n/.test(calls[i - 1])) {
+                    stringifiedCall = "\n" + stringifiedCall;
+                }
+                push.call(calls, stringifiedCall);
+            }
+
+            return calls.length > 0 ? "\n" + calls.join("\n") : "";
+        },
+
+        "t": function (spy) {
+            var objects = [];
+
+            for (var i = 0, l = spy.callCount; i < l; ++i) {
+                push.call(objects, sinon.format(spy.thisValues[i]));
+            }
+
+            return objects.join(", ");
+        },
+
+        "*": function (spy, args) {
+            var formatted = [];
+
+            for (var i = 0, l = args.length; i < l; ++i) {
+                push.call(formatted, sinon.format(args[i]));
+            }
+
+            return formatted.join(", ");
+        }
+    };
+
+    sinon.extend(spy, spyApi);
+
+    spy.spyCall = sinon.spyCall;
+    sinon.spy = spy;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = spy; });
+    } else if (commonJSModule) {
+        module.exports = spy;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global module, require, sinon, process, setImmediate, setTimeout*/
+/**
+ * Stub behavior
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Tim Fischbach (mail@timfischbach.de)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    var slice = Array.prototype.slice;
+    var join = Array.prototype.join;
+    var proto;
+
+    var nextTick = (function () {
+        if (typeof process === "object" && typeof process.nextTick === "function") {
+            return process.nextTick;
+        } else if (typeof setImmediate === "function") {
+            return setImmediate;
+        } else {
+            return function (callback) {
+                setTimeout(callback, 0);
+            };
+        }
+    })();
+
+    function throwsException(error, message) {
+        if (typeof error == "string") {
+            this.exception = new Error(message || "");
+            this.exception.name = error;
+        } else if (!error) {
+            this.exception = new Error("Error");
+        } else {
+            this.exception = error;
+        }
+
+        return this;
+    }
+
+    function getCallback(behavior, args) {
+        var callArgAt = behavior.callArgAt;
+
+        if (callArgAt < 0) {
+            var callArgProp = behavior.callArgProp;
+
+            for (var i = 0, l = args.length; i < l; ++i) {
+                if (!callArgProp && typeof args[i] == "function") {
+                    return args[i];
+                }
+
+                if (callArgProp && args[i] &&
+                    typeof args[i][callArgProp] == "function") {
+                    return args[i][callArgProp];
+                }
+            }
+
+            return null;
+        }
+
+        return args[callArgAt];
+    }
+
+    function getCallbackError(behavior, func, args) {
+        if (behavior.callArgAt < 0) {
+            var msg;
+
+            if (behavior.callArgProp) {
+                msg = sinon.functionName(behavior.stub) +
+                    " expected to yield to '" + behavior.callArgProp +
+                    "', but no object with such a property was passed.";
+            } else {
+                msg = sinon.functionName(behavior.stub) +
+                    " expected to yield, but no callback was passed.";
+            }
+
+            if (args.length > 0) {
+                msg += " Received [" + join.call(args, ", ") + "]";
+            }
+
+            return msg;
+        }
+
+        return "argument at index " + behavior.callArgAt + " is not a function: " + func;
+    }
+
+    function callCallback(behavior, args) {
+        if (typeof behavior.callArgAt == "number") {
+            var func = getCallback(behavior, args);
+
+            if (typeof func != "function") {
+                throw new TypeError(getCallbackError(behavior, func, args));
+            }
+
+            if (behavior.callbackAsync) {
+                nextTick(function() {
+                    func.apply(behavior.callbackContext, behavior.callbackArguments);
+                });
+            } else {
+                func.apply(behavior.callbackContext, behavior.callbackArguments);
+            }
+        }
+    }
+
+    proto = {
+        create: function(stub) {
+            var behavior = sinon.extend({}, sinon.behavior);
+            delete behavior.create;
+            behavior.stub = stub;
+
+            return behavior;
+        },
+
+        isPresent: function() {
+            return (typeof this.callArgAt == 'number' ||
+                    this.exception ||
+                    typeof this.returnArgAt == 'number' ||
+                    this.returnThis ||
+                    this.returnValueDefined);
+        },
+
+        invoke: function(context, args) {
+            callCallback(this, args);
+
+            if (this.exception) {
+                throw this.exception;
+            } else if (typeof this.returnArgAt == 'number') {
+                return args[this.returnArgAt];
+            } else if (this.returnThis) {
+                return context;
+            }
+
+            return this.returnValue;
+        },
+
+        onCall: function(index) {
+            return this.stub.onCall(index);
+        },
+
+        onFirstCall: function() {
+            return this.stub.onFirstCall();
+        },
+
+        onSecondCall: function() {
+            return this.stub.onSecondCall();
+        },
+
+        onThirdCall: function() {
+            return this.stub.onThirdCall();
+        },
+
+        withArgs: function(/* arguments */) {
+            throw new Error('Defining a stub by invoking "stub.onCall(...).withArgs(...)" is not supported. ' +
+                            'Use "stub.withArgs(...).onCall(...)" to define sequential behavior for calls with certain arguments.');
+        },
+
+        callsArg: function callsArg(pos) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = [];
+            this.callbackContext = undefined;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        callsArgOn: function callsArgOn(pos, context) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = [];
+            this.callbackContext = context;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        callsArgWith: function callsArgWith(pos) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = slice.call(arguments, 1);
+            this.callbackContext = undefined;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        callsArgOnWith: function callsArgWith(pos, context) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = slice.call(arguments, 2);
+            this.callbackContext = context;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yields: function () {
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 0);
+            this.callbackContext = undefined;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yieldsOn: function (context) {
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 1);
+            this.callbackContext = context;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yieldsTo: function (prop) {
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 1);
+            this.callbackContext = undefined;
+            this.callArgProp = prop;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yieldsToOn: function (prop, context) {
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 2);
+            this.callbackContext = context;
+            this.callArgProp = prop;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+
+        "throws": throwsException,
+        throwsException: throwsException,
+
+        returns: function returns(value) {
+            this.returnValue = value;
+            this.returnValueDefined = true;
+
+            return this;
+        },
+
+        returnsArg: function returnsArg(pos) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+
+            this.returnArgAt = pos;
+
+            return this;
+        },
+
+        returnsThis: function returnsThis() {
+            this.returnThis = true;
+
+            return this;
+        }
+    };
+
+    // create asynchronous versions of callsArg* and yields* methods
+    for (var method in proto) {
+        // need to avoid creating anotherasync versions of the newly added async methods
+        if (proto.hasOwnProperty(method) &&
+            method.match(/^(callsArg|yields)/) &&
+            !method.match(/Async/)) {
+            proto[method + 'Async'] = (function (syncFnName) {
+                return function () {
+                    var result = this[syncFnName].apply(this, arguments);
+                    this.callbackAsync = true;
+                    return result;
+                };
+            })(method);
+        }
+    }
+
+    sinon.behavior = proto;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = proto; });
+    } else if (commonJSModule) {
+        module.exports = proto;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend spy.js
+ * @depend behavior.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global module, require, sinon*/
+/**
+ * Stub functions
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function stub(object, property, func) {
+        if (!!func && typeof func != "function") {
+            throw new TypeError("Custom stub should be function");
+        }
+
+        var wrapper;
+
+        if (func) {
+            wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func;
+        } else {
+            wrapper = stub.create();
+        }
+
+        if (!object && typeof property === "undefined") {
+            return sinon.stub.create();
+        }
+
+        if (typeof property === "undefined" && typeof object == "object") {
+            for (var prop in object) {
+                if (typeof object[prop] === "function") {
+                    stub(object, prop);
+                }
+            }
+
+            return object;
+        }
+
+        return sinon.wrapMethod(object, property, wrapper);
+    }
+
+    function getDefaultBehavior(stub) {
+        return stub.defaultBehavior || getParentBehaviour(stub) || sinon.behavior.create(stub);
+    }
+
+    function getParentBehaviour(stub) {
+        return (stub.parent && getCurrentBehavior(stub.parent));
+    }
+
+    function getCurrentBehavior(stub) {
+        var behavior = stub.behaviors[stub.callCount - 1];
+        return behavior && behavior.isPresent() ? behavior : getDefaultBehavior(stub);
+    }
+
+    var uuid = 0;
+
+    sinon.extend(stub, (function () {
+        var proto = {
+            create: function create() {
+                var functionStub = function () {
+                    return getCurrentBehavior(functionStub).invoke(this, arguments);
+                };
+
+                functionStub.id = "stub#" + uuid++;
+                var orig = functionStub;
+                functionStub = sinon.spy.create(functionStub);
+                functionStub.func = orig;
+
+                sinon.extend(functionStub, stub);
+                functionStub._create = sinon.stub.create;
+                functionStub.displayName = "stub";
+                functionStub.toString = sinon.functionToString;
+
+                functionStub.defaultBehavior = null;
+                functionStub.behaviors = [];
+
+                return functionStub;
+            },
+
+            resetBehavior: function () {
+                var i;
+
+                this.defaultBehavior = null;
+                this.behaviors = [];
+
+                delete this.returnValue;
+                delete this.returnArgAt;
+                this.returnThis = false;
+
+                if (this.fakes) {
+                    for (i = 0; i < this.fakes.length; i++) {
+                        this.fakes[i].resetBehavior();
+                    }
+                }
+            },
+
+            onCall: function(index) {
+                if (!this.behaviors[index]) {
+                    this.behaviors[index] = sinon.behavior.create(this);
+                }
+
+                return this.behaviors[index];
+            },
+
+            onFirstCall: function() {
+                return this.onCall(0);
+            },
+
+            onSecondCall: function() {
+                return this.onCall(1);
+            },
+
+            onThirdCall: function() {
+                return this.onCall(2);
+            }
+        };
+
+        for (var method in sinon.behavior) {
+            if (sinon.behavior.hasOwnProperty(method) &&
+                !proto.hasOwnProperty(method) &&
+                method != 'create' &&
+                method != 'withArgs' &&
+                method != 'invoke') {
+                proto[method] = (function(behaviorMethod) {
+                    return function() {
+                        this.defaultBehavior = this.defaultBehavior || sinon.behavior.create(this);
+                        this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments);
+                        return this;
+                    };
+                }(method));
+            }
+        }
+
+        return proto;
+    }()));
+
+    sinon.stub = stub;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = stub; });
+    } else if (commonJSModule) {
+        module.exports = stub;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ */
+/*jslint eqeqeq: false, onevar: false, nomen: false*/
+/*global module, require, sinon*/
+/**
+ * Mock functions.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var push = [].push;
+    var match;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    match = sinon.match;
+
+    if (!match && commonJSModule) {
+        match = require("./match");
+    }
+
+    function mock(object) {
+        if (!object) {
+            return sinon.expectation.create("Anonymous mock");
+        }
+
+        return mock.create(object);
+    }
+
+    sinon.mock = mock;
+
+    sinon.extend(mock, (function () {
+        function each(collection, callback) {
+            if (!collection) {
+                return;
+            }
+
+            for (var i = 0, l = collection.length; i < l; i += 1) {
+                callback(collection[i]);
+            }
+        }
+
+        return {
+            create: function create(object) {
+                if (!object) {
+                    throw new TypeError("object is null");
+                }
+
+                var mockObject = sinon.extend({}, mock);
+                mockObject.object = object;
+                delete mockObject.create;
+
+                return mockObject;
+            },
+
+            expects: function expects(method) {
+                if (!method) {
+                    throw new TypeError("method is falsy");
+                }
+
+                if (!this.expectations) {
+                    this.expectations = {};
+                    this.proxies = [];
+                }
+
+                if (!this.expectations[method]) {
+                    this.expectations[method] = [];
+                    var mockObject = this;
+
+                    sinon.wrapMethod(this.object, method, function () {
+                        return mockObject.invokeMethod(method, this, arguments);
+                    });
+
+                    push.call(this.proxies, method);
+                }
+
+                var expectation = sinon.expectation.create(method);
+                push.call(this.expectations[method], expectation);
+
+                return expectation;
+            },
+
+            restore: function restore() {
+                var object = this.object;
+
+                each(this.proxies, function (proxy) {
+                    if (typeof object[proxy].restore == "function") {
+                        object[proxy].restore();
+                    }
+                });
+            },
+
+            verify: function verify() {
+                var expectations = this.expectations || {};
+                var messages = [], met = [];
+
+                each(this.proxies, function (proxy) {
+                    each(expectations[proxy], function (expectation) {
+                        if (!expectation.met()) {
+                            push.call(messages, expectation.toString());
+                        } else {
+                            push.call(met, expectation.toString());
+                        }
+                    });
+                });
+
+                this.restore();
+
+                if (messages.length > 0) {
+                    sinon.expectation.fail(messages.concat(met).join("\n"));
+                } else {
+                    sinon.expectation.pass(messages.concat(met).join("\n"));
+                }
+
+                return true;
+            },
+
+            invokeMethod: function invokeMethod(method, thisValue, args) {
+                var expectations = this.expectations && this.expectations[method];
+                var length = expectations && expectations.length || 0, i;
+
+                for (i = 0; i < length; i += 1) {
+                    if (!expectations[i].met() &&
+                        expectations[i].allowsCall(thisValue, args)) {
+                        return expectations[i].apply(thisValue, args);
+                    }
+                }
+
+                var messages = [], available, exhausted = 0;
+
+                for (i = 0; i < length; i += 1) {
+                    if (expectations[i].allowsCall(thisValue, args)) {
+                        available = available || expectations[i];
+                    } else {
+                        exhausted += 1;
+                    }
+                    push.call(messages, "    " + expectations[i].toString());
+                }
+
+                if (exhausted === 0) {
+                    return available.apply(thisValue, args);
+                }
+
+                messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({
+                    proxy: method,
+                    args: args
+                }));
+
+                sinon.expectation.fail(messages.join("\n"));
+            }
+        };
+    }()));
+
+    var times = sinon.timesInWords;
+
+    sinon.expectation = (function () {
+        var slice = Array.prototype.slice;
+        var _invoke = sinon.spy.invoke;
+
+        function callCountInWords(callCount) {
+            if (callCount == 0) {
+                return "never called";
+            } else {
+                return "called " + times(callCount);
+            }
+        }
+
+        function expectedCallCountInWords(expectation) {
+            var min = expectation.minCalls;
+            var max = expectation.maxCalls;
+
+            if (typeof min == "number" && typeof max == "number") {
+                var str = times(min);
+
+                if (min != max) {
+                    str = "at least " + str + " and at most " + times(max);
+                }
+
+                return str;
+            }
+
+            if (typeof min == "number") {
+                return "at least " + times(min);
+            }
+
+            return "at most " + times(max);
+        }
+
+        function receivedMinCalls(expectation) {
+            var hasMinLimit = typeof expectation.minCalls == "number";
+            return !hasMinLimit || expectation.callCount >= expectation.minCalls;
+        }
+
+        function receivedMaxCalls(expectation) {
+            if (typeof expectation.maxCalls != "number") {
+                return false;
+            }
+
+            return expectation.callCount == expectation.maxCalls;
+        }
+
+        function verifyMatcher(possibleMatcher, arg){
+            if (match && match.isMatcher(possibleMatcher)) {
+                return possibleMatcher.test(arg);
+            } else {
+                return true;
+            }
+        }
+
+        return {
+            minCalls: 1,
+            maxCalls: 1,
+
+            create: function create(methodName) {
+                var expectation = sinon.extend(sinon.stub.create(), sinon.expectation);
+                delete expectation.create;
+                expectation.method = methodName;
+
+                return expectation;
+            },
+
+            invoke: function invoke(func, thisValue, args) {
+                this.verifyCallAllowed(thisValue, args);
+
+                return _invoke.apply(this, arguments);
+            },
+
+            atLeast: function atLeast(num) {
+                if (typeof num != "number") {
+                    throw new TypeError("'" + num + "' is not number");
+                }
+
+                if (!this.limitsSet) {
+                    this.maxCalls = null;
+                    this.limitsSet = true;
+                }
+
+                this.minCalls = num;
+
+                return this;
+            },
+
+            atMost: function atMost(num) {
+                if (typeof num != "number") {
+                    throw new TypeError("'" + num + "' is not number");
+                }
+
+                if (!this.limitsSet) {
+                    this.minCalls = null;
+                    this.limitsSet = true;
+                }
+
+                this.maxCalls = num;
+
+                return this;
+            },
+
+            never: function never() {
+                return this.exactly(0);
+            },
+
+            once: function once() {
+                return this.exactly(1);
+            },
+
+            twice: function twice() {
+                return this.exactly(2);
+            },
+
+            thrice: function thrice() {
+                return this.exactly(3);
+            },
+
+            exactly: function exactly(num) {
+                if (typeof num != "number") {
+                    throw new TypeError("'" + num + "' is not a number");
+                }
+
+                this.atLeast(num);
+                return this.atMost(num);
+            },
+
+            met: function met() {
+                return !this.failed && receivedMinCalls(this);
+            },
+
+            verifyCallAllowed: function verifyCallAllowed(thisValue, args) {
+                if (receivedMaxCalls(this)) {
+                    this.failed = true;
+                    sinon.expectation.fail(this.method + " already called " + times(this.maxCalls));
+                }
+
+                if ("expectedThis" in this && this.expectedThis !== thisValue) {
+                    sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " +
+                        this.expectedThis);
+                }
+
+                if (!("expectedArguments" in this)) {
+                    return;
+                }
+
+                if (!args) {
+                    sinon.expectation.fail(this.method + " received no arguments, expected " +
+                        sinon.format(this.expectedArguments));
+                }
+
+                if (args.length < this.expectedArguments.length) {
+                    sinon.expectation.fail(this.method + " received too few arguments (" + sinon.format(args) +
+                        "), expected " + sinon.format(this.expectedArguments));
+                }
+
+                if (this.expectsExactArgCount &&
+                    args.length != this.expectedArguments.length) {
+                    sinon.expectation.fail(this.method + " received too many arguments (" + sinon.format(args) +
+                        "), expected " + sinon.format(this.expectedArguments));
+                }
+
+                for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) {
+
+                    if (!verifyMatcher(this.expectedArguments[i],args[i])) {
+                        sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) +
+                            ", didn't match " + this.expectedArguments.toString());
+                    }
+
+                    if (!sinon.deepEqual(this.expectedArguments[i], args[i])) {
+                        sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) +
+                            ", expected " + sinon.format(this.expectedArguments));
+                    }
+                }
+            },
+
+            allowsCall: function allowsCall(thisValue, args) {
+                if (this.met() && receivedMaxCalls(this)) {
+                    return false;
+                }
+
+                if ("expectedThis" in this && this.expectedThis !== thisValue) {
+                    return false;
+                }
+
+                if (!("expectedArguments" in this)) {
+                    return true;
+                }
+
+                args = args || [];
+
+                if (args.length < this.expectedArguments.length) {
+                    return false;
+                }
+
+                if (this.expectsExactArgCount &&
+                    args.length != this.expectedArguments.length) {
+                    return false;
+                }
+
+                for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) {
+                    if (!verifyMatcher(this.expectedArguments[i],args[i])) {
+                        return false;
+                    }
+
+                    if (!sinon.deepEqual(this.expectedArguments[i], args[i])) {
+                        return false;
+                    }
+                }
+
+                return true;
+            },
+
+            withArgs: function withArgs() {
+                this.expectedArguments = slice.call(arguments);
+                return this;
+            },
+
+            withExactArgs: function withExactArgs() {
+                this.withArgs.apply(this, arguments);
+                this.expectsExactArgCount = true;
+                return this;
+            },
+
+            on: function on(thisValue) {
+                this.expectedThis = thisValue;
+                return this;
+            },
+
+            toString: function () {
+                var args = (this.expectedArguments || []).slice();
+
+                if (!this.expectsExactArgCount) {
+                    push.call(args, "[...]");
+                }
+
+                var callStr = sinon.spyCall.toString.call({
+                    proxy: this.method || "anonymous mock expectation",
+                    args: args
+                });
+
+                var message = callStr.replace(", [...", "[, ...") + " " +
+                    expectedCallCountInWords(this);
+
+                if (this.met()) {
+                    return "Expectation met: " + message;
+                }
+
+                return "Expected " + message + " (" +
+                    callCountInWords(this.callCount) + ")";
+            },
+
+            verify: function verify() {
+                if (!this.met()) {
+                    sinon.expectation.fail(this.toString());
+                } else {
+                    sinon.expectation.pass(this.toString());
+                }
+
+                return true;
+            },
+
+            pass: function(message) {
+              sinon.assert.pass(message);
+            },
+            fail: function (message) {
+                var exception = new Error(message);
+                exception.name = "ExpectationError";
+
+                throw exception;
+            }
+        };
+    }());
+
+    sinon.mock = mock;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = mock; });
+    } else if (commonJSModule) {
+        module.exports = mock;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ * @depend mock.js
+ */
+/*jslint eqeqeq: false, onevar: false, forin: true*/
+/*global module, require, sinon*/
+/**
+ * Collections of stubs, spies and mocks.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var push = [].push;
+    var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function getFakes(fakeCollection) {
+        if (!fakeCollection.fakes) {
+            fakeCollection.fakes = [];
+        }
+
+        return fakeCollection.fakes;
+    }
+
+    function each(fakeCollection, method) {
+        var fakes = getFakes(fakeCollection);
+
+        for (var i = 0, l = fakes.length; i < l; i += 1) {
+            if (typeof fakes[i][method] == "function") {
+                fakes[i][method]();
+            }
+        }
+    }
+
+    function compact(fakeCollection) {
+        var fakes = getFakes(fakeCollection);
+        var i = 0;
+        while (i < fakes.length) {
+          fakes.splice(i, 1);
+        }
+    }
+
+    var collection = {
+        verify: function resolve() {
+            each(this, "verify");
+        },
+
+        restore: function restore() {
+            each(this, "restore");
+            compact(this);
+        },
+
+        verifyAndRestore: function verifyAndRestore() {
+            var exception;
+
+            try {
+                this.verify();
+            } catch (e) {
+                exception = e;
+            }
+
+            this.restore();
+
+            if (exception) {
+                throw exception;
+            }
+        },
+
+        add: function add(fake) {
+            push.call(getFakes(this), fake);
+            return fake;
+        },
+
+        spy: function spy() {
+            return this.add(sinon.spy.apply(sinon, arguments));
+        },
+
+        stub: function stub(object, property, value) {
+            if (property) {
+                var original = object[property];
+
+                if (typeof original != "function") {
+                    if (!hasOwnProperty.call(object, property)) {
+                        throw new TypeError("Cannot stub non-existent own property " + property);
+                    }
+
+                    object[property] = value;
+
+                    return this.add({
+                        restore: function () {
+                            object[property] = original;
+                        }
+                    });
+                }
+            }
+            if (!property && !!object && typeof object == "object") {
+                var stubbedObj = sinon.stub.apply(sinon, arguments);
+
+                for (var prop in stubbedObj) {
+                    if (typeof stubbedObj[prop] === "function") {
+                        this.add(stubbedObj[prop]);
+                    }
+                }
+
+                return stubbedObj;
+            }
+
+            return this.add(sinon.stub.apply(sinon, arguments));
+        },
+
+        mock: function mock() {
+            return this.add(sinon.mock.apply(sinon, arguments));
+        },
+
+        inject: function inject(obj) {
+            var col = this;
+
+            obj.spy = function () {
+                return col.spy.apply(col, arguments);
+            };
+
+            obj.stub = function () {
+                return col.stub.apply(col, arguments);
+            };
+
+            obj.mock = function () {
+                return col.mock.apply(col, arguments);
+            };
+
+            return obj;
+        }
+    };
+
+    sinon.collection = collection;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = collection; });
+    } else if (commonJSModule) {
+        module.exports = collection;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/
+/*global module, require, window*/
+/**
+ * Fake timer API
+ * setTimeout
+ * setInterval
+ * clearTimeout
+ * clearInterval
+ * tick
+ * reset
+ * Date
+ *
+ * Inspired by jsUnitMockTimeOut from JsUnit
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+if (typeof sinon == "undefined") {
+    var sinon = {};
+}
+
+(function (global) {
+    // node expects setTimeout/setInterval to return a fn object w/ .ref()/.unref()
+    // browsers, a number.
+    // see https://github.com/cjohansen/Sinon.JS/pull/436
+    var timeoutResult = setTimeout(function() {}, 0);
+    var addTimerReturnsObject = typeof timeoutResult === 'object';
+    clearTimeout(timeoutResult);
+
+    var id = 1;
+
+    function addTimer(args, recurring) {
+        if (args.length === 0) {
+            throw new Error("Function requires at least 1 parameter");
+        }
+
+        if (typeof args[0] === "undefined") {
+            throw new Error("Callback must be provided to timer calls");
+        }
+
+        var toId = id++;
+        var delay = args[1] || 0;
+
+        if (!this.timeouts) {
+            this.timeouts = {};
+        }
+
+        this.timeouts[toId] = {
+            id: toId,
+            func: args[0],
+            callAt: this.now + delay,
+            invokeArgs: Array.prototype.slice.call(args, 2)
+        };
+
+        if (recurring === true) {
+            this.timeouts[toId].interval = delay;
+        }
+
+        if (addTimerReturnsObject) {
+            return {
+                id: toId,
+                ref: function() {},
+                unref: function() {}
+            };
+        }
+        else {
+            return toId;
+        }
+    }
+
+    function parseTime(str) {
+        if (!str) {
+            return 0;
+        }
+
+        var strings = str.split(":");
+        var l = strings.length, i = l;
+        var ms = 0, parsed;
+
+        if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
+            throw new Error("tick only understands numbers and 'h:m:s'");
+        }
+
+        while (i--) {
+            parsed = parseInt(strings[i], 10);
+
+            if (parsed >= 60) {
+                throw new Error("Invalid time " + str);
+            }
+
+            ms += parsed * Math.pow(60, (l - i - 1));
+        }
+
+        return ms * 1000;
+    }
+
+    function createObject(object) {
+        var newObject;
+
+        if (Object.create) {
+            newObject = Object.create(object);
+        } else {
+            var F = function () {};
+            F.prototype = object;
+            newObject = new F();
+        }
+
+        newObject.Date.clock = newObject;
+        return newObject;
+    }
+
+    sinon.clock = {
+        now: 0,
+
+        create: function create(now) {
+            var clock = createObject(this);
+
+            if (typeof now == "number") {
+                clock.now = now;
+            }
+
+            if (!!now && typeof now == "object") {
+                throw new TypeError("now should be milliseconds since UNIX epoch");
+            }
+
+            return clock;
+        },
+
+        setTimeout: function setTimeout(callback, timeout) {
+            return addTimer.call(this, arguments, false);
+        },
+
+        clearTimeout: function clearTimeout(timerId) {
+            if (!timerId) {
+                // null appears to be allowed in most browsers, and appears to be relied upon by some libraries, like Bootstrap carousel
+                return;
+            }
+            if (!this.timeouts) {
+                this.timeouts = [];
+            }
+            // in Node, timerId is an object with .ref()/.unref(), and
+            // its .id field is the actual timer id.
+            if (typeof timerId === 'object') {
+              timerId = timerId.id
+            }
+            if (timerId in this.timeouts) {
+                delete this.timeouts[timerId];
+            }
+        },
+
+        setInterval: function setInterval(callback, timeout) {
+            return addTimer.call(this, arguments, true);
+        },
+
+        clearInterval: function clearInterval(timerId) {
+            this.clearTimeout(timerId);
+        },
+
+        setImmediate: function setImmediate(callback) {
+            var passThruArgs = Array.prototype.slice.call(arguments, 1);
+
+            return addTimer.call(this, [callback, 0].concat(passThruArgs), false);
+        },
+
+        clearImmediate: function clearImmediate(timerId) {
+            this.clearTimeout(timerId);
+        },
+
+        tick: function tick(ms) {
+            ms = typeof ms == "number" ? ms : parseTime(ms);
+            var tickFrom = this.now, tickTo = this.now + ms, previous = this.now;
+            var timer = this.firstTimerInRange(tickFrom, tickTo);
+
+            var firstException;
+            while (timer && tickFrom <= tickTo) {
+                if (this.timeouts[timer.id]) {
+                    tickFrom = this.now = timer.callAt;
+                    try {
+                      this.callTimer(timer);
+                    } catch (e) {
+                      firstException = firstException || e;
+                    }
+                }
+
+                timer = this.firstTimerInRange(previous, tickTo);
+                previous = tickFrom;
+            }
+
+            this.now = tickTo;
+
+            if (firstException) {
+              throw firstException;
+            }
+
+            return this.now;
+        },
+
+        firstTimerInRange: function (from, to) {
+            var timer, smallest = null, originalTimer;
+
+            for (var id in this.timeouts) {
+                if (this.timeouts.hasOwnProperty(id)) {
+                    if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) {
+                        continue;
+                    }
+
+                    if (smallest === null || this.timeouts[id].callAt < smallest) {
+                        originalTimer = this.timeouts[id];
+                        smallest = this.timeouts[id].callAt;
+
+                        timer = {
+                            func: this.timeouts[id].func,
+                            callAt: this.timeouts[id].callAt,
+                            interval: this.timeouts[id].interval,
+                            id: this.timeouts[id].id,
+                            invokeArgs: this.timeouts[id].invokeArgs
+                        };
+                    }
+                }
+            }
+
+            return timer || null;
+        },
+
+        callTimer: function (timer) {
+            if (typeof timer.interval == "number") {
+                this.timeouts[timer.id].callAt += timer.interval;
+            } else {
+                delete this.timeouts[timer.id];
+            }
+
+            try {
+                if (typeof timer.func == "function") {
+                    timer.func.apply(null, timer.invokeArgs);
+                } else {
+                    eval(timer.func);
+                }
+            } catch (e) {
+              var exception = e;
+            }
+
+            if (!this.timeouts[timer.id]) {
+                if (exception) {
+                  throw exception;
+                }
+                return;
+            }
+
+            if (exception) {
+              throw exception;
+            }
+        },
+
+        reset: function reset() {
+            this.timeouts = {};
+        },
+
+        Date: (function () {
+            var NativeDate = Date;
+
+            function ClockDate(year, month, date, hour, minute, second, ms) {
+                // Defensive and verbose to avoid potential harm in passing
+                // explicit undefined when user does not pass argument
+                switch (arguments.length) {
+                case 0:
+                    return new NativeDate(ClockDate.clock.now);
+                case 1:
+                    return new NativeDate(year);
+                case 2:
+                    return new NativeDate(year, month);
+                case 3:
+                    return new NativeDate(year, month, date);
+                case 4:
+                    return new NativeDate(year, month, date, hour);
+                case 5:
+                    return new NativeDate(year, month, date, hour, minute);
+                case 6:
+                    return new NativeDate(year, month, date, hour, minute, second);
+                default:
+                    return new NativeDate(year, month, date, hour, minute, second, ms);
+                }
+            }
+
+            return mirrorDateProperties(ClockDate, NativeDate);
+        }())
+    };
+
+    function mirrorDateProperties(target, source) {
+        if (source.now) {
+            target.now = function now() {
+                return target.clock.now;
+            };
+        } else {
+            delete target.now;
+        }
+
+        if (source.toSource) {
+            target.toSource = function toSource() {
+                return source.toSource();
+            };
+        } else {
+            delete target.toSource;
+        }
+
+        target.toString = function toString() {
+            return source.toString();
+        };
+
+        target.prototype = source.prototype;
+        target.parse = source.parse;
+        target.UTC = source.UTC;
+        target.prototype.toUTCString = source.prototype.toUTCString;
+
+        for (var prop in source) {
+            if (source.hasOwnProperty(prop)) {
+                target[prop] = source[prop];
+            }
+        }
+
+        return target;
+    }
+
+    var methods = ["Date", "setTimeout", "setInterval",
+                   "clearTimeout", "clearInterval"];
+
+    if (typeof global.setImmediate !== "undefined") {
+        methods.push("setImmediate");
+    }
+
+    if (typeof global.clearImmediate !== "undefined") {
+        methods.push("clearImmediate");
+    }
+
+    function restore() {
+        var method;
+
+        for (var i = 0, l = this.methods.length; i < l; i++) {
+            method = this.methods[i];
+
+            if (global[method].hadOwnProperty) {
+                global[method] = this["_" + method];
+            } else {
+                try {
+                    delete global[method];
+                } catch (e) {}
+            }
+        }
+
+        // Prevent multiple executions which will completely remove these props
+        this.methods = [];
+    }
+
+    function stubGlobal(method, clock) {
+        clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method);
+        clock["_" + method] = global[method];
+
+        if (method == "Date") {
+            var date = mirrorDateProperties(clock[method], global[method]);
+            global[method] = date;
+        } else {
+            global[method] = function () {
+                return clock[method].apply(clock, arguments);
+            };
+
+            for (var prop in clock[method]) {
+                if (clock[method].hasOwnProperty(prop)) {
+                    global[method][prop] = clock[method][prop];
+                }
+            }
+        }
+
+        global[method].clock = clock;
+    }
+
+    sinon.useFakeTimers = function useFakeTimers(now) {
+        var clock = sinon.clock.create(now);
+        clock.restore = restore;
+        clock.methods = Array.prototype.slice.call(arguments,
+                                                   typeof now == "number" ? 1 : 0);
+
+        if (clock.methods.length === 0) {
+            clock.methods = methods;
+        }
+
+        for (var i = 0, l = clock.methods.length; i < l; i++) {
+            stubGlobal(clock.methods[i], clock);
+        }
+
+        return clock;
+    };
+}(typeof global != "undefined" && typeof global !== "function" ? global : this));
+
+sinon.timers = {
+    setTimeout: setTimeout,
+    clearTimeout: clearTimeout,
+    setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined),
+    clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined),
+    setInterval: setInterval,
+    clearInterval: clearInterval,
+    Date: Date
+};
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = sinon;
+}
+
+/*jslint eqeqeq: false, onevar: false*/
+/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
+/**
+ * Minimal Event interface implementation
+ *
+ * Original implementation by Sven Fuchs: https://gist.github.com/995028
+ * Modifications and tests by Christian Johansen.
+ *
+ * @author Sven Fuchs (svenfuchs@artweb-design.de)
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2011 Sven Fuchs, Christian Johansen
+ */
+
+if (typeof sinon == "undefined") {
+    this.sinon = {};
+}
+
+(function () {
+    var push = [].push;
+
+    sinon.Event = function Event(type, bubbles, cancelable, target) {
+        this.initEvent(type, bubbles, cancelable, target);
+    };
+
+    sinon.Event.prototype = {
+        initEvent: function(type, bubbles, cancelable, target) {
+            this.type = type;
+            this.bubbles = bubbles;
+            this.cancelable = cancelable;
+            this.target = target;
+        },
+
+        stopPropagation: function () {},
+
+        preventDefault: function () {
+            this.defaultPrevented = true;
+        }
+    };
+
+    sinon.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) {
+        this.initEvent(type, false, false, target);
+        this.loaded = progressEventRaw.loaded || null;
+        this.total = progressEventRaw.total || null;
+    };
+
+    sinon.ProgressEvent.prototype = new sinon.Event();
+
+    sinon.ProgressEvent.prototype.constructor =  sinon.ProgressEvent;
+
+    sinon.CustomEvent = function CustomEvent(type, customData, target) {
+        this.initEvent(type, false, false, target);
+        this.detail = customData.detail || null;
+    };
+
+    sinon.CustomEvent.prototype = new sinon.Event();
+
+    sinon.CustomEvent.prototype.constructor =  sinon.CustomEvent;
+
+    sinon.EventTarget = {
+        addEventListener: function addEventListener(event, listener) {
+            this.eventListeners = this.eventListeners || {};
+            this.eventListeners[event] = this.eventListeners[event] || [];
+            push.call(this.eventListeners[event], listener);
+        },
+
+        removeEventListener: function removeEventListener(event, listener) {
+            var listeners = this.eventListeners && this.eventListeners[event] || [];
+
+            for (var i = 0, l = listeners.length; i < l; ++i) {
+                if (listeners[i] == listener) {
+                    return listeners.splice(i, 1);
+                }
+            }
+        },
+
+        dispatchEvent: function dispatchEvent(event) {
+            var type = event.type;
+            var listeners = this.eventListeners && this.eventListeners[type] || [];
+
+            for (var i = 0; i < listeners.length; i++) {
+                if (typeof listeners[i] == "function") {
+                    listeners[i].call(this, event);
+                } else {
+                    listeners[i].handleEvent(event);
+                }
+            }
+
+            return !!event.defaultPrevented;
+        }
+    };
+}());
+
+/**
+ * @depend ../../sinon.js
+ * @depend event.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
+/**
+ * Fake XMLHttpRequest object
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+// wrapper for global
+(function(global) {
+    if (typeof sinon === "undefined") {
+        global.sinon = {};
+    }
+
+    var supportsProgress = typeof ProgressEvent !== "undefined";
+    var supportsCustomEvent = typeof CustomEvent !== "undefined";
+    sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest };
+    var xhr = sinon.xhr;
+    xhr.GlobalXMLHttpRequest = global.XMLHttpRequest;
+    xhr.GlobalActiveXObject = global.ActiveXObject;
+    xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined";
+    xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined";
+    xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX
+                                     ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false;
+    xhr.supportsCORS = xhr.supportsXHR && 'withCredentials' in (new sinon.xhr.GlobalXMLHttpRequest());
+
+    /*jsl:ignore*/
+    var unsafeHeaders = {
+        "Accept-Charset": true,
+        "Accept-Encoding": true,
+        "Connection": true,
+        "Content-Length": true,
+        "Cookie": true,
+        "Cookie2": true,
+        "Content-Transfer-Encoding": true,
+        "Date": true,
+        "Expect": true,
+        "Host": true,
+        "Keep-Alive": true,
+        "Referer": true,
+        "TE": true,
+        "Trailer": true,
+        "Transfer-Encoding": true,
+        "Upgrade": true,
+        "User-Agent": true,
+        "Via": true
+    };
+    /*jsl:end*/
+
+    function FakeXMLHttpRequest() {
+        this.readyState = FakeXMLHttpRequest.UNSENT;
+        this.requestHeaders = {};
+        this.requestBody = null;
+        this.status = 0;
+        this.statusText = "";
+        this.upload = new UploadProgress();
+        if (sinon.xhr.supportsCORS) {
+            this.withCredentials = false;
+        }
+
+
+        var xhr = this;
+        var events = ["loadstart", "load", "abort", "loadend"];
+
+        function addEventListener(eventName) {
+            xhr.addEventListener(eventName, function (event) {
+                var listener = xhr["on" + eventName];
+
+                if (listener && typeof listener == "function") {
+                    listener.call(this, event);
+                }
+            });
+        }
+
+        for (var i = events.length - 1; i >= 0; i--) {
+            addEventListener(events[i]);
+        }
+
+        if (typeof FakeXMLHttpRequest.onCreate == "function") {
+            FakeXMLHttpRequest.onCreate(this);
+        }
+    }
+
+    // An upload object is created for each
+    // FakeXMLHttpRequest and allows upload
+    // events to be simulated using uploadProgress
+    // and uploadError.
+    function UploadProgress() {
+        this.eventListeners = {
+            "progress": [],
+            "load": [],
+            "abort": [],
+            "error": []
+        }
+    }
+
+    UploadProgress.prototype.addEventListener = function(event, listener) {
+        this.eventListeners[event].push(listener);
+    };
+
+    UploadProgress.prototype.removeEventListener = function(event, listener) {
+        var listeners = this.eventListeners[event] || [];
+
+        for (var i = 0, l = listeners.length; i < l; ++i) {
+            if (listeners[i] == listener) {
+                return listeners.splice(i, 1);
+            }
+        }
+    };
+
+    UploadProgress.prototype.dispatchEvent = function(event) {
+        var listeners = this.eventListeners[event.type] || [];
+
+        for (var i = 0, listener; (listener = listeners[i]) != null; i++) {
+            listener(event);
+        }
+    };
+
+    function verifyState(xhr) {
+        if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+
+        if (xhr.sendFlag) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+    }
+
+    // filtering to enable a white-list version of Sinon FakeXhr,
+    // where whitelisted requests are passed through to real XHR
+    function each(collection, callback) {
+        if (!collection) return;
+        for (var i = 0, l = collection.length; i < l; i += 1) {
+            callback(collection[i]);
+        }
+    }
+    function some(collection, callback) {
+        for (var index = 0; index < collection.length; index++) {
+            if(callback(collection[index]) === true) return true;
+        }
+        return false;
+    }
+    // largest arity in XHR is 5 - XHR#open
+    var apply = function(obj,method,args) {
+        switch(args.length) {
+        case 0: return obj[method]();
+        case 1: return obj[method](args[0]);
+        case 2: return obj[method](args[0],args[1]);
+        case 3: return obj[method](args[0],args[1],args[2]);
+        case 4: return obj[method](args[0],args[1],args[2],args[3]);
+        case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]);
+        }
+    };
+
+    FakeXMLHttpRequest.filters = [];
+    FakeXMLHttpRequest.addFilter = function(fn) {
+        this.filters.push(fn)
+    };
+    var IE6Re = /MSIE 6/;
+    FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) {
+        var xhr = new sinon.xhr.workingXHR();
+        each(["open","setRequestHeader","send","abort","getResponseHeader",
+              "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"],
+             function(method) {
+                 fakeXhr[method] = function() {
+                   return apply(xhr,method,arguments);
+                 };
+             });
+
+        var copyAttrs = function(args) {
+            each(args, function(attr) {
+              try {
+                fakeXhr[attr] = xhr[attr]
+              } catch(e) {
+                if(!IE6Re.test(navigator.userAgent)) throw e;
+              }
+            });
+        };
+
+        var stateChange = function() {
+            fakeXhr.readyState = xhr.readyState;
+            if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) {
+                copyAttrs(["status","statusText"]);
+            }
+            if(xhr.readyState >= FakeXMLHttpRequest.LOADING) {
+                copyAttrs(["responseText"]);
+            }
+            if(xhr.readyState === FakeXMLHttpRequest.DONE) {
+                copyAttrs(["responseXML"]);
+            }
+            if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr, { target: fakeXhr });
+        };
+        if(xhr.addEventListener) {
+          for(var event in fakeXhr.eventListeners) {
+              if(fakeXhr.eventListeners.hasOwnProperty(event)) {
+                  each(fakeXhr.eventListeners[event],function(handler) {
+                      xhr.addEventListener(event, handler);
+                  });
+              }
+          }
+          xhr.addEventListener("readystatechange",stateChange);
+        } else {
+          xhr.onreadystatechange = stateChange;
+        }
+        apply(xhr,"open",xhrArgs);
+    };
+    FakeXMLHttpRequest.useFilters = false;
+
+    function verifyRequestOpened(xhr) {
+        if (xhr.readyState != FakeXMLHttpRequest.OPENED) {
+            throw new Error("INVALID_STATE_ERR - " + xhr.readyState);
+        }
+    }
+
+    function verifyRequestSent(xhr) {
+        if (xhr.readyState == FakeXMLHttpRequest.DONE) {
+            throw new Error("Request done");
+        }
+    }
+
+    function verifyHeadersReceived(xhr) {
+        if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) {
+            throw new Error("No headers received");
+        }
+    }
+
+    function verifyResponseBodyType(body) {
+        if (typeof body != "string") {
+            var error = new Error("Attempted to respond to fake XMLHttpRequest with " +
+                                 body + ", which is not a string.");
+            error.name = "InvalidBodyException";
+            throw error;
+        }
+    }
+
+    sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, {
+        async: true,
+
+        open: function open(method, url, async, username, password) {
+            this.method = method;
+            this.url = url;
+            this.async = typeof async == "boolean" ? async : true;
+            this.username = username;
+            this.password = password;
+            this.responseText = null;
+            this.responseXML = null;
+            this.requestHeaders = {};
+            this.sendFlag = false;
+            if(sinon.FakeXMLHttpRequest.useFilters === true) {
+                var xhrArgs = arguments;
+                var defake = some(FakeXMLHttpRequest.filters,function(filter) {
+                    return filter.apply(this,xhrArgs)
+                });
+                if (defake) {
+                  return sinon.FakeXMLHttpRequest.defake(this,arguments);
+                }
+            }
+            this.readyStateChange(FakeXMLHttpRequest.OPENED);
+        },
+
+        readyStateChange: function readyStateChange(state) {
+            this.readyState = state;
+
+            if (typeof this.onreadystatechange == "function") {
+                try {
+                    this.onreadystatechange();
+                } catch (e) {
+                    sinon.logError("Fake XHR onreadystatechange handler", e);
+                }
+            }
+
+            this.dispatchEvent(new sinon.Event("readystatechange"));
+
+            switch (this.readyState) {
+                case FakeXMLHttpRequest.DONE:
+                    this.dispatchEvent(new sinon.Event("load", false, false, this));
+                    this.dispatchEvent(new sinon.Event("loadend", false, false, this));
+                    this.upload.dispatchEvent(new sinon.Event("load", false, false, this));
+                    if (supportsProgress) {
+                        this.upload.dispatchEvent(new sinon.ProgressEvent('progress', {loaded: 100, total: 100}));
+                    }
+                    break;
+            }
+        },
+
+        setRequestHeader: function setRequestHeader(header, value) {
+            verifyState(this);
+
+            if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) {
+                throw new Error("Refused to set unsafe header \"" + header + "\"");
+            }
+
+            if (this.requestHeaders[header]) {
+                this.requestHeaders[header] += "," + value;
+            } else {
+                this.requestHeaders[header] = value;
+            }
+        },
+
+        // Helps testing
+        setResponseHeaders: function setResponseHeaders(headers) {
+            verifyRequestOpened(this);
+            this.responseHeaders = {};
+
+            for (var header in headers) {
+                if (headers.hasOwnProperty(header)) {
+                    this.responseHeaders[header] = headers[header];
+                }
+            }
+
+            if (this.async) {
+                this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED);
+            } else {
+                this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED;
+            }
+        },
+
+        // Currently treats ALL data as a DOMString (i.e. no Document)
+        send: function send(data) {
+            verifyState(this);
+
+            if (!/^(get|head)$/i.test(this.method)) {
+                if (this.requestHeaders["Content-Type"]) {
+                    var value = this.requestHeaders["Content-Type"].split(";");
+                    this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8";
+                } else {
+                    this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
+                }
+
+                this.requestBody = data;
+            }
+
+            this.errorFlag = false;
+            this.sendFlag = this.async;
+            this.readyStateChange(FakeXMLHttpRequest.OPENED);
+
+            if (typeof this.onSend == "function") {
+                this.onSend(this);
+            }
+
+            this.dispatchEvent(new sinon.Event("loadstart", false, false, this));
+        },
+
+        abort: function abort() {
+            this.aborted = true;
+            this.responseText = null;
+            this.errorFlag = true;
+            this.requestHeaders = {};
+
+            if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) {
+                this.readyStateChange(sinon.FakeXMLHttpRequest.DONE);
+                this.sendFlag = false;
+            }
+
+            this.readyState = sinon.FakeXMLHttpRequest.UNSENT;
+
+            this.dispatchEvent(new sinon.Event("abort", false, false, this));
+
+            this.upload.dispatchEvent(new sinon.Event("abort", false, false, this));
+
+            if (typeof this.onerror === "function") {
+                this.onerror();
+            }
+        },
+
+        getResponseHeader: function getResponseHeader(header) {
+            if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
+                return null;
+            }
+
+            if (/^Set-Cookie2?$/i.test(header)) {
+                return null;
+            }
+
+            header = header.toLowerCase();
+
+            for (var h in this.responseHeaders) {
+                if (h.toLowerCase() == header) {
+                    return this.responseHeaders[h];
+                }
+            }
+
+            return null;
+        },
+
+        getAllResponseHeaders: function getAllResponseHeaders() {
+            if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
+                return "";
+            }
+
+            var headers = "";
+
+            for (var header in this.responseHeaders) {
+                if (this.responseHeaders.hasOwnProperty(header) &&
+                    !/^Set-Cookie2?$/i.test(header)) {
+                    headers += header + ": " + this.responseHeaders[header] + "\r\n";
+                }
+            }
+
+            return headers;
+        },
+
+        setResponseBody: function setResponseBody(body) {
+            verifyRequestSent(this);
+            verifyHeadersReceived(this);
+            verifyResponseBodyType(body);
+
+            var chunkSize = this.chunkSize || 10;
+            var index = 0;
+            this.responseText = "";
+
+            do {
+                if (this.async) {
+                    this.readyStateChange(FakeXMLHttpRequest.LOADING);
+                }
+
+                this.responseText += body.substring(index, index + chunkSize);
+                index += chunkSize;
+            } while (index < body.length);
+
+            var type = this.getResponseHeader("Content-Type");
+
+            if (this.responseText &&
+                (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) {
+                try {
+                    this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText);
+                } catch (e) {
+                    // Unable to parse XML - no biggie
+                }
+            }
+
+            if (this.async) {
+                this.readyStateChange(FakeXMLHttpRequest.DONE);
+            } else {
+                this.readyState = FakeXMLHttpRequest.DONE;
+            }
+        },
+
+        respond: function respond(status, headers, body) {
+            this.status = typeof status == "number" ? status : 200;
+            this.statusText = FakeXMLHttpRequest.statusCodes[this.status];
+            this.setResponseHeaders(headers || {});
+            this.setResponseBody(body || "");
+        },
+
+        uploadProgress: function uploadProgress(progressEventRaw) {
+            if (supportsProgress) {
+                this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw));
+            }
+        },
+
+        uploadError: function uploadError(error) {
+            if (supportsCustomEvent) {
+                this.upload.dispatchEvent(new sinon.CustomEvent("error", {"detail": error}));
+            }
+        }
+    });
+
+    sinon.extend(FakeXMLHttpRequest, {
+        UNSENT: 0,
+        OPENED: 1,
+        HEADERS_RECEIVED: 2,
+        LOADING: 3,
+        DONE: 4
+    });
+
+    // Borrowed from JSpec
+    FakeXMLHttpRequest.parseXML = function parseXML(text) {
+        var xmlDoc;
+
+        if (typeof DOMParser != "undefined") {
+            var parser = new DOMParser();
+            xmlDoc = parser.parseFromString(text, "text/xml");
+        } else {
+            xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
+            xmlDoc.async = "false";
+            xmlDoc.loadXML(text);
+        }
+
+        return xmlDoc;
+    };
+
+    FakeXMLHttpRequest.statusCodes = {
+        100: "Continue",
+        101: "Switching Protocols",
+        200: "OK",
+        201: "Created",
+        202: "Accepted",
+        203: "Non-Authoritative Information",
+        204: "No Content",
+        205: "Reset Content",
+        206: "Partial Content",
+        300: "Multiple Choice",
+        301: "Moved Permanently",
+        302: "Found",
+        303: "See Other",
+        304: "Not Modified",
+        305: "Use Proxy",
+        307: "Temporary Redirect",
+        400: "Bad Request",
+        401: "Unauthorized",
+        402: "Payment Required",
+        403: "Forbidden",
+        404: "Not Found",
+        405: "Method Not Allowed",
+        406: "Not Acceptable",
+        407: "Proxy Authentication Required",
+        408: "Request Timeout",
+        409: "Conflict",
+        410: "Gone",
+        411: "Length Required",
+        412: "Precondition Failed",
+        413: "Request Entity Too Large",
+        414: "Request-URI Too Long",
+        415: "Unsupported Media Type",
+        416: "Requested Range Not Satisfiable",
+        417: "Expectation Failed",
+        422: "Unprocessable Entity",
+        500: "Internal Server Error",
+        501: "Not Implemented",
+        502: "Bad Gateway",
+        503: "Service Unavailable",
+        504: "Gateway Timeout",
+        505: "HTTP Version Not Supported"
+    };
+
+    sinon.useFakeXMLHttpRequest = function () {
+        sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) {
+            if (xhr.supportsXHR) {
+                global.XMLHttpRequest = xhr.GlobalXMLHttpRequest;
+            }
+
+            if (xhr.supportsActiveX) {
+                global.ActiveXObject = xhr.GlobalActiveXObject;
+            }
+
+            delete sinon.FakeXMLHttpRequest.restore;
+
+            if (keepOnCreate !== true) {
+                delete sinon.FakeXMLHttpRequest.onCreate;
+            }
+        };
+        if (xhr.supportsXHR) {
+            global.XMLHttpRequest = sinon.FakeXMLHttpRequest;
+        }
+
+        if (xhr.supportsActiveX) {
+            global.ActiveXObject = function ActiveXObject(objId) {
+                if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) {
+
+                    return new sinon.FakeXMLHttpRequest();
+                }
+
+                return new xhr.GlobalActiveXObject(objId);
+            };
+        }
+
+        return sinon.FakeXMLHttpRequest;
+    };
+
+    sinon.FakeXMLHttpRequest = FakeXMLHttpRequest;
+
+})((function(){ return typeof global === "object" ? global : this; })());
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = sinon;
+}
+
+/**
+ * @depend fake_xml_http_request.js
+ */
+/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/
+/*global module, require, window*/
+/**
+ * The Sinon "server" mimics a web server that receives requests from
+ * sinon.FakeXMLHttpRequest and provides an API to respond to those requests,
+ * both synchronously and asynchronously. To respond synchronuously, canned
+ * answers have to be provided upfront.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+if (typeof sinon == "undefined") {
+    var sinon = {};
+}
+
+sinon.fakeServer = (function () {
+    var push = [].push;
+    function F() {}
+
+    function create(proto) {
+        F.prototype = proto;
+        return new F();
+    }
+
+    function responseArray(handler) {
+        var response = handler;
+
+        if (Object.prototype.toString.call(handler) != "[object Array]") {
+            response = [200, {}, handler];
+        }
+
+        if (typeof response[2] != "string") {
+            throw new TypeError("Fake server response body should be string, but was " +
+                                typeof response[2]);
+        }
+
+        return response;
+    }
+
+    var wloc = typeof window !== "undefined" ? window.location : {};
+    var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host);
+
+    function matchOne(response, reqMethod, reqUrl) {
+        var rmeth = response.method;
+        var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase();
+        var url = response.url;
+        var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl));
+
+        return matchMethod && matchUrl;
+    }
+
+    function match(response, request) {
+        var requestUrl = request.url;
+
+        if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) {
+            requestUrl = requestUrl.replace(rCurrLoc, "");
+        }
+
+        if (matchOne(response, this.getHTTPMethod(request), requestUrl)) {
+            if (typeof response.response == "function") {
+                var ru = response.url;
+                var args = [request].concat(ru && typeof ru.exec == "function" ? ru.exec(requestUrl).slice(1) : []);
+                return response.response.apply(response, args);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    return {
+        create: function () {
+            var server = create(this);
+            this.xhr = sinon.useFakeXMLHttpRequest();
+            server.requests = [];
+
+            this.xhr.onCreate = function (xhrObj) {
+                server.addRequest(xhrObj);
+            };
+
+            return server;
+        },
+
+        addRequest: function addRequest(xhrObj) {
+            var server = this;
+            push.call(this.requests, xhrObj);
+
+            xhrObj.onSend = function () {
+                server.handleRequest(this);
+
+                if (server.autoRespond && !server.responding) {
+                    setTimeout(function () {
+                        server.responding = false;
+                        server.respond();
+                    }, server.autoRespondAfter || 10);
+
+                    server.responding = true;
+                }
+            };
+        },
+
+        getHTTPMethod: function getHTTPMethod(request) {
+            if (this.fakeHTTPMethods && /post/i.test(request.method)) {
+                var matches = (request.requestBody || "").match(/_method=([^\b;]+)/);
+                return !!matches ? matches[1] : request.method;
+            }
+
+            return request.method;
+        },
+
+        handleRequest: function handleRequest(xhr) {
+            if (xhr.async) {
+                if (!this.queue) {
+                    this.queue = [];
+                }
+
+                push.call(this.queue, xhr);
+            } else {
+                this.processRequest(xhr);
+            }
+        },
+
+        log: function(response, request) {
+            var str;
+
+            str =  "Request:\n"  + sinon.format(request)  + "\n\n";
+            str += "Response:\n" + sinon.format(response) + "\n\n";
+
+            sinon.log(str);
+        },
+
+        respondWith: function respondWith(method, url, body) {
+            if (arguments.length == 1 && typeof method != "function") {
+                this.response = responseArray(method);
+                return;
+            }
+
+            if (!this.responses) { this.responses = []; }
+
+            if (arguments.length == 1) {
+                body = method;
+                url = method = null;
+            }
+
+            if (arguments.length == 2) {
+                body = url;
+                url = method;
+                method = null;
+            }
+
+            push.call(this.responses, {
+                method: method,
+                url: url,
+                response: typeof body == "function" ? body : responseArray(body)
+            });
+        },
+
+        respond: function respond() {
+            if (arguments.length > 0) this.respondWith.apply(this, arguments);
+            var queue = this.queue || [];
+            var requests = queue.splice(0, queue.length);
+            var request;
+
+            while(request = requests.shift()) {
+                this.processRequest(request);
+            }
+        },
+
+        processRequest: function processRequest(request) {
+            try {
+                if (request.aborted) {
+                    return;
+                }
+
+                var response = this.response || [404, {}, ""];
+
+                if (this.responses) {
+                    for (var l = this.responses.length, i = l - 1; i >= 0; i--) {
+                        if (match.call(this, this.responses[i], request)) {
+                            response = this.responses[i].response;
+                            break;
+                        }
+                    }
+                }
+
+                if (request.readyState != 4) {
+                    sinon.fakeServer.log(response, request);
+
+                    request.respond(response[0], response[1], response[2]);
+                }
+            } catch (e) {
+                sinon.logError("Fake server request processing", e);
+            }
+        },
+
+        restore: function restore() {
+            return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments);
+        }
+    };
+}());
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = sinon;
+}
+
+/**
+ * @depend fake_server.js
+ * @depend fake_timers.js
+ */
+/*jslint browser: true, eqeqeq: false, onevar: false*/
+/*global sinon*/
+/**
+ * Add-on for sinon.fakeServer that automatically handles a fake timer along with
+ * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery
+ * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead,
+ * it polls the object for completion with setInterval. Dispite the direct
+ * motivation, there is nothing jQuery-specific in this file, so it can be used
+ * in any environment where the ajax implementation depends on setInterval or
+ * setTimeout.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function () {
+    function Server() {}
+    Server.prototype = sinon.fakeServer;
+
+    sinon.fakeServerWithClock = new Server();
+
+    sinon.fakeServerWithClock.addRequest = function addRequest(xhr) {
+        if (xhr.async) {
+            if (typeof setTimeout.clock == "object") {
+                this.clock = setTimeout.clock;
+            } else {
+                this.clock = sinon.useFakeTimers();
+                this.resetClock = true;
+            }
+
+            if (!this.longestTimeout) {
+                var clockSetTimeout = this.clock.setTimeout;
+                var clockSetInterval = this.clock.setInterval;
+                var server = this;
+
+                this.clock.setTimeout = function (fn, timeout) {
+                    server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
+
+                    return clockSetTimeout.apply(this, arguments);
+                };
+
+                this.clock.setInterval = function (fn, timeout) {
+                    server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
+
+                    return clockSetInterval.apply(this, arguments);
+                };
+            }
+        }
+
+        return sinon.fakeServer.addRequest.call(this, xhr);
+    };
+
+    sinon.fakeServerWithClock.respond = function respond() {
+        var returnVal = sinon.fakeServer.respond.apply(this, arguments);
+
+        if (this.clock) {
+            this.clock.tick(this.longestTimeout || 0);
+            this.longestTimeout = 0;
+
+            if (this.resetClock) {
+                this.clock.restore();
+                this.resetClock = false;
+            }
+        }
+
+        return returnVal;
+    };
+
+    sinon.fakeServerWithClock.restore = function restore() {
+        if (this.clock) {
+            this.clock.restore();
+        }
+
+        return sinon.fakeServer.restore.apply(this, arguments);
+    };
+}());
+
+/**
+ * @depend ../sinon.js
+ * @depend collection.js
+ * @depend util/fake_timers.js
+ * @depend util/fake_server_with_clock.js
+ */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global require, module*/
+/**
+ * Manages fake collections as well as fake utilities such as Sinon's
+ * timers and fake XHR implementation in one convenient object.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+if (typeof module !== "undefined" && module.exports && typeof require == "function") {
+    var sinon = require("../sinon");
+    sinon.extend(sinon, require("./util/fake_timers"));
+}
+
+(function () {
+    var push = [].push;
+
+    function exposeValue(sandbox, config, key, value) {
+        if (!value) {
+            return;
+        }
+
+        if (config.injectInto && !(key in config.injectInto)) {
+            config.injectInto[key] = value;
+            sandbox.injectedKeys.push(key);
+        } else {
+            push.call(sandbox.args, value);
+        }
+    }
+
+    function prepareSandboxFromConfig(config) {
+        var sandbox = sinon.create(sinon.sandbox);
+
+        if (config.useFakeServer) {
+            if (typeof config.useFakeServer == "object") {
+                sandbox.serverPrototype = config.useFakeServer;
+            }
+
+            sandbox.useFakeServer();
+        }
+
+        if (config.useFakeTimers) {
+            if (typeof config.useFakeTimers == "object") {
+                sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers);
+            } else {
+                sandbox.useFakeTimers();
+            }
+        }
+
+        return sandbox;
+    }
+
+    sinon.sandbox = sinon.extend(sinon.create(sinon.collection), {
+        useFakeTimers: function useFakeTimers() {
+            this.clock = sinon.useFakeTimers.apply(sinon, arguments);
+
+            return this.add(this.clock);
+        },
+
+        serverPrototype: sinon.fakeServer,
+
+        useFakeServer: function useFakeServer() {
+            var proto = this.serverPrototype || sinon.fakeServer;
+
+            if (!proto || !proto.create) {
+                return null;
+            }
+
+            this.server = proto.create();
+            return this.add(this.server);
+        },
+
+        inject: function (obj) {
+            sinon.collection.inject.call(this, obj);
+
+            if (this.clock) {
+                obj.clock = this.clock;
+            }
+
+            if (this.server) {
+                obj.server = this.server;
+                obj.requests = this.server.requests;
+            }
+
+            return obj;
+        },
+
+        restore: function () {
+            sinon.collection.restore.apply(this, arguments);
+            this.restoreContext();
+        },
+
+        restoreContext: function () {
+            if (this.injectedKeys) {
+                for (var i = 0, j = this.injectedKeys.length; i < j; i++) {
+                    delete this.injectInto[this.injectedKeys[i]];
+                }
+                this.injectedKeys = [];
+            }
+        },
+
+        create: function (config) {
+            if (!config) {
+                return sinon.create(sinon.sandbox);
+            }
+
+            var sandbox = prepareSandboxFromConfig(config);
+            sandbox.args = sandbox.args || [];
+            sandbox.injectedKeys = [];
+            sandbox.injectInto = config.injectInto;
+            var prop, value, exposed = sandbox.inject({});
+
+            if (config.properties) {
+                for (var i = 0, l = config.properties.length; i < l; i++) {
+                    prop = config.properties[i];
+                    value = exposed[prop] || prop == "sandbox" && sandbox;
+                    exposeValue(sandbox, config, prop, value);
+                }
+            } else {
+                exposeValue(sandbox, config, "sandbox", value);
+            }
+
+            return sandbox;
+        }
+    });
+
+    sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = sinon.sandbox; });
+    } else if (typeof module !== 'undefined' && module.exports) {
+        module.exports = sinon.sandbox;
+    }
+}());
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ * @depend mock.js
+ * @depend sandbox.js
+ */
+/*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/
+/*global module, require, sinon*/
+/**
+ * Test function, sandboxes fakes
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function test(callback) {
+        var type = typeof callback;
+
+        if (type != "function") {
+            throw new TypeError("sinon.test needs to wrap a test function, got " + type);
+        }
+
+        function sinonSandboxedTest() {
+            var config = sinon.getConfig(sinon.config);
+            config.injectInto = config.injectIntoThis && this || config.injectInto;
+            var sandbox = sinon.sandbox.create(config);
+            var exception, result;
+            var args = Array.prototype.slice.call(arguments).concat(sandbox.args);
+
+            try {
+                result = callback.apply(this, args);
+            } catch (e) {
+                exception = e;
+            }
+
+            if (typeof exception !== "undefined") {
+                sandbox.restore();
+                throw exception;
+            }
+            else {
+                sandbox.verifyAndRestore();
+            }
+
+            return result;
+        };
+
+        if (callback.length) {
+            return function sinonAsyncSandboxedTest(callback) {
+                return sinonSandboxedTest.apply(this, arguments);
+            };
+        }
+
+        return sinonSandboxedTest;
+    }
+
+    test.config = {
+        injectIntoThis: true,
+        injectInto: null,
+        properties: ["spy", "stub", "mock", "clock", "server", "requests"],
+        useFakeTimers: true,
+        useFakeServer: true
+    };
+
+    sinon.test = test;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = test; });
+    } else if (commonJSModule) {
+        module.exports = test;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend test.js
+ */
+/*jslint eqeqeq: false, onevar: false, eqeqeq: false*/
+/*global module, require, sinon*/
+/**
+ * Test case, sandboxes all test functions
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon || !Object.prototype.hasOwnProperty) {
+        return;
+    }
+
+    function createTest(property, setUp, tearDown) {
+        return function () {
+            if (setUp) {
+                setUp.apply(this, arguments);
+            }
+
+            var exception, result;
+
+            try {
+                result = property.apply(this, arguments);
+            } catch (e) {
+                exception = e;
+            }
+
+            if (tearDown) {
+                tearDown.apply(this, arguments);
+            }
+
+            if (exception) {
+                throw exception;
+            }
+
+            return result;
+        };
+    }
+
+    function testCase(tests, prefix) {
+        /*jsl:ignore*/
+        if (!tests || typeof tests != "object") {
+            throw new TypeError("sinon.testCase needs an object with test functions");
+        }
+        /*jsl:end*/
+
+        prefix = prefix || "test";
+        var rPrefix = new RegExp("^" + prefix);
+        var methods = {}, testName, property, method;
+        var setUp = tests.setUp;
+        var tearDown = tests.tearDown;
+
+        for (testName in tests) {
+            if (tests.hasOwnProperty(testName)) {
+                property = tests[testName];
+
+                if (/^(setUp|tearDown)$/.test(testName)) {
+                    continue;
+                }
+
+                if (typeof property == "function" && rPrefix.test(testName)) {
+                    method = property;
+
+                    if (setUp || tearDown) {
+                        method = createTest(property, setUp, tearDown);
+                    }
+
+                    methods[testName] = sinon.test(method);
+                } else {
+                    methods[testName] = tests[testName];
+                }
+            }
+        }
+
+        return methods;
+    }
+
+    sinon.testCase = testCase;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = testCase; });
+    } else if (commonJSModule) {
+        module.exports = testCase;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ */
+/*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+ * Assertions matching the test spy retrieval interface.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon, global) {
+    var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var slice = Array.prototype.slice;
+    var assert;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function verifyIsStub() {
+        var method;
+
+        for (var i = 0, l = arguments.length; i < l; ++i) {
+            method = arguments[i];
+
+            if (!method) {
+                assert.fail("fake is not a spy");
+            }
+
+            if (typeof method != "function") {
+                assert.fail(method + " is not a function");
+            }
+
+            if (typeof method.getCall != "function") {
+                assert.fail(method + " is not stubbed");
+            }
+        }
+    }
+
+    function failAssertion(object, msg) {
+        object = object || global;
+        var failMethod = object.fail || assert.fail;
+        failMethod.call(object, msg);
+    }
+
+    function mirrorPropAsAssertion(name, method, message) {
+        if (arguments.length == 2) {
+            message = method;
+            method = name;
+        }
+
+        assert[name] = function (fake) {
+            verifyIsStub(fake);
+
+            var args = slice.call(arguments, 1);
+            var failed = false;
+
+            if (typeof method == "function") {
+                failed = !method(fake);
+            } else {
+                failed = typeof fake[method] == "function" ?
+                    !fake[method].apply(fake, args) : !fake[method];
+            }
+
+            if (failed) {
+                failAssertion(this, fake.printf.apply(fake, [message].concat(args)));
+            } else {
+                assert.pass(name);
+            }
+        };
+    }
+
+    function exposedName(prefix, prop) {
+        return !prefix || /^fail/.test(prop) ? prop :
+            prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1);
+    }
+
+    assert = {
+        failException: "AssertError",
+
+        fail: function fail(message) {
+            var error = new Error(message);
+            error.name = this.failException || assert.failException;
+
+            throw error;
+        },
+
+        pass: function pass(assertion) {},
+
+        callOrder: function assertCallOrder() {
+            verifyIsStub.apply(null, arguments);
+            var expected = "", actual = "";
+
+            if (!sinon.calledInOrder(arguments)) {
+                try {
+                    expected = [].join.call(arguments, ", ");
+                    var calls = slice.call(arguments);
+                    var i = calls.length;
+                    while (i) {
+                        if (!calls[--i].called) {
+                            calls.splice(i, 1);
+                        }
+                    }
+                    actual = sinon.orderByFirstCall(calls).join(", ");
+                } catch (e) {
+                    // If this fails, we'll just fall back to the blank string
+                }
+
+                failAssertion(this, "expected " + expected + " to be " +
+                              "called in order but were called as " + actual);
+            } else {
+                assert.pass("callOrder");
+            }
+        },
+
+        callCount: function assertCallCount(method, count) {
+            verifyIsStub(method);
+
+            if (method.callCount != count) {
+                var msg = "expected %n to be called " + sinon.timesInWords(count) +
+                    " but was called %c%C";
+                failAssertion(this, method.printf(msg));
+            } else {
+                assert.pass("callCount");
+            }
+        },
+
+        expose: function expose(target, options) {
+            if (!target) {
+                throw new TypeError("target is null or undefined");
+            }
+
+            var o = options || {};
+            var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix;
+            var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail;
+
+            for (var method in this) {
+                if (method != "export" && (includeFail || !/^(fail)/.test(method))) {
+                    target[exposedName(prefix, method)] = this[method];
+                }
+            }
+
+            return target;
+        },
+
+        match: function match(actual, expectation) {
+            var matcher = sinon.match(expectation);
+            if (matcher.test(actual)) {
+                assert.pass("match");
+            } else {
+                var formatted = [
+                    "expected value to match",
+                    "    expected = " + sinon.format(expectation),
+                    "    actual = " + sinon.format(actual)
+                ]
+                failAssertion(this, formatted.join("\n"));
+            }
+        }
+    };
+
+    mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called");
+    mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; },
+                          "expected %n to not have been called but was called %c%C");
+    mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C");
+    mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C");
+    mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C");
+    mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t");
+    mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t");
+    mirrorPropAsAssertion("calledWithNew", "expected %n to be called with new");
+    mirrorPropAsAssertion("alwaysCalledWithNew", "expected %n to always be called with new");
+    mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C");
+    mirrorPropAsAssertion("calledWithMatch", "expected %n to be called with match %*%C");
+    mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C");
+    mirrorPropAsAssertion("alwaysCalledWithMatch", "expected %n to always be called with match %*%C");
+    mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C");
+    mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C");
+    mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C");
+    mirrorPropAsAssertion("neverCalledWithMatch", "expected %n to never be called with match %*%C");
+    mirrorPropAsAssertion("threw", "%n did not throw exception%C");
+    mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C");
+
+    sinon.assert = assert;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = assert; });
+    } else if (commonJSModule) {
+        module.exports = assert;
+    }
+}(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : (typeof self != "undefined") ? self : global));
+
+/**
+ * @depend ../../sinon.js
+ * @depend event.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global sinon, module, require, XDomainRequest*/
+/**
+ * Fake XDomainRequest object
+ */
+
+if (typeof sinon == "undefined") {
+    this.sinon = {};
+}
+sinon.xdr = { XDomainRequest: this.XDomainRequest };
+
+// wrapper for global
+(function (global) {
+    var xdr = sinon.xdr;
+    xdr.GlobalXDomainRequest = global.XDomainRequest;
+    xdr.supportsXDR = typeof xdr.GlobalXDomainRequest != "undefined";
+    xdr.workingXDR = xdr.supportsXDR ? xdr.GlobalXDomainRequest :  false;
+
+    function FakeXDomainRequest() {
+        this.readyState = FakeXDomainRequest.UNSENT;
+        this.requestBody = null;
+        this.requestHeaders = {};
+        this.status = 0;
+        this.timeout = null;
+
+        if (typeof FakeXDomainRequest.onCreate == "function") {
+            FakeXDomainRequest.onCreate(this);
+        }
+    }
+
+    function verifyState(xdr) {
+        if (xdr.readyState !== FakeXDomainRequest.OPENED) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+
+        if (xdr.sendFlag) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+    }
+
+    function verifyRequestSent(xdr) {
+        if (xdr.readyState == FakeXDomainRequest.UNSENT) {
+            throw new Error("Request not sent");
+        }
+        if (xdr.readyState == FakeXDomainRequest.DONE) {
+            throw new Error("Request done");
+        }
+    }
+
+    function verifyResponseBodyType(body) {
+        if (typeof body != "string") {
+            var error = new Error("Attempted to respond to fake XDomainRequest with " +
+                                  body + ", which is not a string.");
+            error.name = "InvalidBodyException";
+            throw error;
+        }
+    }
+
+    sinon.extend(FakeXDomainRequest.prototype, sinon.EventTarget, {
+        open: function open(method, url) {
+            this.method = method;
+            this.url = url;
+
+            this.responseText = null;
+            this.sendFlag = false;
+
+            this.readyStateChange(FakeXDomainRequest.OPENED);
+        },
+
+        readyStateChange: function readyStateChange(state) {
+            this.readyState = state;
+            var eventName = '';
+            switch (this.readyState) {
+            case FakeXDomainRequest.UNSENT:
+                break;
+            case FakeXDomainRequest.OPENED:
+                break;
+            case FakeXDomainRequest.LOADING:
+                if (this.sendFlag){
+                    //raise the progress event
+                    eventName = 'onprogress';
+                }
+                break;
+            case FakeXDomainRequest.DONE:
+                if (this.isTimeout){
+                    eventName = 'ontimeout'
+                }
+                else if (this.errorFlag || (this.status < 200 || this.status > 299)) {
+                    eventName = 'onerror';
+                }
+                else {
+                    eventName = 'onload'
+                }
+                break;
+            }
+
+            // raising event (if defined)
+            if (eventName) {
+                if (typeof this[eventName] == "function") {
+                    try {
+                        this[eventName]();
+                    } catch (e) {
+                        sinon.logError("Fake XHR " + eventName + " handler", e);
+                    }
+                }
+            }
+        },
+
+        send: function send(data) {
+            verifyState(this);
+
+            if (!/^(get|head)$/i.test(this.method)) {
+                this.requestBody = data;
+            }
+            this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
+
+            this.errorFlag = false;
+            this.sendFlag = true;
+            this.readyStateChange(FakeXDomainRequest.OPENED);
+
+            if (typeof this.onSend == "function") {
+                this.onSend(this);
+            }
+        },
+
+        abort: function abort() {
+            this.aborted = true;
+            this.responseText = null;
+            this.errorFlag = true;
+
+            if (this.readyState > sinon.FakeXDomainRequest.UNSENT && this.sendFlag) {
+                this.readyStateChange(sinon.FakeXDomainRequest.DONE);
+                this.sendFlag = false;
+            }
+        },
+
+        setResponseBody: function setResponseBody(body) {
+            verifyRequestSent(this);
+            verifyResponseBodyType(body);
+
+            var chunkSize = this.chunkSize || 10;
+            var index = 0;
+            this.responseText = "";
+
+            do {
+                this.readyStateChange(FakeXDomainRequest.LOADING);
+                this.responseText += body.substring(index, index + chunkSize);
+                index += chunkSize;
+            } while (index < body.length);
+
+            this.readyStateChange(FakeXDomainRequest.DONE);
+        },
+
+        respond: function respond(status, contentType, body) {
+            // content-type ignored, since XDomainRequest does not carry this
+            // we keep the same syntax for respond(...) as for FakeXMLHttpRequest to ease
+            // test integration across browsers
+            this.status = typeof status == "number" ? status : 200;
+            this.setResponseBody(body || "");
+        },
+
+        simulatetimeout: function(){
+            this.status = 0;
+            this.isTimeout = true;
+            // Access to this should actually throw an error
+            this.responseText = undefined;
+            this.readyStateChange(FakeXDomainRequest.DONE);
+        }
+    });
+
+    sinon.extend(FakeXDomainRequest, {
+        UNSENT: 0,
+        OPENED: 1,
+        LOADING: 3,
+        DONE: 4
+    });
+
+    sinon.useFakeXDomainRequest = function () {
+        sinon.FakeXDomainRequest.restore = function restore(keepOnCreate) {
+            if (xdr.supportsXDR) {
+                global.XDomainRequest = xdr.GlobalXDomainRequest;
+            }
+
+            delete sinon.FakeXDomainRequest.restore;
+
+            if (keepOnCreate !== true) {
+                delete sinon.FakeXDomainRequest.onCreate;
+            }
+        };
+        if (xdr.supportsXDR) {
+            global.XDomainRequest = sinon.FakeXDomainRequest;
+        }
+        return sinon.FakeXDomainRequest;
+    };
+
+    sinon.FakeXDomainRequest = FakeXDomainRequest;
+})(this);
+
+if (typeof module == "object" && typeof require == "function") {
+    module.exports = sinon;
+}
+
+return sinon;}.call(typeof window != 'undefined' && window || {}));
diff --git a/resources/lib/sinonjs/sinon-1.9.0.js b/resources/lib/sinonjs/sinon-1.9.0.js
deleted file mode 100644 (file)
index 428b729..0000000
+++ /dev/null
@@ -1,4794 +0,0 @@
-/**
- * Sinon.JS 1.9.0, 2014/03/05
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
- *
- * (The BSD License)
- * 
- * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no
- * All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- * 
- *     * Redistributions of source code must retain the above copyright notice,
- *       this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above copyright notice,
- *       this list of conditions and the following disclaimer in the documentation
- *       and/or other materials provided with the distribution.
- *     * Neither the name of Christian Johansen nor the names of his contributors
- *       may be used to endorse or promote products derived from this software
- *       without specific prior written permission.
- * 
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-this.sinon = (function () {
-var samsam, formatio;
-function define(mod, deps, fn) { if (mod == "samsam") { samsam = deps(); } else { formatio = fn(samsam); } }
-define.amd = true;
-((typeof define === "function" && define.amd && function (m) { define("samsam", m); }) ||
- (typeof module === "object" &&
-      function (m) { module.exports = m(); }) || // Node
- function (m) { this.samsam = m(); } // Browser globals
-)(function () {
-    var o = Object.prototype;
-    var div = typeof document !== "undefined" && document.createElement("div");
-
-    function isNaN(value) {
-        // Unlike global isNaN, this avoids type coercion
-        // typeof check avoids IE host object issues, hat tip to
-        // lodash
-        var val = value; // JsLint thinks value !== value is "weird"
-        return typeof value === "number" && value !== val;
-    }
-
-    function getClass(value) {
-        // Returns the internal [[Class]] by calling Object.prototype.toString
-        // with the provided value as this. Return value is a string, naming the
-        // internal class, e.g. "Array"
-        return o.toString.call(value).split(/[ \]]/)[1];
-    }
-
-    /**
-     * @name samsam.isArguments
-     * @param Object object
-     *
-     * Returns ``true`` if ``object`` is an ``arguments`` object,
-     * ``false`` otherwise.
-     */
-    function isArguments(object) {
-        if (typeof object !== "object" || typeof object.length !== "number" ||
-                getClass(object) === "Array") {
-            return false;
-        }
-        if (typeof object.callee == "function") { return true; }
-        try {
-            object[object.length] = 6;
-            delete object[object.length];
-        } catch (e) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * @name samsam.isElement
-     * @param Object object
-     *
-     * Returns ``true`` if ``object`` is a DOM element node. Unlike
-     * Underscore.js/lodash, this function will return ``false`` if ``object``
-     * is an *element-like* object, i.e. a regular object with a ``nodeType``
-     * property that holds the value ``1``.
-     */
-    function isElement(object) {
-        if (!object || object.nodeType !== 1 || !div) { return false; }
-        try {
-            object.appendChild(div);
-            object.removeChild(div);
-        } catch (e) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * @name samsam.keys
-     * @param Object object
-     *
-     * Return an array of own property names.
-     */
-    function keys(object) {
-        var ks = [], prop;
-        for (prop in object) {
-            if (o.hasOwnProperty.call(object, prop)) { ks.push(prop); }
-        }
-        return ks;
-    }
-
-    /**
-     * @name samsam.isDate
-     * @param Object value
-     *
-     * Returns true if the object is a ``Date``, or *date-like*. Duck typing
-     * of date objects work by checking that the object has a ``getTime``
-     * function whose return value equals the return value from the object's
-     * ``valueOf``.
-     */
-    function isDate(value) {
-        return typeof value.getTime == "function" &&
-            value.getTime() == value.valueOf();
-    }
-
-    /**
-     * @name samsam.isNegZero
-     * @param Object value
-     *
-     * Returns ``true`` if ``value`` is ``-0``.
-     */
-    function isNegZero(value) {
-        return value === 0 && 1 / value === -Infinity;
-    }
-
-    /**
-     * @name samsam.equal
-     * @param Object obj1
-     * @param Object obj2
-     *
-     * Returns ``true`` if two objects are strictly equal. Compared to
-     * ``===`` there are two exceptions:
-     *
-     *   - NaN is considered equal to NaN
-     *   - -0 and +0 are not considered equal
-     */
-    function identical(obj1, obj2) {
-        if (obj1 === obj2 || (isNaN(obj1) && isNaN(obj2))) {
-            return obj1 !== 0 || isNegZero(obj1) === isNegZero(obj2);
-        }
-    }
-
-
-    /**
-     * @name samsam.deepEqual
-     * @param Object obj1
-     * @param Object obj2
-     *
-     * Deep equal comparison. Two values are "deep equal" if:
-     *
-     *   - They are equal, according to samsam.identical
-     *   - They are both date objects representing the same time
-     *   - They are both arrays containing elements that are all deepEqual
-     *   - They are objects with the same set of properties, and each property
-     *     in ``obj1`` is deepEqual to the corresponding property in ``obj2``
-     *
-     * Supports cyclic objects.
-     */
-    function deepEqualCyclic(obj1, obj2) {
-
-        // used for cyclic comparison
-        // contain already visited objects
-        var objects1 = [],
-            objects2 = [],
-        // contain pathes (position in the object structure)
-        // of the already visited objects
-        // indexes same as in objects arrays
-            paths1 = [],
-            paths2 = [],
-        // contains combinations of already compared objects
-        // in the manner: { "$1['ref']$2['ref']": true }
-            compared = {};
-
-        /**
-         * used to check, if the value of a property is an object
-         * (cyclic logic is only needed for objects)
-         * only needed for cyclic logic
-         */
-        function isObject(value) {
-
-            if (typeof value === 'object' && value !== null &&
-                    !(value instanceof Boolean) &&
-                    !(value instanceof Date)    &&
-                    !(value instanceof Number)  &&
-                    !(value instanceof RegExp)  &&
-                    !(value instanceof String)) {
-
-                return true;
-            }
-
-            return false;
-        }
-
-        /**
-         * returns the index of the given object in the
-         * given objects array, -1 if not contained
-         * only needed for cyclic logic
-         */
-        function getIndex(objects, obj) {
-
-            var i;
-            for (i = 0; i < objects.length; i++) {
-                if (objects[i] === obj) {
-                    return i;
-                }
-            }
-
-            return -1;
-        }
-
-        // does the recursion for the deep equal check
-        return (function deepEqual(obj1, obj2, path1, path2) {
-            var type1 = typeof obj1;
-            var type2 = typeof obj2;
-
-            // == null also matches undefined
-            if (obj1 === obj2 ||
-                    isNaN(obj1) || isNaN(obj2) ||
-                    obj1 == null || obj2 == null ||
-                    type1 !== "object" || type2 !== "object") {
-
-                return identical(obj1, obj2);
-            }
-
-            // Elements are only equal if identical(expected, actual)
-            if (isElement(obj1) || isElement(obj2)) { return false; }
-
-            var isDate1 = isDate(obj1), isDate2 = isDate(obj2);
-            if (isDate1 || isDate2) {
-                if (!isDate1 || !isDate2 || obj1.getTime() !== obj2.getTime()) {
-                    return false;
-                }
-            }
-
-            if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
-                if (obj1.toString() !== obj2.toString()) { return false; }
-            }
-
-            var class1 = getClass(obj1);
-            var class2 = getClass(obj2);
-            var keys1 = keys(obj1);
-            var keys2 = keys(obj2);
-
-            if (isArguments(obj1) || isArguments(obj2)) {
-                if (obj1.length !== obj2.length) { return false; }
-            } else {
-                if (type1 !== type2 || class1 !== class2 ||
-                        keys1.length !== keys2.length) {
-                    return false;
-                }
-            }
-
-            var key, i, l,
-                // following vars are used for the cyclic logic
-                value1, value2,
-                isObject1, isObject2,
-                index1, index2,
-                newPath1, newPath2;
-
-            for (i = 0, l = keys1.length; i < l; i++) {
-                key = keys1[i];
-                if (!o.hasOwnProperty.call(obj2, key)) {
-                    return false;
-                }
-
-                // Start of the cyclic logic
-
-                value1 = obj1[key];
-                value2 = obj2[key];
-
-                isObject1 = isObject(value1);
-                isObject2 = isObject(value2);
-
-                // determine, if the objects were already visited
-                // (it's faster to check for isObject first, than to
-                // get -1 from getIndex for non objects)
-                index1 = isObject1 ? getIndex(objects1, value1) : -1;
-                index2 = isObject2 ? getIndex(objects2, value2) : -1;
-
-                // determine the new pathes of the objects
-                // - for non cyclic objects the current path will be extended
-                //   by current property name
-                // - for cyclic objects the stored path is taken
-                newPath1 = index1 !== -1
-                    ? paths1[index1]
-                    : path1 + '[' + JSON.stringify(key) + ']';
-                newPath2 = index2 !== -1
-                    ? paths2[index2]
-                    : path2 + '[' + JSON.stringify(key) + ']';
-
-                // stop recursion if current objects are already compared
-                if (compared[newPath1 + newPath2]) {
-                    return true;
-                }
-
-                // remember the current objects and their pathes
-                if (index1 === -1 && isObject1) {
-                    objects1.push(value1);
-                    paths1.push(newPath1);
-                }
-                if (index2 === -1 && isObject2) {
-                    objects2.push(value2);
-                    paths2.push(newPath2);
-                }
-
-                // remember that the current objects are already compared
-                if (isObject1 && isObject2) {
-                    compared[newPath1 + newPath2] = true;
-                }
-
-                // End of cyclic logic
-
-                // neither value1 nor value2 is a cycle
-                // continue with next level
-                if (!deepEqual(value1, value2, newPath1, newPath2)) {
-                    return false;
-                }
-            }
-
-            return true;
-
-        }(obj1, obj2, '$1', '$2'));
-    }
-
-    var match;
-
-    function arrayContains(array, subset) {
-        if (subset.length === 0) { return true; }
-        var i, l, j, k;
-        for (i = 0, l = array.length; i < l; ++i) {
-            if (match(array[i], subset[0])) {
-                for (j = 0, k = subset.length; j < k; ++j) {
-                    if (!match(array[i + j], subset[j])) { return false; }
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * @name samsam.match
-     * @param Object object
-     * @param Object matcher
-     *
-     * Compare arbitrary value ``object`` with matcher.
-     */
-    match = function match(object, matcher) {
-        if (matcher && typeof matcher.test === "function") {
-            return matcher.test(object);
-        }
-
-        if (typeof matcher === "function") {
-            return matcher(object) === true;
-        }
-
-        if (typeof matcher === "string") {
-            matcher = matcher.toLowerCase();
-            var notNull = typeof object === "string" || !!object;
-            return notNull &&
-                (String(object)).toLowerCase().indexOf(matcher) >= 0;
-        }
-
-        if (typeof matcher === "number") {
-            return matcher === object;
-        }
-
-        if (typeof matcher === "boolean") {
-            return matcher === object;
-        }
-
-        if (getClass(object) === "Array" && getClass(matcher) === "Array") {
-            return arrayContains(object, matcher);
-        }
-
-        if (matcher && typeof matcher === "object") {
-            var prop;
-            for (prop in matcher) {
-                if (!match(object[prop], matcher[prop])) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        throw new Error("Matcher was not a string, a number, a " +
-                        "function, a boolean or an object");
-    };
-
-    return {
-        isArguments: isArguments,
-        isElement: isElement,
-        isDate: isDate,
-        isNegZero: isNegZero,
-        identical: identical,
-        deepEqual: deepEqualCyclic,
-        match: match,
-        keys: keys
-    };
-});
-((typeof define === "function" && define.amd && function (m) {
-    define("formatio", ["samsam"], m);
-}) || (typeof module === "object" && function (m) {
-    module.exports = m(require("samsam"));
-}) || function (m) { this.formatio = m(this.samsam); }
-)(function (samsam) {
-    
-    var formatio = {
-        excludeConstructors: ["Object", /^.$/],
-        quoteStrings: true
-    };
-
-    var hasOwn = Object.prototype.hasOwnProperty;
-
-    var specialObjects = [];
-    if (typeof global !== "undefined") {
-        specialObjects.push({ object: global, value: "[object global]" });
-    }
-    if (typeof document !== "undefined") {
-        specialObjects.push({
-            object: document,
-            value: "[object HTMLDocument]"
-        });
-    }
-    if (typeof window !== "undefined") {
-        specialObjects.push({ object: window, value: "[object Window]" });
-    }
-
-    function functionName(func) {
-        if (!func) { return ""; }
-        if (func.displayName) { return func.displayName; }
-        if (func.name) { return func.name; }
-        var matches = func.toString().match(/function\s+([^\(]+)/m);
-        return (matches && matches[1]) || "";
-    }
-
-    function constructorName(f, object) {
-        var name = functionName(object && object.constructor);
-        var excludes = f.excludeConstructors ||
-                formatio.excludeConstructors || [];
-
-        var i, l;
-        for (i = 0, l = excludes.length; i < l; ++i) {
-            if (typeof excludes[i] === "string" && excludes[i] === name) {
-                return "";
-            } else if (excludes[i].test && excludes[i].test(name)) {
-                return "";
-            }
-        }
-
-        return name;
-    }
-
-    function isCircular(object, objects) {
-        if (typeof object !== "object") { return false; }
-        var i, l;
-        for (i = 0, l = objects.length; i < l; ++i) {
-            if (objects[i] === object) { return true; }
-        }
-        return false;
-    }
-
-    function ascii(f, object, processed, indent) {
-        if (typeof object === "string") {
-            var qs = f.quoteStrings;
-            var quote = typeof qs !== "boolean" || qs;
-            return processed || quote ? '"' + object + '"' : object;
-        }
-
-        if (typeof object === "function" && !(object instanceof RegExp)) {
-            return ascii.func(object);
-        }
-
-        processed = processed || [];
-
-        if (isCircular(object, processed)) { return "[Circular]"; }
-
-        if (Object.prototype.toString.call(object) === "[object Array]") {
-            return ascii.array.call(f, object, processed);
-        }
-
-        if (!object) { return String((1/object) === -Infinity ? "-0" : object); }
-        if (samsam.isElement(object)) { return ascii.element(object); }
-
-        if (typeof object.toString === "function" &&
-                object.toString !== Object.prototype.toString) {
-            return object.toString();
-        }
-
-        var i, l;
-        for (i = 0, l = specialObjects.length; i < l; i++) {
-            if (object === specialObjects[i].object) {
-                return specialObjects[i].value;
-            }
-        }
-
-        return ascii.object.call(f, object, processed, indent);
-    }
-
-    ascii.func = function (func) {
-        return "function " + functionName(func) + "() {}";
-    };
-
-    ascii.array = function (array, processed) {
-        processed = processed || [];
-        processed.push(array);
-        var i, l, pieces = [];
-        for (i = 0, l = array.length; i < l; ++i) {
-            pieces.push(ascii(this, array[i], processed));
-        }
-        return "[" + pieces.join(", ") + "]";
-    };
-
-    ascii.object = function (object, processed, indent) {
-        processed = processed || [];
-        processed.push(object);
-        indent = indent || 0;
-        var pieces = [], properties = samsam.keys(object).sort();
-        var length = 3;
-        var prop, str, obj, i, l;
-
-        for (i = 0, l = properties.length; i < l; ++i) {
-            prop = properties[i];
-            obj = object[prop];
-
-            if (isCircular(obj, processed)) {
-                str = "[Circular]";
-            } else {
-                str = ascii(this, obj, processed, indent + 2);
-            }
-
-            str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str;
-            length += str.length;
-            pieces.push(str);
-        }
-
-        var cons = constructorName(this, object);
-        var prefix = cons ? "[" + cons + "] " : "";
-        var is = "";
-        for (i = 0, l = indent; i < l; ++i) { is += " "; }
-
-        if (length + indent > 80) {
-            return prefix + "{\n  " + is + pieces.join(",\n  " + is) + "\n" +
-                is + "}";
-        }
-        return prefix + "{ " + pieces.join(", ") + " }";
-    };
-
-    ascii.element = function (element) {
-        var tagName = element.tagName.toLowerCase();
-        var attrs = element.attributes, attr, pairs = [], attrName, i, l, val;
-
-        for (i = 0, l = attrs.length; i < l; ++i) {
-            attr = attrs.item(i);
-            attrName = attr.nodeName.toLowerCase().replace("html:", "");
-            val = attr.nodeValue;
-            if (attrName !== "contenteditable" || val !== "inherit") {
-                if (!!val) { pairs.push(attrName + "=\"" + val + "\""); }
-            }
-        }
-
-        var formatted = "<" + tagName + (pairs.length > 0 ? " " : "");
-        var content = element.innerHTML;
-
-        if (content.length > 20) {
-            content = content.substr(0, 20) + "[...]";
-        }
-
-        var res = formatted + pairs.join(" ") + ">" + content +
-                "</" + tagName + ">";
-
-        return res.replace(/ contentEditable="inherit"/, "");
-    };
-
-    function Formatio(options) {
-        for (var opt in options) {
-            this[opt] = options[opt];
-        }
-    }
-
-    Formatio.prototype = {
-        functionName: functionName,
-
-        configure: function (options) {
-            return new Formatio(options);
-        },
-
-        constructorName: function (object) {
-            return constructorName(this, object);
-        },
-
-        ascii: function (object, processed, indent) {
-            return ascii(this, object, processed, indent);
-        }
-    };
-
-    return Formatio.prototype;
-});
-/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/
-/*global module, require, __dirname, document*/
-/**
- * Sinon core utilities. For internal use only.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-var sinon = (function (formatio) {
-    var div = typeof document != "undefined" && document.createElement("div");
-    var hasOwn = Object.prototype.hasOwnProperty;
-
-    function isDOMNode(obj) {
-        var success = false;
-
-        try {
-            obj.appendChild(div);
-            success = div.parentNode == obj;
-        } catch (e) {
-            return false;
-        } finally {
-            try {
-                obj.removeChild(div);
-            } catch (e) {
-                // Remove failed, not much we can do about that
-            }
-        }
-
-        return success;
-    }
-
-    function isElement(obj) {
-        return div && obj && obj.nodeType === 1 && isDOMNode(obj);
-    }
-
-    function isFunction(obj) {
-        return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply);
-    }
-
-    function mirrorProperties(target, source) {
-        for (var prop in source) {
-            if (!hasOwn.call(target, prop)) {
-                target[prop] = source[prop];
-            }
-        }
-    }
-
-    function isRestorable (obj) {
-        return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon;
-    }
-
-    var sinon = {
-        wrapMethod: function wrapMethod(object, property, method) {
-            if (!object) {
-                throw new TypeError("Should wrap property of object");
-            }
-
-            if (typeof method != "function") {
-                throw new TypeError("Method wrapper should be function");
-            }
-
-            var wrappedMethod = object[property],
-                error;
-
-            if (!isFunction(wrappedMethod)) {
-                error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " +
-                                    property + " as function");
-            }
-
-            if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
-                error = new TypeError("Attempted to wrap " + property + " which is already wrapped");
-            }
-
-            if (wrappedMethod.calledBefore) {
-                var verb = !!wrappedMethod.returns ? "stubbed" : "spied on";
-                error = new TypeError("Attempted to wrap " + property + " which is already " + verb);
-            }
-
-            if (error) {
-                if (wrappedMethod._stack) {
-                    error.stack += '\n--------------\n' + wrappedMethod._stack;
-                }
-                throw error;
-            }
-
-            // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem
-            // when using hasOwn.call on objects from other frames.
-            var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property);
-            object[property] = method;
-            method.displayName = property;
-            // Set up a stack trace which can be used later to find what line of
-            // code the original method was created on.
-            method._stack = (new Error('Stack Trace for original')).stack;
-
-            method.restore = function () {
-                // For prototype properties try to reset by delete first.
-                // If this fails (ex: localStorage on mobile safari) then force a reset
-                // via direct assignment.
-                if (!owned) {
-                    delete object[property];
-                }
-                if (object[property] === method) {
-                    object[property] = wrappedMethod;
-                }
-            };
-
-            method.restore.sinon = true;
-            mirrorProperties(method, wrappedMethod);
-
-            return method;
-        },
-
-        extend: function extend(target) {
-            for (var i = 1, l = arguments.length; i < l; i += 1) {
-                for (var prop in arguments[i]) {
-                    if (arguments[i].hasOwnProperty(prop)) {
-                        target[prop] = arguments[i][prop];
-                    }
-
-                    // DONT ENUM bug, only care about toString
-                    if (arguments[i].hasOwnProperty("toString") &&
-                        arguments[i].toString != target.toString) {
-                        target.toString = arguments[i].toString;
-                    }
-                }
-            }
-
-            return target;
-        },
-
-        create: function create(proto) {
-            var F = function () {};
-            F.prototype = proto;
-            return new F();
-        },
-
-        deepEqual: function deepEqual(a, b) {
-            if (sinon.match && sinon.match.isMatcher(a)) {
-                return a.test(b);
-            }
-            if (typeof a != "object" || typeof b != "object") {
-                return a === b;
-            }
-
-            if (isElement(a) || isElement(b)) {
-                return a === b;
-            }
-
-            if (a === b) {
-                return true;
-            }
-
-            if ((a === null && b !== null) || (a !== null && b === null)) {
-                return false;
-            }
-
-            if (a instanceof RegExp && b instanceof RegExp) {
-              return (a.source === b.source) && (a.global === b.global) && 
-                (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline);
-            }
-
-            var aString = Object.prototype.toString.call(a);
-            if (aString != Object.prototype.toString.call(b)) {
-                return false;
-            }
-
-            if (aString == "[object Date]") {
-                return a.valueOf() === b.valueOf();
-            }
-
-            var prop, aLength = 0, bLength = 0;
-
-            if (aString == "[object Array]" && a.length !== b.length) {
-                return false;
-            }
-
-            for (prop in a) {
-                aLength += 1;
-
-                if (!deepEqual(a[prop], b[prop])) {
-                    return false;
-                }
-            }
-
-            for (prop in b) {
-                bLength += 1;
-            }
-
-            return aLength == bLength;
-        },
-
-        functionName: function functionName(func) {
-            var name = func.displayName || func.name;
-
-            // Use function decomposition as a last resort to get function
-            // name. Does not rely on function decomposition to work - if it
-            // doesn't debugging will be slightly less informative
-            // (i.e. toString will say 'spy' rather than 'myFunc').
-            if (!name) {
-                var matches = func.toString().match(/function ([^\s\(]+)/);
-                name = matches && matches[1];
-            }
-
-            return name;
-        },
-
-        functionToString: function toString() {
-            if (this.getCall && this.callCount) {
-                var thisValue, prop, i = this.callCount;
-
-                while (i--) {
-                    thisValue = this.getCall(i).thisValue;
-
-                    for (prop in thisValue) {
-                        if (thisValue[prop] === this) {
-                            return prop;
-                        }
-                    }
-                }
-            }
-
-            return this.displayName || "sinon fake";
-        },
-
-        getConfig: function (custom) {
-            var config = {};
-            custom = custom || {};
-            var defaults = sinon.defaultConfig;
-
-            for (var prop in defaults) {
-                if (defaults.hasOwnProperty(prop)) {
-                    config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop];
-                }
-            }
-
-            return config;
-        },
-
-        format: function (val) {
-            return "" + val;
-        },
-
-        defaultConfig: {
-            injectIntoThis: true,
-            injectInto: null,
-            properties: ["spy", "stub", "mock", "clock", "server", "requests"],
-            useFakeTimers: true,
-            useFakeServer: true
-        },
-
-        timesInWords: function timesInWords(count) {
-            return count == 1 && "once" ||
-                count == 2 && "twice" ||
-                count == 3 && "thrice" ||
-                (count || 0) + " times";
-        },
-
-        calledInOrder: function (spies) {
-            for (var i = 1, l = spies.length; i < l; i++) {
-                if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) {
-                    return false;
-                }
-            }
-
-            return true;
-        },
-
-        orderByFirstCall: function (spies) {
-            return spies.sort(function (a, b) {
-                // uuid, won't ever be equal
-                var aCall = a.getCall(0);
-                var bCall = b.getCall(0);
-                var aId = aCall && aCall.callId || -1;
-                var bId = bCall && bCall.callId || -1;
-
-                return aId < bId ? -1 : 1;
-            });
-        },
-
-        log: function () {},
-
-        logError: function (label, err) {
-            var msg = label + " threw exception: ";
-            sinon.log(msg + "[" + err.name + "] " + err.message);
-            if (err.stack) { sinon.log(err.stack); }
-
-            setTimeout(function () {
-                err.message = msg + err.message;
-                throw err;
-            }, 0);
-        },
-
-        typeOf: function (value) {
-            if (value === null) {
-                return "null";
-            }
-            else if (value === undefined) {
-                return "undefined";
-            }
-            var string = Object.prototype.toString.call(value);
-            return string.substring(8, string.length - 1).toLowerCase();
-        },
-
-        createStubInstance: function (constructor) {
-            if (typeof constructor !== "function") {
-                throw new TypeError("The constructor should be a function.");
-            }
-            return sinon.stub(sinon.create(constructor.prototype));
-        },
-
-        restore: function (object) {
-            if (object !== null && typeof object === "object") {
-                for (var prop in object) {
-                    if (isRestorable(object[prop])) {
-                        object[prop].restore();
-                    }
-                }
-            }
-            else if (isRestorable(object)) {
-                object.restore();
-            }
-        }
-    };
-
-    var isNode = typeof module !== "undefined" && module.exports;
-    var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd;
-
-    if (isAMD) {
-        define(function(){
-            return sinon;
-        });
-    } else if (isNode) {
-        try {
-            formatio = require("formatio");
-        } catch (e) {}
-        module.exports = sinon;
-        module.exports.spy = require("./sinon/spy");
-        module.exports.spyCall = require("./sinon/call");
-        module.exports.behavior = require("./sinon/behavior");
-        module.exports.stub = require("./sinon/stub");
-        module.exports.mock = require("./sinon/mock");
-        module.exports.collection = require("./sinon/collection");
-        module.exports.assert = require("./sinon/assert");
-        module.exports.sandbox = require("./sinon/sandbox");
-        module.exports.test = require("./sinon/test");
-        module.exports.testCase = require("./sinon/test_case");
-        module.exports.assert = require("./sinon/assert");
-        module.exports.match = require("./sinon/match");
-    }
-
-    if (formatio) {
-        var formatter = formatio.configure({ quoteStrings: false });
-        sinon.format = function () {
-            return formatter.ascii.apply(formatter, arguments);
-        };
-    } else if (isNode) {
-        try {
-            var util = require("util");
-            sinon.format = function (value) {
-                return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value;
-            };
-        } catch (e) {
-            /* Node, but no util module - would be very old, but better safe than
-             sorry */
-        }
-    }
-
-    return sinon;
-}(typeof formatio == "object" && formatio));
-
-/* @depend ../sinon.js */
-/*jslint eqeqeq: false, onevar: false, plusplus: false*/
-/*global module, require, sinon*/
-/**
- * Match functions
- *
- * @author Maximilian Antoni (mail@maxantoni.de)
- * @license BSD
- *
- * Copyright (c) 2012 Maximilian Antoni
- */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    function assertType(value, type, name) {
-        var actual = sinon.typeOf(value);
-        if (actual !== type) {
-            throw new TypeError("Expected type of " + name + " to be " +
-                type + ", but was " + actual);
-        }
-    }
-
-    var matcher = {
-        toString: function () {
-            return this.message;
-        }
-    };
-
-    function isMatcher(object) {
-        return matcher.isPrototypeOf(object);
-    }
-
-    function matchObject(expectation, actual) {
-        if (actual === null || actual === undefined) {
-            return false;
-        }
-        for (var key in expectation) {
-            if (expectation.hasOwnProperty(key)) {
-                var exp = expectation[key];
-                var act = actual[key];
-                if (match.isMatcher(exp)) {
-                    if (!exp.test(act)) {
-                        return false;
-                    }
-                } else if (sinon.typeOf(exp) === "object") {
-                    if (!matchObject(exp, act)) {
-                        return false;
-                    }
-                } else if (!sinon.deepEqual(exp, act)) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
-    matcher.or = function (m2) {
-        if (!arguments.length) {
-            throw new TypeError("Matcher expected");
-        } else if (!isMatcher(m2)) {
-            m2 = match(m2);
-        }
-        var m1 = this;
-        var or = sinon.create(matcher);
-        or.test = function (actual) {
-            return m1.test(actual) || m2.test(actual);
-        };
-        or.message = m1.message + ".or(" + m2.message + ")";
-        return or;
-    };
-
-    matcher.and = function (m2) {
-        if (!arguments.length) {
-            throw new TypeError("Matcher expected");
-        } else if (!isMatcher(m2)) {
-            m2 = match(m2);
-        }
-        var m1 = this;
-        var and = sinon.create(matcher);
-        and.test = function (actual) {
-            return m1.test(actual) && m2.test(actual);
-        };
-        and.message = m1.message + ".and(" + m2.message + ")";
-        return and;
-    };
-
-    var match = function (expectation, message) {
-        var m = sinon.create(matcher);
-        var type = sinon.typeOf(expectation);
-        switch (type) {
-        case "object":
-            if (typeof expectation.test === "function") {
-                m.test = function (actual) {
-                    return expectation.test(actual) === true;
-                };
-                m.message = "match(" + sinon.functionName(expectation.test) + ")";
-                return m;
-            }
-            var str = [];
-            for (var key in expectation) {
-                if (expectation.hasOwnProperty(key)) {
-                    str.push(key + ": " + expectation[key]);
-                }
-            }
-            m.test = function (actual) {
-                return matchObject(expectation, actual);
-            };
-            m.message = "match(" + str.join(", ") + ")";
-            break;
-        case "number":
-            m.test = function (actual) {
-                return expectation == actual;
-            };
-            break;
-        case "string":
-            m.test = function (actual) {
-                if (typeof actual !== "string") {
-                    return false;
-                }
-                return actual.indexOf(expectation) !== -1;
-            };
-            m.message = "match(\"" + expectation + "\")";
-            break;
-        case "regexp":
-            m.test = function (actual) {
-                if (typeof actual !== "string") {
-                    return false;
-                }
-                return expectation.test(actual);
-            };
-            break;
-        case "function":
-            m.test = expectation;
-            if (message) {
-                m.message = message;
-            } else {
-                m.message = "match(" + sinon.functionName(expectation) + ")";
-            }
-            break;
-        default:
-            m.test = function (actual) {
-              return sinon.deepEqual(expectation, actual);
-            };
-        }
-        if (!m.message) {
-            m.message = "match(" + expectation + ")";
-        }
-        return m;
-    };
-
-    match.isMatcher = isMatcher;
-
-    match.any = match(function () {
-        return true;
-    }, "any");
-
-    match.defined = match(function (actual) {
-        return actual !== null && actual !== undefined;
-    }, "defined");
-
-    match.truthy = match(function (actual) {
-        return !!actual;
-    }, "truthy");
-
-    match.falsy = match(function (actual) {
-        return !actual;
-    }, "falsy");
-
-    match.same = function (expectation) {
-        return match(function (actual) {
-            return expectation === actual;
-        }, "same(" + expectation + ")");
-    };
-
-    match.typeOf = function (type) {
-        assertType(type, "string", "type");
-        return match(function (actual) {
-            return sinon.typeOf(actual) === type;
-        }, "typeOf(\"" + type + "\")");
-    };
-
-    match.instanceOf = function (type) {
-        assertType(type, "function", "type");
-        return match(function (actual) {
-            return actual instanceof type;
-        }, "instanceOf(" + sinon.functionName(type) + ")");
-    };
-
-    function createPropertyMatcher(propertyTest, messagePrefix) {
-        return function (property, value) {
-            assertType(property, "string", "property");
-            var onlyProperty = arguments.length === 1;
-            var message = messagePrefix + "(\"" + property + "\"";
-            if (!onlyProperty) {
-                message += ", " + value;
-            }
-            message += ")";
-            return match(function (actual) {
-                if (actual === undefined || actual === null ||
-                        !propertyTest(actual, property)) {
-                    return false;
-                }
-                return onlyProperty || sinon.deepEqual(value, actual[property]);
-            }, message);
-        };
-    }
-
-    match.has = createPropertyMatcher(function (actual, property) {
-        if (typeof actual === "object") {
-            return property in actual;
-        }
-        return actual[property] !== undefined;
-    }, "has");
-
-    match.hasOwn = createPropertyMatcher(function (actual, property) {
-        return actual.hasOwnProperty(property);
-    }, "hasOwn");
-
-    match.bool = match.typeOf("boolean");
-    match.number = match.typeOf("number");
-    match.string = match.typeOf("string");
-    match.object = match.typeOf("object");
-    match.func = match.typeOf("function");
-    match.array = match.typeOf("array");
-    match.regexp = match.typeOf("regexp");
-    match.date = match.typeOf("date");
-
-    if (commonJSModule) {
-        module.exports = match;
-    } else {
-        sinon.match = match;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-/**
-  * @depend ../sinon.js
-  * @depend match.js
-  */
-/*jslint eqeqeq: false, onevar: false, plusplus: false*/
-/*global module, require, sinon*/
-/**
-  * Spy calls
-  *
-  * @author Christian Johansen (christian@cjohansen.no)
-  * @author Maximilian Antoni (mail@maxantoni.de)
-  * @license BSD
-  *
-  * Copyright (c) 2010-2013 Christian Johansen
-  * Copyright (c) 2013 Maximilian Antoni
-  */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    function throwYieldError(proxy, text, args) {
-        var msg = sinon.functionName(proxy) + text;
-        if (args.length) {
-            msg += " Received [" + slice.call(args).join(", ") + "]";
-        }
-        throw new Error(msg);
-    }
-
-    var slice = Array.prototype.slice;
-
-    var callProto = {
-        calledOn: function calledOn(thisValue) {
-            if (sinon.match && sinon.match.isMatcher(thisValue)) {
-                return thisValue.test(this.thisValue);
-            }
-            return this.thisValue === thisValue;
-        },
-
-        calledWith: function calledWith() {
-            for (var i = 0, l = arguments.length; i < l; i += 1) {
-                if (!sinon.deepEqual(arguments[i], this.args[i])) {
-                    return false;
-                }
-            }
-
-            return true;
-        },
-
-        calledWithMatch: function calledWithMatch() {
-            for (var i = 0, l = arguments.length; i < l; i += 1) {
-                var actual = this.args[i];
-                var expectation = arguments[i];
-                if (!sinon.match || !sinon.match(expectation).test(actual)) {
-                    return false;
-                }
-            }
-            return true;
-        },
-
-        calledWithExactly: function calledWithExactly() {
-            return arguments.length == this.args.length &&
-                this.calledWith.apply(this, arguments);
-        },
-
-        notCalledWith: function notCalledWith() {
-            return !this.calledWith.apply(this, arguments);
-        },
-
-        notCalledWithMatch: function notCalledWithMatch() {
-            return !this.calledWithMatch.apply(this, arguments);
-        },
-
-        returned: function returned(value) {
-            return sinon.deepEqual(value, this.returnValue);
-        },
-
-        threw: function threw(error) {
-            if (typeof error === "undefined" || !this.exception) {
-                return !!this.exception;
-            }
-
-            return this.exception === error || this.exception.name === error;
-        },
-
-        calledWithNew: function calledWithNew() {
-            return this.proxy.prototype && this.thisValue instanceof this.proxy;
-        },
-
-        calledBefore: function (other) {
-            return this.callId < other.callId;
-        },
-
-        calledAfter: function (other) {
-            return this.callId > other.callId;
-        },
-
-        callArg: function (pos) {
-            this.args[pos]();
-        },
-
-        callArgOn: function (pos, thisValue) {
-            this.args[pos].apply(thisValue);
-        },
-
-        callArgWith: function (pos) {
-            this.callArgOnWith.apply(this, [pos, null].concat(slice.call(arguments, 1)));
-        },
-
-        callArgOnWith: function (pos, thisValue) {
-            var args = slice.call(arguments, 2);
-            this.args[pos].apply(thisValue, args);
-        },
-
-        "yield": function () {
-            this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0)));
-        },
-
-        yieldOn: function (thisValue) {
-            var args = this.args;
-            for (var i = 0, l = args.length; i < l; ++i) {
-                if (typeof args[i] === "function") {
-                    args[i].apply(thisValue, slice.call(arguments, 1));
-                    return;
-                }
-            }
-            throwYieldError(this.proxy, " cannot yield since no callback was passed.", args);
-        },
-
-        yieldTo: function (prop) {
-            this.yieldToOn.apply(this, [prop, null].concat(slice.call(arguments, 1)));
-        },
-
-        yieldToOn: function (prop, thisValue) {
-            var args = this.args;
-            for (var i = 0, l = args.length; i < l; ++i) {
-                if (args[i] && typeof args[i][prop] === "function") {
-                    args[i][prop].apply(thisValue, slice.call(arguments, 2));
-                    return;
-                }
-            }
-            throwYieldError(this.proxy, " cannot yield to '" + prop +
-                "' since no callback was passed.", args);
-        },
-
-        toString: function () {
-            var callStr = this.proxy.toString() + "(";
-            var args = [];
-
-            for (var i = 0, l = this.args.length; i < l; ++i) {
-                args.push(sinon.format(this.args[i]));
-            }
-
-            callStr = callStr + args.join(", ") + ")";
-
-            if (typeof this.returnValue != "undefined") {
-                callStr += " => " + sinon.format(this.returnValue);
-            }
-
-            if (this.exception) {
-                callStr += " !" + this.exception.name;
-
-                if (this.exception.message) {
-                    callStr += "(" + this.exception.message + ")";
-                }
-            }
-
-            return callStr;
-        }
-    };
-
-    callProto.invokeCallback = callProto.yield;
-
-    function createSpyCall(spy, thisValue, args, returnValue, exception, id) {
-        if (typeof id !== "number") {
-            throw new TypeError("Call id is not a number");
-        }
-        var proxyCall = sinon.create(callProto);
-        proxyCall.proxy = spy;
-        proxyCall.thisValue = thisValue;
-        proxyCall.args = args;
-        proxyCall.returnValue = returnValue;
-        proxyCall.exception = exception;
-        proxyCall.callId = id;
-
-        return proxyCall;
-    }
-    createSpyCall.toString = callProto.toString; // used by mocks
-
-    if (commonJSModule) {
-        module.exports = createSpyCall;
-    } else {
-        sinon.spyCall = createSpyCall;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-
-/**
-  * @depend ../sinon.js
-  * @depend call.js
-  */
-/*jslint eqeqeq: false, onevar: false, plusplus: false*/
-/*global module, require, sinon*/
-/**
-  * Spy functions
-  *
-  * @author Christian Johansen (christian@cjohansen.no)
-  * @license BSD
-  *
-  * Copyright (c) 2010-2013 Christian Johansen
-  */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-    var push = Array.prototype.push;
-    var slice = Array.prototype.slice;
-    var callId = 0;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    function spy(object, property) {
-        if (!property && typeof object == "function") {
-            return spy.create(object);
-        }
-
-        if (!object && !property) {
-            return spy.create(function () { });
-        }
-
-        var method = object[property];
-        return sinon.wrapMethod(object, property, spy.create(method));
-    }
-
-    function matchingFake(fakes, args, strict) {
-        if (!fakes) {
-            return;
-        }
-
-        for (var i = 0, l = fakes.length; i < l; i++) {
-            if (fakes[i].matches(args, strict)) {
-                return fakes[i];
-            }
-        }
-    }
-
-    function incrementCallCount() {
-        this.called = true;
-        this.callCount += 1;
-        this.notCalled = false;
-        this.calledOnce = this.callCount == 1;
-        this.calledTwice = this.callCount == 2;
-        this.calledThrice = this.callCount == 3;
-    }
-
-    function createCallProperties() {
-        this.firstCall = this.getCall(0);
-        this.secondCall = this.getCall(1);
-        this.thirdCall = this.getCall(2);
-        this.lastCall = this.getCall(this.callCount - 1);
-    }
-
-    var vars = "a,b,c,d,e,f,g,h,i,j,k,l";
-    function createProxy(func) {
-        // Retain the function length:
-        var p;
-        if (func.length) {
-            eval("p = (function proxy(" + vars.substring(0, func.length * 2 - 1) +
-                ") { return p.invoke(func, this, slice.call(arguments)); });");
-        }
-        else {
-            p = function proxy() {
-                return p.invoke(func, this, slice.call(arguments));
-            };
-        }
-        return p;
-    }
-
-    var uuid = 0;
-
-    // Public API
-    var spyApi = {
-        reset: function () {
-            this.called = false;
-            this.notCalled = true;
-            this.calledOnce = false;
-            this.calledTwice = false;
-            this.calledThrice = false;
-            this.callCount = 0;
-            this.firstCall = null;
-            this.secondCall = null;
-            this.thirdCall = null;
-            this.lastCall = null;
-            this.args = [];
-            this.returnValues = [];
-            this.thisValues = [];
-            this.exceptions = [];
-            this.callIds = [];
-            if (this.fakes) {
-                for (var i = 0; i < this.fakes.length; i++) {
-                    this.fakes[i].reset();
-                }
-            }
-        },
-
-        create: function create(func) {
-            var name;
-
-            if (typeof func != "function") {
-                func = function () { };
-            } else {
-                name = sinon.functionName(func);
-            }
-
-            var proxy = createProxy(func);
-
-            sinon.extend(proxy, spy);
-            delete proxy.create;
-            sinon.extend(proxy, func);
-
-            proxy.reset();
-            proxy.prototype = func.prototype;
-            proxy.displayName = name || "spy";
-            proxy.toString = sinon.functionToString;
-            proxy._create = sinon.spy.create;
-            proxy.id = "spy#" + uuid++;
-
-            return proxy;
-        },
-
-        invoke: function invoke(func, thisValue, args) {
-            var matching = matchingFake(this.fakes, args);
-            var exception, returnValue;
-
-            incrementCallCount.call(this);
-            push.call(this.thisValues, thisValue);
-            push.call(this.args, args);
-            push.call(this.callIds, callId++);
-
-            try {
-                if (matching) {
-                    returnValue = matching.invoke(func, thisValue, args);
-                } else {
-                    returnValue = (this.func || func).apply(thisValue, args);
-                }
-
-                var thisCall = this.getCall(this.callCount - 1);
-                if (thisCall.calledWithNew() && typeof returnValue !== 'object') {
-                    returnValue = thisValue;
-                }
-            } catch (e) {
-                exception = e;
-            }
-
-            push.call(this.exceptions, exception);
-            push.call(this.returnValues, returnValue);
-
-            createCallProperties.call(this);
-
-            if (exception !== undefined) {
-                throw exception;
-            }
-
-            return returnValue;
-        },
-
-        getCall: function getCall(i) {
-            if (i < 0 || i >= this.callCount) {
-                return null;
-            }
-
-            return sinon.spyCall(this, this.thisValues[i], this.args[i],
-                                    this.returnValues[i], this.exceptions[i],
-                                    this.callIds[i]);
-        },
-
-        getCalls: function () {
-            var calls = [];
-            var i;
-
-            for (i = 0; i < this.callCount; i++) {
-                calls.push(this.getCall(i));
-            }
-
-            return calls;
-        },
-
-        calledBefore: function calledBefore(spyFn) {
-            if (!this.called) {
-                return false;
-            }
-
-            if (!spyFn.called) {
-                return true;
-            }
-
-            return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1];
-        },
-
-        calledAfter: function calledAfter(spyFn) {
-            if (!this.called || !spyFn.called) {
-                return false;
-            }
-
-            return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1];
-        },
-
-        withArgs: function () {
-            var args = slice.call(arguments);
-
-            if (this.fakes) {
-                var match = matchingFake(this.fakes, args, true);
-
-                if (match) {
-                    return match;
-                }
-            } else {
-                this.fakes = [];
-            }
-
-            var original = this;
-            var fake = this._create();
-            fake.matchingAguments = args;
-            fake.parent = this;
-            push.call(this.fakes, fake);
-
-            fake.withArgs = function () {
-                return original.withArgs.apply(original, arguments);
-            };
-
-            for (var i = 0; i < this.args.length; i++) {
-                if (fake.matches(this.args[i])) {
-                    incrementCallCount.call(fake);
-                    push.call(fake.thisValues, this.thisValues[i]);
-                    push.call(fake.args, this.args[i]);
-                    push.call(fake.returnValues, this.returnValues[i]);
-                    push.call(fake.exceptions, this.exceptions[i]);
-                    push.call(fake.callIds, this.callIds[i]);
-                }
-            }
-            createCallProperties.call(fake);
-
-            return fake;
-        },
-
-        matches: function (args, strict) {
-            var margs = this.matchingAguments;
-
-            if (margs.length <= args.length &&
-                sinon.deepEqual(margs, args.slice(0, margs.length))) {
-                return !strict || margs.length == args.length;
-            }
-        },
-
-        printf: function (format) {
-            var spy = this;
-            var args = slice.call(arguments, 1);
-            var formatter;
-
-            return (format || "").replace(/%(.)/g, function (match, specifyer) {
-                formatter = spyApi.formatters[specifyer];
-
-                if (typeof formatter == "function") {
-                    return formatter.call(null, spy, args);
-                } else if (!isNaN(parseInt(specifyer, 10))) {
-                    return sinon.format(args[specifyer - 1]);
-                }
-
-                return "%" + specifyer;
-            });
-        }
-    };
-
-    function delegateToCalls(method, matchAny, actual, notCalled) {
-        spyApi[method] = function () {
-            if (!this.called) {
-                if (notCalled) {
-                    return notCalled.apply(this, arguments);
-                }
-                return false;
-            }
-
-            var currentCall;
-            var matches = 0;
-
-            for (var i = 0, l = this.callCount; i < l; i += 1) {
-                currentCall = this.getCall(i);
-
-                if (currentCall[actual || method].apply(currentCall, arguments)) {
-                    matches += 1;
-
-                    if (matchAny) {
-                        return true;
-                    }
-                }
-            }
-
-            return matches === this.callCount;
-        };
-    }
-
-    delegateToCalls("calledOn", true);
-    delegateToCalls("alwaysCalledOn", false, "calledOn");
-    delegateToCalls("calledWith", true);
-    delegateToCalls("calledWithMatch", true);
-    delegateToCalls("alwaysCalledWith", false, "calledWith");
-    delegateToCalls("alwaysCalledWithMatch", false, "calledWithMatch");
-    delegateToCalls("calledWithExactly", true);
-    delegateToCalls("alwaysCalledWithExactly", false, "calledWithExactly");
-    delegateToCalls("neverCalledWith", false, "notCalledWith",
-        function () { return true; });
-    delegateToCalls("neverCalledWithMatch", false, "notCalledWithMatch",
-        function () { return true; });
-    delegateToCalls("threw", true);
-    delegateToCalls("alwaysThrew", false, "threw");
-    delegateToCalls("returned", true);
-    delegateToCalls("alwaysReturned", false, "returned");
-    delegateToCalls("calledWithNew", true);
-    delegateToCalls("alwaysCalledWithNew", false, "calledWithNew");
-    delegateToCalls("callArg", false, "callArgWith", function () {
-        throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
-    });
-    spyApi.callArgWith = spyApi.callArg;
-    delegateToCalls("callArgOn", false, "callArgOnWith", function () {
-        throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
-    });
-    spyApi.callArgOnWith = spyApi.callArgOn;
-    delegateToCalls("yield", false, "yield", function () {
-        throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
-    });
-    // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode.
-    spyApi.invokeCallback = spyApi.yield;
-    delegateToCalls("yieldOn", false, "yieldOn", function () {
-        throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
-    });
-    delegateToCalls("yieldTo", false, "yieldTo", function (property) {
-        throw new Error(this.toString() + " cannot yield to '" + property +
-            "' since it was not yet invoked.");
-    });
-    delegateToCalls("yieldToOn", false, "yieldToOn", function (property) {
-        throw new Error(this.toString() + " cannot yield to '" + property +
-            "' since it was not yet invoked.");
-    });
-
-    spyApi.formatters = {
-        "c": function (spy) {
-            return sinon.timesInWords(spy.callCount);
-        },
-
-        "n": function (spy) {
-            return spy.toString();
-        },
-
-        "C": function (spy) {
-            var calls = [];
-
-            for (var i = 0, l = spy.callCount; i < l; ++i) {
-                var stringifiedCall = "    " + spy.getCall(i).toString();
-                if (/\n/.test(calls[i - 1])) {
-                    stringifiedCall = "\n" + stringifiedCall;
-                }
-                push.call(calls, stringifiedCall);
-            }
-
-            return calls.length > 0 ? "\n" + calls.join("\n") : "";
-        },
-
-        "t": function (spy) {
-            var objects = [];
-
-            for (var i = 0, l = spy.callCount; i < l; ++i) {
-                push.call(objects, sinon.format(spy.thisValues[i]));
-            }
-
-            return objects.join(", ");
-        },
-
-        "*": function (spy, args) {
-            var formatted = [];
-
-            for (var i = 0, l = args.length; i < l; ++i) {
-                push.call(formatted, sinon.format(args[i]));
-            }
-
-            return formatted.join(", ");
-        }
-    };
-
-    sinon.extend(spy, spyApi);
-
-    spy.spyCall = sinon.spyCall;
-
-    if (commonJSModule) {
-        module.exports = spy;
-    } else {
-        sinon.spy = spy;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-/**
- * @depend ../sinon.js
- */
-/*jslint eqeqeq: false, onevar: false*/
-/*global module, require, sinon, process, setImmediate, setTimeout*/
-/**
- * Stub behavior
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @author Tim Fischbach (mail@timfischbach.de)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    var slice = Array.prototype.slice;
-    var join = Array.prototype.join;
-    var proto;
-
-    var nextTick = (function () {
-        if (typeof process === "object" && typeof process.nextTick === "function") {
-            return process.nextTick;
-        } else if (typeof setImmediate === "function") {
-            return setImmediate;
-        } else {
-            return function (callback) {
-                setTimeout(callback, 0);
-            };
-        }
-    })();
-
-    function throwsException(error, message) {
-        if (typeof error == "string") {
-            this.exception = new Error(message || "");
-            this.exception.name = error;
-        } else if (!error) {
-            this.exception = new Error("Error");
-        } else {
-            this.exception = error;
-        }
-
-        return this;
-    }
-
-    function getCallback(behavior, args) {
-        var callArgAt = behavior.callArgAt;
-
-        if (callArgAt < 0) {
-            var callArgProp = behavior.callArgProp;
-
-            for (var i = 0, l = args.length; i < l; ++i) {
-                if (!callArgProp && typeof args[i] == "function") {
-                    return args[i];
-                }
-
-                if (callArgProp && args[i] &&
-                    typeof args[i][callArgProp] == "function") {
-                    return args[i][callArgProp];
-                }
-            }
-
-            return null;
-        }
-
-        return args[callArgAt];
-    }
-
-    function getCallbackError(behavior, func, args) {
-        if (behavior.callArgAt < 0) {
-            var msg;
-
-            if (behavior.callArgProp) {
-                msg = sinon.functionName(behavior.stub) +
-                    " expected to yield to '" + behavior.callArgProp +
-                    "', but no object with such a property was passed.";
-            } else {
-                msg = sinon.functionName(behavior.stub) +
-                    " expected to yield, but no callback was passed.";
-            }
-
-            if (args.length > 0) {
-                msg += " Received [" + join.call(args, ", ") + "]";
-            }
-
-            return msg;
-        }
-
-        return "argument at index " + behavior.callArgAt + " is not a function: " + func;
-    }
-
-    function callCallback(behavior, args) {
-        if (typeof behavior.callArgAt == "number") {
-            var func = getCallback(behavior, args);
-
-            if (typeof func != "function") {
-                throw new TypeError(getCallbackError(behavior, func, args));
-            }
-
-            if (behavior.callbackAsync) {
-                nextTick(function() {
-                    func.apply(behavior.callbackContext, behavior.callbackArguments);
-                });
-            } else {
-                func.apply(behavior.callbackContext, behavior.callbackArguments);
-            }
-        }
-    }
-
-    proto = {
-        create: function(stub) {
-            var behavior = sinon.extend({}, sinon.behavior);
-            delete behavior.create;
-            behavior.stub = stub;
-
-            return behavior;
-        },
-
-        isPresent: function() {
-            return (typeof this.callArgAt == 'number' ||
-                    this.exception ||
-                    typeof this.returnArgAt == 'number' ||
-                    this.returnThis ||
-                    this.returnValueDefined);
-        },
-
-        invoke: function(context, args) {
-            callCallback(this, args);
-
-            if (this.exception) {
-                throw this.exception;
-            } else if (typeof this.returnArgAt == 'number') {
-                return args[this.returnArgAt];
-            } else if (this.returnThis) {
-                return context;
-            }
-
-            return this.returnValue;
-        },
-
-        onCall: function(index) {
-            return this.stub.onCall(index);
-        },
-
-        onFirstCall: function() {
-            return this.stub.onFirstCall();
-        },
-
-        onSecondCall: function() {
-            return this.stub.onSecondCall();
-        },
-
-        onThirdCall: function() {
-            return this.stub.onThirdCall();
-        },
-
-        withArgs: function(/* arguments */) {
-            throw new Error('Defining a stub by invoking "stub.onCall(...).withArgs(...)" is not supported. ' +
-                            'Use "stub.withArgs(...).onCall(...)" to define sequential behavior for calls with certain arguments.');
-        },
-
-        callsArg: function callsArg(pos) {
-            if (typeof pos != "number") {
-                throw new TypeError("argument index is not number");
-            }
-
-            this.callArgAt = pos;
-            this.callbackArguments = [];
-            this.callbackContext = undefined;
-            this.callArgProp = undefined;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-        callsArgOn: function callsArgOn(pos, context) {
-            if (typeof pos != "number") {
-                throw new TypeError("argument index is not number");
-            }
-            if (typeof context != "object") {
-                throw new TypeError("argument context is not an object");
-            }
-
-            this.callArgAt = pos;
-            this.callbackArguments = [];
-            this.callbackContext = context;
-            this.callArgProp = undefined;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-        callsArgWith: function callsArgWith(pos) {
-            if (typeof pos != "number") {
-                throw new TypeError("argument index is not number");
-            }
-
-            this.callArgAt = pos;
-            this.callbackArguments = slice.call(arguments, 1);
-            this.callbackContext = undefined;
-            this.callArgProp = undefined;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-        callsArgOnWith: function callsArgWith(pos, context) {
-            if (typeof pos != "number") {
-                throw new TypeError("argument index is not number");
-            }
-            if (typeof context != "object") {
-                throw new TypeError("argument context is not an object");
-            }
-
-            this.callArgAt = pos;
-            this.callbackArguments = slice.call(arguments, 2);
-            this.callbackContext = context;
-            this.callArgProp = undefined;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-        yields: function () {
-            this.callArgAt = -1;
-            this.callbackArguments = slice.call(arguments, 0);
-            this.callbackContext = undefined;
-            this.callArgProp = undefined;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-        yieldsOn: function (context) {
-            if (typeof context != "object") {
-                throw new TypeError("argument context is not an object");
-            }
-
-            this.callArgAt = -1;
-            this.callbackArguments = slice.call(arguments, 1);
-            this.callbackContext = context;
-            this.callArgProp = undefined;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-        yieldsTo: function (prop) {
-            this.callArgAt = -1;
-            this.callbackArguments = slice.call(arguments, 1);
-            this.callbackContext = undefined;
-            this.callArgProp = prop;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-        yieldsToOn: function (prop, context) {
-            if (typeof context != "object") {
-                throw new TypeError("argument context is not an object");
-            }
-
-            this.callArgAt = -1;
-            this.callbackArguments = slice.call(arguments, 2);
-            this.callbackContext = context;
-            this.callArgProp = prop;
-            this.callbackAsync = false;
-
-            return this;
-        },
-
-
-        "throws": throwsException,
-        throwsException: throwsException,
-
-        returns: function returns(value) {
-            this.returnValue = value;
-            this.returnValueDefined = true;
-
-            return this;
-        },
-
-        returnsArg: function returnsArg(pos) {
-            if (typeof pos != "number") {
-                throw new TypeError("argument index is not number");
-            }
-
-            this.returnArgAt = pos;
-
-            return this;
-        },
-
-        returnsThis: function returnsThis() {
-            this.returnThis = true;
-
-            return this;
-        }
-    };
-
-    // create asynchronous versions of callsArg* and yields* methods
-    for (var method in proto) {
-        // need to avoid creating anotherasync versions of the newly added async methods
-        if (proto.hasOwnProperty(method) &&
-            method.match(/^(callsArg|yields)/) &&
-            !method.match(/Async/)) {
-            proto[method + 'Async'] = (function (syncFnName) {
-                return function () {
-                    var result = this[syncFnName].apply(this, arguments);
-                    this.callbackAsync = true;
-                    return result;
-                };
-            })(method);
-        }
-    }
-
-    if (commonJSModule) {
-        module.exports = proto;
-    } else {
-        sinon.behavior = proto;
-    }
-}(typeof sinon == "object" && sinon || null));
-/**
- * @depend ../sinon.js
- * @depend spy.js
- * @depend behavior.js
- */
-/*jslint eqeqeq: false, onevar: false*/
-/*global module, require, sinon*/
-/**
- * Stub functions
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    function stub(object, property, func) {
-        if (!!func && typeof func != "function") {
-            throw new TypeError("Custom stub should be function");
-        }
-
-        var wrapper;
-
-        if (func) {
-            wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func;
-        } else {
-            wrapper = stub.create();
-        }
-
-        if (!object && typeof property === "undefined") {
-            return sinon.stub.create();
-        }
-
-        if (typeof property === "undefined" && typeof object == "object") {
-            for (var prop in object) {
-                if (typeof object[prop] === "function") {
-                    stub(object, prop);
-                }
-            }
-
-            return object;
-        }
-
-        return sinon.wrapMethod(object, property, wrapper);
-    }
-
-    function getDefaultBehavior(stub) {
-        return stub.defaultBehavior || getParentBehaviour(stub) || sinon.behavior.create(stub);
-    }
-
-    function getParentBehaviour(stub) {
-        return (stub.parent && getCurrentBehavior(stub.parent));
-    }
-
-    function getCurrentBehavior(stub) {
-        var behavior = stub.behaviors[stub.callCount - 1];
-        return behavior && behavior.isPresent() ? behavior : getDefaultBehavior(stub);
-    }
-
-    var uuid = 0;
-
-    sinon.extend(stub, (function () {
-        var proto = {
-            create: function create() {
-                var functionStub = function () {
-                    return getCurrentBehavior(functionStub).invoke(this, arguments);
-                };
-
-                functionStub.id = "stub#" + uuid++;
-                var orig = functionStub;
-                functionStub = sinon.spy.create(functionStub);
-                functionStub.func = orig;
-
-                sinon.extend(functionStub, stub);
-                functionStub._create = sinon.stub.create;
-                functionStub.displayName = "stub";
-                functionStub.toString = sinon.functionToString;
-
-                functionStub.defaultBehavior = null;
-                functionStub.behaviors = [];
-
-                return functionStub;
-            },
-
-            resetBehavior: function () {
-                var i;
-
-                this.defaultBehavior = null;
-                this.behaviors = [];
-
-                delete this.returnValue;
-                delete this.returnArgAt;
-                this.returnThis = false;
-
-                if (this.fakes) {
-                    for (i = 0; i < this.fakes.length; i++) {
-                        this.fakes[i].resetBehavior();
-                    }
-                }
-            },
-
-            onCall: function(index) {
-                if (!this.behaviors[index]) {
-                    this.behaviors[index] = sinon.behavior.create(this);
-                }
-
-                return this.behaviors[index];
-            },
-
-            onFirstCall: function() {
-                return this.onCall(0);
-            },
-
-            onSecondCall: function() {
-                return this.onCall(1);
-            },
-
-            onThirdCall: function() {
-                return this.onCall(2);
-            }
-        };
-
-        for (var method in sinon.behavior) {
-            if (sinon.behavior.hasOwnProperty(method) &&
-                !proto.hasOwnProperty(method) &&
-                method != 'create' &&
-                method != 'withArgs' &&
-                method != 'invoke') {
-                proto[method] = (function(behaviorMethod) {
-                    return function() {
-                        this.defaultBehavior = this.defaultBehavior || sinon.behavior.create(this);
-                        this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments);
-                        return this;
-                    };
-                }(method));
-            }
-        }
-
-        return proto;
-    }()));
-
-    if (commonJSModule) {
-        module.exports = stub;
-    } else {
-        sinon.stub = stub;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-/**
- * @depend ../sinon.js
- * @depend stub.js
- */
-/*jslint eqeqeq: false, onevar: false, nomen: false*/
-/*global module, require, sinon*/
-/**
- * Mock functions.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-    var push = [].push;
-    var match;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    match = sinon.match;
-
-    if (!match && commonJSModule) {
-        match = require("./match");
-    }
-
-    function mock(object) {
-        if (!object) {
-            return sinon.expectation.create("Anonymous mock");
-        }
-
-        return mock.create(object);
-    }
-
-    sinon.mock = mock;
-
-    sinon.extend(mock, (function () {
-        function each(collection, callback) {
-            if (!collection) {
-                return;
-            }
-
-            for (var i = 0, l = collection.length; i < l; i += 1) {
-                callback(collection[i]);
-            }
-        }
-
-        return {
-            create: function create(object) {
-                if (!object) {
-                    throw new TypeError("object is null");
-                }
-
-                var mockObject = sinon.extend({}, mock);
-                mockObject.object = object;
-                delete mockObject.create;
-
-                return mockObject;
-            },
-
-            expects: function expects(method) {
-                if (!method) {
-                    throw new TypeError("method is falsy");
-                }
-
-                if (!this.expectations) {
-                    this.expectations = {};
-                    this.proxies = [];
-                }
-
-                if (!this.expectations[method]) {
-                    this.expectations[method] = [];
-                    var mockObject = this;
-
-                    sinon.wrapMethod(this.object, method, function () {
-                        return mockObject.invokeMethod(method, this, arguments);
-                    });
-
-                    push.call(this.proxies, method);
-                }
-
-                var expectation = sinon.expectation.create(method);
-                push.call(this.expectations[method], expectation);
-
-                return expectation;
-            },
-
-            restore: function restore() {
-                var object = this.object;
-
-                each(this.proxies, function (proxy) {
-                    if (typeof object[proxy].restore == "function") {
-                        object[proxy].restore();
-                    }
-                });
-            },
-
-            verify: function verify() {
-                var expectations = this.expectations || {};
-                var messages = [], met = [];
-
-                each(this.proxies, function (proxy) {
-                    each(expectations[proxy], function (expectation) {
-                        if (!expectation.met()) {
-                            push.call(messages, expectation.toString());
-                        } else {
-                            push.call(met, expectation.toString());
-                        }
-                    });
-                });
-
-                this.restore();
-
-                if (messages.length > 0) {
-                    sinon.expectation.fail(messages.concat(met).join("\n"));
-                } else {
-                    sinon.expectation.pass(messages.concat(met).join("\n"));
-                }
-
-                return true;
-            },
-
-            invokeMethod: function invokeMethod(method, thisValue, args) {
-                var expectations = this.expectations && this.expectations[method];
-                var length = expectations && expectations.length || 0, i;
-
-                for (i = 0; i < length; i += 1) {
-                    if (!expectations[i].met() &&
-                        expectations[i].allowsCall(thisValue, args)) {
-                        return expectations[i].apply(thisValue, args);
-                    }
-                }
-
-                var messages = [], available, exhausted = 0;
-
-                for (i = 0; i < length; i += 1) {
-                    if (expectations[i].allowsCall(thisValue, args)) {
-                        available = available || expectations[i];
-                    } else {
-                        exhausted += 1;
-                    }
-                    push.call(messages, "    " + expectations[i].toString());
-                }
-
-                if (exhausted === 0) {
-                    return available.apply(thisValue, args);
-                }
-
-                messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({
-                    proxy: method,
-                    args: args
-                }));
-
-                sinon.expectation.fail(messages.join("\n"));
-            }
-        };
-    }()));
-
-    var times = sinon.timesInWords;
-
-    sinon.expectation = (function () {
-        var slice = Array.prototype.slice;
-        var _invoke = sinon.spy.invoke;
-
-        function callCountInWords(callCount) {
-            if (callCount == 0) {
-                return "never called";
-            } else {
-                return "called " + times(callCount);
-            }
-        }
-
-        function expectedCallCountInWords(expectation) {
-            var min = expectation.minCalls;
-            var max = expectation.maxCalls;
-
-            if (typeof min == "number" && typeof max == "number") {
-                var str = times(min);
-
-                if (min != max) {
-                    str = "at least " + str + " and at most " + times(max);
-                }
-
-                return str;
-            }
-
-            if (typeof min == "number") {
-                return "at least " + times(min);
-            }
-
-            return "at most " + times(max);
-        }
-
-        function receivedMinCalls(expectation) {
-            var hasMinLimit = typeof expectation.minCalls == "number";
-            return !hasMinLimit || expectation.callCount >= expectation.minCalls;
-        }
-
-        function receivedMaxCalls(expectation) {
-            if (typeof expectation.maxCalls != "number") {
-                return false;
-            }
-
-            return expectation.callCount == expectation.maxCalls;
-        }
-
-        function verifyMatcher(possibleMatcher, arg){
-            if (match && match.isMatcher(possibleMatcher)) {
-                return possibleMatcher.test(arg);
-            } else {
-                return true;
-            }
-        }
-
-        return {
-            minCalls: 1,
-            maxCalls: 1,
-
-            create: function create(methodName) {
-                var expectation = sinon.extend(sinon.stub.create(), sinon.expectation);
-                delete expectation.create;
-                expectation.method = methodName;
-
-                return expectation;
-            },
-
-            invoke: function invoke(func, thisValue, args) {
-                this.verifyCallAllowed(thisValue, args);
-
-                return _invoke.apply(this, arguments);
-            },
-
-            atLeast: function atLeast(num) {
-                if (typeof num != "number") {
-                    throw new TypeError("'" + num + "' is not number");
-                }
-
-                if (!this.limitsSet) {
-                    this.maxCalls = null;
-                    this.limitsSet = true;
-                }
-
-                this.minCalls = num;
-
-                return this;
-            },
-
-            atMost: function atMost(num) {
-                if (typeof num != "number") {
-                    throw new TypeError("'" + num + "' is not number");
-                }
-
-                if (!this.limitsSet) {
-                    this.minCalls = null;
-                    this.limitsSet = true;
-                }
-
-                this.maxCalls = num;
-
-                return this;
-            },
-
-            never: function never() {
-                return this.exactly(0);
-            },
-
-            once: function once() {
-                return this.exactly(1);
-            },
-
-            twice: function twice() {
-                return this.exactly(2);
-            },
-
-            thrice: function thrice() {
-                return this.exactly(3);
-            },
-
-            exactly: function exactly(num) {
-                if (typeof num != "number") {
-                    throw new TypeError("'" + num + "' is not a number");
-                }
-
-                this.atLeast(num);
-                return this.atMost(num);
-            },
-
-            met: function met() {
-                return !this.failed && receivedMinCalls(this);
-            },
-
-            verifyCallAllowed: function verifyCallAllowed(thisValue, args) {
-                if (receivedMaxCalls(this)) {
-                    this.failed = true;
-                    sinon.expectation.fail(this.method + " already called " + times(this.maxCalls));
-                }
-
-                if ("expectedThis" in this && this.expectedThis !== thisValue) {
-                    sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " +
-                        this.expectedThis);
-                }
-
-                if (!("expectedArguments" in this)) {
-                    return;
-                }
-
-                if (!args) {
-                    sinon.expectation.fail(this.method + " received no arguments, expected " +
-                        sinon.format(this.expectedArguments));
-                }
-
-                if (args.length < this.expectedArguments.length) {
-                    sinon.expectation.fail(this.method + " received too few arguments (" + sinon.format(args) +
-                        "), expected " + sinon.format(this.expectedArguments));
-                }
-
-                if (this.expectsExactArgCount &&
-                    args.length != this.expectedArguments.length) {
-                    sinon.expectation.fail(this.method + " received too many arguments (" + sinon.format(args) +
-                        "), expected " + sinon.format(this.expectedArguments));
-                }
-
-                for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) {
-
-                    if (!verifyMatcher(this.expectedArguments[i],args[i])) {
-                        sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) +
-                            ", didn't match " + this.expectedArguments.toString());
-                    }
-
-                    if (!sinon.deepEqual(this.expectedArguments[i], args[i])) {
-                        sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) +
-                            ", expected " + sinon.format(this.expectedArguments));
-                    }
-                }
-            },
-
-            allowsCall: function allowsCall(thisValue, args) {
-                if (this.met() && receivedMaxCalls(this)) {
-                    return false;
-                }
-
-                if ("expectedThis" in this && this.expectedThis !== thisValue) {
-                    return false;
-                }
-
-                if (!("expectedArguments" in this)) {
-                    return true;
-                }
-
-                args = args || [];
-
-                if (args.length < this.expectedArguments.length) {
-                    return false;
-                }
-
-                if (this.expectsExactArgCount &&
-                    args.length != this.expectedArguments.length) {
-                    return false;
-                }
-
-                for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) {
-                    if (!verifyMatcher(this.expectedArguments[i],args[i])) {
-                        return false;
-                    }
-
-                    if (!sinon.deepEqual(this.expectedArguments[i], args[i])) {
-                        return false;
-                    }
-                }
-
-                return true;
-            },
-
-            withArgs: function withArgs() {
-                this.expectedArguments = slice.call(arguments);
-                return this;
-            },
-
-            withExactArgs: function withExactArgs() {
-                this.withArgs.apply(this, arguments);
-                this.expectsExactArgCount = true;
-                return this;
-            },
-
-            on: function on(thisValue) {
-                this.expectedThis = thisValue;
-                return this;
-            },
-
-            toString: function () {
-                var args = (this.expectedArguments || []).slice();
-
-                if (!this.expectsExactArgCount) {
-                    push.call(args, "[...]");
-                }
-
-                var callStr = sinon.spyCall.toString.call({
-                    proxy: this.method || "anonymous mock expectation",
-                    args: args
-                });
-
-                var message = callStr.replace(", [...", "[, ...") + " " +
-                    expectedCallCountInWords(this);
-
-                if (this.met()) {
-                    return "Expectation met: " + message;
-                }
-
-                return "Expected " + message + " (" +
-                    callCountInWords(this.callCount) + ")";
-            },
-
-            verify: function verify() {
-                if (!this.met()) {
-                    sinon.expectation.fail(this.toString());
-                } else {
-                    sinon.expectation.pass(this.toString());
-                }
-
-                return true;
-            },
-
-            pass: function(message) {
-              sinon.assert.pass(message);
-            },
-            fail: function (message) {
-                var exception = new Error(message);
-                exception.name = "ExpectationError";
-
-                throw exception;
-            }
-        };
-    }());
-
-    if (commonJSModule) {
-        module.exports = mock;
-    } else {
-        sinon.mock = mock;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-/**
- * @depend ../sinon.js
- * @depend stub.js
- * @depend mock.js
- */
-/*jslint eqeqeq: false, onevar: false, forin: true*/
-/*global module, require, sinon*/
-/**
- * Collections of stubs, spies and mocks.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-    var push = [].push;
-    var hasOwnProperty = Object.prototype.hasOwnProperty;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    function getFakes(fakeCollection) {
-        if (!fakeCollection.fakes) {
-            fakeCollection.fakes = [];
-        }
-
-        return fakeCollection.fakes;
-    }
-
-    function each(fakeCollection, method) {
-        var fakes = getFakes(fakeCollection);
-
-        for (var i = 0, l = fakes.length; i < l; i += 1) {
-            if (typeof fakes[i][method] == "function") {
-                fakes[i][method]();
-            }
-        }
-    }
-
-    function compact(fakeCollection) {
-        var fakes = getFakes(fakeCollection);
-        var i = 0;
-        while (i < fakes.length) {
-          fakes.splice(i, 1);
-        }
-    }
-
-    var collection = {
-        verify: function resolve() {
-            each(this, "verify");
-        },
-
-        restore: function restore() {
-            each(this, "restore");
-            compact(this);
-        },
-
-        verifyAndRestore: function verifyAndRestore() {
-            var exception;
-
-            try {
-                this.verify();
-            } catch (e) {
-                exception = e;
-            }
-
-            this.restore();
-
-            if (exception) {
-                throw exception;
-            }
-        },
-
-        add: function add(fake) {
-            push.call(getFakes(this), fake);
-            return fake;
-        },
-
-        spy: function spy() {
-            return this.add(sinon.spy.apply(sinon, arguments));
-        },
-
-        stub: function stub(object, property, value) {
-            if (property) {
-                var original = object[property];
-
-                if (typeof original != "function") {
-                    if (!hasOwnProperty.call(object, property)) {
-                        throw new TypeError("Cannot stub non-existent own property " + property);
-                    }
-
-                    object[property] = value;
-
-                    return this.add({
-                        restore: function () {
-                            object[property] = original;
-                        }
-                    });
-                }
-            }
-            if (!property && !!object && typeof object == "object") {
-                var stubbedObj = sinon.stub.apply(sinon, arguments);
-
-                for (var prop in stubbedObj) {
-                    if (typeof stubbedObj[prop] === "function") {
-                        this.add(stubbedObj[prop]);
-                    }
-                }
-
-                return stubbedObj;
-            }
-
-            return this.add(sinon.stub.apply(sinon, arguments));
-        },
-
-        mock: function mock() {
-            return this.add(sinon.mock.apply(sinon, arguments));
-        },
-
-        inject: function inject(obj) {
-            var col = this;
-
-            obj.spy = function () {
-                return col.spy.apply(col, arguments);
-            };
-
-            obj.stub = function () {
-                return col.stub.apply(col, arguments);
-            };
-
-            obj.mock = function () {
-                return col.mock.apply(col, arguments);
-            };
-
-            return obj;
-        }
-    };
-
-    if (commonJSModule) {
-        module.exports = collection;
-    } else {
-        sinon.collection = collection;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/
-/*global module, require, window*/
-/**
- * Fake timer API
- * setTimeout
- * setInterval
- * clearTimeout
- * clearInterval
- * tick
- * reset
- * Date
- *
- * Inspired by jsUnitMockTimeOut from JsUnit
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-if (typeof sinon == "undefined") {
-    var sinon = {};
-}
-
-(function (global) {
-    var id = 1;
-
-    function addTimer(args, recurring) {
-        if (args.length === 0) {
-            throw new Error("Function requires at least 1 parameter");
-        }
-
-        if (typeof args[0] === "undefined") {
-            throw new Error("Callback must be provided to timer calls");
-        }
-
-        var toId = id++;
-        var delay = args[1] || 0;
-
-        if (!this.timeouts) {
-            this.timeouts = {};
-        }
-
-        this.timeouts[toId] = {
-            id: toId,
-            func: args[0],
-            callAt: this.now + delay,
-            invokeArgs: Array.prototype.slice.call(args, 2)
-        };
-
-        if (recurring === true) {
-            this.timeouts[toId].interval = delay;
-        }
-
-        return toId;
-    }
-
-    function parseTime(str) {
-        if (!str) {
-            return 0;
-        }
-
-        var strings = str.split(":");
-        var l = strings.length, i = l;
-        var ms = 0, parsed;
-
-        if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
-            throw new Error("tick only understands numbers and 'h:m:s'");
-        }
-
-        while (i--) {
-            parsed = parseInt(strings[i], 10);
-
-            if (parsed >= 60) {
-                throw new Error("Invalid time " + str);
-            }
-
-            ms += parsed * Math.pow(60, (l - i - 1));
-        }
-
-        return ms * 1000;
-    }
-
-    function createObject(object) {
-        var newObject;
-
-        if (Object.create) {
-            newObject = Object.create(object);
-        } else {
-            var F = function () {};
-            F.prototype = object;
-            newObject = new F();
-        }
-
-        newObject.Date.clock = newObject;
-        return newObject;
-    }
-
-    sinon.clock = {
-        now: 0,
-
-        create: function create(now) {
-            var clock = createObject(this);
-
-            if (typeof now == "number") {
-                clock.now = now;
-            }
-
-            if (!!now && typeof now == "object") {
-                throw new TypeError("now should be milliseconds since UNIX epoch");
-            }
-
-            return clock;
-        },
-
-        setTimeout: function setTimeout(callback, timeout) {
-            return addTimer.call(this, arguments, false);
-        },
-
-        clearTimeout: function clearTimeout(timerId) {
-            if (!this.timeouts) {
-                this.timeouts = [];
-            }
-
-            if (timerId in this.timeouts) {
-                delete this.timeouts[timerId];
-            }
-        },
-
-        setInterval: function setInterval(callback, timeout) {
-            return addTimer.call(this, arguments, true);
-        },
-
-        clearInterval: function clearInterval(timerId) {
-            this.clearTimeout(timerId);
-        },
-
-        setImmediate: function setImmediate(callback) {
-            var passThruArgs = Array.prototype.slice.call(arguments, 1);
-
-            return addTimer.call(this, [callback, 0].concat(passThruArgs), false);
-        },
-
-        clearImmediate: function clearImmediate(timerId) {
-            this.clearTimeout(timerId);
-        },
-
-        tick: function tick(ms) {
-            ms = typeof ms == "number" ? ms : parseTime(ms);
-            var tickFrom = this.now, tickTo = this.now + ms, previous = this.now;
-            var timer = this.firstTimerInRange(tickFrom, tickTo);
-
-            var firstException;
-            while (timer && tickFrom <= tickTo) {
-                if (this.timeouts[timer.id]) {
-                    tickFrom = this.now = timer.callAt;
-                    try {
-                      this.callTimer(timer);
-                    } catch (e) {
-                      firstException = firstException || e;
-                    }
-                }
-
-                timer = this.firstTimerInRange(previous, tickTo);
-                previous = tickFrom;
-            }
-
-            this.now = tickTo;
-
-            if (firstException) {
-              throw firstException;
-            }
-
-            return this.now;
-        },
-
-        firstTimerInRange: function (from, to) {
-            var timer, smallest = null, originalTimer;
-
-            for (var id in this.timeouts) {
-                if (this.timeouts.hasOwnProperty(id)) {
-                    if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) {
-                        continue;
-                    }
-
-                    if (smallest === null || this.timeouts[id].callAt < smallest) {
-                        originalTimer = this.timeouts[id];
-                        smallest = this.timeouts[id].callAt;
-
-                        timer = {
-                            func: this.timeouts[id].func,
-                            callAt: this.timeouts[id].callAt,
-                            interval: this.timeouts[id].interval,
-                            id: this.timeouts[id].id,
-                            invokeArgs: this.timeouts[id].invokeArgs
-                        };
-                    }
-                }
-            }
-
-            return timer || null;
-        },
-
-        callTimer: function (timer) {
-            if (typeof timer.interval == "number") {
-                this.timeouts[timer.id].callAt += timer.interval;
-            } else {
-                delete this.timeouts[timer.id];
-            }
-
-            try {
-                if (typeof timer.func == "function") {
-                    timer.func.apply(null, timer.invokeArgs);
-                } else {
-                    eval(timer.func);
-                }
-            } catch (e) {
-              var exception = e;
-            }
-
-            if (!this.timeouts[timer.id]) {
-                if (exception) {
-                  throw exception;
-                }
-                return;
-            }
-
-            if (exception) {
-              throw exception;
-            }
-        },
-
-        reset: function reset() {
-            this.timeouts = {};
-        },
-
-        Date: (function () {
-            var NativeDate = Date;
-
-            function ClockDate(year, month, date, hour, minute, second, ms) {
-                // Defensive and verbose to avoid potential harm in passing
-                // explicit undefined when user does not pass argument
-                switch (arguments.length) {
-                case 0:
-                    return new NativeDate(ClockDate.clock.now);
-                case 1:
-                    return new NativeDate(year);
-                case 2:
-                    return new NativeDate(year, month);
-                case 3:
-                    return new NativeDate(year, month, date);
-                case 4:
-                    return new NativeDate(year, month, date, hour);
-                case 5:
-                    return new NativeDate(year, month, date, hour, minute);
-                case 6:
-                    return new NativeDate(year, month, date, hour, minute, second);
-                default:
-                    return new NativeDate(year, month, date, hour, minute, second, ms);
-                }
-            }
-
-            return mirrorDateProperties(ClockDate, NativeDate);
-        }())
-    };
-
-    function mirrorDateProperties(target, source) {
-        if (source.now) {
-            target.now = function now() {
-                return target.clock.now;
-            };
-        } else {
-            delete target.now;
-        }
-
-        if (source.toSource) {
-            target.toSource = function toSource() {
-                return source.toSource();
-            };
-        } else {
-            delete target.toSource;
-        }
-
-        target.toString = function toString() {
-            return source.toString();
-        };
-
-        target.prototype = source.prototype;
-        target.parse = source.parse;
-        target.UTC = source.UTC;
-        target.prototype.toUTCString = source.prototype.toUTCString;
-
-        for (var prop in source) {
-            if (source.hasOwnProperty(prop)) {
-                target[prop] = source[prop];
-            }
-        }
-
-        return target;
-    }
-
-    var methods = ["Date", "setTimeout", "setInterval",
-                   "clearTimeout", "clearInterval"];
-
-    if (typeof global.setImmediate !== "undefined") {
-        methods.push("setImmediate");
-    }
-
-    if (typeof global.clearImmediate !== "undefined") {
-        methods.push("clearImmediate");
-    }
-
-    function restore() {
-        var method;
-
-        for (var i = 0, l = this.methods.length; i < l; i++) {
-            method = this.methods[i];
-
-            if (global[method].hadOwnProperty) {
-                global[method] = this["_" + method];
-            } else {
-                try {
-                    delete global[method];
-                } catch (e) {}
-            }
-        }
-
-        // Prevent multiple executions which will completely remove these props
-        this.methods = [];
-    }
-
-    function stubGlobal(method, clock) {
-        clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method);
-        clock["_" + method] = global[method];
-
-        if (method == "Date") {
-            var date = mirrorDateProperties(clock[method], global[method]);
-            global[method] = date;
-        } else {
-            global[method] = function () {
-                return clock[method].apply(clock, arguments);
-            };
-
-            for (var prop in clock[method]) {
-                if (clock[method].hasOwnProperty(prop)) {
-                    global[method][prop] = clock[method][prop];
-                }
-            }
-        }
-
-        global[method].clock = clock;
-    }
-
-    sinon.useFakeTimers = function useFakeTimers(now) {
-        var clock = sinon.clock.create(now);
-        clock.restore = restore;
-        clock.methods = Array.prototype.slice.call(arguments,
-                                                   typeof now == "number" ? 1 : 0);
-
-        if (clock.methods.length === 0) {
-            clock.methods = methods;
-        }
-
-        for (var i = 0, l = clock.methods.length; i < l; i++) {
-            stubGlobal(clock.methods[i], clock);
-        }
-
-        return clock;
-    };
-}(typeof global != "undefined" && typeof global !== "function" ? global : this));
-
-sinon.timers = {
-    setTimeout: setTimeout,
-    clearTimeout: clearTimeout,
-    setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined),
-    clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined),
-    setInterval: setInterval,
-    clearInterval: clearInterval,
-    Date: Date
-};
-
-if (typeof module !== 'undefined' && module.exports) {
-    module.exports = sinon;
-}
-
-/*jslint eqeqeq: false, onevar: false*/
-/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
-/**
- * Minimal Event interface implementation
- *
- * Original implementation by Sven Fuchs: https://gist.github.com/995028
- * Modifications and tests by Christian Johansen.
- *
- * @author Sven Fuchs (svenfuchs@artweb-design.de)
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2011 Sven Fuchs, Christian Johansen
- */
-
-if (typeof sinon == "undefined") {
-    this.sinon = {};
-}
-
-(function () {
-    var push = [].push;
-
-    sinon.Event = function Event(type, bubbles, cancelable, target) {
-        this.initEvent(type, bubbles, cancelable, target);
-    };
-
-    sinon.Event.prototype = {
-        initEvent: function(type, bubbles, cancelable, target) {
-            this.type = type;
-            this.bubbles = bubbles;
-            this.cancelable = cancelable;
-            this.target = target;
-        },
-
-        stopPropagation: function () {},
-
-        preventDefault: function () {
-            this.defaultPrevented = true;
-        }
-    };
-
-    sinon.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) {
-        this.initEvent(type, false, false, target);
-        this.loaded = progressEventRaw.loaded || null;
-        this.total = progressEventRaw.total || null;
-    };
-
-    sinon.ProgressEvent.prototype = new sinon.Event();
-
-    sinon.ProgressEvent.prototype.constructor =  sinon.ProgressEvent;
-
-    sinon.CustomEvent = function CustomEvent(type, customData, target) {
-        this.initEvent(type, false, false, target);
-        this.detail = customData.detail || null;
-    };
-
-    sinon.CustomEvent.prototype = new sinon.Event();
-
-    sinon.CustomEvent.prototype.constructor =  sinon.CustomEvent;
-
-    sinon.EventTarget = {
-        addEventListener: function addEventListener(event, listener) {
-            this.eventListeners = this.eventListeners || {};
-            this.eventListeners[event] = this.eventListeners[event] || [];
-            push.call(this.eventListeners[event], listener);
-        },
-
-        removeEventListener: function removeEventListener(event, listener) {
-            var listeners = this.eventListeners && this.eventListeners[event] || [];
-
-            for (var i = 0, l = listeners.length; i < l; ++i) {
-                if (listeners[i] == listener) {
-                    return listeners.splice(i, 1);
-                }
-            }
-        },
-
-        dispatchEvent: function dispatchEvent(event) {
-            var type = event.type;
-            var listeners = this.eventListeners && this.eventListeners[type] || [];
-
-            for (var i = 0; i < listeners.length; i++) {
-                if (typeof listeners[i] == "function") {
-                    listeners[i].call(this, event);
-                } else {
-                    listeners[i].handleEvent(event);
-                }
-            }
-
-            return !!event.defaultPrevented;
-        }
-    };
-}());
-
-/**
- * @depend ../../sinon.js
- * @depend event.js
- */
-/*jslint eqeqeq: false, onevar: false*/
-/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
-/**
- * Fake XMLHttpRequest object
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-// wrapper for global
-(function(global) {
-    if (typeof sinon === "undefined") {
-        global.sinon = {};
-    }
-
-    var supportsProgress = typeof ProgressEvent !== "undefined";
-    var supportsCustomEvent = typeof CustomEvent !== "undefined";
-    sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest };
-    var xhr = sinon.xhr;
-    xhr.GlobalXMLHttpRequest = global.XMLHttpRequest;
-    xhr.GlobalActiveXObject = global.ActiveXObject;
-    xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined";
-    xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined";
-    xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX
-                                     ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false;
-    xhr.supportsCORS = 'withCredentials' in (new sinon.xhr.GlobalXMLHttpRequest());
-
-    /*jsl:ignore*/
-    var unsafeHeaders = {
-        "Accept-Charset": true,
-        "Accept-Encoding": true,
-        "Connection": true,
-        "Content-Length": true,
-        "Cookie": true,
-        "Cookie2": true,
-        "Content-Transfer-Encoding": true,
-        "Date": true,
-        "Expect": true,
-        "Host": true,
-        "Keep-Alive": true,
-        "Referer": true,
-        "TE": true,
-        "Trailer": true,
-        "Transfer-Encoding": true,
-        "Upgrade": true,
-        "User-Agent": true,
-        "Via": true
-    };
-    /*jsl:end*/
-
-    function FakeXMLHttpRequest() {
-        this.readyState = FakeXMLHttpRequest.UNSENT;
-        this.requestHeaders = {};
-        this.requestBody = null;
-        this.status = 0;
-        this.statusText = "";
-        this.upload = new UploadProgress();
-        if (sinon.xhr.supportsCORS) {
-            this.withCredentials = false;
-        }
-
-
-        var xhr = this;
-        var events = ["loadstart", "load", "abort", "loadend"];
-
-        function addEventListener(eventName) {
-            xhr.addEventListener(eventName, function (event) {
-                var listener = xhr["on" + eventName];
-
-                if (listener && typeof listener == "function") {
-                    listener.call(this, event);
-                }
-            });
-        }
-
-        for (var i = events.length - 1; i >= 0; i--) {
-            addEventListener(events[i]);
-        }
-
-        if (typeof FakeXMLHttpRequest.onCreate == "function") {
-            FakeXMLHttpRequest.onCreate(this);
-        }
-    }
-
-    // An upload object is created for each
-    // FakeXMLHttpRequest and allows upload
-    // events to be simulated using uploadProgress
-    // and uploadError.
-    function UploadProgress() {
-        this.eventListeners = {
-            "progress": [],
-            "load": [],
-            "abort": [],
-            "error": []
-        }
-    }
-
-    UploadProgress.prototype.addEventListener = function(event, listener) {
-        this.eventListeners[event].push(listener);
-    };
-
-    UploadProgress.prototype.removeEventListener = function(event, listener) {
-        var listeners = this.eventListeners[event] || [];
-
-        for (var i = 0, l = listeners.length; i < l; ++i) {
-            if (listeners[i] == listener) {
-                return listeners.splice(i, 1);
-            }
-        }
-    };
-
-    UploadProgress.prototype.dispatchEvent = function(event) {
-        var listeners = this.eventListeners[event.type] || [];
-
-        for (var i = 0, listener; (listener = listeners[i]) != null; i++) {
-            listener(event);
-        }
-    };
-
-    function verifyState(xhr) {
-        if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
-            throw new Error("INVALID_STATE_ERR");
-        }
-
-        if (xhr.sendFlag) {
-            throw new Error("INVALID_STATE_ERR");
-        }
-    }
-
-    // filtering to enable a white-list version of Sinon FakeXhr,
-    // where whitelisted requests are passed through to real XHR
-    function each(collection, callback) {
-        if (!collection) return;
-        for (var i = 0, l = collection.length; i < l; i += 1) {
-            callback(collection[i]);
-        }
-    }
-    function some(collection, callback) {
-        for (var index = 0; index < collection.length; index++) {
-            if(callback(collection[index]) === true) return true;
-        }
-        return false;
-    }
-    // largest arity in XHR is 5 - XHR#open
-    var apply = function(obj,method,args) {
-        switch(args.length) {
-        case 0: return obj[method]();
-        case 1: return obj[method](args[0]);
-        case 2: return obj[method](args[0],args[1]);
-        case 3: return obj[method](args[0],args[1],args[2]);
-        case 4: return obj[method](args[0],args[1],args[2],args[3]);
-        case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]);
-        }
-    };
-
-    FakeXMLHttpRequest.filters = [];
-    FakeXMLHttpRequest.addFilter = function(fn) {
-        this.filters.push(fn)
-    };
-    var IE6Re = /MSIE 6/;
-    FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) {
-        var xhr = new sinon.xhr.workingXHR();
-        each(["open","setRequestHeader","send","abort","getResponseHeader",
-              "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"],
-             function(method) {
-                 fakeXhr[method] = function() {
-                   return apply(xhr,method,arguments);
-                 };
-             });
-
-        var copyAttrs = function(args) {
-            each(args, function(attr) {
-              try {
-                fakeXhr[attr] = xhr[attr]
-              } catch(e) {
-                if(!IE6Re.test(navigator.userAgent)) throw e;
-              }
-            });
-        };
-
-        var stateChange = function() {
-            fakeXhr.readyState = xhr.readyState;
-            if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) {
-                copyAttrs(["status","statusText"]);
-            }
-            if(xhr.readyState >= FakeXMLHttpRequest.LOADING) {
-                copyAttrs(["responseText"]);
-            }
-            if(xhr.readyState === FakeXMLHttpRequest.DONE) {
-                copyAttrs(["responseXML"]);
-            }
-            if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr, { target: fakeXhr });
-        };
-        if(xhr.addEventListener) {
-          for(var event in fakeXhr.eventListeners) {
-              if(fakeXhr.eventListeners.hasOwnProperty(event)) {
-                  each(fakeXhr.eventListeners[event],function(handler) {
-                      xhr.addEventListener(event, handler);
-                  });
-              }
-          }
-          xhr.addEventListener("readystatechange",stateChange);
-        } else {
-          xhr.onreadystatechange = stateChange;
-        }
-        apply(xhr,"open",xhrArgs);
-    };
-    FakeXMLHttpRequest.useFilters = false;
-
-    function verifyRequestOpened(xhr) {
-        if (xhr.readyState != FakeXMLHttpRequest.OPENED) {
-            throw new Error("INVALID_STATE_ERR - " + xhr.readyState);
-        }
-    }
-
-    function verifyRequestSent(xhr) {
-        if (xhr.readyState == FakeXMLHttpRequest.DONE) {
-            throw new Error("Request done");
-        }
-    }
-
-    function verifyHeadersReceived(xhr) {
-        if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) {
-            throw new Error("No headers received");
-        }
-    }
-
-    function verifyResponseBodyType(body) {
-        if (typeof body != "string") {
-            var error = new Error("Attempted to respond to fake XMLHttpRequest with " +
-                                 body + ", which is not a string.");
-            error.name = "InvalidBodyException";
-            throw error;
-        }
-    }
-
-    sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, {
-        async: true,
-
-        open: function open(method, url, async, username, password) {
-            this.method = method;
-            this.url = url;
-            this.async = typeof async == "boolean" ? async : true;
-            this.username = username;
-            this.password = password;
-            this.responseText = null;
-            this.responseXML = null;
-            this.requestHeaders = {};
-            this.sendFlag = false;
-            if(sinon.FakeXMLHttpRequest.useFilters === true) {
-                var xhrArgs = arguments;
-                var defake = some(FakeXMLHttpRequest.filters,function(filter) {
-                    return filter.apply(this,xhrArgs)
-                });
-                if (defake) {
-                  return sinon.FakeXMLHttpRequest.defake(this,arguments);
-                }
-            }
-            this.readyStateChange(FakeXMLHttpRequest.OPENED);
-        },
-
-        readyStateChange: function readyStateChange(state) {
-            this.readyState = state;
-
-            if (typeof this.onreadystatechange == "function") {
-                try {
-                    this.onreadystatechange();
-                } catch (e) {
-                    sinon.logError("Fake XHR onreadystatechange handler", e);
-                }
-            }
-
-            this.dispatchEvent(new sinon.Event("readystatechange"));
-
-            switch (this.readyState) {
-                case FakeXMLHttpRequest.DONE:
-                    this.dispatchEvent(new sinon.Event("load", false, false, this));
-                    this.dispatchEvent(new sinon.Event("loadend", false, false, this));
-                    this.upload.dispatchEvent(new sinon.Event("load", false, false, this));
-                    if (supportsProgress) {
-                        this.upload.dispatchEvent(new sinon.ProgressEvent('progress', {loaded: 100, total: 100}));
-                    }
-                    break;
-            }
-        },
-
-        setRequestHeader: function setRequestHeader(header, value) {
-            verifyState(this);
-
-            if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) {
-                throw new Error("Refused to set unsafe header \"" + header + "\"");
-            }
-
-            if (this.requestHeaders[header]) {
-                this.requestHeaders[header] += "," + value;
-            } else {
-                this.requestHeaders[header] = value;
-            }
-        },
-
-        // Helps testing
-        setResponseHeaders: function setResponseHeaders(headers) {
-            verifyRequestOpened(this);
-            this.responseHeaders = {};
-
-            for (var header in headers) {
-                if (headers.hasOwnProperty(header)) {
-                    this.responseHeaders[header] = headers[header];
-                }
-            }
-
-            if (this.async) {
-                this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED);
-            } else {
-                this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED;
-            }
-        },
-
-        // Currently treats ALL data as a DOMString (i.e. no Document)
-        send: function send(data) {
-            verifyState(this);
-
-            if (!/^(get|head)$/i.test(this.method)) {
-                if (this.requestHeaders["Content-Type"]) {
-                    var value = this.requestHeaders["Content-Type"].split(";");
-                    this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8";
-                } else {
-                    this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
-                }
-
-                this.requestBody = data;
-            }
-
-            this.errorFlag = false;
-            this.sendFlag = this.async;
-            this.readyStateChange(FakeXMLHttpRequest.OPENED);
-
-            if (typeof this.onSend == "function") {
-                this.onSend(this);
-            }
-
-            this.dispatchEvent(new sinon.Event("loadstart", false, false, this));
-        },
-
-        abort: function abort() {
-            this.aborted = true;
-            this.responseText = null;
-            this.errorFlag = true;
-            this.requestHeaders = {};
-
-            if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) {
-                this.readyStateChange(sinon.FakeXMLHttpRequest.DONE);
-                this.sendFlag = false;
-            }
-
-            this.readyState = sinon.FakeXMLHttpRequest.UNSENT;
-
-            this.dispatchEvent(new sinon.Event("abort", false, false, this));
-
-            this.upload.dispatchEvent(new sinon.Event("abort", false, false, this));
-
-            if (typeof this.onerror === "function") {
-                this.onerror();
-            }
-        },
-
-        getResponseHeader: function getResponseHeader(header) {
-            if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
-                return null;
-            }
-
-            if (/^Set-Cookie2?$/i.test(header)) {
-                return null;
-            }
-
-            header = header.toLowerCase();
-
-            for (var h in this.responseHeaders) {
-                if (h.toLowerCase() == header) {
-                    return this.responseHeaders[h];
-                }
-            }
-
-            return null;
-        },
-
-        getAllResponseHeaders: function getAllResponseHeaders() {
-            if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
-                return "";
-            }
-
-            var headers = "";
-
-            for (var header in this.responseHeaders) {
-                if (this.responseHeaders.hasOwnProperty(header) &&
-                    !/^Set-Cookie2?$/i.test(header)) {
-                    headers += header + ": " + this.responseHeaders[header] + "\r\n";
-                }
-            }
-
-            return headers;
-        },
-
-        setResponseBody: function setResponseBody(body) {
-            verifyRequestSent(this);
-            verifyHeadersReceived(this);
-            verifyResponseBodyType(body);
-
-            var chunkSize = this.chunkSize || 10;
-            var index = 0;
-            this.responseText = "";
-
-            do {
-                if (this.async) {
-                    this.readyStateChange(FakeXMLHttpRequest.LOADING);
-                }
-
-                this.responseText += body.substring(index, index + chunkSize);
-                index += chunkSize;
-            } while (index < body.length);
-
-            var type = this.getResponseHeader("Content-Type");
-
-            if (this.responseText &&
-                (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) {
-                try {
-                    this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText);
-                } catch (e) {
-                    // Unable to parse XML - no biggie
-                }
-            }
-
-            if (this.async) {
-                this.readyStateChange(FakeXMLHttpRequest.DONE);
-            } else {
-                this.readyState = FakeXMLHttpRequest.DONE;
-            }
-        },
-
-        respond: function respond(status, headers, body) {
-            this.status = typeof status == "number" ? status : 200;
-            this.statusText = FakeXMLHttpRequest.statusCodes[this.status];
-            this.setResponseHeaders(headers || {});
-            this.setResponseBody(body || "");
-        },
-
-        uploadProgress: function uploadProgress(progressEventRaw) {
-            if (supportsProgress) {
-                this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw));
-            }
-        },
-
-        uploadError: function uploadError(error) {
-            if (supportsCustomEvent) {
-                this.upload.dispatchEvent(new sinon.CustomEvent("error", {"detail": error}));
-            }
-        }
-    });
-
-    sinon.extend(FakeXMLHttpRequest, {
-        UNSENT: 0,
-        OPENED: 1,
-        HEADERS_RECEIVED: 2,
-        LOADING: 3,
-        DONE: 4
-    });
-
-    // Borrowed from JSpec
-    FakeXMLHttpRequest.parseXML = function parseXML(text) {
-        var xmlDoc;
-
-        if (typeof DOMParser != "undefined") {
-            var parser = new DOMParser();
-            xmlDoc = parser.parseFromString(text, "text/xml");
-        } else {
-            xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
-            xmlDoc.async = "false";
-            xmlDoc.loadXML(text);
-        }
-
-        return xmlDoc;
-    };
-
-    FakeXMLHttpRequest.statusCodes = {
-        100: "Continue",
-        101: "Switching Protocols",
-        200: "OK",
-        201: "Created",
-        202: "Accepted",
-        203: "Non-Authoritative Information",
-        204: "No Content",
-        205: "Reset Content",
-        206: "Partial Content",
-        300: "Multiple Choice",
-        301: "Moved Permanently",
-        302: "Found",
-        303: "See Other",
-        304: "Not Modified",
-        305: "Use Proxy",
-        307: "Temporary Redirect",
-        400: "Bad Request",
-        401: "Unauthorized",
-        402: "Payment Required",
-        403: "Forbidden",
-        404: "Not Found",
-        405: "Method Not Allowed",
-        406: "Not Acceptable",
-        407: "Proxy Authentication Required",
-        408: "Request Timeout",
-        409: "Conflict",
-        410: "Gone",
-        411: "Length Required",
-        412: "Precondition Failed",
-        413: "Request Entity Too Large",
-        414: "Request-URI Too Long",
-        415: "Unsupported Media Type",
-        416: "Requested Range Not Satisfiable",
-        417: "Expectation Failed",
-        422: "Unprocessable Entity",
-        500: "Internal Server Error",
-        501: "Not Implemented",
-        502: "Bad Gateway",
-        503: "Service Unavailable",
-        504: "Gateway Timeout",
-        505: "HTTP Version Not Supported"
-    };
-
-    sinon.useFakeXMLHttpRequest = function () {
-        sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) {
-            if (xhr.supportsXHR) {
-                global.XMLHttpRequest = xhr.GlobalXMLHttpRequest;
-            }
-
-            if (xhr.supportsActiveX) {
-                global.ActiveXObject = xhr.GlobalActiveXObject;
-            }
-
-            delete sinon.FakeXMLHttpRequest.restore;
-
-            if (keepOnCreate !== true) {
-                delete sinon.FakeXMLHttpRequest.onCreate;
-            }
-        };
-        if (xhr.supportsXHR) {
-            global.XMLHttpRequest = sinon.FakeXMLHttpRequest;
-        }
-
-        if (xhr.supportsActiveX) {
-            global.ActiveXObject = function ActiveXObject(objId) {
-                if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) {
-
-                    return new sinon.FakeXMLHttpRequest();
-                }
-
-                return new xhr.GlobalActiveXObject(objId);
-            };
-        }
-
-        return sinon.FakeXMLHttpRequest;
-    };
-
-    sinon.FakeXMLHttpRequest = FakeXMLHttpRequest;
-
-})(typeof global === "object" ? global : this);
-
-if (typeof module !== 'undefined' && module.exports) {
-    module.exports = sinon;
-}
-
-/**
- * @depend fake_xml_http_request.js
- */
-/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/
-/*global module, require, window*/
-/**
- * The Sinon "server" mimics a web server that receives requests from
- * sinon.FakeXMLHttpRequest and provides an API to respond to those requests,
- * both synchronously and asynchronously. To respond synchronuously, canned
- * answers have to be provided upfront.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-if (typeof sinon == "undefined") {
-    var sinon = {};
-}
-
-sinon.fakeServer = (function () {
-    var push = [].push;
-    function F() {}
-
-    function create(proto) {
-        F.prototype = proto;
-        return new F();
-    }
-
-    function responseArray(handler) {
-        var response = handler;
-
-        if (Object.prototype.toString.call(handler) != "[object Array]") {
-            response = [200, {}, handler];
-        }
-
-        if (typeof response[2] != "string") {
-            throw new TypeError("Fake server response body should be string, but was " +
-                                typeof response[2]);
-        }
-
-        return response;
-    }
-
-    var wloc = typeof window !== "undefined" ? window.location : {};
-    var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host);
-
-    function matchOne(response, reqMethod, reqUrl) {
-        var rmeth = response.method;
-        var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase();
-        var url = response.url;
-        var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl));
-
-        return matchMethod && matchUrl;
-    }
-
-    function match(response, request) {
-        var requestUrl = request.url;
-
-        if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) {
-            requestUrl = requestUrl.replace(rCurrLoc, "");
-        }
-
-        if (matchOne(response, this.getHTTPMethod(request), requestUrl)) {
-            if (typeof response.response == "function") {
-                var ru = response.url;
-                var args = [request].concat(ru && typeof ru.exec == "function" ? ru.exec(requestUrl).slice(1) : []);
-                return response.response.apply(response, args);
-            }
-
-            return true;
-        }
-
-        return false;
-    }
-
-    function log(response, request) {
-        var str;
-
-        str =  "Request:\n"  + sinon.format(request)  + "\n\n";
-        str += "Response:\n" + sinon.format(response) + "\n\n";
-
-        sinon.log(str);
-    }
-
-    return {
-        create: function () {
-            var server = create(this);
-            this.xhr = sinon.useFakeXMLHttpRequest();
-            server.requests = [];
-
-            this.xhr.onCreate = function (xhrObj) {
-                server.addRequest(xhrObj);
-            };
-
-            return server;
-        },
-
-        addRequest: function addRequest(xhrObj) {
-            var server = this;
-            push.call(this.requests, xhrObj);
-
-            xhrObj.onSend = function () {
-                server.handleRequest(this);
-
-                if (server.autoRespond && !server.responding) {
-                    setTimeout(function () {
-                        server.responding = false;
-                        server.respond();
-                    }, server.autoRespondAfter || 10);
-
-                    server.responding = true;
-                }
-            };
-        },
-
-        getHTTPMethod: function getHTTPMethod(request) {
-            if (this.fakeHTTPMethods && /post/i.test(request.method)) {
-                var matches = (request.requestBody || "").match(/_method=([^\b;]+)/);
-                return !!matches ? matches[1] : request.method;
-            }
-
-            return request.method;
-        },
-
-        handleRequest: function handleRequest(xhr) {
-            if (xhr.async) {
-                if (!this.queue) {
-                    this.queue = [];
-                }
-
-                push.call(this.queue, xhr);
-            } else {
-                this.processRequest(xhr);
-            }
-        },
-
-        respondWith: function respondWith(method, url, body) {
-            if (arguments.length == 1 && typeof method != "function") {
-                this.response = responseArray(method);
-                return;
-            }
-
-            if (!this.responses) { this.responses = []; }
-
-            if (arguments.length == 1) {
-                body = method;
-                url = method = null;
-            }
-
-            if (arguments.length == 2) {
-                body = url;
-                url = method;
-                method = null;
-            }
-
-            push.call(this.responses, {
-                method: method,
-                url: url,
-                response: typeof body == "function" ? body : responseArray(body)
-            });
-        },
-
-        respond: function respond() {
-            if (arguments.length > 0) this.respondWith.apply(this, arguments);
-            var queue = this.queue || [];
-            var requests = queue.splice(0);
-            var request;
-
-            while(request = requests.shift()) {
-                this.processRequest(request);
-            }
-        },
-
-        processRequest: function processRequest(request) {
-            try {
-                if (request.aborted) {
-                    return;
-                }
-
-                var response = this.response || [404, {}, ""];
-
-                if (this.responses) {
-                    for (var l = this.responses.length, i = l - 1; i >= 0; i--) {
-                        if (match.call(this, this.responses[i], request)) {
-                            response = this.responses[i].response;
-                            break;
-                        }
-                    }
-                }
-
-                if (request.readyState != 4) {
-                    log(response, request);
-
-                    request.respond(response[0], response[1], response[2]);
-                }
-            } catch (e) {
-                sinon.logError("Fake server request processing", e);
-            }
-        },
-
-        restore: function restore() {
-            return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments);
-        }
-    };
-}());
-
-if (typeof module !== 'undefined' && module.exports) {
-    module.exports = sinon;
-}
-
-/**
- * @depend fake_server.js
- * @depend fake_timers.js
- */
-/*jslint browser: true, eqeqeq: false, onevar: false*/
-/*global sinon*/
-/**
- * Add-on for sinon.fakeServer that automatically handles a fake timer along with
- * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery
- * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead,
- * it polls the object for completion with setInterval. Dispite the direct
- * motivation, there is nothing jQuery-specific in this file, so it can be used
- * in any environment where the ajax implementation depends on setInterval or
- * setTimeout.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function () {
-    function Server() {}
-    Server.prototype = sinon.fakeServer;
-
-    sinon.fakeServerWithClock = new Server();
-
-    sinon.fakeServerWithClock.addRequest = function addRequest(xhr) {
-        if (xhr.async) {
-            if (typeof setTimeout.clock == "object") {
-                this.clock = setTimeout.clock;
-            } else {
-                this.clock = sinon.useFakeTimers();
-                this.resetClock = true;
-            }
-
-            if (!this.longestTimeout) {
-                var clockSetTimeout = this.clock.setTimeout;
-                var clockSetInterval = this.clock.setInterval;
-                var server = this;
-
-                this.clock.setTimeout = function (fn, timeout) {
-                    server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
-
-                    return clockSetTimeout.apply(this, arguments);
-                };
-
-                this.clock.setInterval = function (fn, timeout) {
-                    server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
-
-                    return clockSetInterval.apply(this, arguments);
-                };
-            }
-        }
-
-        return sinon.fakeServer.addRequest.call(this, xhr);
-    };
-
-    sinon.fakeServerWithClock.respond = function respond() {
-        var returnVal = sinon.fakeServer.respond.apply(this, arguments);
-
-        if (this.clock) {
-            this.clock.tick(this.longestTimeout || 0);
-            this.longestTimeout = 0;
-
-            if (this.resetClock) {
-                this.clock.restore();
-                this.resetClock = false;
-            }
-        }
-
-        return returnVal;
-    };
-
-    sinon.fakeServerWithClock.restore = function restore() {
-        if (this.clock) {
-            this.clock.restore();
-        }
-
-        return sinon.fakeServer.restore.apply(this, arguments);
-    };
-}());
-
-/**
- * @depend ../sinon.js
- * @depend collection.js
- * @depend util/fake_timers.js
- * @depend util/fake_server_with_clock.js
- */
-/*jslint eqeqeq: false, onevar: false, plusplus: false*/
-/*global require, module*/
-/**
- * Manages fake collections as well as fake utilities such as Sinon's
- * timers and fake XHR implementation in one convenient object.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-if (typeof module !== 'undefined' && module.exports) {
-    var sinon = require("../sinon");
-    sinon.extend(sinon, require("./util/fake_timers"));
-}
-
-(function () {
-    var push = [].push;
-
-    function exposeValue(sandbox, config, key, value) {
-        if (!value) {
-            return;
-        }
-
-        if (config.injectInto && !(key in config.injectInto)) {
-            config.injectInto[key] = value;
-            sandbox.injectedKeys.push(key);
-        } else {
-            push.call(sandbox.args, value);
-        }
-    }
-
-    function prepareSandboxFromConfig(config) {
-        var sandbox = sinon.create(sinon.sandbox);
-
-        if (config.useFakeServer) {
-            if (typeof config.useFakeServer == "object") {
-                sandbox.serverPrototype = config.useFakeServer;
-            }
-
-            sandbox.useFakeServer();
-        }
-
-        if (config.useFakeTimers) {
-            if (typeof config.useFakeTimers == "object") {
-                sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers);
-            } else {
-                sandbox.useFakeTimers();
-            }
-        }
-
-        return sandbox;
-    }
-
-    sinon.sandbox = sinon.extend(sinon.create(sinon.collection), {
-        useFakeTimers: function useFakeTimers() {
-            this.clock = sinon.useFakeTimers.apply(sinon, arguments);
-
-            return this.add(this.clock);
-        },
-
-        serverPrototype: sinon.fakeServer,
-
-        useFakeServer: function useFakeServer() {
-            var proto = this.serverPrototype || sinon.fakeServer;
-
-            if (!proto || !proto.create) {
-                return null;
-            }
-
-            this.server = proto.create();
-            return this.add(this.server);
-        },
-
-        inject: function (obj) {
-            sinon.collection.inject.call(this, obj);
-
-            if (this.clock) {
-                obj.clock = this.clock;
-            }
-
-            if (this.server) {
-                obj.server = this.server;
-                obj.requests = this.server.requests;
-            }
-
-            return obj;
-        },
-
-        restore: function () {
-            sinon.collection.restore.apply(this, arguments);
-            this.restoreContext();
-        },
-
-        restoreContext: function () {
-            if (this.injectedKeys) {
-                for (var i = 0, j = this.injectedKeys.length; i < j; i++) {
-                    delete this.injectInto[this.injectedKeys[i]];
-                }
-                this.injectedKeys = [];
-            }
-        },
-
-        create: function (config) {
-            if (!config) {
-                return sinon.create(sinon.sandbox);
-            }
-
-            var sandbox = prepareSandboxFromConfig(config);
-            sandbox.args = sandbox.args || [];
-            sandbox.injectedKeys = [];
-            sandbox.injectInto = config.injectInto;
-            var prop, value, exposed = sandbox.inject({});
-
-            if (config.properties) {
-                for (var i = 0, l = config.properties.length; i < l; i++) {
-                    prop = config.properties[i];
-                    value = exposed[prop] || prop == "sandbox" && sandbox;
-                    exposeValue(sandbox, config, prop, value);
-                }
-            } else {
-                exposeValue(sandbox, config, "sandbox", value);
-            }
-
-            return sandbox;
-        }
-    });
-
-    sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer;
-
-    if (typeof module !== 'undefined' && module.exports) {
-        module.exports = sinon.sandbox;
-    }
-}());
-
-/**
- * @depend ../sinon.js
- * @depend stub.js
- * @depend mock.js
- * @depend sandbox.js
- */
-/*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/
-/*global module, require, sinon*/
-/**
- * Test function, sandboxes fakes
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    function test(callback) {
-        var type = typeof callback;
-
-        if (type != "function") {
-            throw new TypeError("sinon.test needs to wrap a test function, got " + type);
-        }
-
-        return function () {
-            var config = sinon.getConfig(sinon.config);
-            config.injectInto = config.injectIntoThis && this || config.injectInto;
-            var sandbox = sinon.sandbox.create(config);
-            var exception, result;
-            var args = Array.prototype.slice.call(arguments).concat(sandbox.args);
-
-            try {
-                result = callback.apply(this, args);
-            } catch (e) {
-                exception = e;
-            }
-
-            if (typeof exception !== "undefined") {
-                sandbox.restore();
-                throw exception;
-            }
-            else {
-                sandbox.verifyAndRestore();
-            }
-
-            return result;
-        };
-    }
-
-    test.config = {
-        injectIntoThis: true,
-        injectInto: null,
-        properties: ["spy", "stub", "mock", "clock", "server", "requests"],
-        useFakeTimers: true,
-        useFakeServer: true
-    };
-
-    if (commonJSModule) {
-        module.exports = test;
-    } else {
-        sinon.test = test;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-/**
- * @depend ../sinon.js
- * @depend test.js
- */
-/*jslint eqeqeq: false, onevar: false, eqeqeq: false*/
-/*global module, require, sinon*/
-/**
- * Test case, sandboxes all test functions
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function (sinon) {
-    var commonJSModule = typeof module !== 'undefined' && module.exports;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon || !Object.prototype.hasOwnProperty) {
-        return;
-    }
-
-    function createTest(property, setUp, tearDown) {
-        return function () {
-            if (setUp) {
-                setUp.apply(this, arguments);
-            }
-
-            var exception, result;
-
-            try {
-                result = property.apply(this, arguments);
-            } catch (e) {
-                exception = e;
-            }
-
-            if (tearDown) {
-                tearDown.apply(this, arguments);
-            }
-
-            if (exception) {
-                throw exception;
-            }
-
-            return result;
-        };
-    }
-
-    function testCase(tests, prefix) {
-        /*jsl:ignore*/
-        if (!tests || typeof tests != "object") {
-            throw new TypeError("sinon.testCase needs an object with test functions");
-        }
-        /*jsl:end*/
-
-        prefix = prefix || "test";
-        var rPrefix = new RegExp("^" + prefix);
-        var methods = {}, testName, property, method;
-        var setUp = tests.setUp;
-        var tearDown = tests.tearDown;
-
-        for (testName in tests) {
-            if (tests.hasOwnProperty(testName)) {
-                property = tests[testName];
-
-                if (/^(setUp|tearDown)$/.test(testName)) {
-                    continue;
-                }
-
-                if (typeof property == "function" && rPrefix.test(testName)) {
-                    method = property;
-
-                    if (setUp || tearDown) {
-                        method = createTest(property, setUp, tearDown);
-                    }
-
-                    methods[testName] = sinon.test(method);
-                } else {
-                    methods[testName] = tests[testName];
-                }
-            }
-        }
-
-        return methods;
-    }
-
-    if (commonJSModule) {
-        module.exports = testCase;
-    } else {
-        sinon.testCase = testCase;
-    }
-}(typeof sinon == "object" && sinon || null));
-
-/**
- * @depend ../sinon.js
- * @depend stub.js
- */
-/*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/
-/*global module, require, sinon*/
-/**
- * Assertions matching the test spy retrieval interface.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-
-(function (sinon, global) {
-    var commonJSModule = typeof module !== "undefined" && module.exports;
-    var slice = Array.prototype.slice;
-    var assert;
-
-    if (!sinon && commonJSModule) {
-        sinon = require("../sinon");
-    }
-
-    if (!sinon) {
-        return;
-    }
-
-    function verifyIsStub() {
-        var method;
-
-        for (var i = 0, l = arguments.length; i < l; ++i) {
-            method = arguments[i];
-
-            if (!method) {
-                assert.fail("fake is not a spy");
-            }
-
-            if (typeof method != "function") {
-                assert.fail(method + " is not a function");
-            }
-
-            if (typeof method.getCall != "function") {
-                assert.fail(method + " is not stubbed");
-            }
-        }
-    }
-
-    function failAssertion(object, msg) {
-        object = object || global;
-        var failMethod = object.fail || assert.fail;
-        failMethod.call(object, msg);
-    }
-
-    function mirrorPropAsAssertion(name, method, message) {
-        if (arguments.length == 2) {
-            message = method;
-            method = name;
-        }
-
-        assert[name] = function (fake) {
-            verifyIsStub(fake);
-
-            var args = slice.call(arguments, 1);
-            var failed = false;
-
-            if (typeof method == "function") {
-                failed = !method(fake);
-            } else {
-                failed = typeof fake[method] == "function" ?
-                    !fake[method].apply(fake, args) : !fake[method];
-            }
-
-            if (failed) {
-                failAssertion(this, fake.printf.apply(fake, [message].concat(args)));
-            } else {
-                assert.pass(name);
-            }
-        };
-    }
-
-    function exposedName(prefix, prop) {
-        return !prefix || /^fail/.test(prop) ? prop :
-            prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1);
-    }
-
-    assert = {
-        failException: "AssertError",
-
-        fail: function fail(message) {
-            var error = new Error(message);
-            error.name = this.failException || assert.failException;
-
-            throw error;
-        },
-
-        pass: function pass(assertion) {},
-
-        callOrder: function assertCallOrder() {
-            verifyIsStub.apply(null, arguments);
-            var expected = "", actual = "";
-
-            if (!sinon.calledInOrder(arguments)) {
-                try {
-                    expected = [].join.call(arguments, ", ");
-                    var calls = slice.call(arguments);
-                    var i = calls.length;
-                    while (i) {
-                        if (!calls[--i].called) {
-                            calls.splice(i, 1);
-                        }
-                    }
-                    actual = sinon.orderByFirstCall(calls).join(", ");
-                } catch (e) {
-                    // If this fails, we'll just fall back to the blank string
-                }
-
-                failAssertion(this, "expected " + expected + " to be " +
-                              "called in order but were called as " + actual);
-            } else {
-                assert.pass("callOrder");
-            }
-        },
-
-        callCount: function assertCallCount(method, count) {
-            verifyIsStub(method);
-
-            if (method.callCount != count) {
-                var msg = "expected %n to be called " + sinon.timesInWords(count) +
-                    " but was called %c%C";
-                failAssertion(this, method.printf(msg));
-            } else {
-                assert.pass("callCount");
-            }
-        },
-
-        expose: function expose(target, options) {
-            if (!target) {
-                throw new TypeError("target is null or undefined");
-            }
-
-            var o = options || {};
-            var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix;
-            var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail;
-
-            for (var method in this) {
-                if (method != "export" && (includeFail || !/^(fail)/.test(method))) {
-                    target[exposedName(prefix, method)] = this[method];
-                }
-            }
-
-            return target;
-        },
-
-        match: function match(actual, expectation) {
-            var matcher = sinon.match(expectation);
-            if (matcher.test(actual)) {
-                assert.pass("match");
-            } else {
-                var formatted = [
-                    "expected value to match",
-                    "    expected = " + sinon.format(expectation),
-                    "    actual = " + sinon.format(actual)
-                ]
-                failAssertion(this, formatted.join("\n"));
-            }
-        }
-    };
-
-    mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called");
-    mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; },
-                          "expected %n to not have been called but was called %c%C");
-    mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C");
-    mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C");
-    mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C");
-    mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t");
-    mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t");
-    mirrorPropAsAssertion("calledWithNew", "expected %n to be called with new");
-    mirrorPropAsAssertion("alwaysCalledWithNew", "expected %n to always be called with new");
-    mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C");
-    mirrorPropAsAssertion("calledWithMatch", "expected %n to be called with match %*%C");
-    mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C");
-    mirrorPropAsAssertion("alwaysCalledWithMatch", "expected %n to always be called with match %*%C");
-    mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C");
-    mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C");
-    mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C");
-    mirrorPropAsAssertion("neverCalledWithMatch", "expected %n to never be called with match %*%C");
-    mirrorPropAsAssertion("threw", "%n did not throw exception%C");
-    mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C");
-
-    if (commonJSModule) {
-        module.exports = assert;
-    } else {
-        sinon.assert = assert;
-    }
-}(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : (typeof self != "undefined") ? self : global));
-
-return sinon;}.call(typeof window != 'undefined' && window || {}));
diff --git a/resources/lib/sinonjs/sinon-ie-1.10.3.js b/resources/lib/sinonjs/sinon-ie-1.10.3.js
new file mode 100644 (file)
index 0000000..de8c23d
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * Sinon.JS 1.10.3, 2014/07/11
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ *     * Redistributions of source code must retain the above copyright notice,
+ *       this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright notice,
+ *       this list of conditions and the following disclaimer in the documentation
+ *       and/or other materials provided with the distribution.
+ *     * Neither the name of Christian Johansen nor the names of his contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*global sinon, setTimeout, setInterval, clearTimeout, clearInterval, Date*/
+/**
+ * Helps IE run the fake timers. By defining global functions, IE allows
+ * them to be overwritten at a later point. If these are not defined like
+ * this, overwriting them will result in anything from an exception to browser
+ * crash.
+ *
+ * If you don't require fake timers to work in IE, don't include this file.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+function setTimeout() {}
+function clearTimeout() {}
+function setImmediate() {}
+function clearImmediate() {}
+function setInterval() {}
+function clearInterval() {}
+function Date() {}
+
+// Reassign the original functions. Now their writable attribute
+// should be true. Hackish, I know, but it works.
+setTimeout = sinon.timers.setTimeout;
+clearTimeout = sinon.timers.clearTimeout;
+setImmediate = sinon.timers.setImmediate;
+clearImmediate = sinon.timers.clearImmediate;
+setInterval = sinon.timers.setInterval;
+clearInterval = sinon.timers.clearInterval;
+Date = sinon.timers.Date;
+
+/*global sinon*/
+/**
+ * Helps IE run the fake XMLHttpRequest. By defining global functions, IE allows
+ * them to be overwritten at a later point. If these are not defined like
+ * this, overwriting them will result in anything from an exception to browser
+ * crash.
+ *
+ * If you don't require fake XHR to work in IE, don't include this file.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+function XMLHttpRequest() {}
+
+// Reassign the original function. Now its writable attribute
+// should be true. Hackish, I know, but it works.
+XMLHttpRequest = sinon.xhr.XMLHttpRequest || undefined;
+/*global sinon*/
+/**
+ * Helps IE run the fake XDomainRequest. By defining global functions, IE allows
+ * them to be overwritten at a later point. If these are not defined like
+ * this, overwriting them will result in anything from an exception to browser
+ * crash.
+ *
+ * If you don't require fake XDR to work in IE, don't include this file.
+ */
+function XDomainRequest() {}
+
+// Reassign the original function. Now its writable attribute
+// should be true. Hackish, I know, but it works.
+XDomainRequest = sinon.xdr.XDomainRequest || undefined;
diff --git a/resources/lib/sinonjs/sinon-ie-1.9.0.js b/resources/lib/sinonjs/sinon-ie-1.9.0.js
deleted file mode 100644 (file)
index c9fbd9d..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * Sinon.JS 1.9.0, 2014/03/05
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
- *
- * (The BSD License)
- * 
- * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no
- * All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- * 
- *     * Redistributions of source code must retain the above copyright notice,
- *       this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above copyright notice,
- *       this list of conditions and the following disclaimer in the documentation
- *       and/or other materials provided with the distribution.
- *     * Neither the name of Christian Johansen nor the names of his contributors
- *       may be used to endorse or promote products derived from this software
- *       without specific prior written permission.
- * 
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/*global sinon, setTimeout, setInterval, clearTimeout, clearInterval, Date*/
-/**
- * Helps IE run the fake timers. By defining global functions, IE allows
- * them to be overwritten at a later point. If these are not defined like
- * this, overwriting them will result in anything from an exception to browser
- * crash.
- *
- * If you don't require fake timers to work in IE, don't include this file.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-function setTimeout() {}
-function clearTimeout() {}
-function setImmediate() {}
-function clearImmediate() {}
-function setInterval() {}
-function clearInterval() {}
-function Date() {}
-
-// Reassign the original functions. Now their writable attribute
-// should be true. Hackish, I know, but it works.
-setTimeout = sinon.timers.setTimeout;
-clearTimeout = sinon.timers.clearTimeout;
-setImmediate = sinon.timers.setImmediate;
-clearImmediate = sinon.timers.clearImmediate;
-setInterval = sinon.timers.setInterval;
-clearInterval = sinon.timers.clearInterval;
-Date = sinon.timers.Date;
-
-/*global sinon*/
-/**
- * Helps IE run the fake XMLHttpRequest. By defining global functions, IE allows
- * them to be overwritten at a later point. If these are not defined like
- * this, overwriting them will result in anything from an exception to browser
- * crash.
- *
- * If you don't require fake XHR to work in IE, don't include this file.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2013 Christian Johansen
- */
-function XMLHttpRequest() {}
-
-// Reassign the original function. Now its writable attribute
-// should be true. Hackish, I know, but it works.
-XMLHttpRequest = sinon.xhr.XMLHttpRequest || undefined;
index d14c64b..a4039d8 100644 (file)
@@ -2,14 +2,17 @@
  * Skip function for es5-shim module.
  *
  * Test for strict mode as a proxy for full ES5 function support (but not syntax)
- * Per http://kangax.github.io/compat-table/es5/ this is a reasonable short-cut
- * that still allows this to be as short as possible (there are no function "No"s
- * for non-"obsolete" real browsers where strict support is available).
+ * Per http://kangax.github.io/compat-table/es5/ this is a reasonable shortcut
+ * that still allows this to be as short as possible (there are no browsers we
+ * support that have strict mode, but lack other features).
  *
- * Note that this will cause IE9 users to get the shim (which should be close to
- * a no-op but will increase page payload).
+ * Do explicitly test for Function#bind because of PhantomJS (which implements
+ * strict mode, but lacks Function#bind).
+ *
+ * IE9 supports all features except strict mode, so loading es5-shim should be close to
+ * a no-op but does increase page payload).
  */
 return ( function () {
        'use strict';
-       return !this;
+       return !this && !!Function.prototype.bind;
 }() );
index 915cd85..f38decd 100644 (file)
@@ -1,4 +1,6 @@
 ( function ( mw, $ ) {
+       // @deprecated since 1.24.  The 'jquery.json' module will be removed in MW 1.25.  Use the 'json' module.
+
        mw.log.deprecate( $, 'toJSON', $.toJSON, 'Use JSON.stringify instead (module "json" for polyfill).' );
        mw.log.deprecate( $, 'evalJSON', $.evalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
        mw.log.deprecate( $, 'secureEvalJSON', $.secureEvalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
index 8f46645..6bde5d3 100644 (file)
@@ -14,9 +14,7 @@
 .ui-widget .ui-widget { font-size: 1em; }
 .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: sans-serif; font-size: 1em; }
 .ui-widget-content { border: 1px solid #cccccc; /* @embed */ background: #f2f5f7 url(images/ui-bg_highlight-hard_100_f2f5f7_1x100.png) 50% top repeat-x; color: #362b36; }
-.ui-widget-content a { color: #362b36; }
 .ui-widget-header { border-bottom: 1px solid #bbbbbb; line-height: 1em; /* @embed */ background: #ffffff url(images/ui-bg_highlight-soft_100_ffffff_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
-.ui-widget-header a { color: #222222; }
 
 /* Interaction states
 ----------------------------------*/
index 66a3c56..f8641e1 100644 (file)
         * @chainable
         */
        $.fn.arrowSteps = function () {
-               var $steps, width, arrowWidth,
+               var $steps, width, arrowWidth, $stepDiv,
+                       $el = this,
                        paddingSide = $( 'body' ).hasClass( 'rtl' ) ? 'padding-left' : 'padding-right';
 
-               this.addClass( 'arrowSteps' );
-               $steps = this.find( 'li' );
+               $el.addClass( 'arrowSteps' );
+               $steps = $el.find( 'li' );
 
                width = parseInt( 100 / $steps.length, 10 );
                $steps.css( 'width', width + '%' );
                // Every step except the last one has an arrow pointing forward:
                // at the right hand side in LTR languages, and at the left hand side in RTL.
                // Also add in the padding for the calculated arrow width.
-               arrowWidth = parseInt( this.outerHeight(), 10 );
-               $steps.filter( ':not(:last-child)' ).addClass( 'arrow' )
-                       .find( 'div' ).css( paddingSide, arrowWidth.toString() + 'px' );
+               $stepDiv = $steps.filter( ':not(:last-child)' ).addClass( 'arrow' ).find( 'div' );
+
+               // Execute when complete page is fully loaded, including all frames, objects and images
+               $( window ).load( function () {
+                       arrowWidth = parseInt( $el.outerHeight(), 10 );
+                       $stepDiv.css( paddingSide, arrowWidth.toString() + 'px' );
+               } );
+
+               $el.data( 'arrowSteps', $steps );
 
-               this.data( 'arrowSteps', $steps );
                return this;
        };
 
index 05745f8..a4dc33b 100644 (file)
                        } else {
                                collapsibleId = $collapsible.attr( 'id' ) || '';
                                if ( collapsibleId.indexOf( 'mw-customcollapsible-' ) === 0 ) {
-                                       $customTogglers = $( '.' + collapsibleId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) );
-                                       $customTogglers.addClass( 'mw-customtoggle' );
+                                       $customTogglers = $( '.' + collapsibleId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) )
+                                               .addClass( 'mw-customtoggle' );
                                }
                        }
 
                                        togglingHandler( $( this ), $collapsible, e, opts );
                                };
 
-                               $toggleLink = $customTogglers;
-                               $toggleLink.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
+                               $toggleLink = $customTogglers
+                                       .on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+                                       .prop( 'tabIndex', 0 );
 
                        } else {
                                // If this is not a custom case, do the default: wrap the
                                                        $toggleLink = buildDefaultToggleLink().appendTo( $caption );
                                                } else {
                                                        actionHandler = premadeToggleHandler;
-                                                       $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
+                                                       $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+                                                               .prop( 'tabIndex', 0 );
                                                }
                                        } else {
                                                // The toggle-link will be in one the the cells (td or th) of the first row
                                                        $toggleLink = buildDefaultToggleLink().prependTo( $firstItem.eq( -1 ) );
                                                } else {
                                                        actionHandler = premadeToggleHandler;
-                                                       $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
+                                                       $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+                                                               .prop( 'tabIndex', 0 );
                                                }
                                        }
 
                                                $toggleLink.wrap( '<li class="mw-collapsible-toggle-li"></li>' ).parent().prependTo( $collapsible );
                                        } else {
                                                actionHandler = premadeToggleHandler;
-                                               $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
+                                               $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+                                                       .prop( 'tabIndex', 0 );
                                        }
 
                                } else { // <div>, <p> etc.
                                                $toggleLink = buildDefaultToggleLink().prependTo( $collapsible );
                                        } else {
                                                actionHandler = premadeToggleHandler;
-                                               $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
+                                               $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+                                                       .prop( 'tabIndex', 0 );
                                        }
                                }
                        }
 
-                       // Attributes for accessibility. This isn't necessary when the toggler is already
-                       // an <a> or a <button> etc., but it doesn't hurt either, and it's consistent.
-                       $toggleLink.prop( 'tabIndex', 0 );
-
                        // Initial state
                        if ( options.collapsed || $collapsible.hasClass( 'mw-collapsed' ) ) {
                                // One toggler can hook to multiple elements, and one element can have
index 042db91..0573f66 100644 (file)
@@ -25,7 +25,7 @@
        $.fn.textSelection = function ( command, options ) {
                var fn,
                        context,
-                       hasIframe,
+                       hasWikiEditorSurface, // The alt edit surface needs to implement the WikiEditor API
                        needSave,
                        retval;
 
                                        // Position to start selection at
                                        start: undefined,
                                        // Position to end selection at. Defaults to start
-                                       end: undefined,
-                                       // Element to start selection in (iframe only)
-                                       startContainer: undefined,
-                                       // Element to end selection in (iframe only). Defaults to startContainer
-                                       endContainer: undefined
+                                       end: undefined
                                }, options );
 
                                if ( options.end === undefined ) {
                                        options.end = options.start;
                                }
-                               if ( options.endContainer === undefined ) {
-                                       options.endContainer = options.startContainer;
-                               }
                                // FIXME: We may not need character position-based functions if we insert markers in the right places
                                break;
                        case 'scrollToCaretPosition':
                }
 
                context = $( this ).data( 'wikiEditor-context' );
-               hasIframe = context !== undefined && context && context.$iframe !== undefined;
+               hasWikiEditorSurface = ( context !== undefined );
 
                // IE selection restore voodoo
                needSave = false;
-               if ( hasIframe && context.savedSelection !== null ) {
+               if ( hasWikiEditorSurface && context.savedSelection !== null ) {
                        context.fn.restoreSelection();
                        needSave = true;
                }
-               retval = ( hasIframe ? context.fn : fn )[command].call( this, options );
-               if ( hasIframe && needSave ) {
+               retval = ( hasWikiEditorSurface && context.fn[command] !== undefined ? context.fn : fn )[command].call( this, options );
+               if ( hasWikiEditorSurface && needSave ) {
                        context.fn.saveSelection();
                }
 
index edfb34a..e88ae5e 100644 (file)
                                mw.track( 'mw.deprecate', 'api.cbParam' );
                                mw.log.warn( msg );
                        }
+
                        return this.postWithToken( 'edit', params ).done( ok ).fail( err );
                },
 
                /**
-                * Api helper to grab an edit token.
+                * API helper to grab an edit token.
                 *
                 * @param {Function} [ok] Success callback (deprecated)
                 * @param {Function} [err] Error callback (deprecated)
                                mw.track( 'mw.deprecate', 'api.cbParam' );
                                mw.log.warn( msg );
                        }
+
                        return this.getToken( 'edit' ).done( ok ).fail( err );
                },
 
                /**
-                * Create a new section of the page.
+                * Post a new section to the page.
                 * @see #postWithEditToken
                 * @param {mw.Title|String} title Target page
                 * @param {string} header
                 * @param {string} message wikitext message
+                * @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
                 * @param {Function} [ok] Success handler (deprecated)
                 * @param {Function} [err] Error handler (deprecated)
                 * @return {jQuery.Promise}
                 */
-               newSection: function ( title, header, message, ok, err ) {
+               newSection: function ( title, header, message, additionalParams, ok, err ) {
+                       // Until we remove 'ok' and 'err' parameters, we have to support code that passes them,
+                       // but not additionalParams...
+                       if ( $.isFunction( additionalParams ) ) {
+                               err = ok;
+                               ok = additionalParams;
+                               additionalParams = undefined;
+                       }
+
                        if ( ok || err ) {
                                mw.track( 'mw.deprecate', 'api.cbParam' );
                                mw.log.warn( msg );
                        }
-                       return this.postWithEditToken( {
+
+                       return this.postWithEditToken( $.extend( {
                                action: 'edit',
                                section: 'new',
                                format: 'json',
                                title: String( title ),
                                summary: header,
                                text: message
-                       } ).done( ok ).fail( err );
+                       }, additionalParams ) ).done( ok ).fail( err );
                }
        } );
 
diff --git a/resources/src/mediawiki.hidpi-skip.js b/resources/src/mediawiki.hidpi-skip.js
new file mode 100644 (file)
index 0000000..26b63c7
--- /dev/null
@@ -0,0 +1,4 @@
+/*!
+ * Skip function for mediawiki.hdpi.js.
+ */
+return 'srcset' in new Image();
index b65a62b..8c28884 100644 (file)
 }
 
 .transition(@value) {
-       -webkit-transition: @value;
-       -moz-transition: @value;
-       -o-transition: @value;
-       transition: @value;
+       -webkit-transition: @value; // Safari 3.1-6.0, iOS 3.2-6.1, Android 2.1-4.3
+       -moz-transition: @value; // Firefox 4-15
+       -o-transition: @value; // Opera 10.5-12.0
+       transition: @value; // Chrome 26+, Firefox 16+, IE 10+, Safari 6.1+, Opera 12.1+, iOS 7+, Android 4.4+
 }
 
 .box-sizing(@value) {
-       -webkit-box-sizing: @value;
-       -moz-box-sizing: @value;
-       box-sizing: @value;
+       -webkit-box-sizing: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0
+       -moz-box-sizing: @value; // Firefox 4-28,
+       box-sizing: @value; // Chrome 10+, Firefox 29+, IE 8+, Safari 5.1+, Opera 10+, iOS 5+, Android 4+
 }
 
 .box-shadow(@value) {
-       -webkit-box-shadow: @value; // Android 2.3+, iOS 4.0.2-4.2, Safari 3-4
-       box-shadow: @value; // Chrome 6+, Firefox 4+, IE 9+, iOS 5+, Opera 10.50+
+       -webkit-box-shadow: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0
+       box-shadow: @value; // Chrome 10+, Firefox 4+, IE 9+, Safari 5.1+, Opera 11+, iOS 5+, Android 4+
 }
diff --git a/resources/src/mediawiki.less/mediawiki.ui/mixins.less b/resources/src/mediawiki.less/mediawiki.ui/mixins.less
new file mode 100644 (file)
index 0000000..bda4043
--- /dev/null
@@ -0,0 +1,147 @@
+// ----------------------------------------------------------------------------
+// Form styling mixins
+// ----------------------------------------------------------------------------
+
+// Font is not included.
+.agora-field-styling() {
+
+       border: 1px solid @colorGrayLight;
+
+       &:focus {
+               // Styling focus of native checkboxes etc on Mac is almost impossible.
+               &:not([type=checkbox]):not([type=radio]) {
+                       outline: 0; // Removes OS field focus
+               }
+
+               box-shadow: @colorProgressiveShadow 0 0 5px;
+
+               border-color: @colorProgressiveShadow;
+       }
+
+       color: @colorText;
+       padding: 0.35em 0.5em 0.35em 0.5em;
+
+       // Ensure that buttons and inputs are nicely aligned when they have differing heights
+       vertical-align: middle;
+}
+
+.agora-label-styling() {
+       //font-weight: bold;
+       font-size: 0.9em;
+       color: darken(@colorGrayLight, 50%);
+
+       * {
+               font-weight: normal;
+       }
+}
+
+.agora-inline-label-styling() {
+       margin-bottom: 0.5em;
+       cursor: pointer;
+       vertical-align: bottom;
+       line-height: normal;
+
+       font-weight: normal;
+
+       & > input[type="checkbox"],
+       & > input[type="radio"] {
+               width: auto;
+               height: auto;
+               margin: 0 0.1em 0 0;
+               padding: 0;
+               border: 1px solid @colorGrayLight;
+               cursor: pointer;
+       }
+}
+
+// ----------------------------------------------------------------------------
+// Button styling
+// ----------------------------------------------------------------------------
+
+.button-colors(@bgColor) {
+       background: @bgColor;
+
+       &:hover,
+       &:focus {
+               // The inner bottom bevel should match the active background color.
+               box-shadow: 0 1px rgba(0, 0, 0, 10%), inset 0 -3px rgba(0, 0, 0, 20%);
+               border-bottom-color: mix(#000, @bgColor, 20%);
+               outline: none;
+               // remove outline in Firefox
+               &::-moz-focus-inner {
+                       border-color: transparent;
+               }
+       }
+
+       &:active,
+       &.mw-ui-checked {
+               // lessphp doesn't implement shade (https://github.com/leafo/lessphp/issues/528);
+               // it passes it through, then ResourceLoader drops it.
+               // background: shade(@bgColor, 20%);
+               background: mix(#000, @bgColor, 20%);
+               box-shadow: none;
+       }
+}
+
+.button-colors(@bgColor) when (lightness(@bgColor) >= 70%) {
+       color: @colorGrayDark;
+       border: 1px solid @colorGrayLight;
+
+       &:disabled {
+               color: @colorGrayLight;
+
+               // make sure disabled buttons don't have hover and active states
+               &:hover,
+               &:active {
+                       background: @bgColor;
+                       box-shadow: none;
+               }
+       }
+}
+
+.button-colors(@bgColor) when (lightness(@bgColor) < 70%) {
+       color: @colorWhite;
+       // border of the same color as background so that light background and
+       // dark background buttons are the same height (only top and bottom to
+       // make box shadow on hover cover the corners too)
+       border: 1px solid @bgColor;
+       border-left: none;
+       border-right: none;
+       text-shadow: 0 1px rgba(0, 0, 0, .1);
+
+       &:disabled {
+               background: @colorGrayLight;
+               border-color: @colorGrayLight;
+
+               // make sure disabled buttons don't have hover and active states
+               &:hover,
+               &:active,
+               &.mw-ui-checked {
+                       box-shadow: none;
+               }
+       }
+}
+
+.button-colors-quiet(@textColor) {
+       // Quiet buttons all start gray, and reveal
+       // constructive/progressive/destructive color on hover and active.
+       color: @colorGrayDark;
+
+       &:hover,
+       &:focus {
+               // lessphp doesn't implement tint, see above
+               // color: tint(@textColor, 20%);
+               color: mix(#fff, @textColor, 20%);
+       }
+
+       &:active,
+       &.mw-ui-checked {
+               // lessphp doesn't implement shade, see above
+               // color: shade(@textColor, 20%);
+               color: mix(#000, @textColor, 20%);
+       }
+
+       &:disabled {
+               color: @colorGrayLight;
+       }
+}
diff --git a/resources/src/mediawiki.less/mediawiki.ui/variables.less b/resources/src/mediawiki.less/mediawiki.ui/variables.less
new file mode 100644 (file)
index 0000000..ccf869d
--- /dev/null
@@ -0,0 +1,37 @@
+@baseFontSize: 1em;
+
+// FIXME: remove @colorProgressiveShadow (shadows should be generated
+// in LESS by dimming the original colors)
+@colorProgressiveShadow: #4091ed;
+
+// White; for background use, and text use on dark backgrounds
+@colorWhite: #fff;
+// Off-white; for background use on white backgrounds
+@colorOffWhite: #fafafa;
+// Dark gray; for non-text use
+@colorGrayDark: #898989;
+// Light gray; for non-text use
+@colorGrayLight: #ccc;
+// Very light gray; for non-text use
+@colorGrayLighter: #ddd;
+// Lightest gray; for non-text use
+@colorGrayLightest: #eee;
+
+// Dark gray; for body text
+@colorText: #252525;
+// Light gray; for less important body text and links
+@colorTextLight: #696969;
+
+// Blue; for contextual use of a continuing action
+@colorProgressive: #347bff;
+// Orange; for contextual use of returning to a past action
+@colorRegressive: #ff7e1e;
+// Green; for contextual use of a positive finalizing action
+@colorConstructive: #00af89;
+// Red; for contextual use of a negative finalizing action
+@colorDestructive: #d11d13;
+
+// Used in mixins to darken contextual colors by the same amount (eg. focus)
+@colorDarkenPercentage: 13.5%;
+// Used in mixins to lighten contextual colors by the same amount (eg. hover)
+@colorLightenPercentage: 13.5%;
\ No newline at end of file
index e7c962f..1892967 100644 (file)
@@ -2,7 +2,7 @@
  * Show gallery captions when focused. Copied directly from jquery.mw-jump.js.
  * Also Dynamically resize images to justify them.
  */
-( function ( $, mw ) {
+( function ( $ ) {
        $( function () {
                var isTouchScreen,
                        gettingFocus,
@@ -93,7 +93,6 @@
                                        $imageElm,
                                        imageElm,
                                        $caption,
-                                       hookInfo,
                                        i,
                                        j,
                                        avgZoom,
                                                }
 
                                                if ( curRow[j].aspect === 0 || !isFinite( curRow[j].aspect ) ) {
-                                                       mw.log( 'Skipping item ' + j + ' due to aspect: ' + curRow[j].aspect );
                                                        // One of the dimensions are 0. Probably should
                                                        // not try to resize.
                                                        combinedPadding += curRow[j].width;
                                                // Also on the off chance there is a bug in this
                                                // code, would prevent accidentally expanding to
                                                // be 10 billion pixels wide.
-                                               mw.log( 'mw.page.gallery: Cannot fit row, aspect is ' + preferredHeight / curRowHeight );
                                                if ( i === rows.length - 1 ) {
                                                        // If its the last row, and we can't fit it,
                                                        // don't make the entire row huge.
                                        }
                                        if ( !isFinite( preferredHeight ) ) {
                                                // This *definitely* should not happen.
-                                               mw.log( 'mw.page.gallery: Trying to resize row ' + i + ' to ' + preferredHeight + '?!' );
                                                // Skip this row.
                                                continue;
                                        }
                                        if ( preferredHeight < 5 ) {
                                                // Well something clearly went wrong...
-                                               mw.log( {
-                                                       maxWidth: maxWidth,
-                                                       combinedPadding: combinedPadding,
-                                                       combinedAspect: combinedAspect,
-                                                       wantedWidth: wantedWidth
-                                               } );
-                                               mw.log( 'mw.page.gallery: [BUG!] Fitting row ' + i + ' to too small a size: ' + preferredHeight );
                                                // Skip this row.
                                                continue;
                                        }
 
                                                if ( newWidth < 60 || !isFinite( newWidth ) ) {
                                                        // Making something skinnier than this will mess up captions,
-                                                       mw.log( 'mw.page.gallery: Tried to make image ' + newWidth + 'px wide but too narrow.' );
                                                        if ( newWidth < 1 || !isFinite( newWidth ) ) {
                                                                $innerDiv.height( preferredHeight );
                                                                // Don't even try and touch the image size if it could mean
                                                        $caption.width( curRow[j].captionWidth + ( newWidth - curRow[j].imgWidth ) );
                                                }
 
-                                               hookInfo = {
-                                                       fullWidth: newWidth + padding,
-                                                       imgWidth: newWidth,
-                                                       imgHeight: preferredHeight,
-                                                       $innerDiv: $innerDiv,
-                                                       $imageDiv: $imageDiv,
-                                                       $outerDiv: $outerDiv,
-                                                       // Whether the hook took action
-                                                       resolved: false
-                                               };
-
-                                               /**
-                                                * Gallery resize.
-                                                *
-                                                * If your handler resizes an image, it should also set the resolved
-                                                * property to true. Additionally, because this module only exposes this
-                                                * logic temporarily, you should load your module in position top to
-                                                * ensure it is registered before this runs (FIXME: Don't use mw.hook)
-                                                *
-                                                * See TimedMediaHandler for an example.
-                                                *
-                                                * @event mediawiki_page_gallery_resize
-                                                * @member mw.hook
-                                                * @param {Object} hookInfo
-                                                */
-                                               mw.hook( 'mediawiki.page.gallery.resize' ).fire( hookInfo );
-
-                                               if ( !hookInfo.resolved ) {
-                                                       if ( imageElm ) {
-                                                               // We don't always have an img, e.g. in the case of an invalid file.
-                                                               imageElm.width = newWidth;
-                                                               imageElm.height = preferredHeight;
-                                                       } else {
-                                                               // Not a file box.
-                                                               $imageDiv.height( preferredHeight );
-                                                       }
+                                               if ( imageElm ) {
+                                                       // We don't always have an img, e.g. in the case of an invalid file.
+                                                       imageElm.width = newWidth;
+                                                       imageElm.height = preferredHeight;
+                                               } else {
+                                                       // Not a file box.
+                                                       $imageDiv.height( preferredHeight );
                                                }
                                        }
                                }
                        }() );
                } );
        } );
-}( jQuery, mediaWiki ) );
+}( jQuery ) );
index 6a35182..2f1b795 100644 (file)
@@ -68,6 +68,12 @@ figure[typeof*='mw:Image'] {
                float: left;
        }
 
+       &.mw-halign-none {
+               margin: 0;
+               clear: none;
+               float: none;
+       }
+
        &.mw-halign-center {
                margin: 0 auto .5em auto;
                display: table;
index a98c782..ef95507 100644 (file)
@@ -109,6 +109,7 @@ fieldset#mw-searchoptions table {
 }
 fieldset#mw-searchoptions table td {
        padding-right: 1em;
+       white-space: nowrap;
 }
 fieldset#mw-searchoptions div.divider {
        clear: both;
diff --git a/resources/src/mediawiki.ui/components/buttons.less b/resources/src/mediawiki.ui/components/buttons.less
new file mode 100644 (file)
index 0000000..ec08413
--- /dev/null
@@ -0,0 +1,227 @@
+@import "mediawiki.mixins";
+@import "mediawiki.ui/variables";
+@import "mediawiki.ui/mixins";
+
+// Buttons
+//
+// All buttons start with mw-ui-button class, modified by other classes.
+// It can be any element.  Due to a lack of a CSS reset, the exact styling of
+// the button depends on what type of element is used.
+// There are two kinds of buttons, the default is a "Call to Action" with an obvious border
+// and there is a quiet kind without a border.
+//
+// Styleguide 2.
+
+@buttonBorderRadius: 3px;
+@transitionDuration: .1s;
+@transitionFunction: ease-in-out;
+
+// Neutral button styling
+//
+// Markup:
+// <button class="mw-ui-button">.mw-ui-button</button>
+// <button class="mw-ui-button" disabled>.mw-ui-button</button>
+//
+// Styleguide 2.1.
+.mw-ui-button {
+       // Container layout
+       display: inline-block;
+       padding: .5em 1em;
+       margin: 0;
+       .box-sizing(border-box);
+
+       // Disable weird iOS styling
+       -webkit-appearance: none;
+
+       // IE6/IE7 hack
+       // http://stackoverflow.com/a/5838575/365238
+       *display: inline;
+       zoom: 1;
+
+       // Container styling
+       .button-colors(@colorWhite);
+       border-radius: @buttonBorderRadius;
+
+       // Ensure that buttons and inputs are nicely aligned when they have differing heights
+       vertical-align: middle;
+
+       // Content styling
+       text-align: center;
+       font-weight: bold;
+
+       // Interaction styling
+       cursor: pointer;
+
+       &:disabled {
+               text-shadow: none;
+               cursor: default;
+       }
+
+       .transition(background @transitionDuration @transitionFunction, color @transitionDuration @transitionFunction, box-shadow @transitionDuration @transitionFunction;);
+
+       // Styling for specific button types
+       // -----------------------------------------
+
+       // Big buttons
+       //
+       // Not all buttons are equal. You can emphasise certain actions over others
+       // using the mw-ui-big class.
+       //
+       // Markup:
+       // <button class="mw-ui-button mw-ui-big">.mw-ui-button</button>
+       // <button class="mw-ui-button mw-ui-progressive mw-ui-big">.mw-ui-progressive</button>
+       // <button class="mw-ui-button mw-ui-constructive mw-ui-big">.mw-ui-constructive</button>
+       // <button class="mw-ui-button mw-ui-destructive mw-ui-big">.mw-ui-destructive</button>
+       //
+       // Styleguide 2.1.6.
+       &.mw-ui-big {
+               font-size: @baseFontSize * 1.3;
+       }
+
+       // Block buttons
+       //
+       // Some buttons might need to be stacked.
+       //
+       // Markup:
+       // <button class="mw-ui-button mw-ui-block">.mw-ui-button</button>
+       // <button class="mw-ui-button mw-ui-progressive mw-ui-block">.mw-ui-progressive</button>
+       // <button class="mw-ui-button mw-ui-constructive mw-ui-block">.mw-ui-constructive</button>
+       // <button class="mw-ui-button mw-ui-destructive mw-ui-block">.mw-ui-destructive</button>
+       //
+       // Styleguide 2.1.5.
+       &.mw-ui-block {
+               display: block;
+               width: 100%;
+       }
+
+       // Progressive buttons
+       //
+       // Use progressive buttons for actions which lead to a next step in the process.
+       // .mw-ui-primary is deprecated, kept for compatibility.
+       //
+       // Markup:
+       // <button class="mw-ui-button mw-ui-progressive">.mw-ui-progressive</button>
+       // <button class="mw-ui-button mw-ui-progressive" disabled>.mw-ui-progressive</button>
+       //
+       // Styleguide 2.1.1.
+       &.mw-ui-progressive,
+       &.mw-ui-primary {
+               .button-colors(@colorProgressive);
+
+               &.mw-ui-quiet {
+                       .button-colors-quiet(@colorProgressive);
+               }
+       }
+
+       // Constructive buttons
+       //
+       // Use constructive buttons for actions which result in a final action in the process that results
+       // in a change of state.
+       // e.g. save changes button
+       //
+       // Markup:
+       // <button class="mw-ui-button mw-ui-constructive">.mw-ui-constructive</button>
+       // <button class="mw-ui-button mw-ui-constructive" disabled>.mw-ui-constructive</button>
+       //
+       // Styleguide 2.1.2.
+       &.mw-ui-constructive {
+               .button-colors(@colorConstructive);
+
+               &.mw-ui-quiet {
+                       .button-colors-quiet(@colorConstructive);
+               }
+       }
+
+       // Destructive buttons
+       //
+       // Use destructive buttons for actions which result in the destruction of data.
+       // e.g. deleting a page.
+       // This should not be used for cancel buttons.
+       //
+       // Markup:
+       // <button class="mw-ui-button mw-ui-destructive">.mw-ui-destructive</button>
+       // <button class="mw-ui-button mw-ui-destructive" disabled>.mw-ui-destructive</button>
+       //
+       // Styleguide 2.1.3.
+       &.mw-ui-destructive {
+               .button-colors(@colorDestructive);
+
+               &.mw-ui-quiet {
+                       .button-colors-quiet(@colorDestructive);
+               }
+       }
+
+       // Quiet buttons
+       //
+       // Use quiet buttons when they are less important and alongisde other progressive/destructive/progressive buttons.
+       //
+       // Markup:
+       // <button class="mw-ui-button mw-ui-quiet">.mw-ui-button</button>
+       // <button class="mw-ui-button mw-ui-constructive mw-ui-quiet">.mw-ui-constructive</button>
+       // <button class="mw-ui-button mw-ui-constructive mw-ui-quiet" disabled>.mw-ui-constructive</button>
+       // <button class="mw-ui-button mw-ui-destructive mw-ui-quiet">.mw-ui-destructive</button>
+       // <button class="mw-ui-button mw-ui-destructive mw-ui-quiet" disabled>.mw-ui-destructive</button>
+       // <button class="mw-ui-button mw-ui-progressive mw-ui-quiet">.mw-ui-progressive</button>
+       // <button class="mw-ui-button mw-ui-progressive mw-ui-quiet" disabled>.mw-ui-progressive</button>
+       //
+       // Styleguide 2.1.4.
+       &.mw-ui-quiet {
+               background: transparent;
+               border: none;
+               text-shadow: none;
+               .button-colors-quiet(@colorGrayDark);
+
+               &:hover,
+               &:focus {
+                       box-shadow: none;
+               }
+
+               &:active,
+               &:disabled {
+                       background: transparent;
+               }
+       }
+}
+
+a.mw-ui-button {
+       text-decoration: none;
+
+       // This overrides an underline declaration on a:hover and a:focus in
+       // commonElements.css, which the class alone isn't specific enough to do.
+       &:hover,
+       &:focus {
+               text-decoration: none;
+       }
+}
+
+// Button groups
+//
+// Group of buttons. Make sure you clear the floating after using a mw-ui-button-group.
+//
+// Markup:
+// <div class="mw-ui-button-group">
+//   <div class="mw-ui-button">A</div>
+//   <div class="mw-ui-button">B</div>
+//   <div class="mw-ui-button">C</div>
+//   <div class="mw-ui-button">D</div>
+// </div><div style="clear:both"></div>
+//
+// Styleguide 2.2.
+.mw-ui-button-group > * {
+       border-radius: 0;
+       float: left;
+
+       &:first-child {
+               border-top-left-radius: @buttonBorderRadius;
+               border-bottom-left-radius: @buttonBorderRadius;
+       }
+
+       &:not(:first-child) {
+               border-left: none;
+       }
+
+       &:last-child{
+               border-top-right-radius: @buttonBorderRadius;
+               border-bottom-right-radius: @buttonBorderRadius;
+       }
+}
diff --git a/resources/src/mediawiki.ui/components/default/buttons.less b/resources/src/mediawiki.ui/components/default/buttons.less
deleted file mode 100644 (file)
index dce4cd0..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-@import "mediawiki.mixins";
-@import "../../settings/typography";
-@import "../../mixins/effects";
-@import "../../mixins/utilities";
-
-// Buttons
-//
-// All buttons start with mw-ui-button class, modified by other classes.
-// It can be any element.  Due to a lack of a CSS reset, the exact styling of
-// the button depends on what type of element is used.
-// There are two kinds of buttons, the default is a "Call to Action" with an obvious border
-// and there is a quiet kind without a border.
-//
-// Styleguide 2.
-
-@buttonBorderRadius: 3px;
-@transitionDuration: .1s;
-@transitionFunction: ease-in-out;
-
-// Neutral button styling
-//
-// Markup:
-// <button class="mw-ui-button">.mw-ui-button</button>
-// <button class="mw-ui-button" disabled>.mw-ui-button</button>
-//
-// Styleguide 2.1.
-.mw-ui-button {
-       // Container layout
-       display: inline-block;
-       padding: .5em 1em;
-       margin: 0;
-       .box-sizing(border-box);
-
-       // Disable weird iOS styling
-       -webkit-appearance: none;
-
-       // IE6/IE7 hack
-       // http://stackoverflow.com/a/5838575/365238
-       *display: inline;
-       zoom: 1;
-
-       // Container styling
-       .button-colors(@colorWhite);
-       border-radius: @buttonBorderRadius;
-
-       // Ensure that buttons and inputs are nicely aligned when they have differing heights
-       vertical-align: middle;
-
-       // Content styling
-       text-align: center;
-       font-weight: bold;
-
-       // Interaction styling
-       cursor: pointer;
-
-       &:disabled {
-               text-shadow: none;
-               cursor: default;
-       }
-
-       .transition(background @transitionDuration @transitionFunction, color @transitionDuration @transitionFunction, box-shadow @transitionDuration @transitionFunction;);
-
-       // Styling for specific button types
-       // -----------------------------------------
-
-       // Big buttons
-       //
-       // Not all buttons are equal. You can emphasise certain actions over others
-       // using the mw-ui-big class.
-       //
-       // Markup:
-       // <button class="mw-ui-button mw-ui-big">.mw-ui-button</button>
-       // <button class="mw-ui-button mw-ui-progressive mw-ui-big">.mw-ui-progressive</button>
-       // <button class="mw-ui-button mw-ui-constructive mw-ui-big">.mw-ui-constructive</button>
-       // <button class="mw-ui-button mw-ui-destructive mw-ui-big">.mw-ui-destructive</button>
-       //
-       // Styleguide 2.1.6.
-       &.mw-ui-big {
-               font-size: @baseFontSize * 1.3;
-       }
-
-       // Block buttons
-       //
-       // Some buttons might need to be stacked.
-       //
-       // Markup:
-       // <button class="mw-ui-button mw-ui-block">.mw-ui-button</button>
-       // <button class="mw-ui-button mw-ui-progressive mw-ui-block">.mw-ui-progressive</button>
-       // <button class="mw-ui-button mw-ui-constructive mw-ui-block">.mw-ui-constructive</button>
-       // <button class="mw-ui-button mw-ui-destructive mw-ui-block">.mw-ui-destructive</button>
-       //
-       // Styleguide 2.1.5.
-       &.mw-ui-block {
-               display: block;
-               width: 100%;
-       }
-
-       // Progressive buttons
-       //
-       // Use progressive buttons for actions which lead to a next step in the process.
-       // .mw-ui-primary is deprecated, kept for compatibility.
-       //
-       // Markup:
-       // <button class="mw-ui-button mw-ui-progressive">.mw-ui-progressive</button>
-       // <button class="mw-ui-button mw-ui-progressive" disabled>.mw-ui-progressive</button>
-       //
-       // Styleguide 2.1.1.
-       &.mw-ui-progressive,
-       &.mw-ui-primary {
-               .button-colors(@colorProgressive);
-
-               &.mw-ui-quiet {
-                       .button-colors-quiet(@colorProgressive);
-               }
-       }
-
-       // Constructive buttons
-       //
-       // Use constructive buttons for actions which result in a final action in the process that results
-       // in a change of state.
-       // e.g. save changes button
-       //
-       // Markup:
-       // <button class="mw-ui-button mw-ui-constructive">.mw-ui-constructive</button>
-       // <button class="mw-ui-button mw-ui-constructive" disabled>.mw-ui-constructive</button>
-       //
-       // Styleguide 2.1.2.
-       &.mw-ui-constructive {
-               .button-colors(@colorConstructive);
-
-               &.mw-ui-quiet {
-                       .button-colors-quiet(@colorConstructive);
-               }
-       }
-
-       // Destructive buttons
-       //
-       // Use destructive buttons for actions which result in the destruction of data.
-       // e.g. deleting a page.
-       // This should not be used for cancel buttons.
-       //
-       // Markup:
-       // <button class="mw-ui-button mw-ui-destructive">.mw-ui-destructive</button>
-       // <button class="mw-ui-button mw-ui-destructive" disabled>.mw-ui-destructive</button>
-       //
-       // Styleguide 2.1.3.
-       &.mw-ui-destructive {
-               .button-colors(@colorDestructive);
-
-               &.mw-ui-quiet {
-                       .button-colors-quiet(@colorDestructive);
-               }
-       }
-
-       // Quiet buttons
-       //
-       // Use quiet buttons when they are less important and alongisde other progressive/destructive/progressive buttons.
-       //
-       // Markup:
-       // <button class="mw-ui-button mw-ui-quiet">.mw-ui-button</button>
-       // <button class="mw-ui-button mw-ui-constructive mw-ui-quiet">.mw-ui-constructive</button>
-       // <button class="mw-ui-button mw-ui-constructive mw-ui-quiet" disabled>.mw-ui-constructive</button>
-       // <button class="mw-ui-button mw-ui-destructive mw-ui-quiet">.mw-ui-destructive</button>
-       // <button class="mw-ui-button mw-ui-destructive mw-ui-quiet" disabled>.mw-ui-destructive</button>
-       // <button class="mw-ui-button mw-ui-progressive mw-ui-quiet">.mw-ui-progressive</button>
-       // <button class="mw-ui-button mw-ui-progressive mw-ui-quiet" disabled>.mw-ui-progressive</button>
-       //
-       // Styleguide 2.1.4.
-       &.mw-ui-quiet {
-               background: transparent;
-               border: none;
-               text-shadow: none;
-               .button-colors-quiet(@colorGrayDark);
-
-               &:hover,
-               &:focus {
-                       box-shadow: none;
-               }
-
-               &:active,
-               &:disabled {
-                       background: transparent;
-               }
-       }
-}
-
-a.mw-ui-button {
-       text-decoration: none;
-
-       // This overrides an underline declaration on a:hover and a:focus in
-       // commonElements.css, which the class alone isn't specific enough to do.
-       &:hover,
-       &:focus {
-               text-decoration: none;
-       }
-}
-
-// Button groups
-//
-// Group of buttons. Make sure you clear the floating after using a mw-ui-button-group.
-//
-// Markup:
-// <div class="mw-ui-button-group">
-//   <div class="mw-ui-button">A</div>
-//   <div class="mw-ui-button">B</div>
-//   <div class="mw-ui-button">C</div>
-//   <div class="mw-ui-button">D</div>
-// </div><div style="clear:both"></div>
-//
-// Styleguide 2.2.
-.mw-ui-button-group > * {
-       border-radius: 0;
-       float: left;
-
-       &:first-child {
-               border-top-left-radius: @buttonBorderRadius;
-               border-bottom-left-radius: @buttonBorderRadius;
-       }
-
-       &:not(:first-child) {
-               border-left: none;
-       }
-
-       &:last-child{
-               border-top-right-radius: @buttonBorderRadius;
-               border-bottom-right-radius: @buttonBorderRadius;
-       }
-}
diff --git a/resources/src/mediawiki.ui/components/default/forms.less b/resources/src/mediawiki.ui/components/default/forms.less
deleted file mode 100644 (file)
index 6c40c26..0000000
+++ /dev/null
@@ -1,183 +0,0 @@
-// Form elements and layouts
-
-@import "mediawiki.mixins";
-@import "../../mixins/utilities";
-@import "../../mixins/forms";
-
-// --------------------------------------------------------------------------
-// Layouts
-// --------------------------------------------------------------------------
-
-// The FancyCaptcha image CAPTCHA used on WMF wikis drives the width of the
-// 'VForm' design, the form can't be narrower than this.
-@captchaContainerWidth: 290px;
-@defaultFormWidth: @captchaContainerWidth;
-
-// Forms
-//
-// Styleguide 3.
-
-// VForm
-//
-// Style a compact vertical stacked form ("VForm") and the elements in divs
-// within it. See button section on guidance of how and when to use mw-ui-constructive.
-//
-// Markup:
-// <form class="mw-ui-vform">
-//   <div class="mw-ui-vform-field">This is a form example.</div>
-//   <div class="mw-ui-vform-field">
-//     <label>Username </label>
-//     <input value="input">
-//   </div>
-//   <div class="mw-ui-vform-field">
-//     <button class="mw-ui-button mw-ui-constructive">Button in vform</button>
-//   </div>
-// </form>
-//
-// Styleguide 3.1.
-.mw-ui-vform {
-       .box-sizing(border-box);
-
-       width: @defaultFormWidth;
-
-       // MW currently doesn't use the type attribute everywhere on inputs.
-       input,
-       select,
-       .mw-ui-button {
-               display: block;
-               .box-sizing(border-box);
-               margin: 0;
-               width: 100%;
-       }
-
-       // We exclude buttons because they'll generally use mw-ui-button.
-       // Otherwise, we'll unintentionally override that.
-       input:not([type=button]):not([type=submit]):not([type=file]) {
-               .agora-field-styling(); // mixins/forms.less
-       }
-
-       // Give dropdown lists the same spacing as input fields for consistency.
-       // Values taken from .agora-field-styling() in mixins/form.less
-       select {
-               padding: 0.35em 0.5em 0.35em 0.5em;
-               vertical-align: middle;
-       }
-
-       label {
-               display: block;
-               .box-sizing(border-box);
-               .agora-label-styling();
-               width: auto;
-               margin: 0 0 0.2em;
-               padding: 0;
-       }
-
-       // Override input styling just for checkboxes and radio inputs.
-       input[type="checkbox"],
-       input[type="radio"] {
-               display: inline;
-               .box-sizing(content-box);
-               width: auto;
-       }
-
-
-       // Styles for information boxes
-       //
-       // Regular HTMLForm uses .error class, some special pages like
-       // SpecialUserlogin (login and create account) use .errorbox.
-       //
-       // Markup:
-       // <form class="mw-ui-vform">
-       //   <div class="errorbox">An error occurred</div>
-       //   <div class="warningbox">A warning to be noted</div>
-       //   <div class="successbox">Action successful!</div>
-       //   <div class="error">A different kind of error</div>
-       //   <div class="error">
-       //     <ul><li>There are problems with some of your input.</li></ul>
-       //   </div>
-       //   <div class="mw-ui-vform-field">
-       //     <input type="text" value="input" class="mw-ui-input">
-       //   </div>
-       //   <div class="mw-ui-vform-field">
-       //     <select>
-       //       <option value="1">Option 1</option>
-       //       <option value="2">Option 2</option>
-       //     </select>
-       //     <span class="error">The value you specified is not a valid option.</span>
-       //   </div>
-       //   <div class="mw-ui-vform-field">
-       //     <button class="mw-ui-button">Button in vform</button>
-       //   </div>
-       // </form>
-       //
-       // Styleguide 3.1.
-       .error,
-       .errorbox,
-       .warningbox,
-       .successbox {
-               .box-sizing(border-box);
-               font-size: 0.9em;
-               margin: 0 0 1em 0;
-               padding: 0.5em;
-               word-wrap: break-word;
-       }
-
-       // Colours taken from those for .errorbox in skins/common/shared.css
-       .error {
-               color: #cc0000;
-               border: 1px solid #fac5c5;
-               background-color: #fae3e3;
-               text-shadow: 0 1px #fae3e3;
-       }
-
-       // This specifies styling for individual field validation error messages.
-       // Show them below the fields to prevent line break glitches, and leave
-       // some space between the field and the error message box.
-       .mw-ui-vform-div .error, /* for backwards-compatibility, remove before 1.24 */
-       .mw-ui-vform-field .error {
-               display: block;
-               margin-top: 5px;
-       }
-
-}
-
-// --------------------------------------------------------------------------
-// Elements
-// --------------------------------------------------------------------------
-
-// A wrapper for a single form field: the <input> / <select> / <button> element,
-// help text, labels, associated error/warning/success messages, and so on.
-// Elements with this class are generated by HTMLFormField in core MediaWiki.
-//
-// (We use a broad definition of 'field' here: a purely textual information
-// block is also a "field".)
-.mw-ui-vform-div, /* for backwards-compatibility, remove before 1.24 */
-.mw-ui-vform-field {
-       display: block;
-       margin: 0 0 15px;
-       padding: 0;
-       width: 100%;
-}
-
-// Apply mw-ui-input to individual input fields to style them.
-// You generally don't need to use this class if <input> is within an Agora
-// form container such as mw-ui-vform
-.mw-ui-input {
-       .agora-field-styling(); // mixins/forms.less
-}
-
-// Apply mw-ui-label to individual elements to style them.
-// You generally don't need to use this class if <label> is within an Agora
-// form container such as mw-ui-vform
-.mw-ui-label {
-       .agora-label-styling(); // mixins/forms.less
-}
-
-// Nesting an input checkbox or radio button inside a label with this class
-// improves alignment, e.g.
-//     <label class="mw-ui-checkbox-label">
-//             <input type="checkbox">The label text
-//     </label>
-.mw-ui-checkbox-label, .mw-ui-radio-label {
-       .agora-inline-label-styling();
-}
diff --git a/resources/src/mediawiki.ui/components/forms.less b/resources/src/mediawiki.ui/components/forms.less
new file mode 100644 (file)
index 0000000..2e586a6
--- /dev/null
@@ -0,0 +1,190 @@
+// Form elements and layouts
+
+@import "mediawiki.mixins";
+@import "mediawiki.ui/variables";
+@import "mediawiki.ui/mixins";
+
+// --------------------------------------------------------------------------
+// Layouts
+// --------------------------------------------------------------------------
+
+// The FancyCaptcha image CAPTCHA used on WMF wikis drives the width of the
+// 'VForm' design, the form can't be narrower than this.
+@captchaContainerWidth: 290px;
+@defaultFormWidth: @captchaContainerWidth;
+
+// Forms
+//
+// Styleguide 3.
+
+// VForm
+//
+// Style a compact vertical stacked form ("VForm") and the elements in divs
+// within it. See button section on guidance of how and when to use mw-ui-constructive.
+//
+// Markup:
+// <form class="mw-ui-vform">
+//   <div class="mw-ui-vform-field">This is a form example.</div>
+//   <div class="mw-ui-vform-field">
+//     <label>Username </label>
+//     <input value="input">
+//   </div>
+//   <div class="mw-ui-vform-field">
+//     <button class="mw-ui-button mw-ui-constructive">Button in vform</button>
+//   </div>
+// </form>
+//
+// Styleguide 3.1.
+.mw-ui-vform {
+       .box-sizing(border-box);
+
+       width: @defaultFormWidth;
+
+       // MW currently doesn't use the type attribute everywhere on inputs.
+       input,
+       select,
+       .mw-ui-button {
+               display: block;
+               .box-sizing(border-box);
+               margin: 0;
+               width: 100%;
+       }
+
+       // We exclude buttons because they'll generally use mw-ui-button.
+       // Otherwise, we'll unintentionally override that.
+       input:not([type=button]):not([type=submit]):not([type=file]) {
+               .agora-field-styling(); // mixins/forms.less
+       }
+
+       // Give dropdown lists the same spacing as input fields for consistency.
+       // Values taken from .agora-field-styling() in mixins/form.less
+       select {
+               padding: 0.35em 0.5em 0.35em 0.5em;
+               vertical-align: middle;
+       }
+
+       label {
+               display: block;
+               .box-sizing(border-box);
+               .agora-label-styling();
+               width: auto;
+               margin: 0 0 0.2em;
+               padding: 0;
+       }
+
+       // Override input styling just for checkboxes and radio inputs.
+       input[type="checkbox"],
+       input[type="radio"] {
+               display: inline;
+               .box-sizing(content-box);
+               width: auto;
+       }
+
+
+       // Styles for information boxes
+       //
+       // Regular HTMLForm uses .error class, some special pages like
+       // SpecialUserlogin (login and create account) use .errorbox.
+       //
+       // Markup:
+       // <form class="mw-ui-vform">
+       //   <div class="errorbox">An error occurred</div>
+       //   <div class="warningbox">A warning to be noted</div>
+       //   <div class="successbox">Action successful!</div>
+       //   <div class="error">A different kind of error</div>
+       //   <div class="error">
+       //     <ul><li>There are problems with some of your input.</li></ul>
+       //   </div>
+       //   <div class="mw-ui-vform-field">
+       //     <input type="text" value="input" class="mw-ui-input">
+       //   </div>
+       //   <div class="mw-ui-vform-field">
+       //     <select>
+       //       <option value="1">Option 1</option>
+       //       <option value="2">Option 2</option>
+       //     </select>
+       //     <span class="error">The value you specified is not a valid option.</span>
+       //   </div>
+       //   <div class="mw-ui-vform-field">
+       //     <button class="mw-ui-button">Button in vform</button>
+       //   </div>
+       // </form>
+       //
+       // Styleguide 3.1.
+       .error,
+       .errorbox,
+       .warningbox,
+       .successbox {
+               .box-sizing(border-box);
+               font-size: 0.9em;
+               margin: 0 0 1em 0;
+               padding: 0.5em;
+               word-wrap: break-word;
+       }
+
+       // Colours taken from those for .errorbox in skins/common/shared.css
+       .error {
+               color: #cc0000;
+               border: 1px solid #fac5c5;
+               background-color: #fae3e3;
+               text-shadow: 0 1px #fae3e3;
+       }
+
+       // This specifies styling for individual field validation error messages.
+       // Show them below the fields to prevent line break glitches, and leave
+       // some space between the field and the error message box.
+       .mw-ui-vform-div .error, /* for backwards-compatibility, remove before 1.24 */
+       .mw-ui-vform-field .error {
+               display: block;
+               margin-top: 5px;
+       }
+
+}
+
+// --------------------------------------------------------------------------
+// Elements
+// --------------------------------------------------------------------------
+
+// A wrapper for a single form field: the <input> / <select> / <button> element,
+// help text, labels, associated error/warning/success messages, and so on.
+// Elements with this class are generated by HTMLFormField in core MediaWiki.
+//
+// (We use a broad definition of 'field' here: a purely textual information
+// block is also a "field".)
+.mw-ui-vform-div, /* for backwards-compatibility, remove before 1.24 */
+.mw-ui-vform-field {
+       display: block;
+       margin: 0 0 15px;
+       padding: 0;
+       width: 100%;
+
+       input {
+               font-size: 1em;
+               line-height: 1.4;
+       }
+}
+
+// Apply mw-ui-input to individual input fields to style them.
+// You generally don't need to use this class if <input> is within an Agora
+// form container such as mw-ui-vform
+.mw-ui-input {
+       .agora-field-styling(); // mixins/forms.less
+       font-size: 1em;
+       line-height: 1.4em;
+}
+
+// Apply mw-ui-label to individual elements to style them.
+// You generally don't need to use this class if <label> is within an Agora
+// form container such as mw-ui-vform
+.mw-ui-label {
+       .agora-label-styling(); // mixins/forms.less
+}
+
+// Nesting an input checkbox or radio button inside a label with this class
+// improves alignment, e.g.
+//     <label class="mw-ui-checkbox-label">
+//             <input type="checkbox">The label text
+//     </label>
+.mw-ui-checkbox-label, .mw-ui-radio-label {
+       .agora-inline-label-styling();
+}
index 9aea429..ccfb677 100644 (file)
@@ -1,19 +1,55 @@
-// Generic helper classes that could be used in many elements/layouts
-
-// --------------------------------------------------------------------------
-// Positioning
-// --------------------------------------------------------------------------
-
-@import "../mixins/utilities";
+// Utilities
+//
+// Other things which effect the behaviour of components
+//
+// Styleguide 4.
 
+// Flush left
+//
+// Used when you want to push an element to the left of its containing element
+//
+// Markup:
+// <div class="mw-ui-vform-field">
+//   <label>Username <a href="#" class="mw-ui-flush-left">?</a></label>
+//   <input>
+// </div>
+//
+// Styleguide 4.1.
 .mw-ui-flush-left {
-       .agora-flush-left();
+       float: left;
+       margin-left: 0;
+       padding-left: 0;
 }
 
+// Flush right
+//
+// Used when you want to push an element to the right of its containing element
+//
+// Markup:
+// <div class="mw-ui-vform-field">
+//   <label>Username <a href="#" class="mw-ui-flush-right">?</a></label>
+//   <input>
+// </div>
+//
+// Styleguide 4.2.
 .mw-ui-flush-right {
-       .agora-flush-right();
+       float: right;
+       padding-right: 0;
+       margin-right: 0;
 }
 
+// Center block
+//
+// Centers the element in its containing element
+//
+// Markup:
+// <div>
+//   <button class="mw-ui-center-block">click me</button>
+// </div>
+//
+// Styleguide 4.3.
 .mw-ui-center-block {
-       .agora-center-block();
+       display: block;
+       margin-left: auto;
+       margin-right: auto;
 }
diff --git a/resources/src/mediawiki.ui/components/vector/buttons.less b/resources/src/mediawiki.ui/components/vector/buttons.less
deleted file mode 100644 (file)
index 1536338..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-@import "../default/buttons"; // Layer Vector on top of the default settings.
-@import "../../mixins/type";
-
-.mw-ui-button {
-       .vector-type();
-}
diff --git a/resources/src/mediawiki.ui/components/vector/containers.less b/resources/src/mediawiki.ui/components/vector/containers.less
deleted file mode 100644 (file)
index 1e9ec05..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-// No default settings for containers yet.
-@import "../../mixins/type";
-
-.mw-ui-container {
-       .vector-type();
-}
diff --git a/resources/src/mediawiki.ui/components/vector/forms.less b/resources/src/mediawiki.ui/components/vector/forms.less
deleted file mode 100644 (file)
index 2bbd8f0..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-@import "../default/forms"; // Layer Vector on top of the default settings.
-@import "../../mixins/type";
-
-.mw-ui-vform,
-.mw-ui-vform input,
-.mw-ui-input {
-       .vector-type();
-}
index e576937..d21b814 100644 (file)
@@ -1,7 +1,6 @@
 /**
- * Provide Agora appearance for mw-ui-* classes when using a skin other than
- * Vector.
+ * Provide Agora appearance for mw-ui-* classes.
  */
 
+@import "components/forms";
 @import "components/utilities";
-@import "components/default/forms";
diff --git a/resources/src/mediawiki.ui/mixins/effects.less b/resources/src/mediawiki.ui/mixins/effects.less
deleted file mode 100644 (file)
index 9759f63..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-@import "../settings/colors";
-// ----------------------------------------------------------------------------
-// Button styling
-// ----------------------------------------------------------------------------
-
-.button-colors(@bgColor) {
-       background: @bgColor;
-
-       &:hover,
-       &:focus {
-               // The inner bottom bevel should match the active background color.
-               box-shadow: 0 1px rgba(0, 0, 0, 10%), inset 0 -3px rgba(0, 0, 0, 20%);
-               border-bottom-color: mix(#000, @bgColor, 20%);
-               outline: none;
-               // remove outline in Firefox
-               &::-moz-focus-inner {
-                       border-color: transparent;
-               }
-       }
-
-       &:active,
-       &.mw-ui-checked {
-               // lessphp doesn't implement shade (https://github.com/leafo/lessphp/issues/528);
-               // it passes it through, then ResourceLoader drops it.
-               // background: shade(@bgColor, 20%);
-               background: mix(#000, @bgColor, 20%);
-               box-shadow: none;
-       }
-}
-
-.button-colors(@bgColor) when (lightness(@bgColor) >= 70%) {
-       color: @colorGrayDark;
-       border: 1px solid @colorGrayLight;
-
-       &:disabled {
-               color: @colorGrayLight;
-
-               // make sure disabled buttons don't have hover and active states
-               &:hover,
-               &:active {
-                       background: @bgColor;
-                       box-shadow: none;
-               }
-       }
-}
-
-.button-colors(@bgColor) when (lightness(@bgColor) < 70%) {
-       color: @colorWhite;
-       // border of the same color as background so that light background and
-       // dark background buttons are the same height (only top and bottom to
-       // make box shadow on hover cover the corners too)
-       border: 1px solid @bgColor;
-       border-left: none;
-       border-right: none;
-       text-shadow: 0 1px rgba(0, 0, 0, .1);
-
-       &:disabled {
-               background: @colorGrayLight;
-               border-color: @colorGrayLight;
-
-               // make sure disabled buttons don't have hover and active states
-               &:hover,
-               &:active,
-               &.mw-ui-checked {
-                       box-shadow: none;
-               }
-       }
-}
-
-.button-colors-quiet(@textColor) {
-       // Quiet buttons all start gray, and reveal
-       // constructive/progressive/destructive color on hover and active.
-       color: @colorGrayDark;
-
-       &:hover,
-       &:focus {
-               // lessphp doesn't implement tint, see above
-               // color: tint(@textColor, 20%);
-               color: mix(#fff, @textColor, 20%);
-       }
-
-       &:active,
-       &.mw-ui-checked {
-               // lessphp doesn't implement shade, see above
-               // color: shade(@textColor, 20%);
-               color: mix(#000, @textColor, 20%);
-       }
-
-       &:disabled {
-               color: @colorGrayLight;
-       }
-}
diff --git a/resources/src/mediawiki.ui/mixins/forms.less b/resources/src/mediawiki.ui/mixins/forms.less
deleted file mode 100644 (file)
index 20f42a0..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-@import "../settings/colors";
-
-// Font is not included.
-// For Vector, that should be layered on top with vector-type
-.agora-field-styling() {
-
-       border: 1px solid @colorGrayLight;
-
-       &:focus {
-               // Styling focus of native checkboxes etc on Mac is almost impossible.
-               &:not([type=checkbox]):not([type=radio]) {
-                       outline: 0; // Removes OS field focus
-               }
-
-               box-shadow: @colorProgressiveShadow 0 0 5px;
-
-               border-color: @colorProgressiveShadow;
-       }
-
-       color: @colorText;
-       padding: 0.35em 0.5em 0.35em 0.5em;
-
-       // Ensure that buttons and inputs are nicely aligned when they have differing heights
-       vertical-align: middle;
-}
-
-.agora-label-styling() {
-       //font-weight: bold;
-       font-size: 0.9em;
-       color: darken(@colorGrayLight, 50%);
-
-       * {
-               font-weight: normal;
-       }
-}
-
-.agora-inline-label-styling() {
-       margin-bottom: 0.5em;
-       cursor: pointer;
-       vertical-align: bottom;
-       line-height: normal;
-
-       font-weight: normal;
-
-       & > input[type="checkbox"],
-       & > input[type="radio"] {
-               width: auto;
-               height: auto;
-               margin: 0 0.1em 0 0;
-               padding: 0;
-               border: 1px solid @colorGrayLight;
-               cursor: pointer;
-       }
-}
diff --git a/resources/src/mediawiki.ui/mixins/type.less b/resources/src/mediawiki.ui/mixins/type.less
deleted file mode 100644 (file)
index 4a01168..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-@import "../settings/typography";
-
-.vector-type() {
-       font-size: @baseFontSize;
-       line-height: @baseLineHeight;
-}
diff --git a/resources/src/mediawiki.ui/mixins/utilities.less b/resources/src/mediawiki.ui/mixins/utilities.less
deleted file mode 100644 (file)
index 3d7b732..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-.agora-flush-left() {
-       float: left;
-       margin-left: 0;
-       padding-left: 0;
-}
-
-.agora-flush-right() {
-       float: right;
-       margin-right: 0;
-       padding-right: 0;
-}
-
-.agora-center-block() {
-       display: block;
-       margin-left: auto;
-       margin-right: auto;
-}
diff --git a/resources/src/mediawiki.ui/settings/colors.less b/resources/src/mediawiki.ui/settings/colors.less
deleted file mode 100644 (file)
index d456f86..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-// FIXME: remove @colorProgressiveShadow (shadows should be generated
-// in LESS by dimming the original colors)
-@colorProgressiveShadow: #4091ed;
-
-// White; for background use, and text use on dark backgrounds
-@colorWhite: #fff;
-// Off-white; for background use on white backgrounds
-@colorOffWhite: #fafafa;
-// Dark gray; for non-text use
-@colorGrayDark: #898989;
-// Light gray; for non-text use
-@colorGrayLight: #ccc;
-// Very light gray; for non-text use
-@colorGrayLighter: #ddd;
-// Lightest gray; for non-text use
-@colorGrayLightest: #eee;
-
-// Dark gray; for body text
-@colorText: #252525;
-// Light gray; for less important body text and links
-@colorTextLight: #696969;
-
-// Blue; for contextual use of a continuing action
-@colorProgressive: #347bff;
-// Orange; for contextual use of returning to a past action
-@colorRegressive: #ff7e1e;
-// Green; for contextual use of a positive finalizing action
-@colorConstructive: #00af89;
-// Red; for contextual use of a negative finalizing action
-@colorDestructive: #d11d13;
-
-// Used in mixins to darken contextual colors by the same amount (eg. focus)
-@colorDarkenPercentage: 13.5%;
-// Used in mixins to lighten contextual colors by the same amount (eg. hover)
-@colorLightenPercentage: 13.5%;
\ No newline at end of file
diff --git a/resources/src/mediawiki.ui/settings/typography.less b/resources/src/mediawiki.ui/settings/typography.less
deleted file mode 100644 (file)
index 83651ed..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-@baseFontSize: 1em;
-@baseLineHeight: 1.4 * @baseFontSize;
-@baseFontColor: @colorText;
-
-@smallFontSize: 0.75em;
diff --git a/resources/src/mediawiki.ui/vector.less b/resources/src/mediawiki.ui/vector.less
deleted file mode 100644 (file)
index 04e88e8..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Provide Agora appearance for mw-ui-* classes when using the Vector skin.
- */
-
-// Typography
-//
-// We prefer the usage of Georgia Bold for all headings. Georgia Regular is used to place emphasis on pull-out or short quotations. This latter usage should be used sparingly. 
-//
-// We prefer the use of Helvetica Neue Regular for body copy. Helvetiva Neue Bold for sub-headers. Pull-out quotes within the body copy should use Helvetica Neue Bold. Helvetica Neue is an not open-source font. Hence, below is a list of preferred alternate choices.
-//
-// Second choice: Helvetica
-//
-// Third choice: Arial
-//
-// Wiki content is often predominantly text; hence, visual hierarchy must be clear. Use these recommended type sizes to inform and establish information hierarchy and organization.
-//
-// Unless if you plan to put extra attention and manually adjust spacing, avoid justifying texts and paragraphs as they are harder to read and create unnecessary visual distractions. Along with centered text, they convey a formal and less friendly environment. 
-//
-// It will be important to talk about other languages, scripts and writing direction - with the same level as importance, not as an afterthought.
-//
-// Styleguide 1.
-
-@import "mediawiki.mixins";
-@import "components/utilities";
-@import "components/vector/forms";
-@import "components/vector/containers";
index 6533db1..88de7d8 100644 (file)
         * dialog box. Submitting that dialog box appends its contents to a
         * wiki page that you specify, as a new section.
         *
-        * Not compatible with LiquidThreads.
+        * This feature works with classic MediaWiki pages
+        * and is not compatible with LiquidThreads or Flow.
         *
-        * Minimal example in how to use it:
+        * Minimal usage example:
         *
         *     var feedback = new mw.Feedback();
         *     $( '#myButton' ).click( function () { feedback.launch(); } );
                                        )
                                );
 
-                               // undo some damage from dialog css
-                               this.$dialog.find( 'a' ).css( {
-                                       color: '#0645ad'
-                               } );
-
-                               this.$dialog.dialog( {
-                                       width: 500,
-                                       autoOpen: false,
-                                       title: mw.msg( this.dialogTitleMessageKey ),
-                                       modal: true,
-                                       buttons: fb.buttons
-                               } );
+                       this.$dialog.dialog( {
+                               width: 500,
+                               autoOpen: false,
+                               title: mw.msg( this.dialogTitleMessageKey ),
+                               modal: true,
+                               buttons: fb.buttons
+                       } );
 
                        this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get( 0 );
                        this.messageInput = this.$dialog.find( 'textarea.feedback-message' ).get( 0 );
-
                },
 
                /**
                displayBugs: function () {
                        var fb = this,
                                bugsButtons = {};
+
                        this.display( 'bugs' );
                        bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () {
                                window.open( fb.bugsLink, '_blank' );
                displayThanks: function () {
                        var fb = this,
                                closeButton = {};
+
                        this.display( 'thanks' );
                        closeButton[ mw.msg( 'feedback-close' ) ] = function () {
                                fb.$dialog.dialog( 'close' );
                displayForm: function ( contents ) {
                        var fb = this,
                                formButtons = {};
+
                        this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : '';
                        this.messageInput.value = ( contents && contents.message ) ? contents.message : '';
 
                displayError: function ( message ) {
                        var fb = this,
                                closeButton = {};
+
                        this.display( 'error' );
                        this.$dialog.find( '.feedback-error-msg' ).msg( message );
                        closeButton[ mw.msg( 'feedback-close' ) ] = function () {
 
                        this.displaySubmitting();
 
-                       this.api.newSection( this.title, subject, message ).done( ok ).fail( err );
+                       // Post the message, resolving redirects
+                       this.api.newSection(
+                               this.title,
+                               subject,
+                               message,
+                               { redirect: true }
+                       ).done( ok ).fail( err );
                },
 
                /**
                        this.$dialog.dialog( 'open' );
                        this.subjectInput.focus();
                }
-
        };
-
 }( mediaWiki, jQuery ) );
index 12888bd..9061acd 100644 (file)
                         * Format:
                         *     {
                         *         'moduleName': {
+                        *             // At registry
                         *             'version': ############## (unix timestamp),
                         *             'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
                         *             'group': 'somegroup', (or) null,
                         *             'source': 'local', 'someforeignwiki', (or) null
                         *             'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
+                        *             'skip': 'return !!window.Example', (or) null
+                        *
+                        *             // Added during implementation
                         *             'script': ...,
                         *             'style': ...,
                         *             'messages': { 'key': 'value' },
                                /**
                                 * Execute a function as soon as one or more required modules are ready.
                                 *
+                                * If the required modules are already loaded, the function will be
+                                * executed immediately and the modules will not be reloaded.
+                                *
                                 * Example of inline dependency on OOjs:
                                 *
                                 *     mw.loader.using( 'oojs', function () {
index a3a20e5..684d3d3 100644 (file)
@@ -1,10 +1,12 @@
 {
        "@metadata": {
                "authors": [
-                       "Yury Tarasievich"
+                       "Yury Tarasievich",
+                       "Mikalai Udodau"
                ]
        },
        "skinname-monobook": "Манабук",
+       "monobook-desc": "Класічная вокладка MediaWiki з 2004 года, названая ў гонар чорна-белай фатаграфіі кнігі ў фоне старонкі",
        "monobook.css": "/* CSS, упісаны сюды, будзе дзейнічаць на браўзер кожнага чытача з актыўнай світай Monobook */",
        "monobook.js": "/* Any JavaScript here will be loaded for users using the MonoBook skin */"
 }
index 8674693..ba907dd 100644 (file)
@@ -1,8 +1,10 @@
 {
        "@metadata": {
                "authors": [
-                       "Martorell"
+                       "Martorell",
+                       "Toniher"
                ]
        },
+       "monobook-desc": "El tema clàssic de MediaWiki des del 2004, que rep el nom d'una foto en blanc i negre d'un llibre en el fons de la pàgina.",
        "monobook.css": "/* Editeu aquest fitxer per personalitzar l'aparença del monobook per a tot el lloc sencer */"
 }
index 2e61ad3..0e53e4a 100644 (file)
@@ -6,6 +6,7 @@
                ]
        },
        "skinname-monobook": "MonoBook",
+       "monobook-desc": "A aparencia clásica de MediaWiki desde 2004; recibe o seu nome pola foto en branco e negro dun libro que aparece no fondo das páxinas",
        "monobook.css": "/* O CSS que se coloque aquí afectará a quen use a aparencia Monobook */",
        "monobook.js": "/* Calquera JavaScript que haxa aquí será cargado para os usuarios que usen a aparencia MonoBook */"
 }
index 83ce352..d87d663 100644 (file)
@@ -2,10 +2,12 @@
        "@metadata": {
                "authors": [
                        "Iwan Novirion",
-                       "Rex"
+                       "Rex",
+                       "Arifin.wijaya"
                ]
        },
        "skinname-monobook": "MonoBook",
+       "monobook-desc": "Kulit MediaWiki klasik sejak tahun 2004, dinamai foto hitam-putih dari buku di latar belakang halaman",
        "monobook.css": "/* CSS yang ada di sini akan diterapkan pada kulit Monobook. */",
        "monobook.js": "/* Semua JavaScript di sini akan dimuatkan untuk para pengguna yang menggunakan kulit MonoBook */"
 }
index 0425b83..df34886 100644 (file)
@@ -2,11 +2,12 @@
        "@metadata": {
                "authors": [
                        "Hamilton Abreu",
-                       "Fúlvio"
+                       "Fúlvio",
+                       "Vitorvicentevalente"
                ]
        },
        "skinname-monobook": "MonoBook",
-       "monobook-desc": "A clássica skin do MediaWiki desde 2004, tendo este nome devido a uma imagem em preto-e-branco de um livro no plano de fundo da página",
+       "monobook-desc": "O tema clássico do MediaWiki desde 2004, tendo este nome sido atribuído devido a uma imagem a preto-e-branco de um livro no plano de fundo da página",
        "monobook.css": "/* Código CSS colocado aqui afectará os utilizadores do tema Monobook */",
        "monobook.js": "/* Código Javascript colocado aqui será carregado para utilizadores do tema Monobook */"
 }
index 7813cc1..e6a6a78 100644 (file)
@@ -4,6 +4,7 @@
                        "Cwlin0416"
                ]
        },
+       "monobook-desc": "MediaWiki 自 2004 年以來的經典外觀,根據頁面背景的書本黑白照命名",
        "monobook.css": "/* 此 CSS 會影響使用 Monobook 介面外觀的使用者 */",
        "monobook.js": "/* 此 JavaScript 會用於使用 Monobook 介面外觀使用者 */"
 }
index f70c7a4..33cd230 100644 (file)
@@ -127,8 +127,6 @@ pre, .mw-code {
        vertical-align: text-bottom;
 }
 
-/* TODO: Remove #bodyContent selector (kept for backwards compatibility with cached html) */
-#bodyContent,
 .mw-body-content {
        position: relative;
        line-height: @content-line-height;
index 05a1e61..5bb6f1a 100644 (file)
@@ -1,5 +1,8 @@
 /* mediawiki.notification */
-.skin-vector {
+
+// This wrapper class is needed to ensure these rules have larger CSS
+// selector specificity than default styles
+.mediawiki {
        .mw-notification-area {
                font-size: 0.8em;
        }
index b84c18f..5dbc57c 100644 (file)
@@ -7,6 +7,7 @@
                        "Хомелка"
                ]
        },
+       "vector-skin-desc": "Сучасная версія вокладкі Манабук, з абноўленым відам і шматлікімі зручнымі паляпшэннямі",
        "vector-action-addsection": "Дадаць тэму",
        "vector-action-delete": "Сцерці",
        "vector-action-move": "Перанесці",
index a371374..31d5173 100644 (file)
@@ -6,9 +6,11 @@
                        "Calak",
                        "Paucabot",
                        "Ssola",
-                       "Vriullop"
+                       "Vriullop",
+                       "Toniher"
                ]
        },
+       "vector-skin-desc": "Versió moderna del MonoBook amb un nou aspesctes i moltes millores en la usabilitat",
        "vector-action-addsection": "Nova secció",
        "vector-action-delete": "Esborra",
        "vector-action-move": "Reanomena",
index de19c39..455b9bd 100644 (file)
@@ -6,6 +6,7 @@
                        "Vivaelcelta"
                ]
        },
+       "vector-skin-desc": "Versión moderna da aparencia MonoBook, cun aspecto fresco e moitas melloras na usabilidade",
        "vector.css": "/* O CSS que se coloque aquí afectará a quen use a aparencia Vector */",
        "vector.js": "/* Calquera JavaScript que haxa aquí será cargado para os usuarios que usen a aparencia Vector */",
        "vector-action-addsection": "Nova sección",
index 3116632..ef439a3 100644 (file)
@@ -9,6 +9,7 @@
                ]
        },
        "skinname-vector": "Vektor",
+       "vector-skin-desc": "Versi modern dari MonoBook dengan tampilan segar dan banyak perbaikan kegunaan",
        "vector.css": "/* CSS nan ado di siko diterapkan pado kulik Vektor. */",
        "vector.js": "/* Semua JavaScript di sini akan dimuatkan untuk para pengguna yang menggunakan kulit Vector */",
        "vector-action-addsection": "Bagian baru",
index 8f6f09a..2f756b5 100644 (file)
@@ -5,7 +5,7 @@
                ]
        },
        "vector-action-addsection": "موضوع اضاف بكيد",
-       "vector-action-delete": "حذف بكيد",
+       "vector-action-delete": "پاکسا کردن",
        "vector-action-move": "جاوه جا بوئيت",
        "vector-action-protect": "حمايت بكيد",
        "vector-action-undelete": "حذف نبيئني",
index bae61b5..0ef82e0 100644 (file)
@@ -6,6 +6,7 @@
                        "Nghtwlkr"
                ]
        },
+       "vector-skin-desc": "Moderne versjon av MonoBook med et friskt utseende og mange bruksforbedringer",
        "vector-action-addsection": "Nytt emne",
        "vector-action-delete": "Slett",
        "vector-action-move": "Flytt",
index 2bf1407..7bd2fd1 100644 (file)
@@ -27,5 +27,5 @@
        "vector-view-history": "Переглянути історію",
        "vector-view-view": "Читати",
        "vector-view-viewsource": "Переглянути код",
-       "vector-more-actions": "Ð\91Ñ\96лÑ\8cÑ\88е"
+       "vector-more-actions": "Ще"
 }
index 3e67c21..0036c34 100644 (file)
@@ -7,6 +7,7 @@
                        "Mark85296341"
                ]
        },
+       "vector-skin-desc": "現代版的 MonoBook,有著較新穎的外觀與許多使用性的改進",
        "vector.css": "/* 此 CSS 會影響使用 Vector 介面外觀的使用者 */",
        "vector.js": "/* 此 JavaScript 會用於使用 Vector 介面外觀使用者 */",
        "vector-action-addsection": "加入主題",
diff --git a/skins/common/images/tipsy-arrow.gif b/skins/common/images/tipsy-arrow.gif
deleted file mode 100644 (file)
index 9f1a15b..0000000
Binary files a/skins/common/images/tipsy-arrow.gif and /dev/null differ
index a5612e2..5a8335d 100644 (file)
@@ -372,12 +372,16 @@ input#wpSummary {
        display: none;
 }
 
-/* Convenience links to edit block, delete and protect reasons */
+/**
+ * Convenience links to edit block, delete and protect reasons
+ * and upload licenses
+ */
 p.mw-ipb-conveniencelinks,
 p.mw-protect-editreasons,
 p.mw-filedelete-editreasons,
 p.mw-delete-editreasons,
-p.mw-revdel-editreasons {
+p.mw-revdel-editreasons,
+p.mw-upload-editlicenses {
        font-size: 90%;
        text-align: right;
 }
@@ -1128,37 +1132,6 @@ ol:lang(or) li {
        margin-left: 20px;
 }
 
-.tipsy {
-       padding: 5px 5px 10px;
-       font-size: 12px;
-       position: absolute;
-       z-index: 100000;
-       overflow: visible;
-}
-
-.tipsy-inner {
-       padding: 5px 8px 4px 8px;
-       background-color: #d6f3ff;
-       color: black;
-       border: 1px solid #5dc9f4;
-       max-width: 300px;
-       text-align: left;
-}
-
-.tipsy-arrow {
-       position: absolute;
-       /* @embed */
-       background: url(images/tipsy-arrow.gif) no-repeat top left;
-       width: 13px;
-       height: 13px;
-}
-
-.tipsy-se .tipsy-arrow {
-       bottom: -2px;
-       right: 10px;
-       background-position: 0% 100%;
-}
-
 #mw-clearyourcache,
 #mw-sitecsspreview,
 #mw-sitejspreview,
index e4d9324..4a3889d 100644 (file)
@@ -1087,7 +1087,7 @@ Non-html5 tags should be accepted
 </p>
 !! end
 
-## a,rtc not permitted
+## a not permitted
 ## i,b,br omitted
 !! test
 Text-level semantic html elements in wikitext
@@ -1109,7 +1109,7 @@ Text-level semantic html elements in wikitext
 <sub>text</sub>
 <u>text</u>
 <mark>text</mark>
-<ruby><rb>明日<rp>(</rp><rt>Ashita</rt><rp>)</rp></rb></ruby>
+<ruby><rb>明日</rb><rp>(</rp><rt>Ashita</rt><rp> </rp><rtc>あした</rtc><rp>)</rp></ruby>
 <bdi>text</bdi>
 <bdo>text</bdo>
 <span>text</span>
@@ -1132,7 +1132,7 @@ Text-level semantic html elements in wikitext
 <sub>text</sub>
 <u>text</u>
 <mark>text</mark>
-<ruby><rb>明日<rp>(</rp><rt>Ashita</rt><rp>)</rp></rb></ruby>
+<ruby><rb>明日</rb><rp>(</rp><rt>Ashita</rt><rp> </rp><rtc>あした</rtc><rp>)</rp></ruby>
 <bdi>text</bdi>
 <bdo>text</bdo>
 <span>text</span>
@@ -1140,6 +1140,45 @@ Text-level semantic html elements in wikitext
 </p>
 !! end
 
+# test cases taken from
+# http://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
+!! test
+Ruby markup (W3C-style)
+!! wikitext
+; Mono-ruby for individual base characters
+: <ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby>
+; Group ruby
+: <ruby>今日<rt>きょう</rt></ruby>
+; Jukugo ruby
+: <ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby>
+; Inline ruby
+: <ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby>
+; Double-sided ruby
+: <ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby>
+<ruby>
+<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc>
+<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc>
+<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc>
+</ruby>
+!! html
+<dl><dt> Mono-ruby for individual base characters</dt>
+<dd> <ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby></dd>
+<dt> Group ruby</dt>
+<dd> <ruby>今日<rt>きょう</rt></ruby></dd>
+<dt> Jukugo ruby</dt>
+<dd> <ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby></dd>
+<dt> Inline ruby</dt>
+<dd> <ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby></dd>
+<dt> Double-sided ruby</dt>
+<dd> <ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby></dd></dl>
+<p><ruby>
+<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc>
+<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc>
+<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc>
+</ruby>
+</p>
+!! end
+
 !! test
 Non-word characters don't terminate tag names (bug 17663, 40670, 52022)
 !! wikitext
@@ -2583,41 +2622,28 @@ File:foobar.jpg
 !!end
 
 !! test
-Leading pipes outside of tables
-!! options
-parsoid
-!! wikitext
-| foo
-!! html
-<p>| foo</p>
-!! end
-
-!! test
-Leading pipes outside of tables 2
-!! options
-parsoid
-!! wikitext
-a
-| foo
-b
-!! html
-<p>a
-| foo
-b</p>
-!! end
-
-!! test
-Leading pipes outside of tables 3
-!! options
-parsoid
+Table wikitext syntax outside wiki-tables
 !! wikitext
 a
+! not a table heading
+|- not a table row
+| not a table cell
 | class="foo bar" | baz
 b
+|}
+|-
+c
 !! html
 <p>a
+! not a table heading
+|- not a table row
+| not a table cell
 | class="foo bar" | baz
-b</p>
+b
+|}
+|-
+c
+</p>
 !! end
 
 !!test
@@ -6129,6 +6155,16 @@ Link with multiple ":" in a subpage-supporting namespace (bug 63636)
 <p><a rel="mw:WikiLink" href="./User:Foo/Test/63636:Bar">Test</a></p>
 !! end
 
+!! test
+Purely hash wikilink
+!! options
+title=[[User:test/123]]
+!! wikitext
+[[#a|b]]
+!! html/parsoid
+<p data-parsoid='{}'><a rel="mw:WikiLink" href="../User:Test/123#a" data-parsoid='{"stx":"piped","a":{"href":"../User:Test/123#a"},"sa":{"href":"#a"}}'>b</a></p>
+!! end
+
 !! test
 1. Interaction of linktrail and template encapsulation
 !! options
@@ -6366,9 +6402,12 @@ Interlanguage link
 !! wikitext
 Blah blah blah
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
@@ -6377,9 +6416,13 @@ Double interlanguage link
 Blah blah blah
 [[es:Spanish]]
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Spanish"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
@@ -6389,9 +6432,12 @@ language=ln
 !! wikitext
 Blah blah blah
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
@@ -6402,19 +6448,26 @@ language=ln
 Blah blah blah
 [[es:Spanish]]
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Spanish"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
 "Extra" interlanguage links (bug 32189 / gerrit 111390)
 !! wikitext
 Blah blah blah
-[[mul:Multilingual]]
-!! html
+[[mul:Article]]
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" title="Multilingual" href="//wikisource.org/wiki/Article"/></p>
 !! end
 
 !! test
@@ -8674,6 +8727,42 @@ Un-closed <includeonly>
 !! html
 !! end
 
+!! test
+Includes and comments at SOL
+!! wikitext
+<!-- comment --><noinclude><!-- comment --></noinclude><!-- comment -->== hu ==
+
+<noinclude>
+some
+</noinclude>* stuff
+* here
+
+<includeonly>can have stuff</includeonly>=== here ===
+
+!! html/php
+<h2><span class="mw-headline" id="hu">hu</span></h2>
+<p>some
+</p>
+<ul><li> stuff</li>
+<li> here</li></ul>
+<h3><span class="mw-headline" id="here">here</span></h3>
+
+!! html/parsoid
+<!-- comment --><meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"&lt;noinclude>"}'/><!-- comment --><meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"&lt;/noinclude>"}'/><!-- comment -->
+<h2 data-parsoid='{}'> hu </h2>
+
+<meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"&lt;noinclude>"}'/>
+
+<p data-parsoid='{}'>some</p>
+<meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"&lt;/noinclude>"}'/>
+<ul data-parsoid='{}'>
+<li data-parsoid='{}'> stuff</li>
+
+<li data-parsoid='{}'> here</li></ul>
+
+<h3 data-parsoid='{}'> here </h3>
+!! end
+
 # TODO: test with DOM fragment reuse!
 !! test
 Parsoid: DOM fragment reuse
@@ -8843,9 +8932,10 @@ parsoid=wt2html,wt2wt
 |c
 |}
 !!html/parsoid
-<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>a&lt;/includeonly>"}'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":"&lt;/includeonly>"}'/><span data-parsoid='{"fostered":true,"autoInsertedEnd":true,"autoInsertedStart":true}'>{{{b}}}</span><table about="#mwt1" typeof="mw:Param" data-parsoid='{"a":{" ":null},"sa":{" ":""},"src":"{| {{{b}}}\n|c\n|}"}'>
+<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>a&lt;/includeonly>"'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><table about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"{{{b}}}","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Param\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[31,38,null,null],&amp;quot;src&amp;quot;:&amp;quot;{{{b}}}&amp;quot;}\">{{{b}}}&lt;/span>"},{"html":""}]]}' data-parsoid='{"a":{"{{{b}}}":null},"sa":{"{{{b}}}":""}}'>
 <tbody><tr><td>c</td></tr>
 </tbody></table>
+
 !!end
 
 ###
@@ -11886,15 +11976,19 @@ parsoid
 ### Inter-language links
 ###
 !! test
-Inter-language links
+Interlanguage links
 !! options
 ill
 !! wikitext
 [[es:Alimento]]
 [[fr:Nourriture]]
-[[zh:&#39135;&#21697;]]
-!! html
+[[zh:食品]]
+!! html/php
 es:Alimento fr:Nourriture zh:食品
+!! html/parsoid
+<p><link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Alimento"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/Nourriture"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/食品"/></p>
 !! end
 
 !! test
@@ -11906,8 +12000,13 @@ ill
 [[es:2]]
 [[fr:1]]
 [[fr:2]]
-!! html
+!! html/php
 es:1 fr:1
+!! html/parsoid
+<p><link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/1"/>
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/2"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/1"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/2"/></p>
 !! end
 
 ###
@@ -18436,12 +18535,12 @@ B <ref group="X" name="b" />
 <ref name="b">foo</ref>
 </references>
 !! html
-<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo bar for a"},"attrs":{}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo bar for a&lt;/ref>"}'><a href="#cite_note-2" data-parsoid="{}">[2]</a></span>
-B <span about="#mwt4" class="reference" data-mw='{"name":"ref","attrs":{"group":"X","name":"b"}}' id="cite_ref-b-3-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref name=\"b\" group=\"X\" />"}'><a href="#cite_note-b-3" data-parsoid="{}">[X 1]</a></span></p>
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo bar for a"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo bar for a&lt;/ref>"}'><a href="#cite_note-1" data-parsoid="{}">[1]</a></span>
+B <span about="#mwt4" class="reference" data-mw='{"name":"ref","attrs":{"group":"X","name":"b"}}' id="cite_ref-b-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref group=\"X\" name=\"b\" />"}'><a href="#cite_note-b-2" data-parsoid="{}">[X 1]</a></span></p>
 
-<ol class="references" typeof="mw:Extension/references" about="#mwt6" data-parsoid='{"src":"&lt;references />"}' data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-b-1" id="cite_note-b-1" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}">↑</span> foo</li><li about="#cite_note-2" id="cite_note-2" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-2-0" data-parsoid="{}">↑</a></span> foo bar for a</li></ol>
+<ol class="references" typeof="mw:Extension/references" about="#mwt6" data-parsoid='{"src":"&lt;references />"}' data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-1-0" data-parsoid="{}">↑</a></span> foo bar for a</li></ol>
 
-<ol class="references" typeof="mw:Extension/references" about="#mwt8" data-parsoid='{"src":"&lt;references group=\"X\">\n&lt;ref name=\"b\">foo&lt;/ref>\n&lt;/references>","group":"X"}' data-mw='{"name":"references","body":{"extsrc":"&lt;ref name=\"b\">foo&lt;/ref>","html":"\n&lt;span about=\"#mwt10\" class=\"reference\" data-mw=&#39;{\"name\":\"ref\",\"body\":{\"html\":\"foo\"},\"attrs\":{\"name\":\"b\"}}&#39; rel=\"dc:references\" typeof=\"mw:Extension/ref\">&lt;a href=\"#cite_note-b-1\">[1]&lt;/a>&lt;/span>\n"},"attrs":{"group":"X"}}'><li about="#cite_note-b-3" id="cite_note-b-3" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-b-3-0" data-parsoid="{}">↑</a></span> </li></ol>
+<ol class="references" typeof="mw:Extension/references" about="#mwt8" data-parsoid='{"src":"&lt;references group=\"X\">\n&lt;ref name=\"b\">foo&lt;/ref>\n&lt;/references>","group":"X"}' data-mw='{"name":"references","body":{"extsrc":"&lt;ref name=\"b\">foo&lt;/ref>","html":"\n&lt;span about=\"#mwt10\" class=\"reference\" data-mw=&#39;{\"name\":\"ref\",\"body\":{\"html\":\"foo\"},\"attrs\":{\"name\":\"b\"}}&#39; rel=\"dc:references\" typeof=\"mw:Extension/ref\">&lt;a href=\"#cite_note-b-2\">[X 1]&lt;/a>&lt;/span>\n"},"attrs":{"group":"X"}}'><li about="#cite_note-b-2" id="cite_note-b-2" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-b-2-0" data-parsoid="{}">↑</a></span> foo</li></ol>
 !! end
 
 !! test
@@ -19582,6 +19681,16 @@ parsoid
  <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure>
 !! end
 
+!! test
+5. Nowiki escaping should account for indent-pres
+!! options
+parsoid=html2wt
+!! html
+<pre>==foo==</pre>
+!! wikitext
+ ==foo==
+!! end
+
 #### --------------- Behavior Switches --------------------
 !! test
 1. Valid behavior switches should be escaped
@@ -20374,6 +20483,17 @@ parsoid=html2wt
 # All these tests are marked Parsoid html2wt and html2html only
 # ----------------------------------------------------------------
 
+!! test
+Serialize interwiki links pointing to the current wiki as plain wiki links (bug 65869)
+!! options
+parsoid=html2wt
+language=es
+!! wikitext
+[[Foo]]
+!! html
+<p><a rel="mw:ExtLink" href="http://es.wikipedia.org/wiki/Foo">Foo</a></p>
+!! end
+
 !! test
 Image: Modifying size of an image (1)
 !! options
index c9184e8..3015895 100644 (file)
@@ -246,6 +246,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
                $this->mwGlobals = array();
                RequestContext::resetMain();
+               MediaHandler::resetCache();
 
                $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
 
index 7c684d5..e22f505 100644 (file)
@@ -6,7 +6,7 @@ class GitInfoTest extends MediaWikiTestCase {
 
        protected function setUp() {
                parent::setUp();
-               $this->setMwGlobals( 'wgCacheDirectory', __DIR__ . '/../data' );
+               $this->setMwGlobals( 'wgGitInfoCacheDirectory', __DIR__ . '/../data/gitinfo' );
        }
 
        public function testValidJsonData() {
index 06b512d..91b531d 100644 (file)
@@ -369,6 +369,8 @@ class GlobalTest extends MediaWikiTestCase {
         * @covers ::swap
         */
        public function testSwapVarsTest() {
+               $this->hideDeprecated( 'swap' );
+
                $var1 = 1;
                $var2 = 2;
 
index e934965..c561e70 100644 (file)
@@ -112,7 +112,8 @@ class HtmlTest extends MediaWikiTestCase {
                        Html::expandAttributes( array( 'foo' => false ) ),
                        'skip keys with false value'
                );
-               $this->assertNotEmpty(
+               $this->assertEquals(
+                       ' foo=""',
                        Html::expandAttributes( array( 'foo' => '' ) ),
                        'keep keys with an empty string'
                );
@@ -153,6 +154,33 @@ class HtmlTest extends MediaWikiTestCase {
                );
        }
 
+       /**
+        * @covers HTML::expandAttributes
+        */
+       public function testExpandAttributesForNumbers() {
+               $this->assertEquals(
+                       ' value=1',
+                       Html::expandAttributes( array( 'value' => 1 ) ),
+                       'Integer value is cast to a string'
+               );
+               $this->assertEquals(
+                       ' value=1.1',
+                       Html::expandAttributes( array( 'value' => 1.1 ) ),
+                       'Float value is cast to a string'
+               );
+       }
+
+       /**
+        * @covers HTML::expandAttributes
+        */
+       public function testExpandAttributesForObjects() {
+               $this->assertEquals(
+                       ' value=stringValue',
+                       Html::expandAttributes( array( 'value' => new HtmlTestValue() ) ),
+                       'Object value is converted to a string'
+               );
+       }
+
        /**
         * Test for Html::expandAttributes()
         * Please note it output a string prefixed with a space!
@@ -299,6 +327,21 @@ class HtmlTest extends MediaWikiTestCase {
                );
        }
 
+       /**
+        * @covers Html::expandAttributes
+        * @expectedException MWException
+        */
+       public function testExpandAttributes_ArrayOnNonListValueAttribute_ThrowsException() {
+               // Real-life test case found in the Popups extension (see Gerrit cf0fd64),
+               // when used with an outdated BetaFeatures extension (see Gerrit deda1e7)
+               Html::expandAttributes( array(
+                       'src' => array(
+                               'ltr' => 'ltr.svg',
+                               'rtl' => 'rtl.svg'
+                       )
+               ) );
+       }
+
        /**
         * @covers Html::namespaceSelector
         */
@@ -665,3 +708,9 @@ class HtmlTest extends MediaWikiTestCase {
                );
        }
 }
+
+class HtmlTestValue {
+       function __toString() {
+               return 'stringValue';
+       }
+}
index 542b3d6..5fee505 100644 (file)
@@ -175,13 +175,13 @@ mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"
                );
        }
 
-
        /**
         * @dataProvider provideMakeResourceLoaderLink
         * @covers OutputPage::makeResourceLoaderLink
         */
        public function testMakeResourceLoaderLink( $args, $expectedHtml) {
                $this->setMwGlobals( array(
+                       'wgResourceLoaderDebug' => false,
                        'wgResourceLoaderUseESI' => true,
                        'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
                        // Affects whether CDATA is inserted
@@ -195,6 +195,7 @@ mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"
                $method = $class->getMethod( 'makeResourceLoaderLink' );
                $method->setAccessible( true );
                $ctx = new RequestContext();
+               $ctx->setLanguage( 'en' );
                $out = new OutputPage( $ctx );
                $rl = $out->getResourceLoader();
                $rl->register( array(
index ac80a9a..988a4a4 100644 (file)
@@ -41,6 +41,9 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                                NS_MEDIAWIKI => 'editinterface',
                        ),
                ) );
+               // Without this testUserBlock will use a non-English context on non-English MediaWiki
+               // installations (because of how Title::checkUserBlock is implemented) and fail.
+               RequestContext::resetMain();
 
                $this->userName = 'Useruser';
                $this->altUserName = 'Altuseruser';
diff --git a/tests/phpunit/includes/filerepo/file/FileTest.php b/tests/phpunit/includes/filerepo/file/FileTest.php
new file mode 100644 (file)
index 0000000..9232ce4
--- /dev/null
@@ -0,0 +1,348 @@
+<?php
+
+class FileRepoFileTest extends MediaWikiMediaTestCase {
+       /**
+        * @dataProvider getThumbnailBucketProvider
+        * @covers File::getThumbnailBucket
+        */
+       public function testGetThumbnailBucket( $data ) {
+               $this->setMwGlobals( 'wgThumbnailBuckets', $data['buckets'] );
+               $this->setMwGlobals( 'wgThumbnailMinimumBucketDistance', $data['minimumBucketDistance'] );
+
+               $fileMock = $this->getMockBuilder( 'File' )
+                       ->setConstructorArgs( array( 'fileMock', false ) )
+                       ->setMethods( array( 'getWidth' ) )
+                       ->getMockForAbstractClass();
+
+               $fileMock->expects( $this->any() )->method( 'getWidth' )->will(
+                       $this->returnValue( $data['width'] ) );
+
+               $this->assertEquals(
+                       $data['expectedBucket'],
+                       $fileMock->getThumbnailBucket( $data['requestedWidth'] ),
+                       $data['message'] );
+       }
+
+       public function getThumbnailBucketProvider() {
+               $defaultBuckets = array( 256, 512, 1024, 2048, 4096 );
+
+               return array(
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'minimumBucketDistance' => 0,
+                               'width' => 3000,
+                               'requestedWidth' => 120,
+                               'expectedBucket' => 256,
+                               'message' => 'Picking bucket bigger than requested size'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'minimumBucketDistance' => 0,
+                               'width' => 3000,
+                               'requestedWidth' => 300,
+                               'expectedBucket' => 512,
+                               'message' => 'Picking bucket bigger than requested size'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'minimumBucketDistance' => 0,
+                               'width' => 3000,
+                               'requestedWidth' => 1024,
+                               'expectedBucket' => 2048,
+                               'message' => 'Picking bucket bigger than requested size'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'minimumBucketDistance' => 0,
+                               'width' => 3000,
+                               'requestedWidth' => 2048,
+                               'expectedBucket' => false,
+                               'message' => 'Picking no bucket because none is bigger than the requested size'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'minimumBucketDistance' => 0,
+                               'width' => 3000,
+                               'requestedWidth' => 3500,
+                               'expectedBucket' => false,
+                               'message' => 'Picking no bucket because requested size is bigger than original'
+                       ) ),
+                       array( array(
+                               'buckets' => array( 1024 ),
+                               'minimumBucketDistance' => 0,
+                               'width' => 3000,
+                               'requestedWidth' => 1024,
+                               'expectedBucket' => false,
+                               'message' => 'Picking no bucket because requested size equals biggest bucket'
+                       ) ),
+                       array( array(
+                               'buckets' => null,
+                               'minimumBucketDistance' => 0,
+                               'width' => 3000,
+                               'requestedWidth' => 1024,
+                               'expectedBucket' => false,
+                               'message' => 'Picking no bucket because no buckets have been specified'
+                       ) ),
+                       array( array(
+                               'buckets' => array( 256, 512 ),
+                               'minimumBucketDistance' => 10,
+                               'width' => 3000,
+                               'requestedWidth' => 245,
+                               'expectedBucket' => 256,
+                               'message' => 'Requested width is distant enough from next bucket for it to be picked'
+                       ) ),
+                       array( array(
+                               'buckets' => array( 256, 512 ),
+                               'minimumBucketDistance' => 10,
+                               'width' => 3000,
+                               'requestedWidth' => 246,
+                               'expectedBucket' => 512,
+                               'message' => 'Requested width is too close to next bucket, picking next one'
+                       ) ),
+               );
+       }
+
+       /**
+        * @dataProvider getThumbnailSourceProvider
+        * @covers File::getThumbnailSource
+        */
+       public function testGetThumbnailSource( $data ) {
+               $backendMock = $this->getMockBuilder( 'FSFileBackend' )
+                       ->setConstructorArgs( array( array( 'name' => 'backendMock', 'wikiId' => wfWikiId() ) ) )
+                       ->getMock();
+
+               $repoMock = $this->getMockBuilder( 'FileRepo' )
+                       ->setConstructorArgs( array( array( 'name' => 'repoMock', 'backend' => $backendMock ) ) )
+                       ->setMethods( array( 'fileExists', 'getLocalReference' ) )
+                       ->getMock();
+
+               $fsFile = new FSFile( 'fsFilePath' );
+
+               $repoMock->expects( $this->any() )->method( 'fileExists' )->will(
+                       $this->returnValue( true ) );
+
+               $repoMock->expects( $this->any() )->method( 'getLocalReference' )->will(
+                       $this->returnValue( $fsFile ) );
+
+               $handlerMock = $this->getMock( 'BitmapHandler', array( 'supportsBucketing' ) );
+               $handlerMock->expects( $this->any() )->method( 'supportsBucketing' )->will(
+                       $this->returnValue( $data['supportsBucketing'] ) );
+
+               $fileMock = $this->getMockBuilder( 'File' )
+                       ->setConstructorArgs( array( 'fileMock', $repoMock ) )
+                       ->setMethods( array( 'getThumbnailBucket', 'getLocalRefPath', 'getHandler' ) )
+                       ->getMockForAbstractClass();
+
+               $fileMock->expects( $this->any() )->method( 'getThumbnailBucket' )->will(
+                       $this->returnValue( $data['thumbnailBucket'] ) );
+
+               $fileMock->expects( $this->any() )->method( 'getLocalRefPath' )->will(
+                       $this->returnValue( 'localRefPath' ) );
+
+               $fileMock->expects( $this->any() )->method( 'getHandler' )->will(
+                       $this->returnValue( $handlerMock ) );
+
+               $reflection = new ReflectionClass( $fileMock );
+               $reflection_property = $reflection->getProperty( 'handler' );
+               $reflection_property->setAccessible( true );
+               $reflection_property->setValue( $fileMock, $handlerMock );
+
+               if ( !is_null( $data['tmpBucketedThumbCache'] ) ) {
+                       $reflection_property = $reflection->getProperty( 'tmpBucketedThumbCache' );
+                       $reflection_property->setAccessible( true );
+                       $reflection_property->setValue( $fileMock, $data['tmpBucketedThumbCache'] );
+               }
+
+               $result = $fileMock->getThumbnailSource(
+                       array( 'physicalWidth' => $data['physicalWidth'] ) );
+
+               $this->assertEquals( $data['expectedPath'], $result['path'], $data['message'] );
+       }
+
+       public function getThumbnailSourceProvider() {
+               return array(
+                       array( array(
+                               'supportsBucketing' => true,
+                               'tmpBucketedThumbCache' => null,
+                               'thumbnailBucket' => 1024,
+                               'physicalWidth' => 2048,
+                               'expectedPath' => 'fsFilePath',
+                               'message' => 'Path downloaded from storage'
+                       ) ),
+                       array( array(
+                               'supportsBucketing' => true,
+                               'tmpBucketedThumbCache' => array( 1024 => '/tmp/shouldnotexist' + rand() ),
+                               'thumbnailBucket' => 1024,
+                               'physicalWidth' => 2048,
+                               'expectedPath' => 'fsFilePath',
+                               'message' => 'Path downloaded from storage because temp file is missing'
+                       ) ),
+                       array( array(
+                               'supportsBucketing' => true,
+                               'tmpBucketedThumbCache' => array( 1024 => '/tmp' ),
+                               'thumbnailBucket' => 1024,
+                               'physicalWidth' => 2048,
+                               'expectedPath' => '/tmp',
+                               'message' => 'Temporary path because temp file was found'
+                       ) ),
+                       array( array(
+                               'supportsBucketing' => false,
+                               'tmpBucketedThumbCache' => null,
+                               'thumbnailBucket' => 1024,
+                               'physicalWidth' => 2048,
+                               'expectedPath' => 'localRefPath',
+                               'message' => 'Original file path because bucketing is unsupported by handler'
+                       ) ),
+                       array( array(
+                               'supportsBucketing' => true,
+                               'tmpBucketedThumbCache' => null,
+                               'thumbnailBucket' => false,
+                               'physicalWidth' => 2048,
+                               'expectedPath' => 'localRefPath',
+                               'message' => 'Original file path because no width provided'
+                       ) ),
+               );
+       }
+
+       /**
+        * @dataProvider generateBucketsIfNeededProvider
+        * @covers File::generateBucketsIfNeeded
+        */
+       public function testGenerateBucketsIfNeeded( $data ) {
+               $this->setMwGlobals( 'wgThumbnailBuckets', $data['buckets'] );
+
+               $backendMock = $this->getMockBuilder( 'FSFileBackend' )
+                       ->setConstructorArgs( array( array( 'name' => 'backendMock', 'wikiId' => wfWikiId() ) ) )
+                       ->getMock();
+
+               $repoMock = $this->getMockBuilder( 'FileRepo' )
+                       ->setConstructorArgs( array( array( 'name' => 'repoMock', 'backend' => $backendMock ) ) )
+                       ->setMethods( array( 'fileExists', 'getLocalReference' ) )
+                       ->getMock();
+
+               $fileMock = $this->getMockBuilder( 'File' )
+                       ->setConstructorArgs( array( 'fileMock', $repoMock ) )
+                       ->setMethods( array( 'getWidth', 'getBucketThumbPath', 'makeTransformTmpFile', 'generateAndSaveThumb', 'getHandler' ) )
+                       ->getMockForAbstractClass();
+
+               $handlerMock = $this->getMock( 'JpegHandler', array( 'supportsBucketing' ) );
+               $handlerMock->expects( $this->any() )->method( 'supportsBucketing' )->will(
+                       $this->returnValue( true ) );
+
+               $fileMock->expects( $this->any() )->method( 'getHandler' )->will(
+                       $this->returnValue( $handlerMock ) );
+
+               $reflectionMethod = new ReflectionMethod( 'File', 'generateBucketsIfNeeded' );
+               $reflectionMethod->setAccessible( true );
+
+               $fileMock->expects( $this->any() )
+                       ->method( 'getWidth' )
+                       ->will( $this->returnValue( $data['width'] ) );
+
+               $fileMock->expects( $data['expectedGetBucketThumbPathCalls'] )
+                       ->method( 'getBucketThumbPath' );
+
+               $repoMock->expects( $data['expectedFileExistsCalls'] )
+                       ->method( 'fileExists' )
+                       ->will( $this->returnValue( $data['fileExistsReturn'] ) );
+
+               $fileMock->expects( $data['expectedMakeTransformTmpFile'] )
+                       ->method( 'makeTransformTmpFile' )
+                       ->will( $this->returnValue( $data['makeTransformTmpFileReturn'] ) );
+
+               $fileMock->expects( $data['expectedGenerateAndSaveThumb'] )
+                       ->method( 'generateAndSaveThumb' )
+                       ->will( $this->returnValue( $data['generateAndSaveThumbReturn'] ) );
+
+               $this->assertEquals( $data['expectedResult'],
+                       $reflectionMethod->invoke(
+                               $fileMock,
+                               array(
+                                       'physicalWidth' => $data['physicalWidth'],
+                                       'physicalHeight' => $data['physicalHeight'] )
+                               ),
+                               $data['message'] );
+       }
+
+       public function generateBucketsIfNeededProvider() {
+               $defaultBuckets = array( 256, 512, 1024, 2048, 4096 );
+
+               return array(
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'width' => 256,
+                               'physicalWidth' => 256,
+                               'physicalHeight' => 100,
+                               'expectedGetBucketThumbPathCalls' => $this->never(),
+                               'expectedFileExistsCalls' => $this->never(),
+                               'fileExistsReturn' => null,
+                               'expectedMakeTransformTmpFile' => $this->never(),
+                               'makeTransformTmpFileReturn' => false,
+                               'expectedGenerateAndSaveThumb' => $this->never(),
+                               'generateAndSaveThumbReturn' => false,
+                               'expectedResult' => false,
+                               'message' => 'No bucket found, nothing to generate'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'width' => 5000,
+                               'physicalWidth' => 300,
+                               'physicalHeight' => 200,
+                               'expectedGetBucketThumbPathCalls' => $this->once(),
+                               'expectedFileExistsCalls' => $this->once(),
+                               'fileExistsReturn' => true,
+                               'expectedMakeTransformTmpFile' => $this->never(),
+                               'makeTransformTmpFileReturn' => false,
+                               'expectedGenerateAndSaveThumb' => $this->never(),
+                               'generateAndSaveThumbReturn' => false,
+                               'expectedResult' => false,
+                               'message' => 'File already exists, no reason to generate buckets'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'width' => 5000,
+                               'physicalWidth' => 300,
+                               'physicalHeight' => 200,
+                               'expectedGetBucketThumbPathCalls' => $this->once(),
+                               'expectedFileExistsCalls' => $this->once(),
+                               'fileExistsReturn' => false,
+                               'expectedMakeTransformTmpFile' => $this->once(),
+                               'makeTransformTmpFileReturn' => false,
+                               'expectedGenerateAndSaveThumb' => $this->never(),
+                               'generateAndSaveThumbReturn' => false,
+                               'expectedResult' => false,
+                               'message' => 'Cannot generate temp file for bucket'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'width' => 5000,
+                               'physicalWidth' => 300,
+                               'physicalHeight' => 200,
+                               'expectedGetBucketThumbPathCalls' => $this->once(),
+                               'expectedFileExistsCalls' => $this->once(),
+                               'fileExistsReturn' => false,
+                               'expectedMakeTransformTmpFile' => $this->once(),
+                               'makeTransformTmpFileReturn' => new TempFSFile( '/tmp/foo' ),
+                               'expectedGenerateAndSaveThumb' => $this->once(),
+                               'generateAndSaveThumbReturn' => false,
+                               'expectedResult' => false,
+                               'message' => 'Bucket image could not be generated'
+                       ) ),
+                       array( array(
+                               'buckets' => $defaultBuckets,
+                               'width' => 5000,
+                               'physicalWidth' => 300,
+                               'physicalHeight' => 200,
+                               'expectedGetBucketThumbPathCalls' => $this->once(),
+                               'expectedFileExistsCalls' => $this->once(),
+                               'fileExistsReturn' => false,
+                               'expectedMakeTransformTmpFile' => $this->once(),
+                               'makeTransformTmpFileReturn' => new TempFSFile( '/tmp/foo' ),
+                               'expectedGenerateAndSaveThumb' => $this->once(),
+                               'generateAndSaveThumbReturn' => new ThumbnailImage( false, 'bar', false, false ),
+                               'expectedResult' => true,
+                               'message' => 'Bucket image could not be generated'
+                       ) ),
+               );
+       }
+}
index b99ef86..334d5b5 100644 (file)
@@ -14,7 +14,7 @@ class MWMessagePackTest extends MediaWikiTestCase {
         * serialization function.
         */
        public function provider() {
-               return array(
+               $tests = array(
                        array( 'nil', null, 'c0' ),
                        array( 'bool', true, 'c3' ),
                        array( 'bool', false, 'c2' ),
@@ -25,16 +25,12 @@ class MWMessagePackTest extends MediaWikiTestCase {
                        array( 'uint 8', 128, 'cc80' ),
                        array( 'uint 16', 1000, 'cd03e8' ),
                        array( 'uint 32', 100000, 'ce000186a0' ),
-                       array( 'uint 64', 10000000000, 'cf00000002540be400' ),
                        array( 'negative fixnum', -1, 'ff' ),
                        array( 'negative fixnum', -2, 'fe' ),
                        array( 'int 8', -128, 'd080' ),
                        array( 'int 8', -35, 'd0dd' ),
                        array( 'int 16', -1000, 'd1fc18' ),
                        array( 'int 32', -100000, 'd2fffe7960' ),
-                       array( 'int 64', -10000000000, 'd3fffffffdabf41c00' ),
-                       array( 'int 64', -223372036854775807, 'd3fce66c50e2840001' ),
-                       array( 'int 64', -9223372036854775807, 'd38000000000000001' ),
                        array( 'double', 0.1, 'cb3fb999999999999a' ),
                        array( 'double', 1.1, 'cb3ff199999999999a' ),
                        array( 'double', 123.456, 'cb405edd2f1a9fbe77' ),
@@ -56,6 +52,15 @@ class MWMessagePackTest extends MediaWikiTestCase {
                                '82a36f6e6501a374776f02'
                        ),
                );
+
+               if ( PHP_INT_SIZE > 4 ) {
+                       $tests[] = array( 'uint 64', 10000000000, 'cf00000002540be400' );
+                       $tests[] = array( 'int 64', -10000000000, 'd3fffffffdabf41c00' );
+                       $tests[] = array( 'int 64', -223372036854775807, 'd3fce66c50e2840001' );
+                       $tests[] = array( 'int 64', -9223372036854775807, 'd38000000000000001' );
+               }
+
+               return $tests;
        }
 
        /**
@@ -65,6 +70,6 @@ class MWMessagePackTest extends MediaWikiTestCase {
         */
        public function testPack( $type, $value, $expected ) {
                $actual = bin2hex( MWMessagePack::pack( $value ) );
-               $this->assertEquals( $actual, $expected, $type );
+               $this->assertEquals( $expected, $actual, $type );
        }
 }
index 8402522..c720d7b 100644 (file)
@@ -1,4 +1,8 @@
 <?php
+
+/**
+ * @group Media
+ */
 class BitmapMetadataHandlerTest extends MediaWikiTestCase {
 
        protected function setUp() {
index 9395b66..1972c96 100644 (file)
@@ -1,5 +1,8 @@
 <?php
 
+/**
+ * @group Media
+ */
 class BitmapScalingTest extends MediaWikiTestCase {
 
        protected function setUp() {
index 76cefe5..d779207 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 /**
+ * @group Media
  * @covers DjVuHandler
  */
 class DjVuTest extends MediaWikiMediaTestCase {
@@ -34,8 +35,8 @@ class DjVuTest extends MediaWikiMediaTestCase {
        public function testInvalidFile() {
                $this->assertEquals(
                        'a:1:{s:5:"error";s:25:"Error extracting metadata";}',
-                       $this->handler->getMetadata( null, $this->filePath . '/README' ),
-                       'Getting Metadata for an inexistent file should returns false'
+                       $this->handler->getMetadata( null, $this->filePath . '/some-nonexistent-file' ),
+                       'Getting metadata for an inexistent file should return false'
                );
        }
 
index 44b2070..41330f4 100644 (file)
@@ -1,5 +1,8 @@
 <?php
 
+/**
+ * @group Media
+ */
 class ExifBitmapTest extends MediaWikiTestCase {
 
        /**
index 6a1e422..247e352 100644 (file)
@@ -2,6 +2,7 @@
 /**
  * Tests related to auto rotation.
  *
+ * @group Media
  * @group medium
  *
  * @todo covers tags
index 735663c..f3c05fb 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 /**
+ * @group Media
  * @covers Exif
  */
 class ExifTest extends MediaWikiTestCase {
index 7bc785e..4b8f213 100644 (file)
@@ -1,5 +1,8 @@
 <?php
 
+/**
+ * @group Media
+ */
 class FakeDimensionFile extends File {
        public $mustRender = false;
 
index daaefc0..002e2cb 100644 (file)
@@ -1,5 +1,8 @@
 <?php
 
+/**
+ * @group Media
+ */
 class FormatMetadataTest extends MediaWikiMediaTestCase {
 
        protected function setUp() {
index 3491112..6aecd8b 100644 (file)
@@ -1,4 +1,8 @@
 <?php
+
+/**
+ * @group Media
+ */
 class GIFMetadataExtractorTest extends MediaWikiTestCase {
 
        protected function setUp() {
index 17b2964..52a51cc 100644 (file)
@@ -1,4 +1,8 @@
 <?php
+
+/**
+ * @group Media
+ */
 class GIFHandlerTest extends MediaWikiMediaTestCase {
 
        /** @var GIFHandler */
index b556a75..06542cf 100644 (file)
@@ -1,5 +1,8 @@
 <?php
 
+/**
+ * @group Media
+ */
 class IPTCTest extends MediaWikiTestCase {
 
        /**
index b10f55c..80e03cc 100644 (file)
@@ -6,6 +6,7 @@
  * but it costs money). The implementation of it currently in MediaWiki is based
  * solely on reading the standard, without any real world test files.
  *
+ * @group Media
  * @covers JpegMetadataExtractor
  */
 class JpegMetadataExtractorTest extends MediaWikiTestCase {
index c856b1c..2436e7d 100644 (file)
@@ -1,5 +1,7 @@
 <?php
+
 /**
+ * @group Media
  * @covers JpegHandler
  */
 class JpegTest extends MediaWikiMediaTestCase {
index c28898b..d8cfcc4 100644 (file)
@@ -1,5 +1,8 @@
 <?php
 
+/**
+ * @group Media
+ */
 class MediaHandlerTest extends MediaWikiTestCase {
 
        /**
index 84deb1b..a9eaa9e 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 /**
+ * @group Media
  * @covers PNGMetadataExtractor
  */
 class PNGMetadataExtractorTest extends MediaWikiTestCase {
index 14e4d57..9a4826c 100644 (file)
@@ -1,4 +1,8 @@
 <?php
+
+/**
+ * @group Media
+ */
 class PNGHandlerTest extends MediaWikiMediaTestCase {
 
        /** @var PNGHandler */
index f06bd6f..ab33d1c 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 /**
+ * @group Media
  * @covers SVGMetadataExtractor
  */
 class SVGMetadataExtractorTest extends MediaWikiTestCase {
index e3bb05e..1361a92 100644 (file)
@@ -1,5 +1,8 @@
 <?php
 
+/**
+ * @group Media
+ */
 class SvgTest extends MediaWikiMediaTestCase {
 
        protected function setUp() {
index 26d7204..d114820 100644 (file)
@@ -1,4 +1,8 @@
 <?php
+
+/**
+ * @group Media
+ */
 class TiffTest extends MediaWikiTestCase {
 
        /** @var TiffHandler */
index ae4fa8b..7fc3275 100644 (file)
@@ -1,4 +1,8 @@
 <?php
+
+/**
+ * @group Media
+ */
 class XCFHandlerTest extends MediaWikiMediaTestCase {
 
        /** @var XCFHandler */
index 25ae1e6..798e492 100644 (file)
@@ -1,7 +1,8 @@
 <?php
 
 /**
- * @todo covers tags
+ * @group Media
+ * @covers XMPReader
  */
 class XMPTest extends MediaWikiTestCase {
 
index 96bf5e4..ebec8f6 100644 (file)
@@ -1,4 +1,8 @@
 <?php
+
+/**
+ * @group Media
+ */
 class XMPValidateTest extends MediaWikiTestCase {
 
        /**
index dbebeb7..1f1d750 100644 (file)
@@ -114,6 +114,9 @@ class SpecialSearchTest extends MediaWikiTestCase {
         * https://gerrit.wikimedia.org/r/4841
         */
        public function testSearchTermIsNotExpanded() {
+               $this->setMwGlobals( array(
+                       'wgSearchType' => null,
+               ) );
 
                # Initialize [[Special::Search]]
                $search = new SpecialSearch();
index 73d7ff9..358b0fe 100644 (file)
  */
 class MediaWikiPageLinkRendererTest extends MediaWikiTestCase {
 
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( array(
+                       'wgContLang' => Language::factory( 'en' ),
+               ) );
+       }
+
        /**
         * Returns a mock GenderCache that will return "female" always.
         *
diff --git a/tests/phpunit/maintenance/getSlaveServerTest.php b/tests/phpunit/maintenance/getSlaveServerTest.php
deleted file mode 100644 (file)
index 165dc55..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-require_once __DIR__ . "/../../../maintenance/getSlaveServer.php";
-
-/**
- * Tests for getSlaveServer
- *
- * @group Database
- * @covers GetSlaveServer
- */
-class GetSlaveServerTest extends MediaWikiTestCase {
-
-       /**
-        * Yields a regular expression that matches a good DB server name
-        *
-        * It matches IPs or hostnames, both optionally followed by a
-        * port specification
-        *
-        * @return string The regular expression
-        */
-       private function getServerRE() {
-               if ( $this->db->getType() === 'sqlite' ) {
-                       // for SQLite, only the empty string is a good server name
-                       return '';
-               }
-
-               $octet = '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])';
-               $ip = "(($octet\.){3}$octet)";
-
-               $label = '([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)';
-               $hostname = "($label(\.$label)*)";
-
-               return "($ip|$hostname)(:[0-9]{1,5})?";
-       }
-
-       function testPlain() {
-               $gss = new GetSlaveServer();
-               $gss->execute();
-
-               $this->expectOutputRegex( "/^" . self::getServerRE() . "\n$/D" );
-       }
-
-       function testXmlDumpsBackupUseCase() {
-               global $wgDBprefix;
-
-               global $argv;
-               $argv = array( null, "--globals" );
-
-               $gss = new GetSlaveServer();
-               $gss->loadParamsAndArgs();
-               $gss->execute();
-               $gss->globals();
-
-               // The main answer
-               $output = $this->getActualOutput();
-               $firstLineEndPos = strpos( $output, "\n" );
-               if ( $firstLineEndPos === false ) {
-                       $this->fail( "Could not find end of first line of output" );
-               }
-               $firstLine = substr( $output, 0, $firstLineEndPos );
-               $this->assertRegExp( "/^" . self::getServerRE() . "$/D",
-                       $firstLine, "DB Server" );
-
-               // xmldumps-backup relies on the wgDBprefix in the output.
-               $this->expectOutputRegex( "/^[[:space:]]*\[wgDBprefix\][[:space:]]*=> "
-                       . $wgDBprefix . "$/m" );
-       }
-}
index 12f147e..2bdc9c9 100644 (file)
@@ -74,13 +74,23 @@ class AutoLoaderTest extends MediaWikiTestCase {
                                )
                        /imx', $contents, $matches, PREG_SET_ORDER );
 
+                       $namespaceMatch = array();
+                       preg_match( '/
+                               ^ [\t ]*
+                                       namespace \s+
+                                               ([a-zA-Z0-9_]+(\\\\[a-zA-Z0-9_]+)*)
+                                       \s* ;
+                       /imx', $contents, $namespaceMatch );
+                       $fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : '';
+
                        $classesInFile = array();
                        $aliasesInFile = array();
 
                        foreach ( $matches as $match ) {
                                if ( !empty( $match['class'] ) ) {
-                                       $actual[$match['class']] = $file;
-                                       $classesInFile[$match['class']] = true;
+                                       $class = $fileNamespace . $match['class'];
+                                       $actual[$class] = $file;
+                                       $classesInFile[$class] = true;
                                } else {
                                        $aliasesInFile[$match['alias']] = $match['original'];
                                }
index b2b5322..34007ed 100644 (file)
@@ -8,14 +8,14 @@ return array(
 
        'test.sinonjs' => array(
                'scripts' => array(
-                       'resources/lib/sinonjs/sinon-1.9.0.js',
+                       'resources/lib/sinonjs/sinon-1.10.3.js',
                        // We want tests to work in IE, but can't include this as it
                        // will break the placeholders in Sinon because the hack it uses
                        // to hijack IE globals relies on running in the global scope
                        // and in ResourceLoader this won't be running in the global scope.
                        // Including it results (among other things) in sandboxed timers
                        // being broken due to Date inheritance being undefined.
-                       // 'resources/lib/sinonjs/sinon-ie-1.9.0.js',
+                       // 'resources/lib/sinonjs/sinon-ie-1.10.3.js',
                ),
                'targets' => array( 'desktop', 'mobile' ),
        ),
index 50e89da..2eda8f1 100644 (file)
                                        // As a convenience feature, automatically restore warnings if they're
                                        // still suppressed by the end of the test.
                                        restoreWarnings();
+
+                                       // Check for incomplete animations/requests/etc and throw
+                                       // error if there are any.
+                                       if ( $.timers && $.timers.length !== 0 ) {
+                                               // Test may need to use fake timers, wait for animations or
+                                               // call $.fx.stop().
+                                               throw new Error( 'Unfinished animations: ' + $.timers.length );
+                                       }
+                                       if ( $.active !== undefined && $.active !== 0 ) {
+                                               // Test may need to use fake XHR, wait for requests or
+                                               // call abort().
+                                               throw new Error( 'Unfinished AJAX requests: ' + $.active );
+                                       }
                                }
                        };
                };
index d2e7f6e..8c24f39 100644 (file)
@@ -318,7 +318,7 @@ class DbTestRecorder extends DbTestPreviewer {
                        array(
                                'tr_date' => $this->db->timestamp(),
                                'tr_mw_version' => $this->version,
-                               'tr_php_version' => phpversion(),
+                               'tr_php_version' => PHP_VERSION,
                                'tr_db_version' => $this->db->getServerVersion(),
                                'tr_uname' => php_uname()
                        ),
index c0042c2..89cc834 100644 (file)
--- a/thumb.php
+++ b/thumb.php
@@ -118,7 +118,7 @@ function wfStreamThumb( array $params ) {
        }
        if ( isset( $params['width'] ) && substr( $params['width'], -2 ) == 'px' ) {
                // strip the px (pixel) suffix, if found
-               $params['width'] = substr( $width, 0, strlen( $width ) - 2 );
+               $params['width'] = substr( $params['width'], 0, -2 );
        }
        if ( isset( $params['p'] ) ) {
                $params['page'] = $params['p'];