resources: Move more various single-file mediawiki.* modules to src/
authorTimo Tijhof <krinklemail@gmail.com>
Wed, 9 May 2018 17:40:57 +0000 (18:40 +0100)
committerTimo Tijhof <krinklemail@gmail.com>
Wed, 9 May 2018 19:14:25 +0000 (20:14 +0100)
* Reduce clutter in src/mediawiki/.
* Make these files and modules easier to discover and associate.

Follows-up I677edac3b5e, which only moved simple cases where no
related modules existed.

This commit also moves files for modules that have some related
multi-file modules. As well as files that previously did not
strictly have their path match directly to their module name.

For example:
- 'mediawiki.checkboxtoggle.css' to 'mediawiki.checkboxtoggle.styles.css',
  because its module name is 'mediawiki.checkboxtoggle.styles'.
- 'mediawiki/page/gallery-slideshow.js' to 'mediawiki.page.gallery.slideshow.js',
  because its module name uses a dot, not a dash.
- 'mediawiki/page/watch.js' to 'mediawiki.page.watch.ajax.js',
  because its module name also includes 'ajax'. This also makes it matches
  the way "mediawiki.page.patrol.ajax" files were already named.

Ideas for later:
- Consider merging 'mediawiki.ForeignApi' and 'mediawiki.ForeignApi.core.'.
- Consider merging 'mediawiki.page.ready' and 'mediawiki.page.startup'.

Bug: T193826
Change-Id: I9564b05df305b7d217c9a03b80ce92476279e5c8

63 files changed:
resources/Resources.php
resources/src/mediawiki.ForeignApi.core.js [new file with mode: 0644]
resources/src/mediawiki.ForeignStructuredUpload.js [new file with mode: 0644]
resources/src/mediawiki.ForeignUpload.js [new file with mode: 0644]
resources/src/mediawiki.Upload.Dialog.js [new file with mode: 0644]
resources/src/mediawiki.Upload.js [new file with mode: 0644]
resources/src/mediawiki.api.category.js [new file with mode: 0644]
resources/src/mediawiki.api.edit.js [new file with mode: 0644]
resources/src/mediawiki.api.js [new file with mode: 0644]
resources/src/mediawiki.api.login.js [new file with mode: 0644]
resources/src/mediawiki.api.messages.js [new file with mode: 0644]
resources/src/mediawiki.api.options.js [new file with mode: 0644]
resources/src/mediawiki.api.parse.js [new file with mode: 0644]
resources/src/mediawiki.api.rollback.js [new file with mode: 0644]
resources/src/mediawiki.api.upload.js [new file with mode: 0644]
resources/src/mediawiki.api.user.js [new file with mode: 0644]
resources/src/mediawiki.api.watch.js [new file with mode: 0644]
resources/src/mediawiki.apipretty.css [new file with mode: 0644]
resources/src/mediawiki.checkboxtoggle.js [new file with mode: 0644]
resources/src/mediawiki.checkboxtoggle.styles.css [new file with mode: 0644]
resources/src/mediawiki.notification.convertmessagebox.js [new file with mode: 0644]
resources/src/mediawiki.notification.convertmessagebox.styles.less [new file with mode: 0644]
resources/src/mediawiki.page.gallery.js [new file with mode: 0644]
resources/src/mediawiki.page.gallery.slideshow.js [new file with mode: 0644]
resources/src/mediawiki.page.image.pagination.js [new file with mode: 0644]
resources/src/mediawiki.page.patrol.ajax.js [new file with mode: 0644]
resources/src/mediawiki.page.ready.js [new file with mode: 0644]
resources/src/mediawiki.page.rollback.js [new file with mode: 0644]
resources/src/mediawiki.page.startup.js [new file with mode: 0644]
resources/src/mediawiki.page.watch.ajax.js [new file with mode: 0644]
resources/src/mediawiki.template.js [new file with mode: 0644]
resources/src/mediawiki.template.regexp.js [new file with mode: 0644]
resources/src/mediawiki/ForeignApi.js [deleted file]
resources/src/mediawiki/api.js [deleted file]
resources/src/mediawiki/api/category.js [deleted file]
resources/src/mediawiki/api/edit.js [deleted file]
resources/src/mediawiki/api/login.js [deleted file]
resources/src/mediawiki/api/messages.js [deleted file]
resources/src/mediawiki/api/options.js [deleted file]
resources/src/mediawiki/api/parse.js [deleted file]
resources/src/mediawiki/api/rollback.js [deleted file]
resources/src/mediawiki/api/upload.js [deleted file]
resources/src/mediawiki/api/user.js [deleted file]
resources/src/mediawiki/api/watch.js [deleted file]
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js [deleted file]
resources/src/mediawiki/mediawiki.ForeignUpload.js [deleted file]
resources/src/mediawiki/mediawiki.Upload.Dialog.js [deleted file]
resources/src/mediawiki/mediawiki.Upload.js [deleted file]
resources/src/mediawiki/mediawiki.apipretty.css [deleted file]
resources/src/mediawiki/mediawiki.checkboxtoggle.css [deleted file]
resources/src/mediawiki/mediawiki.checkboxtoggle.js [deleted file]
resources/src/mediawiki/mediawiki.notification.convertmessagebox.js [deleted file]
resources/src/mediawiki/mediawiki.notification.convertmessagebox.styles.less [deleted file]
resources/src/mediawiki/mediawiki.template.js [deleted file]
resources/src/mediawiki/mediawiki.template.regexp.js [deleted file]
resources/src/mediawiki/page/gallery-slideshow.js [deleted file]
resources/src/mediawiki/page/gallery.js [deleted file]
resources/src/mediawiki/page/image-pagination.js [deleted file]
resources/src/mediawiki/page/patrol.ajax.js [deleted file]
resources/src/mediawiki/page/ready.js [deleted file]
resources/src/mediawiki/page/rollback.js [deleted file]
resources/src/mediawiki/page/startup.js [deleted file]
resources/src/mediawiki/page/watch.js [deleted file]

index d41352e..4ecf89a 100644 (file)
@@ -869,7 +869,7 @@ return [
                'targets' => [ 'desktop' ],
        ],
        'mediawiki.template' => [
-               'scripts' => 'resources/src/mediawiki/mediawiki.template.js',
+               'scripts' => 'resources/src/mediawiki.template.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.template.mustache' => [
@@ -881,16 +881,16 @@ return [
                'dependencies' => 'mediawiki.template',
        ],
        'mediawiki.template.regexp' => [
-               'scripts' => 'resources/src/mediawiki/mediawiki.template.regexp.js',
+               'scripts' => 'resources/src/mediawiki.template.regexp.js',
                'targets' => [ 'desktop', 'mobile' ],
                'dependencies' => 'mediawiki.template',
        ],
        'mediawiki.apipretty' => [
-               'styles' => 'resources/src/mediawiki/mediawiki.apipretty.css',
+               'styles' => 'resources/src/mediawiki.apipretty.css',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api' => [
-               'scripts' => 'resources/src/mediawiki/api.js',
+               'scripts' => 'resources/src/mediawiki.api.js',
                'dependencies' => [
                        'mediawiki.util',
                        'user.tokens',
@@ -898,14 +898,14 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api.category' => [
-               'scripts' => 'resources/src/mediawiki/api/category.js',
+               'scripts' => 'resources/src/mediawiki.api.category.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.Title',
                ],
        ],
        'mediawiki.api.edit' => [
-               'scripts' => 'resources/src/mediawiki/api/edit.js',
+               'scripts' => 'resources/src/mediawiki.api.edit.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.user',
@@ -913,21 +913,21 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api.login' => [
-               'scripts' => 'resources/src/mediawiki/api/login.js',
+               'scripts' => 'resources/src/mediawiki.api.login.js',
                'dependencies' => 'mediawiki.api',
        ],
        'mediawiki.api.options' => [
-               'scripts' => 'resources/src/mediawiki/api/options.js',
+               'scripts' => 'resources/src/mediawiki.api.options.js',
                'dependencies' => 'mediawiki.api',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api.parse' => [
-               'scripts' => 'resources/src/mediawiki/api/parse.js',
+               'scripts' => 'resources/src/mediawiki.api.parse.js',
                'dependencies' => 'mediawiki.api',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api.upload' => [
-               'scripts' => 'resources/src/mediawiki/api/upload.js',
+               'scripts' => 'resources/src/mediawiki.api.upload.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.api.edit',
@@ -935,27 +935,27 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api.user' => [
-               'scripts' => 'resources/src/mediawiki/api/user.js',
+               'scripts' => 'resources/src/mediawiki.api.user.js',
                'dependencies' => [
                        'mediawiki.api',
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api.watch' => [
-               'scripts' => 'resources/src/mediawiki/api/watch.js',
+               'scripts' => 'resources/src/mediawiki.api.watch.js',
                'dependencies' => [
                        'mediawiki.api',
                ],
        ],
        'mediawiki.api.messages' => [
-               'scripts' => 'resources/src/mediawiki/api/messages.js',
+               'scripts' => 'resources/src/mediawiki.api.messages.js',
                'dependencies' => [
                        'mediawiki.api',
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.api.rollback' => [
-               'scripts' => 'resources/src/mediawiki/api/rollback.js',
+               'scripts' => 'resources/src/mediawiki.api.rollback.js',
                'dependencies' => [
                        'mediawiki.api',
                ],
@@ -1043,7 +1043,7 @@ return [
                'dependencies' => 'mediawiki.ForeignApi.core',
        ],
        'mediawiki.ForeignApi.core' => [
-               'scripts' => 'resources/src/mediawiki/ForeignApi.js',
+               'scripts' => 'resources/src/mediawiki.ForeignApi.core.js',
                'dependencies' => [
                        'mediawiki.api',
                        'oojs',
@@ -1175,15 +1175,15 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.notification.convertmessagebox' => [
+               'scripts' => 'resources/src/mediawiki.notification.convertmessagebox.js',
                'dependencies' => [
                        'mediawiki.notification',
                ],
-               'scripts' => 'resources/src/mediawiki/mediawiki.notification.convertmessagebox.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.notification.convertmessagebox.styles' => [
                'styles' => [
-                       'resources/src/mediawiki/mediawiki.notification.convertmessagebox.styles.less',
+                       'resources/src/mediawiki.notification.convertmessagebox.styles.less',
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
@@ -1228,13 +1228,13 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.Upload' => [
-               'scripts' => 'resources/src/mediawiki/mediawiki.Upload.js',
+               'scripts' => 'resources/src/mediawiki.Upload.js',
                'dependencies' => [
                        'mediawiki.api.upload',
                ],
        ],
        'mediawiki.ForeignUpload' => [
-               'scripts' => 'resources/src/mediawiki/mediawiki.ForeignUpload.js',
+               'scripts' => 'resources/src/mediawiki.ForeignUpload.js',
                'dependencies' => [
                        'mediawiki.ForeignApi',
                        'mediawiki.Upload',
@@ -1250,7 +1250,7 @@ return [
                'class' => ResourceLoaderUploadDialogModule::class,
        ],
        'mediawiki.ForeignStructuredUpload' => [
-               'scripts' => 'resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js',
+               'scripts' => 'resources/src/mediawiki.ForeignStructuredUpload.js',
                'dependencies' => [
                        'mediawiki.ForeignUpload',
                        'mediawiki.ForeignStructuredUpload.config',
@@ -1261,7 +1261,7 @@ return [
        ],
        'mediawiki.Upload.Dialog' => [
                'scripts' => [
-                       'resources/src/mediawiki/mediawiki.Upload.Dialog.js',
+                       'resources/src/mediawiki.Upload.Dialog.js',
                ],
                'dependencies' => [
                        'mediawiki.Upload.BookletLayout',
@@ -1400,10 +1400,10 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.checkboxtoggle' => [
-               'scripts' => 'resources/src/mediawiki/mediawiki.checkboxtoggle.js',
+               'scripts' => 'resources/src/mediawiki.checkboxtoggle.js',
        ],
        'mediawiki.checkboxtoggle.styles' => [
-               'styles' => 'resources/src/mediawiki/mediawiki.checkboxtoggle.css',
+               'styles' => 'resources/src/mediawiki.checkboxtoggle.styles.css',
        ],
        'mediawiki.cookie' => [
                'scripts' => 'resources/src/mediawiki.cookie.js',
@@ -1679,7 +1679,7 @@ return [
        /* MediaWiki Page */
 
        'mediawiki.page.gallery' => [
-               'scripts' => 'resources/src/mediawiki/page/gallery.js',
+               'scripts' => 'resources/src/mediawiki.page.gallery.js',
                'dependencies' => [
                        'mediawiki.page.gallery.styles',
                        'jquery.throttle-debounce',
@@ -1693,7 +1693,7 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.page.gallery.slideshow' => [
-               'scripts' => 'resources/src/mediawiki/page/gallery-slideshow.js',
+               'scripts' => 'resources/src/mediawiki.page.gallery.slideshow.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.Title',
@@ -1708,7 +1708,7 @@ return [
                ]
        ],
        'mediawiki.page.ready' => [
-               'scripts' => 'resources/src/mediawiki/page/ready.js',
+               'scripts' => 'resources/src/mediawiki.page.ready.js',
                'dependencies' => [
                        'jquery.accessKeyLabel',
                        'jquery.checkboxShiftClick',
@@ -1717,11 +1717,11 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.page.startup' => [
-               'scripts' => 'resources/src/mediawiki/page/startup.js',
+               'scripts' => 'resources/src/mediawiki.page.startup.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.page.patrol.ajax' => [
-               'scripts' => 'resources/src/mediawiki/page/patrol.ajax.js',
+               'scripts' => 'resources/src/mediawiki.page.patrol.ajax.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.util',
@@ -1737,7 +1737,7 @@ return [
                ],
        ],
        'mediawiki.page.watch.ajax' => [
-               'scripts' => 'resources/src/mediawiki/page/watch.js',
+               'scripts' => 'resources/src/mediawiki.page.watch.ajax.js',
                'dependencies' => [
                        'mediawiki.api.watch',
                        'mediawiki.notify',
@@ -1762,7 +1762,7 @@ return [
                ],
        ],
        'mediawiki.page.rollback' => [
-               'scripts' => 'resources/src/mediawiki/page/rollback.js',
+               'scripts' => 'resources/src/mediawiki.page.rollback.js',
                'dependencies' => [
                        'mediawiki.api.rollback',
                        'mediawiki.notify',
@@ -1775,7 +1775,7 @@ return [
                ],
        ],
        'mediawiki.page.image.pagination' => [
-               'scripts' => 'resources/src/mediawiki/page/image-pagination.js',
+               'scripts' => 'resources/src/mediawiki.page.image.pagination.js',
                'dependencies' => [
                        'mediawiki.util',
                        'jquery.spinner',
diff --git a/resources/src/mediawiki.ForeignApi.core.js b/resources/src/mediawiki.ForeignApi.core.js
new file mode 100644 (file)
index 0000000..1a3cdd5
--- /dev/null
@@ -0,0 +1,119 @@
+( function ( mw, $ ) {
+
+       /**
+        * Create an object like mw.Api, but automatically handling everything required to communicate
+        * with another MediaWiki wiki via cross-origin requests (CORS).
+        *
+        * The foreign wiki must be configured to accept requests from the current wiki. See
+        * <https://www.mediawiki.org/wiki/Manual:$wgCrossSiteAJAXdomains> for details.
+        *
+        *     var api = new mw.ForeignApi( 'https://commons.wikimedia.org/w/api.php' );
+        *     api.get( {
+        *         action: 'query',
+        *         meta: 'userinfo'
+        *     } ).done( function ( data ) {
+        *         console.log( data );
+        *     } );
+        *
+        * To ensure that the user at the foreign wiki is logged in, pass the `assert: 'user'` parameter
+        * to #get/#post (since MW 1.23): if they are not, the API request will fail. (Note that this
+        * doesn't guarantee that it's the same user.)
+        *
+        * Authentication-related MediaWiki extensions may extend this class to ensure that the user
+        * authenticated on the current wiki will be automatically authenticated on the foreign one. These
+        * extension modules should be registered using the ResourceLoaderForeignApiModules hook. See
+        * CentralAuth for a practical example. The general pattern to extend and override the name is:
+        *
+        *     function MyForeignApi() {};
+        *     OO.inheritClass( MyForeignApi, mw.ForeignApi );
+        *     mw.ForeignApi = MyForeignApi;
+        *
+        * @class mw.ForeignApi
+        * @extends mw.Api
+        * @since 1.26
+        *
+        * @constructor
+        * @param {string|mw.Uri} url URL pointing to another wiki's `api.php` endpoint.
+        * @param {Object} [options] See mw.Api.
+        * @param {Object} [options.anonymous=false] Perform all requests anonymously. Use this option if
+        *     the target wiki may otherwise not accept cross-origin requests, or if you don't need to
+        *     perform write actions or read restricted information and want to avoid the overhead.
+        *
+        * @author Bartosz DziewoÅ„ski
+        * @author Jon Robson
+        */
+       function CoreForeignApi( url, options ) {
+               if ( !url || $.isPlainObject( url ) ) {
+                       throw new Error( 'mw.ForeignApi() requires a `url` parameter' );
+               }
+
+               this.apiUrl = String( url );
+               this.anonymous = options && options.anonymous;
+
+               options = $.extend( /* deep=*/ true,
+                       {
+                               ajax: {
+                                       url: this.apiUrl,
+                                       xhrFields: {
+                                               withCredentials: !this.anonymous
+                                       }
+                               },
+                               parameters: {
+                                       // Add 'origin' query parameter to all requests.
+                                       origin: this.getOrigin()
+                               }
+                       },
+                       options
+               );
+
+               // Call parent constructor
+               CoreForeignApi.parent.call( this, options );
+       }
+
+       OO.inheritClass( CoreForeignApi, mw.Api );
+
+       /**
+        * Return the origin to use for API requests, in the required format (protocol, host and port, if
+        * any).
+        *
+        * @protected
+        * @return {string}
+        */
+       CoreForeignApi.prototype.getOrigin = function () {
+               var origin;
+               if ( this.anonymous ) {
+                       return '*';
+               }
+               origin = location.protocol + '//' + location.hostname;
+               if ( location.port ) {
+                       origin += ':' + location.port;
+               }
+               return origin;
+       };
+
+       /**
+        * @inheritdoc
+        */
+       CoreForeignApi.prototype.ajax = function ( parameters, ajaxOptions ) {
+               var url, origin, newAjaxOptions;
+
+               // 'origin' query parameter must be part of the request URI, and not just POST request body
+               if ( ajaxOptions.type === 'POST' ) {
+                       url = ( ajaxOptions && ajaxOptions.url ) || this.defaults.ajax.url;
+                       origin = ( parameters && parameters.origin ) || this.defaults.parameters.origin;
+                       url += ( url.indexOf( '?' ) !== -1 ? '&' : '?' ) +
+                               // Depending on server configuration, MediaWiki may forbid periods in URLs, due to an IE 6
+                               // XSS bug. So let's escape them here. See WebRequest::checkUrlExtension() and T30235.
+                               'origin=' + encodeURIComponent( origin ).replace( /\./g, '%2E' );
+                       newAjaxOptions = $.extend( {}, ajaxOptions, { url: url } );
+               } else {
+                       newAjaxOptions = ajaxOptions;
+               }
+
+               return CoreForeignApi.parent.prototype.ajax.call( this, parameters, newAjaxOptions );
+       };
+
+       // Expose
+       mw.ForeignApi = CoreForeignApi;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.ForeignStructuredUpload.js b/resources/src/mediawiki.ForeignStructuredUpload.js
new file mode 100644 (file)
index 0000000..177861e
--- /dev/null
@@ -0,0 +1,250 @@
+( function ( mw, $, OO ) {
+       /**
+        * Used to represent an upload in progress on the frontend.
+        *
+        * This subclass will upload to a wiki using a structured metadata
+        * system similar to (or identical to) the one on Wikimedia Commons.
+        *
+        * See <https://commons.wikimedia.org/wiki/Commons:Structured_data> for
+        * a more detailed description of how that system works.
+        *
+        * **TODO: This currently only supports uploads under CC-BY-SA 4.0,
+        * and should really have support for more licenses.**
+        *
+        * @class mw.ForeignStructuredUpload
+        * @extends mw.ForeignUpload
+        *
+        * @constructor
+        * @param {string} [target]
+        * @param {Object} [apiconfig]
+        */
+       function ForeignStructuredUpload( target, apiconfig ) {
+               this.date = undefined;
+               this.descriptions = [];
+               this.categories = [];
+
+               // Config for uploads to local wiki.
+               // Can be overridden with foreign wiki config when #loadConfig is called.
+               this.config = mw.config.get( 'wgUploadDialog' );
+
+               mw.ForeignUpload.call( this, target, apiconfig );
+       }
+
+       OO.inheritClass( ForeignStructuredUpload, mw.ForeignUpload );
+
+       /**
+        * Get the configuration for the form and filepage from the foreign wiki, if any, and use it for
+        * this upload.
+        *
+        * @return {jQuery.Promise} Promise returning config object
+        */
+       ForeignStructuredUpload.prototype.loadConfig = function () {
+               var deferred,
+                       upload = this;
+
+               if ( this.configPromise ) {
+                       return this.configPromise;
+               }
+
+               if ( this.target === 'local' ) {
+                       deferred = $.Deferred();
+                       setTimeout( function () {
+                               // Resolve asynchronously, so that it's harder to accidentally write synchronous code that
+                               // will break for cross-wiki uploads
+                               deferred.resolve( upload.config );
+                       } );
+                       this.configPromise = deferred.promise();
+               } else {
+                       this.configPromise = this.apiPromise.then( function ( api ) {
+                               // Get the config from the foreign wiki
+                               return api.get( {
+                                       action: 'query',
+                                       meta: 'siteinfo',
+                                       siprop: 'uploaddialog',
+                                       // For convenient true/false booleans
+                                       formatversion: 2
+                               } ).then( function ( resp ) {
+                                       // Foreign wiki might be running a pre-1.27 MediaWiki, without support for this
+                                       if ( resp.query && resp.query.uploaddialog ) {
+                                               upload.config = resp.query.uploaddialog;
+                                               return upload.config;
+                                       } else {
+                                               return $.Deferred().reject( 'upload-foreign-cant-load-config' );
+                                       }
+                               }, function () {
+                                       return $.Deferred().reject( 'upload-foreign-cant-load-config' );
+                               } );
+                       } );
+               }
+
+               return this.configPromise;
+       };
+
+       /**
+        * Add categories to the upload.
+        *
+        * @param {string[]} categories Array of categories to which this upload will be added.
+        */
+       ForeignStructuredUpload.prototype.addCategories = function ( categories ) {
+               // The length of the array must be less than 10000.
+               // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push#Merging_two_arrays
+               Array.prototype.push.apply( this.categories, categories );
+       };
+
+       /**
+        * Empty the list of categories for the upload.
+        */
+       ForeignStructuredUpload.prototype.clearCategories = function () {
+               this.categories = [];
+       };
+
+       /**
+        * Add a description to the upload.
+        *
+        * @param {string} language The language code for the description's language. Must have a template on the target wiki to work properly.
+        * @param {string} description The description of the file.
+        */
+       ForeignStructuredUpload.prototype.addDescription = function ( language, description ) {
+               this.descriptions.push( {
+                       language: language,
+                       text: description
+               } );
+       };
+
+       /**
+        * Empty the list of descriptions for the upload.
+        */
+       ForeignStructuredUpload.prototype.clearDescriptions = function () {
+               this.descriptions = [];
+       };
+
+       /**
+        * Set the date of creation for the upload.
+        *
+        * @param {Date} date
+        */
+       ForeignStructuredUpload.prototype.setDate = function ( date ) {
+               this.date = date;
+       };
+
+       /**
+        * Get the text of the file page, to be created on upload. Brings together
+        * several different pieces of information to create useful text.
+        *
+        * @return {string}
+        */
+       ForeignStructuredUpload.prototype.getText = function () {
+               return this.config.format.filepage
+                       // Replace "named parameters" with the given information
+                       .replace( '$DESCRIPTION', this.getDescriptions() )
+                       .replace( '$DATE', this.getDate() )
+                       .replace( '$SOURCE', this.getSource() )
+                       .replace( '$AUTHOR', this.getUser() )
+                       .replace( '$LICENSE', this.getLicense() )
+                       .replace( '$CATEGORIES', this.getCategories() );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       ForeignStructuredUpload.prototype.getComment = function () {
+               var
+                       isLocal = this.target === 'local',
+                       comment = typeof this.config.comment === 'string' ?
+                               this.config.comment :
+                               this.config.comment[ isLocal ? 'local' : 'foreign' ];
+               return comment
+                       .replace( '$PAGENAME', mw.config.get( 'wgPageName' ) )
+                       .replace( '$HOST', location.host );
+       };
+
+       /**
+        * Gets the wikitext for the creation date of this upload.
+        *
+        * @private
+        * @return {string}
+        */
+       ForeignStructuredUpload.prototype.getDate = function () {
+               if ( !this.date ) {
+                       return '';
+               }
+
+               return this.date.toString();
+       };
+
+       /**
+        * Fetches the wikitext for any descriptions that have been added
+        * to the upload.
+        *
+        * @private
+        * @return {string}
+        */
+       ForeignStructuredUpload.prototype.getDescriptions = function () {
+               var upload = this;
+               return this.descriptions.map( function ( desc ) {
+                       return upload.config.format.description
+                               .replace( '$LANGUAGE', desc.language )
+                               .replace( '$TEXT', desc.text );
+               } ).join( '\n' );
+       };
+
+       /**
+        * Fetches the wikitext for the categories to which the upload will
+        * be added.
+        *
+        * @private
+        * @return {string}
+        */
+       ForeignStructuredUpload.prototype.getCategories = function () {
+               if ( this.categories.length === 0 ) {
+                       return this.config.format.uncategorized;
+               }
+
+               return this.categories.map( function ( cat ) {
+                       return '[[Category:' + cat + ']]';
+               } ).join( '\n' );
+       };
+
+       /**
+        * Gets the wikitext for the license of the upload.
+        *
+        * @private
+        * @return {string}
+        */
+       ForeignStructuredUpload.prototype.getLicense = function () {
+               return this.config.format.license;
+       };
+
+       /**
+        * Get the source. This should be some sort of localised text for "Own work".
+        *
+        * @private
+        * @return {string}
+        */
+       ForeignStructuredUpload.prototype.getSource = function () {
+               return this.config.format.ownwork;
+       };
+
+       /**
+        * Get the username.
+        *
+        * @private
+        * @return {string}
+        */
+       ForeignStructuredUpload.prototype.getUser = function () {
+               var username, namespace;
+               // Do not localise, we don't know the language of target wiki
+               namespace = 'User';
+               username = mw.config.get( 'wgUserName' );
+               if ( !username ) {
+                       // The user is not logged in locally. However, they might be logged in on the foreign wiki.
+                       // We should record their username there. (If they're not logged in there either, this will
+                       // record the IP address.) It's also possible that the user opened this dialog, got an error
+                       // about not being logged in, logged in in another browser tab, then continued uploading.
+                       username = '{{subst:REVISIONUSER}}';
+               }
+               return '[[' + namespace + ':' + username + '|' + username + ']]';
+       };
+
+       mw.ForeignStructuredUpload = ForeignStructuredUpload;
+}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki.ForeignUpload.js b/resources/src/mediawiki.ForeignUpload.js
new file mode 100644 (file)
index 0000000..08fc01d
--- /dev/null
@@ -0,0 +1,143 @@
+( function ( mw, OO, $ ) {
+       /**
+        * Used to represent an upload in progress on the frontend.
+        *
+        * Subclassed to upload to a foreign API, with no other goodies. Use
+        * this for a generic foreign image repository on your wiki farm.
+        *
+        * Note you can provide the {@link #target target} or not - if the first argument is
+        * an object, we assume you want the default, and treat it as apiconfig
+        * instead.
+        *
+        * @class mw.ForeignUpload
+        * @extends mw.Upload
+        *
+        * @constructor
+        * @param {string} [target] Used to set up the target
+        *     wiki. If not remote, this class behaves identically to mw.Upload (unless further subclassed)
+        *     Use the same names as set in $wgForeignFileRepos for this. Also,
+        *     make sure there is an entry in the $wgForeignUploadTargets array for this name.
+        * @param {Object} [apiconfig] Passed to the constructor of mw.ForeignApi or mw.Api, as needed.
+        */
+       function ForeignUpload( target, apiconfig ) {
+               var api,
+                       validTargets = mw.config.get( 'wgForeignUploadTargets' ),
+                       upload = this;
+
+               if ( typeof target === 'object' ) {
+                       // target probably wasn't passed in, it must
+                       // be apiconfig
+                       apiconfig = target;
+                       target = undefined;
+               }
+
+               // * Use the given `target` first;
+               // * If not given, fall back to default (first) ForeignUploadTarget;
+               // * If none is configured, fall back to local uploads.
+               this.target = target || validTargets[ 0 ] || 'local';
+
+               // Now we have several different options.
+               // If the local wiki is the target, then we can skip a bunch of steps
+               // and just return an mw.Api object, because we don't need any special
+               // configuration for that.
+               // However, if the target is a remote wiki, we must check the API
+               // to confirm that the target is one that this site is configured to
+               // support.
+               if ( validTargets.length === 0 ) {
+                       this.apiPromise = $.Deferred().reject( 'upload-dialog-disabled' );
+               } else if ( this.target === 'local' ) {
+                       // If local uploads were requested, but they are disabled, fail.
+                       if ( !mw.config.get( 'wgEnableUploads' ) ) {
+                               this.apiPromise = $.Deferred().reject( 'uploaddisabledtext' );
+                       } else {
+                               // We'll ignore the CORS and centralauth stuff if the target is
+                               // the local wiki.
+                               this.apiPromise = $.Deferred().resolve( new mw.Api( apiconfig ) );
+                       }
+               } else {
+                       api = new mw.Api();
+                       this.apiPromise = api.get( {
+                               action: 'query',
+                               meta: 'filerepoinfo',
+                               friprop: [ 'name', 'scriptDirUrl', 'canUpload' ]
+                       } ).then( function ( data ) {
+                               var i, repo,
+                                       repos = data.query.repos;
+
+                               // First pass - try to find the passed-in target and check
+                               // that it's configured for uploads.
+                               for ( i in repos ) {
+                                       repo = repos[ i ];
+
+                                       // Skip repos that are not our target, or if they
+                                       // are the target, cannot be uploaded to.
+                                       if ( repo.name === upload.target && repo.canUpload === '' ) {
+                                               return new mw.ForeignApi(
+                                                       repo.scriptDirUrl + '/api.php',
+                                                       apiconfig
+                                               );
+                                       }
+                               }
+
+                               return $.Deferred().reject( 'upload-foreign-cant-upload' );
+                       } );
+               }
+
+               // Build the upload object without an API - this class overrides the
+               // actual API call methods to wait for the apiPromise to resolve
+               // before continuing.
+               mw.Upload.call( this, null );
+       }
+
+       OO.inheritClass( ForeignUpload, mw.Upload );
+
+       /**
+        * @property {string} target
+        * Used to specify the target repository of the upload.
+        *
+        * If you set this to something that isn't 'local', you must be sure to
+        * add that target to $wgForeignUploadTargets in LocalSettings, and the
+        * repository must be set up to use CORS and CentralAuth.
+        *
+        * Most wikis use "shared" to refer to Wikimedia Commons, we assume that
+        * in this class and in the messages linked to it.
+        *
+        * Defaults to the first available foreign upload target,
+        * or to local uploads if no foreign target is configured.
+        */
+
+       /**
+        * @inheritdoc
+        */
+       ForeignUpload.prototype.getApi = function () {
+               return this.apiPromise;
+       };
+
+       /**
+        * Override from mw.Upload to make sure the API info is found and allowed
+        *
+        * @inheritdoc
+        */
+       ForeignUpload.prototype.upload = function () {
+               var upload = this;
+               return this.apiPromise.then( function ( api ) {
+                       upload.api = api;
+                       return mw.Upload.prototype.upload.call( upload );
+               } );
+       };
+
+       /**
+        * Override from mw.Upload to make sure the API info is found and allowed
+        *
+        * @inheritdoc
+        */
+       ForeignUpload.prototype.uploadToStash = function () {
+               var upload = this;
+               return this.apiPromise.then( function ( api ) {
+                       upload.api = api;
+                       return mw.Upload.prototype.uploadToStash.call( upload );
+               } );
+       };
+
+       mw.ForeignUpload = ForeignUpload;
+}( mediaWiki, OO, jQuery ) );
diff --git a/resources/src/mediawiki.Upload.Dialog.js b/resources/src/mediawiki.Upload.Dialog.js
new file mode 100644 (file)
index 0000000..00c04bc
--- /dev/null
@@ -0,0 +1,230 @@
+( function ( $, mw ) {
+
+       /**
+        * mw.Upload.Dialog controls a {@link mw.Upload.BookletLayout BookletLayout}.
+        *
+        * ## Usage
+        *
+        * To use, setup a {@link OO.ui.WindowManager window manager} like for normal
+        * dialogs:
+        *
+        *     var uploadDialog = new mw.Upload.Dialog();
+        *     var windowManager = new OO.ui.WindowManager();
+        *     $( 'body' ).append( windowManager.$element );
+        *     windowManager.addWindows( [ uploadDialog ] );
+        *     windowManager.openWindow( uploadDialog );
+        *
+        * The dialog's closing promise can be used to get details of the upload.
+        *
+        * If you want to use a different OO.ui.BookletLayout, for example the
+        * mw.ForeignStructuredUpload.BookletLayout, like in the case of of the upload
+        * interface in VisualEditor, you can pass it in the {@link #cfg-bookletClass}:
+        *
+        *     var uploadDialog = new mw.Upload.Dialog( {
+        *         bookletClass: mw.ForeignStructuredUpload.BookletLayout
+        *     } );
+        *
+        *
+        * @class mw.Upload.Dialog
+        * @uses mw.Upload
+        * @uses mw.Upload.BookletLayout
+        * @extends OO.ui.ProcessDialog
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        * @cfg {Function} [bookletClass=mw.Upload.BookletLayout] Booklet class to be
+        *     used for the steps
+        * @cfg {Object} [booklet] Booklet constructor configuration
+        */
+       mw.Upload.Dialog = function ( config ) {
+               // Config initialization
+               config = $.extend( {
+                       bookletClass: mw.Upload.BookletLayout
+               }, config );
+
+               // Parent constructor
+               mw.Upload.Dialog.parent.call( this, config );
+
+               // Initialize
+               this.bookletClass = config.bookletClass;
+               this.bookletConfig = config.booklet;
+       };
+
+       /* Setup */
+
+       OO.inheritClass( mw.Upload.Dialog, OO.ui.ProcessDialog );
+
+       /* Static Properties */
+
+       /**
+        * @inheritdoc
+        * @property name
+        */
+       mw.Upload.Dialog.static.name = 'mwUploadDialog';
+
+       /**
+        * @inheritdoc
+        * @property title
+        */
+       mw.Upload.Dialog.static.title = mw.msg( 'upload-dialog-title' );
+
+       /**
+        * @inheritdoc
+        * @property actions
+        */
+       mw.Upload.Dialog.static.actions = [
+               {
+                       flags: 'safe',
+                       action: 'cancel',
+                       label: mw.msg( 'upload-dialog-button-cancel' ),
+                       modes: [ 'upload', 'insert' ]
+               },
+               {
+                       flags: 'safe',
+                       action: 'cancelupload',
+                       label: mw.msg( 'upload-dialog-button-back' ),
+                       modes: [ 'info' ]
+               },
+               {
+                       flags: [ 'primary', 'progressive' ],
+                       label: mw.msg( 'upload-dialog-button-done' ),
+                       action: 'insert',
+                       modes: 'insert'
+               },
+               {
+                       flags: [ 'primary', 'progressive' ],
+                       label: mw.msg( 'upload-dialog-button-save' ),
+                       action: 'save',
+                       modes: 'info'
+               },
+               {
+                       flags: [ 'primary', 'progressive' ],
+                       label: mw.msg( 'upload-dialog-button-upload' ),
+                       action: 'upload',
+                       modes: 'upload'
+               }
+       ];
+
+       /* Methods */
+
+       /**
+        * @inheritdoc
+        */
+       mw.Upload.Dialog.prototype.initialize = function () {
+               // Parent method
+               mw.Upload.Dialog.parent.prototype.initialize.call( this );
+
+               this.uploadBooklet = this.createUploadBooklet();
+               this.uploadBooklet.connect( this, {
+                       set: 'onUploadBookletSet',
+                       uploadValid: 'onUploadValid',
+                       infoValid: 'onInfoValid'
+               } );
+
+               this.$body.append( this.uploadBooklet.$element );
+       };
+
+       /**
+        * Create an upload booklet
+        *
+        * @protected
+        * @return {mw.Upload.BookletLayout} An upload booklet
+        */
+       mw.Upload.Dialog.prototype.createUploadBooklet = function () {
+               // eslint-disable-next-line new-cap
+               return new this.bookletClass( $.extend( {
+                       $overlay: this.$overlay
+               }, this.bookletConfig ) );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.Upload.Dialog.prototype.getBodyHeight = function () {
+               return 600;
+       };
+
+       /**
+        * Handle panelNameSet events from the upload booklet
+        *
+        * @protected
+        * @param {OO.ui.PageLayout} page Current page
+        */
+       mw.Upload.Dialog.prototype.onUploadBookletSet = function ( page ) {
+               this.actions.setMode( page.getName() );
+               this.actions.setAbilities( { upload: false, save: false } );
+       };
+
+       /**
+        * Handle uploadValid events
+        *
+        * {@link OO.ui.ActionSet#setAbilities Sets abilities}
+        * for the dialog accordingly.
+        *
+        * @protected
+        * @param {boolean} isValid The panel is complete and valid
+        */
+       mw.Upload.Dialog.prototype.onUploadValid = function ( isValid ) {
+               this.actions.setAbilities( { upload: isValid } );
+       };
+
+       /**
+        * Handle infoValid events
+        *
+        * {@link OO.ui.ActionSet#setAbilities Sets abilities}
+        * for the dialog accordingly.
+        *
+        * @protected
+        * @param {boolean} isValid The panel is complete and valid
+        */
+       mw.Upload.Dialog.prototype.onInfoValid = function ( isValid ) {
+               this.actions.setAbilities( { save: isValid } );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.Upload.Dialog.prototype.getSetupProcess = function ( data ) {
+               return mw.Upload.Dialog.parent.prototype.getSetupProcess.call( this, data )
+                       .next( function () {
+                               return this.uploadBooklet.initialize();
+                       }, this );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.Upload.Dialog.prototype.getActionProcess = function ( action ) {
+               var dialog = this;
+
+               if ( action === 'upload' ) {
+                       return new OO.ui.Process( this.uploadBooklet.uploadFile() );
+               }
+               if ( action === 'save' ) {
+                       return new OO.ui.Process( this.uploadBooklet.saveFile() );
+               }
+               if ( action === 'insert' ) {
+                       return new OO.ui.Process( function () {
+                               dialog.close( dialog.upload );
+                       } );
+               }
+               if ( action === 'cancel' ) {
+                       return new OO.ui.Process( this.close().closed );
+               }
+               if ( action === 'cancelupload' ) {
+                       return new OO.ui.Process( this.uploadBooklet.initialize() );
+               }
+
+               return mw.Upload.Dialog.parent.prototype.getActionProcess.call( this, action );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.Upload.Dialog.prototype.getTeardownProcess = function ( data ) {
+               return mw.Upload.Dialog.parent.prototype.getTeardownProcess.call( this, data )
+                       .next( function () {
+                               this.uploadBooklet.clear();
+                       }, this );
+       };
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.Upload.js b/resources/src/mediawiki.Upload.js
new file mode 100644 (file)
index 0000000..7e6cfb6
--- /dev/null
@@ -0,0 +1,393 @@
+( function ( mw, $ ) {
+       var UP;
+
+       /**
+        * Used to represent an upload in progress on the frontend.
+        * Most of the functionality is implemented in mw.Api.plugin.upload,
+        * but this model class will tie it together as well as let you perform
+        * actions in a logical way.
+        *
+        * A simple example:
+        *
+        *     var file = new OO.ui.SelectFileWidget(),
+        *       button = new OO.ui.ButtonWidget( { label: 'Save' } ),
+        *       upload = new mw.Upload;
+        *
+        *     button.on( 'click', function () {
+        *       upload.setFile( file.getValue() );
+        *       upload.setFilename( file.getValue().name );
+        *       upload.upload();
+        *     } );
+        *
+        *     $( 'body' ).append( file.$element, button.$element );
+        *
+        * You can also choose to {@link #uploadToStash stash the upload} and
+        * {@link #finishStashUpload finalize} it later:
+        *
+        *     var file, // Some file object
+        *       upload = new mw.Upload,
+        *       stashPromise = $.Deferred();
+        *
+        *     upload.setFile( file );
+        *     upload.uploadToStash().then( function () {
+        *       stashPromise.resolve();
+        *     } );
+        *
+        *     stashPromise.then( function () {
+        *       upload.setFilename( 'foo' );
+        *       upload.setText( 'bar' );
+        *       upload.finishStashUpload().then( function () {
+        *         console.log( 'Done!' );
+        *       } );
+        *     } );
+        *
+        * @class mw.Upload
+        *
+        * @constructor
+        * @param {Object|mw.Api} [apiconfig] A mw.Api object (or subclass), or configuration
+        *     to pass to the constructor of mw.Api.
+        */
+       function Upload( apiconfig ) {
+               this.api = ( apiconfig instanceof mw.Api ) ? apiconfig : new mw.Api( apiconfig );
+
+               this.watchlist = false;
+               this.text = '';
+               this.comment = '';
+               this.filename = null;
+               this.file = null;
+               this.setState( Upload.State.NEW );
+
+               this.imageinfo = undefined;
+       }
+
+       UP = Upload.prototype;
+
+       /**
+        * Get the mw.Api instance used by this Upload object.
+        *
+        * @return {jQuery.Promise}
+        * @return {Function} return.done
+        * @return {mw.Api} return.done.api
+        */
+       UP.getApi = function () {
+               return $.Deferred().resolve( this.api ).promise();
+       };
+
+       /**
+        * Set the text of the file page, to be created on file upload.
+        *
+        * @param {string} text
+        */
+       UP.setText = function ( text ) {
+               this.text = text;
+       };
+
+       /**
+        * Set the filename, to be finalized on upload.
+        *
+        * @param {string} filename
+        */
+       UP.setFilename = function ( filename ) {
+               this.filename = filename;
+       };
+
+       /**
+        * Set the stashed file to finish uploading.
+        *
+        * @param {string} filekey
+        */
+       UP.setFilekey = function ( filekey ) {
+               var upload = this;
+
+               this.setState( Upload.State.STASHED );
+               this.stashPromise = $.Deferred().resolve( function ( data ) {
+                       return upload.api.uploadFromStash( filekey, data );
+               } );
+       };
+
+       /**
+        * Sets the filename based on the filename as it was on the upload.
+        */
+       UP.setFilenameFromFile = function () {
+               var file = this.getFile();
+               if ( !file ) {
+                       return;
+               }
+               if ( file.nodeType && file.nodeType === Node.ELEMENT_NODE ) {
+                       // File input element, use getBasename to cut out the path
+                       this.setFilename( this.getBasename( file.value ) );
+               } else if ( file.name ) {
+                       // HTML5 FileAPI File object, but use getBasename to be safe
+                       this.setFilename( this.getBasename( file.name ) );
+               } else {
+                       // If we ever implement uploading files from clipboard, they might not have a name
+                       this.setFilename( '?' );
+               }
+       };
+
+       /**
+        * Set the file to be uploaded.
+        *
+        * @param {HTMLInputElement|File|Blob} file
+        */
+       UP.setFile = function ( file ) {
+               this.file = file;
+       };
+
+       /**
+        * Set whether the file should be watchlisted after upload.
+        *
+        * @param {boolean} watchlist
+        */
+       UP.setWatchlist = function ( watchlist ) {
+               this.watchlist = watchlist;
+       };
+
+       /**
+        * Set the edit comment for the upload.
+        *
+        * @param {string} comment
+        */
+       UP.setComment = function ( comment ) {
+               this.comment = comment;
+       };
+
+       /**
+        * Get the text of the file page, to be created on file upload.
+        *
+        * @return {string}
+        */
+       UP.getText = function () {
+               return this.text;
+       };
+
+       /**
+        * Get the filename, to be finalized on upload.
+        *
+        * @return {string}
+        */
+       UP.getFilename = function () {
+               return this.filename;
+       };
+
+       /**
+        * Get the file being uploaded.
+        *
+        * @return {HTMLInputElement|File|Blob}
+        */
+       UP.getFile = function () {
+               return this.file;
+       };
+
+       /**
+        * Get the boolean for whether the file will be watchlisted after upload.
+        *
+        * @return {boolean}
+        */
+       UP.getWatchlist = function () {
+               return this.watchlist;
+       };
+
+       /**
+        * Get the current value of the edit comment for the upload.
+        *
+        * @return {string}
+        */
+       UP.getComment = function () {
+               return this.comment;
+       };
+
+       /**
+        * Gets the base filename from a path name.
+        *
+        * @param {string} path
+        * @return {string}
+        */
+       UP.getBasename = function ( path ) {
+               if ( path === undefined || path === null ) {
+                       return '';
+               }
+
+               // Find the index of the last path separator in the
+               // path, and add 1. Then, take the entire string after that.
+               return path.slice(
+                       Math.max(
+                               path.lastIndexOf( '/' ),
+                               path.lastIndexOf( '\\' )
+                       ) + 1
+               );
+       };
+
+       /**
+        * Sets the state and state details (if any) of the upload.
+        *
+        * @param {mw.Upload.State} state
+        * @param {Object} stateDetails
+        */
+       UP.setState = function ( state, stateDetails ) {
+               this.state = state;
+               this.stateDetails = stateDetails;
+       };
+
+       /**
+        * Gets the state of the upload.
+        *
+        * @return {mw.Upload.State}
+        */
+       UP.getState = function () {
+               return this.state;
+       };
+
+       /**
+        * Gets details of the current state.
+        *
+        * @return {string}
+        */
+       UP.getStateDetails = function () {
+               return this.stateDetails;
+       };
+
+       /**
+        * Get the imageinfo object for the finished upload.
+        * Only available once the upload is finished! Don't try to get it
+        * beforehand.
+        *
+        * @return {Object|undefined}
+        */
+       UP.getImageInfo = function () {
+               return this.imageinfo;
+       };
+
+       /**
+        * Upload the file directly.
+        *
+        * @return {jQuery.Promise}
+        */
+       UP.upload = function () {
+               var upload = this;
+
+               if ( !this.getFile() ) {
+                       return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
+               }
+
+               if ( !this.getFilename() ) {
+                       return $.Deferred().reject( 'No filename set. Call setFilename to add one.' );
+               }
+
+               this.setState( Upload.State.UPLOADING );
+
+               return this.api.chunkedUpload( this.getFile(), {
+                       watchlist: ( this.getWatchlist() ) ? 1 : undefined,
+                       comment: this.getComment(),
+                       filename: this.getFilename(),
+                       text: this.getText()
+               } ).then( function ( result ) {
+                       upload.setState( Upload.State.UPLOADED );
+                       upload.imageinfo = result.upload.imageinfo;
+                       return result;
+               }, function ( errorCode, result ) {
+                       if ( result && result.upload && result.upload.warnings ) {
+                               upload.setState( Upload.State.WARNING, result );
+                       } else {
+                               upload.setState( Upload.State.ERROR, result );
+                       }
+                       return $.Deferred().reject( errorCode, result );
+               } );
+       };
+
+       /**
+        * Upload the file to the stash to be completed later.
+        *
+        * @return {jQuery.Promise}
+        */
+       UP.uploadToStash = function () {
+               var upload = this;
+
+               if ( !this.getFile() ) {
+                       return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
+               }
+
+               if ( !this.getFilename() ) {
+                       this.setFilenameFromFile();
+               }
+
+               this.setState( Upload.State.UPLOADING );
+
+               this.stashPromise = this.api.chunkedUploadToStash( this.getFile(), {
+                       filename: this.getFilename()
+               } ).then( function ( finishStash ) {
+                       upload.setState( Upload.State.STASHED );
+                       return finishStash;
+               }, function ( errorCode, result ) {
+                       if ( result && result.upload && result.upload.warnings ) {
+                               upload.setState( Upload.State.WARNING, result );
+                       } else {
+                               upload.setState( Upload.State.ERROR, result );
+                       }
+                       return $.Deferred().reject( errorCode, result );
+               } );
+
+               return this.stashPromise;
+       };
+
+       /**
+        * Finish a stash upload.
+        *
+        * @return {jQuery.Promise}
+        */
+       UP.finishStashUpload = function () {
+               var upload = this;
+
+               if ( !this.stashPromise ) {
+                       return $.Deferred().reject( 'This upload has not been stashed, please upload it to the stash first.' );
+               }
+
+               return this.stashPromise.then( function ( finishStash ) {
+                       upload.setState( Upload.State.UPLOADING );
+
+                       return finishStash( {
+                               watchlist: ( upload.getWatchlist() ) ? 1 : undefined,
+                               comment: upload.getComment(),
+                               filename: upload.getFilename(),
+                               text: upload.getText()
+                       } ).then( function ( result ) {
+                               upload.setState( Upload.State.UPLOADED );
+                               upload.imageinfo = result.upload.imageinfo;
+                               return result;
+                       }, function ( errorCode, result ) {
+                               if ( result && result.upload && result.upload.warnings ) {
+                                       upload.setState( Upload.State.WARNING, result );
+                               } else {
+                                       upload.setState( Upload.State.ERROR, result );
+                               }
+                               return $.Deferred().reject( errorCode, result );
+                       } );
+               } );
+       };
+
+       /**
+        * @enum mw.Upload.State
+        * State of uploads represented in simple terms.
+        */
+       Upload.State = {
+               /** Upload not yet started */
+               NEW: 0,
+
+               /** Upload finished, but there was a warning */
+               WARNING: 1,
+
+               /** Upload finished, but there was an error */
+               ERROR: 2,
+
+               /** Upload in progress */
+               UPLOADING: 3,
+
+               /** Upload finished, but not published, call #finishStashUpload */
+               STASHED: 4,
+
+               /** Upload finished and published */
+               UPLOADED: 5
+       };
+
+       mw.Upload = Upload;
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.category.js b/resources/src/mediawiki.api.category.js
new file mode 100644 (file)
index 0000000..85df90e
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+ * @class mw.Api.plugin.category
+ */
+( function ( mw, $ ) {
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * Determine if a category exists.
+                *
+                * @param {mw.Title|string} title
+                * @return {jQuery.Promise}
+                * @return {Function} return.done
+                * @return {boolean} return.done.isCategory Whether the category exists.
+                */
+               isCategory: function ( title ) {
+                       var apiPromise = this.get( {
+                               formatversion: 2,
+                               prop: 'categoryinfo',
+                               titles: [ String( title ) ]
+                       } );
+
+                       return apiPromise
+                               .then( function ( data ) {
+                                       return !!(
+                                               data.query && // query is missing on title=""
+                                               data.query.pages && // query.pages is missing on title="#" or title="mw:"
+                                               data.query.pages[ 0 ].categoryinfo
+                                       );
+                               } )
+                               .promise( { abort: apiPromise.abort } );
+               },
+
+               /**
+                * Get a list of categories that match a certain prefix.
+                *
+                * E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
+                *
+                * @param {string} prefix Prefix to match.
+                * @return {jQuery.Promise}
+                * @return {Function} return.done
+                * @return {string[]} return.done.categories Matched categories
+                */
+               getCategoriesByPrefix: function ( prefix ) {
+                       // Fetch with allpages to only get categories that have a corresponding description page.
+                       var apiPromise = this.get( {
+                               formatversion: 2,
+                               list: 'allpages',
+                               apprefix: prefix,
+                               apnamespace: mw.config.get( 'wgNamespaceIds' ).category
+                       } );
+
+                       return apiPromise
+                               .then( function ( data ) {
+                                       return data.query.allpages.map( function ( category ) {
+                                               return new mw.Title( category.title ).getMainText();
+                                       } );
+                               } )
+                               .promise( { abort: apiPromise.abort } );
+               },
+
+               /**
+                * Get the categories that a particular page on the wiki belongs to.
+                *
+                * @param {mw.Title|string} title
+                * @return {jQuery.Promise}
+                * @return {Function} return.done
+                * @return {boolean|mw.Title[]} return.done.categories List of category titles or false
+                *  if title was not found.
+                */
+               getCategories: function ( title ) {
+                       var apiPromise = this.get( {
+                               formatversion: 2,
+                               prop: 'categories',
+                               titles: [ String( title ) ]
+                       } );
+
+                       return apiPromise
+                               .then( function ( data ) {
+                                       var page;
+
+                                       if ( !data.query || !data.query.pages ) {
+                                               return false;
+                                       }
+                                       page = data.query.pages[ 0 ];
+                                       if ( !page.categories ) {
+                                               return false;
+                                       }
+                                       return page.categories.map( function ( cat ) {
+                                               return new mw.Title( cat.title );
+                                       } );
+                               } )
+                               .promise( { abort: apiPromise.abort } );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.category
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.edit.js b/resources/src/mediawiki.api.edit.js
new file mode 100644 (file)
index 0000000..21c55c7
--- /dev/null
@@ -0,0 +1,199 @@
+/**
+ * @class mw.Api.plugin.edit
+ */
+( function ( mw, $ ) {
+
+       $.extend( mw.Api.prototype, {
+
+               /**
+                * Post to API with csrf token. If we have no token, get one and try to post.
+                * If we have a cached token try using that, and if it fails, blank out the
+                * cached token and start over.
+                *
+                * @param {Object} params API parameters
+                * @param {Object} [ajaxOptions]
+                * @return {jQuery.Promise} See #post
+                */
+               postWithEditToken: function ( params, ajaxOptions ) {
+                       return this.postWithToken( 'csrf', params, ajaxOptions );
+               },
+
+               /**
+                * API helper to grab a csrf token.
+                *
+                * @return {jQuery.Promise} Received token.
+                */
+               getEditToken: function () {
+                       return this.getToken( 'csrf' );
+               },
+
+               /**
+                * Create a new page.
+                *
+                * Example:
+                *
+                *     new mw.Api().create( 'Sandbox',
+                *         { summary: 'Load sand particles.' },
+                *         'Sand.'
+                *     );
+                *
+                * @since 1.28
+                * @param {mw.Title|string} title Page title
+                * @param {Object} params Edit API parameters
+                * @param {string} params.summary Edit summary
+                * @param {string} content
+                * @return {jQuery.Promise} API response
+                */
+               create: function ( title, params, content ) {
+                       return this.postWithEditToken( $.extend( {
+                               action: 'edit',
+                               title: String( title ),
+                               text: content,
+                               formatversion: '2',
+
+                               // Protect against errors and conflicts
+                               assert: mw.user.isAnon() ? undefined : 'user',
+                               createonly: true
+                       }, params ) ).then( function ( data ) {
+                               return data.edit;
+                       } );
+               },
+
+               /**
+                * Edit an existing page.
+                *
+                * To create a new page, use #create() instead.
+                *
+                * Simple transformation:
+                *
+                *     new mw.Api()
+                *         .edit( 'Sandbox', function ( revision ) {
+                *             return revision.content.replace( 'foo', 'bar' );
+                *         } )
+                *         .then( function () {
+                *             console.log( 'Saved! ');
+                *         } );
+                *
+                * Set save parameters by returning an object instead of a string:
+                *
+                *     new mw.Api().edit(
+                *         'Sandbox',
+                *         function ( revision ) {
+                *             return {
+                *                 text: revision.content.replace( 'foo', 'bar' ),
+                *                 summary: 'Replace "foo" with "bar".',
+                *                 assert: 'bot',
+                *                 minor: true
+                *             };
+                *         }
+                *     )
+                *     .then( function () {
+                *         console.log( 'Saved! ');
+                *     } );
+                *
+                * Transform asynchronously by returning a promise.
+                *
+                *     new mw.Api()
+                *         .edit( 'Sandbox', function ( revision ) {
+                *             return Spelling
+                *                 .corrections( revision.content )
+                *                 .then( function ( report ) {
+                *                     return {
+                *                         text: report.output,
+                *                         summary: report.changelog
+                *                     };
+                *                 } );
+                *         } )
+                *         .then( function () {
+                *             console.log( 'Saved! ');
+                *         } );
+                *
+                * @since 1.28
+                * @param {mw.Title|string} title Page title
+                * @param {Function} transform Callback that prepares the edit
+                * @param {Object} transform.revision Current revision
+                * @param {string} transform.revision.content Current revision content
+                * @param {string|Object|jQuery.Promise} transform.return New content, object with edit
+                *  API parameters, or promise providing one of those.
+                * @return {jQuery.Promise} Edit API response
+                */
+               edit: function ( title, transform ) {
+                       var basetimestamp, curtimestamp,
+                               api = this;
+
+                       title = String( title );
+
+                       return api.get( {
+                               action: 'query',
+                               prop: 'revisions',
+                               rvprop: [ 'content', 'timestamp' ],
+                               titles: [ title ],
+                               formatversion: '2',
+                               curtimestamp: true
+                       } )
+                               .then( function ( data ) {
+                                       var page, revision;
+                                       if ( !data.query || !data.query.pages ) {
+                                               return $.Deferred().reject( 'unknown' );
+                                       }
+                                       page = data.query.pages[ 0 ];
+                                       if ( !page || page.invalid ) {
+                                               return $.Deferred().reject( 'invalidtitle' );
+                                       }
+                                       if ( page.missing ) {
+                                               return $.Deferred().reject( 'nocreate-missing' );
+                                       }
+                                       revision = page.revisions[ 0 ];
+                                       basetimestamp = revision.timestamp;
+                                       curtimestamp = data.curtimestamp;
+                                       return transform( {
+                                               timestamp: revision.timestamp,
+                                               content: revision.content
+                                       } );
+                               } )
+                               .then( function ( params ) {
+                                       var editParams = typeof params === 'object' ? params : { text: String( params ) };
+                                       return api.postWithEditToken( $.extend( {
+                                               action: 'edit',
+                                               title: title,
+                                               formatversion: '2',
+
+                                               // Protect against errors and conflicts
+                                               assert: mw.user.isAnon() ? undefined : 'user',
+                                               basetimestamp: basetimestamp,
+                                               starttimestamp: curtimestamp,
+                                               nocreate: true
+                                       }, editParams ) );
+                               } )
+                               .then( function ( data ) {
+                                       return data.edit;
+                               } );
+               },
+
+               /**
+                * 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 }`
+                * @return {jQuery.Promise}
+                */
+               newSection: function ( title, header, message, additionalParams ) {
+                       return this.postWithEditToken( $.extend( {
+                               action: 'edit',
+                               section: 'new',
+                               title: String( title ),
+                               summary: header,
+                               text: message
+                       }, additionalParams ) );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.edit
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.js b/resources/src/mediawiki.api.js
new file mode 100644 (file)
index 0000000..0038ed8
--- /dev/null
@@ -0,0 +1,506 @@
+( function ( mw, $ ) {
+
+       /**
+        * @class mw.Api
+        */
+
+       /**
+        * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
+        *     `options` to mw.Api constructor.
+        * @property {Object} defaultOptions.parameters Default query parameters for API requests.
+        * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
+        * @property {boolean} defaultOptions.useUS Whether to use U+001F when joining multi-valued
+        *     parameters (since 1.28). Default is true if ajax.url is not set, false otherwise for
+        *     compatibility.
+        * @private
+        */
+       var defaultOptions = {
+                       parameters: {
+                               action: 'query',
+                               format: 'json'
+                       },
+                       ajax: {
+                               url: mw.util.wikiScript( 'api' ),
+                               timeout: 30 * 1000, // 30 seconds
+                               dataType: 'json'
+                       }
+               },
+
+               // Keyed by ajax url and symbolic name for the individual request
+               promises = {};
+
+       function mapLegacyToken( action ) {
+               // Legacy types for backward-compatibility with API action=tokens.
+               var csrfActions = [
+                       'edit',
+                       'delete',
+                       'protect',
+                       'move',
+                       'block',
+                       'unblock',
+                       'email',
+                       'import',
+                       'options'
+               ];
+               if ( csrfActions.indexOf( action ) !== -1 ) {
+                       mw.track( 'mw.deprecate', 'apitoken_' + action );
+                       mw.log.warn( 'Use of the "' + action + '" token is deprecated. Use "csrf" instead.' );
+                       return 'csrf';
+               }
+               return action;
+       }
+
+       // Pre-populate with fake ajax promises to save http requests for tokens
+       // we already have on the page via the user.tokens module (T36733).
+       promises[ defaultOptions.ajax.url ] = {};
+       $.each( mw.user.tokens.get(), function ( key, value ) {
+               // This requires #getToken to use the same key as user.tokens.
+               // Format: token-type + "Token" (eg. csrfToken, patrolToken, watchToken).
+               promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
+                       .resolve( value )
+                       .promise( { abort: function () {} } );
+       } );
+
+       /**
+        * Constructor to create an object to interact with the API of a particular MediaWiki server.
+        * mw.Api objects represent the API of a particular MediaWiki server.
+        *
+        *     var api = new mw.Api();
+        *     api.get( {
+        *         action: 'query',
+        *         meta: 'userinfo'
+        *     } ).done( function ( data ) {
+        *         console.log( data );
+        *     } );
+        *
+        * Since MW 1.25, multiple values for a parameter can be specified using an array:
+        *
+        *     var api = new mw.Api();
+        *     api.get( {
+        *         action: 'query',
+        *         meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo'
+        *     } ).done( function ( data ) {
+        *         console.log( data );
+        *     } );
+        *
+        * Since MW 1.26, boolean values for a parameter can be specified directly. If the value is
+        * `false` or `undefined`, the parameter will be omitted from the request, as required by the API.
+        *
+        * @constructor
+        * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
+        *  each individual request by passing them to #get or #post (or directly #ajax) later on.
+        */
+       mw.Api = function ( options ) {
+               options = options || {};
+
+               // Force a string if we got a mw.Uri object
+               if ( options.ajax && options.ajax.url !== undefined ) {
+                       options.ajax.url = String( options.ajax.url );
+               }
+
+               options = $.extend( { useUS: !options.ajax || !options.ajax.url }, options );
+
+               options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
+               options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
+
+               this.defaults = options;
+               this.requests = [];
+       };
+
+       mw.Api.prototype = {
+               /**
+                * Abort all unfinished requests issued by this Api object.
+                *
+                * @method
+                */
+               abort: function () {
+                       this.requests.forEach( function ( request ) {
+                               if ( request ) {
+                                       request.abort();
+                               }
+                       } );
+               },
+
+               /**
+                * Perform API get request
+                *
+                * @param {Object} parameters
+                * @param {Object} [ajaxOptions]
+                * @return {jQuery.Promise}
+                */
+               get: function ( parameters, ajaxOptions ) {
+                       ajaxOptions = ajaxOptions || {};
+                       ajaxOptions.type = 'GET';
+                       return this.ajax( parameters, ajaxOptions );
+               },
+
+               /**
+                * Perform API post request
+                *
+                * @param {Object} parameters
+                * @param {Object} [ajaxOptions]
+                * @return {jQuery.Promise}
+                */
+               post: function ( parameters, ajaxOptions ) {
+                       ajaxOptions = ajaxOptions || {};
+                       ajaxOptions.type = 'POST';
+                       return this.ajax( parameters, ajaxOptions );
+               },
+
+               /**
+                * Massage parameters from the nice format we accept into a format suitable for the API.
+                *
+                * NOTE: A value of undefined/null in an array will be represented by Array#join()
+                * as the empty string. Should we filter silently? Warn? Leave as-is?
+                *
+                * @private
+                * @param {Object} parameters (modified in-place)
+                * @param {boolean} useUS Whether to use U+001F when joining multi-valued parameters.
+                */
+               preprocessParameters: function ( parameters, useUS ) {
+                       var key;
+                       // Handle common MediaWiki API idioms for passing parameters
+                       for ( key in parameters ) {
+                               // Multiple values are pipe-separated
+                               if ( Array.isArray( parameters[ key ] ) ) {
+                                       if ( !useUS || parameters[ key ].join( '' ).indexOf( '|' ) === -1 ) {
+                                               parameters[ key ] = parameters[ key ].join( '|' );
+                                       } else {
+                                               parameters[ key ] = '\x1f' + parameters[ key ].join( '\x1f' );
+                                       }
+                               } else if ( parameters[ key ] === false || parameters[ key ] === undefined ) {
+                                       // Boolean values are only false when not given at all
+                                       delete parameters[ key ];
+                               }
+                       }
+               },
+
+               /**
+                * Perform the API call.
+                *
+                * @param {Object} parameters
+                * @param {Object} [ajaxOptions]
+                * @return {jQuery.Promise} Done: API response data and the jqXHR object.
+                *  Fail: Error code
+                */
+               ajax: function ( parameters, ajaxOptions ) {
+                       var token, requestIndex,
+                               api = this,
+                               apiDeferred = $.Deferred(),
+                               xhr, key, formData;
+
+                       parameters = $.extend( {}, this.defaults.parameters, parameters );
+                       ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
+
+                       // Ensure that token parameter is last (per [[mw:API:Edit#Token]]).
+                       if ( parameters.token ) {
+                               token = parameters.token;
+                               delete parameters.token;
+                       }
+
+                       this.preprocessParameters( parameters, this.defaults.useUS );
+
+                       // If multipart/form-data has been requested and emulation is possible, emulate it
+                       if (
+                               ajaxOptions.type === 'POST' &&
+                               window.FormData &&
+                               ajaxOptions.contentType === 'multipart/form-data'
+                       ) {
+
+                               formData = new FormData();
+
+                               for ( key in parameters ) {
+                                       formData.append( key, parameters[ key ] );
+                               }
+                               // If we extracted a token parameter, add it back in.
+                               if ( token ) {
+                                       formData.append( 'token', token );
+                               }
+
+                               ajaxOptions.data = formData;
+
+                               // Prevent jQuery from mangling our FormData object
+                               ajaxOptions.processData = false;
+                               // Prevent jQuery from overriding the Content-Type header
+                               ajaxOptions.contentType = false;
+                       } else {
+                               // This works because jQuery accepts data as a query string or as an Object
+                               ajaxOptions.data = $.param( parameters );
+                               // If we extracted a token parameter, add it back in.
+                               if ( token ) {
+                                       ajaxOptions.data += '&token=' + encodeURIComponent( token );
+                               }
+
+                               // Depending on server configuration, MediaWiki may forbid periods in URLs, due to an IE 6
+                               // XSS bug. So let's escape them here. See WebRequest::checkUrlExtension() and T30235.
+                               ajaxOptions.data = ajaxOptions.data.replace( /\./g, '%2E' );
+
+                               if ( ajaxOptions.contentType === 'multipart/form-data' ) {
+                                       // We were asked to emulate but can't, so drop the Content-Type header, otherwise
+                                       // it'll be wrong and the server will fail to decode the POST body
+                                       delete ajaxOptions.contentType;
+                               }
+                       }
+
+                       // Make the AJAX request
+                       xhr = $.ajax( ajaxOptions )
+                               // If AJAX fails, reject API call with error code 'http'
+                               // and details in second argument.
+                               .fail( function ( xhr, textStatus, exception ) {
+                                       apiDeferred.reject( 'http', {
+                                               xhr: xhr,
+                                               textStatus: textStatus,
+                                               exception: exception
+                                       } );
+                               } )
+                               // AJAX success just means "200 OK" response, also check API error codes
+                               .done( function ( result, textStatus, jqXHR ) {
+                                       var code;
+                                       if ( result === undefined || result === null || result === '' ) {
+                                               apiDeferred.reject( 'ok-but-empty',
+                                                       'OK response but empty result (check HTTP headers?)',
+                                                       result,
+                                                       jqXHR
+                                               );
+                                       } else if ( result.error ) {
+                                               // errorformat=bc
+                                               code = result.error.code === undefined ? 'unknown' : result.error.code;
+                                               apiDeferred.reject( code, result, result, jqXHR );
+                                       } else if ( result.errors ) {
+                                               // errorformat!=bc
+                                               code = result.errors[ 0 ].code === undefined ? 'unknown' : result.errors[ 0 ].code;
+                                               apiDeferred.reject( code, result, result, jqXHR );
+                                       } else {
+                                               apiDeferred.resolve( result, jqXHR );
+                                       }
+                               } );
+
+                       requestIndex = this.requests.length;
+                       this.requests.push( xhr );
+                       xhr.always( function () {
+                               api.requests[ requestIndex ] = null;
+                       } );
+                       // Return the Promise
+                       return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
+                               if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
+                                       mw.log( 'mw.Api error: ', code, details );
+                               }
+                       } );
+               },
+
+               /**
+                * Post to API with specified type of token. If we have no token, get one and try to post.
+                * If we have a cached token try using that, and if it fails, blank out the
+                * cached token and start over. For example to change an user option you could do:
+                *
+                *     new mw.Api().postWithToken( 'csrf', {
+                *         action: 'options',
+                *         optionname: 'gender',
+                *         optionvalue: 'female'
+                *     } );
+                *
+                * @param {string} tokenType The name of the token, like options or edit.
+                * @param {Object} params API parameters
+                * @param {Object} [ajaxOptions]
+                * @return {jQuery.Promise} See #post
+                * @since 1.22
+                */
+               postWithToken: function ( tokenType, params, ajaxOptions ) {
+                       var api = this,
+                               abortedPromise = $.Deferred().reject( 'http',
+                                       { textStatus: 'abort', exception: 'abort' } ).promise(),
+                               abortable,
+                               aborted;
+
+                       return api.getToken( tokenType, params.assert ).then( function ( token ) {
+                               params.token = token;
+                               // Request was aborted while token request was running, but we
+                               // don't want to unnecessarily abort token requests, so abort
+                               // a fake request instead
+                               if ( aborted ) {
+                                       return abortedPromise;
+                               }
+
+                               return ( abortable = api.post( params, ajaxOptions ) ).catch(
+                                       // Error handler
+                                       function ( code ) {
+                                               if ( code === 'badtoken' ) {
+                                                       api.badToken( tokenType );
+                                                       // Try again, once
+                                                       params.token = undefined;
+                                                       abortable = null;
+                                                       return api.getToken( tokenType, params.assert ).then( function ( token ) {
+                                                               params.token = token;
+                                                               if ( aborted ) {
+                                                                       return abortedPromise;
+                                                               }
+
+                                                               return ( abortable = api.post( params, ajaxOptions ) );
+                                                       } );
+                                               }
+
+                                               // Let caller handle the error code
+                                               return $.Deferred().rejectWith( this, arguments );
+                                       }
+                               );
+                       } ).promise( { abort: function () {
+                               if ( abortable ) {
+                                       abortable.abort();
+                               } else {
+                                       aborted = true;
+                               }
+                       } } );
+               },
+
+               /**
+                * Get a token for a certain action from the API.
+                *
+                * The assert parameter is only for internal use by #postWithToken.
+                *
+                * @since 1.22
+                * @param {string} type Token type
+                * @param {string} [assert]
+                * @return {jQuery.Promise} Received token.
+                */
+               getToken: function ( type, assert ) {
+                       var apiPromise, promiseGroup, d, reject;
+                       type = mapLegacyToken( type );
+                       promiseGroup = promises[ this.defaults.ajax.url ];
+                       d = promiseGroup && promiseGroup[ type + 'Token' ];
+
+                       if ( !promiseGroup ) {
+                               promiseGroup = promises[ this.defaults.ajax.url ] = {};
+                       }
+
+                       if ( !d ) {
+                               apiPromise = this.get( {
+                                       action: 'query',
+                                       meta: 'tokens',
+                                       type: type,
+                                       assert: assert
+                               } );
+                               reject = function () {
+                                       // Clear promise. Do not cache errors.
+                                       delete promiseGroup[ type + 'Token' ];
+
+                                       // Let caller handle the error code
+                                       return $.Deferred().rejectWith( this, arguments );
+                               };
+                               d = apiPromise
+                                       .then( function ( res ) {
+                                               if ( !res.query ) {
+                                                       return reject( 'query-missing', res );
+                                               }
+                                               // If token type is unknown, it is omitted from the response
+                                               if ( !res.query.tokens[ type + 'token' ] ) {
+                                                       return $.Deferred().reject( 'token-missing', res );
+                                               }
+                                               return res.query.tokens[ type + 'token' ];
+                                       }, reject )
+                                       // Attach abort handler
+                                       .promise( { abort: apiPromise.abort } );
+
+                               // Store deferred now so that we can use it again even if it isn't ready yet
+                               promiseGroup[ type + 'Token' ] = d;
+                       }
+
+                       return d;
+               },
+
+               /**
+                * Indicate that the cached token for a certain action of the API is bad.
+                *
+                * Call this if you get a 'badtoken' error when using the token returned by #getToken.
+                * You may also want to use #postWithToken instead, which invalidates bad cached tokens
+                * automatically.
+                *
+                * @param {string} type Token type
+                * @since 1.26
+                */
+               badToken: function ( type ) {
+                       var promiseGroup = promises[ this.defaults.ajax.url ];
+
+                       type = mapLegacyToken( type );
+                       if ( promiseGroup ) {
+                               delete promiseGroup[ type + 'Token' ];
+                       }
+               }
+       };
+
+       /**
+        * @static
+        * @property {Array}
+        * Very incomplete and outdated list of errors we might receive from the API. Do not use.
+        * @deprecated since 1.29
+        */
+       mw.Api.errors = [
+               // occurs when POST aborted
+               // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
+               'ok-but-empty',
+
+               // timeout
+               'timeout',
+
+               // really a warning, but we treat it like an error
+               'duplicate',
+               'duplicate-archive',
+
+               // upload succeeded, but no image info.
+               // this is probably impossible, but might as well check for it
+               'noimageinfo',
+               // remote errors, defined in API
+               'uploaddisabled',
+               'nomodule',
+               'mustbeposted',
+               'badaccess-groups',
+               'missingresult',
+               'missingparam',
+               'invalid-file-key',
+               'copyuploaddisabled',
+               'mustbeloggedin',
+               'empty-file',
+               'file-too-large',
+               'filetype-missing',
+               'filetype-banned',
+               'filetype-banned-type',
+               'filename-tooshort',
+               'illegal-filename',
+               'verification-error',
+               'hookaborted',
+               'unknown-error',
+               'internal-error',
+               'overwrite',
+               'badtoken',
+               'fetchfileerror',
+               'fileexists-shared-forbidden',
+               'invalidtitle',
+               'notloggedin',
+               'autoblocked',
+               'blocked',
+
+               // Stash-specific errors - expanded
+               'stashfailed',
+               'stasherror',
+               'stashedfilenotfound',
+               'stashpathinvalid',
+               'stashfilestorage',
+               'stashzerolength',
+               'stashnotloggedin',
+               'stashwrongowner',
+               'stashnosuchfilekey'
+       ];
+       mw.log.deprecate( mw.Api, 'errors', mw.Api.errors, null, 'mw.Api.errors' );
+
+       /**
+        * @static
+        * @property {Array}
+        * Very incomplete and outdated list of warnings we might receive from the API. Do not use.
+        * @deprecated since 1.29
+        */
+       mw.Api.warnings = [
+               'duplicate',
+               'exists'
+       ];
+       mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings, null, 'mw.Api.warnings' );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.login.js b/resources/src/mediawiki.api.login.js
new file mode 100644 (file)
index 0000000..2b709aa
--- /dev/null
@@ -0,0 +1,60 @@
+/**
+ * Make the two-step login easier.
+ *
+ * @author Niklas Laxström
+ * @class mw.Api.plugin.login
+ * @since 1.22
+ */
+( function ( mw, $ ) {
+       'use strict';
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * @param {string} username
+                * @param {string} password
+                * @return {jQuery.Promise} See mw.Api#post
+                */
+               login: function ( username, password ) {
+                       var params, apiPromise, innerPromise,
+                               api = this;
+
+                       params = {
+                               action: 'login',
+                               lgname: username,
+                               lgpassword: password
+                       };
+
+                       apiPromise = api.post( params );
+
+                       return apiPromise
+                               .then( function ( data ) {
+                                       params.lgtoken = data.login.token;
+                                       innerPromise = api.post( params )
+                                               .then( function ( data ) {
+                                                       var code;
+                                                       if ( data.login.result !== 'Success' ) {
+                                                               // Set proper error code whenever possible
+                                                               code = data.error && data.error.code || 'unknown';
+                                                               return $.Deferred().reject( code, data );
+                                                       }
+                                                       return data;
+                                               } );
+                                       return innerPromise;
+                               } )
+                               .promise( {
+                                       abort: function () {
+                                               apiPromise.abort();
+                                               if ( innerPromise ) {
+                                                       innerPromise.abort();
+                                               }
+                                       }
+                               } );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.login
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.messages.js b/resources/src/mediawiki.api.messages.js
new file mode 100644 (file)
index 0000000..688f0b2
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Allows to retrieve a specific or a set of
+ * messages to be added to mw.messages and returned
+ * by the Api.
+ *
+ * @class mw.Api.plugin.messages
+ * @since 1.27
+ */
+( function ( mw, $ ) {
+       'use strict';
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * Get a set of messages.
+                *
+                * @param {Array} messages Messages to retrieve
+                * @param {Object} [options] Additional parameters for the API call
+                * @return {jQuery.Promise}
+                */
+               getMessages: function ( messages, options ) {
+                       options = options || {};
+                       return this.get( $.extend( {
+                               action: 'query',
+                               meta: 'allmessages',
+                               ammessages: messages,
+                               amlang: mw.config.get( 'wgUserLanguage' ),
+                               formatversion: 2
+                       }, options ) ).then( function ( data ) {
+                               var result = {};
+
+                               data.query.allmessages.forEach( function ( obj ) {
+                                       if ( !obj.missing ) {
+                                               result[ obj.name ] = obj.content;
+                                       }
+                               } );
+
+                               return result;
+                       } );
+               },
+
+               /**
+                * Loads a set of messages and add them to mw.messages.
+                *
+                * @param {Array} messages Messages to retrieve
+                * @param {Object} [options] Additional parameters for the API call
+                * @return {jQuery.Promise}
+                */
+               loadMessages: function ( messages, options ) {
+                       return this.getMessages( messages, options ).then( $.proxy( mw.messages, 'set' ) );
+               },
+
+               /**
+                * Loads a set of messages and add them to mw.messages. Only messages that are not already known
+                * are loaded. If all messages are known, the returned promise is resolved immediately.
+                *
+                * @param {Array} messages Messages to retrieve
+                * @param {Object} [options] Additional parameters for the API call
+                * @return {jQuery.Promise}
+                */
+               loadMessagesIfMissing: function ( messages, options ) {
+                       var missing = messages.filter( function ( msg ) {
+                               return !mw.message( msg ).exists();
+                       } );
+
+                       if ( missing.length === 0 ) {
+                               return $.Deferred().resolve();
+                       }
+
+                       return this.getMessages( missing, options ).then( $.proxy( mw.messages, 'set' ) );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.messages
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.options.js b/resources/src/mediawiki.api.options.js
new file mode 100644 (file)
index 0000000..4930c4f
--- /dev/null
@@ -0,0 +1,102 @@
+/**
+ * @class mw.Api.plugin.options
+ */
+( function ( mw, $ ) {
+
+       $.extend( mw.Api.prototype, {
+
+               /**
+                * Asynchronously save the value of a single user option using the API. See #saveOptions.
+                *
+                * @param {string} name
+                * @param {string|null} value
+                * @return {jQuery.Promise}
+                */
+               saveOption: function ( name, value ) {
+                       var param = {};
+                       param[ name ] = value;
+                       return this.saveOptions( param );
+               },
+
+               /**
+                * Asynchronously save the values of user options using the API.
+                *
+                * If a value of `null` is provided, the given option will be reset to the default value.
+                *
+                * Any warnings returned by the API, including warnings about invalid option names or values,
+                * are ignored. However, do not rely on this behavior.
+                *
+                * If necessary, the options will be saved using several sequential API requests. Only one promise
+                * is always returned that will be resolved when all requests complete.
+                *
+                * @param {Object} options Options as a `{ name: value, â€¦ }` object
+                * @return {jQuery.Promise}
+                */
+               saveOptions: function ( options ) {
+                       var name, value, bundleable,
+                               grouped = [],
+                               promise = $.Deferred().resolve();
+
+                       for ( name in options ) {
+                               value = options[ name ] === null ? null : String( options[ name ] );
+
+                               // Can we bundle this option, or does it need a separate request?
+                               if ( this.defaults.useUS ) {
+                                       bundleable = name.indexOf( '=' ) === -1;
+                               } else {
+                                       bundleable =
+                                               ( value === null || value.indexOf( '|' ) === -1 ) &&
+                                               ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
+                               }
+
+                               if ( bundleable ) {
+                                       if ( value !== null ) {
+                                               grouped.push( name + '=' + value );
+                                       } else {
+                                               // Omitting value resets the option
+                                               grouped.push( name );
+                                       }
+                               } else {
+                                       if ( value !== null ) {
+                                               promise = promise.then( function ( name, value ) {
+                                                       return this.postWithToken( 'csrf', {
+                                                               formatversion: 2,
+                                                               action: 'options',
+                                                               optionname: name,
+                                                               optionvalue: value
+                                                       } );
+                                               }.bind( this, name, value ) );
+                                       } else {
+                                               // Omitting value resets the option
+                                               promise = promise.then( function ( name ) {
+                                                       return this.postWithToken( 'csrf', {
+                                                               formatversion: 2,
+                                                               action: 'options',
+                                                               optionname: name
+                                                       } );
+                                               }.bind( this, name ) );
+                                       }
+                               }
+                       }
+
+                       if ( grouped.length ) {
+                               promise = promise.then( function () {
+                                       return this.postWithToken( 'csrf', {
+                                               formatversion: 2,
+                                               action: 'options',
+                                               change: grouped
+                                       } );
+                               }.bind( this ) );
+                       }
+
+                       return promise;
+               }
+
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.options
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.parse.js b/resources/src/mediawiki.api.parse.js
new file mode 100644 (file)
index 0000000..f38e88b
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * @class mw.Api.plugin.parse
+ */
+( function ( mw, $ ) {
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * Convenience method for 'action=parse'.
+                *
+                * @param {string|mw.Title} content Content to parse, either as a wikitext string or
+                *   a mw.Title.
+                * @param {Object} additionalParams Parameters object to set custom settings, e.g.
+                *   redirects, sectionpreview.  prop should not be overridden.
+                * @return {jQuery.Promise}
+                * @return {Function} return.done
+                * @return {string} return.done.data Parsed HTML of `wikitext`.
+                */
+               parse: function ( content, additionalParams ) {
+                       var apiPromise,
+                               config = $.extend( {
+                                       formatversion: 2,
+                                       action: 'parse',
+                                       contentmodel: 'wikitext'
+                               }, additionalParams );
+
+                       if ( mw.Title && content instanceof mw.Title ) {
+                               // Parse existing page
+                               config.page = content.getPrefixedDb();
+                       } else {
+                               // Parse wikitext from input
+                               config.text = String( content );
+                       }
+
+                       apiPromise = this.get( config );
+
+                       return apiPromise
+                               .then( function ( data ) {
+                                       return data.parse.text;
+                               } )
+                               .promise( { abort: apiPromise.abort } );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.parse
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.rollback.js b/resources/src/mediawiki.api.rollback.js
new file mode 100644 (file)
index 0000000..322143d
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * @class mw.Api.plugin.rollback
+ * @since 1.28
+ */
+( function ( mw, $ ) {
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * Convenience method for `action=rollback`.
+                *
+                * @param {string|mw.Title} page
+                * @param {string} user
+                * @param {Object} [params] Additional parameters
+                * @return {jQuery.Promise}
+                */
+               rollback: function ( page, user, params ) {
+                       return this.postWithToken( 'rollback', $.extend( {
+                               action: 'rollback',
+                               title: String( page ),
+                               user: user,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       }, params ) ).then( function ( data ) {
+                               return data.rollback;
+                       } );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.rollback
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.upload.js b/resources/src/mediawiki.api.upload.js
new file mode 100644 (file)
index 0000000..29bd59a
--- /dev/null
@@ -0,0 +1,668 @@
+/**
+ * Provides an interface for uploading files to MediaWiki.
+ *
+ * @class mw.Api.plugin.upload
+ * @singleton
+ */
+( function ( mw, $ ) {
+       var nonce = 0,
+               fieldsAllowed = {
+                       stash: true,
+                       filekey: true,
+                       filename: true,
+                       comment: true,
+                       text: true,
+                       watchlist: true,
+                       ignorewarnings: true,
+                       chunk: true,
+                       offset: true,
+                       filesize: true,
+                       async: true
+               };
+
+       /**
+        * Get nonce for iframe IDs on the page.
+        *
+        * @private
+        * @return {number}
+        */
+       function getNonce() {
+               return nonce++;
+       }
+
+       /**
+        * Given a non-empty object, return one of its keys.
+        *
+        * @private
+        * @param {Object} obj
+        * @return {string}
+        */
+       function getFirstKey( obj ) {
+               var key;
+               for ( key in obj ) {
+                       if ( obj.hasOwnProperty( key ) ) {
+                               return key;
+                       }
+               }
+       }
+
+       /**
+        * Get new iframe object for an upload.
+        *
+        * @private
+        * @param {string} id
+        * @return {HTMLIframeElement}
+        */
+       function getNewIframe( id ) {
+               var frame = document.createElement( 'iframe' );
+               frame.id = id;
+               frame.name = id;
+               return frame;
+       }
+
+       /**
+        * Shortcut for getting hidden inputs
+        *
+        * @private
+        * @param {string} name
+        * @param {string} val
+        * @return {jQuery}
+        */
+       function getHiddenInput( name, val ) {
+               return $( '<input>' ).attr( 'type', 'hidden' )
+                       .attr( 'name', name )
+                       .val( val );
+       }
+
+       /**
+        * Process the result of the form submission, returned to an iframe.
+        * This is the iframe's onload event.
+        *
+        * @param {HTMLIframeElement} iframe Iframe to extract result from
+        * @return {Object} Response from the server. The return value may or may
+        *   not be an XMLDocument, this code was copied from elsewhere, so if you
+        *   see an unexpected return type, please file a bug.
+        */
+       function processIframeResult( iframe ) {
+               var json,
+                       doc = iframe.contentDocument || frames[ iframe.id ].document;
+
+               if ( doc.XMLDocument ) {
+                       // The response is a document property in IE
+                       return doc.XMLDocument;
+               }
+
+               if ( doc.body ) {
+                       // Get the json string
+                       // We're actually searching through an HTML doc here --
+                       // according to mdale we need to do this
+                       // because IE does not load JSON properly in an iframe
+                       json = $( doc.body ).find( 'pre' ).text();
+
+                       return JSON.parse( json );
+               }
+
+               // Response is a xml document
+               return doc;
+       }
+
+       function formDataAvailable() {
+               return window.FormData !== undefined &&
+                       window.File !== undefined &&
+                       window.File.prototype.slice !== undefined;
+       }
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * Upload a file to MediaWiki.
+                *
+                * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
+                * iframe if it doesn't.
+                *
+                * Caveats of iframe upload:
+                * - The returned jQuery.Promise will not receive `progress` notifications during the upload
+                * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
+                * - You must pass a HTMLInputElement and not a File for it to be possible
+                *
+                * @param {HTMLInputElement|File|Blob} file HTML input type=file element with a file already inside
+                *  of it, or a File object.
+                * @param {Object} data Other upload options, see action=upload API docs for more
+                * @return {jQuery.Promise}
+                */
+               upload: function ( file, data ) {
+                       var isFileInput, canUseFormData;
+
+                       isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
+
+                       if ( formDataAvailable() && isFileInput && file.files ) {
+                               file = file.files[ 0 ];
+                       }
+
+                       if ( !file ) {
+                               throw new Error( 'No file' );
+                       }
+
+                       // Blobs are allowed in formdata uploads, it turns out
+                       canUseFormData = formDataAvailable() && ( file instanceof window.File || file instanceof window.Blob );
+
+                       if ( !isFileInput && !canUseFormData ) {
+                               throw new Error( 'Unsupported argument type passed to mw.Api.upload' );
+                       }
+
+                       if ( canUseFormData ) {
+                               return this.uploadWithFormData( file, data );
+                       }
+
+                       return this.uploadWithIframe( file, data );
+               },
+
+               /**
+                * Upload a file to MediaWiki with an iframe and a form.
+                *
+                * This method is necessary for browsers without the File/FormData
+                * APIs, and continues to work in browsers with those APIs.
+                *
+                * The rough sketch of how this method works is as follows:
+                * 1. An iframe is loaded with no content.
+                * 2. A form is submitted with the passed-in file input and some extras.
+                * 3. The MediaWiki API receives that form data, and sends back a response.
+                * 4. The response is sent to the iframe, because we set target=(iframe id)
+                * 5. The response is parsed out of the iframe's document, and passed back
+                *    through the promise.
+                *
+                * @private
+                * @param {HTMLInputElement} file The file input with a file in it.
+                * @param {Object} data Other upload options, see action=upload API docs for more
+                * @return {jQuery.Promise}
+                */
+               uploadWithIframe: function ( file, data ) {
+                       var key,
+                               tokenPromise = $.Deferred(),
+                               api = this,
+                               deferred = $.Deferred(),
+                               nonce = getNonce(),
+                               id = 'uploadframe-' + nonce,
+                               $form = $( '<form>' ),
+                               iframe = getNewIframe( id ),
+                               $iframe = $( iframe );
+
+                       for ( key in data ) {
+                               if ( !fieldsAllowed[ key ] ) {
+                                       delete data[ key ];
+                               }
+                       }
+
+                       data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+                       $form.addClass( 'mw-api-upload-form' );
+
+                       $form.css( 'display', 'none' )
+                               .attr( {
+                                       action: this.defaults.ajax.url,
+                                       method: 'POST',
+                                       target: id,
+                                       enctype: 'multipart/form-data'
+                               } );
+
+                       $iframe.one( 'load', function () {
+                               $iframe.one( 'load', function () {
+                                       var result = processIframeResult( iframe );
+                                       deferred.notify( 1 );
+
+                                       if ( !result ) {
+                                               deferred.reject( 'ok-but-empty', 'No response from API on upload attempt.' );
+                                       } else if ( result.error ) {
+                                               if ( result.error.code === 'badtoken' ) {
+                                                       api.badToken( 'csrf' );
+                                               }
+
+                                               deferred.reject( result.error.code, result );
+                                       } else if ( result.upload && result.upload.warnings ) {
+                                               deferred.reject( getFirstKey( result.upload.warnings ), result );
+                                       } else {
+                                               deferred.resolve( result );
+                                       }
+                               } );
+                               tokenPromise.done( function () {
+                                       $form.submit();
+                               } );
+                       } );
+
+                       $iframe.on( 'error', function ( error ) {
+                               deferred.reject( 'http', error );
+                       } );
+
+                       $iframe.prop( 'src', 'about:blank' ).hide();
+
+                       file.name = 'file';
+
+                       $.each( data, function ( key, val ) {
+                               $form.append( getHiddenInput( key, val ) );
+                       } );
+
+                       if ( !data.filename && !data.stash ) {
+                               throw new Error( 'Filename not included in file data.' );
+                       }
+
+                       if ( this.needToken() ) {
+                               this.getEditToken().then( function ( token ) {
+                                       $form.append( getHiddenInput( 'token', token ) );
+                                       tokenPromise.resolve();
+                               }, tokenPromise.reject );
+                       } else {
+                               tokenPromise.resolve();
+                       }
+
+                       $( 'body' ).append( $form, $iframe );
+
+                       deferred.always( function () {
+                               $form.remove();
+                               $iframe.remove();
+                       } );
+
+                       return deferred.promise();
+               },
+
+               /**
+                * Uploads a file using the FormData API.
+                *
+                * @private
+                * @param {File} file
+                * @param {Object} data Other upload options, see action=upload API docs for more
+                * @return {jQuery.Promise}
+                */
+               uploadWithFormData: function ( file, data ) {
+                       var key, request,
+                               deferred = $.Deferred();
+
+                       for ( key in data ) {
+                               if ( !fieldsAllowed[ key ] ) {
+                                       delete data[ key ];
+                               }
+                       }
+
+                       data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+                       if ( !data.chunk ) {
+                               data.file = file;
+                       }
+
+                       if ( !data.filename && !data.stash ) {
+                               throw new Error( 'Filename not included in file data.' );
+                       }
+
+                       // Use this.postWithEditToken() or this.post()
+                       request = this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
+                               // Use FormData (if we got here, we know that it's available)
+                               contentType: 'multipart/form-data',
+                               // No timeout (default from mw.Api is 30 seconds)
+                               timeout: 0,
+                               // Provide upload progress notifications
+                               xhr: function () {
+                                       var xhr = $.ajaxSettings.xhr();
+                                       if ( xhr.upload ) {
+                                               // need to bind this event before we open the connection (see note at
+                                               // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
+                                               xhr.upload.addEventListener( 'progress', function ( ev ) {
+                                                       if ( ev.lengthComputable ) {
+                                                               deferred.notify( ev.loaded / ev.total );
+                                                       }
+                                               } );
+                                       }
+                                       return xhr;
+                               }
+                       } )
+                               .done( function ( result ) {
+                                       deferred.notify( 1 );
+                                       if ( result.upload && result.upload.warnings ) {
+                                               deferred.reject( getFirstKey( result.upload.warnings ), result );
+                                       } else {
+                                               deferred.resolve( result );
+                                       }
+                               } )
+                               .fail( function ( errorCode, result ) {
+                                       deferred.notify( 1 );
+                                       deferred.reject( errorCode, result );
+                               } );
+
+                       return deferred.promise( { abort: request.abort } );
+               },
+
+               /**
+                * Upload a file in several chunks.
+                *
+                * @param {File} file
+                * @param {Object} data Other upload options, see action=upload API docs for more
+                * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
+                * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
+                * @return {jQuery.Promise}
+                */
+               chunkedUpload: function ( file, data, chunkSize, chunkRetries ) {
+                       var start, end, promise, next, active,
+                               deferred = $.Deferred();
+
+                       chunkSize = chunkSize === undefined ? 5 * 1024 * 1024 : chunkSize;
+                       chunkRetries = chunkRetries === undefined ? 1 : chunkRetries;
+
+                       if ( !data.filename ) {
+                               throw new Error( 'Filename not included in file data.' );
+                       }
+
+                       // Submit first chunk to get the filekey
+                       active = promise = this.uploadChunk( file, data, 0, chunkSize, '', chunkRetries )
+                               .done( chunkSize >= file.size ? deferred.resolve : null )
+                               .fail( deferred.reject )
+                               .progress( deferred.notify );
+
+                       // Now iteratively submit the rest of the chunks
+                       for ( start = chunkSize; start < file.size; start += chunkSize ) {
+                               end = Math.min( start + chunkSize, file.size );
+                               next = $.Deferred();
+
+                               // We could simply chain one this.uploadChunk after another with
+                               // .then(), but then we'd hit an `Uncaught RangeError: Maximum
+                               // call stack size exceeded` at as low as 1024 calls in Firefox
+                               // 47. This'll work around it, but comes with the drawback of
+                               // having to properly relay the results to the returned promise.
+                               // eslint-disable-next-line no-loop-func
+                               promise.done( function ( start, end, next, result ) {
+                                       var filekey = result.upload.filekey;
+                                       active = this.uploadChunk( file, data, start, end, filekey, chunkRetries )
+                                               .done( end === file.size ? deferred.resolve : next.resolve )
+                                               .fail( deferred.reject )
+                                               .progress( deferred.notify );
+                               // start, end & next must be bound to closure, or they'd have
+                               // changed by the time the promises are resolved
+                               }.bind( this, start, end, next ) );
+
+                               promise = next;
+                       }
+
+                       return deferred.promise( { abort: active.abort } );
+               },
+
+               /**
+                * Uploads 1 chunk.
+                *
+                * @private
+                * @param {File} file
+                * @param {Object} data Other upload options, see action=upload API docs for more
+                * @param {number} start Chunk start position
+                * @param {number} end Chunk end position
+                * @param {string} [filekey] File key, for follow-up chunks
+                * @param {number} [retries] Amount of times to retry request
+                * @return {jQuery.Promise}
+                */
+               uploadChunk: function ( file, data, start, end, filekey, retries ) {
+                       var upload,
+                               api = this,
+                               chunk = this.slice( file, start, end );
+
+                       // When uploading in chunks, we're going to be issuing a lot more
+                       // requests and there's always a chance of 1 getting dropped.
+                       // In such case, it could be useful to try again: a network hickup
+                       // doesn't necessarily have to result in upload failure...
+                       retries = retries === undefined ? 1 : retries;
+
+                       data.filesize = file.size;
+                       data.chunk = chunk;
+                       data.offset = start;
+
+                       // filekey must only be added when uploading follow-up chunks; the
+                       // first chunk should never have a filekey (it'll be generated)
+                       if ( filekey && start !== 0 ) {
+                               data.filekey = filekey;
+                       }
+
+                       upload = this.uploadWithFormData( file, data );
+                       return upload.then(
+                               null,
+                               function ( code, result ) {
+                                       var retry;
+
+                                       // uploadWithFormData will reject uploads with warnings, but
+                                       // these warnings could be "harmless" or recovered from
+                                       // (e.g. exists-normalized, when it'll be renamed later)
+                                       // In the case of (only) a warning, we still want to
+                                       // continue the chunked upload until it completes: then
+                                       // reject it - at least it's been fully uploaded by then and
+                                       // failure handlers have a complete result object (including
+                                       // possibly more warnings, e.g. duplicate)
+                                       // This matches .upload, which also completes the upload.
+                                       if ( result.upload && result.upload.warnings && code in result.upload.warnings ) {
+                                               if ( end === file.size ) {
+                                                       // uploaded last chunk = reject with result data
+                                                       return $.Deferred().reject( code, result );
+                                               } else {
+                                                       // still uploading chunks = resolve to keep going
+                                                       return $.Deferred().resolve( result );
+                                               }
+                                       }
+
+                                       if ( retries === 0 ) {
+                                               return $.Deferred().reject( code, result );
+                                       }
+
+                                       // If the call flat out failed, we may want to try again...
+                                       retry = api.uploadChunk.bind( this, file, data, start, end, filekey, retries - 1 );
+                                       return api.retry( code, result, retry );
+                               },
+                               function ( fraction ) {
+                                       // Since we're only uploading small parts of a file, we
+                                       // need to adjust the reported progress to reflect where
+                                       // we actually are in the combined upload
+                                       return ( start + fraction * ( end - start ) ) / file.size;
+                               }
+                       ).promise( { abort: upload.abort } );
+               },
+
+               /**
+                * Launch the upload anew if it failed because of network issues.
+                *
+                * @private
+                * @param {string} code Error code
+                * @param {Object} result API result
+                * @param {Function} callable
+                * @return {jQuery.Promise}
+                */
+               retry: function ( code, result, callable ) {
+                       var uploadPromise,
+                               retryTimer,
+                               deferred = $.Deferred(),
+                               // Wrap around the callable, so that once it completes, it'll
+                               // resolve/reject the promise we'll return
+                               retry = function () {
+                                       uploadPromise = callable();
+                                       uploadPromise.then( deferred.resolve, deferred.reject );
+                               };
+
+                       // Don't retry if the request failed because we aborted it (or if
+                       // it's another kind of request failure)
+                       if ( code !== 'http' || result.textStatus === 'abort' ) {
+                               return deferred.reject( code, result );
+                       }
+
+                       retryTimer = setTimeout( retry, 1000 );
+                       return deferred.promise( { abort: function () {
+                               // Clear the scheduled upload, or abort if already in flight
+                               if ( retryTimer ) {
+                                       clearTimeout( retryTimer );
+                               }
+                               if ( uploadPromise.abort ) {
+                                       uploadPromise.abort();
+                               }
+                       } } );
+               },
+
+               /**
+                * Slice a chunk out of a File object.
+                *
+                * @private
+                * @param {File} file
+                * @param {number} start
+                * @param {number} stop
+                * @return {Blob}
+                */
+               slice: function ( file, start, stop ) {
+                       if ( file.mozSlice ) {
+                               // FF <= 12
+                               return file.mozSlice( start, stop, file.type );
+                       } else if ( file.webkitSlice ) {
+                               // Chrome <= 20
+                               return file.webkitSlice( start, stop, file.type );
+                       } else {
+                               // On really old browser versions (before slice was prefixed),
+                               // slice() would take (start, length) instead of (start, end)
+                               // We'll ignore that here...
+                               return file.slice( start, stop, file.type );
+                       }
+               },
+
+               /**
+                * This function will handle how uploads to stash (via uploadToStash or
+                * chunkedUploadToStash) are resolved/rejected.
+                *
+                * After a successful stash, it'll resolve with a callback which, when
+                * called, will finalize the upload in stash (with the given data, or
+                * with additional/conflicting data)
+                *
+                * A failed stash can still be recovered from as long as 'filekey' is
+                * present. In that case, it'll also resolve with the callback to
+                * finalize the upload (all warnings are then ignored.)
+                * Otherwise, it'll just reject as you'd expect, with code & result.
+                *
+                * @private
+                * @param {jQuery.Promise} uploadPromise
+                * @param {Object} data
+                * @return {jQuery.Promise}
+                * @return {Function} return.finishUpload Call this function to finish the upload.
+                * @return {Object} return.finishUpload.data Additional data for the upload.
+                * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
+                * @return {Object} return.finishUpload.return.data API return value for the final upload
+                */
+               finishUploadToStash: function ( uploadPromise, data ) {
+                       var filekey,
+                               api = this;
+
+                       function finishUpload( moreData ) {
+                               return api.uploadFromStash( filekey, $.extend( data, moreData ) );
+                       }
+
+                       return uploadPromise.then(
+                               function ( result ) {
+                                       filekey = result.upload.filekey;
+                                       return finishUpload;
+                               },
+                               function ( errorCode, result ) {
+                                       if ( result && result.upload && result.upload.filekey ) {
+                                               // Ignore any warnings if 'filekey' was returned, that's all we care about
+                                               filekey = result.upload.filekey;
+                                               return $.Deferred().resolve( finishUpload );
+                                       }
+                                       return $.Deferred().reject( errorCode, result );
+                               }
+                       );
+               },
+
+               /**
+                * Upload a file to the stash.
+                *
+                * This function will return a promise, which when resolved, will pass back a function
+                * to finish the stash upload. You can call that function with an argument containing
+                * more, or conflicting, data to pass to the server. For example:
+                *
+                *     // upload a file to the stash with a placeholder filename
+                *     api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
+                *         // finish is now the function we can use to finalize the upload
+                *         // pass it a new filename from user input to override the initial value
+                *         finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
+                *             // the upload is complete, data holds the API response
+                *         } );
+                *     } );
+                *
+                * @param {File|HTMLInputElement} file
+                * @param {Object} [data]
+                * @return {jQuery.Promise}
+                * @return {Function} return.finishUpload Call this function to finish the upload.
+                * @return {Object} return.finishUpload.data Additional data for the upload.
+                * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
+                * @return {Object} return.finishUpload.return.data API return value for the final upload
+                */
+               uploadToStash: function ( file, data ) {
+                       var promise;
+
+                       if ( !data.filename ) {
+                               throw new Error( 'Filename not included in file data.' );
+                       }
+
+                       promise = this.upload( file, { stash: true, filename: data.filename } );
+
+                       return this.finishUploadToStash( promise, data );
+               },
+
+               /**
+                * Upload a file to the stash, in chunks.
+                *
+                * This function will return a promise, which when resolved, will pass back a function
+                * to finish the stash upload.
+                *
+                * @see #method-uploadToStash
+                * @param {File|HTMLInputElement} file
+                * @param {Object} [data]
+                * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
+                * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
+                * @return {jQuery.Promise}
+                * @return {Function} return.finishUpload Call this function to finish the upload.
+                * @return {Object} return.finishUpload.data Additional data for the upload.
+                * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
+                * @return {Object} return.finishUpload.return.data API return value for the final upload
+                */
+               chunkedUploadToStash: function ( file, data, chunkSize, chunkRetries ) {
+                       var promise;
+
+                       if ( !data.filename ) {
+                               throw new Error( 'Filename not included in file data.' );
+                       }
+
+                       promise = this.chunkedUpload(
+                               file,
+                               { stash: true, filename: data.filename },
+                               chunkSize,
+                               chunkRetries
+                       );
+
+                       return this.finishUploadToStash( promise, data );
+               },
+
+               /**
+                * Finish an upload in the stash.
+                *
+                * @param {string} filekey
+                * @param {Object} data
+                * @return {jQuery.Promise}
+                */
+               uploadFromStash: function ( filekey, data ) {
+                       data.filekey = filekey;
+                       data.action = 'upload';
+                       data.format = 'json';
+
+                       if ( !data.filename ) {
+                               throw new Error( 'Filename not included in file data.' );
+                       }
+
+                       return this.postWithEditToken( data ).then( function ( result ) {
+                               if ( result.upload && result.upload.warnings ) {
+                                       return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
+                               }
+                               return result;
+                       } );
+               },
+
+               needToken: function () {
+                       return true;
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.upload
+        */
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.user.js b/resources/src/mediawiki.api.user.js
new file mode 100644 (file)
index 0000000..e7b4b6d
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * @class mw.Api.plugin.user
+ * @since 1.27
+ */
+( function ( mw, $ ) {
+
+       $.extend( mw.Api.prototype, {
+
+               /**
+                * Get the current user's groups and rights.
+                *
+                * @return {jQuery.Promise}
+                * @return {Function} return.done
+                * @return {Object} return.done.userInfo
+                * @return {string[]} return.done.userInfo.groups User groups that the current user belongs to
+                * @return {string[]} return.done.userInfo.rights Current user's rights
+                */
+               getUserInfo: function () {
+                       return this.get( {
+                               action: 'query',
+                               meta: 'userinfo',
+                               uiprop: [ 'groups', 'rights' ]
+                       } ).then( function ( data ) {
+                               if ( data.query && data.query.userinfo ) {
+                                       return data.query.userinfo;
+                               }
+                               return $.Deferred().reject().promise();
+                       } );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.user
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api.watch.js b/resources/src/mediawiki.api.watch.js
new file mode 100644 (file)
index 0000000..025c111
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * @class mw.Api.plugin.watch
+ * @since 1.19
+ */
+( function ( mw, $ ) {
+
+       /**
+        * @private
+        * @static
+        * @context mw.Api
+        *
+        * @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
+        *  array thereof. If an array is passed, the return value passed to the promise will also be an
+        *  array of appropriate objects.
+        * @param {Object} [addParams]
+        * @return {jQuery.Promise}
+        * @return {Function} return.done
+        * @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
+        *  parameter)
+        * @return {string} return.done.watch.title Full pagename
+        * @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
+        */
+       function doWatchInternal( pages, addParams ) {
+               // XXX: Parameter addParams is undocumented because we inherit this
+               // documentation in the public method...
+               var apiPromise = this.postWithToken( 'watch',
+                       $.extend(
+                               {
+                                       formatversion: 2,
+                                       action: 'watch',
+                                       titles: Array.isArray( pages ) ? pages : String( pages )
+                               },
+                               addParams
+                       )
+               );
+
+               return apiPromise
+                       .then( function ( data ) {
+                               // If a single page was given (not an array) respond with a single item as well.
+                               return Array.isArray( pages ) ? data.watch : data.watch[ 0 ];
+                       } )
+                       .promise( { abort: apiPromise.abort } );
+       }
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * Convenience method for `action=watch`.
+                *
+                * @inheritdoc #doWatchInternal
+                */
+               watch: function ( pages ) {
+                       return doWatchInternal.call( this, pages );
+               },
+
+               /**
+                * Convenience method for `action=watch&unwatch=1`.
+                *
+                * @inheritdoc #doWatchInternal
+                */
+               unwatch: function ( pages ) {
+                       return doWatchInternal.call( this, pages, { unwatch: 1 } );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.watch
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.apipretty.css b/resources/src/mediawiki.apipretty.css
new file mode 100644 (file)
index 0000000..99e4569
--- /dev/null
@@ -0,0 +1,11 @@
+.mw-special-ApiHelp h1.firstHeading {
+       display: none;
+}
+
+.api-pretty-header {
+       font-size: small;
+}
+
+.api-pretty-content {
+       white-space: pre-wrap;
+}
diff --git a/resources/src/mediawiki.checkboxtoggle.js b/resources/src/mediawiki.checkboxtoggle.js
new file mode 100644 (file)
index 0000000..76bc86c
--- /dev/null
@@ -0,0 +1,38 @@
+/*!
+ * Allows users to perform all / none / invert operations on a list of
+ * checkboxes on the page.
+ *
+ * @licence GNU GPL v2+
+ * @author Luke Faraone <luke at faraone dot cc>
+ *
+ * Based on ext.nuke.js from https://www.mediawiki.org/wiki/Extension:Nuke by
+ * Jeroen De Dauw <jeroendedauw at gmail dot com>
+ */
+
+( function ( $ ) {
+       'use strict';
+
+       $( function () {
+               // FIXME: This shouldn't be a global selector to avoid conflicts
+               // with unrelated content on the same page. (T131318)
+               var $checkboxes = $( 'li input[type="checkbox"]' );
+
+               function selectAll( check ) {
+                       $checkboxes.prop( 'checked', check );
+               }
+
+               $( '.mw-checkbox-all' ).click( function () {
+                       selectAll( true );
+               } );
+               $( '.mw-checkbox-none' ).click( function () {
+                       selectAll( false );
+               } );
+               $( '.mw-checkbox-invert' ).click( function () {
+                       $checkboxes.prop( 'checked', function ( i, val ) {
+                               return !val;
+                       } );
+               } );
+
+       } );
+
+}( jQuery ) );
diff --git a/resources/src/mediawiki.checkboxtoggle.styles.css b/resources/src/mediawiki.checkboxtoggle.styles.css
new file mode 100644 (file)
index 0000000..3da0d43
--- /dev/null
@@ -0,0 +1,3 @@
+.client-nojs .mw-checkbox-toggle-controls {
+       display: none;
+}
diff --git a/resources/src/mediawiki.notification.convertmessagebox.js b/resources/src/mediawiki.notification.convertmessagebox.js
new file mode 100644 (file)
index 0000000..5d46de6
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Usage:
+ *
+ *     var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
+ *
+ * @class mw.plugin.convertmessagebox
+ * @singleton
+ */
+( function ( mw, $ ) {
+       'use strict';
+
+       /**
+        * Convert a messagebox to a notification.
+        *
+        * Checks if a message box with class `.mw-notify-success`, `.mw-notify-warning`, or `.mw-notify-error`
+        * exists and converts it into a mw.Notification with the text of the element or a given message key.
+        *
+        * By default the notification will automatically hide after 5s, or when the user clicks the element.
+        * This can be overridden by setting attribute `data-mw-autohide="true"`.
+        *
+        * @param {Object} [options] Options
+        * @param {mw.Message} [options.msg] Message key (must be loaded already)
+        */
+       function convertmessagebox( options ) {
+               var $msgBox, type, autoHide, msg, notif,
+                       $successBox = $( '.mw-notify-success' ),
+                       $warningBox = $( '.mw-notify-warning' ),
+                       $errorBox = $( '.mw-notify-error' );
+
+               // If there is a message box and javascript is enabled, use a slick notification instead!
+               if ( $successBox.length ) {
+                       $msgBox = $successBox;
+                       type = 'info';
+               } else if ( $warningBox.length ) {
+                       $msgBox = $warningBox;
+                       type = 'warn';
+               } else if ( $errorBox.length ) {
+                       $msgBox = $errorBox;
+                       type = 'error';
+               } else {
+                       return;
+               }
+
+               autoHide = $msgBox.attr( 'data-mw-autohide' ) === 'true';
+
+               // If the msg param is given, use it, otherwise use the text of the successbox
+               msg = options && options.msg || $msgBox.text();
+               $msgBox.detach();
+
+               notif = mw.notification.notify( msg, { autoHide: autoHide, type: type } );
+               if ( !autoHide ) {
+                       // 'change' event not reliable!
+                       $( document ).one( 'keydown mousedown', function () {
+                               if ( notif ) {
+                                       notif.close();
+                                       notif = null;
+                               }
+                       } );
+               }
+       }
+
+       module.exports = convertmessagebox;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.notification.convertmessagebox.styles.less b/resources/src/mediawiki.notification.convertmessagebox.styles.less
new file mode 100644 (file)
index 0000000..2371f4e
--- /dev/null
@@ -0,0 +1,7 @@
+.client-js {
+       .mw-notify-success,
+       .mw-notify-warning,
+       .mw-notify-error {
+               display: none;
+       }
+}
diff --git a/resources/src/mediawiki.page.gallery.js b/resources/src/mediawiki.page.gallery.js
new file mode 100644 (file)
index 0000000..79937e5
--- /dev/null
@@ -0,0 +1,268 @@
+/*!
+ * Show gallery captions when focused. Copied directly from jquery.mw-jump.js.
+ * Also Dynamically resize images to justify them.
+ */
+( function ( mw, $ ) {
+       var $galleries,
+               bound = false,
+               // Is there a better way to detect a touchscreen? Current check taken from stack overflow.
+               isTouchScreen = !!( window.ontouchstart !== undefined ||
+                       window.DocumentTouch !== undefined && document instanceof window.DocumentTouch
+               );
+
+       /**
+        * Perform the layout justification.
+        *
+        * @ignore
+        * @context {HTMLElement} A `ul.mw-gallery-*` element
+        */
+       function justify() {
+               var lastTop,
+                       $img,
+                       imgWidth,
+                       imgHeight,
+                       captionWidth,
+                       rows = [],
+                       $gallery = $( this );
+
+               $gallery.children( 'li.gallerybox' ).each( function () {
+                       // Math.floor to be paranoid if things are off by 0.00000000001
+                       var top = Math.floor( $( this ).position().top ),
+                               $this = $( this );
+
+                       if ( top !== lastTop ) {
+                               rows[ rows.length ] = [];
+                               lastTop = top;
+                       }
+
+                       $img = $this.find( 'div.thumb a.image img' );
+                       if ( $img.length && $img[ 0 ].height ) {
+                               imgHeight = $img[ 0 ].height;
+                               imgWidth = $img[ 0 ].width;
+                       } else {
+                               // If we don't have a real image, get the containing divs width/height.
+                               // Note that if we do have a real image, using this method will generally
+                               // give the same answer, but can be different in the case of a very
+                               // narrow image where extra padding is added.
+                               imgHeight = $this.children().children( 'div:first' ).height();
+                               imgWidth = $this.children().children( 'div:first' ).width();
+                       }
+
+                       // Hack to make an edge case work ok
+                       if ( imgHeight < 30 ) {
+                               // Don't try and resize this item.
+                               imgHeight = 0;
+                       }
+
+                       captionWidth = $this.children().children( 'div.gallerytextwrapper' ).width();
+                       rows[ rows.length - 1 ][ rows[ rows.length - 1 ].length ] = {
+                               $elm: $this,
+                               width: $this.outerWidth(),
+                               imgWidth: imgWidth,
+                               // XXX: can divide by 0 ever happen?
+                               aspect: imgWidth / imgHeight,
+                               captionWidth: captionWidth,
+                               height: imgHeight
+                       };
+
+                       // Save all boundaries so we can restore them on window resize
+                       $this.data( 'imgWidth', imgWidth );
+                       $this.data( 'imgHeight', imgHeight );
+                       $this.data( 'width', $this.outerWidth() );
+                       $this.data( 'captionWidth', captionWidth );
+               } );
+
+               ( function () {
+                       var maxWidth,
+                               combinedAspect,
+                               combinedPadding,
+                               curRow,
+                               curRowHeight,
+                               wantedWidth,
+                               preferredHeight,
+                               newWidth,
+                               padding,
+                               $outerDiv,
+                               $innerDiv,
+                               $imageDiv,
+                               $imageElm,
+                               imageElm,
+                               $caption,
+                               i,
+                               j,
+                               avgZoom,
+                               totalZoom = 0;
+
+                       for ( i = 0; i < rows.length; i++ ) {
+                               maxWidth = $gallery.width();
+                               combinedAspect = 0;
+                               combinedPadding = 0;
+                               curRow = rows[ i ];
+                               curRowHeight = 0;
+
+                               for ( j = 0; j < curRow.length; j++ ) {
+                                       if ( curRowHeight === 0 ) {
+                                               if ( isFinite( curRow[ j ].height ) ) {
+                                                       // Get the height of this row, by taking the first
+                                                       // non-out of bounds height
+                                                       curRowHeight = curRow[ j ].height;
+                                               }
+                                       }
+
+                                       if ( curRow[ j ].aspect === 0 || !isFinite( curRow[ j ].aspect ) ) {
+                                               // One of the dimensions are 0. Probably should
+                                               // not try to resize.
+                                               combinedPadding += curRow[ j ].width;
+                                       } else {
+                                               combinedAspect += curRow[ j ].aspect;
+                                               combinedPadding += curRow[ j ].width - curRow[ j ].imgWidth;
+                                       }
+                               }
+
+                               // Add some padding for inter-element spacing.
+                               combinedPadding += 5 * curRow.length;
+                               wantedWidth = maxWidth - combinedPadding;
+                               preferredHeight = wantedWidth / combinedAspect;
+
+                               if ( preferredHeight > curRowHeight * 1.5 ) {
+                                       // Only expand at most 1.5 times current size
+                                       // As that's as high a resolution as we have.
+                                       // Also on the off chance there is a bug in this
+                                       // code, would prevent accidentally expanding to
+                                       // be 10 billion pixels wide.
+                                       if ( i === rows.length - 1 ) {
+                                               // If its the last row, and we can't fit it,
+                                               // don't make the entire row huge.
+                                               avgZoom = ( totalZoom / ( rows.length - 1 ) ) * curRowHeight;
+                                               if ( isFinite( avgZoom ) && avgZoom >= 1 && avgZoom <= 1.5 ) {
+                                                       preferredHeight = avgZoom;
+                                               } else {
+                                                       // Probably a single row gallery
+                                                       preferredHeight = curRowHeight;
+                                               }
+                                       } else {
+                                               preferredHeight = 1.5 * curRowHeight;
+                                       }
+                               }
+                               if ( !isFinite( preferredHeight ) ) {
+                                       // This *definitely* should not happen.
+                                       // Skip this row.
+                                       continue;
+                               }
+                               if ( preferredHeight < 5 ) {
+                                       // Well something clearly went wrong...
+                                       // Skip this row.
+                                       continue;
+                               }
+
+                               if ( preferredHeight / curRowHeight > 1 ) {
+                                       totalZoom += preferredHeight / curRowHeight;
+                               } else {
+                                       // If we shrink, still consider that a zoom of 1
+                                       totalZoom += 1;
+                               }
+
+                               for ( j = 0; j < curRow.length; j++ ) {
+                                       newWidth = preferredHeight * curRow[ j ].aspect;
+                                       padding = curRow[ j ].width - curRow[ j ].imgWidth;
+                                       $outerDiv = curRow[ j ].$elm;
+                                       $innerDiv = $outerDiv.children( 'div' ).first();
+                                       $imageDiv = $innerDiv.children( 'div.thumb' );
+                                       $imageElm = $imageDiv.find( 'img' ).first();
+                                       imageElm = $imageElm.length ? $imageElm[ 0 ] : null;
+                                       $caption = $outerDiv.find( 'div.gallerytextwrapper' );
+
+                                       // Since we are going to re-adjust the height, the vertical
+                                       // centering margins need to be reset.
+                                       $imageDiv.children( 'div' ).css( 'margin', '0px auto' );
+
+                                       if ( newWidth < 60 || !isFinite( newWidth ) ) {
+                                               // Making something skinnier than this will mess up captions,
+                                               if ( newWidth < 1 || !isFinite( newWidth ) ) {
+                                                       $innerDiv.height( preferredHeight );
+                                                       // Don't even try and touch the image size if it could mean
+                                                       // making it disappear.
+                                                       continue;
+                                               }
+                                       } else {
+                                               $outerDiv.width( newWidth + padding );
+                                               $innerDiv.width( newWidth + padding );
+                                               $imageDiv.width( newWidth );
+                                               $caption.width( curRow[ j ].captionWidth + ( newWidth - curRow[ j ].imgWidth ) );
+                                       }
+
+                                       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 );
+                                       }
+                               }
+                       }
+               }() );
+       }
+
+       function handleResizeStart() {
+               $galleries.children( 'li.gallerybox' ).each( function () {
+                       var imgWidth = $( this ).data( 'imgWidth' ),
+                               imgHeight = $( this ).data( 'imgHeight' ),
+                               width = $( this ).data( 'width' ),
+                               captionWidth = $( this ).data( 'captionWidth' ),
+                               $innerDiv = $( this ).children( 'div' ).first(),
+                               $imageDiv = $innerDiv.children( 'div.thumb' ),
+                               $imageElm, imageElm;
+
+                       // Restore original sizes so we can arrange the elements as on freshly loaded page
+                       $( this ).width( width );
+                       $innerDiv.width( width );
+                       $imageDiv.width( imgWidth );
+                       $( this ).find( 'div.gallerytextwrapper' ).width( captionWidth );
+
+                       $imageElm = $( this ).find( 'img' ).first();
+                       imageElm = $imageElm.length ? $imageElm[ 0 ] : null;
+                       if ( imageElm ) {
+                               imageElm.width = imgWidth;
+                               imageElm.height = imgHeight;
+                       } else {
+                               $imageDiv.height( imgHeight );
+                       }
+               } );
+       }
+
+       function handleResizeEnd() {
+               $galleries.each( justify );
+       }
+
+       mw.hook( 'wikipage.content' ).add( function ( $content ) {
+               if ( isTouchScreen ) {
+                       // Always show the caption for a touch screen.
+                       $content.find( 'ul.mw-gallery-packed-hover' )
+                               .addClass( 'mw-gallery-packed-overlay' )
+                               .removeClass( 'mw-gallery-packed-hover' );
+               } else {
+                       // Note use of just "a", not a.image, since we want this to trigger if a link in
+                       // the caption receives focus
+                       $content.find( 'ul.mw-gallery-packed-hover li.gallerybox' ).on( 'focus blur', 'a', function ( e ) {
+                               // Confusingly jQuery leaves e.type as focusout for delegated blur events
+                               var gettingFocus = e.type !== 'blur' && e.type !== 'focusout';
+                               $( this ).closest( 'li.gallerybox' ).toggleClass( 'mw-gallery-focused', gettingFocus );
+                       } );
+               }
+
+               $galleries = $content.find( 'ul.mw-gallery-packed-overlay, ul.mw-gallery-packed-hover, ul.mw-gallery-packed' );
+               // Call the justification asynchronous because live preview fires the hook with detached $content.
+               setTimeout( function () {
+                       $galleries.each( justify );
+
+                       // Bind here instead of in the top scope as the callbacks use $galleries.
+                       if ( !bound ) {
+                               bound = true;
+                               $( window )
+                                       .resize( $.debounce( 300, true, handleResizeStart ) )
+                                       .resize( $.debounce( 300, handleResizeEnd ) );
+                       }
+               } );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page.gallery.slideshow.js b/resources/src/mediawiki.page.gallery.slideshow.js
new file mode 100644 (file)
index 0000000..6e9ff0e
--- /dev/null
@@ -0,0 +1,460 @@
+/*!
+ * mw.GallerySlideshow: Interface controls for the slideshow gallery
+ */
+( function ( mw, $, OO ) {
+       /**
+        * mw.GallerySlideshow encapsulates the user interface of the slideshow
+        * galleries. An object is instantiated for each `.mw-gallery-slideshow`
+        * element.
+        *
+        * @class mw.GallerySlideshow
+        * @uses mw.Title
+        * @uses mw.Api
+        * @param {jQuery} gallery The `<ul>` element of the gallery.
+        */
+       mw.GallerySlideshow = function ( gallery ) {
+               // Properties
+               this.$gallery = $( gallery );
+               this.$galleryCaption = this.$gallery.find( '.gallerycaption' );
+               this.$galleryBox = this.$gallery.find( '.gallerybox' );
+               this.$currentImage = null;
+               this.imageInfoCache = {};
+               if ( this.$gallery.parent().attr( 'id' ) !== 'mw-content-text' ) {
+                       this.$container = this.$gallery.parent();
+               }
+
+               // Initialize
+               this.drawCarousel();
+               this.setSizeRequirement();
+               this.toggleThumbnails( !!this.$gallery.attr( 'data-showthumbnails' ) );
+               this.showCurrentImage();
+
+               // Events
+               $( window ).on(
+                       'resize',
+                       OO.ui.debounce(
+                               this.setSizeRequirement.bind( this ),
+                               100
+                       )
+               );
+
+               // Disable thumbnails' link, instead show the image in the carousel
+               this.$galleryBox.on( 'click', function ( e ) {
+                       this.$currentImage = $( e.currentTarget );
+                       this.showCurrentImage();
+                       return false;
+               }.bind( this ) );
+       };
+
+       /* Properties */
+       /**
+        * @property {jQuery} $gallery The `<ul>` element of the gallery.
+        */
+
+       /**
+        * @property {jQuery} $galleryCaption The `<li>` that has the gallery caption.
+        */
+
+       /**
+        * @property {jQuery} $galleryBox Selection of `<li>` elements that have thumbnails.
+        */
+
+       /**
+        * @property {jQuery} $carousel The `<li>` elements that contains the carousel.
+        */
+
+       /**
+        * @property {jQuery} $interface The `<div>` elements that contains the interface buttons.
+        */
+
+       /**
+        * @property {jQuery} $img The `<img>` element that'll display the current image.
+        */
+
+       /**
+        * @property {jQuery} $imgLink The `<a>` element that links to the image's File page.
+        */
+
+       /**
+        * @property {jQuery} $imgCaption The `<p>` element that holds the image caption.
+        */
+
+       /**
+        * @property {jQuery} $imgContainer The `<div>` element that contains the image.
+        */
+
+       /**
+        * @property {jQuery} $currentImage The `<li>` element of the current image.
+        */
+
+       /**
+        * @property {jQuery} $container If the gallery contained in an element that is
+        *   not the main content element, then it stores that element.
+        */
+
+       /**
+        * @property {Object} imageInfoCache A key value pair of thumbnail URLs and image info.
+        */
+
+       /**
+        * @property {number} imageWidth Width of the image based on viewport size
+        */
+
+       /**
+        * @property {number} imageHeight Height of the image based on viewport size
+        *   the URLs in the required size.
+        */
+
+       /* Setup */
+       OO.initClass( mw.GallerySlideshow );
+
+       /* Methods */
+       /**
+        * Draws the carousel and the interface around it.
+        */
+       mw.GallerySlideshow.prototype.drawCarousel = function () {
+               var next, prev, toggle, interfaceElements, carouselStack;
+
+               this.$carousel = $( '<li>' ).addClass( 'gallerycarousel' );
+
+               // Buttons for the interface
+               prev = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       icon: 'previous'
+               } ).on( 'click', this.prevImage.bind( this ) );
+
+               next = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       icon: 'next'
+               } ).on( 'click', this.nextImage.bind( this ) );
+
+               toggle = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       icon: 'imageGallery',
+                       title: mw.msg( 'gallery-slideshow-toggle' )
+               } ).on( 'click', this.toggleThumbnails.bind( this ) );
+
+               interfaceElements = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       classes: [ 'mw-gallery-slideshow-buttons' ],
+                       $content: $( '<div>' ).append(
+                               prev.$element,
+                               toggle.$element,
+                               next.$element
+                       )
+               } );
+               this.$interface = interfaceElements.$element;
+
+               // Containers for the current image, caption etc.
+               this.$img = $( '<img>' );
+               this.$imgLink = $( '<a>' ).append( this.$img );
+               this.$imgCaption = $( '<p>' ).attr( 'class', 'mw-gallery-slideshow-caption' );
+               this.$imgContainer = $( '<div>' )
+                       .attr( 'class', 'mw-gallery-slideshow-img-container' )
+                       .append( this.$imgLink );
+
+               carouselStack = new OO.ui.StackLayout( {
+                       continuous: true,
+                       expanded: false,
+                       items: [
+                               interfaceElements,
+                               new OO.ui.PanelLayout( {
+                                       expanded: false,
+                                       $content: this.$imgContainer
+                               } ),
+                               new OO.ui.PanelLayout( {
+                                       expanded: false,
+                                       $content: this.$imgCaption
+                               } )
+                       ]
+               } );
+               this.$carousel.append( carouselStack.$element );
+
+               // Append below the caption or as the first element in the gallery
+               if ( this.$galleryCaption.length !== 0 ) {
+                       this.$galleryCaption.after( this.$carousel );
+               } else {
+                       this.$gallery.prepend( this.$carousel );
+               }
+       };
+
+       /**
+        * Sets the {@link #imageWidth} and {@link #imageHeight} properties
+        * based on the size of the window. Also flushes the
+        * {@link #imageInfoCache} as we'll now need URLs for a different
+        * size.
+        */
+       mw.GallerySlideshow.prototype.setSizeRequirement = function () {
+               var w, h;
+
+               if ( this.$container !== undefined ) {
+                       w = this.$container.width() * 0.9;
+                       h = ( this.$container.height() - this.getChromeHeight() ) * 0.9;
+               } else {
+                       w = this.$imgContainer.width();
+                       h = Math.min( $( window ).height() * ( 3 / 4 ), this.$imgContainer.width() ) - this.getChromeHeight();
+               }
+
+               // Only update and flush the cache if the size changed
+               if ( w !== this.imageWidth || h !== this.imageHeight ) {
+                       this.imageWidth = w;
+                       this.imageHeight = h;
+                       this.imageInfoCache = {};
+                       this.setImageSize();
+               }
+       };
+
+       /**
+        * Gets the height of the interface elements and the
+        * gallery's caption.
+        *
+        * @return {number} Height
+        */
+       mw.GallerySlideshow.prototype.getChromeHeight = function () {
+               return this.$interface.outerHeight() + this.$galleryCaption.outerHeight();
+       };
+
+       /**
+        * Sets the height and width of {@link #$img} based on the
+        * proportion of the image and the values generated by
+        * {@link #setSizeRequirement}.
+        *
+        * @return {boolean} Whether or not the image was sized.
+        */
+       mw.GallerySlideshow.prototype.setImageSize = function () {
+               if ( this.$img === undefined || this.$thumbnail === undefined ) {
+                       return false;
+               }
+
+               // Reset height and width
+               this.$img
+                       .removeAttr( 'width' )
+                       .removeAttr( 'height' );
+
+               // Stretch image to take up the required size
+               this.$img.attr( 'height', ( this.imageHeight - this.$imgCaption.outerHeight() ) + 'px' );
+
+               // Make the image smaller in case the current image
+               // size is larger than the original file size.
+               this.getImageInfo( this.$thumbnail ).done( function ( info ) {
+                       // NOTE: There will be a jump when resizing the window
+                       // because the cache is cleared and this a new network request.
+                       if (
+                               info.thumbwidth < this.$img.width() ||
+                               info.thumbheight < this.$img.height()
+                       ) {
+                               this.$img.attr( 'width', info.thumbwidth + 'px' );
+                               this.$img.attr( 'height', info.thumbheight + 'px' );
+                       }
+               }.bind( this ) );
+
+               return true;
+       };
+
+       /**
+        * Displays the image set as {@link #$currentImage} in the carousel.
+        */
+       mw.GallerySlideshow.prototype.showCurrentImage = function () {
+               var imageLi = this.getCurrentImage(),
+                       caption = imageLi.find( '.gallerytext' );
+
+               // The order of the following is important for size calculations
+               // 1. Highlight current thumbnail
+               this.$gallery
+                       .find( '.gallerybox.slideshow-current' )
+                       .removeClass( 'slideshow-current' );
+               imageLi.addClass( 'slideshow-current' );
+
+               // 2. Show thumbnail
+               this.$thumbnail = imageLi.find( 'img' );
+               this.$img.attr( 'src', this.$thumbnail.attr( 'src' ) );
+               this.$img.attr( 'alt', this.$thumbnail.attr( 'alt' ) );
+               this.$imgLink.attr( 'href', imageLi.find( 'a' ).eq( 0 ).attr( 'href' ) );
+
+               // 3. Copy caption
+               this.$imgCaption
+                       .empty()
+                       .append( caption.clone() );
+
+               // 4. Stretch thumbnail to correct size
+               this.setImageSize();
+
+               // 5. Load image at the required size
+               this.loadImage( this.$thumbnail ).done( function ( info, $img ) {
+                       // Show this image to the user only if its still the current one
+                       if ( this.$thumbnail.attr( 'src' ) === $img.attr( 'src' ) ) {
+                               this.$img.attr( 'src', info.thumburl );
+                               this.setImageSize();
+
+                               // Keep the next image ready
+                               this.loadImage( this.getNextImage().find( 'img' ) );
+                       }
+               }.bind( this ) );
+       };
+
+       /**
+        * Loads the full image given the `<img>` element of the thumbnail.
+        *
+        * @param {Object} $img
+        * @return {jQuery.Promise} Resolves with the images URL and original
+        *      element once the image has loaded.
+        */
+       mw.GallerySlideshow.prototype.loadImage = function ( $img ) {
+               var img, d = $.Deferred();
+
+               this.getImageInfo( $img ).done( function ( info ) {
+                       img = new Image();
+                       img.src = info.thumburl;
+                       img.onload = function () {
+                               d.resolve( info, $img );
+                       };
+                       img.onerror = function () {
+                               d.reject();
+                       };
+               } ).fail( function () {
+                       d.reject();
+               } );
+
+               return d.promise();
+       };
+
+       /**
+        * Gets the image's info given an `<img>` element.
+        *
+        * @param {Object} $img
+        * @return {jQuery.Promise} Resolves with the image's info.
+        */
+       mw.GallerySlideshow.prototype.getImageInfo = function ( $img ) {
+               var api, title, params,
+                       imageSrc = $img.attr( 'src' );
+
+               // Reject promise if there is no thumbnail image
+               if ( $img[ 0 ] === undefined ) {
+                       return $.Deferred().reject();
+               }
+
+               if ( this.imageInfoCache[ imageSrc ] === undefined ) {
+                       api = new mw.Api();
+                       // TODO: This supports only gallery of images
+                       title = mw.Title.newFromImg( $img );
+                       params = {
+                               action: 'query',
+                               formatversion: 2,
+                               titles: title.toString(),
+                               prop: 'imageinfo',
+                               iiprop: 'url'
+                       };
+
+                       // Check which dimension we need to request, based on
+                       // image and container proportions.
+                       if ( this.getDimensionToRequest( $img ) === 'height' ) {
+                               params.iiurlheight = this.imageHeight;
+                       } else {
+                               params.iiurlwidth = this.imageWidth;
+                       }
+
+                       this.imageInfoCache[ imageSrc ] = api.get( params ).then( function ( data ) {
+                               if ( OO.getProp( data, 'query', 'pages', 0, 'imageinfo', 0, 'thumburl' ) !== undefined ) {
+                                       return data.query.pages[ 0 ].imageinfo[ 0 ];
+                               } else {
+                                       return $.Deferred().reject();
+                               }
+                       } );
+               }
+
+               return this.imageInfoCache[ imageSrc ];
+       };
+
+       /**
+        * Given an image, the method checks whether to use the height
+        * or the width to request the larger image.
+        *
+        * @param {jQuery} $img
+        * @return {string}
+        */
+       mw.GallerySlideshow.prototype.getDimensionToRequest = function ( $img ) {
+               var ratio = $img.width() / $img.height();
+
+               if ( this.imageHeight * ratio <= this.imageWidth ) {
+                       return 'height';
+               } else {
+                       return 'width';
+               }
+       };
+
+       /**
+        * Toggles visibility of the thumbnails.
+        *
+        * @param {boolean} show Optional argument to control the state
+        */
+       mw.GallerySlideshow.prototype.toggleThumbnails = function ( show ) {
+               this.$galleryBox.toggle( show );
+               this.$carousel.toggleClass( 'mw-gallery-slideshow-thumbnails-toggled', show );
+       };
+
+       /**
+        * Getter method for {@link #$currentImage}
+        *
+        * @return {jQuery}
+        */
+       mw.GallerySlideshow.prototype.getCurrentImage = function () {
+               this.$currentImage = this.$currentImage || this.$galleryBox.eq( 0 );
+               return this.$currentImage;
+       };
+
+       /**
+        * Gets the image after the current one. Returns the first image if
+        * the current one is the last.
+        *
+        * @return {jQuery}
+        */
+       mw.GallerySlideshow.prototype.getNextImage = function () {
+               // Not the last image in the gallery
+               if ( this.$currentImage.next( '.gallerybox' )[ 0 ] !== undefined ) {
+                       return this.$currentImage.next( '.gallerybox' );
+               } else {
+                       return this.$galleryBox.eq( 0 );
+               }
+       };
+
+       /**
+        * Gets the image before the current one. Returns the last image if
+        * the current one is the first.
+        *
+        * @return {jQuery}
+        */
+       mw.GallerySlideshow.prototype.getPrevImage = function () {
+               // Not the first image in the gallery
+               if ( this.$currentImage.prev( '.gallerybox' )[ 0 ] !== undefined ) {
+                       return this.$currentImage.prev( '.gallerybox' );
+               } else {
+                       return this.$galleryBox.last();
+               }
+       };
+
+       /**
+        * Sets the {@link #$currentImage} to the next one and shows
+        * it in the carousel
+        */
+       mw.GallerySlideshow.prototype.nextImage = function () {
+               this.$currentImage = this.getNextImage();
+               this.showCurrentImage();
+       };
+
+       /**
+        * Sets the {@link #$currentImage} to the previous one and shows
+        * it in the carousel
+        */
+       mw.GallerySlideshow.prototype.prevImage = function () {
+               this.$currentImage = this.getPrevImage();
+               this.showCurrentImage();
+       };
+
+       // Bootstrap all slideshow galleries
+       mw.hook( 'wikipage.content' ).add( function ( $content ) {
+               $content.find( '.mw-gallery-slideshow' ).each( function () {
+                       // eslint-disable-next-line no-new
+                       new mw.GallerySlideshow( this );
+               } );
+       } );
+}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki.page.image.pagination.js b/resources/src/mediawiki.page.image.pagination.js
new file mode 100644 (file)
index 0000000..06c34a5
--- /dev/null
@@ -0,0 +1,143 @@
+/*!
+ * Implement AJAX navigation for multi-page images so the user may browse without a full page reload.
+ */
+
+/* eslint-disable no-use-before-define */
+
+( function ( mw, $ ) {
+       var jqXhr, $multipageimage, $spinner,
+               cache = {},
+               cacheOrder = [];
+
+       /* Fetch the next page, caching up to 10 last-loaded pages.
+        * @param {string} url
+        * @return {jQuery.Promise}
+        */
+       function fetchPageData( url ) {
+               if ( jqXhr && jqXhr.abort ) {
+                       // Prevent race conditions and piling up pending requests
+                       jqXhr.abort();
+               }
+               jqXhr = undefined;
+
+               // Try the cache
+               if ( cache[ url ] ) {
+                       // Update access freshness
+                       cacheOrder.splice( cacheOrder.indexOf( url ), 1 );
+                       cacheOrder.push( url );
+                       return $.Deferred().resolve( cache[ url ] ).promise();
+               }
+
+               // TODO Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
+               // (thumbnail urls) and update the interface manually.
+               jqXhr = $.ajax( url ).then( function ( data ) {
+                       return $( data ).find( 'table.multipageimage' ).contents();
+               } );
+
+               // Handle cache updates
+               jqXhr.done( function ( $contents ) {
+                       jqXhr = undefined;
+
+                       // Cache the newly loaded page
+                       cache[ url ] = $contents;
+                       cacheOrder.push( url );
+
+                       // Remove the oldest entry if we're over the limit
+                       if ( cacheOrder.length > 10 ) {
+                               delete cache[ cacheOrder[ 0 ] ];
+                               cacheOrder = cacheOrder.slice( 1 );
+                       }
+               } );
+
+               return jqXhr.promise();
+       }
+
+       /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
+        * @param {string} url
+        * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
+        *   true, this function won't push a new history state, for the browser did so already).
+        */
+       function switchPage( url, hist ) {
+               var $tr, promise;
+
+               // Start fetching data (might be cached)
+               promise = fetchPageData( url );
+
+               // Add a new spinner if one doesn't already exist and the data is not already ready
+               if ( !$spinner && promise.state() !== 'resolved' ) {
+                       $tr = $multipageimage.find( 'tr' );
+                       $spinner = $.createSpinner( {
+                               size: 'large',
+                               type: 'block'
+                       } )
+                               // Copy the old content dimensions equal so that the current scroll position is not
+                               // lost between emptying the table is and receiving the new contents.
+                               .css( {
+                                       height: $tr.outerHeight(),
+                                       width: $tr.outerWidth()
+                               } );
+
+                       $multipageimage.empty().append( $spinner );
+               }
+
+               promise.done( function ( $contents ) {
+                       $spinner = undefined;
+
+                       // Replace table contents
+                       $multipageimage.empty().append( $contents.clone() );
+
+                       bindPageNavigation( $multipageimage );
+
+                       // Fire hook because the page's content has changed
+                       mw.hook( 'wikipage.content' ).fire( $multipageimage );
+
+                       // Update browser history and address bar. But not if we came here from a history
+                       // event, in which case the url is already updated by the browser.
+                       if ( history.pushState && !hist ) {
+                               history.pushState( { tag: 'mw-pagination' }, document.title, url );
+                       }
+               } );
+       }
+
+       function bindPageNavigation( $container ) {
+               $container.find( '.multipageimagenavbox' ).one( 'click', 'a', function ( e ) {
+                       var page, url;
+
+                       // Generate the same URL on client side as the one generated in ImagePage::openShowImage.
+                       // We avoid using the URL in the link directly since it could have been manipulated (T68608)
+                       page = Number( mw.util.getParamValue( 'page', this.href ) );
+                       url = mw.util.getUrl( mw.config.get( 'wgPageName' ), { page: page } );
+
+                       switchPage( url );
+                       e.preventDefault();
+               } );
+
+               $container.find( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
+                       switchPage( this.action + '?' + $( this ).serialize() );
+                       e.preventDefault();
+               } );
+       }
+
+       $( function () {
+               if ( mw.config.get( 'wgCanonicalNamespace' ) !== 'File' ) {
+                       return;
+               }
+               $multipageimage = $( 'table.multipageimage' );
+               if ( !$multipageimage.length ) {
+                       return;
+               }
+
+               bindPageNavigation( $multipageimage );
+
+               // Update the url using the History API (if available)
+               if ( history.pushState && history.replaceState ) {
+                       history.replaceState( { tag: 'mw-pagination' }, '' );
+                       $( window ).on( 'popstate', function ( e ) {
+                               var state = e.originalEvent.state;
+                               if ( state && state.tag === 'mw-pagination' ) {
+                                       switchPage( location.href, true );
+                               }
+                       } );
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page.patrol.ajax.js b/resources/src/mediawiki.page.patrol.ajax.js
new file mode 100644 (file)
index 0000000..d8fb249
--- /dev/null
@@ -0,0 +1,64 @@
+/*!
+ * Animate patrol links to use asynchronous API requests to
+ * patrol pages, rather than navigating to a different URI.
+ *
+ * @since 1.21
+ * @author Marius Hoch <hoo@online.de>
+ */
+( function ( mw, $ ) {
+       if ( !mw.user.tokens.exists( 'patrolToken' ) ) {
+               // Current user has no patrol right, or an old cached version of user.tokens
+               // that didn't have patrolToken yet.
+               return;
+       }
+       $( function () {
+               var $patrolLinks = $( '.patrollink[data-mw="interface"] a' );
+               $patrolLinks.on( 'click', function ( e ) {
+                       var $spinner, rcid, apiRequest;
+
+                       // Preload the notification module for mw.notify
+                       mw.loader.load( 'mediawiki.notification' );
+
+                       // Hide the link and create a spinner to show it inside the brackets.
+                       $spinner = $.createSpinner( {
+                               size: 'small',
+                               type: 'inline'
+                       } );
+                       $( this ).hide().after( $spinner );
+
+                       rcid = mw.util.getParamValue( 'rcid', this.href );
+                       apiRequest = new mw.Api();
+
+                       apiRequest.postWithToken( 'patrol', {
+                               formatversion: 2,
+                               action: 'patrol',
+                               rcid: rcid
+                       } ).done( function ( data ) {
+                               var title;
+                               // Remove all patrollinks from the page (including any spinners inside).
+                               $patrolLinks.closest( '.patrollink' ).remove();
+                               if ( data.patrol !== undefined ) {
+                                       // Success
+                                       title = new mw.Title( data.patrol.title );
+                                       mw.notify( mw.msg( 'markedaspatrollednotify', title.toText() ) );
+                               } else {
+                                       // This should never happen as errors should trigger fail
+                                       mw.notify( mw.msg( 'markedaspatrollederrornotify' ), { type: 'error' } );
+                               }
+                       } ).fail( function ( error ) {
+                               $spinner.remove();
+                               // Restore the patrol link. This allows the user to try again
+                               // (or open it in a new window, bypassing this ajax module).
+                               $patrolLinks.show();
+                               if ( error === 'noautopatrol' ) {
+                                       // Can't patrol own
+                                       mw.notify( mw.msg( 'markedaspatrollederror-noautopatrol' ), { type: 'warn' } );
+                               } else {
+                                       mw.notify( mw.msg( 'markedaspatrollederrornotify' ), { type: 'error' } );
+                               }
+                       } );
+
+                       e.preventDefault();
+               } );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page.ready.js b/resources/src/mediawiki.page.ready.js
new file mode 100644 (file)
index 0000000..e147664
--- /dev/null
@@ -0,0 +1,58 @@
+( function ( mw, $ ) {
+       mw.hook( 'wikipage.content' ).add( function ( $content ) {
+               var $sortable, $collapsible;
+
+               $collapsible = $content.find( '.mw-collapsible' );
+               if ( $collapsible.length ) {
+                       // Preloaded by Skin::getDefaultModules()
+                       mw.loader.using( 'jquery.makeCollapsible', function () {
+                               $collapsible.makeCollapsible();
+                       } );
+               }
+
+               $sortable = $content.find( 'table.sortable' );
+               if ( $sortable.length ) {
+                       // Preloaded by Skin::getDefaultModules()
+                       mw.loader.using( 'jquery.tablesorter', function () {
+                               $sortable.tablesorter();
+                       } );
+               }
+
+               // Run jquery.checkboxShiftClick
+               $content.find( 'input[type="checkbox"]:not(.noshiftselect)' ).checkboxShiftClick();
+       } );
+
+       // Things outside the wikipage content
+       $( function () {
+               var $nodes;
+
+               // Add accesskey hints to the tooltips
+               $( '[accesskey]' ).updateTooltipAccessKeys();
+
+               $nodes = $( '.catlinks[data-mw="interface"]' );
+               if ( $nodes.length ) {
+                       /**
+                        * Fired when categories are being added to the DOM
+                        *
+                        * It is encouraged to fire it before the main DOM is changed (when $content
+                        * is still detached).  However, this order is not defined either way, so you
+                        * should only rely on $content itself.
+                        *
+                        * This includes the ready event on a page load (including post-edit loads)
+                        * and when content has been previewed with LivePreview.
+                        *
+                        * @event wikipage_categories
+                        * @member mw.hook
+                        * @param {jQuery} $content The most appropriate element containing the content,
+                        *   such as .catlinks
+                        */
+                       mw.hook( 'wikipage.categories' ).fire( $nodes );
+               }
+
+               $( '#t-print a' ).click( function ( e ) {
+                       window.print();
+                       e.preventDefault();
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page.rollback.js b/resources/src/mediawiki.page.rollback.js
new file mode 100644 (file)
index 0000000..6db518d
--- /dev/null
@@ -0,0 +1,67 @@
+/*!
+ * Enhance rollback links by using asynchronous API requests,
+ * rather than navigating to an action page.
+ *
+ * @since 1.28
+ * @author Timo Tijhof
+ */
+( function ( mw, $ ) {
+
+       $( function () {
+               $( '.mw-rollback-link' ).on( 'click', 'a[data-mw="interface"]', function ( e ) {
+                       var api, $spinner,
+                               $link = $( this ),
+                               url = this.href,
+                               page = mw.util.getParamValue( 'title', url ),
+                               user = mw.util.getParamValue( 'from', url );
+
+                       if ( !page || user === null ) {
+                               // Let native browsing handle the link
+                               return true;
+                       }
+
+                       // Preload the notification module for mw.notify
+                       mw.loader.load( 'mediawiki.notification' );
+
+                       // Remove event handler so that next click (re-try) uses server action
+                       $( e.delegateTarget ).off( 'click' );
+
+                       // Hide the link and create a spinner to show it inside the brackets.
+                       $spinner = $.createSpinner( { size: 'small', type: 'inline' } );
+                       $link.hide().after( $spinner );
+
+                       // @todo: data.messageHtml is no more. Convert to using errorformat=html.
+                       api = new mw.Api();
+                       api.rollback( page, user )
+                               .then( function ( data ) {
+                                       mw.notify( $.parseHTML( data.messageHtml ), {
+                                               title: mw.msg( 'actioncomplete' )
+                                       } );
+
+                                       // Remove link container and the subsequent text node containing " | ".
+                                       if ( e.delegateTarget.nextSibling && e.delegateTarget.nextSibling.nodeType === Node.TEXT_NODE ) {
+                                               $( e.delegateTarget.nextSibling ).remove();
+                                       }
+                                       $( e.delegateTarget ).remove();
+                               }, function ( errorCode, data ) {
+                                       var message = data && data.error && data.error.messageHtml ?
+                                                       $.parseHTML( data.error.messageHtml ) :
+                                                       mw.msg( 'rollbackfailed' ),
+                                               type = errorCode === 'alreadyrolled' ? 'warn' : 'error';
+
+                                       mw.notify( message, {
+                                               type: type,
+                                               title: mw.msg( 'rollbackfailed' ),
+                                               autoHide: false
+                                       } );
+
+                                       // Restore the link (enables user to try again)
+                                       $spinner.remove();
+                                       $link.show();
+                               } );
+
+                       e.preventDefault();
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page.startup.js b/resources/src/mediawiki.page.startup.js
new file mode 100644 (file)
index 0000000..7514044
--- /dev/null
@@ -0,0 +1,49 @@
+( function ( mw, $ ) {
+       // Break out of framesets
+       if ( mw.config.get( 'wgBreakFrames' ) ) {
+               // Note: In IE < 9 strict comparison to window is non-standard (the standard didn't exist yet)
+               // it works only comparing to window.self or window.window (http://stackoverflow.com/q/4850978/319266)
+               if ( window.top !== window.self ) {
+                       // Un-trap us from framesets
+                       window.top.location.href = location.href;
+               }
+       }
+
+       $( function () {
+               var $diff;
+
+               /**
+                * Fired when wiki content is being added to the DOM
+                *
+                * It is encouraged to fire it before the main DOM is changed (when $content
+                * is still detached).  However, this order is not defined either way, so you
+                * should only rely on $content itself.
+                *
+                * This includes the ready event on a page load (including post-edit loads)
+                * and when content has been previewed with LivePreview.
+                *
+                * @event wikipage_content
+                * @member mw.hook
+                * @param {jQuery} $content The most appropriate element containing the content,
+                *   such as #mw-content-text (regular content root) or #wikiPreview (live preview
+                *   root)
+                */
+               mw.hook( 'wikipage.content' ).fire( $( '#mw-content-text' ) );
+
+               $diff = $( 'table.diff[data-mw="interface"]' );
+               if ( $diff.length ) {
+                       /**
+                        * Fired when the diff is added to a page containing a diff
+                        *
+                        * Similar to the {@link mw.hook#event-wikipage_content wikipage.content hook}
+                        * $diff may still be detached when the hook is fired.
+                        *
+                        * @event wikipage_diff
+                        * @member mw.hook
+                        * @param {jQuery} $diff The root element of the MediaWiki diff (`table.diff`).
+                        */
+                       mw.hook( 'wikipage.diff' ).fire( $diff.eq( 0 ) );
+               }
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page.watch.ajax.js b/resources/src/mediawiki.page.watch.ajax.js
new file mode 100644 (file)
index 0000000..5b41876
--- /dev/null
@@ -0,0 +1,193 @@
+/**
+ * Animate watch/unwatch links to use asynchronous API requests to
+ * watch pages, rather than navigating to a different URI.
+ *
+ * Usage:
+ *
+ *     var watch = require( 'mediawiki.page.watch.ajax' );
+ *     watch.updateWatchLink(
+ *         $node,
+ *         'watch',
+ *         'loading'
+ *     );
+ *
+ * @class mw.plugin.page.watch.ajax
+ * @singleton
+ */
+( function ( mw, $ ) {
+       var watch,
+               // The name of the page to watch or unwatch
+               title = mw.config.get( 'wgRelevantPageName' );
+
+       /**
+        * Update the link text, link href attribute and (if applicable)
+        * "loading" class.
+        *
+        * @param {jQuery} $link Anchor tag of (un)watch link
+        * @param {string} action One of 'watch', 'unwatch'
+        * @param {string} [state="idle"] 'idle' or 'loading'. Default is 'idle'
+        */
+       function updateWatchLink( $link, action, state ) {
+               var msgKey, $li, otherAction;
+
+               // A valid but empty jQuery object shouldn't throw a TypeError
+               if ( !$link.length ) {
+                       return;
+               }
+
+               // Invalid actions shouldn't silently turn the page in an unrecoverable state
+               if ( action !== 'watch' && action !== 'unwatch' ) {
+                       throw new Error( 'Invalid action' );
+               }
+
+               // message keys 'watch', 'watching', 'unwatch' or 'unwatching'.
+               msgKey = state === 'loading' ? action + 'ing' : action;
+               otherAction = action === 'watch' ? 'unwatch' : 'watch';
+               $li = $link.closest( 'li' );
+
+               // Trigger a 'watchpage' event for this List item.
+               // Announce the otherAction value as the first param.
+               // Used to monitor the state of watch link.
+               // TODO: Revise when system wide hooks are implemented
+               if ( state === undefined ) {
+                       $li.trigger( 'watchpage.mw', otherAction );
+               }
+
+               $link
+                       .text( mw.msg( msgKey ) )
+                       .attr( 'title', mw.msg( 'tooltip-ca-' + action ) )
+                       .updateTooltipAccessKeys()
+                       .attr( 'href', mw.util.getUrl( title, { action: action } ) );
+
+               // Most common ID style
+               if ( $li.prop( 'id' ) === 'ca-' + otherAction ) {
+                       $li.prop( 'id', 'ca-' + action );
+               }
+
+               if ( state === 'loading' ) {
+                       $link.addClass( 'loading' );
+               } else {
+                       $link.removeClass( 'loading' );
+               }
+       }
+
+       /**
+        * TODO: This should be moved somewhere more accessible.
+        *
+        * @private
+        * @param {string} url
+        * @return {string} The extracted action, defaults to 'view'
+        */
+       function mwUriGetAction( url ) {
+               var action, actionPaths, key, m, parts;
+
+               // TODO: Does MediaWiki give action path or query param
+               // precedence? If the former, move this to the bottom
+               action = mw.util.getParamValue( 'action', url );
+               if ( action !== null ) {
+                       return action;
+               }
+
+               actionPaths = mw.config.get( 'wgActionPaths' );
+               for ( key in actionPaths ) {
+                       if ( actionPaths.hasOwnProperty( key ) ) {
+                               parts = actionPaths[ key ].split( '$1' );
+                               parts = parts.map( mw.RegExp.escape );
+                               m = new RegExp( parts.join( '(.+)' ) ).exec( url );
+                               if ( m && m[ 1 ] ) {
+                                       return key;
+                               }
+
+                       }
+               }
+
+               return 'view';
+       }
+
+       // Expose public methods
+       watch = {
+               updateWatchLink: updateWatchLink
+       };
+       module.exports = watch;
+
+       $( function () {
+               var $links = $( '.mw-watchlink a[data-mw="interface"], a.mw-watchlink[data-mw="interface"]' );
+               if ( !$links.length ) {
+                       // Fallback to the class-based exclusion method for backwards-compatibility
+                       $links = $( '.mw-watchlink a, a.mw-watchlink' );
+                       // Restrict to core interfaces, ignore user-generated content
+                       $links = $links.filter( ':not( #bodyContent *, #content * )' );
+               }
+
+               $links.click( function ( e ) {
+                       var mwTitle, action, api, $link;
+
+                       mwTitle = mw.Title.newFromText( title );
+                       action = mwUriGetAction( this.href );
+
+                       if ( !mwTitle || ( action !== 'watch' && action !== 'unwatch' ) ) {
+                               // Let native browsing handle the link
+                               return true;
+                       }
+                       e.preventDefault();
+                       e.stopPropagation();
+
+                       $link = $( this );
+
+                       if ( $link.hasClass( 'loading' ) ) {
+                               return;
+                       }
+
+                       updateWatchLink( $link, action, 'loading' );
+
+                       // Preload the notification module for mw.notify
+                       mw.loader.load( 'mediawiki.notification' );
+
+                       api = new mw.Api();
+
+                       api[ action ]( title )
+                               .done( function ( watchResponse ) {
+                                       var message, otherAction = action === 'watch' ? 'unwatch' : 'watch';
+
+                                       if ( mwTitle.getNamespaceId() > 0 && mwTitle.getNamespaceId() % 2 === 1 ) {
+                                               message = action === 'watch' ? 'addedwatchtext-talk' : 'removedwatchtext-talk';
+                                       } else {
+                                               message = action === 'watch' ? 'addedwatchtext' : 'removedwatchtext';
+                                       }
+
+                                       mw.notify( mw.message( message, mwTitle.getPrefixedText() ).parseDom(), {
+                                               tag: 'watch-self'
+                                       } );
+
+                                       // Set link to opposite
+                                       updateWatchLink( $link, otherAction );
+
+                                       // Update the "Watch this page" checkbox on action=edit when the
+                                       // page is watched or unwatched via the tab (T14395).
+                                       $( '#wpWatchthis' ).prop( 'checked', watchResponse.watched === true );
+                               } )
+                               .fail( function () {
+                                       var msg, link;
+
+                                       // Reset link to non-loading mode
+                                       updateWatchLink( $link, action );
+
+                                       // Format error message
+                                       link = mw.html.element(
+                                               'a', {
+                                                       href: mw.util.getUrl( title ),
+                                                       title: mwTitle.getPrefixedText()
+                                               }, mwTitle.getPrefixedText()
+                                       );
+                                       msg = mw.message( 'watcherrortext', link );
+
+                                       // Report to user about the error
+                                       mw.notify( msg, {
+                                               tag: 'watch-self',
+                                               type: 'error'
+                                       } );
+                               } );
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.template.js b/resources/src/mediawiki.template.js
new file mode 100644 (file)
index 0000000..4a3157c
--- /dev/null
@@ -0,0 +1,124 @@
+/**
+ * @class mw.template
+ * @singleton
+ */
+( function ( mw, $ ) {
+       var compiledTemplates = {},
+               compilers = {};
+
+       mw.template = {
+               /**
+                * Register a new compiler.
+                *
+                * A compiler is any object that implements a compile() method. The compile() method must
+                * return a Template interface with a method render() that returns HTML.
+                *
+                * The compiler name must correspond with the name suffix of templates that use this compiler.
+                *
+                * @param {string} name Compiler name
+                * @param {Object} compiler
+                */
+               registerCompiler: function ( name, compiler ) {
+                       if ( !compiler.compile ) {
+                               throw new Error( 'Compiler must implement a compile method' );
+                       }
+                       compilers[ name ] = compiler;
+               },
+
+               /**
+                * Get the name of the associated compiler based on a template name.
+                *
+                * @param {string} templateName Name of a template (including suffix)
+                * @return {string} Name of a compiler
+                */
+               getCompilerName: function ( templateName ) {
+                       var nameParts = templateName.split( '.' );
+                       if ( nameParts.length < 2 ) {
+                               throw new Error( 'Template name must have a suffix' );
+                       }
+                       return nameParts[ nameParts.length - 1 ];
+               },
+
+               /**
+                * Get a compiler via its name.
+                *
+                * @param {string} name Name of a compiler
+                * @return {Object} The compiler
+                */
+               getCompiler: function ( name ) {
+                       var compiler = compilers[ name ];
+                       if ( !compiler ) {
+                               throw new Error( 'Unknown compiler ' + name );
+                       }
+                       return compiler;
+               },
+
+               /**
+                * Register a template associated with a module.
+                *
+                * Precompiles the newly added template based on the suffix in its name.
+                *
+                * @param {string} moduleName Name of the ResourceLoader module the template is associated with
+                * @param {string} templateName Name of the template (including suffix)
+                * @param {string} templateBody Contents of the template (e.g. html markup)
+                * @return {Object} Compiled template
+                */
+               add: function ( moduleName, templateName, templateBody ) {
+                       // Precompile and add to cache
+                       var compiled = this.compile( templateBody, this.getCompilerName( templateName ) );
+                       if ( !compiledTemplates[ moduleName ] ) {
+                               compiledTemplates[ moduleName ] = {};
+                       }
+                       compiledTemplates[ moduleName ][ templateName ] = compiled;
+
+                       return compiled;
+               },
+
+               /**
+                * Get a compiled template by module and template name.
+                *
+                * @param {string} moduleName Name of the module to retrieve the template from
+                * @param {string} templateName Name of template to be retrieved
+                * @return {Object} Compiled template
+                */
+               get: function ( moduleName, templateName ) {
+                       var moduleTemplates;
+
+                       // Try cache first
+                       if ( compiledTemplates[ moduleName ] && compiledTemplates[ moduleName ][ templateName ] ) {
+                               return compiledTemplates[ moduleName ][ templateName ];
+                       }
+
+                       moduleTemplates = mw.templates.get( moduleName );
+                       if ( !moduleTemplates || !moduleTemplates[ templateName ] ) {
+                               throw new Error( 'Template ' + templateName + ' not found in module ' + moduleName );
+                       }
+
+                       // Compiled and add to cache
+                       return this.add( moduleName, templateName, moduleTemplates[ templateName ] );
+               },
+
+               /**
+                * Compile a string of template markup with an engine of choice.
+                *
+                * @param {string} templateBody Template body
+                * @param {string} compilerName The name of a registered compiler
+                * @return {Object} Compiled template
+                */
+               compile: function ( templateBody, compilerName ) {
+                       return this.getCompiler( compilerName ).compile( templateBody );
+               }
+       };
+
+       // Register basic html compiler
+       mw.template.registerCompiler( 'html', {
+               compile: function ( src ) {
+                       return {
+                               render: function () {
+                                       return $( $.parseHTML( src.trim() ) );
+                               }
+                       };
+               }
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.template.regexp.js b/resources/src/mediawiki.template.regexp.js
new file mode 100644 (file)
index 0000000..3ec0a1f
--- /dev/null
@@ -0,0 +1,15 @@
+mediaWiki.template.registerCompiler( 'regexp', {
+       compile: function ( src ) {
+               return {
+                       render: function () {
+                               return new RegExp(
+                                       src
+                                               // Remove whitespace
+                                               .replace( /\s+/g, '' )
+                                               // Remove named capturing groups
+                                               .replace( /\?<\w+?>/g, '' )
+                               );
+                       }
+               };
+       }
+} );
diff --git a/resources/src/mediawiki/ForeignApi.js b/resources/src/mediawiki/ForeignApi.js
deleted file mode 100644 (file)
index 1a3cdd5..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-( function ( mw, $ ) {
-
-       /**
-        * Create an object like mw.Api, but automatically handling everything required to communicate
-        * with another MediaWiki wiki via cross-origin requests (CORS).
-        *
-        * The foreign wiki must be configured to accept requests from the current wiki. See
-        * <https://www.mediawiki.org/wiki/Manual:$wgCrossSiteAJAXdomains> for details.
-        *
-        *     var api = new mw.ForeignApi( 'https://commons.wikimedia.org/w/api.php' );
-        *     api.get( {
-        *         action: 'query',
-        *         meta: 'userinfo'
-        *     } ).done( function ( data ) {
-        *         console.log( data );
-        *     } );
-        *
-        * To ensure that the user at the foreign wiki is logged in, pass the `assert: 'user'` parameter
-        * to #get/#post (since MW 1.23): if they are not, the API request will fail. (Note that this
-        * doesn't guarantee that it's the same user.)
-        *
-        * Authentication-related MediaWiki extensions may extend this class to ensure that the user
-        * authenticated on the current wiki will be automatically authenticated on the foreign one. These
-        * extension modules should be registered using the ResourceLoaderForeignApiModules hook. See
-        * CentralAuth for a practical example. The general pattern to extend and override the name is:
-        *
-        *     function MyForeignApi() {};
-        *     OO.inheritClass( MyForeignApi, mw.ForeignApi );
-        *     mw.ForeignApi = MyForeignApi;
-        *
-        * @class mw.ForeignApi
-        * @extends mw.Api
-        * @since 1.26
-        *
-        * @constructor
-        * @param {string|mw.Uri} url URL pointing to another wiki's `api.php` endpoint.
-        * @param {Object} [options] See mw.Api.
-        * @param {Object} [options.anonymous=false] Perform all requests anonymously. Use this option if
-        *     the target wiki may otherwise not accept cross-origin requests, or if you don't need to
-        *     perform write actions or read restricted information and want to avoid the overhead.
-        *
-        * @author Bartosz DziewoÅ„ski
-        * @author Jon Robson
-        */
-       function CoreForeignApi( url, options ) {
-               if ( !url || $.isPlainObject( url ) ) {
-                       throw new Error( 'mw.ForeignApi() requires a `url` parameter' );
-               }
-
-               this.apiUrl = String( url );
-               this.anonymous = options && options.anonymous;
-
-               options = $.extend( /* deep=*/ true,
-                       {
-                               ajax: {
-                                       url: this.apiUrl,
-                                       xhrFields: {
-                                               withCredentials: !this.anonymous
-                                       }
-                               },
-                               parameters: {
-                                       // Add 'origin' query parameter to all requests.
-                                       origin: this.getOrigin()
-                               }
-                       },
-                       options
-               );
-
-               // Call parent constructor
-               CoreForeignApi.parent.call( this, options );
-       }
-
-       OO.inheritClass( CoreForeignApi, mw.Api );
-
-       /**
-        * Return the origin to use for API requests, in the required format (protocol, host and port, if
-        * any).
-        *
-        * @protected
-        * @return {string}
-        */
-       CoreForeignApi.prototype.getOrigin = function () {
-               var origin;
-               if ( this.anonymous ) {
-                       return '*';
-               }
-               origin = location.protocol + '//' + location.hostname;
-               if ( location.port ) {
-                       origin += ':' + location.port;
-               }
-               return origin;
-       };
-
-       /**
-        * @inheritdoc
-        */
-       CoreForeignApi.prototype.ajax = function ( parameters, ajaxOptions ) {
-               var url, origin, newAjaxOptions;
-
-               // 'origin' query parameter must be part of the request URI, and not just POST request body
-               if ( ajaxOptions.type === 'POST' ) {
-                       url = ( ajaxOptions && ajaxOptions.url ) || this.defaults.ajax.url;
-                       origin = ( parameters && parameters.origin ) || this.defaults.parameters.origin;
-                       url += ( url.indexOf( '?' ) !== -1 ? '&' : '?' ) +
-                               // Depending on server configuration, MediaWiki may forbid periods in URLs, due to an IE 6
-                               // XSS bug. So let's escape them here. See WebRequest::checkUrlExtension() and T30235.
-                               'origin=' + encodeURIComponent( origin ).replace( /\./g, '%2E' );
-                       newAjaxOptions = $.extend( {}, ajaxOptions, { url: url } );
-               } else {
-                       newAjaxOptions = ajaxOptions;
-               }
-
-               return CoreForeignApi.parent.prototype.ajax.call( this, parameters, newAjaxOptions );
-       };
-
-       // Expose
-       mw.ForeignApi = CoreForeignApi;
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api.js b/resources/src/mediawiki/api.js
deleted file mode 100644 (file)
index 0038ed8..0000000
+++ /dev/null
@@ -1,506 +0,0 @@
-( function ( mw, $ ) {
-
-       /**
-        * @class mw.Api
-        */
-
-       /**
-        * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
-        *     `options` to mw.Api constructor.
-        * @property {Object} defaultOptions.parameters Default query parameters for API requests.
-        * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
-        * @property {boolean} defaultOptions.useUS Whether to use U+001F when joining multi-valued
-        *     parameters (since 1.28). Default is true if ajax.url is not set, false otherwise for
-        *     compatibility.
-        * @private
-        */
-       var defaultOptions = {
-                       parameters: {
-                               action: 'query',
-                               format: 'json'
-                       },
-                       ajax: {
-                               url: mw.util.wikiScript( 'api' ),
-                               timeout: 30 * 1000, // 30 seconds
-                               dataType: 'json'
-                       }
-               },
-
-               // Keyed by ajax url and symbolic name for the individual request
-               promises = {};
-
-       function mapLegacyToken( action ) {
-               // Legacy types for backward-compatibility with API action=tokens.
-               var csrfActions = [
-                       'edit',
-                       'delete',
-                       'protect',
-                       'move',
-                       'block',
-                       'unblock',
-                       'email',
-                       'import',
-                       'options'
-               ];
-               if ( csrfActions.indexOf( action ) !== -1 ) {
-                       mw.track( 'mw.deprecate', 'apitoken_' + action );
-                       mw.log.warn( 'Use of the "' + action + '" token is deprecated. Use "csrf" instead.' );
-                       return 'csrf';
-               }
-               return action;
-       }
-
-       // Pre-populate with fake ajax promises to save http requests for tokens
-       // we already have on the page via the user.tokens module (T36733).
-       promises[ defaultOptions.ajax.url ] = {};
-       $.each( mw.user.tokens.get(), function ( key, value ) {
-               // This requires #getToken to use the same key as user.tokens.
-               // Format: token-type + "Token" (eg. csrfToken, patrolToken, watchToken).
-               promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
-                       .resolve( value )
-                       .promise( { abort: function () {} } );
-       } );
-
-       /**
-        * Constructor to create an object to interact with the API of a particular MediaWiki server.
-        * mw.Api objects represent the API of a particular MediaWiki server.
-        *
-        *     var api = new mw.Api();
-        *     api.get( {
-        *         action: 'query',
-        *         meta: 'userinfo'
-        *     } ).done( function ( data ) {
-        *         console.log( data );
-        *     } );
-        *
-        * Since MW 1.25, multiple values for a parameter can be specified using an array:
-        *
-        *     var api = new mw.Api();
-        *     api.get( {
-        *         action: 'query',
-        *         meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo'
-        *     } ).done( function ( data ) {
-        *         console.log( data );
-        *     } );
-        *
-        * Since MW 1.26, boolean values for a parameter can be specified directly. If the value is
-        * `false` or `undefined`, the parameter will be omitted from the request, as required by the API.
-        *
-        * @constructor
-        * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
-        *  each individual request by passing them to #get or #post (or directly #ajax) later on.
-        */
-       mw.Api = function ( options ) {
-               options = options || {};
-
-               // Force a string if we got a mw.Uri object
-               if ( options.ajax && options.ajax.url !== undefined ) {
-                       options.ajax.url = String( options.ajax.url );
-               }
-
-               options = $.extend( { useUS: !options.ajax || !options.ajax.url }, options );
-
-               options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
-               options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
-
-               this.defaults = options;
-               this.requests = [];
-       };
-
-       mw.Api.prototype = {
-               /**
-                * Abort all unfinished requests issued by this Api object.
-                *
-                * @method
-                */
-               abort: function () {
-                       this.requests.forEach( function ( request ) {
-                               if ( request ) {
-                                       request.abort();
-                               }
-                       } );
-               },
-
-               /**
-                * Perform API get request
-                *
-                * @param {Object} parameters
-                * @param {Object} [ajaxOptions]
-                * @return {jQuery.Promise}
-                */
-               get: function ( parameters, ajaxOptions ) {
-                       ajaxOptions = ajaxOptions || {};
-                       ajaxOptions.type = 'GET';
-                       return this.ajax( parameters, ajaxOptions );
-               },
-
-               /**
-                * Perform API post request
-                *
-                * @param {Object} parameters
-                * @param {Object} [ajaxOptions]
-                * @return {jQuery.Promise}
-                */
-               post: function ( parameters, ajaxOptions ) {
-                       ajaxOptions = ajaxOptions || {};
-                       ajaxOptions.type = 'POST';
-                       return this.ajax( parameters, ajaxOptions );
-               },
-
-               /**
-                * Massage parameters from the nice format we accept into a format suitable for the API.
-                *
-                * NOTE: A value of undefined/null in an array will be represented by Array#join()
-                * as the empty string. Should we filter silently? Warn? Leave as-is?
-                *
-                * @private
-                * @param {Object} parameters (modified in-place)
-                * @param {boolean} useUS Whether to use U+001F when joining multi-valued parameters.
-                */
-               preprocessParameters: function ( parameters, useUS ) {
-                       var key;
-                       // Handle common MediaWiki API idioms for passing parameters
-                       for ( key in parameters ) {
-                               // Multiple values are pipe-separated
-                               if ( Array.isArray( parameters[ key ] ) ) {
-                                       if ( !useUS || parameters[ key ].join( '' ).indexOf( '|' ) === -1 ) {
-                                               parameters[ key ] = parameters[ key ].join( '|' );
-                                       } else {
-                                               parameters[ key ] = '\x1f' + parameters[ key ].join( '\x1f' );
-                                       }
-                               } else if ( parameters[ key ] === false || parameters[ key ] === undefined ) {
-                                       // Boolean values are only false when not given at all
-                                       delete parameters[ key ];
-                               }
-                       }
-               },
-
-               /**
-                * Perform the API call.
-                *
-                * @param {Object} parameters
-                * @param {Object} [ajaxOptions]
-                * @return {jQuery.Promise} Done: API response data and the jqXHR object.
-                *  Fail: Error code
-                */
-               ajax: function ( parameters, ajaxOptions ) {
-                       var token, requestIndex,
-                               api = this,
-                               apiDeferred = $.Deferred(),
-                               xhr, key, formData;
-
-                       parameters = $.extend( {}, this.defaults.parameters, parameters );
-                       ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
-
-                       // Ensure that token parameter is last (per [[mw:API:Edit#Token]]).
-                       if ( parameters.token ) {
-                               token = parameters.token;
-                               delete parameters.token;
-                       }
-
-                       this.preprocessParameters( parameters, this.defaults.useUS );
-
-                       // If multipart/form-data has been requested and emulation is possible, emulate it
-                       if (
-                               ajaxOptions.type === 'POST' &&
-                               window.FormData &&
-                               ajaxOptions.contentType === 'multipart/form-data'
-                       ) {
-
-                               formData = new FormData();
-
-                               for ( key in parameters ) {
-                                       formData.append( key, parameters[ key ] );
-                               }
-                               // If we extracted a token parameter, add it back in.
-                               if ( token ) {
-                                       formData.append( 'token', token );
-                               }
-
-                               ajaxOptions.data = formData;
-
-                               // Prevent jQuery from mangling our FormData object
-                               ajaxOptions.processData = false;
-                               // Prevent jQuery from overriding the Content-Type header
-                               ajaxOptions.contentType = false;
-                       } else {
-                               // This works because jQuery accepts data as a query string or as an Object
-                               ajaxOptions.data = $.param( parameters );
-                               // If we extracted a token parameter, add it back in.
-                               if ( token ) {
-                                       ajaxOptions.data += '&token=' + encodeURIComponent( token );
-                               }
-
-                               // Depending on server configuration, MediaWiki may forbid periods in URLs, due to an IE 6
-                               // XSS bug. So let's escape them here. See WebRequest::checkUrlExtension() and T30235.
-                               ajaxOptions.data = ajaxOptions.data.replace( /\./g, '%2E' );
-
-                               if ( ajaxOptions.contentType === 'multipart/form-data' ) {
-                                       // We were asked to emulate but can't, so drop the Content-Type header, otherwise
-                                       // it'll be wrong and the server will fail to decode the POST body
-                                       delete ajaxOptions.contentType;
-                               }
-                       }
-
-                       // Make the AJAX request
-                       xhr = $.ajax( ajaxOptions )
-                               // If AJAX fails, reject API call with error code 'http'
-                               // and details in second argument.
-                               .fail( function ( xhr, textStatus, exception ) {
-                                       apiDeferred.reject( 'http', {
-                                               xhr: xhr,
-                                               textStatus: textStatus,
-                                               exception: exception
-                                       } );
-                               } )
-                               // AJAX success just means "200 OK" response, also check API error codes
-                               .done( function ( result, textStatus, jqXHR ) {
-                                       var code;
-                                       if ( result === undefined || result === null || result === '' ) {
-                                               apiDeferred.reject( 'ok-but-empty',
-                                                       'OK response but empty result (check HTTP headers?)',
-                                                       result,
-                                                       jqXHR
-                                               );
-                                       } else if ( result.error ) {
-                                               // errorformat=bc
-                                               code = result.error.code === undefined ? 'unknown' : result.error.code;
-                                               apiDeferred.reject( code, result, result, jqXHR );
-                                       } else if ( result.errors ) {
-                                               // errorformat!=bc
-                                               code = result.errors[ 0 ].code === undefined ? 'unknown' : result.errors[ 0 ].code;
-                                               apiDeferred.reject( code, result, result, jqXHR );
-                                       } else {
-                                               apiDeferred.resolve( result, jqXHR );
-                                       }
-                               } );
-
-                       requestIndex = this.requests.length;
-                       this.requests.push( xhr );
-                       xhr.always( function () {
-                               api.requests[ requestIndex ] = null;
-                       } );
-                       // Return the Promise
-                       return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
-                               if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
-                                       mw.log( 'mw.Api error: ', code, details );
-                               }
-                       } );
-               },
-
-               /**
-                * Post to API with specified type of token. If we have no token, get one and try to post.
-                * If we have a cached token try using that, and if it fails, blank out the
-                * cached token and start over. For example to change an user option you could do:
-                *
-                *     new mw.Api().postWithToken( 'csrf', {
-                *         action: 'options',
-                *         optionname: 'gender',
-                *         optionvalue: 'female'
-                *     } );
-                *
-                * @param {string} tokenType The name of the token, like options or edit.
-                * @param {Object} params API parameters
-                * @param {Object} [ajaxOptions]
-                * @return {jQuery.Promise} See #post
-                * @since 1.22
-                */
-               postWithToken: function ( tokenType, params, ajaxOptions ) {
-                       var api = this,
-                               abortedPromise = $.Deferred().reject( 'http',
-                                       { textStatus: 'abort', exception: 'abort' } ).promise(),
-                               abortable,
-                               aborted;
-
-                       return api.getToken( tokenType, params.assert ).then( function ( token ) {
-                               params.token = token;
-                               // Request was aborted while token request was running, but we
-                               // don't want to unnecessarily abort token requests, so abort
-                               // a fake request instead
-                               if ( aborted ) {
-                                       return abortedPromise;
-                               }
-
-                               return ( abortable = api.post( params, ajaxOptions ) ).catch(
-                                       // Error handler
-                                       function ( code ) {
-                                               if ( code === 'badtoken' ) {
-                                                       api.badToken( tokenType );
-                                                       // Try again, once
-                                                       params.token = undefined;
-                                                       abortable = null;
-                                                       return api.getToken( tokenType, params.assert ).then( function ( token ) {
-                                                               params.token = token;
-                                                               if ( aborted ) {
-                                                                       return abortedPromise;
-                                                               }
-
-                                                               return ( abortable = api.post( params, ajaxOptions ) );
-                                                       } );
-                                               }
-
-                                               // Let caller handle the error code
-                                               return $.Deferred().rejectWith( this, arguments );
-                                       }
-                               );
-                       } ).promise( { abort: function () {
-                               if ( abortable ) {
-                                       abortable.abort();
-                               } else {
-                                       aborted = true;
-                               }
-                       } } );
-               },
-
-               /**
-                * Get a token for a certain action from the API.
-                *
-                * The assert parameter is only for internal use by #postWithToken.
-                *
-                * @since 1.22
-                * @param {string} type Token type
-                * @param {string} [assert]
-                * @return {jQuery.Promise} Received token.
-                */
-               getToken: function ( type, assert ) {
-                       var apiPromise, promiseGroup, d, reject;
-                       type = mapLegacyToken( type );
-                       promiseGroup = promises[ this.defaults.ajax.url ];
-                       d = promiseGroup && promiseGroup[ type + 'Token' ];
-
-                       if ( !promiseGroup ) {
-                               promiseGroup = promises[ this.defaults.ajax.url ] = {};
-                       }
-
-                       if ( !d ) {
-                               apiPromise = this.get( {
-                                       action: 'query',
-                                       meta: 'tokens',
-                                       type: type,
-                                       assert: assert
-                               } );
-                               reject = function () {
-                                       // Clear promise. Do not cache errors.
-                                       delete promiseGroup[ type + 'Token' ];
-
-                                       // Let caller handle the error code
-                                       return $.Deferred().rejectWith( this, arguments );
-                               };
-                               d = apiPromise
-                                       .then( function ( res ) {
-                                               if ( !res.query ) {
-                                                       return reject( 'query-missing', res );
-                                               }
-                                               // If token type is unknown, it is omitted from the response
-                                               if ( !res.query.tokens[ type + 'token' ] ) {
-                                                       return $.Deferred().reject( 'token-missing', res );
-                                               }
-                                               return res.query.tokens[ type + 'token' ];
-                                       }, reject )
-                                       // Attach abort handler
-                                       .promise( { abort: apiPromise.abort } );
-
-                               // Store deferred now so that we can use it again even if it isn't ready yet
-                               promiseGroup[ type + 'Token' ] = d;
-                       }
-
-                       return d;
-               },
-
-               /**
-                * Indicate that the cached token for a certain action of the API is bad.
-                *
-                * Call this if you get a 'badtoken' error when using the token returned by #getToken.
-                * You may also want to use #postWithToken instead, which invalidates bad cached tokens
-                * automatically.
-                *
-                * @param {string} type Token type
-                * @since 1.26
-                */
-               badToken: function ( type ) {
-                       var promiseGroup = promises[ this.defaults.ajax.url ];
-
-                       type = mapLegacyToken( type );
-                       if ( promiseGroup ) {
-                               delete promiseGroup[ type + 'Token' ];
-                       }
-               }
-       };
-
-       /**
-        * @static
-        * @property {Array}
-        * Very incomplete and outdated list of errors we might receive from the API. Do not use.
-        * @deprecated since 1.29
-        */
-       mw.Api.errors = [
-               // occurs when POST aborted
-               // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
-               'ok-but-empty',
-
-               // timeout
-               'timeout',
-
-               // really a warning, but we treat it like an error
-               'duplicate',
-               'duplicate-archive',
-
-               // upload succeeded, but no image info.
-               // this is probably impossible, but might as well check for it
-               'noimageinfo',
-               // remote errors, defined in API
-               'uploaddisabled',
-               'nomodule',
-               'mustbeposted',
-               'badaccess-groups',
-               'missingresult',
-               'missingparam',
-               'invalid-file-key',
-               'copyuploaddisabled',
-               'mustbeloggedin',
-               'empty-file',
-               'file-too-large',
-               'filetype-missing',
-               'filetype-banned',
-               'filetype-banned-type',
-               'filename-tooshort',
-               'illegal-filename',
-               'verification-error',
-               'hookaborted',
-               'unknown-error',
-               'internal-error',
-               'overwrite',
-               'badtoken',
-               'fetchfileerror',
-               'fileexists-shared-forbidden',
-               'invalidtitle',
-               'notloggedin',
-               'autoblocked',
-               'blocked',
-
-               // Stash-specific errors - expanded
-               'stashfailed',
-               'stasherror',
-               'stashedfilenotfound',
-               'stashpathinvalid',
-               'stashfilestorage',
-               'stashzerolength',
-               'stashnotloggedin',
-               'stashwrongowner',
-               'stashnosuchfilekey'
-       ];
-       mw.log.deprecate( mw.Api, 'errors', mw.Api.errors, null, 'mw.Api.errors' );
-
-       /**
-        * @static
-        * @property {Array}
-        * Very incomplete and outdated list of warnings we might receive from the API. Do not use.
-        * @deprecated since 1.29
-        */
-       mw.Api.warnings = [
-               'duplicate',
-               'exists'
-       ];
-       mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings, null, 'mw.Api.warnings' );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/category.js b/resources/src/mediawiki/api/category.js
deleted file mode 100644 (file)
index 85df90e..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * @class mw.Api.plugin.category
- */
-( function ( mw, $ ) {
-
-       $.extend( mw.Api.prototype, {
-               /**
-                * Determine if a category exists.
-                *
-                * @param {mw.Title|string} title
-                * @return {jQuery.Promise}
-                * @return {Function} return.done
-                * @return {boolean} return.done.isCategory Whether the category exists.
-                */
-               isCategory: function ( title ) {
-                       var apiPromise = this.get( {
-                               formatversion: 2,
-                               prop: 'categoryinfo',
-                               titles: [ String( title ) ]
-                       } );
-
-                       return apiPromise
-                               .then( function ( data ) {
-                                       return !!(
-                                               data.query && // query is missing on title=""
-                                               data.query.pages && // query.pages is missing on title="#" or title="mw:"
-                                               data.query.pages[ 0 ].categoryinfo
-                                       );
-                               } )
-                               .promise( { abort: apiPromise.abort } );
-               },
-
-               /**
-                * Get a list of categories that match a certain prefix.
-                *
-                * E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
-                *
-                * @param {string} prefix Prefix to match.
-                * @return {jQuery.Promise}
-                * @return {Function} return.done
-                * @return {string[]} return.done.categories Matched categories
-                */
-               getCategoriesByPrefix: function ( prefix ) {
-                       // Fetch with allpages to only get categories that have a corresponding description page.
-                       var apiPromise = this.get( {
-                               formatversion: 2,
-                               list: 'allpages',
-                               apprefix: prefix,
-                               apnamespace: mw.config.get( 'wgNamespaceIds' ).category
-                       } );
-
-                       return apiPromise
-                               .then( function ( data ) {
-                                       return data.query.allpages.map( function ( category ) {
-                                               return new mw.Title( category.title ).getMainText();
-                                       } );
-                               } )
-                               .promise( { abort: apiPromise.abort } );
-               },
-
-               /**
-                * Get the categories that a particular page on the wiki belongs to.
-                *
-                * @param {mw.Title|string} title
-                * @return {jQuery.Promise}
-                * @return {Function} return.done
-                * @return {boolean|mw.Title[]} return.done.categories List of category titles or false
-                *  if title was not found.
-                */
-               getCategories: function ( title ) {
-                       var apiPromise = this.get( {
-                               formatversion: 2,
-                               prop: 'categories',
-                               titles: [ String( title ) ]
-                       } );
-
-                       return apiPromise
-                               .then( function ( data ) {
-                                       var page;
-
-                                       if ( !data.query || !data.query.pages ) {
-                                               return false;
-                                       }
-                                       page = data.query.pages[ 0 ];
-                                       if ( !page.categories ) {
-                                               return false;
-                                       }
-                                       return page.categories.map( function ( cat ) {
-                                               return new mw.Title( cat.title );
-                                       } );
-                               } )
-                               .promise( { abort: apiPromise.abort } );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.category
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/edit.js b/resources/src/mediawiki/api/edit.js
deleted file mode 100644 (file)
index 21c55c7..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-/**
- * @class mw.Api.plugin.edit
- */
-( function ( mw, $ ) {
-
-       $.extend( mw.Api.prototype, {
-
-               /**
-                * Post to API with csrf token. If we have no token, get one and try to post.
-                * If we have a cached token try using that, and if it fails, blank out the
-                * cached token and start over.
-                *
-                * @param {Object} params API parameters
-                * @param {Object} [ajaxOptions]
-                * @return {jQuery.Promise} See #post
-                */
-               postWithEditToken: function ( params, ajaxOptions ) {
-                       return this.postWithToken( 'csrf', params, ajaxOptions );
-               },
-
-               /**
-                * API helper to grab a csrf token.
-                *
-                * @return {jQuery.Promise} Received token.
-                */
-               getEditToken: function () {
-                       return this.getToken( 'csrf' );
-               },
-
-               /**
-                * Create a new page.
-                *
-                * Example:
-                *
-                *     new mw.Api().create( 'Sandbox',
-                *         { summary: 'Load sand particles.' },
-                *         'Sand.'
-                *     );
-                *
-                * @since 1.28
-                * @param {mw.Title|string} title Page title
-                * @param {Object} params Edit API parameters
-                * @param {string} params.summary Edit summary
-                * @param {string} content
-                * @return {jQuery.Promise} API response
-                */
-               create: function ( title, params, content ) {
-                       return this.postWithEditToken( $.extend( {
-                               action: 'edit',
-                               title: String( title ),
-                               text: content,
-                               formatversion: '2',
-
-                               // Protect against errors and conflicts
-                               assert: mw.user.isAnon() ? undefined : 'user',
-                               createonly: true
-                       }, params ) ).then( function ( data ) {
-                               return data.edit;
-                       } );
-               },
-
-               /**
-                * Edit an existing page.
-                *
-                * To create a new page, use #create() instead.
-                *
-                * Simple transformation:
-                *
-                *     new mw.Api()
-                *         .edit( 'Sandbox', function ( revision ) {
-                *             return revision.content.replace( 'foo', 'bar' );
-                *         } )
-                *         .then( function () {
-                *             console.log( 'Saved! ');
-                *         } );
-                *
-                * Set save parameters by returning an object instead of a string:
-                *
-                *     new mw.Api().edit(
-                *         'Sandbox',
-                *         function ( revision ) {
-                *             return {
-                *                 text: revision.content.replace( 'foo', 'bar' ),
-                *                 summary: 'Replace "foo" with "bar".',
-                *                 assert: 'bot',
-                *                 minor: true
-                *             };
-                *         }
-                *     )
-                *     .then( function () {
-                *         console.log( 'Saved! ');
-                *     } );
-                *
-                * Transform asynchronously by returning a promise.
-                *
-                *     new mw.Api()
-                *         .edit( 'Sandbox', function ( revision ) {
-                *             return Spelling
-                *                 .corrections( revision.content )
-                *                 .then( function ( report ) {
-                *                     return {
-                *                         text: report.output,
-                *                         summary: report.changelog
-                *                     };
-                *                 } );
-                *         } )
-                *         .then( function () {
-                *             console.log( 'Saved! ');
-                *         } );
-                *
-                * @since 1.28
-                * @param {mw.Title|string} title Page title
-                * @param {Function} transform Callback that prepares the edit
-                * @param {Object} transform.revision Current revision
-                * @param {string} transform.revision.content Current revision content
-                * @param {string|Object|jQuery.Promise} transform.return New content, object with edit
-                *  API parameters, or promise providing one of those.
-                * @return {jQuery.Promise} Edit API response
-                */
-               edit: function ( title, transform ) {
-                       var basetimestamp, curtimestamp,
-                               api = this;
-
-                       title = String( title );
-
-                       return api.get( {
-                               action: 'query',
-                               prop: 'revisions',
-                               rvprop: [ 'content', 'timestamp' ],
-                               titles: [ title ],
-                               formatversion: '2',
-                               curtimestamp: true
-                       } )
-                               .then( function ( data ) {
-                                       var page, revision;
-                                       if ( !data.query || !data.query.pages ) {
-                                               return $.Deferred().reject( 'unknown' );
-                                       }
-                                       page = data.query.pages[ 0 ];
-                                       if ( !page || page.invalid ) {
-                                               return $.Deferred().reject( 'invalidtitle' );
-                                       }
-                                       if ( page.missing ) {
-                                               return $.Deferred().reject( 'nocreate-missing' );
-                                       }
-                                       revision = page.revisions[ 0 ];
-                                       basetimestamp = revision.timestamp;
-                                       curtimestamp = data.curtimestamp;
-                                       return transform( {
-                                               timestamp: revision.timestamp,
-                                               content: revision.content
-                                       } );
-                               } )
-                               .then( function ( params ) {
-                                       var editParams = typeof params === 'object' ? params : { text: String( params ) };
-                                       return api.postWithEditToken( $.extend( {
-                                               action: 'edit',
-                                               title: title,
-                                               formatversion: '2',
-
-                                               // Protect against errors and conflicts
-                                               assert: mw.user.isAnon() ? undefined : 'user',
-                                               basetimestamp: basetimestamp,
-                                               starttimestamp: curtimestamp,
-                                               nocreate: true
-                                       }, editParams ) );
-                               } )
-                               .then( function ( data ) {
-                                       return data.edit;
-                               } );
-               },
-
-               /**
-                * 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 }`
-                * @return {jQuery.Promise}
-                */
-               newSection: function ( title, header, message, additionalParams ) {
-                       return this.postWithEditToken( $.extend( {
-                               action: 'edit',
-                               section: 'new',
-                               title: String( title ),
-                               summary: header,
-                               text: message
-                       }, additionalParams ) );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.edit
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/login.js b/resources/src/mediawiki/api/login.js
deleted file mode 100644 (file)
index 2b709aa..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Make the two-step login easier.
- *
- * @author Niklas Laxström
- * @class mw.Api.plugin.login
- * @since 1.22
- */
-( function ( mw, $ ) {
-       'use strict';
-
-       $.extend( mw.Api.prototype, {
-               /**
-                * @param {string} username
-                * @param {string} password
-                * @return {jQuery.Promise} See mw.Api#post
-                */
-               login: function ( username, password ) {
-                       var params, apiPromise, innerPromise,
-                               api = this;
-
-                       params = {
-                               action: 'login',
-                               lgname: username,
-                               lgpassword: password
-                       };
-
-                       apiPromise = api.post( params );
-
-                       return apiPromise
-                               .then( function ( data ) {
-                                       params.lgtoken = data.login.token;
-                                       innerPromise = api.post( params )
-                                               .then( function ( data ) {
-                                                       var code;
-                                                       if ( data.login.result !== 'Success' ) {
-                                                               // Set proper error code whenever possible
-                                                               code = data.error && data.error.code || 'unknown';
-                                                               return $.Deferred().reject( code, data );
-                                                       }
-                                                       return data;
-                                               } );
-                                       return innerPromise;
-                               } )
-                               .promise( {
-                                       abort: function () {
-                                               apiPromise.abort();
-                                               if ( innerPromise ) {
-                                                       innerPromise.abort();
-                                               }
-                                       }
-                               } );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.login
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/messages.js b/resources/src/mediawiki/api/messages.js
deleted file mode 100644 (file)
index 688f0b2..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Allows to retrieve a specific or a set of
- * messages to be added to mw.messages and returned
- * by the Api.
- *
- * @class mw.Api.plugin.messages
- * @since 1.27
- */
-( function ( mw, $ ) {
-       'use strict';
-
-       $.extend( mw.Api.prototype, {
-               /**
-                * Get a set of messages.
-                *
-                * @param {Array} messages Messages to retrieve
-                * @param {Object} [options] Additional parameters for the API call
-                * @return {jQuery.Promise}
-                */
-               getMessages: function ( messages, options ) {
-                       options = options || {};
-                       return this.get( $.extend( {
-                               action: 'query',
-                               meta: 'allmessages',
-                               ammessages: messages,
-                               amlang: mw.config.get( 'wgUserLanguage' ),
-                               formatversion: 2
-                       }, options ) ).then( function ( data ) {
-                               var result = {};
-
-                               data.query.allmessages.forEach( function ( obj ) {
-                                       if ( !obj.missing ) {
-                                               result[ obj.name ] = obj.content;
-                                       }
-                               } );
-
-                               return result;
-                       } );
-               },
-
-               /**
-                * Loads a set of messages and add them to mw.messages.
-                *
-                * @param {Array} messages Messages to retrieve
-                * @param {Object} [options] Additional parameters for the API call
-                * @return {jQuery.Promise}
-                */
-               loadMessages: function ( messages, options ) {
-                       return this.getMessages( messages, options ).then( $.proxy( mw.messages, 'set' ) );
-               },
-
-               /**
-                * Loads a set of messages and add them to mw.messages. Only messages that are not already known
-                * are loaded. If all messages are known, the returned promise is resolved immediately.
-                *
-                * @param {Array} messages Messages to retrieve
-                * @param {Object} [options] Additional parameters for the API call
-                * @return {jQuery.Promise}
-                */
-               loadMessagesIfMissing: function ( messages, options ) {
-                       var missing = messages.filter( function ( msg ) {
-                               return !mw.message( msg ).exists();
-                       } );
-
-                       if ( missing.length === 0 ) {
-                               return $.Deferred().resolve();
-                       }
-
-                       return this.getMessages( missing, options ).then( $.proxy( mw.messages, 'set' ) );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.messages
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/options.js b/resources/src/mediawiki/api/options.js
deleted file mode 100644 (file)
index 4930c4f..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * @class mw.Api.plugin.options
- */
-( function ( mw, $ ) {
-
-       $.extend( mw.Api.prototype, {
-
-               /**
-                * Asynchronously save the value of a single user option using the API. See #saveOptions.
-                *
-                * @param {string} name
-                * @param {string|null} value
-                * @return {jQuery.Promise}
-                */
-               saveOption: function ( name, value ) {
-                       var param = {};
-                       param[ name ] = value;
-                       return this.saveOptions( param );
-               },
-
-               /**
-                * Asynchronously save the values of user options using the API.
-                *
-                * If a value of `null` is provided, the given option will be reset to the default value.
-                *
-                * Any warnings returned by the API, including warnings about invalid option names or values,
-                * are ignored. However, do not rely on this behavior.
-                *
-                * If necessary, the options will be saved using several sequential API requests. Only one promise
-                * is always returned that will be resolved when all requests complete.
-                *
-                * @param {Object} options Options as a `{ name: value, â€¦ }` object
-                * @return {jQuery.Promise}
-                */
-               saveOptions: function ( options ) {
-                       var name, value, bundleable,
-                               grouped = [],
-                               promise = $.Deferred().resolve();
-
-                       for ( name in options ) {
-                               value = options[ name ] === null ? null : String( options[ name ] );
-
-                               // Can we bundle this option, or does it need a separate request?
-                               if ( this.defaults.useUS ) {
-                                       bundleable = name.indexOf( '=' ) === -1;
-                               } else {
-                                       bundleable =
-                                               ( value === null || value.indexOf( '|' ) === -1 ) &&
-                                               ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
-                               }
-
-                               if ( bundleable ) {
-                                       if ( value !== null ) {
-                                               grouped.push( name + '=' + value );
-                                       } else {
-                                               // Omitting value resets the option
-                                               grouped.push( name );
-                                       }
-                               } else {
-                                       if ( value !== null ) {
-                                               promise = promise.then( function ( name, value ) {
-                                                       return this.postWithToken( 'csrf', {
-                                                               formatversion: 2,
-                                                               action: 'options',
-                                                               optionname: name,
-                                                               optionvalue: value
-                                                       } );
-                                               }.bind( this, name, value ) );
-                                       } else {
-                                               // Omitting value resets the option
-                                               promise = promise.then( function ( name ) {
-                                                       return this.postWithToken( 'csrf', {
-                                                               formatversion: 2,
-                                                               action: 'options',
-                                                               optionname: name
-                                                       } );
-                                               }.bind( this, name ) );
-                                       }
-                               }
-                       }
-
-                       if ( grouped.length ) {
-                               promise = promise.then( function () {
-                                       return this.postWithToken( 'csrf', {
-                                               formatversion: 2,
-                                               action: 'options',
-                                               change: grouped
-                                       } );
-                               }.bind( this ) );
-                       }
-
-                       return promise;
-               }
-
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.options
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/parse.js b/resources/src/mediawiki/api/parse.js
deleted file mode 100644 (file)
index f38e88b..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * @class mw.Api.plugin.parse
- */
-( function ( mw, $ ) {
-
-       $.extend( mw.Api.prototype, {
-               /**
-                * Convenience method for 'action=parse'.
-                *
-                * @param {string|mw.Title} content Content to parse, either as a wikitext string or
-                *   a mw.Title.
-                * @param {Object} additionalParams Parameters object to set custom settings, e.g.
-                *   redirects, sectionpreview.  prop should not be overridden.
-                * @return {jQuery.Promise}
-                * @return {Function} return.done
-                * @return {string} return.done.data Parsed HTML of `wikitext`.
-                */
-               parse: function ( content, additionalParams ) {
-                       var apiPromise,
-                               config = $.extend( {
-                                       formatversion: 2,
-                                       action: 'parse',
-                                       contentmodel: 'wikitext'
-                               }, additionalParams );
-
-                       if ( mw.Title && content instanceof mw.Title ) {
-                               // Parse existing page
-                               config.page = content.getPrefixedDb();
-                       } else {
-                               // Parse wikitext from input
-                               config.text = String( content );
-                       }
-
-                       apiPromise = this.get( config );
-
-                       return apiPromise
-                               .then( function ( data ) {
-                                       return data.parse.text;
-                               } )
-                               .promise( { abort: apiPromise.abort } );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.parse
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/rollback.js b/resources/src/mediawiki/api/rollback.js
deleted file mode 100644 (file)
index 322143d..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * @class mw.Api.plugin.rollback
- * @since 1.28
- */
-( function ( mw, $ ) {
-
-       $.extend( mw.Api.prototype, {
-               /**
-                * Convenience method for `action=rollback`.
-                *
-                * @param {string|mw.Title} page
-                * @param {string} user
-                * @param {Object} [params] Additional parameters
-                * @return {jQuery.Promise}
-                */
-               rollback: function ( page, user, params ) {
-                       return this.postWithToken( 'rollback', $.extend( {
-                               action: 'rollback',
-                               title: String( page ),
-                               user: user,
-                               uselang: mw.config.get( 'wgUserLanguage' )
-                       }, params ) ).then( function ( data ) {
-                               return data.rollback;
-                       } );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.rollback
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/upload.js b/resources/src/mediawiki/api/upload.js
deleted file mode 100644 (file)
index 29bd59a..0000000
+++ /dev/null
@@ -1,668 +0,0 @@
-/**
- * Provides an interface for uploading files to MediaWiki.
- *
- * @class mw.Api.plugin.upload
- * @singleton
- */
-( function ( mw, $ ) {
-       var nonce = 0,
-               fieldsAllowed = {
-                       stash: true,
-                       filekey: true,
-                       filename: true,
-                       comment: true,
-                       text: true,
-                       watchlist: true,
-                       ignorewarnings: true,
-                       chunk: true,
-                       offset: true,
-                       filesize: true,
-                       async: true
-               };
-
-       /**
-        * Get nonce for iframe IDs on the page.
-        *
-        * @private
-        * @return {number}
-        */
-       function getNonce() {
-               return nonce++;
-       }
-
-       /**
-        * Given a non-empty object, return one of its keys.
-        *
-        * @private
-        * @param {Object} obj
-        * @return {string}
-        */
-       function getFirstKey( obj ) {
-               var key;
-               for ( key in obj ) {
-                       if ( obj.hasOwnProperty( key ) ) {
-                               return key;
-                       }
-               }
-       }
-
-       /**
-        * Get new iframe object for an upload.
-        *
-        * @private
-        * @param {string} id
-        * @return {HTMLIframeElement}
-        */
-       function getNewIframe( id ) {
-               var frame = document.createElement( 'iframe' );
-               frame.id = id;
-               frame.name = id;
-               return frame;
-       }
-
-       /**
-        * Shortcut for getting hidden inputs
-        *
-        * @private
-        * @param {string} name
-        * @param {string} val
-        * @return {jQuery}
-        */
-       function getHiddenInput( name, val ) {
-               return $( '<input>' ).attr( 'type', 'hidden' )
-                       .attr( 'name', name )
-                       .val( val );
-       }
-
-       /**
-        * Process the result of the form submission, returned to an iframe.
-        * This is the iframe's onload event.
-        *
-        * @param {HTMLIframeElement} iframe Iframe to extract result from
-        * @return {Object} Response from the server. The return value may or may
-        *   not be an XMLDocument, this code was copied from elsewhere, so if you
-        *   see an unexpected return type, please file a bug.
-        */
-       function processIframeResult( iframe ) {
-               var json,
-                       doc = iframe.contentDocument || frames[ iframe.id ].document;
-
-               if ( doc.XMLDocument ) {
-                       // The response is a document property in IE
-                       return doc.XMLDocument;
-               }
-
-               if ( doc.body ) {
-                       // Get the json string
-                       // We're actually searching through an HTML doc here --
-                       // according to mdale we need to do this
-                       // because IE does not load JSON properly in an iframe
-                       json = $( doc.body ).find( 'pre' ).text();
-
-                       return JSON.parse( json );
-               }
-
-               // Response is a xml document
-               return doc;
-       }
-
-       function formDataAvailable() {
-               return window.FormData !== undefined &&
-                       window.File !== undefined &&
-                       window.File.prototype.slice !== undefined;
-       }
-
-       $.extend( mw.Api.prototype, {
-               /**
-                * Upload a file to MediaWiki.
-                *
-                * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
-                * iframe if it doesn't.
-                *
-                * Caveats of iframe upload:
-                * - The returned jQuery.Promise will not receive `progress` notifications during the upload
-                * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
-                * - You must pass a HTMLInputElement and not a File for it to be possible
-                *
-                * @param {HTMLInputElement|File|Blob} file HTML input type=file element with a file already inside
-                *  of it, or a File object.
-                * @param {Object} data Other upload options, see action=upload API docs for more
-                * @return {jQuery.Promise}
-                */
-               upload: function ( file, data ) {
-                       var isFileInput, canUseFormData;
-
-                       isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
-
-                       if ( formDataAvailable() && isFileInput && file.files ) {
-                               file = file.files[ 0 ];
-                       }
-
-                       if ( !file ) {
-                               throw new Error( 'No file' );
-                       }
-
-                       // Blobs are allowed in formdata uploads, it turns out
-                       canUseFormData = formDataAvailable() && ( file instanceof window.File || file instanceof window.Blob );
-
-                       if ( !isFileInput && !canUseFormData ) {
-                               throw new Error( 'Unsupported argument type passed to mw.Api.upload' );
-                       }
-
-                       if ( canUseFormData ) {
-                               return this.uploadWithFormData( file, data );
-                       }
-
-                       return this.uploadWithIframe( file, data );
-               },
-
-               /**
-                * Upload a file to MediaWiki with an iframe and a form.
-                *
-                * This method is necessary for browsers without the File/FormData
-                * APIs, and continues to work in browsers with those APIs.
-                *
-                * The rough sketch of how this method works is as follows:
-                * 1. An iframe is loaded with no content.
-                * 2. A form is submitted with the passed-in file input and some extras.
-                * 3. The MediaWiki API receives that form data, and sends back a response.
-                * 4. The response is sent to the iframe, because we set target=(iframe id)
-                * 5. The response is parsed out of the iframe's document, and passed back
-                *    through the promise.
-                *
-                * @private
-                * @param {HTMLInputElement} file The file input with a file in it.
-                * @param {Object} data Other upload options, see action=upload API docs for more
-                * @return {jQuery.Promise}
-                */
-               uploadWithIframe: function ( file, data ) {
-                       var key,
-                               tokenPromise = $.Deferred(),
-                               api = this,
-                               deferred = $.Deferred(),
-                               nonce = getNonce(),
-                               id = 'uploadframe-' + nonce,
-                               $form = $( '<form>' ),
-                               iframe = getNewIframe( id ),
-                               $iframe = $( iframe );
-
-                       for ( key in data ) {
-                               if ( !fieldsAllowed[ key ] ) {
-                                       delete data[ key ];
-                               }
-                       }
-
-                       data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
-                       $form.addClass( 'mw-api-upload-form' );
-
-                       $form.css( 'display', 'none' )
-                               .attr( {
-                                       action: this.defaults.ajax.url,
-                                       method: 'POST',
-                                       target: id,
-                                       enctype: 'multipart/form-data'
-                               } );
-
-                       $iframe.one( 'load', function () {
-                               $iframe.one( 'load', function () {
-                                       var result = processIframeResult( iframe );
-                                       deferred.notify( 1 );
-
-                                       if ( !result ) {
-                                               deferred.reject( 'ok-but-empty', 'No response from API on upload attempt.' );
-                                       } else if ( result.error ) {
-                                               if ( result.error.code === 'badtoken' ) {
-                                                       api.badToken( 'csrf' );
-                                               }
-
-                                               deferred.reject( result.error.code, result );
-                                       } else if ( result.upload && result.upload.warnings ) {
-                                               deferred.reject( getFirstKey( result.upload.warnings ), result );
-                                       } else {
-                                               deferred.resolve( result );
-                                       }
-                               } );
-                               tokenPromise.done( function () {
-                                       $form.submit();
-                               } );
-                       } );
-
-                       $iframe.on( 'error', function ( error ) {
-                               deferred.reject( 'http', error );
-                       } );
-
-                       $iframe.prop( 'src', 'about:blank' ).hide();
-
-                       file.name = 'file';
-
-                       $.each( data, function ( key, val ) {
-                               $form.append( getHiddenInput( key, val ) );
-                       } );
-
-                       if ( !data.filename && !data.stash ) {
-                               throw new Error( 'Filename not included in file data.' );
-                       }
-
-                       if ( this.needToken() ) {
-                               this.getEditToken().then( function ( token ) {
-                                       $form.append( getHiddenInput( 'token', token ) );
-                                       tokenPromise.resolve();
-                               }, tokenPromise.reject );
-                       } else {
-                               tokenPromise.resolve();
-                       }
-
-                       $( 'body' ).append( $form, $iframe );
-
-                       deferred.always( function () {
-                               $form.remove();
-                               $iframe.remove();
-                       } );
-
-                       return deferred.promise();
-               },
-
-               /**
-                * Uploads a file using the FormData API.
-                *
-                * @private
-                * @param {File} file
-                * @param {Object} data Other upload options, see action=upload API docs for more
-                * @return {jQuery.Promise}
-                */
-               uploadWithFormData: function ( file, data ) {
-                       var key, request,
-                               deferred = $.Deferred();
-
-                       for ( key in data ) {
-                               if ( !fieldsAllowed[ key ] ) {
-                                       delete data[ key ];
-                               }
-                       }
-
-                       data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
-                       if ( !data.chunk ) {
-                               data.file = file;
-                       }
-
-                       if ( !data.filename && !data.stash ) {
-                               throw new Error( 'Filename not included in file data.' );
-                       }
-
-                       // Use this.postWithEditToken() or this.post()
-                       request = this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
-                               // Use FormData (if we got here, we know that it's available)
-                               contentType: 'multipart/form-data',
-                               // No timeout (default from mw.Api is 30 seconds)
-                               timeout: 0,
-                               // Provide upload progress notifications
-                               xhr: function () {
-                                       var xhr = $.ajaxSettings.xhr();
-                                       if ( xhr.upload ) {
-                                               // need to bind this event before we open the connection (see note at
-                                               // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
-                                               xhr.upload.addEventListener( 'progress', function ( ev ) {
-                                                       if ( ev.lengthComputable ) {
-                                                               deferred.notify( ev.loaded / ev.total );
-                                                       }
-                                               } );
-                                       }
-                                       return xhr;
-                               }
-                       } )
-                               .done( function ( result ) {
-                                       deferred.notify( 1 );
-                                       if ( result.upload && result.upload.warnings ) {
-                                               deferred.reject( getFirstKey( result.upload.warnings ), result );
-                                       } else {
-                                               deferred.resolve( result );
-                                       }
-                               } )
-                               .fail( function ( errorCode, result ) {
-                                       deferred.notify( 1 );
-                                       deferred.reject( errorCode, result );
-                               } );
-
-                       return deferred.promise( { abort: request.abort } );
-               },
-
-               /**
-                * Upload a file in several chunks.
-                *
-                * @param {File} file
-                * @param {Object} data Other upload options, see action=upload API docs for more
-                * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
-                * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
-                * @return {jQuery.Promise}
-                */
-               chunkedUpload: function ( file, data, chunkSize, chunkRetries ) {
-                       var start, end, promise, next, active,
-                               deferred = $.Deferred();
-
-                       chunkSize = chunkSize === undefined ? 5 * 1024 * 1024 : chunkSize;
-                       chunkRetries = chunkRetries === undefined ? 1 : chunkRetries;
-
-                       if ( !data.filename ) {
-                               throw new Error( 'Filename not included in file data.' );
-                       }
-
-                       // Submit first chunk to get the filekey
-                       active = promise = this.uploadChunk( file, data, 0, chunkSize, '', chunkRetries )
-                               .done( chunkSize >= file.size ? deferred.resolve : null )
-                               .fail( deferred.reject )
-                               .progress( deferred.notify );
-
-                       // Now iteratively submit the rest of the chunks
-                       for ( start = chunkSize; start < file.size; start += chunkSize ) {
-                               end = Math.min( start + chunkSize, file.size );
-                               next = $.Deferred();
-
-                               // We could simply chain one this.uploadChunk after another with
-                               // .then(), but then we'd hit an `Uncaught RangeError: Maximum
-                               // call stack size exceeded` at as low as 1024 calls in Firefox
-                               // 47. This'll work around it, but comes with the drawback of
-                               // having to properly relay the results to the returned promise.
-                               // eslint-disable-next-line no-loop-func
-                               promise.done( function ( start, end, next, result ) {
-                                       var filekey = result.upload.filekey;
-                                       active = this.uploadChunk( file, data, start, end, filekey, chunkRetries )
-                                               .done( end === file.size ? deferred.resolve : next.resolve )
-                                               .fail( deferred.reject )
-                                               .progress( deferred.notify );
-                               // start, end & next must be bound to closure, or they'd have
-                               // changed by the time the promises are resolved
-                               }.bind( this, start, end, next ) );
-
-                               promise = next;
-                       }
-
-                       return deferred.promise( { abort: active.abort } );
-               },
-
-               /**
-                * Uploads 1 chunk.
-                *
-                * @private
-                * @param {File} file
-                * @param {Object} data Other upload options, see action=upload API docs for more
-                * @param {number} start Chunk start position
-                * @param {number} end Chunk end position
-                * @param {string} [filekey] File key, for follow-up chunks
-                * @param {number} [retries] Amount of times to retry request
-                * @return {jQuery.Promise}
-                */
-               uploadChunk: function ( file, data, start, end, filekey, retries ) {
-                       var upload,
-                               api = this,
-                               chunk = this.slice( file, start, end );
-
-                       // When uploading in chunks, we're going to be issuing a lot more
-                       // requests and there's always a chance of 1 getting dropped.
-                       // In such case, it could be useful to try again: a network hickup
-                       // doesn't necessarily have to result in upload failure...
-                       retries = retries === undefined ? 1 : retries;
-
-                       data.filesize = file.size;
-                       data.chunk = chunk;
-                       data.offset = start;
-
-                       // filekey must only be added when uploading follow-up chunks; the
-                       // first chunk should never have a filekey (it'll be generated)
-                       if ( filekey && start !== 0 ) {
-                               data.filekey = filekey;
-                       }
-
-                       upload = this.uploadWithFormData( file, data );
-                       return upload.then(
-                               null,
-                               function ( code, result ) {
-                                       var retry;
-
-                                       // uploadWithFormData will reject uploads with warnings, but
-                                       // these warnings could be "harmless" or recovered from
-                                       // (e.g. exists-normalized, when it'll be renamed later)
-                                       // In the case of (only) a warning, we still want to
-                                       // continue the chunked upload until it completes: then
-                                       // reject it - at least it's been fully uploaded by then and
-                                       // failure handlers have a complete result object (including
-                                       // possibly more warnings, e.g. duplicate)
-                                       // This matches .upload, which also completes the upload.
-                                       if ( result.upload && result.upload.warnings && code in result.upload.warnings ) {
-                                               if ( end === file.size ) {
-                                                       // uploaded last chunk = reject with result data
-                                                       return $.Deferred().reject( code, result );
-                                               } else {
-                                                       // still uploading chunks = resolve to keep going
-                                                       return $.Deferred().resolve( result );
-                                               }
-                                       }
-
-                                       if ( retries === 0 ) {
-                                               return $.Deferred().reject( code, result );
-                                       }
-
-                                       // If the call flat out failed, we may want to try again...
-                                       retry = api.uploadChunk.bind( this, file, data, start, end, filekey, retries - 1 );
-                                       return api.retry( code, result, retry );
-                               },
-                               function ( fraction ) {
-                                       // Since we're only uploading small parts of a file, we
-                                       // need to adjust the reported progress to reflect where
-                                       // we actually are in the combined upload
-                                       return ( start + fraction * ( end - start ) ) / file.size;
-                               }
-                       ).promise( { abort: upload.abort } );
-               },
-
-               /**
-                * Launch the upload anew if it failed because of network issues.
-                *
-                * @private
-                * @param {string} code Error code
-                * @param {Object} result API result
-                * @param {Function} callable
-                * @return {jQuery.Promise}
-                */
-               retry: function ( code, result, callable ) {
-                       var uploadPromise,
-                               retryTimer,
-                               deferred = $.Deferred(),
-                               // Wrap around the callable, so that once it completes, it'll
-                               // resolve/reject the promise we'll return
-                               retry = function () {
-                                       uploadPromise = callable();
-                                       uploadPromise.then( deferred.resolve, deferred.reject );
-                               };
-
-                       // Don't retry if the request failed because we aborted it (or if
-                       // it's another kind of request failure)
-                       if ( code !== 'http' || result.textStatus === 'abort' ) {
-                               return deferred.reject( code, result );
-                       }
-
-                       retryTimer = setTimeout( retry, 1000 );
-                       return deferred.promise( { abort: function () {
-                               // Clear the scheduled upload, or abort if already in flight
-                               if ( retryTimer ) {
-                                       clearTimeout( retryTimer );
-                               }
-                               if ( uploadPromise.abort ) {
-                                       uploadPromise.abort();
-                               }
-                       } } );
-               },
-
-               /**
-                * Slice a chunk out of a File object.
-                *
-                * @private
-                * @param {File} file
-                * @param {number} start
-                * @param {number} stop
-                * @return {Blob}
-                */
-               slice: function ( file, start, stop ) {
-                       if ( file.mozSlice ) {
-                               // FF <= 12
-                               return file.mozSlice( start, stop, file.type );
-                       } else if ( file.webkitSlice ) {
-                               // Chrome <= 20
-                               return file.webkitSlice( start, stop, file.type );
-                       } else {
-                               // On really old browser versions (before slice was prefixed),
-                               // slice() would take (start, length) instead of (start, end)
-                               // We'll ignore that here...
-                               return file.slice( start, stop, file.type );
-                       }
-               },
-
-               /**
-                * This function will handle how uploads to stash (via uploadToStash or
-                * chunkedUploadToStash) are resolved/rejected.
-                *
-                * After a successful stash, it'll resolve with a callback which, when
-                * called, will finalize the upload in stash (with the given data, or
-                * with additional/conflicting data)
-                *
-                * A failed stash can still be recovered from as long as 'filekey' is
-                * present. In that case, it'll also resolve with the callback to
-                * finalize the upload (all warnings are then ignored.)
-                * Otherwise, it'll just reject as you'd expect, with code & result.
-                *
-                * @private
-                * @param {jQuery.Promise} uploadPromise
-                * @param {Object} data
-                * @return {jQuery.Promise}
-                * @return {Function} return.finishUpload Call this function to finish the upload.
-                * @return {Object} return.finishUpload.data Additional data for the upload.
-                * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
-                * @return {Object} return.finishUpload.return.data API return value for the final upload
-                */
-               finishUploadToStash: function ( uploadPromise, data ) {
-                       var filekey,
-                               api = this;
-
-                       function finishUpload( moreData ) {
-                               return api.uploadFromStash( filekey, $.extend( data, moreData ) );
-                       }
-
-                       return uploadPromise.then(
-                               function ( result ) {
-                                       filekey = result.upload.filekey;
-                                       return finishUpload;
-                               },
-                               function ( errorCode, result ) {
-                                       if ( result && result.upload && result.upload.filekey ) {
-                                               // Ignore any warnings if 'filekey' was returned, that's all we care about
-                                               filekey = result.upload.filekey;
-                                               return $.Deferred().resolve( finishUpload );
-                                       }
-                                       return $.Deferred().reject( errorCode, result );
-                               }
-                       );
-               },
-
-               /**
-                * Upload a file to the stash.
-                *
-                * This function will return a promise, which when resolved, will pass back a function
-                * to finish the stash upload. You can call that function with an argument containing
-                * more, or conflicting, data to pass to the server. For example:
-                *
-                *     // upload a file to the stash with a placeholder filename
-                *     api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
-                *         // finish is now the function we can use to finalize the upload
-                *         // pass it a new filename from user input to override the initial value
-                *         finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
-                *             // the upload is complete, data holds the API response
-                *         } );
-                *     } );
-                *
-                * @param {File|HTMLInputElement} file
-                * @param {Object} [data]
-                * @return {jQuery.Promise}
-                * @return {Function} return.finishUpload Call this function to finish the upload.
-                * @return {Object} return.finishUpload.data Additional data for the upload.
-                * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
-                * @return {Object} return.finishUpload.return.data API return value for the final upload
-                */
-               uploadToStash: function ( file, data ) {
-                       var promise;
-
-                       if ( !data.filename ) {
-                               throw new Error( 'Filename not included in file data.' );
-                       }
-
-                       promise = this.upload( file, { stash: true, filename: data.filename } );
-
-                       return this.finishUploadToStash( promise, data );
-               },
-
-               /**
-                * Upload a file to the stash, in chunks.
-                *
-                * This function will return a promise, which when resolved, will pass back a function
-                * to finish the stash upload.
-                *
-                * @see #method-uploadToStash
-                * @param {File|HTMLInputElement} file
-                * @param {Object} [data]
-                * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
-                * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
-                * @return {jQuery.Promise}
-                * @return {Function} return.finishUpload Call this function to finish the upload.
-                * @return {Object} return.finishUpload.data Additional data for the upload.
-                * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
-                * @return {Object} return.finishUpload.return.data API return value for the final upload
-                */
-               chunkedUploadToStash: function ( file, data, chunkSize, chunkRetries ) {
-                       var promise;
-
-                       if ( !data.filename ) {
-                               throw new Error( 'Filename not included in file data.' );
-                       }
-
-                       promise = this.chunkedUpload(
-                               file,
-                               { stash: true, filename: data.filename },
-                               chunkSize,
-                               chunkRetries
-                       );
-
-                       return this.finishUploadToStash( promise, data );
-               },
-
-               /**
-                * Finish an upload in the stash.
-                *
-                * @param {string} filekey
-                * @param {Object} data
-                * @return {jQuery.Promise}
-                */
-               uploadFromStash: function ( filekey, data ) {
-                       data.filekey = filekey;
-                       data.action = 'upload';
-                       data.format = 'json';
-
-                       if ( !data.filename ) {
-                               throw new Error( 'Filename not included in file data.' );
-                       }
-
-                       return this.postWithEditToken( data ).then( function ( result ) {
-                               if ( result.upload && result.upload.warnings ) {
-                                       return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
-                               }
-                               return result;
-                       } );
-               },
-
-               needToken: function () {
-                       return true;
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.upload
-        */
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/user.js b/resources/src/mediawiki/api/user.js
deleted file mode 100644 (file)
index e7b4b6d..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * @class mw.Api.plugin.user
- * @since 1.27
- */
-( function ( mw, $ ) {
-
-       $.extend( mw.Api.prototype, {
-
-               /**
-                * Get the current user's groups and rights.
-                *
-                * @return {jQuery.Promise}
-                * @return {Function} return.done
-                * @return {Object} return.done.userInfo
-                * @return {string[]} return.done.userInfo.groups User groups that the current user belongs to
-                * @return {string[]} return.done.userInfo.rights Current user's rights
-                */
-               getUserInfo: function () {
-                       return this.get( {
-                               action: 'query',
-                               meta: 'userinfo',
-                               uiprop: [ 'groups', 'rights' ]
-                       } ).then( function ( data ) {
-                               if ( data.query && data.query.userinfo ) {
-                                       return data.query.userinfo;
-                               }
-                               return $.Deferred().reject().promise();
-                       } );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.user
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/api/watch.js b/resources/src/mediawiki/api/watch.js
deleted file mode 100644 (file)
index 025c111..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * @class mw.Api.plugin.watch
- * @since 1.19
- */
-( function ( mw, $ ) {
-
-       /**
-        * @private
-        * @static
-        * @context mw.Api
-        *
-        * @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
-        *  array thereof. If an array is passed, the return value passed to the promise will also be an
-        *  array of appropriate objects.
-        * @param {Object} [addParams]
-        * @return {jQuery.Promise}
-        * @return {Function} return.done
-        * @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
-        *  parameter)
-        * @return {string} return.done.watch.title Full pagename
-        * @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
-        */
-       function doWatchInternal( pages, addParams ) {
-               // XXX: Parameter addParams is undocumented because we inherit this
-               // documentation in the public method...
-               var apiPromise = this.postWithToken( 'watch',
-                       $.extend(
-                               {
-                                       formatversion: 2,
-                                       action: 'watch',
-                                       titles: Array.isArray( pages ) ? pages : String( pages )
-                               },
-                               addParams
-                       )
-               );
-
-               return apiPromise
-                       .then( function ( data ) {
-                               // If a single page was given (not an array) respond with a single item as well.
-                               return Array.isArray( pages ) ? data.watch : data.watch[ 0 ];
-                       } )
-                       .promise( { abort: apiPromise.abort } );
-       }
-
-       $.extend( mw.Api.prototype, {
-               /**
-                * Convenience method for `action=watch`.
-                *
-                * @inheritdoc #doWatchInternal
-                */
-               watch: function ( pages ) {
-                       return doWatchInternal.call( this, pages );
-               },
-
-               /**
-                * Convenience method for `action=watch&unwatch=1`.
-                *
-                * @inheritdoc #doWatchInternal
-                */
-               unwatch: function ( pages ) {
-                       return doWatchInternal.call( this, pages, { unwatch: 1 } );
-               }
-       } );
-
-       /**
-        * @class mw.Api
-        * @mixins mw.Api.plugin.watch
-        */
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js
deleted file mode 100644 (file)
index 177861e..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-( function ( mw, $, OO ) {
-       /**
-        * Used to represent an upload in progress on the frontend.
-        *
-        * This subclass will upload to a wiki using a structured metadata
-        * system similar to (or identical to) the one on Wikimedia Commons.
-        *
-        * See <https://commons.wikimedia.org/wiki/Commons:Structured_data> for
-        * a more detailed description of how that system works.
-        *
-        * **TODO: This currently only supports uploads under CC-BY-SA 4.0,
-        * and should really have support for more licenses.**
-        *
-        * @class mw.ForeignStructuredUpload
-        * @extends mw.ForeignUpload
-        *
-        * @constructor
-        * @param {string} [target]
-        * @param {Object} [apiconfig]
-        */
-       function ForeignStructuredUpload( target, apiconfig ) {
-               this.date = undefined;
-               this.descriptions = [];
-               this.categories = [];
-
-               // Config for uploads to local wiki.
-               // Can be overridden with foreign wiki config when #loadConfig is called.
-               this.config = mw.config.get( 'wgUploadDialog' );
-
-               mw.ForeignUpload.call( this, target, apiconfig );
-       }
-
-       OO.inheritClass( ForeignStructuredUpload, mw.ForeignUpload );
-
-       /**
-        * Get the configuration for the form and filepage from the foreign wiki, if any, and use it for
-        * this upload.
-        *
-        * @return {jQuery.Promise} Promise returning config object
-        */
-       ForeignStructuredUpload.prototype.loadConfig = function () {
-               var deferred,
-                       upload = this;
-
-               if ( this.configPromise ) {
-                       return this.configPromise;
-               }
-
-               if ( this.target === 'local' ) {
-                       deferred = $.Deferred();
-                       setTimeout( function () {
-                               // Resolve asynchronously, so that it's harder to accidentally write synchronous code that
-                               // will break for cross-wiki uploads
-                               deferred.resolve( upload.config );
-                       } );
-                       this.configPromise = deferred.promise();
-               } else {
-                       this.configPromise = this.apiPromise.then( function ( api ) {
-                               // Get the config from the foreign wiki
-                               return api.get( {
-                                       action: 'query',
-                                       meta: 'siteinfo',
-                                       siprop: 'uploaddialog',
-                                       // For convenient true/false booleans
-                                       formatversion: 2
-                               } ).then( function ( resp ) {
-                                       // Foreign wiki might be running a pre-1.27 MediaWiki, without support for this
-                                       if ( resp.query && resp.query.uploaddialog ) {
-                                               upload.config = resp.query.uploaddialog;
-                                               return upload.config;
-                                       } else {
-                                               return $.Deferred().reject( 'upload-foreign-cant-load-config' );
-                                       }
-                               }, function () {
-                                       return $.Deferred().reject( 'upload-foreign-cant-load-config' );
-                               } );
-                       } );
-               }
-
-               return this.configPromise;
-       };
-
-       /**
-        * Add categories to the upload.
-        *
-        * @param {string[]} categories Array of categories to which this upload will be added.
-        */
-       ForeignStructuredUpload.prototype.addCategories = function ( categories ) {
-               // The length of the array must be less than 10000.
-               // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push#Merging_two_arrays
-               Array.prototype.push.apply( this.categories, categories );
-       };
-
-       /**
-        * Empty the list of categories for the upload.
-        */
-       ForeignStructuredUpload.prototype.clearCategories = function () {
-               this.categories = [];
-       };
-
-       /**
-        * Add a description to the upload.
-        *
-        * @param {string} language The language code for the description's language. Must have a template on the target wiki to work properly.
-        * @param {string} description The description of the file.
-        */
-       ForeignStructuredUpload.prototype.addDescription = function ( language, description ) {
-               this.descriptions.push( {
-                       language: language,
-                       text: description
-               } );
-       };
-
-       /**
-        * Empty the list of descriptions for the upload.
-        */
-       ForeignStructuredUpload.prototype.clearDescriptions = function () {
-               this.descriptions = [];
-       };
-
-       /**
-        * Set the date of creation for the upload.
-        *
-        * @param {Date} date
-        */
-       ForeignStructuredUpload.prototype.setDate = function ( date ) {
-               this.date = date;
-       };
-
-       /**
-        * Get the text of the file page, to be created on upload. Brings together
-        * several different pieces of information to create useful text.
-        *
-        * @return {string}
-        */
-       ForeignStructuredUpload.prototype.getText = function () {
-               return this.config.format.filepage
-                       // Replace "named parameters" with the given information
-                       .replace( '$DESCRIPTION', this.getDescriptions() )
-                       .replace( '$DATE', this.getDate() )
-                       .replace( '$SOURCE', this.getSource() )
-                       .replace( '$AUTHOR', this.getUser() )
-                       .replace( '$LICENSE', this.getLicense() )
-                       .replace( '$CATEGORIES', this.getCategories() );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       ForeignStructuredUpload.prototype.getComment = function () {
-               var
-                       isLocal = this.target === 'local',
-                       comment = typeof this.config.comment === 'string' ?
-                               this.config.comment :
-                               this.config.comment[ isLocal ? 'local' : 'foreign' ];
-               return comment
-                       .replace( '$PAGENAME', mw.config.get( 'wgPageName' ) )
-                       .replace( '$HOST', location.host );
-       };
-
-       /**
-        * Gets the wikitext for the creation date of this upload.
-        *
-        * @private
-        * @return {string}
-        */
-       ForeignStructuredUpload.prototype.getDate = function () {
-               if ( !this.date ) {
-                       return '';
-               }
-
-               return this.date.toString();
-       };
-
-       /**
-        * Fetches the wikitext for any descriptions that have been added
-        * to the upload.
-        *
-        * @private
-        * @return {string}
-        */
-       ForeignStructuredUpload.prototype.getDescriptions = function () {
-               var upload = this;
-               return this.descriptions.map( function ( desc ) {
-                       return upload.config.format.description
-                               .replace( '$LANGUAGE', desc.language )
-                               .replace( '$TEXT', desc.text );
-               } ).join( '\n' );
-       };
-
-       /**
-        * Fetches the wikitext for the categories to which the upload will
-        * be added.
-        *
-        * @private
-        * @return {string}
-        */
-       ForeignStructuredUpload.prototype.getCategories = function () {
-               if ( this.categories.length === 0 ) {
-                       return this.config.format.uncategorized;
-               }
-
-               return this.categories.map( function ( cat ) {
-                       return '[[Category:' + cat + ']]';
-               } ).join( '\n' );
-       };
-
-       /**
-        * Gets the wikitext for the license of the upload.
-        *
-        * @private
-        * @return {string}
-        */
-       ForeignStructuredUpload.prototype.getLicense = function () {
-               return this.config.format.license;
-       };
-
-       /**
-        * Get the source. This should be some sort of localised text for "Own work".
-        *
-        * @private
-        * @return {string}
-        */
-       ForeignStructuredUpload.prototype.getSource = function () {
-               return this.config.format.ownwork;
-       };
-
-       /**
-        * Get the username.
-        *
-        * @private
-        * @return {string}
-        */
-       ForeignStructuredUpload.prototype.getUser = function () {
-               var username, namespace;
-               // Do not localise, we don't know the language of target wiki
-               namespace = 'User';
-               username = mw.config.get( 'wgUserName' );
-               if ( !username ) {
-                       // The user is not logged in locally. However, they might be logged in on the foreign wiki.
-                       // We should record their username there. (If they're not logged in there either, this will
-                       // record the IP address.) It's also possible that the user opened this dialog, got an error
-                       // about not being logged in, logged in in another browser tab, then continued uploading.
-                       username = '{{subst:REVISIONUSER}}';
-               }
-               return '[[' + namespace + ':' + username + '|' + username + ']]';
-       };
-
-       mw.ForeignStructuredUpload = ForeignStructuredUpload;
-}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki/mediawiki.ForeignUpload.js b/resources/src/mediawiki/mediawiki.ForeignUpload.js
deleted file mode 100644 (file)
index 08fc01d..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-( function ( mw, OO, $ ) {
-       /**
-        * Used to represent an upload in progress on the frontend.
-        *
-        * Subclassed to upload to a foreign API, with no other goodies. Use
-        * this for a generic foreign image repository on your wiki farm.
-        *
-        * Note you can provide the {@link #target target} or not - if the first argument is
-        * an object, we assume you want the default, and treat it as apiconfig
-        * instead.
-        *
-        * @class mw.ForeignUpload
-        * @extends mw.Upload
-        *
-        * @constructor
-        * @param {string} [target] Used to set up the target
-        *     wiki. If not remote, this class behaves identically to mw.Upload (unless further subclassed)
-        *     Use the same names as set in $wgForeignFileRepos for this. Also,
-        *     make sure there is an entry in the $wgForeignUploadTargets array for this name.
-        * @param {Object} [apiconfig] Passed to the constructor of mw.ForeignApi or mw.Api, as needed.
-        */
-       function ForeignUpload( target, apiconfig ) {
-               var api,
-                       validTargets = mw.config.get( 'wgForeignUploadTargets' ),
-                       upload = this;
-
-               if ( typeof target === 'object' ) {
-                       // target probably wasn't passed in, it must
-                       // be apiconfig
-                       apiconfig = target;
-                       target = undefined;
-               }
-
-               // * Use the given `target` first;
-               // * If not given, fall back to default (first) ForeignUploadTarget;
-               // * If none is configured, fall back to local uploads.
-               this.target = target || validTargets[ 0 ] || 'local';
-
-               // Now we have several different options.
-               // If the local wiki is the target, then we can skip a bunch of steps
-               // and just return an mw.Api object, because we don't need any special
-               // configuration for that.
-               // However, if the target is a remote wiki, we must check the API
-               // to confirm that the target is one that this site is configured to
-               // support.
-               if ( validTargets.length === 0 ) {
-                       this.apiPromise = $.Deferred().reject( 'upload-dialog-disabled' );
-               } else if ( this.target === 'local' ) {
-                       // If local uploads were requested, but they are disabled, fail.
-                       if ( !mw.config.get( 'wgEnableUploads' ) ) {
-                               this.apiPromise = $.Deferred().reject( 'uploaddisabledtext' );
-                       } else {
-                               // We'll ignore the CORS and centralauth stuff if the target is
-                               // the local wiki.
-                               this.apiPromise = $.Deferred().resolve( new mw.Api( apiconfig ) );
-                       }
-               } else {
-                       api = new mw.Api();
-                       this.apiPromise = api.get( {
-                               action: 'query',
-                               meta: 'filerepoinfo',
-                               friprop: [ 'name', 'scriptDirUrl', 'canUpload' ]
-                       } ).then( function ( data ) {
-                               var i, repo,
-                                       repos = data.query.repos;
-
-                               // First pass - try to find the passed-in target and check
-                               // that it's configured for uploads.
-                               for ( i in repos ) {
-                                       repo = repos[ i ];
-
-                                       // Skip repos that are not our target, or if they
-                                       // are the target, cannot be uploaded to.
-                                       if ( repo.name === upload.target && repo.canUpload === '' ) {
-                                               return new mw.ForeignApi(
-                                                       repo.scriptDirUrl + '/api.php',
-                                                       apiconfig
-                                               );
-                                       }
-                               }
-
-                               return $.Deferred().reject( 'upload-foreign-cant-upload' );
-                       } );
-               }
-
-               // Build the upload object without an API - this class overrides the
-               // actual API call methods to wait for the apiPromise to resolve
-               // before continuing.
-               mw.Upload.call( this, null );
-       }
-
-       OO.inheritClass( ForeignUpload, mw.Upload );
-
-       /**
-        * @property {string} target
-        * Used to specify the target repository of the upload.
-        *
-        * If you set this to something that isn't 'local', you must be sure to
-        * add that target to $wgForeignUploadTargets in LocalSettings, and the
-        * repository must be set up to use CORS and CentralAuth.
-        *
-        * Most wikis use "shared" to refer to Wikimedia Commons, we assume that
-        * in this class and in the messages linked to it.
-        *
-        * Defaults to the first available foreign upload target,
-        * or to local uploads if no foreign target is configured.
-        */
-
-       /**
-        * @inheritdoc
-        */
-       ForeignUpload.prototype.getApi = function () {
-               return this.apiPromise;
-       };
-
-       /**
-        * Override from mw.Upload to make sure the API info is found and allowed
-        *
-        * @inheritdoc
-        */
-       ForeignUpload.prototype.upload = function () {
-               var upload = this;
-               return this.apiPromise.then( function ( api ) {
-                       upload.api = api;
-                       return mw.Upload.prototype.upload.call( upload );
-               } );
-       };
-
-       /**
-        * Override from mw.Upload to make sure the API info is found and allowed
-        *
-        * @inheritdoc
-        */
-       ForeignUpload.prototype.uploadToStash = function () {
-               var upload = this;
-               return this.apiPromise.then( function ( api ) {
-                       upload.api = api;
-                       return mw.Upload.prototype.uploadToStash.call( upload );
-               } );
-       };
-
-       mw.ForeignUpload = ForeignUpload;
-}( mediaWiki, OO, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.Upload.Dialog.js b/resources/src/mediawiki/mediawiki.Upload.Dialog.js
deleted file mode 100644 (file)
index 00c04bc..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-( function ( $, mw ) {
-
-       /**
-        * mw.Upload.Dialog controls a {@link mw.Upload.BookletLayout BookletLayout}.
-        *
-        * ## Usage
-        *
-        * To use, setup a {@link OO.ui.WindowManager window manager} like for normal
-        * dialogs:
-        *
-        *     var uploadDialog = new mw.Upload.Dialog();
-        *     var windowManager = new OO.ui.WindowManager();
-        *     $( 'body' ).append( windowManager.$element );
-        *     windowManager.addWindows( [ uploadDialog ] );
-        *     windowManager.openWindow( uploadDialog );
-        *
-        * The dialog's closing promise can be used to get details of the upload.
-        *
-        * If you want to use a different OO.ui.BookletLayout, for example the
-        * mw.ForeignStructuredUpload.BookletLayout, like in the case of of the upload
-        * interface in VisualEditor, you can pass it in the {@link #cfg-bookletClass}:
-        *
-        *     var uploadDialog = new mw.Upload.Dialog( {
-        *         bookletClass: mw.ForeignStructuredUpload.BookletLayout
-        *     } );
-        *
-        *
-        * @class mw.Upload.Dialog
-        * @uses mw.Upload
-        * @uses mw.Upload.BookletLayout
-        * @extends OO.ui.ProcessDialog
-        *
-        * @constructor
-        * @param {Object} [config] Configuration options
-        * @cfg {Function} [bookletClass=mw.Upload.BookletLayout] Booklet class to be
-        *     used for the steps
-        * @cfg {Object} [booklet] Booklet constructor configuration
-        */
-       mw.Upload.Dialog = function ( config ) {
-               // Config initialization
-               config = $.extend( {
-                       bookletClass: mw.Upload.BookletLayout
-               }, config );
-
-               // Parent constructor
-               mw.Upload.Dialog.parent.call( this, config );
-
-               // Initialize
-               this.bookletClass = config.bookletClass;
-               this.bookletConfig = config.booklet;
-       };
-
-       /* Setup */
-
-       OO.inheritClass( mw.Upload.Dialog, OO.ui.ProcessDialog );
-
-       /* Static Properties */
-
-       /**
-        * @inheritdoc
-        * @property name
-        */
-       mw.Upload.Dialog.static.name = 'mwUploadDialog';
-
-       /**
-        * @inheritdoc
-        * @property title
-        */
-       mw.Upload.Dialog.static.title = mw.msg( 'upload-dialog-title' );
-
-       /**
-        * @inheritdoc
-        * @property actions
-        */
-       mw.Upload.Dialog.static.actions = [
-               {
-                       flags: 'safe',
-                       action: 'cancel',
-                       label: mw.msg( 'upload-dialog-button-cancel' ),
-                       modes: [ 'upload', 'insert' ]
-               },
-               {
-                       flags: 'safe',
-                       action: 'cancelupload',
-                       label: mw.msg( 'upload-dialog-button-back' ),
-                       modes: [ 'info' ]
-               },
-               {
-                       flags: [ 'primary', 'progressive' ],
-                       label: mw.msg( 'upload-dialog-button-done' ),
-                       action: 'insert',
-                       modes: 'insert'
-               },
-               {
-                       flags: [ 'primary', 'progressive' ],
-                       label: mw.msg( 'upload-dialog-button-save' ),
-                       action: 'save',
-                       modes: 'info'
-               },
-               {
-                       flags: [ 'primary', 'progressive' ],
-                       label: mw.msg( 'upload-dialog-button-upload' ),
-                       action: 'upload',
-                       modes: 'upload'
-               }
-       ];
-
-       /* Methods */
-
-       /**
-        * @inheritdoc
-        */
-       mw.Upload.Dialog.prototype.initialize = function () {
-               // Parent method
-               mw.Upload.Dialog.parent.prototype.initialize.call( this );
-
-               this.uploadBooklet = this.createUploadBooklet();
-               this.uploadBooklet.connect( this, {
-                       set: 'onUploadBookletSet',
-                       uploadValid: 'onUploadValid',
-                       infoValid: 'onInfoValid'
-               } );
-
-               this.$body.append( this.uploadBooklet.$element );
-       };
-
-       /**
-        * Create an upload booklet
-        *
-        * @protected
-        * @return {mw.Upload.BookletLayout} An upload booklet
-        */
-       mw.Upload.Dialog.prototype.createUploadBooklet = function () {
-               // eslint-disable-next-line new-cap
-               return new this.bookletClass( $.extend( {
-                       $overlay: this.$overlay
-               }, this.bookletConfig ) );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.Upload.Dialog.prototype.getBodyHeight = function () {
-               return 600;
-       };
-
-       /**
-        * Handle panelNameSet events from the upload booklet
-        *
-        * @protected
-        * @param {OO.ui.PageLayout} page Current page
-        */
-       mw.Upload.Dialog.prototype.onUploadBookletSet = function ( page ) {
-               this.actions.setMode( page.getName() );
-               this.actions.setAbilities( { upload: false, save: false } );
-       };
-
-       /**
-        * Handle uploadValid events
-        *
-        * {@link OO.ui.ActionSet#setAbilities Sets abilities}
-        * for the dialog accordingly.
-        *
-        * @protected
-        * @param {boolean} isValid The panel is complete and valid
-        */
-       mw.Upload.Dialog.prototype.onUploadValid = function ( isValid ) {
-               this.actions.setAbilities( { upload: isValid } );
-       };
-
-       /**
-        * Handle infoValid events
-        *
-        * {@link OO.ui.ActionSet#setAbilities Sets abilities}
-        * for the dialog accordingly.
-        *
-        * @protected
-        * @param {boolean} isValid The panel is complete and valid
-        */
-       mw.Upload.Dialog.prototype.onInfoValid = function ( isValid ) {
-               this.actions.setAbilities( { save: isValid } );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.Upload.Dialog.prototype.getSetupProcess = function ( data ) {
-               return mw.Upload.Dialog.parent.prototype.getSetupProcess.call( this, data )
-                       .next( function () {
-                               return this.uploadBooklet.initialize();
-                       }, this );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.Upload.Dialog.prototype.getActionProcess = function ( action ) {
-               var dialog = this;
-
-               if ( action === 'upload' ) {
-                       return new OO.ui.Process( this.uploadBooklet.uploadFile() );
-               }
-               if ( action === 'save' ) {
-                       return new OO.ui.Process( this.uploadBooklet.saveFile() );
-               }
-               if ( action === 'insert' ) {
-                       return new OO.ui.Process( function () {
-                               dialog.close( dialog.upload );
-                       } );
-               }
-               if ( action === 'cancel' ) {
-                       return new OO.ui.Process( this.close().closed );
-               }
-               if ( action === 'cancelupload' ) {
-                       return new OO.ui.Process( this.uploadBooklet.initialize() );
-               }
-
-               return mw.Upload.Dialog.parent.prototype.getActionProcess.call( this, action );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.Upload.Dialog.prototype.getTeardownProcess = function ( data ) {
-               return mw.Upload.Dialog.parent.prototype.getTeardownProcess.call( this, data )
-                       .next( function () {
-                               this.uploadBooklet.clear();
-                       }, this );
-       };
-}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.Upload.js b/resources/src/mediawiki/mediawiki.Upload.js
deleted file mode 100644 (file)
index 7e6cfb6..0000000
+++ /dev/null
@@ -1,393 +0,0 @@
-( function ( mw, $ ) {
-       var UP;
-
-       /**
-        * Used to represent an upload in progress on the frontend.
-        * Most of the functionality is implemented in mw.Api.plugin.upload,
-        * but this model class will tie it together as well as let you perform
-        * actions in a logical way.
-        *
-        * A simple example:
-        *
-        *     var file = new OO.ui.SelectFileWidget(),
-        *       button = new OO.ui.ButtonWidget( { label: 'Save' } ),
-        *       upload = new mw.Upload;
-        *
-        *     button.on( 'click', function () {
-        *       upload.setFile( file.getValue() );
-        *       upload.setFilename( file.getValue().name );
-        *       upload.upload();
-        *     } );
-        *
-        *     $( 'body' ).append( file.$element, button.$element );
-        *
-        * You can also choose to {@link #uploadToStash stash the upload} and
-        * {@link #finishStashUpload finalize} it later:
-        *
-        *     var file, // Some file object
-        *       upload = new mw.Upload,
-        *       stashPromise = $.Deferred();
-        *
-        *     upload.setFile( file );
-        *     upload.uploadToStash().then( function () {
-        *       stashPromise.resolve();
-        *     } );
-        *
-        *     stashPromise.then( function () {
-        *       upload.setFilename( 'foo' );
-        *       upload.setText( 'bar' );
-        *       upload.finishStashUpload().then( function () {
-        *         console.log( 'Done!' );
-        *       } );
-        *     } );
-        *
-        * @class mw.Upload
-        *
-        * @constructor
-        * @param {Object|mw.Api} [apiconfig] A mw.Api object (or subclass), or configuration
-        *     to pass to the constructor of mw.Api.
-        */
-       function Upload( apiconfig ) {
-               this.api = ( apiconfig instanceof mw.Api ) ? apiconfig : new mw.Api( apiconfig );
-
-               this.watchlist = false;
-               this.text = '';
-               this.comment = '';
-               this.filename = null;
-               this.file = null;
-               this.setState( Upload.State.NEW );
-
-               this.imageinfo = undefined;
-       }
-
-       UP = Upload.prototype;
-
-       /**
-        * Get the mw.Api instance used by this Upload object.
-        *
-        * @return {jQuery.Promise}
-        * @return {Function} return.done
-        * @return {mw.Api} return.done.api
-        */
-       UP.getApi = function () {
-               return $.Deferred().resolve( this.api ).promise();
-       };
-
-       /**
-        * Set the text of the file page, to be created on file upload.
-        *
-        * @param {string} text
-        */
-       UP.setText = function ( text ) {
-               this.text = text;
-       };
-
-       /**
-        * Set the filename, to be finalized on upload.
-        *
-        * @param {string} filename
-        */
-       UP.setFilename = function ( filename ) {
-               this.filename = filename;
-       };
-
-       /**
-        * Set the stashed file to finish uploading.
-        *
-        * @param {string} filekey
-        */
-       UP.setFilekey = function ( filekey ) {
-               var upload = this;
-
-               this.setState( Upload.State.STASHED );
-               this.stashPromise = $.Deferred().resolve( function ( data ) {
-                       return upload.api.uploadFromStash( filekey, data );
-               } );
-       };
-
-       /**
-        * Sets the filename based on the filename as it was on the upload.
-        */
-       UP.setFilenameFromFile = function () {
-               var file = this.getFile();
-               if ( !file ) {
-                       return;
-               }
-               if ( file.nodeType && file.nodeType === Node.ELEMENT_NODE ) {
-                       // File input element, use getBasename to cut out the path
-                       this.setFilename( this.getBasename( file.value ) );
-               } else if ( file.name ) {
-                       // HTML5 FileAPI File object, but use getBasename to be safe
-                       this.setFilename( this.getBasename( file.name ) );
-               } else {
-                       // If we ever implement uploading files from clipboard, they might not have a name
-                       this.setFilename( '?' );
-               }
-       };
-
-       /**
-        * Set the file to be uploaded.
-        *
-        * @param {HTMLInputElement|File|Blob} file
-        */
-       UP.setFile = function ( file ) {
-               this.file = file;
-       };
-
-       /**
-        * Set whether the file should be watchlisted after upload.
-        *
-        * @param {boolean} watchlist
-        */
-       UP.setWatchlist = function ( watchlist ) {
-               this.watchlist = watchlist;
-       };
-
-       /**
-        * Set the edit comment for the upload.
-        *
-        * @param {string} comment
-        */
-       UP.setComment = function ( comment ) {
-               this.comment = comment;
-       };
-
-       /**
-        * Get the text of the file page, to be created on file upload.
-        *
-        * @return {string}
-        */
-       UP.getText = function () {
-               return this.text;
-       };
-
-       /**
-        * Get the filename, to be finalized on upload.
-        *
-        * @return {string}
-        */
-       UP.getFilename = function () {
-               return this.filename;
-       };
-
-       /**
-        * Get the file being uploaded.
-        *
-        * @return {HTMLInputElement|File|Blob}
-        */
-       UP.getFile = function () {
-               return this.file;
-       };
-
-       /**
-        * Get the boolean for whether the file will be watchlisted after upload.
-        *
-        * @return {boolean}
-        */
-       UP.getWatchlist = function () {
-               return this.watchlist;
-       };
-
-       /**
-        * Get the current value of the edit comment for the upload.
-        *
-        * @return {string}
-        */
-       UP.getComment = function () {
-               return this.comment;
-       };
-
-       /**
-        * Gets the base filename from a path name.
-        *
-        * @param {string} path
-        * @return {string}
-        */
-       UP.getBasename = function ( path ) {
-               if ( path === undefined || path === null ) {
-                       return '';
-               }
-
-               // Find the index of the last path separator in the
-               // path, and add 1. Then, take the entire string after that.
-               return path.slice(
-                       Math.max(
-                               path.lastIndexOf( '/' ),
-                               path.lastIndexOf( '\\' )
-                       ) + 1
-               );
-       };
-
-       /**
-        * Sets the state and state details (if any) of the upload.
-        *
-        * @param {mw.Upload.State} state
-        * @param {Object} stateDetails
-        */
-       UP.setState = function ( state, stateDetails ) {
-               this.state = state;
-               this.stateDetails = stateDetails;
-       };
-
-       /**
-        * Gets the state of the upload.
-        *
-        * @return {mw.Upload.State}
-        */
-       UP.getState = function () {
-               return this.state;
-       };
-
-       /**
-        * Gets details of the current state.
-        *
-        * @return {string}
-        */
-       UP.getStateDetails = function () {
-               return this.stateDetails;
-       };
-
-       /**
-        * Get the imageinfo object for the finished upload.
-        * Only available once the upload is finished! Don't try to get it
-        * beforehand.
-        *
-        * @return {Object|undefined}
-        */
-       UP.getImageInfo = function () {
-               return this.imageinfo;
-       };
-
-       /**
-        * Upload the file directly.
-        *
-        * @return {jQuery.Promise}
-        */
-       UP.upload = function () {
-               var upload = this;
-
-               if ( !this.getFile() ) {
-                       return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
-               }
-
-               if ( !this.getFilename() ) {
-                       return $.Deferred().reject( 'No filename set. Call setFilename to add one.' );
-               }
-
-               this.setState( Upload.State.UPLOADING );
-
-               return this.api.chunkedUpload( this.getFile(), {
-                       watchlist: ( this.getWatchlist() ) ? 1 : undefined,
-                       comment: this.getComment(),
-                       filename: this.getFilename(),
-                       text: this.getText()
-               } ).then( function ( result ) {
-                       upload.setState( Upload.State.UPLOADED );
-                       upload.imageinfo = result.upload.imageinfo;
-                       return result;
-               }, function ( errorCode, result ) {
-                       if ( result && result.upload && result.upload.warnings ) {
-                               upload.setState( Upload.State.WARNING, result );
-                       } else {
-                               upload.setState( Upload.State.ERROR, result );
-                       }
-                       return $.Deferred().reject( errorCode, result );
-               } );
-       };
-
-       /**
-        * Upload the file to the stash to be completed later.
-        *
-        * @return {jQuery.Promise}
-        */
-       UP.uploadToStash = function () {
-               var upload = this;
-
-               if ( !this.getFile() ) {
-                       return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
-               }
-
-               if ( !this.getFilename() ) {
-                       this.setFilenameFromFile();
-               }
-
-               this.setState( Upload.State.UPLOADING );
-
-               this.stashPromise = this.api.chunkedUploadToStash( this.getFile(), {
-                       filename: this.getFilename()
-               } ).then( function ( finishStash ) {
-                       upload.setState( Upload.State.STASHED );
-                       return finishStash;
-               }, function ( errorCode, result ) {
-                       if ( result && result.upload && result.upload.warnings ) {
-                               upload.setState( Upload.State.WARNING, result );
-                       } else {
-                               upload.setState( Upload.State.ERROR, result );
-                       }
-                       return $.Deferred().reject( errorCode, result );
-               } );
-
-               return this.stashPromise;
-       };
-
-       /**
-        * Finish a stash upload.
-        *
-        * @return {jQuery.Promise}
-        */
-       UP.finishStashUpload = function () {
-               var upload = this;
-
-               if ( !this.stashPromise ) {
-                       return $.Deferred().reject( 'This upload has not been stashed, please upload it to the stash first.' );
-               }
-
-               return this.stashPromise.then( function ( finishStash ) {
-                       upload.setState( Upload.State.UPLOADING );
-
-                       return finishStash( {
-                               watchlist: ( upload.getWatchlist() ) ? 1 : undefined,
-                               comment: upload.getComment(),
-                               filename: upload.getFilename(),
-                               text: upload.getText()
-                       } ).then( function ( result ) {
-                               upload.setState( Upload.State.UPLOADED );
-                               upload.imageinfo = result.upload.imageinfo;
-                               return result;
-                       }, function ( errorCode, result ) {
-                               if ( result && result.upload && result.upload.warnings ) {
-                                       upload.setState( Upload.State.WARNING, result );
-                               } else {
-                                       upload.setState( Upload.State.ERROR, result );
-                               }
-                               return $.Deferred().reject( errorCode, result );
-                       } );
-               } );
-       };
-
-       /**
-        * @enum mw.Upload.State
-        * State of uploads represented in simple terms.
-        */
-       Upload.State = {
-               /** Upload not yet started */
-               NEW: 0,
-
-               /** Upload finished, but there was a warning */
-               WARNING: 1,
-
-               /** Upload finished, but there was an error */
-               ERROR: 2,
-
-               /** Upload in progress */
-               UPLOADING: 3,
-
-               /** Upload finished, but not published, call #finishStashUpload */
-               STASHED: 4,
-
-               /** Upload finished and published */
-               UPLOADED: 5
-       };
-
-       mw.Upload = Upload;
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.apipretty.css b/resources/src/mediawiki/mediawiki.apipretty.css
deleted file mode 100644 (file)
index 99e4569..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-.mw-special-ApiHelp h1.firstHeading {
-       display: none;
-}
-
-.api-pretty-header {
-       font-size: small;
-}
-
-.api-pretty-content {
-       white-space: pre-wrap;
-}
diff --git a/resources/src/mediawiki/mediawiki.checkboxtoggle.css b/resources/src/mediawiki/mediawiki.checkboxtoggle.css
deleted file mode 100644 (file)
index 3da0d43..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-.client-nojs .mw-checkbox-toggle-controls {
-       display: none;
-}
diff --git a/resources/src/mediawiki/mediawiki.checkboxtoggle.js b/resources/src/mediawiki/mediawiki.checkboxtoggle.js
deleted file mode 100644 (file)
index 76bc86c..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*!
- * Allows users to perform all / none / invert operations on a list of
- * checkboxes on the page.
- *
- * @licence GNU GPL v2+
- * @author Luke Faraone <luke at faraone dot cc>
- *
- * Based on ext.nuke.js from https://www.mediawiki.org/wiki/Extension:Nuke by
- * Jeroen De Dauw <jeroendedauw at gmail dot com>
- */
-
-( function ( $ ) {
-       'use strict';
-
-       $( function () {
-               // FIXME: This shouldn't be a global selector to avoid conflicts
-               // with unrelated content on the same page. (T131318)
-               var $checkboxes = $( 'li input[type="checkbox"]' );
-
-               function selectAll( check ) {
-                       $checkboxes.prop( 'checked', check );
-               }
-
-               $( '.mw-checkbox-all' ).click( function () {
-                       selectAll( true );
-               } );
-               $( '.mw-checkbox-none' ).click( function () {
-                       selectAll( false );
-               } );
-               $( '.mw-checkbox-invert' ).click( function () {
-                       $checkboxes.prop( 'checked', function ( i, val ) {
-                               return !val;
-                       } );
-               } );
-
-       } );
-
-}( jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.notification.convertmessagebox.js b/resources/src/mediawiki/mediawiki.notification.convertmessagebox.js
deleted file mode 100644 (file)
index 5d46de6..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Usage:
- *
- *     var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
- *
- * @class mw.plugin.convertmessagebox
- * @singleton
- */
-( function ( mw, $ ) {
-       'use strict';
-
-       /**
-        * Convert a messagebox to a notification.
-        *
-        * Checks if a message box with class `.mw-notify-success`, `.mw-notify-warning`, or `.mw-notify-error`
-        * exists and converts it into a mw.Notification with the text of the element or a given message key.
-        *
-        * By default the notification will automatically hide after 5s, or when the user clicks the element.
-        * This can be overridden by setting attribute `data-mw-autohide="true"`.
-        *
-        * @param {Object} [options] Options
-        * @param {mw.Message} [options.msg] Message key (must be loaded already)
-        */
-       function convertmessagebox( options ) {
-               var $msgBox, type, autoHide, msg, notif,
-                       $successBox = $( '.mw-notify-success' ),
-                       $warningBox = $( '.mw-notify-warning' ),
-                       $errorBox = $( '.mw-notify-error' );
-
-               // If there is a message box and javascript is enabled, use a slick notification instead!
-               if ( $successBox.length ) {
-                       $msgBox = $successBox;
-                       type = 'info';
-               } else if ( $warningBox.length ) {
-                       $msgBox = $warningBox;
-                       type = 'warn';
-               } else if ( $errorBox.length ) {
-                       $msgBox = $errorBox;
-                       type = 'error';
-               } else {
-                       return;
-               }
-
-               autoHide = $msgBox.attr( 'data-mw-autohide' ) === 'true';
-
-               // If the msg param is given, use it, otherwise use the text of the successbox
-               msg = options && options.msg || $msgBox.text();
-               $msgBox.detach();
-
-               notif = mw.notification.notify( msg, { autoHide: autoHide, type: type } );
-               if ( !autoHide ) {
-                       // 'change' event not reliable!
-                       $( document ).one( 'keydown mousedown', function () {
-                               if ( notif ) {
-                                       notif.close();
-                                       notif = null;
-                               }
-                       } );
-               }
-       }
-
-       module.exports = convertmessagebox;
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.notification.convertmessagebox.styles.less b/resources/src/mediawiki/mediawiki.notification.convertmessagebox.styles.less
deleted file mode 100644 (file)
index 2371f4e..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-.client-js {
-       .mw-notify-success,
-       .mw-notify-warning,
-       .mw-notify-error {
-               display: none;
-       }
-}
diff --git a/resources/src/mediawiki/mediawiki.template.js b/resources/src/mediawiki/mediawiki.template.js
deleted file mode 100644 (file)
index 4a3157c..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * @class mw.template
- * @singleton
- */
-( function ( mw, $ ) {
-       var compiledTemplates = {},
-               compilers = {};
-
-       mw.template = {
-               /**
-                * Register a new compiler.
-                *
-                * A compiler is any object that implements a compile() method. The compile() method must
-                * return a Template interface with a method render() that returns HTML.
-                *
-                * The compiler name must correspond with the name suffix of templates that use this compiler.
-                *
-                * @param {string} name Compiler name
-                * @param {Object} compiler
-                */
-               registerCompiler: function ( name, compiler ) {
-                       if ( !compiler.compile ) {
-                               throw new Error( 'Compiler must implement a compile method' );
-                       }
-                       compilers[ name ] = compiler;
-               },
-
-               /**
-                * Get the name of the associated compiler based on a template name.
-                *
-                * @param {string} templateName Name of a template (including suffix)
-                * @return {string} Name of a compiler
-                */
-               getCompilerName: function ( templateName ) {
-                       var nameParts = templateName.split( '.' );
-                       if ( nameParts.length < 2 ) {
-                               throw new Error( 'Template name must have a suffix' );
-                       }
-                       return nameParts[ nameParts.length - 1 ];
-               },
-
-               /**
-                * Get a compiler via its name.
-                *
-                * @param {string} name Name of a compiler
-                * @return {Object} The compiler
-                */
-               getCompiler: function ( name ) {
-                       var compiler = compilers[ name ];
-                       if ( !compiler ) {
-                               throw new Error( 'Unknown compiler ' + name );
-                       }
-                       return compiler;
-               },
-
-               /**
-                * Register a template associated with a module.
-                *
-                * Precompiles the newly added template based on the suffix in its name.
-                *
-                * @param {string} moduleName Name of the ResourceLoader module the template is associated with
-                * @param {string} templateName Name of the template (including suffix)
-                * @param {string} templateBody Contents of the template (e.g. html markup)
-                * @return {Object} Compiled template
-                */
-               add: function ( moduleName, templateName, templateBody ) {
-                       // Precompile and add to cache
-                       var compiled = this.compile( templateBody, this.getCompilerName( templateName ) );
-                       if ( !compiledTemplates[ moduleName ] ) {
-                               compiledTemplates[ moduleName ] = {};
-                       }
-                       compiledTemplates[ moduleName ][ templateName ] = compiled;
-
-                       return compiled;
-               },
-
-               /**
-                * Get a compiled template by module and template name.
-                *
-                * @param {string} moduleName Name of the module to retrieve the template from
-                * @param {string} templateName Name of template to be retrieved
-                * @return {Object} Compiled template
-                */
-               get: function ( moduleName, templateName ) {
-                       var moduleTemplates;
-
-                       // Try cache first
-                       if ( compiledTemplates[ moduleName ] && compiledTemplates[ moduleName ][ templateName ] ) {
-                               return compiledTemplates[ moduleName ][ templateName ];
-                       }
-
-                       moduleTemplates = mw.templates.get( moduleName );
-                       if ( !moduleTemplates || !moduleTemplates[ templateName ] ) {
-                               throw new Error( 'Template ' + templateName + ' not found in module ' + moduleName );
-                       }
-
-                       // Compiled and add to cache
-                       return this.add( moduleName, templateName, moduleTemplates[ templateName ] );
-               },
-
-               /**
-                * Compile a string of template markup with an engine of choice.
-                *
-                * @param {string} templateBody Template body
-                * @param {string} compilerName The name of a registered compiler
-                * @return {Object} Compiled template
-                */
-               compile: function ( templateBody, compilerName ) {
-                       return this.getCompiler( compilerName ).compile( templateBody );
-               }
-       };
-
-       // Register basic html compiler
-       mw.template.registerCompiler( 'html', {
-               compile: function ( src ) {
-                       return {
-                               render: function () {
-                                       return $( $.parseHTML( src.trim() ) );
-                               }
-                       };
-               }
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.template.regexp.js b/resources/src/mediawiki/mediawiki.template.regexp.js
deleted file mode 100644 (file)
index 3ec0a1f..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-mediaWiki.template.registerCompiler( 'regexp', {
-       compile: function ( src ) {
-               return {
-                       render: function () {
-                               return new RegExp(
-                                       src
-                                               // Remove whitespace
-                                               .replace( /\s+/g, '' )
-                                               // Remove named capturing groups
-                                               .replace( /\?<\w+?>/g, '' )
-                               );
-                       }
-               };
-       }
-} );
diff --git a/resources/src/mediawiki/page/gallery-slideshow.js b/resources/src/mediawiki/page/gallery-slideshow.js
deleted file mode 100644 (file)
index 6e9ff0e..0000000
+++ /dev/null
@@ -1,460 +0,0 @@
-/*!
- * mw.GallerySlideshow: Interface controls for the slideshow gallery
- */
-( function ( mw, $, OO ) {
-       /**
-        * mw.GallerySlideshow encapsulates the user interface of the slideshow
-        * galleries. An object is instantiated for each `.mw-gallery-slideshow`
-        * element.
-        *
-        * @class mw.GallerySlideshow
-        * @uses mw.Title
-        * @uses mw.Api
-        * @param {jQuery} gallery The `<ul>` element of the gallery.
-        */
-       mw.GallerySlideshow = function ( gallery ) {
-               // Properties
-               this.$gallery = $( gallery );
-               this.$galleryCaption = this.$gallery.find( '.gallerycaption' );
-               this.$galleryBox = this.$gallery.find( '.gallerybox' );
-               this.$currentImage = null;
-               this.imageInfoCache = {};
-               if ( this.$gallery.parent().attr( 'id' ) !== 'mw-content-text' ) {
-                       this.$container = this.$gallery.parent();
-               }
-
-               // Initialize
-               this.drawCarousel();
-               this.setSizeRequirement();
-               this.toggleThumbnails( !!this.$gallery.attr( 'data-showthumbnails' ) );
-               this.showCurrentImage();
-
-               // Events
-               $( window ).on(
-                       'resize',
-                       OO.ui.debounce(
-                               this.setSizeRequirement.bind( this ),
-                               100
-                       )
-               );
-
-               // Disable thumbnails' link, instead show the image in the carousel
-               this.$galleryBox.on( 'click', function ( e ) {
-                       this.$currentImage = $( e.currentTarget );
-                       this.showCurrentImage();
-                       return false;
-               }.bind( this ) );
-       };
-
-       /* Properties */
-       /**
-        * @property {jQuery} $gallery The `<ul>` element of the gallery.
-        */
-
-       /**
-        * @property {jQuery} $galleryCaption The `<li>` that has the gallery caption.
-        */
-
-       /**
-        * @property {jQuery} $galleryBox Selection of `<li>` elements that have thumbnails.
-        */
-
-       /**
-        * @property {jQuery} $carousel The `<li>` elements that contains the carousel.
-        */
-
-       /**
-        * @property {jQuery} $interface The `<div>` elements that contains the interface buttons.
-        */
-
-       /**
-        * @property {jQuery} $img The `<img>` element that'll display the current image.
-        */
-
-       /**
-        * @property {jQuery} $imgLink The `<a>` element that links to the image's File page.
-        */
-
-       /**
-        * @property {jQuery} $imgCaption The `<p>` element that holds the image caption.
-        */
-
-       /**
-        * @property {jQuery} $imgContainer The `<div>` element that contains the image.
-        */
-
-       /**
-        * @property {jQuery} $currentImage The `<li>` element of the current image.
-        */
-
-       /**
-        * @property {jQuery} $container If the gallery contained in an element that is
-        *   not the main content element, then it stores that element.
-        */
-
-       /**
-        * @property {Object} imageInfoCache A key value pair of thumbnail URLs and image info.
-        */
-
-       /**
-        * @property {number} imageWidth Width of the image based on viewport size
-        */
-
-       /**
-        * @property {number} imageHeight Height of the image based on viewport size
-        *   the URLs in the required size.
-        */
-
-       /* Setup */
-       OO.initClass( mw.GallerySlideshow );
-
-       /* Methods */
-       /**
-        * Draws the carousel and the interface around it.
-        */
-       mw.GallerySlideshow.prototype.drawCarousel = function () {
-               var next, prev, toggle, interfaceElements, carouselStack;
-
-               this.$carousel = $( '<li>' ).addClass( 'gallerycarousel' );
-
-               // Buttons for the interface
-               prev = new OO.ui.ButtonWidget( {
-                       framed: false,
-                       icon: 'previous'
-               } ).on( 'click', this.prevImage.bind( this ) );
-
-               next = new OO.ui.ButtonWidget( {
-                       framed: false,
-                       icon: 'next'
-               } ).on( 'click', this.nextImage.bind( this ) );
-
-               toggle = new OO.ui.ButtonWidget( {
-                       framed: false,
-                       icon: 'imageGallery',
-                       title: mw.msg( 'gallery-slideshow-toggle' )
-               } ).on( 'click', this.toggleThumbnails.bind( this ) );
-
-               interfaceElements = new OO.ui.PanelLayout( {
-                       expanded: false,
-                       classes: [ 'mw-gallery-slideshow-buttons' ],
-                       $content: $( '<div>' ).append(
-                               prev.$element,
-                               toggle.$element,
-                               next.$element
-                       )
-               } );
-               this.$interface = interfaceElements.$element;
-
-               // Containers for the current image, caption etc.
-               this.$img = $( '<img>' );
-               this.$imgLink = $( '<a>' ).append( this.$img );
-               this.$imgCaption = $( '<p>' ).attr( 'class', 'mw-gallery-slideshow-caption' );
-               this.$imgContainer = $( '<div>' )
-                       .attr( 'class', 'mw-gallery-slideshow-img-container' )
-                       .append( this.$imgLink );
-
-               carouselStack = new OO.ui.StackLayout( {
-                       continuous: true,
-                       expanded: false,
-                       items: [
-                               interfaceElements,
-                               new OO.ui.PanelLayout( {
-                                       expanded: false,
-                                       $content: this.$imgContainer
-                               } ),
-                               new OO.ui.PanelLayout( {
-                                       expanded: false,
-                                       $content: this.$imgCaption
-                               } )
-                       ]
-               } );
-               this.$carousel.append( carouselStack.$element );
-
-               // Append below the caption or as the first element in the gallery
-               if ( this.$galleryCaption.length !== 0 ) {
-                       this.$galleryCaption.after( this.$carousel );
-               } else {
-                       this.$gallery.prepend( this.$carousel );
-               }
-       };
-
-       /**
-        * Sets the {@link #imageWidth} and {@link #imageHeight} properties
-        * based on the size of the window. Also flushes the
-        * {@link #imageInfoCache} as we'll now need URLs for a different
-        * size.
-        */
-       mw.GallerySlideshow.prototype.setSizeRequirement = function () {
-               var w, h;
-
-               if ( this.$container !== undefined ) {
-                       w = this.$container.width() * 0.9;
-                       h = ( this.$container.height() - this.getChromeHeight() ) * 0.9;
-               } else {
-                       w = this.$imgContainer.width();
-                       h = Math.min( $( window ).height() * ( 3 / 4 ), this.$imgContainer.width() ) - this.getChromeHeight();
-               }
-
-               // Only update and flush the cache if the size changed
-               if ( w !== this.imageWidth || h !== this.imageHeight ) {
-                       this.imageWidth = w;
-                       this.imageHeight = h;
-                       this.imageInfoCache = {};
-                       this.setImageSize();
-               }
-       };
-
-       /**
-        * Gets the height of the interface elements and the
-        * gallery's caption.
-        *
-        * @return {number} Height
-        */
-       mw.GallerySlideshow.prototype.getChromeHeight = function () {
-               return this.$interface.outerHeight() + this.$galleryCaption.outerHeight();
-       };
-
-       /**
-        * Sets the height and width of {@link #$img} based on the
-        * proportion of the image and the values generated by
-        * {@link #setSizeRequirement}.
-        *
-        * @return {boolean} Whether or not the image was sized.
-        */
-       mw.GallerySlideshow.prototype.setImageSize = function () {
-               if ( this.$img === undefined || this.$thumbnail === undefined ) {
-                       return false;
-               }
-
-               // Reset height and width
-               this.$img
-                       .removeAttr( 'width' )
-                       .removeAttr( 'height' );
-
-               // Stretch image to take up the required size
-               this.$img.attr( 'height', ( this.imageHeight - this.$imgCaption.outerHeight() ) + 'px' );
-
-               // Make the image smaller in case the current image
-               // size is larger than the original file size.
-               this.getImageInfo( this.$thumbnail ).done( function ( info ) {
-                       // NOTE: There will be a jump when resizing the window
-                       // because the cache is cleared and this a new network request.
-                       if (
-                               info.thumbwidth < this.$img.width() ||
-                               info.thumbheight < this.$img.height()
-                       ) {
-                               this.$img.attr( 'width', info.thumbwidth + 'px' );
-                               this.$img.attr( 'height', info.thumbheight + 'px' );
-                       }
-               }.bind( this ) );
-
-               return true;
-       };
-
-       /**
-        * Displays the image set as {@link #$currentImage} in the carousel.
-        */
-       mw.GallerySlideshow.prototype.showCurrentImage = function () {
-               var imageLi = this.getCurrentImage(),
-                       caption = imageLi.find( '.gallerytext' );
-
-               // The order of the following is important for size calculations
-               // 1. Highlight current thumbnail
-               this.$gallery
-                       .find( '.gallerybox.slideshow-current' )
-                       .removeClass( 'slideshow-current' );
-               imageLi.addClass( 'slideshow-current' );
-
-               // 2. Show thumbnail
-               this.$thumbnail = imageLi.find( 'img' );
-               this.$img.attr( 'src', this.$thumbnail.attr( 'src' ) );
-               this.$img.attr( 'alt', this.$thumbnail.attr( 'alt' ) );
-               this.$imgLink.attr( 'href', imageLi.find( 'a' ).eq( 0 ).attr( 'href' ) );
-
-               // 3. Copy caption
-               this.$imgCaption
-                       .empty()
-                       .append( caption.clone() );
-
-               // 4. Stretch thumbnail to correct size
-               this.setImageSize();
-
-               // 5. Load image at the required size
-               this.loadImage( this.$thumbnail ).done( function ( info, $img ) {
-                       // Show this image to the user only if its still the current one
-                       if ( this.$thumbnail.attr( 'src' ) === $img.attr( 'src' ) ) {
-                               this.$img.attr( 'src', info.thumburl );
-                               this.setImageSize();
-
-                               // Keep the next image ready
-                               this.loadImage( this.getNextImage().find( 'img' ) );
-                       }
-               }.bind( this ) );
-       };
-
-       /**
-        * Loads the full image given the `<img>` element of the thumbnail.
-        *
-        * @param {Object} $img
-        * @return {jQuery.Promise} Resolves with the images URL and original
-        *      element once the image has loaded.
-        */
-       mw.GallerySlideshow.prototype.loadImage = function ( $img ) {
-               var img, d = $.Deferred();
-
-               this.getImageInfo( $img ).done( function ( info ) {
-                       img = new Image();
-                       img.src = info.thumburl;
-                       img.onload = function () {
-                               d.resolve( info, $img );
-                       };
-                       img.onerror = function () {
-                               d.reject();
-                       };
-               } ).fail( function () {
-                       d.reject();
-               } );
-
-               return d.promise();
-       };
-
-       /**
-        * Gets the image's info given an `<img>` element.
-        *
-        * @param {Object} $img
-        * @return {jQuery.Promise} Resolves with the image's info.
-        */
-       mw.GallerySlideshow.prototype.getImageInfo = function ( $img ) {
-               var api, title, params,
-                       imageSrc = $img.attr( 'src' );
-
-               // Reject promise if there is no thumbnail image
-               if ( $img[ 0 ] === undefined ) {
-                       return $.Deferred().reject();
-               }
-
-               if ( this.imageInfoCache[ imageSrc ] === undefined ) {
-                       api = new mw.Api();
-                       // TODO: This supports only gallery of images
-                       title = mw.Title.newFromImg( $img );
-                       params = {
-                               action: 'query',
-                               formatversion: 2,
-                               titles: title.toString(),
-                               prop: 'imageinfo',
-                               iiprop: 'url'
-                       };
-
-                       // Check which dimension we need to request, based on
-                       // image and container proportions.
-                       if ( this.getDimensionToRequest( $img ) === 'height' ) {
-                               params.iiurlheight = this.imageHeight;
-                       } else {
-                               params.iiurlwidth = this.imageWidth;
-                       }
-
-                       this.imageInfoCache[ imageSrc ] = api.get( params ).then( function ( data ) {
-                               if ( OO.getProp( data, 'query', 'pages', 0, 'imageinfo', 0, 'thumburl' ) !== undefined ) {
-                                       return data.query.pages[ 0 ].imageinfo[ 0 ];
-                               } else {
-                                       return $.Deferred().reject();
-                               }
-                       } );
-               }
-
-               return this.imageInfoCache[ imageSrc ];
-       };
-
-       /**
-        * Given an image, the method checks whether to use the height
-        * or the width to request the larger image.
-        *
-        * @param {jQuery} $img
-        * @return {string}
-        */
-       mw.GallerySlideshow.prototype.getDimensionToRequest = function ( $img ) {
-               var ratio = $img.width() / $img.height();
-
-               if ( this.imageHeight * ratio <= this.imageWidth ) {
-                       return 'height';
-               } else {
-                       return 'width';
-               }
-       };
-
-       /**
-        * Toggles visibility of the thumbnails.
-        *
-        * @param {boolean} show Optional argument to control the state
-        */
-       mw.GallerySlideshow.prototype.toggleThumbnails = function ( show ) {
-               this.$galleryBox.toggle( show );
-               this.$carousel.toggleClass( 'mw-gallery-slideshow-thumbnails-toggled', show );
-       };
-
-       /**
-        * Getter method for {@link #$currentImage}
-        *
-        * @return {jQuery}
-        */
-       mw.GallerySlideshow.prototype.getCurrentImage = function () {
-               this.$currentImage = this.$currentImage || this.$galleryBox.eq( 0 );
-               return this.$currentImage;
-       };
-
-       /**
-        * Gets the image after the current one. Returns the first image if
-        * the current one is the last.
-        *
-        * @return {jQuery}
-        */
-       mw.GallerySlideshow.prototype.getNextImage = function () {
-               // Not the last image in the gallery
-               if ( this.$currentImage.next( '.gallerybox' )[ 0 ] !== undefined ) {
-                       return this.$currentImage.next( '.gallerybox' );
-               } else {
-                       return this.$galleryBox.eq( 0 );
-               }
-       };
-
-       /**
-        * Gets the image before the current one. Returns the last image if
-        * the current one is the first.
-        *
-        * @return {jQuery}
-        */
-       mw.GallerySlideshow.prototype.getPrevImage = function () {
-               // Not the first image in the gallery
-               if ( this.$currentImage.prev( '.gallerybox' )[ 0 ] !== undefined ) {
-                       return this.$currentImage.prev( '.gallerybox' );
-               } else {
-                       return this.$galleryBox.last();
-               }
-       };
-
-       /**
-        * Sets the {@link #$currentImage} to the next one and shows
-        * it in the carousel
-        */
-       mw.GallerySlideshow.prototype.nextImage = function () {
-               this.$currentImage = this.getNextImage();
-               this.showCurrentImage();
-       };
-
-       /**
-        * Sets the {@link #$currentImage} to the previous one and shows
-        * it in the carousel
-        */
-       mw.GallerySlideshow.prototype.prevImage = function () {
-               this.$currentImage = this.getPrevImage();
-               this.showCurrentImage();
-       };
-
-       // Bootstrap all slideshow galleries
-       mw.hook( 'wikipage.content' ).add( function ( $content ) {
-               $content.find( '.mw-gallery-slideshow' ).each( function () {
-                       // eslint-disable-next-line no-new
-                       new mw.GallerySlideshow( this );
-               } );
-       } );
-}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki/page/gallery.js b/resources/src/mediawiki/page/gallery.js
deleted file mode 100644 (file)
index 79937e5..0000000
+++ /dev/null
@@ -1,268 +0,0 @@
-/*!
- * Show gallery captions when focused. Copied directly from jquery.mw-jump.js.
- * Also Dynamically resize images to justify them.
- */
-( function ( mw, $ ) {
-       var $galleries,
-               bound = false,
-               // Is there a better way to detect a touchscreen? Current check taken from stack overflow.
-               isTouchScreen = !!( window.ontouchstart !== undefined ||
-                       window.DocumentTouch !== undefined && document instanceof window.DocumentTouch
-               );
-
-       /**
-        * Perform the layout justification.
-        *
-        * @ignore
-        * @context {HTMLElement} A `ul.mw-gallery-*` element
-        */
-       function justify() {
-               var lastTop,
-                       $img,
-                       imgWidth,
-                       imgHeight,
-                       captionWidth,
-                       rows = [],
-                       $gallery = $( this );
-
-               $gallery.children( 'li.gallerybox' ).each( function () {
-                       // Math.floor to be paranoid if things are off by 0.00000000001
-                       var top = Math.floor( $( this ).position().top ),
-                               $this = $( this );
-
-                       if ( top !== lastTop ) {
-                               rows[ rows.length ] = [];
-                               lastTop = top;
-                       }
-
-                       $img = $this.find( 'div.thumb a.image img' );
-                       if ( $img.length && $img[ 0 ].height ) {
-                               imgHeight = $img[ 0 ].height;
-                               imgWidth = $img[ 0 ].width;
-                       } else {
-                               // If we don't have a real image, get the containing divs width/height.
-                               // Note that if we do have a real image, using this method will generally
-                               // give the same answer, but can be different in the case of a very
-                               // narrow image where extra padding is added.
-                               imgHeight = $this.children().children( 'div:first' ).height();
-                               imgWidth = $this.children().children( 'div:first' ).width();
-                       }
-
-                       // Hack to make an edge case work ok
-                       if ( imgHeight < 30 ) {
-                               // Don't try and resize this item.
-                               imgHeight = 0;
-                       }
-
-                       captionWidth = $this.children().children( 'div.gallerytextwrapper' ).width();
-                       rows[ rows.length - 1 ][ rows[ rows.length - 1 ].length ] = {
-                               $elm: $this,
-                               width: $this.outerWidth(),
-                               imgWidth: imgWidth,
-                               // XXX: can divide by 0 ever happen?
-                               aspect: imgWidth / imgHeight,
-                               captionWidth: captionWidth,
-                               height: imgHeight
-                       };
-
-                       // Save all boundaries so we can restore them on window resize
-                       $this.data( 'imgWidth', imgWidth );
-                       $this.data( 'imgHeight', imgHeight );
-                       $this.data( 'width', $this.outerWidth() );
-                       $this.data( 'captionWidth', captionWidth );
-               } );
-
-               ( function () {
-                       var maxWidth,
-                               combinedAspect,
-                               combinedPadding,
-                               curRow,
-                               curRowHeight,
-                               wantedWidth,
-                               preferredHeight,
-                               newWidth,
-                               padding,
-                               $outerDiv,
-                               $innerDiv,
-                               $imageDiv,
-                               $imageElm,
-                               imageElm,
-                               $caption,
-                               i,
-                               j,
-                               avgZoom,
-                               totalZoom = 0;
-
-                       for ( i = 0; i < rows.length; i++ ) {
-                               maxWidth = $gallery.width();
-                               combinedAspect = 0;
-                               combinedPadding = 0;
-                               curRow = rows[ i ];
-                               curRowHeight = 0;
-
-                               for ( j = 0; j < curRow.length; j++ ) {
-                                       if ( curRowHeight === 0 ) {
-                                               if ( isFinite( curRow[ j ].height ) ) {
-                                                       // Get the height of this row, by taking the first
-                                                       // non-out of bounds height
-                                                       curRowHeight = curRow[ j ].height;
-                                               }
-                                       }
-
-                                       if ( curRow[ j ].aspect === 0 || !isFinite( curRow[ j ].aspect ) ) {
-                                               // One of the dimensions are 0. Probably should
-                                               // not try to resize.
-                                               combinedPadding += curRow[ j ].width;
-                                       } else {
-                                               combinedAspect += curRow[ j ].aspect;
-                                               combinedPadding += curRow[ j ].width - curRow[ j ].imgWidth;
-                                       }
-                               }
-
-                               // Add some padding for inter-element spacing.
-                               combinedPadding += 5 * curRow.length;
-                               wantedWidth = maxWidth - combinedPadding;
-                               preferredHeight = wantedWidth / combinedAspect;
-
-                               if ( preferredHeight > curRowHeight * 1.5 ) {
-                                       // Only expand at most 1.5 times current size
-                                       // As that's as high a resolution as we have.
-                                       // Also on the off chance there is a bug in this
-                                       // code, would prevent accidentally expanding to
-                                       // be 10 billion pixels wide.
-                                       if ( i === rows.length - 1 ) {
-                                               // If its the last row, and we can't fit it,
-                                               // don't make the entire row huge.
-                                               avgZoom = ( totalZoom / ( rows.length - 1 ) ) * curRowHeight;
-                                               if ( isFinite( avgZoom ) && avgZoom >= 1 && avgZoom <= 1.5 ) {
-                                                       preferredHeight = avgZoom;
-                                               } else {
-                                                       // Probably a single row gallery
-                                                       preferredHeight = curRowHeight;
-                                               }
-                                       } else {
-                                               preferredHeight = 1.5 * curRowHeight;
-                                       }
-                               }
-                               if ( !isFinite( preferredHeight ) ) {
-                                       // This *definitely* should not happen.
-                                       // Skip this row.
-                                       continue;
-                               }
-                               if ( preferredHeight < 5 ) {
-                                       // Well something clearly went wrong...
-                                       // Skip this row.
-                                       continue;
-                               }
-
-                               if ( preferredHeight / curRowHeight > 1 ) {
-                                       totalZoom += preferredHeight / curRowHeight;
-                               } else {
-                                       // If we shrink, still consider that a zoom of 1
-                                       totalZoom += 1;
-                               }
-
-                               for ( j = 0; j < curRow.length; j++ ) {
-                                       newWidth = preferredHeight * curRow[ j ].aspect;
-                                       padding = curRow[ j ].width - curRow[ j ].imgWidth;
-                                       $outerDiv = curRow[ j ].$elm;
-                                       $innerDiv = $outerDiv.children( 'div' ).first();
-                                       $imageDiv = $innerDiv.children( 'div.thumb' );
-                                       $imageElm = $imageDiv.find( 'img' ).first();
-                                       imageElm = $imageElm.length ? $imageElm[ 0 ] : null;
-                                       $caption = $outerDiv.find( 'div.gallerytextwrapper' );
-
-                                       // Since we are going to re-adjust the height, the vertical
-                                       // centering margins need to be reset.
-                                       $imageDiv.children( 'div' ).css( 'margin', '0px auto' );
-
-                                       if ( newWidth < 60 || !isFinite( newWidth ) ) {
-                                               // Making something skinnier than this will mess up captions,
-                                               if ( newWidth < 1 || !isFinite( newWidth ) ) {
-                                                       $innerDiv.height( preferredHeight );
-                                                       // Don't even try and touch the image size if it could mean
-                                                       // making it disappear.
-                                                       continue;
-                                               }
-                                       } else {
-                                               $outerDiv.width( newWidth + padding );
-                                               $innerDiv.width( newWidth + padding );
-                                               $imageDiv.width( newWidth );
-                                               $caption.width( curRow[ j ].captionWidth + ( newWidth - curRow[ j ].imgWidth ) );
-                                       }
-
-                                       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 );
-                                       }
-                               }
-                       }
-               }() );
-       }
-
-       function handleResizeStart() {
-               $galleries.children( 'li.gallerybox' ).each( function () {
-                       var imgWidth = $( this ).data( 'imgWidth' ),
-                               imgHeight = $( this ).data( 'imgHeight' ),
-                               width = $( this ).data( 'width' ),
-                               captionWidth = $( this ).data( 'captionWidth' ),
-                               $innerDiv = $( this ).children( 'div' ).first(),
-                               $imageDiv = $innerDiv.children( 'div.thumb' ),
-                               $imageElm, imageElm;
-
-                       // Restore original sizes so we can arrange the elements as on freshly loaded page
-                       $( this ).width( width );
-                       $innerDiv.width( width );
-                       $imageDiv.width( imgWidth );
-                       $( this ).find( 'div.gallerytextwrapper' ).width( captionWidth );
-
-                       $imageElm = $( this ).find( 'img' ).first();
-                       imageElm = $imageElm.length ? $imageElm[ 0 ] : null;
-                       if ( imageElm ) {
-                               imageElm.width = imgWidth;
-                               imageElm.height = imgHeight;
-                       } else {
-                               $imageDiv.height( imgHeight );
-                       }
-               } );
-       }
-
-       function handleResizeEnd() {
-               $galleries.each( justify );
-       }
-
-       mw.hook( 'wikipage.content' ).add( function ( $content ) {
-               if ( isTouchScreen ) {
-                       // Always show the caption for a touch screen.
-                       $content.find( 'ul.mw-gallery-packed-hover' )
-                               .addClass( 'mw-gallery-packed-overlay' )
-                               .removeClass( 'mw-gallery-packed-hover' );
-               } else {
-                       // Note use of just "a", not a.image, since we want this to trigger if a link in
-                       // the caption receives focus
-                       $content.find( 'ul.mw-gallery-packed-hover li.gallerybox' ).on( 'focus blur', 'a', function ( e ) {
-                               // Confusingly jQuery leaves e.type as focusout for delegated blur events
-                               var gettingFocus = e.type !== 'blur' && e.type !== 'focusout';
-                               $( this ).closest( 'li.gallerybox' ).toggleClass( 'mw-gallery-focused', gettingFocus );
-                       } );
-               }
-
-               $galleries = $content.find( 'ul.mw-gallery-packed-overlay, ul.mw-gallery-packed-hover, ul.mw-gallery-packed' );
-               // Call the justification asynchronous because live preview fires the hook with detached $content.
-               setTimeout( function () {
-                       $galleries.each( justify );
-
-                       // Bind here instead of in the top scope as the callbacks use $galleries.
-                       if ( !bound ) {
-                               bound = true;
-                               $( window )
-                                       .resize( $.debounce( 300, true, handleResizeStart ) )
-                                       .resize( $.debounce( 300, handleResizeEnd ) );
-                       }
-               } );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/image-pagination.js b/resources/src/mediawiki/page/image-pagination.js
deleted file mode 100644 (file)
index 06c34a5..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-/*!
- * Implement AJAX navigation for multi-page images so the user may browse without a full page reload.
- */
-
-/* eslint-disable no-use-before-define */
-
-( function ( mw, $ ) {
-       var jqXhr, $multipageimage, $spinner,
-               cache = {},
-               cacheOrder = [];
-
-       /* Fetch the next page, caching up to 10 last-loaded pages.
-        * @param {string} url
-        * @return {jQuery.Promise}
-        */
-       function fetchPageData( url ) {
-               if ( jqXhr && jqXhr.abort ) {
-                       // Prevent race conditions and piling up pending requests
-                       jqXhr.abort();
-               }
-               jqXhr = undefined;
-
-               // Try the cache
-               if ( cache[ url ] ) {
-                       // Update access freshness
-                       cacheOrder.splice( cacheOrder.indexOf( url ), 1 );
-                       cacheOrder.push( url );
-                       return $.Deferred().resolve( cache[ url ] ).promise();
-               }
-
-               // TODO Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
-               // (thumbnail urls) and update the interface manually.
-               jqXhr = $.ajax( url ).then( function ( data ) {
-                       return $( data ).find( 'table.multipageimage' ).contents();
-               } );
-
-               // Handle cache updates
-               jqXhr.done( function ( $contents ) {
-                       jqXhr = undefined;
-
-                       // Cache the newly loaded page
-                       cache[ url ] = $contents;
-                       cacheOrder.push( url );
-
-                       // Remove the oldest entry if we're over the limit
-                       if ( cacheOrder.length > 10 ) {
-                               delete cache[ cacheOrder[ 0 ] ];
-                               cacheOrder = cacheOrder.slice( 1 );
-                       }
-               } );
-
-               return jqXhr.promise();
-       }
-
-       /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
-        * @param {string} url
-        * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
-        *   true, this function won't push a new history state, for the browser did so already).
-        */
-       function switchPage( url, hist ) {
-               var $tr, promise;
-
-               // Start fetching data (might be cached)
-               promise = fetchPageData( url );
-
-               // Add a new spinner if one doesn't already exist and the data is not already ready
-               if ( !$spinner && promise.state() !== 'resolved' ) {
-                       $tr = $multipageimage.find( 'tr' );
-                       $spinner = $.createSpinner( {
-                               size: 'large',
-                               type: 'block'
-                       } )
-                               // Copy the old content dimensions equal so that the current scroll position is not
-                               // lost between emptying the table is and receiving the new contents.
-                               .css( {
-                                       height: $tr.outerHeight(),
-                                       width: $tr.outerWidth()
-                               } );
-
-                       $multipageimage.empty().append( $spinner );
-               }
-
-               promise.done( function ( $contents ) {
-                       $spinner = undefined;
-
-                       // Replace table contents
-                       $multipageimage.empty().append( $contents.clone() );
-
-                       bindPageNavigation( $multipageimage );
-
-                       // Fire hook because the page's content has changed
-                       mw.hook( 'wikipage.content' ).fire( $multipageimage );
-
-                       // Update browser history and address bar. But not if we came here from a history
-                       // event, in which case the url is already updated by the browser.
-                       if ( history.pushState && !hist ) {
-                               history.pushState( { tag: 'mw-pagination' }, document.title, url );
-                       }
-               } );
-       }
-
-       function bindPageNavigation( $container ) {
-               $container.find( '.multipageimagenavbox' ).one( 'click', 'a', function ( e ) {
-                       var page, url;
-
-                       // Generate the same URL on client side as the one generated in ImagePage::openShowImage.
-                       // We avoid using the URL in the link directly since it could have been manipulated (T68608)
-                       page = Number( mw.util.getParamValue( 'page', this.href ) );
-                       url = mw.util.getUrl( mw.config.get( 'wgPageName' ), { page: page } );
-
-                       switchPage( url );
-                       e.preventDefault();
-               } );
-
-               $container.find( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
-                       switchPage( this.action + '?' + $( this ).serialize() );
-                       e.preventDefault();
-               } );
-       }
-
-       $( function () {
-               if ( mw.config.get( 'wgCanonicalNamespace' ) !== 'File' ) {
-                       return;
-               }
-               $multipageimage = $( 'table.multipageimage' );
-               if ( !$multipageimage.length ) {
-                       return;
-               }
-
-               bindPageNavigation( $multipageimage );
-
-               // Update the url using the History API (if available)
-               if ( history.pushState && history.replaceState ) {
-                       history.replaceState( { tag: 'mw-pagination' }, '' );
-                       $( window ).on( 'popstate', function ( e ) {
-                               var state = e.originalEvent.state;
-                               if ( state && state.tag === 'mw-pagination' ) {
-                                       switchPage( location.href, true );
-                               }
-                       } );
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/patrol.ajax.js b/resources/src/mediawiki/page/patrol.ajax.js
deleted file mode 100644 (file)
index d8fb249..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*!
- * Animate patrol links to use asynchronous API requests to
- * patrol pages, rather than navigating to a different URI.
- *
- * @since 1.21
- * @author Marius Hoch <hoo@online.de>
- */
-( function ( mw, $ ) {
-       if ( !mw.user.tokens.exists( 'patrolToken' ) ) {
-               // Current user has no patrol right, or an old cached version of user.tokens
-               // that didn't have patrolToken yet.
-               return;
-       }
-       $( function () {
-               var $patrolLinks = $( '.patrollink[data-mw="interface"] a' );
-               $patrolLinks.on( 'click', function ( e ) {
-                       var $spinner, rcid, apiRequest;
-
-                       // Preload the notification module for mw.notify
-                       mw.loader.load( 'mediawiki.notification' );
-
-                       // Hide the link and create a spinner to show it inside the brackets.
-                       $spinner = $.createSpinner( {
-                               size: 'small',
-                               type: 'inline'
-                       } );
-                       $( this ).hide().after( $spinner );
-
-                       rcid = mw.util.getParamValue( 'rcid', this.href );
-                       apiRequest = new mw.Api();
-
-                       apiRequest.postWithToken( 'patrol', {
-                               formatversion: 2,
-                               action: 'patrol',
-                               rcid: rcid
-                       } ).done( function ( data ) {
-                               var title;
-                               // Remove all patrollinks from the page (including any spinners inside).
-                               $patrolLinks.closest( '.patrollink' ).remove();
-                               if ( data.patrol !== undefined ) {
-                                       // Success
-                                       title = new mw.Title( data.patrol.title );
-                                       mw.notify( mw.msg( 'markedaspatrollednotify', title.toText() ) );
-                               } else {
-                                       // This should never happen as errors should trigger fail
-                                       mw.notify( mw.msg( 'markedaspatrollederrornotify' ), { type: 'error' } );
-                               }
-                       } ).fail( function ( error ) {
-                               $spinner.remove();
-                               // Restore the patrol link. This allows the user to try again
-                               // (or open it in a new window, bypassing this ajax module).
-                               $patrolLinks.show();
-                               if ( error === 'noautopatrol' ) {
-                                       // Can't patrol own
-                                       mw.notify( mw.msg( 'markedaspatrollederror-noautopatrol' ), { type: 'warn' } );
-                               } else {
-                                       mw.notify( mw.msg( 'markedaspatrollederrornotify' ), { type: 'error' } );
-                               }
-                       } );
-
-                       e.preventDefault();
-               } );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/ready.js b/resources/src/mediawiki/page/ready.js
deleted file mode 100644 (file)
index e147664..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-( function ( mw, $ ) {
-       mw.hook( 'wikipage.content' ).add( function ( $content ) {
-               var $sortable, $collapsible;
-
-               $collapsible = $content.find( '.mw-collapsible' );
-               if ( $collapsible.length ) {
-                       // Preloaded by Skin::getDefaultModules()
-                       mw.loader.using( 'jquery.makeCollapsible', function () {
-                               $collapsible.makeCollapsible();
-                       } );
-               }
-
-               $sortable = $content.find( 'table.sortable' );
-               if ( $sortable.length ) {
-                       // Preloaded by Skin::getDefaultModules()
-                       mw.loader.using( 'jquery.tablesorter', function () {
-                               $sortable.tablesorter();
-                       } );
-               }
-
-               // Run jquery.checkboxShiftClick
-               $content.find( 'input[type="checkbox"]:not(.noshiftselect)' ).checkboxShiftClick();
-       } );
-
-       // Things outside the wikipage content
-       $( function () {
-               var $nodes;
-
-               // Add accesskey hints to the tooltips
-               $( '[accesskey]' ).updateTooltipAccessKeys();
-
-               $nodes = $( '.catlinks[data-mw="interface"]' );
-               if ( $nodes.length ) {
-                       /**
-                        * Fired when categories are being added to the DOM
-                        *
-                        * It is encouraged to fire it before the main DOM is changed (when $content
-                        * is still detached).  However, this order is not defined either way, so you
-                        * should only rely on $content itself.
-                        *
-                        * This includes the ready event on a page load (including post-edit loads)
-                        * and when content has been previewed with LivePreview.
-                        *
-                        * @event wikipage_categories
-                        * @member mw.hook
-                        * @param {jQuery} $content The most appropriate element containing the content,
-                        *   such as .catlinks
-                        */
-                       mw.hook( 'wikipage.categories' ).fire( $nodes );
-               }
-
-               $( '#t-print a' ).click( function ( e ) {
-                       window.print();
-                       e.preventDefault();
-               } );
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/rollback.js b/resources/src/mediawiki/page/rollback.js
deleted file mode 100644 (file)
index 6db518d..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*!
- * Enhance rollback links by using asynchronous API requests,
- * rather than navigating to an action page.
- *
- * @since 1.28
- * @author Timo Tijhof
- */
-( function ( mw, $ ) {
-
-       $( function () {
-               $( '.mw-rollback-link' ).on( 'click', 'a[data-mw="interface"]', function ( e ) {
-                       var api, $spinner,
-                               $link = $( this ),
-                               url = this.href,
-                               page = mw.util.getParamValue( 'title', url ),
-                               user = mw.util.getParamValue( 'from', url );
-
-                       if ( !page || user === null ) {
-                               // Let native browsing handle the link
-                               return true;
-                       }
-
-                       // Preload the notification module for mw.notify
-                       mw.loader.load( 'mediawiki.notification' );
-
-                       // Remove event handler so that next click (re-try) uses server action
-                       $( e.delegateTarget ).off( 'click' );
-
-                       // Hide the link and create a spinner to show it inside the brackets.
-                       $spinner = $.createSpinner( { size: 'small', type: 'inline' } );
-                       $link.hide().after( $spinner );
-
-                       // @todo: data.messageHtml is no more. Convert to using errorformat=html.
-                       api = new mw.Api();
-                       api.rollback( page, user )
-                               .then( function ( data ) {
-                                       mw.notify( $.parseHTML( data.messageHtml ), {
-                                               title: mw.msg( 'actioncomplete' )
-                                       } );
-
-                                       // Remove link container and the subsequent text node containing " | ".
-                                       if ( e.delegateTarget.nextSibling && e.delegateTarget.nextSibling.nodeType === Node.TEXT_NODE ) {
-                                               $( e.delegateTarget.nextSibling ).remove();
-                                       }
-                                       $( e.delegateTarget ).remove();
-                               }, function ( errorCode, data ) {
-                                       var message = data && data.error && data.error.messageHtml ?
-                                                       $.parseHTML( data.error.messageHtml ) :
-                                                       mw.msg( 'rollbackfailed' ),
-                                               type = errorCode === 'alreadyrolled' ? 'warn' : 'error';
-
-                                       mw.notify( message, {
-                                               type: type,
-                                               title: mw.msg( 'rollbackfailed' ),
-                                               autoHide: false
-                                       } );
-
-                                       // Restore the link (enables user to try again)
-                                       $spinner.remove();
-                                       $link.show();
-                               } );
-
-                       e.preventDefault();
-               } );
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/startup.js b/resources/src/mediawiki/page/startup.js
deleted file mode 100644 (file)
index 7514044..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-( function ( mw, $ ) {
-       // Break out of framesets
-       if ( mw.config.get( 'wgBreakFrames' ) ) {
-               // Note: In IE < 9 strict comparison to window is non-standard (the standard didn't exist yet)
-               // it works only comparing to window.self or window.window (http://stackoverflow.com/q/4850978/319266)
-               if ( window.top !== window.self ) {
-                       // Un-trap us from framesets
-                       window.top.location.href = location.href;
-               }
-       }
-
-       $( function () {
-               var $diff;
-
-               /**
-                * Fired when wiki content is being added to the DOM
-                *
-                * It is encouraged to fire it before the main DOM is changed (when $content
-                * is still detached).  However, this order is not defined either way, so you
-                * should only rely on $content itself.
-                *
-                * This includes the ready event on a page load (including post-edit loads)
-                * and when content has been previewed with LivePreview.
-                *
-                * @event wikipage_content
-                * @member mw.hook
-                * @param {jQuery} $content The most appropriate element containing the content,
-                *   such as #mw-content-text (regular content root) or #wikiPreview (live preview
-                *   root)
-                */
-               mw.hook( 'wikipage.content' ).fire( $( '#mw-content-text' ) );
-
-               $diff = $( 'table.diff[data-mw="interface"]' );
-               if ( $diff.length ) {
-                       /**
-                        * Fired when the diff is added to a page containing a diff
-                        *
-                        * Similar to the {@link mw.hook#event-wikipage_content wikipage.content hook}
-                        * $diff may still be detached when the hook is fired.
-                        *
-                        * @event wikipage_diff
-                        * @member mw.hook
-                        * @param {jQuery} $diff The root element of the MediaWiki diff (`table.diff`).
-                        */
-                       mw.hook( 'wikipage.diff' ).fire( $diff.eq( 0 ) );
-               }
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/watch.js b/resources/src/mediawiki/page/watch.js
deleted file mode 100644 (file)
index 5b41876..0000000
+++ /dev/null
@@ -1,193 +0,0 @@
-/**
- * Animate watch/unwatch links to use asynchronous API requests to
- * watch pages, rather than navigating to a different URI.
- *
- * Usage:
- *
- *     var watch = require( 'mediawiki.page.watch.ajax' );
- *     watch.updateWatchLink(
- *         $node,
- *         'watch',
- *         'loading'
- *     );
- *
- * @class mw.plugin.page.watch.ajax
- * @singleton
- */
-( function ( mw, $ ) {
-       var watch,
-               // The name of the page to watch or unwatch
-               title = mw.config.get( 'wgRelevantPageName' );
-
-       /**
-        * Update the link text, link href attribute and (if applicable)
-        * "loading" class.
-        *
-        * @param {jQuery} $link Anchor tag of (un)watch link
-        * @param {string} action One of 'watch', 'unwatch'
-        * @param {string} [state="idle"] 'idle' or 'loading'. Default is 'idle'
-        */
-       function updateWatchLink( $link, action, state ) {
-               var msgKey, $li, otherAction;
-
-               // A valid but empty jQuery object shouldn't throw a TypeError
-               if ( !$link.length ) {
-                       return;
-               }
-
-               // Invalid actions shouldn't silently turn the page in an unrecoverable state
-               if ( action !== 'watch' && action !== 'unwatch' ) {
-                       throw new Error( 'Invalid action' );
-               }
-
-               // message keys 'watch', 'watching', 'unwatch' or 'unwatching'.
-               msgKey = state === 'loading' ? action + 'ing' : action;
-               otherAction = action === 'watch' ? 'unwatch' : 'watch';
-               $li = $link.closest( 'li' );
-
-               // Trigger a 'watchpage' event for this List item.
-               // Announce the otherAction value as the first param.
-               // Used to monitor the state of watch link.
-               // TODO: Revise when system wide hooks are implemented
-               if ( state === undefined ) {
-                       $li.trigger( 'watchpage.mw', otherAction );
-               }
-
-               $link
-                       .text( mw.msg( msgKey ) )
-                       .attr( 'title', mw.msg( 'tooltip-ca-' + action ) )
-                       .updateTooltipAccessKeys()
-                       .attr( 'href', mw.util.getUrl( title, { action: action } ) );
-
-               // Most common ID style
-               if ( $li.prop( 'id' ) === 'ca-' + otherAction ) {
-                       $li.prop( 'id', 'ca-' + action );
-               }
-
-               if ( state === 'loading' ) {
-                       $link.addClass( 'loading' );
-               } else {
-                       $link.removeClass( 'loading' );
-               }
-       }
-
-       /**
-        * TODO: This should be moved somewhere more accessible.
-        *
-        * @private
-        * @param {string} url
-        * @return {string} The extracted action, defaults to 'view'
-        */
-       function mwUriGetAction( url ) {
-               var action, actionPaths, key, m, parts;
-
-               // TODO: Does MediaWiki give action path or query param
-               // precedence? If the former, move this to the bottom
-               action = mw.util.getParamValue( 'action', url );
-               if ( action !== null ) {
-                       return action;
-               }
-
-               actionPaths = mw.config.get( 'wgActionPaths' );
-               for ( key in actionPaths ) {
-                       if ( actionPaths.hasOwnProperty( key ) ) {
-                               parts = actionPaths[ key ].split( '$1' );
-                               parts = parts.map( mw.RegExp.escape );
-                               m = new RegExp( parts.join( '(.+)' ) ).exec( url );
-                               if ( m && m[ 1 ] ) {
-                                       return key;
-                               }
-
-                       }
-               }
-
-               return 'view';
-       }
-
-       // Expose public methods
-       watch = {
-               updateWatchLink: updateWatchLink
-       };
-       module.exports = watch;
-
-       $( function () {
-               var $links = $( '.mw-watchlink a[data-mw="interface"], a.mw-watchlink[data-mw="interface"]' );
-               if ( !$links.length ) {
-                       // Fallback to the class-based exclusion method for backwards-compatibility
-                       $links = $( '.mw-watchlink a, a.mw-watchlink' );
-                       // Restrict to core interfaces, ignore user-generated content
-                       $links = $links.filter( ':not( #bodyContent *, #content * )' );
-               }
-
-               $links.click( function ( e ) {
-                       var mwTitle, action, api, $link;
-
-                       mwTitle = mw.Title.newFromText( title );
-                       action = mwUriGetAction( this.href );
-
-                       if ( !mwTitle || ( action !== 'watch' && action !== 'unwatch' ) ) {
-                               // Let native browsing handle the link
-                               return true;
-                       }
-                       e.preventDefault();
-                       e.stopPropagation();
-
-                       $link = $( this );
-
-                       if ( $link.hasClass( 'loading' ) ) {
-                               return;
-                       }
-
-                       updateWatchLink( $link, action, 'loading' );
-
-                       // Preload the notification module for mw.notify
-                       mw.loader.load( 'mediawiki.notification' );
-
-                       api = new mw.Api();
-
-                       api[ action ]( title )
-                               .done( function ( watchResponse ) {
-                                       var message, otherAction = action === 'watch' ? 'unwatch' : 'watch';
-
-                                       if ( mwTitle.getNamespaceId() > 0 && mwTitle.getNamespaceId() % 2 === 1 ) {
-                                               message = action === 'watch' ? 'addedwatchtext-talk' : 'removedwatchtext-talk';
-                                       } else {
-                                               message = action === 'watch' ? 'addedwatchtext' : 'removedwatchtext';
-                                       }
-
-                                       mw.notify( mw.message( message, mwTitle.getPrefixedText() ).parseDom(), {
-                                               tag: 'watch-self'
-                                       } );
-
-                                       // Set link to opposite
-                                       updateWatchLink( $link, otherAction );
-
-                                       // Update the "Watch this page" checkbox on action=edit when the
-                                       // page is watched or unwatched via the tab (T14395).
-                                       $( '#wpWatchthis' ).prop( 'checked', watchResponse.watched === true );
-                               } )
-                               .fail( function () {
-                                       var msg, link;
-
-                                       // Reset link to non-loading mode
-                                       updateWatchLink( $link, action );
-
-                                       // Format error message
-                                       link = mw.html.element(
-                                               'a', {
-                                                       href: mw.util.getUrl( title ),
-                                                       title: mwTitle.getPrefixedText()
-                                               }, mwTitle.getPrefixedText()
-                                       );
-                                       msg = mw.message( 'watcherrortext', link );
-
-                                       // Report to user about the error
-                                       mw.notify( msg, {
-                                               tag: 'watch-self',
-                                               type: 'error'
-                                       } );
-                               } );
-               } );
-       } );
-
-}( mediaWiki, jQuery ) );