Refactor upload dialog to make it configurable
authorBartosz Dziewoński <matma.rex@gmail.com>
Thu, 5 May 2016 15:20:05 +0000 (11:20 -0400)
committerBartosz Dziewoński <matma.rex@gmail.com>
Mon, 16 May 2016 19:14:58 +0000 (21:14 +0200)
This aims to solve all the problems and fulfill all the use cases.
It allows the dialog to be configured for Wikimedia Commons without
hardcoding anything, and it should be flexible enough for third-party
use. The default configuration should be sane for any wiki.

The file upload dialog can be configured using $wgUploadDialog.
See DefaultSettings.php for documentation. Example configuration for
Wikimedia Commons: Id56370e2334c8fe34e88180356232b48c244b7c4.

Configuration is loaded using ResourceLoaderUploadDialogModule for
local uploads or using ApiQuerySiteinfo (action=query&meta=siteinfo)
for uploads to a foreign wiki. Custom localisation messages may be
loaded using action=query&meta=allmessages.

Renamed messages:
  upload-form-label-own-work-message-local       -> upload-form-label-own-work-message-generic-local
  upload-form-label-not-own-work-message-local   -> upload-form-label-not-own-work-message-generic-local
  upload-form-label-not-own-work-local-local     -> upload-form-label-not-own-work-local-generic-local
  upload-form-label-own-work-message-default     -> upload-form-label-own-work-message-generic-foreign
  upload-form-label-not-own-work-message-default -> upload-form-label-not-own-work-message-generic-foreign
  upload-form-label-not-own-work-local-default   -> upload-form-label-not-own-work-local-generic-foreign

Deleted messages, moved to WikimediaMessages in Id2977e19330aeaf854157d4355cd17e5dc72f16a:
  upload-form-label-own-work-message-shared
  upload-form-label-not-own-work-message-shared
  upload-form-label-not-own-work-local-shared

Bug: T118097
Bug: T120998
Bug: T121632
Bug: T121633
Bug: T127895
Change-Id: I3017b8f09c27625deb7a92d6f667895b71cc0637

14 files changed:
RELEASE-NOTES-1.27
autoload.php
includes/DefaultSettings.php
includes/api/ApiQuerySiteinfo.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/resourceloader/ResourceLoaderUploadDialogModule.php [new file with mode: 0644]
languages/i18n/en.json
languages/i18n/qqq.json
resources/Resources.php
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js
resources/src/mediawiki/mediawiki.ForeignUpload.js
resources/src/mediawiki/mediawiki.Upload.BookletLayout.js

index f44e8c5..7c50e4f 100644 (file)
@@ -239,6 +239,8 @@ The following PHP extensions are strongly recommended:
    like changing a password.
 ** Two new globals, $wgChangeCredentialsBlacklist and $wgRemoveCredentialsBlacklist
    can be used to prevent the web UI and the API changing certain authentication data.
+* The file upload dialog (available if you install WikiEditor or VisualEditor)
+  can now be configured using $wgUploadDialog.
 
 === External library changes in 1.27 ===
 
index 6cfffad..f79bace 100644 (file)
@@ -1143,6 +1143,7 @@ $wgAutoloadLocalClasses = [
        'ResourceLoaderSkinModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSkinModule.php',
        'ResourceLoaderSpecialCharacterDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php',
        'ResourceLoaderStartUpModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderStartUpModule.php',
+       'ResourceLoaderUploadDialogModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUploadDialogModule.php',
        'ResourceLoaderUserCSSPrefsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php',
        'ResourceLoaderUserDefaultsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserDefaultsModule.php',
        'ResourceLoaderUserGroupsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserGroupsModule.php',
index 6088e8f..0b70d16 100644 (file)
@@ -535,6 +535,64 @@ $wgUseInstantCommons = false;
  */
 $wgForeignUploadTargets = [];
 
+/**
+ * Configuration for file uploads using the embeddable upload dialog
+ * (https://www.mediawiki.org/wiki/Upload_dialog).
+ *
+ * This applies also to foreign uploads to this wiki (the configuration is loaded by remote wikis
+ * using the action=query&meta=siteinfo API).
+ *
+ * See below for documentation of each property. None of the properties may be omitted.
+ */
+$wgUploadDialog = [
+       // Fields to make available in the dialog. `true` means that this field is visible, `false` means
+       // that it is hidden. The "Name" field can't be hidden. Note that you also have to add the
+       // matching replacement to the 'filepage' format key below to make use of these.
+       'fields' => [
+               'description' => true,
+               'date' => false,
+               'categories' => false,
+       ],
+       // Suffix of localisation messages used to describe the license under which the uploaded file will
+       // be released. The same value may be set for both 'local' and 'foreign' uploads.
+       'licensemessages' => [
+               // The 'local' messages are used for local uploads on this wiki:
+               // * upload-form-label-own-work-message-generic-local
+               // * upload-form-label-not-own-work-message-generic-local
+               // * upload-form-label-not-own-work-local-generic-local
+               'local' => 'generic-local',
+               // The 'foreign' messages are used for cross-wiki uploads from other wikis to this wiki:
+               // * upload-form-label-own-work-message-generic-foreign
+               // * upload-form-label-not-own-work-message-generic-foreign
+               // * upload-form-label-not-own-work-local-generic-foreign
+               'foreign' => 'generic-foreign',
+       ],
+       // Upload comment to use. Available replacements:
+       // * $HOST - domain name from which a cross-wiki upload originates
+       // * $PAGENAME - wiki page name from which an upload originates
+       'comment' => '',
+       // Format of the file page wikitext to be generated from the fields input by the user.
+       'format' => [
+               // Wrapper for the whole page. Available replacements:
+               // * $DESCRIPTION - file description, as input by the user (only if the 'description' field is
+               //   enabled), wrapped as defined below in the 'description' key
+               // * $DATE - file creation date, as input by the user (only if the 'date' field is enabled)
+               // * $SOURCE - as defined below in the 'ownwork' key, may be extended in the future
+               // * $AUTHOR - linked user name, may be extended in the future
+               // * $LICENSE - as defined below in the 'license' key, may be extended in the future
+               // * $CATEGORIES - file categories wikitext, as input by the user (only if the 'categories'
+               //   field is enabled), or if no input, as defined below in the 'uncategorized' key
+               'filepage' => '$DESCRIPTION',
+               // Wrapped for file description. Available replacements:
+               // * $LANGUAGE - source wiki's content language
+               // * $TEXT - input by the user
+               'description' => '$TEXT',
+               'ownwork' => '',
+               'license' => '',
+               'uncategorized' => '',
+       ],
+];
+
 /**
  * File backend structure configuration.
  *
index f05556e..a08740a 100644 (file)
@@ -108,6 +108,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                case 'defaultoptions':
                                        $fit = $this->appendDefaultOptions( $p );
                                        break;
+                               case 'uploaddialog':
+                                       $fit = $this->appendUploadDialog( $p );
+                                       break;
                                default:
                                        ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" );
                        }
@@ -771,6 +774,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                return $this->getResult()->addValue( 'query', $property, $options );
        }
 
+       public function appendUploadDialog( $property ) {
+               $config = $this->getConfig()->get( 'UploadDialog' );
+               return $this->getResult()->addValue( 'query', $property, $config );
+       }
+
        private function formatParserTags( $item ) {
                return "<{$item}>";
        }
@@ -838,6 +846,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                        'variables',
                                        'protocols',
                                        'defaultoptions',
+                                       'uploaddialog',
                                ],
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
                        ],
index 43eed78..a802cc7 100644 (file)
        "apihelp-query+siteinfo-paramvalue-prop-variables": "Returns a list of variable IDs.",
        "apihelp-query+siteinfo-paramvalue-prop-protocols": "Returns a list of protocols that are allowed in external links.",
        "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Returns the default values for user preferences.",
+       "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "Returns the upload dialog configuration.",
        "apihelp-query+siteinfo-param-filteriw": "Return only local or only nonlocal entries of the interwiki map.",
        "apihelp-query+siteinfo-param-showalldb": "List all database servers, not just the one lagging the most.",
        "apihelp-query+siteinfo-param-numberingroup": "Lists the number of users in user groups.",
index 37bb4b0..e6faa21 100644 (file)
        "apihelp-query+siteinfo-paramvalue-prop-variables": "{{doc-apihelp-paramvalue|query+siteinfo|prop|variables}}",
        "apihelp-query+siteinfo-paramvalue-prop-protocols": "{{doc-apihelp-paramvalue|query+siteinfo|prop|protocols}}",
        "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "{{doc-apihelp-paramvalue|query+siteinfo|prop|defaultoptions}}",
+       "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "{{doc-apihelp-paramvalue|query+siteinfo|prop|uploaddialog}}",
        "apihelp-query+siteinfo-param-filteriw": "{{doc-apihelp-param|query+siteinfo|filteriw}}",
        "apihelp-query+siteinfo-param-showalldb": "{{doc-apihelp-param|query+siteinfo|showalldb}}",
        "apihelp-query+siteinfo-param-numberingroup": "{{doc-apihelp-param|query+siteinfo|numberingroup}}",
diff --git a/includes/resourceloader/ResourceLoaderUploadDialogModule.php b/includes/resourceloader/ResourceLoaderUploadDialogModule.php
new file mode 100644 (file)
index 0000000..52e2210
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * ResourceLoader module for the upload dialog configuration data.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for the upload dialog configuration data.
+ *
+ * @since 1.27
+ */
+class ResourceLoaderUploadDialogModule extends ResourceLoaderModule {
+
+       protected $targets = [ 'desktop', 'mobile' ];
+
+       public function getScript( ResourceLoaderContext $context ) {
+               $config = $context->getResourceLoader()->getConfig();
+               return ResourceLoader::makeConfigSetScript( [
+                       'wgUploadDialog' => $config->get( 'UploadDialog' ),
+               ] );
+       }
+
+       public function enableModuleContentVersion() {
+               return true;
+       }
+}
index c522df8..8b9fefe 100644 (file)
        "upload-form-label-own-work": "This is my own work",
        "upload-form-label-infoform-categories": "Categories",
        "upload-form-label-infoform-date": "Date",
-       "upload-form-label-own-work-message-local": "I confirm that I am uploading this file following the terms of service and licensing policies on {{SITENAME}}.",
-       "upload-form-label-not-own-work-message-local": "If you are not able to upload this file under the policies of {{SITENAME}}, please close this dialog and try another method.",
-       "upload-form-label-not-own-work-local-local": "You may also want to try [[Special:Upload|the default upload page]].",
-       "upload-form-label-own-work-message-default": "I understand that I am uploading this file to a shared repository. I confirm that I am doing so following the terms of service and licensing policies there.",
-       "upload-form-label-not-own-work-message-default": "If you are not able to upload this file under the policies of the shared repository, please close this dialog and try another method.",
-       "upload-form-label-not-own-work-local-default": "You may also want to try using [[Special:Upload|the upload page on {{SITENAME}}]], if this file can be uploaded there under their policies.",
-       "upload-form-label-own-work-message-shared": "I attest that I own the copyright on this file, and agree to irrevocably release this file to Wikimedia Commons under the [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0] license, and I agree to the [https://wikimediafoundation.org/wiki/Terms_of_Use Terms of Use].",
-       "upload-form-label-not-own-work-message-shared": "If you do not own the copyright on this file, or you wish to release it under a different license, consider using the [https://commons.wikimedia.org/wiki/Special:UploadWizard Commons Upload Wizard].",
-       "upload-form-label-not-own-work-local-shared": "You may also want to try using [[Special:Upload|the upload page on {{SITENAME}}]], if the site allows the upload of this file under their policies.",
+       "upload-form-label-own-work-message-generic-local": "I confirm that I am uploading this file following the terms of service and licensing policies on {{SITENAME}}.",
+       "upload-form-label-not-own-work-message-generic-local": "If you are not able to upload this file under the policies of {{SITENAME}}, please close this dialog and try another method.",
+       "upload-form-label-not-own-work-local-generic-local": "You may also want to try [[Special:Upload|the default upload page]].",
+       "upload-form-label-own-work-message-generic-foreign": "I understand that I am uploading this file to a shared repository. I confirm that I am doing so following the terms of service and licensing policies there.",
+       "upload-form-label-not-own-work-message-generic-foreign": "If you are not able to upload this file under the policies of the shared repository, please close this dialog and try another method.",
+       "upload-form-label-not-own-work-local-generic-foreign": "You may also want to try using [[Special:Upload|the upload page on {{SITENAME}}]], if this file can be uploaded there under their policies.",
        "backend-fail-stream": "Could not stream file \"$1\".",
        "backend-fail-backup": "Could not backup file \"$1\".",
        "backend-fail-notexists": "The file $1 does not exist.",
index 8fa5b74..21e1344 100644 (file)
        "upload-form-label-own-work": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Label for own work confirmation checkbox",
        "upload-form-label-infoform-categories": "Label for category selector input\n{{Identical|Category}}",
        "upload-form-label-infoform-date": "Label for date input\n{{Identical|Date}}",
-       "upload-form-label-own-work-message-local": "Message shown by local when a user affirms that their file upload to the local wiki follows the terms of service and licensing policies of the local wiki.",
-       "upload-form-label-not-own-work-message-local": "Message shown by local when a user cannot upload a file to the local wiki.",
-       "upload-form-label-not-own-work-local-local": "Suggests uploading a file via Special:Upload instead of using whatever method they're currently using.",
-       "upload-form-label-own-work-message-default": "Message shown by default when a user affirms that they are allowed to upload a file to a remote wiki.",
-       "upload-form-label-not-own-work-message-default": "Message shown by default when a user cannot upload a file to a remote wiki.",
-       "upload-form-label-not-own-work-local-default": "Suggests uploading a file locally instead of to a remote wiki.",
-       "upload-form-label-own-work-message-shared": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Legal message, confirming that the user is allowed to upload the file.",
-       "upload-form-label-not-own-work-message-shared": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Explains alternatives when the copyright isn't owned by the uploader.",
-       "upload-form-label-not-own-work-local-shared": "[[File:Cross-wiki media upload dialog, December 2015 AB test option 1.png|thumb]] Message suggesting the user might want to upload a file locally instead of to Wikimedia Commons.",
+       "upload-form-label-own-work-message-generic-local": "Message shown by local when a user affirms that their file upload to the local wiki follows the terms of service and licensing policies of the local wiki.",
+       "upload-form-label-not-own-work-message-generic-local": "Message shown by local when a user cannot upload a file to the local wiki.",
+       "upload-form-label-not-own-work-local-generic-local": "Suggests uploading a file via Special:Upload instead of using whatever method they're currently using.",
+       "upload-form-label-own-work-message-generic-foreign": "Message shown by default when a user affirms that they are allowed to upload a file to a remote wiki.",
+       "upload-form-label-not-own-work-message-generic-foreign": "Message shown by default when a user cannot upload a file to a remote wiki.",
+       "upload-form-label-not-own-work-local-generic-foreign": "Suggests uploading a file locally instead of to a remote wiki.",
        "backend-fail-stream": "Parameters:\n* $1 - a filename",
        "backend-fail-backup": "Parameters:\n* $1 - a filename",
        "backend-fail-notexists": "Parameters:\n* $1 - a filename",
index cf2abdb..3fc5801 100644 (file)
@@ -1169,10 +1169,14 @@ return [
                        'upload-foreign-cant-upload',
                ]
        ],
+       'mediawiki.ForeignStructuredUpload.config' => [
+               'class' => 'ResourceLoaderUploadDialogModule',
+       ],
        'mediawiki.ForeignStructuredUpload' => [
                'scripts' => 'resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js',
                'dependencies' => [
                        'mediawiki.ForeignUpload',
+                       'mediawiki.ForeignStructuredUpload.config',
                ],
        ],
        'mediawiki.Upload.Dialog' => [
@@ -1284,6 +1288,7 @@ return [
                        'mediawiki.widgets.CategorySelector',
                        'mediawiki.widgets.DateInputWidget',
                        'mediawiki.jqueryMsg',
+                       'mediawiki.api.messages',
                        'moment',
                        'mediawiki.libs.jpegmeta',
                ],
@@ -1291,15 +1296,12 @@ return [
                        'upload-form-label-own-work',
                        'upload-form-label-infoform-categories',
                        'upload-form-label-infoform-date',
-                       'upload-form-label-own-work-message-default',
-                       'upload-form-label-not-own-work-message-default',
-                       'upload-form-label-not-own-work-local-default',
-                       'upload-form-label-own-work-message-shared',
-                       'upload-form-label-not-own-work-message-shared',
-                       'upload-form-label-not-own-work-local-shared',
-                       'upload-form-label-own-work-message-local',
-                       'upload-form-label-not-own-work-message-local',
-                       'upload-form-label-not-own-work-local-local',
+                       'upload-form-label-own-work-message-generic-local',
+                       'upload-form-label-not-own-work-message-generic-local',
+                       'upload-form-label-not-own-work-local-generic-local',
+                       'upload-form-label-own-work-message-generic-foreign',
+                       'upload-form-label-not-own-work-message-generic-foreign',
+                       'upload-form-label-not-own-work-local-generic-foreign',
                ],
        ],
        'mediawiki.toc' => [
index 8509fbc..16fec73 100644 (file)
                var booklet = this;
                return mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this ).then(
                        function () {
-                               // Point the CategorySelector to the right wiki
-                               return booklet.upload.getApi().then(
-                                       function ( api ) {
+                               return $.when(
+                                       // Point the CategorySelector to the right wiki
+                                       booklet.upload.getApi().then( function ( api ) {
                                                // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
                                                if ( api.apiUrl ) {
                                                        // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
                                                        booklet.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
                                                }
                                                return $.Deferred().resolve();
-                                       },
-                                       function () {
-                                               return $.Deferred().resolve();
-                                       }
+                                       } ),
+                                       // Set up booklet fields and license messages to match configuration
+                                       booklet.upload.loadConfig().then( function ( config ) {
+                                               var
+                                                       msgPromise,
+                                                       isLocal = booklet.upload.target === 'local',
+                                                       fields = config.fields,
+                                                       msgs = config.licensemessages[ isLocal ? 'local' : 'foreign' ];
+
+                                               // Hide disabled fields
+                                               booklet.descriptionField.toggle( !!fields.description );
+                                               booklet.categoriesField.toggle( !!fields.categories );
+                                               booklet.dateField.toggle( !!fields.date );
+                                               // Update form validity
+                                               booklet.onInfoFormChange();
+
+                                               // Load license messages from the remote wiki if we don't have these messages locally
+                                               // (this means that we only load messages from the foreign wiki for custom config)
+                                               if ( mw.message( 'upload-form-label-own-work-message-' + msgs ).exists() ) {
+                                                       msgPromise = $.Deferred().resolve();
+                                               } else {
+                                                       msgPromise = booklet.upload.apiPromise.then( function ( api ) {
+                                                               return api.loadMessages( [
+                                                                       'upload-form-label-own-work-message-' + msgs,
+                                                                       'upload-form-label-not-own-work-message-' + msgs,
+                                                                       'upload-form-label-not-own-work-local-' + msgs
+                                                               ] );
+                                                       } );
+                                               }
+
+                                               // Update license messages
+                                               return msgPromise.then( function () {
+                                                       booklet.$ownWorkMessage
+                                                               .msg( 'upload-form-label-own-work-message-' + msgs )
+                                                               .find( 'a' ).attr( 'target', '_blank' );
+                                                       booklet.$notOwnWorkMessage
+                                                               .msg( 'upload-form-label-not-own-work-message-' + msgs )
+                                                               .find( 'a' ).attr( 'target', '_blank' );
+                                                       booklet.$notOwnWorkLocal
+                                                               .msg( 'upload-form-label-not-own-work-local-' + msgs )
+                                                               .find( 'a' ).attr( 'target', '_blank' );
+                                               } );
+                                       } )
                                );
-                       },
-                       function () {
-                               return $.Deferred().resolve();
                        }
+               ).then(
+                       null,
+                       // Always resolve, never reject
+                       function () { return $.Deferred().resolve(); }
                );
        };
 
         * @inheritdoc
         */
        mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () {
-               var fieldset, $ownWorkMessage, $notOwnWorkMessage,
-                       ownWorkMessage, notOwnWorkMessage, notOwnWorkLocal,
-                       validTargets = mw.config.get( 'wgForeignUploadTargets' ),
-                       target = this.target || validTargets[ 0 ] || 'local',
+               var fieldset,
                        layout = this;
 
-               // upload-form-label-own-work-message-local
-               // upload-form-label-own-work-message-shared
-               ownWorkMessage = mw.message( 'upload-form-label-own-work-message-' + target );
-               // upload-form-label-not-own-work-message-local
-               // upload-form-label-not-own-work-message-shared
-               notOwnWorkMessage = mw.message( 'upload-form-label-not-own-work-message-' + target );
-               // upload-form-label-not-own-work-local-local
-               // upload-form-label-not-own-work-local-shared
-               notOwnWorkLocal = mw.message( 'upload-form-label-not-own-work-local-' + target );
-
-               if ( !ownWorkMessage.exists() ) {
-                       ownWorkMessage = mw.message( 'upload-form-label-own-work-message-default' );
-               }
-               if ( !notOwnWorkMessage.exists() ) {
-                       notOwnWorkMessage = mw.message( 'upload-form-label-not-own-work-message-default' );
-               }
-               if ( !notOwnWorkLocal.exists() ) {
-                       notOwnWorkLocal = mw.message( 'upload-form-label-not-own-work-local-default' );
-               }
-
-               $ownWorkMessage = $( '<p>' ).append( ownWorkMessage.parseDom() )
+               // These elements are filled with text in #initialize
+               // TODO Refactor this to be in one place
+               this.$ownWorkMessage = $( '<p>' )
                        .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
-               $notOwnWorkMessage = $( '<div>' ).append(
-                       $( '<p>' ).append( notOwnWorkMessage.parseDom() ),
-                       $( '<p>' ).append( notOwnWorkLocal.parseDom() )
-               );
-               $ownWorkMessage.add( $notOwnWorkMessage ).find( 'a' ).attr( 'target', '_blank' );
+               this.$notOwnWorkMessage = $( '<p>' );
+               this.$notOwnWorkLocal = $( '<p>' );
 
                this.selectFileWidget = new OO.ui.SelectFileWidget( {
                        showDropTarget: true
                } );
                this.messageLabel = new OO.ui.LabelWidget( {
-                       label: $notOwnWorkMessage
+                       label: $( '<div>' ).append(
+                               this.$notOwnWorkMessage,
+                               this.$notOwnWorkLocal
+                       )
                } );
                this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) {
                        layout.messageLabel.toggle( !on );
                                align: 'inline',
                                label: $( '<div>' ).append(
                                        $( '<p>' ).text( mw.msg( 'upload-form-label-own-work' ) ),
-                                       $ownWorkMessage
+                                       this.$ownWorkMessage
                                )
                        } ),
                        new OO.ui.FieldLayout( this.messageLabel, {
                        mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
                } );
 
+               this.filenameField = new OO.ui.FieldLayout( this.filenameWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-name' ),
+                       align: 'top',
+                       classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
+                       notices: [ mw.msg( 'upload-form-label-infoform-name-tooltip' ) ]
+               } );
+               this.descriptionField = new OO.ui.FieldLayout( this.descriptionWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-description' ),
+                       align: 'top',
+                       classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
+                       notices: [ mw.msg( 'upload-form-label-infoform-description-tooltip' ) ]
+               } );
+               this.categoriesField = new OO.ui.FieldLayout( this.categoriesWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-categories' ),
+                       align: 'top'
+               } );
+               this.dateField = new OO.ui.FieldLayout( this.dateWidget, {
+                       label: mw.msg( 'upload-form-label-infoform-date' ),
+                       align: 'top'
+               } );
+
                fieldset = new OO.ui.FieldsetLayout( {
                        label: mw.msg( 'upload-form-label-infoform-title' )
                } );
                fieldset.addItems( [
-                       new OO.ui.FieldLayout( this.filenameWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-name' ),
-                               align: 'top',
-                               classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
-                               notices: [ mw.msg( 'upload-form-label-infoform-name-tooltip' ) ]
-                       } ),
-                       new OO.ui.FieldLayout( this.descriptionWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-description' ),
-                               align: 'top',
-                               classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
-                               notices: [ mw.msg( 'upload-form-label-infoform-description-tooltip' ) ]
-                       } ),
-                       new OO.ui.FieldLayout( this.categoriesWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-categories' ),
-                               align: 'top'
-                       } ),
-                       new OO.ui.FieldLayout( this.dateWidget, {
-                               label: mw.msg( 'upload-form-label-infoform-date' ),
-                               align: 'top'
-                       } )
+                       this.filenameField,
+                       this.descriptionField,
+                       this.categoriesField,
+                       this.dateField
                ] );
                this.infoForm = new OO.ui.FormLayout( {
                        classes: [ 'mw-upload-bookletLayout-infoForm' ],
         * @inheritdoc
         */
        mw.ForeignStructuredUpload.BookletLayout.prototype.onInfoFormChange = function () {
-               var layout = this;
-               $.when(
-                       this.filenameWidget.getValidity(),
-                       this.descriptionWidget.getValidity(),
-                       this.dateWidget.getValidity()
-               ).done( function () {
+               var layout = this,
+                       validityPromises = [];
+
+               validityPromises.push( this.filenameWidget.getValidity() );
+               if ( this.descriptionField.isVisible() ) {
+                       validityPromises.push( this.descriptionWidget.getValidity() );
+               }
+               if ( this.dateField.isVisible() ) {
+                       validityPromises.push( this.dateWidget.getValidity() );
+               }
+
+               $.when.apply( $, validityPromises ).done( function () {
                        layout.emit( 'infoValid', true );
                } ).fail( function () {
                        layout.emit( 'infoValid', false );
index f90071c..4a0366a 100644 (file)
@@ -1,4 +1,4 @@
-( function ( mw, OO ) {
+( function ( mw, $, OO ) {
        /**
         * @class mw.ForeignStructuredUpload
         * @extends mw.ForeignUpload
                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;
+                               } );
+                       } );
+               }
+
+               return this.configPromise;
+       };
+
        /**
         * Add categories to the upload.
         *
         * @return {string}
         */
        ForeignStructuredUpload.prototype.getText = function () {
-               return (
-                       '== {{int:filedesc}} ==\n' +
-                       '{{Information' +
-                       '\n|description=' +
-                       this.getDescriptions() +
-                       '\n|date=' +
-                       this.getDate() +
-                       '\n|source=' +
-                       this.getSource() +
-                       '\n|author=' +
-                       this.getUser() +
-                       '\n}}\n\n' +
-                       '== {{int:license-header}} ==\n' +
-                       this.getLicense() +
-                       '\n\n' +
-                       this.getCategories()
-               );
+               return this.config.format.filepage
+                       // Replace "numbered 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 () {
+               return this.config.comment
+                       .replace( '$PAGENAME', mw.config.get( 'wgPageName' ) )
+                       .replace( '$HOST', location.host );
        };
 
        /**
 
                for ( i = 0; i < this.descriptions.length; i++ ) {
                        desc = this.descriptions[ i ];
-                       templateCalls.push( '{{' + desc.language + '|1=' + desc.text + '}}' );
+                       templateCalls.push(
+                               this.config.format.description
+                                       .replace( '$LANGUAGE', desc.language )
+                                       .replace( '$TEXT', desc.text )
+                       );
                }
 
                return templateCalls.join( '\n' );
                var i, cat, categoryLinks = [];
 
                if ( this.categories.length === 0 ) {
-                       return '{{subst:unc}}';
+                       return this.config.format.uncategorized;
                }
 
                for ( i = 0; i < this.categories.length; i++ ) {
         * @return {string}
         */
        ForeignStructuredUpload.prototype.getLicense = function () {
-               // Make sure this matches the messages for different targets in
-               // mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm
-               return this.target === 'shared' ? '{{self|cc-by-sa-4.0}}' : '';
+               return this.config.format.license;
        };
 
        /**
         * @return {string}
         */
        ForeignStructuredUpload.prototype.getSource = function () {
-               return '{{own}}';
+               return this.config.format.ownwork;
        };
 
        /**
        };
 
        mw.ForeignStructuredUpload = ForeignStructuredUpload;
-}( mediaWiki, OO ) );
+}( mediaWiki, jQuery, OO ) );
index 1a0b59a..eeeab68 100644 (file)
                // actual API call methods to wait for the apiPromise to resolve
                // before continuing.
                mw.Upload.call( this, null );
-
-               if ( this.target !== 'local' ) {
-                       // Keep these untranslated. We don't know the content language of the foreign wiki, best to
-                       // stick to English in the text.
-                       this.setComment( 'Cross-wiki upload from ' + location.host );
-               }
        }
 
        OO.inheritClass( ForeignUpload, mw.Upload );
index 2b28cb4..7a7469a 100644 (file)
 
                return this.upload.getApi().then(
                        function ( api ) {
-                               // If the user can't upload anything, don't give them the option to.
-                               return api.getUserInfo().then(
-                                       function ( userInfo ) {
+                               return $.when(
+                                       booklet.upload.loadConfig(),
+                                       // If the user can't upload anything, don't give them the option to.
+                                       api.getUserInfo().then( function ( userInfo ) {
                                                if ( userInfo.rights.indexOf( 'upload' ) === -1 ) {
                                                        // TODO Use a better error message when not all logged-in users can upload
                                                        booklet.getPage( 'upload' ).$element.msg( 'api-error-mustbeloggedin' );
                                                }
                                                return $.Deferred().resolve();
-                                       },
-                                       function () {
-                                               return $.Deferred().resolve();
-                                       }
+                                       } )
+                               ).then(
+                                       null,
+                                       // Always resolve, never reject
+                                       function () { return $.Deferred().resolve(); }
                                );
                        },
                        function ( errorMsg ) {