Legoktm has uploaded a new change for review.
https://gerrit.wikimedia.org/r/107411
Change subject: [WIP] Revamp API
......................................................................
[WIP] Revamp API
The API is now a modular module, similar to
action=query. This means that each submodule
can properly provide help messages and hints for
extensions like ApiSandbox.
The old API was renamed to action=flow-old, and
should be removed after the new one is complete.
Change-Id: I09f53f8288ad86fee30dc0a4ce4b9af748aea792
---
M Flow.php
M includes/api/ApiFlow.php
A includes/api/ApiFlowBase.php
A includes/api/ApiFlowNewTopic.php
A includes/api/ApiFlowOld.php
M modules/base/ext.flow.base.js
6 files changed, 547 insertions(+), 92 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow
refs/changes/11/107411/1
diff --git a/Flow.php b/Flow.php
index 551538b..e357937 100755
--- a/Flow.php
+++ b/Flow.php
@@ -156,9 +156,13 @@
$wgAutoloadClasses['ApiQueryFlow'] = "$dir/includes/api/ApiQueryFlow.php";
$wgAutoloadClasses['ApiParsoidUtilsFlow'] =
"$dir/includes/api/ApiParsoidUtilsFlow.php";
$wgAutoloadClasses['ApiFlow'] = "$dir/includes/api/ApiFlow.php";
+$wgAutoloadClasses['ApiFlowBase'] = "$dir/includes/api/ApiFlowBase.php";
+$wgAutoloadClasses['ApiFlowNewTopic'] =
"$dir/includes/api/ApiFlowNewTopic.php";
+
+$wgAutoloadClasses['ApiFlowOld'] = "$dir/includes/api/ApiFlowOld.php";
$wgAPIListModules['flow'] = 'ApiQueryFlow';
$wgAPIModules['flow-parsoid-utils'] = 'ApiParsoidUtilsFlow';
-$wgAPIModules['flow'] = 'ApiFlow';
+$wgAPIModules['flow-old'] = 'ApiFlowOld';
// Housekeeping hooks
$wgHooks['LoadExtensionSchemaUpdates'][] = 'FlowHooks::getSchemaUpdates';
diff --git a/includes/api/ApiFlow.php b/includes/api/ApiFlow.php
index 53c23e5..b7cfee5 100644
--- a/includes/api/ApiFlow.php
+++ b/includes/api/ApiFlow.php
@@ -7,99 +7,60 @@
/** @var Flow\Container $container */
protected $container;
+ /** @var \Flow\WorkflowLoader $loader */
+ protected $loader;
+
+ /** @var ApiModuleManager $modulemanager */
+ protected $modulemanager;
+
+ private $params;
+
+ static $modules = array(
+ 'new-topic' => 'ApiFlowNewTopic',
+ 'edit-header' => 'ApiFlowEditHeader',
+ 'edit-post' => 'ApiFlowEditPost',
+ 'reply' => 'ApiFlowReply',
+ 'moderate-topic' => 'ApiFlowModerateTopic',
+ 'moderate-post' => 'ApiFlowModeratePost',
+ 'hide-post' => 'ApiFlowHidePost',
+ 'delete-post' => 'ApiFlowDeletePost',
+ 'suppress-post' => 'ApiFlowSuppressPost',
+ 'restore-post' => 'ApiFlowRestorePost',
+ 'edit-title' => 'ApiFlowEditTitle',
+ );
+
+ public static function getActionFromClass( $classname ) {
+ $flip = array_flip( self::$modules );
+ return $flip[$classname];
+ }
+
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
+ $this->modulemanager = new ApiModuleManager( $this );
+ $this->modulemanager->addModules( self::$modules, 'submodule' );
+ }
+
+ public function getModuleManager() {
+ return $this->modulemanager;
+ }
+
public function execute() {
- $this->container = Flow\Container::getContainer();
- $params = $this->extractRequestParams();
- $output = array();
-
- if ( ! $params['workflow'] && ! $params['page'] ) {
- $this->dieUsage( 'One of workflow or page parameters
must be provided', 'badparams' );
- return;
- }
-
- $id = UUID::create( $params['workflow'] );
- $page = false;
- if ( $params['page'] ) {
- $page = Title::newFromText( $params['page'] );
- }
- $this->loader = $this->container['factory.loader.workflow']
- ->createWorkflowLoader( $page, $id );
- $occupationController =
$this->container['occupation_controller'];
- $workflow = $this->loader->getWorkflow();
- $article = new Article( $workflow->getArticleTitle(), 0 );
-
- // @todo: this is a hack; see ParsoidUtils::convert
- global $wgFlowParsoidTitle;
- $wgFlowParsoidTitle = $workflow->getArticleTitle();
-
- $isNew = $workflow->isNew();
- // Is this making unnecesary db round trips?
- if ( !$isNew ) {
- $occupationController->ensureFlowRevision( $article );
- }
- $requestParams = json_decode( $params['params'], true );
-
- if ( ! $requestParams ) {
- $this->dieUsage( 'The params parameter must be a valid
JSON string', 'badparams' );
- return;
- }
-
- $request = new DerivativeRequest( $this->getRequest(),
$requestParams, true );
-
- $blocks = $this->loader->createBlocks();
- $action = $params['flowaction'];
- $user = $this->getUser();
-
- foreach( $blocks as $block ) {
- $block->init( $action, $user );
- }
-
- $blocksToCommit = $this->loader->handleSubmit( $action,
$blocks, $user, $request );
- if ( $blocksToCommit ) {
- $commitResults = $this->loader->commit(
$this->loader->getWorkflow(), $blocksToCommit );
-
- $savedBlocks = array( '_element' => 'block' );
-
- foreach( $blocksToCommit as $block ) {
- $savedBlocks[] = $block->getName();
+ $this->params = $this->extractRequestParams();
+ $modules = array();
+ $this->instantiateModules( $modules, 'submodule' );
+ // This is some of the logic from ApiQuery::initModules
+ $wasPosted = $this->getRequest()->wasPosted();
+ /** @var $module ApiFlowBase */
+ foreach ( $modules as $moduleName => $module ) {
+ if ( !$wasPosted && $module->mustBePosted() ) {
+ $this->dieUsageMsg( array( 'mustbeposted',
$moduleName ) );
}
-
- $output[$action] = array(
- 'result' => array(),
- );
-
- $doRender = ( $params['render'] != false );
-
- foreach( $commitResults as $key => $value ) {
- $output[$action]['result'][$key] =
$this->processCommitResult( $value, $doRender );
- }
- if ( $isNew && !$workflow->isNew() ) {
- // Workflow was just created, ensure its
underlying page is owned by flow
- $occupationController->ensureFlowRevision(
$article );
- }
- } else {
- $output[$action] = array(
- 'result' => 'error',
- 'errors' => array(),
- );
-
- foreach( $blocks as $block ) {
- if ( $block->hasErrors() ) {
- $errors = $block->getErrors();
- $nativeErrors = array();
-
- foreach( $errors as $key ) {
- $nativeErrors[$key]['message']
= $block->getErrorMessage( $key )->plain();
- $nativeErrors[$key]['extra'] =
$block->getErrorExtra( $key );
- }
-
-
$output[$action]['errors'][$block->getName()] = $nativeErrors;
- }
- }
+ $module->profileIn();
+ $module->execute();
+ wfRunHooks( 'APIFlowAfterExecute', $module );
+ $module->profileOut();
}
- $this->getResult()->addValue( null, $this->getModuleName(),
$output );
- return true;
}
protected function processCommitResult( $result, $render = true ) {
@@ -135,14 +96,39 @@
return $output;
}
+ /**
+ * Create instances of all modules requested by the client
+ * @fixme Copied from ApiQuery::instantiateModules, don't duplicate code
+ * @param array $modules to append instantiated modules to
+ * @param string $param Parameter name to read modules from
+ */
+ private function instantiateModules( &$modules, $param ) {
+ if ( isset( $this->params[$param] ) ) {
+ foreach ( $this->params[$param] as $moduleName ) {
+ $instance = $this->modulemanager->getModule(
$moduleName, $param );
+ if ( $instance === null ) {
+ ApiBase::dieDebug( __METHOD__, 'Error
instantiating module' );
+ }
+ // Ignore duplicates. TODO 2.0: die()?
+ if ( !array_key_exists( $moduleName, $modules )
) {
+ $modules[$moduleName] = $instance;
+ }
+ }
+ }
+ }
+
+ // @fixme need help generation code from ApiQuery
+
+
public function getDescription() {
return 'Allows actions to be taken on Flow Workflows';
}
public function getAllowedParams() {
return array(
- 'flowaction' => array(
+ 'submodule' => array(
ApiBase::PARAM_REQUIRED => true,
+ ApiBase::PARAM_TYPE =>
$this->modulemanager->getNames( 'submodules' ),
),
'workflow' => array(
ApiBase::PARAM_DFLT => null,
diff --git a/includes/api/ApiFlowBase.php b/includes/api/ApiFlowBase.php
new file mode 100644
index 0000000..b4fcf98
--- /dev/null
+++ b/includes/api/ApiFlowBase.php
@@ -0,0 +1,222 @@
+<?php
+
+use Flow\WorkflowLoader;
+use Flow\Model\UUID;
+use Flow\Container;
+use Flow\TalkpageManager;
+use Flow\Block\AbstractBlock;
+
+
+abstract class ApiFlowBase extends ApiBase {
+
+ /**
+ * Use $this->getContainer() to access this
+ * @var Container $container
+ */
+ private $container;
+
+ /** @var Title|bool $page */
+ protected $page;
+
+ /** @var WorkflowLoader $loader */
+ protected $loader;
+
+ /** @var TalkpageManager $controller */
+ protected $controller;
+
+ public function __construct( $api, $modName, $prefix = '' ) {
+ parent::__construct( $api->getMain(), $modName, $prefix );
+ }
+
+ /*
+ * Return the name of the flow action
+ * @return string
+ */
+ protected function getAction() {
+ return ApiFlow::getActionFromClass( __CLASS__ );
+ }
+
+
+ /**
+ *
+ * @return bool|UUID
+ */
+ protected function getId() {
+ $params = $this->extractRequestParams();
+ if ( isset( $params['workflow'] ) ) {
+ return UUID::create( $params['workflow'] );
+ }
+
+ return false;
+ }
+
+ /**
+ * @return Title|bool false if no title provided
+ */
+ protected function getPage() {
+ if ( $this->page === null ) {
+ $params = $this->extractRequestParams();
+ if ( isset( $params['page'] ) ) {
+ $this->page = Title::newFromText(
$params['page'] );
+ if ( !$this->page ) {
+ $this->dieUsage( 'Invalid page
provided', 'invalid-page' );
+ }
+ } else {
+ $this->page = false;
+ }
+ }
+
+ return $this->page;
+ }
+
+ /**
+ * @return Container
+ */
+ protected function getContainer() {
+ if ( is_null( $this->container ) ) {
+ $this->container = Container::getContainer();
+ }
+
+ return $this->container;
+ }
+
+ /**
+ * @return WorkflowLoader
+ */
+ protected function getLoader() {
+ if ( $this->loader === null ) {
+ $container = $this->getContainer();
+ $this->loader = $container['factory.loader.workflow']
+ ->createWorkflowLoader( $this->getPage(),
$this->getId() );
+ }
+
+ return $this->loader;
+ }
+
+ /**
+ * @return TalkpageManager
+ */
+ protected function getOccupationController() {
+ if ( $this->controller === null ) {
+ $container = $this->getContainer();
+ $this->controller = $container['occupation_controller'];
+ }
+
+ return $this->controller;
+ }
+
+ abstract protected function getUsedParameters();
+
+ /**
+ * @return DerivativeRequest
+ */
+ protected function getModifiedRequest() {
+ $params = array();
+ $extracted = $this->extractRequestParams();
+ foreach ( $this->getUsedParameters() as $name ) {
+ // @todo check if we can just use $extracted directly
+ $params[$name] = $extracted[$name];
+ }
+
+ return new DerivativeRequest(
+ $this->getRequest(),
+ $params,
+ $this->getRequest()->wasPosted() // Note that we also
enforce this at the API level.
+ );
+ }
+
+ public function execute() {
+ $loader = $this->getLoader();
+ $blocks = $loader->createBlocks();
+ /** @var \Flow\Model\Workflow $workflow */
+ $workflow = $loader->getWorkflow();
+ $action = $this->getAction();
+ $controller = $this->getOccupationController();
+ $user = $this->getUser();
+ $params = $this->extractRequestParams();
+
+ $isNew = $workflow->isNew();
+ $article = new Article( $workflow->getArticleTitle(), 0 );
+
+ // @fixme: this is a hack; see ParsoidUtils::convert
+ global $wgFlowParsoidTitle;
+ $wgFlowParsoidTitle = $workflow->getArticleTitle();
+
+ $isNew = $workflow->isNew();
+ // Is this making unnecesary db round trips?
+ if ( !$isNew ) {
+ $controller->ensureFlowRevision( $article );
+ }
+
+ /** @var AbstractBlock $block */
+ foreach ( $blocks as $block ) {
+ $block->init( $action, $user );
+ }
+
+ $blocksToCommit = $loader->handleSubmit( $action, $blocks,
$user, $this->getModifiedRequest() );
+ if ( $blocksToCommit ) {
+ $commitResults = $loader->commit( $workflow,
$blocksToCommit );
+ $savedBlocks = array( '_element' => 'block' );
+
+ foreach( $blocksToCommit as $block ) {
+ $savedBlocks[] = $block->getName();
+ }
+
+ $output[$action] = array(
+ 'result' => array(),
+ );
+
+ $doRender = ( $params['render'] != false );
+
+ foreach( $commitResults as $key => $value ) {
+ $output[$action]['result'][$key] =
$this->processCommitResult( $value, $doRender );
+ }
+ if ( $isNew && !$workflow->isNew() ) {
+ // Workflow was just created, ensure its
underlying page is owned by flow
+ $controller->ensureFlowRevision( $article );
+ }
+ } else {
+ $output[$action] = array(
+ 'result' => 'error',
+ 'errors' => array(),
+ );
+
+ foreach( $blocks as $block ) {
+ if ( $block->hasErrors() ) {
+ $errors = $block->getErrors();
+ $nativeErrors = array();
+
+ foreach( $errors as $key ) {
+ $nativeErrors[$key]['message']
= $block->getErrorMessage( $key )->plain();
+ $nativeErrors[$key]['extra'] =
$block->getErrorExtra( $key );
+ }
+
+
$output[$action]['errors'][$block->getName()] = $nativeErrors;
+ }
+ }
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(),
$output );
+ }
+
+ protected function processCommitResult( $result, $render = true ) {
+ $container = $this->getContainer();
+ $templating = $container['templating'];
+ $output = array();
+ foreach( $result as $key => $value ) {
+ if ( $value instanceof UUID ) {
+ $output[$key] = $value->getHex();
+ } elseif ( $key === 'render-function' ) {
+ if ( $render ) {
+ $function = $value;
+ $output['rendered'] = $function(
$templating );
+ }
+ } else {
+ $output[$key] = $value;
+ }
+ }
+
+ return $output;
+ }
+
+}
\ No newline at end of file
diff --git a/includes/api/ApiFlowNewTopic.php b/includes/api/ApiFlowNewTopic.php
new file mode 100644
index 0000000..31c26d5
--- /dev/null
+++ b/includes/api/ApiFlowNewTopic.php
@@ -0,0 +1,47 @@
+<?php
+
+class ApiFlowNewTopic extends ApiFlowBase {
+
+ public function __construct( $api ) {
+ parent::__construct( $api, 'new-topic', 'nt' );
+ }
+
+ /**
+ * Taken from ext.flow.base.js
+ * @return array
+ */
+ protected function getUsedParameters() {
+ // @todo can we just use getAllowedParams?
+ return array( 'topic', 'content' );
+ }
+
+ public function getAllowedParams() {
+ // @todo do we need to array_merge with parent?
+ return array(
+ 'topic' => array(
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'content' => array(
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'topic' => 'Text for new topic header',
+ 'content' => 'Content for new topic',
+ );
+ }
+
+ public function getDescription() {
+ return 'Creates a new Flow topic on the given workflow';
+ }
+
+ public function getExamples() {
+ return array(
+
'api.php?action=flow&submodule=new-topic&nttopic=Hi&ntcontent=Nice%20to&20meet%20you&workflow=',
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/includes/api/ApiFlowOld.php b/includes/api/ApiFlowOld.php
new file mode 100644
index 0000000..4119bf0
--- /dev/null
+++ b/includes/api/ApiFlowOld.php
@@ -0,0 +1,196 @@
+<?php
+
+use Flow\Model\UUID;
+
+class ApiFlowOld extends ApiBase {
+
+ /** @var Flow\Container $container */
+ protected $container;
+
+ /** @var \Flow\WorkflowLoader $loader */
+ protected $loader;
+
+ public function execute() {
+ $this->container = Flow\Container::getContainer();
+ $params = $this->extractRequestParams();
+ $output = array();
+
+ if ( ! $params['workflow'] && ! $params['page'] ) {
+ $this->dieUsage( 'One of workflow or page parameters
must be provided', 'badparams' );
+ return;
+ }
+
+ $id = UUID::create( $params['workflow'] );
+ $page = false;
+ if ( $params['page'] ) {
+ $page = Title::newFromText( $params['page'] );
+ }
+ $this->loader = $this->container['factory.loader.workflow']
+ ->createWorkflowLoader( $page, $id );
+ $occupationController =
$this->container['occupation_controller'];
+ $workflow = $this->loader->getWorkflow();
+ $article = new Article( $workflow->getArticleTitle(), 0 );
+
+ // @todo: this is a hack; see ParsoidUtils::convert
+ global $wgFlowParsoidTitle;
+ $wgFlowParsoidTitle = $workflow->getArticleTitle();
+
+ $isNew = $workflow->isNew();
+ // Is this making unnecesary db round trips?
+ if ( !$isNew ) {
+ $occupationController->ensureFlowRevision( $article );
+ }
+ $requestParams = json_decode( $params['params'], true );
+
+ if ( ! $requestParams ) {
+ $this->dieUsage( 'The params parameter must be a valid
JSON string', 'badparams' );
+ return;
+ }
+
+ $request = new DerivativeRequest( $this->getRequest(),
$requestParams, true );
+
+ $blocks = $this->loader->createBlocks();
+ $action = $params['flowaction'];
+ $user = $this->getUser();
+
+ foreach( $blocks as $block ) {
+ $block->init( $action, $user );
+ }
+
+ $blocksToCommit = $this->loader->handleSubmit( $action,
$blocks, $user, $request );
+ if ( $blocksToCommit ) {
+ $commitResults = $this->loader->commit(
$this->loader->getWorkflow(), $blocksToCommit );
+
+ $savedBlocks = array( '_element' => 'block' );
+
+ foreach( $blocksToCommit as $block ) {
+ $savedBlocks[] = $block->getName();
+ }
+
+ $output[$action] = array(
+ 'result' => array(),
+ );
+
+ $doRender = ( $params['render'] != false );
+
+ foreach( $commitResults as $key => $value ) {
+ $output[$action]['result'][$key] =
$this->processCommitResult( $value, $doRender );
+ }
+ if ( $isNew && !$workflow->isNew() ) {
+ // Workflow was just created, ensure its
underlying page is owned by flow
+ $occupationController->ensureFlowRevision(
$article );
+ }
+ } else {
+ $output[$action] = array(
+ 'result' => 'error',
+ 'errors' => array(),
+ );
+
+ foreach( $blocks as $block ) {
+ if ( $block->hasErrors() ) {
+ $errors = $block->getErrors();
+ $nativeErrors = array();
+
+ foreach( $errors as $key ) {
+ $nativeErrors[$key]['message']
= $block->getErrorMessage( $key )->plain();
+ $nativeErrors[$key]['extra'] =
$block->getErrorExtra( $key );
+ }
+
+
$output[$action]['errors'][$block->getName()] = $nativeErrors;
+ }
+ }
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(),
$output );
+ return true;
+ }
+
+ protected function processCommitResult( $result, $render = true ) {
+ $templating = $this->container['templating'];
+ $output = array();
+ foreach( $result as $key => $value ) {
+ if ( $value instanceof UUID ) {
+ $output[$key] = $value->getHex();
+ } elseif ( $key === 'render-function' ) {
+ if ( $render ) {
+ $function = $value;
+ $output['rendered'] = $function(
$templating );
+ }
+ } else {
+ $output[$key] = $value;
+ }
+ }
+
+ return $output;
+ }
+
+ protected function doRerender( $blocks ) {
+ $templating = $this->container['templating'];
+
+ $output = array();
+
+ $output['count'] = count( $blocks );
+
+ foreach( $blocks as $block ) {
+ $output[$block->getName()] = $block->render(
$templating, array() );
+ }
+
+ return $output;
+ }
+
+ public function getDescription() {
+ return 'Allows actions to be taken on Flow Workflows';
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'flowaction' => array(
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'workflow' => array(
+ ApiBase::PARAM_DFLT => null,
+ ),
+ 'page' => null,
+ 'params' => array(
+ ApiBase::PARAM_DFLT => '{}',
+ ),
+ 'token' => array(
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'render' => array(
+ ApiBase::PARAM_DFLT => false,
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'action' => 'The action to take',
+ 'workflow' => 'The Workflow to take an action on',
+ 'params' => 'The parameters to pass',
+ 'token' => 'A token retrieved from
api.php?action=tokens&type=flow',
+ 'render' => 'Set this to something to include a
block-specific rendering in the output',
+ );
+ }
+
+ public function getExamples() {
+ // @todo: fill out an example
+ return array(
+ ''
+ => ''
+ );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function needsToken() {
+ return true;
+ }
+
+ public function getTokenSalt() {
+ global $wgFlowTokenSalt;
+ return $wgFlowTokenSalt;
+ }
+}
diff --git a/modules/base/ext.flow.base.js b/modules/base/ext.flow.base.js
index 192b1a9..e128f50 100644
--- a/modules/base/ext.flow.base.js
+++ b/modules/base/ext.flow.base.js
@@ -25,7 +25,7 @@
)
.done( function ( data ) {
var request = {
- 'action' : 'flow',
+ 'action' : 'flow-old',
'flowaction' : action,
'params' : $.toJSON( options ),
'token' : data.tokens.flowtoken
--
To view, visit https://gerrit.wikimedia.org/r/107411
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I09f53f8288ad86fee30dc0a4ce4b9af748aea792
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Legoktm <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits