mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-05 03:08:37 +00:00
234 lines
7.3 KiB
JavaScript
234 lines
7.3 KiB
JavaScript
|
/*!
|
||
|
* VisualEditor MediaWiki ArticleTargetSaver.
|
||
|
*
|
||
|
* @copyright 2011-2019 VisualEditor Team and others; see AUTHORS.txt
|
||
|
* @license The MIT License (MIT); see LICENSE.txt
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Target saver.
|
||
|
*
|
||
|
* Light-weight saver.
|
||
|
*
|
||
|
* @class mw.libs.ve.targetSaver
|
||
|
* @singleton
|
||
|
*/
|
||
|
( function () {
|
||
|
mw.libs.ve = mw.libs.ve || {};
|
||
|
|
||
|
mw.libs.ve.targetSaver = {
|
||
|
/**
|
||
|
* Preload the library required for deflating so the user doesn't
|
||
|
* have to wait when postHtml is called.
|
||
|
*/
|
||
|
preloadDeflate: function () {
|
||
|
mw.loader.load( 'mediawiki.deflate' );
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Compress a string with deflate.
|
||
|
*
|
||
|
* @param {string} html HTML to deflate
|
||
|
* @return {jQuery.Promise} Promise resolved with deflated HTML
|
||
|
*/
|
||
|
deflate: function ( html ) {
|
||
|
return mw.loader.using( 'mediawiki.deflate' ).then( function () {
|
||
|
return mw.deflate( html );
|
||
|
} );
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get HTML to send to Parsoid.
|
||
|
*
|
||
|
* If the document was generated from scratch (e.g. inside VisualEditor's converter), the
|
||
|
* source document can be passed in to transplant the head tag, as well as the attributes
|
||
|
* on the html and body tags.
|
||
|
*
|
||
|
* @param {HTMLDocument} newDoc Document generated by ve.dm.Converter. Will be modified.
|
||
|
* @param {HTMLDocument} [oldDoc] Old document to copy attributes from.
|
||
|
* @return {string} Full HTML document
|
||
|
*/
|
||
|
getHtml: function ( newDoc, oldDoc ) {
|
||
|
var i, len;
|
||
|
|
||
|
function copyAttributes( from, to ) {
|
||
|
var i, len;
|
||
|
for ( i = 0, len = from.attributes.length; i < len; i++ ) {
|
||
|
to.setAttribute( from.attributes[ i ].name, from.attributes[ i ].value );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( oldDoc ) {
|
||
|
// Copy the head from the old document
|
||
|
for ( i = 0, len = oldDoc.head.childNodes.length; i < len; i++ ) {
|
||
|
newDoc.head.appendChild( oldDoc.head.childNodes[ i ].cloneNode( true ) );
|
||
|
}
|
||
|
// Copy attributes from the old document for the html, head and body
|
||
|
copyAttributes( oldDoc.documentElement, newDoc.documentElement );
|
||
|
copyAttributes( oldDoc.head, newDoc.head );
|
||
|
copyAttributes( oldDoc.body, newDoc.body );
|
||
|
}
|
||
|
|
||
|
// Filter out junk that may have been added by browser plugins
|
||
|
$( newDoc )
|
||
|
.find( [
|
||
|
'script', // T54884, T65229, T96533, T103430
|
||
|
'noscript', // T144891
|
||
|
'object', // T65229
|
||
|
'style:not( [ data-mw ] )', // T55252, but allow <style data-mw/> e.g. TemplateStyles T188143
|
||
|
'embed', // T53521, T54791, T65121
|
||
|
'a[href^="javascript:"]', // T200971
|
||
|
'img[src^="data:"]', // T192392
|
||
|
'div[id="myEventWatcherDiv"]', // T53423
|
||
|
'div[id="sendToInstapaperResults"]', // T63776
|
||
|
'div[id="kloutify"]', // T69006
|
||
|
'div[id^="mittoHidden"]', // T70900
|
||
|
'div.hon.certificateLink', // HON (T209619)
|
||
|
'div.donut-container' // Web of Trust (T189148)
|
||
|
].join( ',' ) )
|
||
|
.remove();
|
||
|
|
||
|
// data-mw-section-id is copied to headings by ve.unwrapParsoidSections
|
||
|
// Remove these to avoid triggering selser.
|
||
|
$( newDoc ).find( '[data-mw-section-id]:not( section )' ).removeAttr( 'data-mw-section-id' );
|
||
|
|
||
|
// Add doctype manually
|
||
|
// ve.serializeXhtml is loaded separately from utils.parsing
|
||
|
// eslint-disable-next-line no-undef
|
||
|
return '<!doctype html>' + ve.serializeXhtml( newDoc );
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Serialize and deflate an HTML document
|
||
|
*
|
||
|
* @param {HTMLDocument} doc Document generated by ve.dm.Converter. Will be modified.
|
||
|
* @param {HTMLDocument} [oldDoc] Old document to copy attributes from.
|
||
|
* @return {jQuery.Promise} Promise resolved with deflated HTML
|
||
|
*/
|
||
|
deflateDoc: function ( doc, oldDoc ) {
|
||
|
return this.deflate( this.getHtml( doc, oldDoc ) );
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Post an HTML document to the API.
|
||
|
*
|
||
|
* Serializes the document to HTML, deflates it, then passes to #postHtml.
|
||
|
*
|
||
|
* @param {HTMLDocument} doc Document to save
|
||
|
* @param {Object} extraData Extra data to send to the API
|
||
|
* @param {Object} options Options
|
||
|
* @return {jQuery.Promise} Promise which resolves if the post was successful
|
||
|
*/
|
||
|
saveDoc: function ( doc, extraData, options ) {
|
||
|
return this.deflateDoc( doc ).then( function ( html ) {
|
||
|
return this.postHtml(
|
||
|
html,
|
||
|
null,
|
||
|
extraData,
|
||
|
options
|
||
|
);
|
||
|
} );
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Post HTML to the API.
|
||
|
*
|
||
|
* By default uses action=visualeditoredit, paction=save.
|
||
|
*
|
||
|
* @param {string} html HTML to post. Deflating is optional but recommended.
|
||
|
* Should be included for retries even if a cache key is provided.
|
||
|
* @param {string} [cacheKey] Optional cache key of HTML stashed on server.
|
||
|
* @param {Object} [extraData] Extra data to send to the API
|
||
|
* @param {Object} [options] Options
|
||
|
* @param {mw.Api} [options.api] Api to use
|
||
|
* @param {Function} [options.now] Function returning current time in milliseconds for tracking, e.g. ve.now
|
||
|
* @param {Function} [options.track] Tracking function
|
||
|
* @param {string} [options.eventName] Event name for tracking
|
||
|
* @return {jQuery.Promise} Promise which resolves if the post was successful
|
||
|
*/
|
||
|
postHtml: function ( html, cacheKey, extraData, options ) {
|
||
|
var request, fullEventName, api, data, start,
|
||
|
saver = this;
|
||
|
|
||
|
options = options || {};
|
||
|
api = options.api || new mw.Api();
|
||
|
|
||
|
if ( options.now ) {
|
||
|
start = options.now();
|
||
|
}
|
||
|
|
||
|
if ( cacheKey ) {
|
||
|
data = $.extend( {}, extraData, { cachekey: cacheKey } );
|
||
|
} else {
|
||
|
data = $.extend( {}, extraData, { html: html } );
|
||
|
}
|
||
|
data = $.extend(
|
||
|
{
|
||
|
action: 'visualeditoredit',
|
||
|
paction: 'save',
|
||
|
format: 'json',
|
||
|
formatversion: 2
|
||
|
},
|
||
|
data
|
||
|
);
|
||
|
|
||
|
if ( data.token ) {
|
||
|
request = api.post( data, { contentType: 'multipart/form-data' } );
|
||
|
} else {
|
||
|
request = api.postWithToken( 'csrf', data, { contentType: 'multipart/form-data' } );
|
||
|
}
|
||
|
return request.then(
|
||
|
function ( response, jqxhr ) {
|
||
|
var eventData;
|
||
|
// Log data about the request if eventName was set
|
||
|
if ( options.track && options.eventName ) {
|
||
|
eventData = {
|
||
|
bytes: require( 'mediawiki.String' ).byteLength( jqxhr.responseText ),
|
||
|
duration: options.now() - start
|
||
|
};
|
||
|
fullEventName = 'performance.system.' + options.eventName +
|
||
|
( cacheKey ? '.withCacheKey' : '.withoutCacheKey' );
|
||
|
options.track( fullEventName, eventData );
|
||
|
}
|
||
|
return jqxhr;
|
||
|
},
|
||
|
function ( errorName, errorObject ) {
|
||
|
var eventData,
|
||
|
responseText = OO.getProp( errorObject, 'xhr', 'responseText' );
|
||
|
|
||
|
if ( responseText && options.track && options.eventName ) {
|
||
|
eventData = {
|
||
|
bytes: require( 'mediawiki.String' ).byteLength( responseText ),
|
||
|
duration: options.now() - start
|
||
|
};
|
||
|
if ( errorName === 'badcachekey' ) {
|
||
|
fullEventName = 'performance.system.' + options.eventName + '.badCacheKey';
|
||
|
} else {
|
||
|
fullEventName = 'performance.system.' + options.eventName + '.withoutCacheKey';
|
||
|
}
|
||
|
options.track( fullEventName, eventData );
|
||
|
}
|
||
|
// This cache key is evidently bad, clear it
|
||
|
if ( options.onCacheKeyFail ) {
|
||
|
options.onCacheKeyFail();
|
||
|
}
|
||
|
if ( errorName === 'badcachekey' ) {
|
||
|
// If the cache key failed, try again without the cache key
|
||
|
return saver.postHtml(
|
||
|
html,
|
||
|
null,
|
||
|
extraData,
|
||
|
options
|
||
|
);
|
||
|
} else {
|
||
|
// Failed for some other reason - let caller handle it.
|
||
|
// FIXME Can't just `return this` because all callers are broken.
|
||
|
return $.Deferred().reject( null, errorName, errorObject ).promise();
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
};
|
||
|
}() );
|