mediawiki-extensions-Visual.../ApiVisualEditor.php

459 lines
13 KiB
PHP
Raw Normal View History

<?php
/**
* Parsoid API wrapper.
*
* @file
* @ingroup Extensions
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
class ApiVisualEditor extends ApiBase {
protected function getHTML( $title, $parserParams ) {
global $wgVisualEditorParsoidURL, $wgVisualEditorParsoidPrefix,
$wgVisualEditorParsoidTimeout;
$restoring = false;
if ( $title->exists() ) {
$latestRevision = Revision::newFromTitle( $title );
if ( $latestRevision === null ) {
return false;
}
$revision = null;
if ( !isset( $parserParams['oldid'] ) || $parserParams['oldid'] === 0 ) {
$parserParams['oldid'] = $latestRevision->getId();
$revision = $latestRevision;
} else {
$revision = Revision::newFromId( $parserParams['oldid'] );
if ( $revision === null ) {
return false;
}
}
$restoring = $revision && !$revision->isCurrent();
$oldid = $parserParams['oldid'];
$req = MWHttpRequest::factory( wfAppendQuery(
$wgVisualEditorParsoidURL . '/' . $wgVisualEditorParsoidPrefix .
'/' . urlencode( $title->getPrefixedDBkey() ),
$parserParams
),
array(
'method' => 'GET',
'timeout' => $wgVisualEditorParsoidTimeout
)
);
$status = $req->execute();
if ( $status->isOK() ) {
$content = $req->getContent();
} elseif ( $status->isGood() ) {
$this->dieUsage( $req->getContent(), 'parsoidserver-http-'.$req->getStatus() );
} elseif ( $errors = $status->getErrorsByType( 'error' ) ) {
$error = $errors[0];
$code = $error['message'];
if ( count( $error['params'] ) ) {
$message = $error['params'][0];
} else {
$message = 'MWHttpRequest error';
}
$this->dieUsage( $message, 'parsoidserver-'.$code );
}
if ( $content === false ) {
return false;
}
$timestamp = $latestRevision->getTimestamp();
} else {
$content = '';
$timestamp = wfTimestampNow();
$oldid = 0;
}
return array(
'result' => array(
'content' => $content,
'basetimestamp' => $timestamp,
'starttimestamp' => wfTimestampNow(),
'oldid' => $oldid,
),
'restoring' => $restoring,
);
}
protected function postHTML( $title, $html, $parserParams ) {
global $wgVisualEditorParsoidURL, $wgVisualEditorParsoidPrefix,
$wgVisualEditorParsoidTimeout;
if ( $parserParams['oldid'] === 0 ) {
$parserParams['oldid'] = '';
}
return Http::post(
$wgVisualEditorParsoidURL . '/' . $wgVisualEditorParsoidPrefix .
'/' . urlencode( $title->getPrefixedDBkey() ),
array(
'postData' => array(
'content' => $html,
'oldid' => $parserParams['oldid']
),
'timeout' => $wgVisualEditorParsoidTimeout
)
);
}
protected function saveWikitext( $title, $wikitext, $params ) {
$apiParams = array(
'action' => 'edit',
'title' => $title->getPrefixedDBkey(),
'text' => $wikitext,
'summary' => $params['summary'],
'basetimestamp' => $params['basetimestamp'],
'starttimestamp' => $params['starttimestamp'],
'token' => $params['token'],
);
if ( $params['minor'] ) {
$apiParams['minor'] = true;
}
// FIXME add some way that the user's preferences can be respected
$apiParams['watchlist'] = $params['watch'] ? 'watch' : 'unwatch';
if ( $params['captchaid'] ) {
$apiParams['captchaid'] = $params['captchaid'];
}
if ( $params['captchaword'] ) {
$apiParams['captchaword'] = $params['captchaword'];
}
$api = new ApiMain(
new DerivativeRequest(
$this->getRequest(),
$apiParams,
true // was posted
),
true // enable write
);
$api->execute();
return $api->getResultData();
}
protected function parseWikitext( $title ) {
$apiParams = array(
'action' => 'parse',
'page' => $title->getPrefixedDBkey()
);
$api = new ApiMain(
new DerivativeRequest(
$this->getRequest(),
$apiParams,
false // was posted?
),
true // enable write?
);
$api->execute();
$result = $api->getResultData();
$content = isset( $result['parse']['text']['*'] ) ? $result['parse']['text']['*'] : false;
$revision = Revision::newFromId( $result['parse']['revid'] );
$timestamp = $revision ? $revision->getTimestamp() : wfTimestampNow();
if ( $content === false || ( strlen( $content ) && $revision === null ) ) {
return false;
}
return array(
'content' => $content,
'basetimestamp' => $timestamp,
'starttimestamp' => wfTimestampNow()
);
}
protected function parseWikitextFragment( $wikitext, $title = null ) {
$apiParams = array(
'action' => 'parse',
'title' => $title,
'prop' => 'text',
'disablepp' => true,
'text' => $wikitext
);
$api = new ApiMain(
new DerivativeRequest(
$this->getRequest(),
$apiParams,
false // was posted?
),
true // enable write?
);
$api->execute();
$result = $api->getResultData();
return isset( $result['parse']['text']['*'] ) ? $result['parse']['text']['*'] : false;
}
protected function diffWikitext( $title, $wikitext ) {
$apiParams = array(
'action' => 'query',
'prop' => 'revisions',
'titles' => $title->getPrefixedDBkey(),
'rvdifftotext' => $wikitext
);
$api = new ApiMain(
new DerivativeRequest(
$this->getRequest(),
$apiParams,
false // was posted?
),
false // enable write?
);
$api->execute();
$result = $api->getResultData();
(bug 42654) Implement Show changes in Save dialog. Turned saveDialog-body into slide-based swapper. Moved footer into saveDiaog-body so that the license text doesn't stay under the diff-slide (and move body bottom padding to foot top) Wrapped buttons and title in a saveDialog-header and converted closeButton from absolutely positioned to a floated layout. This way the title doesn't need to be repositioned but will scooch over if the prevButton gets shown/hidden. Update API "diff" action to include table wrapper and table header. Without it the mediawiki CSS for diff doesn't work properly (needs colgroups for proper width of the "-" and "+" column etc) Renamed -saving class to -disabled for consistency. Set prop.disabled to really lock/unlock buttons, not just visual (otherwise the click handlers are still triggered on click, can potentially cause actions to be triggered when not expected) Using a ve message for "Show your changes" title instead of re-using core tooltip-savepage in a different context. Diff slide triggers "auto width" on dialog (inline undo of width: 29em), keeping min-width, to allow it to expand as wide as needed. Functions that I copied as base for onShowChanges and onShowChangeError had some incorrect argument descriptions. Fixed in both. Note: * Pass function to .off(), so that only that one is unbound instead of any "resize" handler on the page (by other extensions or gadgets or core) * NB: ve.bind ($.proxy) preserves internal guid, so that $.Event can find the bound function by the original reference. * keydown has an anonymous function, should either moved to prototype or namespaced, did latter for now, save enough and better than destructive .off('keydown') Change-Id: I9d05ef6e3e2461bdcf363232f7b0fbad5e24f506
2012-12-07 16:23:23 +00:00
if ( !isset( $result['query']['pages'][$title->getArticleID()]['revisions'][0]['diff']['*'] ) ) {
return array( 'result' => 'fail' );
(bug 42654) Implement Show changes in Save dialog. Turned saveDialog-body into slide-based swapper. Moved footer into saveDiaog-body so that the license text doesn't stay under the diff-slide (and move body bottom padding to foot top) Wrapped buttons and title in a saveDialog-header and converted closeButton from absolutely positioned to a floated layout. This way the title doesn't need to be repositioned but will scooch over if the prevButton gets shown/hidden. Update API "diff" action to include table wrapper and table header. Without it the mediawiki CSS for diff doesn't work properly (needs colgroups for proper width of the "-" and "+" column etc) Renamed -saving class to -disabled for consistency. Set prop.disabled to really lock/unlock buttons, not just visual (otherwise the click handlers are still triggered on click, can potentially cause actions to be triggered when not expected) Using a ve message for "Show your changes" title instead of re-using core tooltip-savepage in a different context. Diff slide triggers "auto width" on dialog (inline undo of width: 29em), keeping min-width, to allow it to expand as wide as needed. Functions that I copied as base for onShowChanges and onShowChangeError had some incorrect argument descriptions. Fixed in both. Note: * Pass function to .off(), so that only that one is unbound instead of any "resize" handler on the page (by other extensions or gadgets or core) * NB: ve.bind ($.proxy) preserves internal guid, so that $.Event can find the bound function by the original reference. * keydown has an anonymous function, should either moved to prototype or namespaced, did latter for now, save enough and better than destructive .off('keydown') Change-Id: I9d05ef6e3e2461bdcf363232f7b0fbad5e24f506
2012-12-07 16:23:23 +00:00
}
$diffRows = $result['query']['pages'][$title->getArticleID()]['revisions'][0]['diff']['*'];
if ( $diffRows !== '' ) {
$context = new DerivativeContext( $this->getContext() );
$context->setTitle( $title );
$engine = new DifferenceEngine( $context );
return array(
'result' => 'success',
'diff' => $engine->addHeader(
$diffRows,
wfMessage( 'currentrev' )->parse(),
wfMessage( 'yourtext' )->parse()
)
);
} else {
return array( 'result' => 'nochanges' );
}
}
public function execute() {
global $wgVisualEditorNamespaces, $wgVisualEditorUseChangeTagging,
$wgVisualEditorEditNotices;
$user = $this->getUser();
$params = $this->extractRequestParams();
$page = Title::newFromText( $params['page'] );
if ( !$page ) {
$this->dieUsageMsg( 'invalidtitle', $params['page'] );
}
if ( !in_array( $page->getNamespace(), $wgVisualEditorNamespaces ) ) {
$this->dieUsage( "VisualEditor is not enabled in namespace " .
$page->getNamespace(), 'novenamespace' );
}
$parserParams = array();
if ( isset( $params['oldid'] ) ) {
$parserParams['oldid'] = $params['oldid'];
}
switch ( $params['paction'] ) {
case 'parse':
$parsed = $this->getHTML( $page, $parserParams );
// Dirty hack to provide the correct context for edit notices
global $wgTitle; // FIXME NOOOOOOOOES
$wgTitle = $page;
$notices = $page->getEditNotices();
if ( $user->isAnon() ) {
$wgVisualEditorEditNotices[] = 'anoneditwarning';
}
if ( $parsed && $parsed['restoring'] ) {
$wgVisualEditorEditNotices[] = 'editingold';
}
// Page protected from editing
if ( $page->getNamespace() != NS_MEDIAWIKI && $page->isProtected( 'edit' ) ) {
# Is the title semi-protected?
if ( $page->isSemiProtected() ) {
$wgVisualEditorEditNotices[] = 'semiprotectedpagewarning';
} else {
# Then it must be protected based on static groups (regular)
$wgVisualEditorEditNotices[] = 'protectedpagewarning';
}
}
// Page protected from creation
if ( !$page->exists() && $page->getRestrictions( 'create' ) ) {
$wgVisualEditorEditNotices[] = 'titleprotectedwarning';
}
if ( count( $wgVisualEditorEditNotices ) ) {
foreach ( $wgVisualEditorEditNotices as $key ) {
$notices[] = wfMessage( $key )->parseAsBlock();
}
}
if ( $parsed === false ) {
$this->dieUsage( 'Error contacting the Parsoid server', 'parsoidserver' );
} else {
$result = array_merge(
array(
'result' => 'success',
'notices' => $notices
),
$parsed['result']
);
}
break;
case 'parsefragment':
$content = $this->parseWikitextFragment( $params['wikitext'], $page->getText() );
if ( $content === false ) {
$this->dieUsage( 'Error contacting the Parsoid server', 'parsoidserver' );
} else {
$result = array(
'result' => 'success',
'content' => $content
);
}
break;
case 'serialize':
if ( $params['html'] === null ) {
$this->dieUsageMsg( 'missingparam', 'html' );
}
$content = $this->postHTML( $page, $params['html'], $parserParams );
if ( $content === false ) {
$this->dieUsage( 'Error contacting the Parsoid server', 'parsoidserver' );
} else {
$result = array( 'result' => 'success', 'content' => $content );
}
break;
case 'save':
$wikitext = $this->postHTML( $page, $params['html'], $parserParams );
if ( $wikitext === false ) {
$this->dieUsage( 'Error contacting the Parsoid server', 'parsoidserver' );
}
$saveresult = $this->saveWikitext( $page, $wikitext, $params );
$editStatus = $saveresult['edit']['result'];
// Error
if ( !isset( $saveresult['edit']['result'] ) || $editStatus !== 'Success' ) {
$result = array(
'result' => 'error',
'edit' => $saveresult['edit']
);
// Success
} else {
if ( isset( $saveresult['edit']['newrevid'] ) && $wgVisualEditorUseChangeTagging ) {
ChangeTags::addTags( 'visualeditor', null,
intval( $saveresult['edit']['newrevid'] ),
null
);
if ( $params['needcheck'] ) {
ChangeTags::addTags( 'visualeditor-needcheck', null,
intval( $saveresult['edit']['newrevid'] ),
null
);
}
}
// Return result of parseWikitext instead of saveWikitext so that the
// frontend can update the page rendering without a refresh.
$result = $this->parseWikitext( $page );
if ( $result === false ) {
$this->dieUsage( 'Error contacting the Parsoid server', 'parsoidserver' );
}
if ( isset( $saveresult['edit']['newrevid'] ) ) {
$result['newrevid'] = intval( $saveresult['edit']['newrevid'] );
}
$result['result'] = 'success';
}
break;
case 'diff':
$wikitext = $this->postHTML( $page, $params['html'], $parserParams );
if ( $wikitext === false ) {
$this->dieUsage( 'Error contacting the Parsoid server', 'parsoidserver' );
}
$diff = $this->diffWikitext( $page, $wikitext );
if ( $diff['result'] === 'fail' ) {
$this->dieUsage( 'Diff failed', 'difffailed' );
}
$result = $diff;
break;
}
$this->getResult()->addValue( null, $this->getModuleName(), $result );
}
public function getAllowedParams() {
return array(
'page' => array(
ApiBase::PARAM_REQUIRED => true,
),
'paction' => array(
ApiBase::PARAM_REQUIRED => true,
ApiBase::PARAM_TYPE => array( 'parse', 'parsefragment', 'serialize', 'save', 'diff' ),
),
'token' => array(
ApiBase::PARAM_REQUIRED => true,
),
'wikitext' => null,
'basetimestamp' => null,
'starttimestamp' => null,
'needcheck' => array(
ApiBase::PARAM_TYPE => 'boolean'
),
'oldid' => null,
'minor' => null,
'watch' => null,
'html' => null,
'summary' => null,
'captchaid' => null,
'captchaword' => null,
);
}
public function needsToken() {
return true;
}
public function getTokenSalt() {
return '';
}
public function mustBePosted() {
return true;
}
public function isWriteMode() {
return true;
}
public function getVersion() {
return __CLASS__ . ': $Id$';
}
public function getParamDescription() {
return array(
'page' => 'The page to perform actions on.',
'paction' => 'Action to perform',
'oldid' => 'The revision number to use. For paction=save, defauls to latest revision.' +
' Required for other actions. Use 0 for new page.',
'minor' => 'Flag for minor edit.',
'html' => 'HTML to send to parsoid in exchange for wikitext',
'summary' => 'Edit summary',
'basetimestamp' => 'When saving, set this to the timestamp of the revision that was'
.' edited. Used to detect edit conflicts.',
'starttimestamp' => 'When saving, set this to the timestamp of when the page was loaded.'
.' Used to detect edit conflicts.',
'token' => 'Edit token',
'needcheck' => 'When saving, set this parameter if the revision might have roundtrip'
. 'problems. This will result in the edit being tagged.',
'captchaid' => 'Captcha id (when saving with a captcha response).',
'captchaword' => 'Answer to the captcha (when saving with a captcha response).',
);
}
public function getDescription() {
return 'Returns HTML5 for a page from the parsoid service.';
}
}