new: [localTool:batchActions] Added framework to execute batch actions on list of connections

pull/68/head
mokaddem 2021-07-07 15:05:32 +02:00
parent ce9fc762bc
commit 41e9666224
6 changed files with 240 additions and 13 deletions

View File

@ -44,8 +44,56 @@ class LocalToolsController extends AppController
if (!empty($responsePayload)) { if (!empty($responsePayload)) {
return $responsePayload; return $responsePayload;
} }
$connector = $this->LocalTools->getConnectors($connectorName)[$connectorName];
$this->set('metaGroup', 'Administration'); $this->set('metaGroup', 'Administration');
$this->set('connector', $connectorName); $this->set('connectorName', $connectorName);
$this->set('connector', $connector);
}
public function batchAction($actionName)
{
$params = $this->ParamHandler->harvestParams(['connection_ids']);
$params['connection_ids'] = explode(',', $params['connection_ids']);
$connections = $this->LocalTools->query()->where(['id IN' => $params['connection_ids']])->all();
if (empty($connections)) {
throw new NotFoundException(__('Invalid connector.'));
}
$connection = $connections->first();
if ($this->request->is(['post', 'put'])) {
$actionParams = $this->LocalTools->getActionFilterOptions($connection->connector, $actionName);
$params = array_merge($params, $this->ParamHandler->harvestParams($actionParams));
$results = [];
$successes = 0;
$this->LocalTools->loadConnector($connection->connector);
foreach ($connections as $connection) {
$actionDetails = $this->LocalTools->getActionDetails($actionName);
$params['connection'] = $connection;
$tmpResult = $this->LocalTools->action($this->ACL->getUser()['id'], $connection->connector, $actionName, $params, $this->request);
$tmpResult['connection'] = $connection;
$results[$connection->id] = $tmpResult;
$successes += $tmpResult['success'] ? 1 : 0;
}
$success = $successes > 0;
$message = __('{0} / {1} operations were successful', $successes, count($results));
$this->CRUD->setResponseForController('batchAction', $success, $message, $results, $results);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
if (!empty($success)) {
$this->Flash->success($message);
$this->redirect(['controller' => 'localTools', 'action' => 'connectorIndex', $actionName]);
} else {
$this->Flash->error($message);
$this->redirect(['controller' => 'localTools', 'action' => 'connectorIndex', $actionName]);
}
} else {
$params['connection'] = $connection;
$results = $this->LocalTools->action($this->ACL->getUser()['id'], $connection->connector, $actionName, $params, $this->request);
$this->set('data', $results);
$this->set('metaGroup', 'Administration');
$this->render('/Common/getForm');
}
} }
public function action($connectionId, $actionName) public function action($connectionId, $actionName)

View File

@ -25,6 +25,13 @@ class CommonConnectorTools
$this->exposedFunctions[] = $functionName; $this->exposedFunctions[] = $functionName;
} }
public function getBatchActionFunctions(): array
{
return array_filter($this->exposedFunctions, function($function) {
return $function['type'] == 'batchAction';
});
}
public function runAction($action, $params) { public function runAction($action, $params) {
if (!in_array($action, $exposedFunctions)) { if (!in_array($action, $exposedFunctions)) {
throw new MethodNotAllowedException(__('Invalid connector function called.')); throw new MethodNotAllowedException(__('Invalid connector function called.'));

View File

@ -94,6 +94,20 @@ class MispConnector extends CommonConnectorTools
'sort', 'sort',
'direction' 'direction'
] ]
],
'batchAPIAction' => [
'type' => 'batchAction',
'scope' => 'childAction',
'params' => [
'method',
'url',
'body',
],
'ui' => [
'text' => 'Batch API',
'icon' => 'terminal',
'variant' => 'primary',
]
] ]
]; ];
public $version = '0.1'; public $version = '0.1';
@ -230,7 +244,10 @@ class MispConnector extends CommonConnectorTools
throw new NotFoundException(__('No connection object received.')); throw new NotFoundException(__('No connection object received.'));
} }
$url = $this->urlAppendParams($url, $params); $url = $this->urlAppendParams($url, $params);
$response = $this->HTTPClientPOST($url, $params['connection'], json_encode($params['body'])); if (!is_string($params['body'])) {
$params['body'] = json_encode($params['body']);
}
$response = $this->HTTPClientPOST($url, $params['connection'], $params['body']);
if ($response->isOk()) { if ($response->isOk()) {
return $response; return $response;
} else { } else {
@ -791,6 +808,57 @@ class MispConnector extends CommonConnectorTools
throw new MethodNotAllowedException(__('Invalid http request type for the given action.')); throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
} }
public function batchAPIAction(array $params): array
{
if ($params['request']->is(['get'])) {
return [
'data' => [
'title' => __('Execute API Request'),
'description' => __('Perform an API Request on the list of selected connections'),
'fields' => [
[
'field' => 'connection_ids',
'type' => 'hidden',
'value' => $params['connection_ids']
],
[
'field' => 'method',
'label' => __('Method'),
'type' => 'dropdown',
'options' => ['GET' => 'GET', 'POST' => 'POST']
],
[
'field' => 'url',
'label' => __('Relative URL'),
'type' => 'text',
],
[
'field' => 'body',
'label' => __('POST Body'),
'type' => 'codemirror',
],
],
'submit' => [
'action' => $params['request']->getParam('action')
],
'url' => ['controller' => 'localTools', 'action' => 'batchAction', 'batchAPIAction']
]
];
} else if ($params['request']->is(['post'])) {
if ($params['method'] == 'GET') {
$response = $this->getData($params['url'], $params);
} else {
$response = $this->postData($params['url'], $params);
}
if ($response->getStatusCode() == 200) {
return ['success' => 1, 'message' => __('API query successful'), 'data' => $response->getJson()];
} else {
return ['success' => 0, 'message' => __('API query failed'), 'data' => $response->getJson()];
}
}
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
}
public function initiateConnection(array $params): array public function initiateConnection(array $params): array
{ {
$params['connection_settings'] = json_decode($params['connection']['settings'], true); $params['connection_settings'] = json_decode($params['connection']['settings'], true);

View File

@ -310,7 +310,7 @@ class LocalToolsTable extends AppTable
public function isValidSettings($settings, array $context) public function isValidSettings($settings, array $context)
{ {
$settings = json_decode($settings, true); $settings = json_decode($settings, true);
$validationErrors = $this->getLocalToolsSettingValidationErrors($context['data']['id'], $settings); $validationErrors = $this->getLocalToolsSettingValidationErrors($context['data']['connector'], $settings);
return $this->getValidationMessage($validationErrors); return $this->getValidationMessage($validationErrors);
} }
@ -323,9 +323,9 @@ class LocalToolsTable extends AppTable
return empty($messages) ? true : implode('; ', $messages); return empty($messages) ? true : implode('; ', $messages);
} }
public function getLocalToolsSettingValidationErrors($connectionId, array $settings): array public function getLocalToolsSettingValidationErrors($connectorName, array $settings): array
{ {
$connector = array_values($this->getConnectorByConnectionId($connectionId))[0]; $connector = array_values($this->getConnectors($connectorName))[0];
$errors = []; $errors = [];
if (method_exists($connector, 'addSettingValidatorRules')) { if (method_exists($connector, 'addSettingValidatorRules')) {
$validator = new Validator(); $validator = new Validator();

View File

@ -1,16 +1,36 @@
<?php <?php
$multiSelectActions = [];
foreach ($connector->getBatchActionFunctions() as $actionName => $actionData) {
$multiSelectActions[] = [
'text' => $actionData['ui']['text'],
'icon' => $actionData['ui']['icon'],
'variant' => $actionData['ui']['variant'],
'params' => ['data-actionname' => $actionName],
'onclick' => 'handleMultiSelectAction'
];
}
echo $this->element('genericElements/IndexTable/index_table', [ echo $this->element('genericElements/IndexTable/index_table', [
'data' => [ 'data' => [
'data' => $data, 'data' => $data,
'top_bar' => [ 'top_bar' => [
'children' => [ 'children' => [
[
'type' => 'multi_select_actions',
'children' => $multiSelectActions,
'data' => [
'id' => [
'value_path' => 'id'
]
]
],
[ [
'type' => 'simple', 'type' => 'simple',
'children' => [ 'children' => [
'data' => [ 'data' => [
'type' => 'simple', 'type' => 'simple',
'text' => __('Add connection'), 'text' => __('Add connection'),
'popover_url' => sprintf('/localTools/add/%s', h($connector)) 'popover_url' => sprintf('/localTools/add/%s', h($connectorName))
] ]
] ]
], ],
@ -72,23 +92,106 @@ echo $this->element('genericElements/IndexTable/index_table', [
[ [
'open_modal' => '/localTools/connectLocal/[onclick_params_data_path]', 'open_modal' => '/localTools/connectLocal/[onclick_params_data_path]',
'modal_params_data_path' => 'id', 'modal_params_data_path' => 'id',
'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connector)), 'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connectorName)),
'icon' => 'plug' 'icon' => 'plug'
], ],
[ [
'open_modal' => '/localTools/edit/[onclick_params_data_path]', 'open_modal' => '/localTools/edit/[onclick_params_data_path]',
'modal_params_data_path' => 'id', 'modal_params_data_path' => 'id',
'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connector)), 'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connectorName)),
'icon' => 'edit' 'icon' => 'edit'
], ],
[ [
'open_modal' => '/localTools/delete/[onclick_params_data_path]', 'open_modal' => '/localTools/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id', 'modal_params_data_path' => 'id',
'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connector)), 'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connectorName)),
'icon' => 'trash' 'icon' => 'trash'
], ],
] ]
] ]
]); ]);
echo '</div>';
?> ?>
<script>
function handleMultiSelectAction(idList, selectedRows, $table, $clicked) {
const url = `/localTools/batchAction/${$clicked.data('actionname')}?connection_ids=${encodeURIComponent(idList)}`
const reloadUrl = '/localTools/connectorIndex/<?= $connectorName ?>'
const successCallback = function([requestData, modalObject]) {
includeResultInModal(requestData, modalObject)
UI.reload(reloadUrl, UI.getContainerForTable($table), $table)
}
const failCallback = function([requestData, modalObject]) {
includeResultInModal(requestData, modalObject)
}
UI.submissionModal(url, successCallback, failCallback, {closeOnSuccess: false})
}
function includeResultInModal(requestData, modalObject) {
const resultsHaveErrors = checkResultsHaveErrors(requestData.data)
let tableData = []
let tableHeader = []
if (resultsHaveErrors) {
tableHeader = ['<?= __('Connection ID') ?>', '<?= __('Connection Name') ?>', '<?= __('Message') ?>', '<?= __('Error') ?>', '<?= __('Success') ?>', '<?= __('Result') ?>']
} else {
tableHeader = ['<?= __('Connection ID') ?>', '<?= __('Connection Name') ?>', '<?= __('Message') ?>', '<?= __('Success') ?>', '<?= __('Result') ?>']
}
for (const key in requestData.data) {
if (Object.hasOwnProperty.call(requestData.data, key)) {
const singleResult = requestData.data[key];
$faIcon = $('<i class="fa"></i>').addClass(singleResult.success ? 'fa-check text-success' : 'fa-times text-danger')
$jsonResult = $('<pre class="p-2 rounded mb-0" style="max-width: 400px; max-height: 300px;background: #eeeeee55;"></pre>').append(
$('<code></code>').text(JSON.stringify(singleResult.data, null, 4))
)
if (resultsHaveErrors) {
tableData.push([singleResult.connection.id, singleResult.connection.name, singleResult.message, JSON.stringify(singleResult.errors, null, 4), $faIcon, $jsonResult])
} else {
tableData.push([singleResult.connection.id, singleResult.connection.name, singleResult.message, $faIcon, $jsonResult])
}
}
}
handleMessageTable(
modalObject.$modal,
tableHeader,
tableData
)
const $footer = $(modalObject.ajaxApi.statusNode).parent()
modalObject.ajaxApi.statusNode.remove()
const $cancelButton = $footer.find('button[data-dismiss="modal"]')
$cancelButton.text('<?= __('OK') ?>').removeClass('btn-secondary').addClass('btn-primary')
}
function constructMessageTable(header, data) {
return HtmlHelper.table(
header,
data,
{
small: true,
borderless: true,
tableClass: ['message-table', 'mt-4 mb-0'],
}
)
}
function handleMessageTable($modal, header, data) {
const $modalBody = $modal.find('.modal-body')
const $messageTable = $modalBody.find('table.message-table')
const messageTableHTML = constructMessageTable(header, data)[0].outerHTML
if ($messageTable.length) {
$messageTable.html(messageTableHTML)
} else {
$modalBody.append(messageTableHTML)
}
}
function checkResultsHaveErrors(result) {
for (const key in result) {
if (Object.hasOwnProperty.call(result, key)) {
const singleResult = result[key];
if(!singleResult.success) {
return true
}
}
}
return false
}
</script>

View File

@ -6,11 +6,12 @@
'variant' => $child['variant'] ?? 'primary', 'variant' => $child['variant'] ?? 'primary',
'text' => $child['text'], 'text' => $child['text'],
'outline' => !empty($child['outline']), 'outline' => !empty($child['outline']),
'params' => [ 'icon' => $child['icon'] ?? null,
'params' => array_merge([
'data-onclick-function' => $child['onclick'] ?? '', 'data-onclick-function' => $child['onclick'] ?? '',
'data-table-random-value' => $tableRandomValue, 'data-table-random-value' => $tableRandomValue,
'onclick' => 'multiActionClickHandler(this)' 'onclick' => 'multiActionClickHandler(this)'
] ], $child['params'] ?? [])
]); ]);
} }
echo sprintf( echo sprintf(
@ -68,7 +69,7 @@
}) })
const functionName = $clicked.data('onclick-function') const functionName = $clicked.data('onclick-function')
if (functionName && typeof window[functionName] === 'function') { if (functionName && typeof window[functionName] === 'function') {
window[functionName](selectedIDs, selectedData, $table) window[functionName](selectedIDs, selectedData, $table, $clicked)
} }
} }
</script> </script>