From 85b745666fb1cfdd0e3d3f317a7b8ddc8cc77ede Mon Sep 17 00:00:00 2001 From: Alex Monk Date: Thu, 8 Oct 2015 23:16:56 +0100 Subject: [PATCH] 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 --- ApiVisualEditor.php | 24 ++++++---- ApiVisualEditorEdit.php | 4 +- VisualEditor.hooks.php | 1 + extension.json | 1 + modules/ve-mw/i18n/en.json | 2 + modules/ve-mw/i18n/qqq.json | 2 + .../ve.init.mw.DesktopArticleTarget.init.js | 5 +- .../ve.init.mw.DesktopArticleTarget.js | 10 ++-- modules/ve-mw/init/ve.init.mw.Target.js | 23 +++++---- modules/ve-mw/init/ve.init.mw.TargetLoader.js | 47 ++++++++++++++----- 10 files changed, 85 insertions(+), 34 deletions(-) diff --git a/ApiVisualEditor.php b/ApiVisualEditor.php index c7f43715fd..b0e33885bc 100644 --- a/ApiVisualEditor.php +++ b/ApiVisualEditor.php @@ -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, ); diff --git a/ApiVisualEditorEdit.php b/ApiVisualEditorEdit.php index 471112b3fc..5509dc472f 100644 --- a/ApiVisualEditorEdit.php +++ b/ApiVisualEditorEdit.php @@ -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.', diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php index 101c62b550..a0b951707c 100644 --- a/VisualEditor.hooks.php +++ b/VisualEditor.hooks.php @@ -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; diff --git a/extension.json b/extension.json index 90d3dfcebb..84d2e108b8 100644 --- a/extension.json +++ b/extension.json @@ -30,6 +30,7 @@ "visualeditor-enable-experimental": "ext.visualEditor.experimental" }, "VisualEditorRestbaseURL": false, + "VisualEditorFullRestbaseURL": false, "VisualEditorSerializationCacheTimeout": 3600, "VisualEditorUseChangeTagging": true, "VisualEditorSupportedSkins": [ diff --git a/modules/ve-mw/i18n/en.json b/modules/ve-mw/i18n/en.json index afe9545b71..3bee2cb9f6 100644 --- a/modules/ve-mw/i18n/en.json +++ b/modules/ve-mw/i18n/en.json @@ -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.", diff --git a/modules/ve-mw/i18n/qqq.json b/modules/ve-mw/i18n/qqq.json index 5fc0e173b4..d8c64bf0f1 100644 --- a/modules/ve-mw/i18n/qqq.json +++ b/modules/ve-mw/i18n/qqq.json @@ -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}}", diff --git a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js index 4b39277fd6..70d21ad13b 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js +++ b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js @@ -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 diff --git a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js index 6c1404dad0..96a1b8fd76 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js @@ -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). diff --git a/modules/ve-mw/init/ve.init.mw.Target.js b/modules/ve-mw/init/ve.init.mw.Target.js index 618c2f534f..736cf8dbc2 100644 --- a/modules/ve-mw/init/ve.init.mw.Target.js +++ b/modules/ve-mw/init/ve.init.mw.Target.js @@ -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 ); diff --git a/modules/ve-mw/init/ve.init.mw.TargetLoader.js b/modules/ve-mw/init/ve.init.mw.TargetLoader.js index 82763b2710..efe452e125 100644 --- a/modules/ve-mw/init/ve.init.mw.TargetLoader.js +++ b/modules/ve-mw/init/ve.init.mw.TargetLoader.js @@ -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; } )