/* global moment */ ( function () { /** * @classdesc Encapsulates the process of uploading a file * to MediaWiki using the {@link mw.Upload upload model}. * The booklet emits events that can be used to get the stashed * upload and the final file. It can be extended to accept * additional fields from the user for specific scenarios like * for Commons, or campaigns. * * ## Structure * * The {@link OO.ui.BookletLayout booklet layout} has three steps: * * - **Upload**: Has a {@link OO.ui.SelectFileInputWidget field} to get the file object. * * - **Information**: Has a {@link OO.ui.FormLayout form} to collect metadata. This can be * extended. * * - **Insert**: Has details on how to use the file that was uploaded. * * Each step has a form associated with it defined in * {@link mw.Upload.BookletLayout#renderUploadForm renderUploadForm}, * {@link mw.Upload.BookletLayout#renderInfoForm renderInfoForm}, and * {@link mw.Upload.BookletLayout#renderInsertForm renderInfoForm}. The * {@link mw.Upload.BookletLayout#getFile getFile}, * {@link mw.Upload.BookletLayout#getFilename getFilename}, and * {@link mw.Upload.BookletLayout#getText getText} methods are used to get * the information filled in these forms, required to call * {@link mw.Upload mw.Upload}. * * ## Usage * * See the {@link mw.Upload.Dialog upload dialog}. * * The {@link mw.Upload.BookletLayout.event:fileUploaded fileUploaded}, * and {@link mw.Upload.BookletLayout.event:fileSaved fileSaved} events can * be used to get details of the upload. * * ## Extending * * To extend using {@link mw.Upload mw.Upload}, override * {@link mw.Upload.BookletLayout#renderInfoForm renderInfoForm} to render * the form required for the specific use-case. Update the * {@link mw.Upload.BookletLayout#getFilename getFilename}, and * {@link mw.Upload.BookletLayout#getText getText} methods to return data * from your newly created form. If you added new fields you'll also have * to update the {@link mw.Upload.BookletLayout#clear} method. * * If you plan to use a different upload model, apart from what is mentioned * above, you'll also have to override the * {@link mw.Upload.BookletLayout#createUpload createUpload} method to * return the new model. The {@link #saveFile saveFile}, and * the {@link mw.Upload.BookletLayout#uploadFile uploadFile} methods need to be * overridden to use the new model and data returned from the forms. * * @class mw.Upload.BookletLayout * @extends OO.ui.BookletLayout * * @constructor * @description Create an instance of `mw.Upload.BookletLayout`. * @param {Object} config Configuration options; see also the config parameter for the * {@link mw.Upload.BookletLayout} constructor. * @param {jQuery} [config.$overlay] Overlay to use for widgets in the booklet * @param {string} [config.filekey] Sets the stashed file to finish uploading. Overrides most of the file selection process, and fetches a thumbnail from the server. */ mw.Upload.BookletLayout = function ( config ) { // Parent constructor mw.Upload.BookletLayout.super.call( this, config ); this.$overlay = config.$overlay; this.filekey = config.filekey; this.renderUploadForm(); this.renderInfoForm(); this.renderInsertForm(); this.addPages( [ new OO.ui.PageLayout( 'initializing', { scrollable: true, padded: true, content: [ new OO.ui.ProgressBarWidget( { indeterminate: true } ) ] } ), new OO.ui.PageLayout( 'upload', { scrollable: true, padded: true, content: [ this.uploadForm ] } ), new OO.ui.PageLayout( 'info', { scrollable: true, padded: true, content: [ this.infoForm ] } ), new OO.ui.PageLayout( 'insert', { scrollable: true, padded: true, content: [ this.insertForm ] } ) ] ); }; /* Setup */ OO.inheritClass( mw.Upload.BookletLayout, OO.ui.BookletLayout ); /* Events */ /** * Progress events for the uploaded file. * * @event mw.Upload.BookletLayout.fileUploadProgress * @param {number} progress In percentage * @param {Object} duration Duration object from `moment.duration()` */ /** * The file has finished uploading. * * @event mw.Upload.BookletLayout.fileUploaded */ /** * The file has been saved to the database. * * @event mw.Upload.BookletLayout.fileSaved * @param {Object} imageInfo See {@link mw.Upload#getImageInfo} */ /** * The upload form has changed. * * @event mw.Upload.BookletLayout.uploadValid * @param {boolean} isValid The form is valid */ /** * The info form has changed. * * @event mw.Upload.BookletLayout.infoValid * @param {boolean} isValid The form is valid */ /* Properties */ /** * The form rendered in the first step to get the file object. * Rendered in {@link mw.Upload.BookletLayout#renderUploadForm renderUploadForm}. * * @name mw.Upload.BookletLayout.prototype.uploadForm * @type {OO.ui.FormLayout} */ /** * The form rendered in the second step to get metadata. * Rendered in {@link mw.Upload.BookletLayout#renderInfoForm renderInfoForm}. * * @name mw.Upload.BookletLayout.prototype.infoForm * @type {OO.ui.FormLayout} */ /** * The form rendered in the third step to show usage. * Rendered in {@link mw.Upload.BookletLayout#renderInsertForm renderInsertForm}. * * @name mw.Upload.BookletLayout.prototype.insertForm * @type {OO.ui.FormLayout} */ /* Methods */ /** * Initialize for a new upload. * * @return {jQuery.Promise} Promise resolved when everything is initialized */ mw.Upload.BookletLayout.prototype.initialize = function () { this.clear(); this.upload = this.createUpload(); this.setPage( 'initializing' ); if ( this.filekey ) { this.setFilekey( this.filekey ); } return this.upload.getApi().then( // If the user can't upload anything, don't give them the option to. ( api ) => api.getUserInfo().then( ( userInfo ) => { this.setPage( 'upload' ); if ( !userInfo.rights.includes( 'upload' ) ) { if ( !mw.user.isNamed() ) { this.getPage( 'upload' ).$element.msg( 'apierror-mustbeloggedin', mw.msg( 'action-upload' ) ); } else { this.getPage( 'upload' ).$element.msg( 'apierror-permissiondenied', mw.msg( 'action-upload' ) ); } } return $.Deferred().resolve(); }, // Always resolve, never reject () => { this.setPage( 'upload' ); return $.Deferred().resolve(); } ), ( errorMsg ) => { this.setPage( 'upload' ); // eslint-disable-next-line mediawiki/msg-doc this.getPage( 'upload' ).$element.msg( errorMsg ); return $.Deferred().resolve(); } ); }; /** * Create a new upload model. * * @protected * @return {mw.Upload} Upload model */ mw.Upload.BookletLayout.prototype.createUpload = function () { return new mw.Upload( { parameters: { errorformat: 'html', errorlang: mw.config.get( 'wgUserLanguage' ), errorsuselocal: 1, formatversion: 2 } } ); }; /* Uploading */ /** * Uploads the file that was added in the upload form. Uses * {@link mw.Upload.BookletLayout#getFile getFile} to get the HTML5 * file object. * * @protected * @fires mw.Upload.BookletLayout.fileUploadProgress * @fires mw.Upload.BookletLayout.fileUploaded * @return {jQuery.Promise} */ mw.Upload.BookletLayout.prototype.uploadFile = function () { const deferred = $.Deferred(), startTime = mw.now(), file = this.getFile(); this.setPage( 'info' ); if ( this.filekey ) { if ( file === null ) { // Someone gonna get-a hurt real bad throw new Error( 'filekey not passed into file select widget, which is impossible. Quitting while we\'re behind.' ); } // Stashed file already uploaded. deferred.resolve(); this.uploadPromise = deferred; this.emit( 'fileUploaded' ); return deferred; } this.setFilename( file.name ); this.upload.setFile( file ); // The original file name might contain invalid characters, so use our sanitized one this.upload.setFilename( this.getFilename() ); this.uploadPromise = this.upload.uploadToStash(); this.uploadPromise.then( () => { deferred.resolve(); this.emit( 'fileUploaded' ); }, () => { // These errors will be thrown while the user is on the info page. this.getErrorMessageForStateDetails().then( ( errorMessage ) => { deferred.reject( errorMessage ); } ); }, ( progress ) => { const elapsedTime = mw.now() - startTime, estimatedTotalTime = ( 1 / progress ) * elapsedTime, estimatedRemainingTime = moment.duration( estimatedTotalTime - elapsedTime ); this.emit( 'fileUploadProgress', progress, estimatedRemainingTime ); } ); // If there is an error in uploading, come back to the upload page deferred.fail( () => { this.setPage( 'upload' ); } ); return deferred; }; /** * Saves the stash finalizes upload. Uses * {@link mw.Upload.BookletLayout#getFilename getFilename}, and * {@link mw.Upload.BookletLayout#getText getText} to get details from * the form. * * @protected * @fires mw.Upload.BookletLayout.fileSaved * @return {jQuery.Promise} Rejects the promise with an * {@link OO.ui.Error error}, or resolves if the upload was successful. */ mw.Upload.BookletLayout.prototype.saveFile = function () { const deferred = $.Deferred(); this.upload.setFilename( this.getFilename() ); this.upload.setText( this.getText() ); this.uploadPromise.then( () => { this.upload.finishStashUpload().then( () => { // Normalize page name and localise the 'File:' prefix const name = new mw.Title( 'File:' + this.upload.getFilename() ).toString(); this.filenameUsageWidget.setValue( '[[' + name + ']]' ); this.setPage( 'insert' ); deferred.resolve(); this.emit( 'fileSaved', this.upload.getImageInfo() ); }, () => { this.getErrorMessageForStateDetails().then( ( errorMessage ) => { deferred.reject( errorMessage ); } ); } ); } ); return deferred.promise(); }; /** * Get an error message (as OO.ui.Error object) that should be displayed to the user for current * state and state details. * * @protected * @return {jQuery.Promise|undefined} A Promise that will be resolved with an OO.ui.Error. */ mw.Upload.BookletLayout.prototype.getErrorMessageForStateDetails = function () { const state = this.upload.getState(), stateDetails = this.upload.getStateDetails(), warnings = stateDetails.upload && stateDetails.upload.warnings, $ul = $( '
' ).msg( 'fileexists', 'File:' + warnings.exists ), { recoverable: false } ) ); } else if ( warnings[ 'exists-normalized' ] !== undefined ) { return $.Deferred().resolve( new OO.ui.Error( $( '
' ).msg( 'fileexists', 'File:' + warnings[ 'exists-normalized' ] ), { recoverable: false } ) ); } else if ( warnings[ 'page-exists' ] !== undefined ) { return $.Deferred().resolve( new OO.ui.Error( $( '