Allow switching from wikitext to VE

Just by pressing the VE tab for now

Requires a relatively new version of restbase

Bug: T49779
Change-Id: I2a5294345f5e0f469c1dd1bdd29dbce211571a4e
This commit is contained in:
Alex Monk 2015-10-08 23:16:56 +01:00
parent 297d7d2cfc
commit 85b745666f
10 changed files with 85 additions and 34 deletions

View file

@ -85,7 +85,7 @@ class ApiVisualEditor extends ApiBase {
return new $class( $params );
}
private function requestRestbase( $method, $path, $params ) {
private function requestRestbase( $method, $path, $params, $reqheaders = array() ) {
$request = array(
'method' => $method,
'url' => '/restbase/local/v1/' . $path
@ -95,6 +95,7 @@ class ApiVisualEditor extends ApiBase {
} else {
$request['body'] = $params;
}
$request['headers'] = $reqheaders;
$response = $this->serviceClient->run( $request );
if ( $response['code'] === 200 && $response['error'] === "" ) {
// If response was served directly from Varnish, use the response
@ -116,11 +117,11 @@ class ApiVisualEditor extends ApiBase {
return $response['body'];
}
protected function storeInSerializationCache( $title, $oldid, $html ) {
protected function storeInSerializationCache( $title, $oldid, $html, $etag ) {
global $wgMemc;
// Convert the VE HTML to wikitext
$text = $this->postHTML( $title, $html, array( 'oldid' => $oldid ) );
$text = $this->postHTML( $title, $html, array( 'oldid' => $oldid ), $etag );
if ( $text === false ) {
return false;
}
@ -149,7 +150,7 @@ class ApiVisualEditor extends ApiBase {
return $wgMemc->get( $key );
}
protected function postHTML( $title, $html, $parserParams ) {
protected function postHTML( $title, $html, $parserParams, $etag ) {
if ( $parserParams['oldid'] === 0 ) {
$parserParams['oldid'] = '';
}
@ -163,7 +164,8 @@ class ApiVisualEditor extends ApiBase {
array(
'html' => $html,
'scrub_wikitext' => 1,
)
),
array( 'If-Match' => $etag )
);
}
@ -568,7 +570,7 @@ class ApiVisualEditor extends ApiBase {
if ( $params['html'] === null ) {
$this->dieUsageMsg( 'missingparam', 'html' );
}
$content = $this->postHTML( $title, $html, $parserParams );
$content = $this->postHTML( $title, $html, $parserParams, $params['etag'] );
if ( $content === false ) {
$this->dieUsage( 'Error contacting the document server', 'docserver' );
}
@ -583,7 +585,7 @@ class ApiVisualEditor extends ApiBase {
$this->dieUsage( 'No cached serialization found with that key', 'badcachekey' );
}
} else {
$wikitext = $this->postHTML( $title, $html, $parserParams );
$wikitext = $this->postHTML( $title, $html, $parserParams, $params['etag'] );
if ( $wikitext === false ) {
$this->dieUsage( 'Error contacting the document server', 'docserver' );
}
@ -601,7 +603,12 @@ class ApiVisualEditor extends ApiBase {
if ( !isset( $parserParams['oldid'] ) ) {
$parserParams['oldid'] = Revision::newFromTitle( $title )->getId();
}
$key = $this->storeInSerializationCache( $title, $parserParams['oldid'], $html );
$key = $this->storeInSerializationCache(
$title,
$parserParams['oldid'],
$html,
$params['etag']
);
$result = array( 'result' => 'success', 'cachekey' => $key );
break;
@ -669,6 +676,7 @@ class ApiVisualEditor extends ApiBase {
'wikitext' => null,
'oldid' => null,
'html' => null,
'etag' => null,
'cachekey' => null,
'pst' => false,
);

View file

@ -149,7 +149,7 @@ class ApiVisualEditorEdit extends ApiVisualEditor {
$this->dieUsage( 'No cached serialization found with that key', 'badcachekey' );
}
} else {
$wikitext = $this->postHTML( $page, $html, $parserParams );
$wikitext = $this->postHTML( $page, $html, $parserParams, $params['etag'] );
if ( $wikitext === false ) {
$this->dieUsage( 'Error contacting the Parsoid/RESTbase server', 'docserver' );
}
@ -253,6 +253,7 @@ class ApiVisualEditorEdit extends ApiVisualEditor {
'minor' => null,
'watch' => null,
'html' => null,
'etag' => null,
'summary' => null,
'captchaid' => null,
'captchaword' => null,
@ -285,6 +286,7 @@ class ApiVisualEditorEdit extends ApiVisualEditor {
'oldid' => 'The revision number to use. Defaults to latest revision. Use 0 for new page.',
'minor' => 'Flag for minor edit.',
'html' => 'HTML to send to Parsoid in exchange for wikitext',
'etag' => 'ETag to send',
'summary' => 'Edit summary',
'basetimestamp' => 'When saving, set this to the timestamp of the revision that was'
. ' edited. Used to detect edit conflicts.',

View file

@ -469,6 +469,7 @@ class VisualEditorHooks {
'namespacesWithSubpages' => $coreConfig->get( 'NamespacesWithSubpages' ),
'specialBooksources' => urldecode( SpecialPage::getTitleFor( 'Booksources' )->getPrefixedURL() ),
'restbaseUrl' => $coreConfig->get( 'VisualEditorRestbaseURL' ),
'fullRestbaseUrl' => $coreConfig->get( 'VisualEditorFullRestbaseURL' ),
);
return true;

View file

@ -30,6 +30,7 @@
"visualeditor-enable-experimental": "ext.visualEditor.experimental"
},
"VisualEditorRestbaseURL": false,
"VisualEditorFullRestbaseURL": false,
"VisualEditorSerializationCacheTimeout": 3600,
"VisualEditorUseChangeTagging": true,
"VisualEditorSupportedSkins": [

View file

@ -23,6 +23,7 @@
"apihelp-visualeditor-param-cachekey": "For serialize or diff, use the result of a previous serializeforcache request with this key. Overrides $1html.",
"apihelp-visualeditor-param-format": "",
"apihelp-visualeditor-param-html": "HTML to send to Parsoid to convert to wikitext.",
"apihelp-visualeditor-param-etag": "ETag to send.",
"apihelp-visualeditor-param-oldid": "The revision number to use (defaults to latest revision).",
"apihelp-visualeditor-param-paction": "Action to perform.",
"apihelp-visualeditor-param-page": "The page to perform actions on.",
@ -35,6 +36,7 @@
"apihelp-visualeditoredit-param-captchaid": "Captcha ID (when saving with a captcha response).",
"apihelp-visualeditoredit-param-captchaword": "Answer to the captcha (when saving with a captcha response).",
"apihelp-visualeditoredit-param-html": "HTML to send to Parsoid in exchange for wikitext.",
"apihelp-visualeditoredit-param-etag": "ETag to send.",
"apihelp-visualeditoredit-param-minor": "Flag for minor edit.",
"apihelp-visualeditoredit-param-needcheck": "When saving, set this parameter if the revision might have roundtrip problems. This will result in the edit being tagged.",
"apihelp-visualeditoredit-param-oldid": "The revision number to use. Defaults to latest revision. Use 0 for a new page.",

View file

@ -33,6 +33,7 @@
"apihelp-visualeditor-param-cachekey": "In computer science, in the context of data storage, serialization is the process of translating data structures or object state into a format that can be stored (for example, in a file or memory buffer, or transmitted across a network connection link) and reconstructed later in the same or another computer environment.\n{{doc-apihelp-param|visualeditor|cachekey}}",
"apihelp-visualeditor-param-format": "{{doc-apihelp-param|visualeditor|format}}",
"apihelp-visualeditor-param-html": "{{doc-apihelp-param|visualeditor|html}}",
"apihelp-visualeditor-param-etag": "{{doc-apihelp-param|visualeditor|etag}}",
"apihelp-visualeditor-param-oldid": "{{doc-apihelp-param|visualeditor|oldid}}",
"apihelp-visualeditor-param-paction": "{{doc-apihelp-param|visualeditor|paction}}",
"apihelp-visualeditor-param-page": "{{doc-apihelp-param|visualeditor|page}}",
@ -45,6 +46,7 @@
"apihelp-visualeditoredit-param-captchaid": "{{doc-apihelp-param|visualeditoredit|captchaid}}",
"apihelp-visualeditoredit-param-captchaword": "{{doc-apihelp-param|visualeditoredit|captchaword}}",
"apihelp-visualeditoredit-param-html": "{{doc-apihelp-param|visualeditoredit|html}}",
"apihelp-visualeditoredit-param-etag": "{{doc-apihelp-param|visualeditoredit|etag}}",
"apihelp-visualeditoredit-param-minor": "{{doc-apihelp-param|visualeditoredit|minor}}",
"apihelp-visualeditoredit-param-needcheck": "{{doc-apihelp-param|visualeditoredit|needcheck}}",
"apihelp-visualeditoredit-param-oldid": "{{doc-apihelp-param|visualeditoredit|oldid}}",

View file

@ -160,7 +160,7 @@
.then( function () {
return mw.libs.ve.targetLoader.requestPageData(
mw.config.get( 'wgRelevantPageName' ),
uri.query.oldid,
uri.query.oldid || $( 'input[name=parentRevId]' ).val(),
'mwTarget' // ve.init.mw.DesktopArticleTarget.static.name
);
} )
@ -206,6 +206,9 @@
isViewPage = (
mw.config.get( 'wgIsArticle' ) &&
!( 'diff' in uri.query )
) || (
mw.config.get( 'wgAction' ) === 'edit' ||
mw.config.get( 'wgAction' ) === 'submit'
);
// On a view page, extend the current URI so parameters like oldid are carried over
// On a non-view page, use viewUri

View file

@ -60,7 +60,7 @@ ve.init.mw.DesktopArticleTarget = function VeInitMwDesktopArticleTarget( config
this.initialEditSummary = currentUri.query.summary;
this.namespaceName = mw.config.get( 'wgCanonicalNamespace' );
this.viewUri = new mw.Uri( mw.util.getUrl( this.pageName ) );
this.veEditUri = this.viewUri.clone().extend( { veaction: 'edit' } );
this.veEditUri = this.viewUri.clone().extend( { veaction: 'edit', action: null } );
this.isViewPage = (
mw.config.get( 'wgAction' ) === 'view' &&
currentUri.query.diff === undefined
@ -758,7 +758,7 @@ ve.init.mw.DesktopArticleTarget.prototype.onToolbarMetaButtonClick = function ()
* @inheritdoc
*/
ve.init.mw.DesktopArticleTarget.prototype.editSource = function () {
if ( !this.getSurface().getModel().hasBeenModified() ) {
if ( !this.getSurface().getModel().hasBeenModified() && !this.fromEditedState ) {
this.switchToWikitextEditor( true, false );
return;
}
@ -848,7 +848,7 @@ ve.init.mw.DesktopArticleTarget.prototype.setupSkinTabs = function () {
if ( target.getSurface() && !target.deactivating ) {
target.editSource();
if ( target.getSurface().getModel().hasBeenModified() ) {
if ( target.getSurface().getModel().hasBeenModified() || target.fromEditedState ) {
e.preventDefault();
}
}
@ -1001,6 +1001,7 @@ ve.init.mw.DesktopArticleTarget.prototype.transformPage = function () {
// Set the current URL
uri = this.currentUri;
uri.query.veaction = 'edit';
delete uri.query.action;
history.pushState( this.popState, document.title, uri );
}
@ -1032,6 +1033,9 @@ ve.init.mw.DesktopArticleTarget.prototype.restorePage = function () {
if ( 'vesection' in uri.query ) {
delete uri.query.vesection;
}
if ( 'action' in uri.query ) {
delete uri.query.action;
}
// If there are any other query parameters left, re-use that uri object.
// Otherwise use the canonical style view url (T44553, T102363).

View file

@ -293,8 +293,7 @@ ve.init.mw.Target.static.fixBase = function ( doc ) {
* @param {string} status Text status message
*/
ve.init.mw.Target.prototype.loadSuccess = function ( response ) {
var i, len, linkData, aboutDoc, docRevIdMatches,
docRevId = 0,
var i, len, linkData, aboutDoc, docRevId, docRevIdMatches,
data = response ? response.visualeditor : null;
if ( typeof data.content !== 'string' ) {
@ -302,6 +301,8 @@ ve.init.mw.Target.prototype.loadSuccess = function ( response ) {
} else {
ve.track( 'trace.parseResponse.enter' );
this.originalHtml = data.content;
this.etag = data.etag;
this.fromEditedState = data.fromEditedState;
this.doc = ve.parseXhtml( this.originalHtml );
// Fix relative or missing base URL if needed
@ -321,7 +322,7 @@ ve.init.mw.Target.prototype.loadSuccess = function ( response ) {
docRevId = parseInt( docRevIdMatches[ 1 ] );
}
}
if ( docRevId !== this.revid ) {
if ( docRevId && docRevId !== this.revid ) {
if ( this.retriedRevIdConflict ) {
// Retried already, just error the second time.
this.loadFail(
@ -388,7 +389,7 @@ ve.init.mw.Target.prototype.onReady = function () {
);
this.loading = false;
this.edited = false;
this.edited = this.fromEditedState;
this.setupSurface( this.doc, function () {
// loadSuccess() may have called setAssumeExistence( true );
ve.init.platform.linkCache.setAssumeExistence( false );
@ -1264,7 +1265,8 @@ ve.init.mw.Target.prototype.prepareCacheKey = function ( doc ) {
paction: 'serializeforcache',
html: deflatedHtml,
page: target.pageName,
oldid: target.revid
oldid: target.revid,
etag: target.etag
},
{ contentType: 'multipart/form-data' }
);
@ -1544,7 +1546,8 @@ ve.init.mw.Target.prototype.save = function ( doc, options ) {
oldid: this.revid,
basetimestamp: this.baseTimeStamp,
starttimestamp: this.startTimeStamp,
token: this.editToken
token: this.editToken,
etag: this.etag
} );
this.saving = this.tryWithPreparedCacheKey( doc, data, 'save' )
@ -1569,7 +1572,8 @@ ve.init.mw.Target.prototype.showChanges = function ( doc ) {
action: 'visualeditor',
paction: 'diff',
page: this.pageName,
oldid: this.revid
oldid: this.revid,
etag: this.etag
}, 'diff' )
.done( this.showChangesSuccess.bind( this ) )
.fail( this.showChangesFail.bind( this ) );
@ -1646,7 +1650,8 @@ ve.init.mw.Target.prototype.serialize = function ( doc, callback ) {
action: 'visualeditor',
paction: 'serialize',
page: this.pageName,
oldid: this.revid
oldid: this.revid,
etag: this.etag
}, 'serialize' )
.done( ve.init.mw.Target.prototype.serializeSuccess.bind( this ) )
.fail( ve.init.mw.Target.prototype.serializeFail.bind( this ) );
@ -1773,7 +1778,7 @@ ve.init.mw.Target.prototype.attachToolbarSaveButton = function () {
ve.init.mw.Target.prototype.updateToolbarSaveButtonState = function () {
var isDisabled;
this.edited = this.getSurface().getModel().hasBeenModified();
this.edited = this.getSurface().getModel().hasBeenModified() || this.fromEditedState;
// Disable the save button if we have no history
isDisabled = !this.edited && !this.restoring;
this.toolbarSaveButton.setDisabled( isDisabled );

View file

@ -93,10 +93,11 @@
* @return {jQuery.Promise} Abortable promise resolved with a JSON object
*/
requestPageData: function ( pageName, oldid, targetName ) {
var start, apiXhr, restbaseXhr, apiPromise, restbasePromise, dataPromise,
var start, apiXhr, restbaseXhr, apiPromise, restbasePromise, dataPromise, pageHtmlUrl,
fromEditedState = false,
data = {
action: 'visualeditor',
paction: conf.restbaseUrl ? 'metadata' : 'parse',
paction: ( conf.fullRestbaseUrl || conf.restbaseUrl ) ? 'metadata' : 'parse',
page: pageName,
uselang: mw.config.get( 'wgUserLanguage' )
};
@ -124,14 +125,34 @@
return data;
} );
if ( conf.restbaseUrl ) {
if ( conf.fullRestbaseUrl || conf.restbaseUrl ) {
ve.track( 'trace.restbaseLoad.enter' );
restbaseXhr = $.ajax( {
url: conf.restbaseUrl + encodeURIComponent( pageName ) +
( oldid === undefined ? '' : '/' + oldid ),
type: 'GET',
dataType: 'text'
} );
if ( conf.fullRestbaseUrl && $( '#wpTextbox1' ).length ) {
fromEditedState = true;
restbaseXhr = $.post(
conf.fullRestbaseUrl + 'v1/transform/wikitext/to/html/' +
encodeURIComponent( pageName ) +
( oldid === undefined ? '' : '/' + oldid ),
{
title: pageName,
oldid: oldid,
wikitext: $( '#wpTextbox1' ).val(),
stash: 'true'
}
);
} else {
if ( conf.fullRestbaseUrl ) {
pageHtmlUrl = conf.fullRestbaseUrl + 'v1/page/html/';
} else {
pageHtmlUrl = conf.restbaseUrl;
}
restbaseXhr = $.ajax( {
url: pageHtmlUrl + encodeURIComponent( pageName ) +
( oldid === undefined ? '' : '/' + oldid ),
type: 'GET',
dataType: 'text'
} );
}
restbasePromise = restbaseXhr.then(
function ( data, status, jqxhr ) {
ve.track( 'trace.restbaseLoad.exit' );
@ -140,7 +161,7 @@
duration: ve.now() - start,
targetName: targetName
} );
return data;
return [ data, jqxhr.getResponseHeader( 'etag' ) ];
},
function ( response ) {
if ( response.status === 404 ) {
@ -155,9 +176,11 @@
);
dataPromise = $.when( apiPromise, restbasePromise )
.then( function ( apiData, restbaseHtml ) {
.then( function ( apiData, restbaseData ) {
if ( apiData.visualeditor ) {
apiData.visualeditor.content = restbaseHtml;
apiData.visualeditor.content = restbaseData[ 0 ];
apiData.visualeditor.etag = restbaseData[ 1 ];
apiData.visualeditor.fromEditedState = fromEditedState;
}
return apiData;
} )