mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 18:39:52 +00:00
42757a724d
Use mw.config wgCurRevisionId for oldie instead of uri query. That way it also set on regular views, and as a bonus it the normalised value (e.g. if on a page with oldid in query but the id doesn't exist or is somehow invalid, it won't use it). Centralise page existence logic in JS and rename Target.oldId to Target.pageRevId (to further indicate that it is always set, not just on oldId views. To detect an oldId view, do a boolean check on value from currentUri.query.oldid directly). In PHP Api, move oldid logic to execute() method instead of locally from the getHTML call. That way it is always set, avoids hitting this bug in other methods instead of just getHTML(). Since the mw.Target constructor now retrieves the ID from mw.config directly, the second argument was removed. Change-Id: I223235a6ea8b4178c50beeaaedb709b2de7cf0b5
407 lines
12 KiB
JavaScript
407 lines
12 KiB
JavaScript
/*global mw */
|
|
|
|
/**
|
|
* VisualEditor MediaWiki initialization Target class.
|
|
*
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* MediaWiki target.
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @extends {ve.EventEmitter}
|
|
* @param {String} pageName Name of target page
|
|
*/
|
|
ve.init.mw.Target = function VeInitMwTarget( pageName ) {
|
|
// Parent constructor
|
|
ve.EventEmitter.call( this );
|
|
|
|
// Properties
|
|
this.pageName = pageName;
|
|
this.pageExists = mw.config.get( 'wgArticleId', 0 ) !== 0;
|
|
this.pageRevId = mw.config.get( 'wgCurRevisionId' );
|
|
this.editToken = mw.user.tokens.get( 'editToken' );
|
|
this.apiUrl = mw.util.wikiScript( 'api' );
|
|
this.submitUrl = ( new mw.Uri( mw.util.wikiGetlink( this.pageName ) ) )
|
|
.extend( { 'action': 'submit' } );
|
|
this.modules = ['ext.visualEditor.core', 'ext.visualEditor.specialMessages']
|
|
.concat(
|
|
window.devicePixelRatio > 1 ?
|
|
['ext.visualEditor.viewPageTarget.icons-vector', 'ext.visualEditor.icons-vector'] :
|
|
['ext.visualEditor.viewPageTarget.icons-raster', 'ext.visualEditor.icons-raster']
|
|
);
|
|
this.loading = false;
|
|
this.saving = false;
|
|
this.serializing = false;
|
|
this.submitting = false;
|
|
this.baseTimeStamp = null;
|
|
this.startTimeStamp = null;
|
|
this.dom = null;
|
|
this.isMobileDevice = (
|
|
'ontouchstart' in window ||
|
|
( window.DocumentTouch && document instanceof window.DocumentTouch )
|
|
);
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
ve.inheritClass( ve.init.mw.Target, ve.EventEmitter );
|
|
|
|
/* Static Methods */
|
|
|
|
/**
|
|
* Handle response to a successful load request.
|
|
*
|
|
* This method is called within the context of a target instance. If successful the DOM from the
|
|
* server will be parsed, stored in {this.dom} and then {ve.init.mw.Target.onReady} will be called once
|
|
* the modules are ready.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} response XHR Response object
|
|
* @param {String} status Text status message
|
|
* @emits loadError (null, message, null)
|
|
*/
|
|
ve.init.mw.Target.onLoad = function ( response ) {
|
|
var data = response.visualeditor;
|
|
if ( !data && !response.error ) {
|
|
ve.init.mw.Target.onLoadError.call(
|
|
this, null, 'Invalid response in response from server', null
|
|
);
|
|
} else if ( response.error || data.result === 'error' ) {
|
|
ve.init.mw.Target.onLoadError.call( this, null, 'Server error', null );
|
|
} else if ( typeof data.content !== 'string' ) {
|
|
ve.init.mw.Target.onLoadError.call(
|
|
this, null, 'No HTML content in response from server', null
|
|
);
|
|
} else {
|
|
this.dom = $( '<div>' ).html( data.content )[0];
|
|
this.baseTimeStamp = data.basetimestamp;
|
|
this.startTimeStamp = data.starttimestamp;
|
|
// Everything worked, the page was loaded, continue as soon as the module is ready
|
|
mw.loader.using( this.modules, ve.bind( ve.init.mw.Target.onReady, this ) );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Respond to both DOM and modules being loaded and ready.
|
|
*
|
|
* This method is called within the context of a target instance. After the load event is emitted
|
|
* this.dom is cleared, allowing it to be garbage collected.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @emits load (dom)
|
|
*/
|
|
ve.init.mw.Target.onReady = function () {
|
|
this.loading = false;
|
|
this.emit( 'load', this.dom );
|
|
// Release DOM data
|
|
this.dom = null;
|
|
};
|
|
|
|
/**
|
|
* Respond to an unsuccessful load request.
|
|
*
|
|
* This method is called within the context of a target instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} response XHR Response object
|
|
* @param {String} status Text status message
|
|
* @param {Mixed} error Thrown exception or HTTP error string
|
|
* @emits loadError (response, status, error)
|
|
*/
|
|
ve.init.mw.Target.onLoadError = function ( response, status, error ) {
|
|
this.loading = false;
|
|
this.emit( 'loadError', response, status, error );
|
|
};
|
|
|
|
/**
|
|
* Respond to a successful save request.
|
|
*
|
|
* This method is called within the context of a target instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} response XHR Response object
|
|
* @param {String} status Text status message
|
|
* @emits save (html)
|
|
* @emits saveError (null, message, null)
|
|
*/
|
|
ve.init.mw.Target.onSave = function ( response ) {
|
|
this.saving = false;
|
|
var data = response.visualeditor;
|
|
if ( !data && !response.error ) {
|
|
ve.init.mw.Target.onSaveError.call( this, null, 'Invalid response from server', null );
|
|
} else if ( response.error ) {
|
|
if (response.error.code === 'editconflict' ) {
|
|
this.emit( 'editConflict' );
|
|
} else {
|
|
ve.init.mw.Target.onSaveError.call(
|
|
this, null, 'Unsuccessful request: ' + response.error.info, null
|
|
);
|
|
}
|
|
} else if ( data.result !== 'success' ) {
|
|
ve.init.mw.Target.onSaveError.call( this, null, 'Failed request: ' + data.result, null );
|
|
} else if ( typeof data.content !== 'string' ) {
|
|
ve.init.mw.Target.onSaveError.call(
|
|
this, null, 'Invalid HTML content in response from server', null
|
|
);
|
|
} else {
|
|
this.emit( 'save', data.content );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Respond to an unsuccessful save request.
|
|
*
|
|
* This method is called within the context of a target instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} data HTTP Response object
|
|
* @param {String} status Text status message
|
|
* @param {Mixed} error Thrown exception or HTTP error string
|
|
* @emits saveError (response, status, error)
|
|
*/
|
|
ve.init.mw.Target.onSaveError = function ( response, status, error ) {
|
|
this.saving = false;
|
|
this.emit( 'saveError', response, status, error );
|
|
};
|
|
|
|
/**
|
|
* Respond to a successful serialize request.
|
|
*
|
|
* This method is called within the context of a target instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} response XHR Response object
|
|
* @param {String} status Text status message
|
|
* @emits save (html)
|
|
* @emits saveError (null, message, null)
|
|
*/
|
|
ve.init.mw.Target.onSerialize = function ( response ) {
|
|
this.serializing = false;
|
|
var data = response.visualeditor;
|
|
if ( !data && !response.error ) {
|
|
ve.init.mw.Target.onSerializeError.call( this, null, 'Invalid response from server', null );
|
|
} else if ( response.error || data.result === 'error' ) {
|
|
ve.init.mw.Target.onSerializeError.call( this, null, 'Server error', null );
|
|
} else if ( typeof data.content !== 'string' ) {
|
|
ve.init.mw.Target.onSerializeError.call(
|
|
this, null, 'No Wikitext content in response from server', null
|
|
);
|
|
} else {
|
|
if ( typeof this.serializeCallback === 'function' ) {
|
|
this.serializeCallback( data.content );
|
|
delete this.serializeCallback;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Respond to an unsuccessful serialize request.
|
|
*
|
|
* This method is called within the context of a target instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} data HTTP Response object
|
|
* @param {String} status Text status message
|
|
* @param {Mixed} error Thrown exception or HTTP error string
|
|
* @emits saveError (response, status, error)
|
|
*/
|
|
ve.init.mw.Target.onSerializeError = function ( response, status, error ) {
|
|
this.serializing = false;
|
|
this.emit( 'serializeError', response, status, error );
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Gets DOM from Parsoid API.
|
|
*
|
|
* This method performs an asynchronous action and uses a callback function to handle the result.
|
|
*
|
|
* A side-effect of calling this method is that it requests {this.modules} be loaded.
|
|
*
|
|
* @method
|
|
* @returns {Boolean} Loading has been started
|
|
*/
|
|
ve.init.mw.Target.prototype.load = function () {
|
|
// Prevent duplicate requests
|
|
if ( this.loading ) {
|
|
return false;
|
|
}
|
|
// Start loading the module immediately
|
|
mw.loader.load( this.modules );
|
|
// Load DOM
|
|
this.loading = true;
|
|
$.ajax( {
|
|
'url': this.apiUrl,
|
|
'data': {
|
|
'action': 'visualeditor',
|
|
'paction': 'parse',
|
|
'page': this.pageName,
|
|
'oldid': this.pageRevId,
|
|
'token': this.editToken,
|
|
'format': 'json'
|
|
},
|
|
'dataType': 'json',
|
|
'type': 'POST',
|
|
// Wait up to 100 seconds before giving up
|
|
'timeout': 100000,
|
|
'cache': 'false',
|
|
'success': ve.bind( ve.init.mw.Target.onLoad, this ),
|
|
'error': ve.bind( ve.init.mw.Target.onLoadError, this )
|
|
} );
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Posts DOM to Parsoid API.
|
|
*
|
|
* This method performs an asynchronous action and uses a callback function to handle the result.
|
|
*
|
|
* @example
|
|
* target.save( dom, { 'summary': 'test', 'minor': true, 'watch': false } );
|
|
*
|
|
* @method
|
|
* @param {HTMLElement} dom DOM to save
|
|
* @param {Object} options Saving options
|
|
* - {String} summary Edit summary
|
|
* - {Boolean} minor Edit is a minor edit
|
|
* - {Boolean} watch Watch this page
|
|
* @returns {Boolean} Saving has been started
|
|
*/
|
|
ve.init.mw.Target.prototype.save = function ( dom, options ) {
|
|
// Prevent duplicate requests
|
|
if ( this.saving ) {
|
|
return false;
|
|
}
|
|
// Save DOM
|
|
this.saving = true;
|
|
$.ajax( {
|
|
'url': this.apiUrl,
|
|
'data': {
|
|
'format': 'json',
|
|
'action': 'visualeditor',
|
|
'paction': 'save',
|
|
'page': this.pageName,
|
|
'oldid': this.pageRevId,
|
|
'basetimestamp': this.baseTimeStamp,
|
|
'starttimestamp': this.startTimeStamp,
|
|
'html': $( dom ).html(),
|
|
'token': this.editToken,
|
|
'summary': options.summary,
|
|
'minor': options.minor,
|
|
'watch': options.watch
|
|
},
|
|
'dataType': 'json',
|
|
'type': 'POST',
|
|
// Wait up to 10 seconds before giving up
|
|
'timeout': 10000,
|
|
'success': ve.bind( ve.init.mw.Target.onSave, this ),
|
|
'error': ve.bind( ve.init.mw.Target.onSaveError, this )
|
|
} );
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Posts DOM to Parsoid API.
|
|
*
|
|
* This method performs a synchronous action and will take the user to a new page when complete.
|
|
*
|
|
* @example
|
|
* target.submit( wikitext, { 'summary': 'test', 'minor': true, 'watch': false } );
|
|
*
|
|
* @method
|
|
* @param {String} wikitext Wikitext to submit
|
|
* @param {Object} options Saving options
|
|
* - {String} summary Edit summary
|
|
* - {Boolean} minor Edit is a minor edit
|
|
* - {Boolean} watch Watch this page
|
|
* @returns {Boolean} Submitting has been started
|
|
*/
|
|
ve.init.mw.Target.prototype.submit = function ( wikitext, options ) {
|
|
// Prevent duplicate requests
|
|
if ( this.submitting ) {
|
|
return false;
|
|
}
|
|
// Save DOM
|
|
this.submitting = true;
|
|
var key,
|
|
$form = $( '<form method="post" enctype="multipart/form-data"></form>' ),
|
|
params = {
|
|
'format': 'text/x-wiki',
|
|
'oldid': this.pageRevId || 0,
|
|
'wpStarttime': this.baseTimeStamp,
|
|
'wpEdittime': this.startTimeStamp,
|
|
'wpTextbox1': wikitext,
|
|
'wpSummary': options.summary,
|
|
'wpWatchthis': options.watch,
|
|
'wpMinoredit': options.minor,
|
|
'wpEditToken': this.editToken,
|
|
'wpSave': 1
|
|
};
|
|
// Add params as hidden fields
|
|
for ( key in params ) {
|
|
$form.append( $( '<input type="hidden">' ).attr( { 'name': key, 'value': params[key] } ) );
|
|
}
|
|
// Submit the form, mimicking a traditional edit
|
|
$form.attr( 'action', this.submitUrl ).appendTo( 'body' ).submit();
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Gets Wikitext from Parsoid API.
|
|
*
|
|
* This method performs an asynchronous action and uses a callback function to handle the result.
|
|
*
|
|
* @example
|
|
* target.serialize(
|
|
* dom,
|
|
* function ( wikitext ) {
|
|
* // Do something with the loaded DOM
|
|
* }
|
|
* );
|
|
*
|
|
* @method
|
|
* @param {HTMLElement} dom DOM to serialize
|
|
* @param {Function} callback Function to call when complete, accepts error and wikitext arguments
|
|
* @returns {Boolean} Serializing has beeen started
|
|
*/
|
|
ve.init.mw.Target.prototype.serialize = function ( dom, callback ) {
|
|
// Prevent duplicate requests
|
|
if ( this.serializing ) {
|
|
return false;
|
|
}
|
|
// Load DOM
|
|
this.serializing = true;
|
|
this.serializeCallback = callback;
|
|
$.ajax( {
|
|
'url': this.apiUrl,
|
|
'data': {
|
|
'action': 'visualeditor',
|
|
'paction': 'serialize',
|
|
'html': $( dom ).html(),
|
|
'page': this.pageName,
|
|
'token': this.editToken,
|
|
'format': 'json'
|
|
},
|
|
'dataType': 'json',
|
|
'type': 'POST',
|
|
// Wait up to 100 seconds before giving up
|
|
'timeout': 100000,
|
|
'cache': 'false',
|
|
'success': ve.bind( ve.init.mw.Target.onSerialize, this ),
|
|
'error': ve.bind( ve.init.mw.Target.onSerializeError, this )
|
|
} );
|
|
return true;
|
|
};
|