diff --git a/config/Migrations/20210618102027_OutboxSystem.php b/config/Migrations/20210618102027_OutboxSystem.php
new file mode 100644
index 0000000..2b74b71
--- /dev/null
+++ b/config/Migrations/20210618102027_OutboxSystem.php
@@ -0,0 +1,90 @@
+table('outbox', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+ $table
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('uuid', 'uuid', [
+ 'default' => null,
+ 'null' => false,
+ ])
+ ->addColumn('scope', 'string', [
+ 'default' => null,
+ 'null' => false,
+ 'limit' => 191,
+ 'comment' => 'The to model linked with the message',
+ ])
+ ->addColumn('action', 'string', [
+ 'default' => null,
+ 'null' => false,
+ 'limit' => 191,
+ 'comment' => 'The action linked with the message',
+ ])
+ ->addColumn('title', 'string', [
+ 'default' => null,
+ 'null' => false,
+ 'limit' => 191,
+ ])
+ ->addColumn('user_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->addColumn('comment', 'text', [
+ 'default' => null,
+ 'null' => true,
+ ])
+ ->addColumn('description', 'text', [
+ 'default' => null,
+ 'null' => true,
+ ])
+ ->addColumn('data', 'text', [
+ 'default' => null,
+ 'null' => true,
+ 'limit' => MysqlAdapter::TEXT_LONG
+ ])
+ ->addColumn('created', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ]);
+
+ $table->addForeignKey('user_id', 'users', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']);
+
+ $table->addIndex(['uuid'], ['unique' => true])
+ ->addIndex('scope')
+ ->addIndex('action')
+ ->addIndex('title')
+ ->addIndex('created')
+ ->addIndex('user_id');
+
+ $table->create();
+ }
+}
+
diff --git a/libraries/default/InboxProcessors/GenericInboxProcessor.php b/libraries/default/InboxProcessors/GenericInboxProcessor.php
index f78a066..ea71884 100644
--- a/libraries/default/InboxProcessors/GenericInboxProcessor.php
+++ b/libraries/default/InboxProcessors/GenericInboxProcessor.php
@@ -193,7 +193,7 @@ class GenericInboxProcessor
$requestData['action'] = $this->action;
$requestData['description'] = $this->description;
$request = $this->generateRequest($requestData);
- $savedRequest = $this->Inbox->save($request);
+ $savedRequest = $this->Inbox->createEntry($request);
return $this->genActionResult(
$savedRequest,
$savedRequest !== false,
diff --git a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php
new file mode 100644
index 0000000..a88ec1b
--- /dev/null
+++ b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php
@@ -0,0 +1,107 @@
+Broods->find()
+ ->where(['id' => $broodId])
+ ->first();
+ return $brood;
+ }
+
+ protected function getLocalTool($toolId)
+ {
+ $tool = $this->LocalTools->find()
+ ->where(['id' => $toolId])
+ ->first();
+ return $tool;
+ }
+}
+
+class ResendFailedMessageProcessor extends BroodsOutboxProcessor implements GenericProcessorActionI {
+ public $action = 'ResendFailedMessage';
+ protected $description;
+
+ public function __construct() {
+ parent::__construct();
+ $this->description = __('Handle re-sending messages that failed to be received from other cerebrate instances.');
+ $this->Broods = TableRegistry::getTableLocator()->get('Broods');
+ $this->LocalTools = \Cake\ORM\TableRegistry::getTableLocator()->get('LocalTools');
+ }
+
+ protected function addValidatorRules($validator)
+ {
+ return $validator;
+ }
+
+ public function getViewVariables($request)
+ {
+ $request->brood = $this->getIssuerBrood($request['data']['brood_id']);
+ $request->individual = $request->user->individual;
+ $request->localTool = $this->getLocalTool($request['data']['local_tool_id']);
+ $request->remoteTool = $request['data']['remote_tool'];
+ return [
+ 'request' => $request,
+ ];
+ }
+
+ public function create($requestData) {
+ $this->validateRequestData($requestData);
+ $brood = $requestData['brood'];
+ $requestData['title'] = __('Issue while sending message to Cerebrate instance `{0}` using `{1}`', $brood->name, sprintf('%s.%s', $requestData['model'], $requestData['action']));
+ return parent::create($requestData);
+ }
+
+ public function process($id, $requestData, $outboxRequest)
+ {
+ if (!empty($requestData['is_delete'])) { // -> declined
+ $success = true;
+ $messageSucess = __('Message successfully deleted');
+ $messageFail = '';
+ } else {
+ $brood = $this->getIssuerBrood((int) $outboxRequest->data['brood_id']);
+ $url = $outboxRequest->data['url'];
+ $dataSent = $outboxRequest->data['sent'];
+ $dataSent['connectorName'] = 'MispConnector';
+ $response = $this->Broods->sendRequest($brood, $url, true, $dataSent);
+ $jsonReply = $response->getJson();
+ $success = !empty($jsonReply['success']);
+ $messageSuccess = __('Message successfully sent to `{0}`', $brood->name);
+ $messageFail = __('Could not send message to `{0}`.', $brood->name);
+ }
+ if ($success) {
+ $this->discard($id, $requestData);
+ }
+ return $this->genActionResult(
+ [],
+ $success,
+ $success ? $messageSuccess : $messageFail,
+ []
+ );
+ }
+
+ public function discard($id, $requestData)
+ {
+ return parent::discard($id, $requestData);
+ }
+}
diff --git a/libraries/default/OutboxProcessors/GenericOutboxProcessor.php b/libraries/default/OutboxProcessors/GenericOutboxProcessor.php
new file mode 100644
index 0000000..eb97d28
--- /dev/null
+++ b/libraries/default/OutboxProcessors/GenericOutboxProcessor.php
@@ -0,0 +1,218 @@
+Outbox = TableRegistry::getTableLocator()->get('Outbox');
+ if ($registerActions) {
+ $this->registerActionInProcessor();
+ }
+ $this->assignProcessingTemplate();
+ }
+
+ private function assignProcessingTemplate()
+ {
+ $processingTemplatePath = $this->getProcessingTemplatePath();
+ $file = new File($this->processingTemplatesDirectory . DS . $processingTemplatePath);
+ if ($file->exists()) {
+ $this->processingTemplate = str_replace('.php', '', $processingTemplatePath);
+ }
+ $file->close();
+ }
+
+ protected function updateProcessingTemplate($request)
+ {
+ }
+
+ public function getRegisteredActions()
+ {
+ return $this->registeredActions;
+ }
+ public function getScope()
+ {
+ return $this->scope;
+ }
+ public function getDescription()
+ {
+ return $this->description ?? '';
+ }
+
+ protected function getProcessingTemplatePath()
+ {
+ return sprintf('%s/%s.php',
+ $this->scope,
+ $this->action
+ );
+ }
+
+ public function getProcessingTemplate()
+ {
+ return $this->processingTemplate;
+ }
+
+ public function render($request=[], Cake\Http\ServerRequest $serverRequest)
+ {
+ $viewVariables = $this->getViewVariables($request);
+ $this->updateProcessingTemplate($request);
+ $processingTemplate = $this->getProcessingTemplate();
+ $builder = new ViewBuilder();
+ $builder->disableAutoLayout()
+ ->setClassName('Monad')
+ ->setTemplate($processingTemplate);
+ $view = $builder->build($viewVariables);
+ $view->setRequest($serverRequest);
+ return $view->render();
+ }
+
+ protected function generateRequest($requestData)
+ {
+ $request = $this->Outbox->newEmptyEntity();
+ $request = $this->Outbox->patchEntity($request, $requestData);
+ if ($request->getErrors()) {
+ throw new Exception(__('Could not create request.{0}Reason: {1}', PHP_EOL, json_encode($request->getErrors())), 1);
+ }
+ return $request;
+ }
+
+ protected function validateRequestData($requestData)
+ {
+ $errors = [];
+ if (!isset($requestData['data'])) {
+ $errors[] = __('No request data provided');
+ }
+ $validator = new Validator();
+ if (method_exists($this, 'addValidatorRules')) {
+ $validator = $this->addValidatorRules($validator);
+ $errors = $validator->validate($requestData['data']);
+ }
+ if (!empty($errors)) {
+ throw new Exception('Error while validating request data. ' . json_encode($errors), 1);
+ }
+ }
+
+ protected function registerActionInProcessor()
+ {
+ foreach ($this->registeredActions as $i => $action) {
+ $className = "{$action}Processor";
+ $reflection = new ReflectionClass($className);
+ if ($reflection->isAbstract() || $reflection->isInterface()) {
+ throw new Exception(__('Cannot create instance of %s, as it is abstract or is an interface'));
+ }
+ $this->{$action} = $reflection->newInstance();
+ }
+ }
+
+ protected function getViewVariablesConfirmModal($id, $title='', $question='', $actionName='')
+ {
+ return [
+ 'title' => !empty($title) ? $title : __('Process request {0}', $id),
+ 'question' => !empty($question) ? $question : __('Confirm request {0}', $id),
+ 'actionName' => !empty($actionName) ? $actionName : __('Confirm'),
+ 'path' => ['controller' => 'outbox', 'action' => 'process', $id]
+ ];
+ }
+
+ public function getViewVariables($request)
+ {
+ return $this->getViewVariablesConfirmModal($request->id, '', '', '');
+ }
+
+ protected function genActionResult($data, $success, $message, $errors=[])
+ {
+ return [
+ 'data' => $data,
+ 'success' => $success,
+ 'message' => $message,
+ 'errors' => $errors,
+ ];
+ }
+
+ public function genHTTPReply($controller, $processResult, $redirect=null)
+ {
+ $scope = $this->scope;
+ $action = $this->action;
+ if ($processResult['success']) {
+ $message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} successfully processed.', $id);
+ if ($controller->ParamHandler->isRest()) {
+ $response = $controller->RestResponse->viewData($processResult, 'json');
+ } else if ($controller->ParamHandler->isAjax()) {
+ $response = $controller->RestResponse->ajaxSuccessResponse('OutboxProcessor', "{$scope}.{$action}", $processResult['data'], $message);
+ } else {
+ $controller->Flash->success($message);
+ if (!is_null($redirect)) {
+ $response = $controller->redirect($redirect);
+ } else {
+ $response = $controller->redirect(['action' => 'index']);
+ }
+ }
+ } else {
+ $message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} could not be processed.', $id);
+ if ($controller->ParamHandler->isRest()) {
+ $response = $controller->RestResponse->viewData($processResult, 'json');
+ } else if ($controller->ParamHandler->isAjax()) {
+ $response = $controller->RestResponse->ajaxFailResponse('OutboxProcessor', "{$scope}.{$action}", $processResult['data'], $message, $processResult['errors']);
+ } else {
+ $controller->Flash->error($message);
+ if (!is_null($redirect)) {
+ $response = $controller->redirect($redirect);
+ } else {
+ $response = $controller->redirect(['action' => 'index']);
+ }
+ }
+ }
+
+ return $response;
+ }
+
+ public function checkLoading()
+ {
+ return 'Assimilation successful!';
+ }
+
+ public function create($requestData)
+ {
+ $user_id = Router::getRequest()->getSession()->read('Auth.id');
+ $requestData['scope'] = $this->scope;
+ $requestData['action'] = $this->action;
+ $requestData['description'] = $this->description;
+ $requestData['user_id'] = $user_id;
+ $request = $this->generateRequest($requestData);
+ $savedRequest = $this->Outbox->createEntry($request);
+ return $this->genActionResult(
+ $savedRequest,
+ $savedRequest !== false,
+ __('{0} request for {1} created', $this->scope, $this->action),
+ $request->getErrors()
+ );
+ }
+
+ public function discard($id, $requestData)
+ {
+ $request = $this->Outbox->get($id);
+ $this->Outbox->delete($request);
+ return $this->genActionResult(
+ [],
+ true,
+ __('{0}.{1} request #{2} discarded', $this->scope, $this->action, $id)
+ );
+ }
+}
diff --git a/libraries/default/OutboxProcessors/TemplateOutoxProcessor.php.template b/libraries/default/OutboxProcessors/TemplateOutoxProcessor.php.template
new file mode 100644
index 0000000..07dc21f
--- /dev/null
+++ b/libraries/default/OutboxProcessors/TemplateOutoxProcessor.php.template
@@ -0,0 +1,65 @@
+description = __('~to-be-defined~');
+ $this->Users = TableRegistry::getTableLocator()->get('Users');
+ }
+
+ protected function addValidatorRules($validator)
+ {
+ return $validator;
+ }
+
+ public function create($requestData) {
+ $this->validateRequestData($requestData);
+ $requestData['title'] = __('~to-be-defined~');
+ return parent::create($requestData);
+ }
+
+ public function process($id, $requestData, $inboxRequest)
+ {
+ $proposalAccepted = false;
+ $saveResult = [];
+ if ($proposalAccepted) {
+ $this->discard($id, $requestData);
+ }
+ return $this->genActionResult(
+ $saveResult,
+ $proposalAccepted,
+ $proposalAccepted ? __('success') : __('fail'),
+ []
+ );
+ }
+
+ public function discard($id, $requestData)
+ {
+ return parent::discard($id, $requestData);
+ }
+}
\ No newline at end of file
diff --git a/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php b/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php
new file mode 100644
index 0000000..6e0a018
--- /dev/null
+++ b/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php
@@ -0,0 +1,138 @@
+ 'cancel',
+ 'variant' => 'secondary',
+ 'text' => __('Cancel'),
+ ],
+ [
+ 'clickFunction' => 'deleteEntry',
+ 'variant' => 'danger',
+ 'text' => __('Delete Message'),
+ ],
+ [
+ 'clickFunction' => 'resendMessage',
+ 'text' => __('Re-Send Message'),
+ ]
+];
+
+$tools = sprintf(
+'
+ %s
+ %s
+ %s
+
',
+ sprintf('%s%s',
+ sprintf('/localTools/view/%s', h($request['localTool']->id)),
+ h($request['localTool']->description),
+ h($request['localTool']->name),
+ __('(local tool)')
+ ),
+ sprintf('', $this->FontAwesome->getClass('long-arrow-alt-right')),
+ sprintf('%s%s',
+ sprintf('/localTools/broodTools/%s', h($request['data']['remote_tool']['id'])),
+ h($request['data']['remote_tool']['description']),
+ h($request['data']['remote_tool']['name']),
+ __('(remote tool)')
+ )
+);
+
+
+$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
+ 'fields' => [
+ ['key' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
+ return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
+ }],
+ ['key' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
+ return sprintf('%s',
+ $this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
+ h($brood['name'])
+ );
+ }],
+ ['key' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
+ return sprintf('%s',
+ $this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
+ h($individual['email'])
+ );
+ }],
+ ['key' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
+ $html = '';
+ foreach ($alignments as $alignment) {
+ $html .= sprintf('',
+ h($alignment['type']),
+ $this->Url->build(['controller' => 'users', 'action' => 'view', $alignment['organisation']['id']]),
+ h($alignment['organisation']['name'])
+ );
+ }
+ return $html;
+ }],
+ ],
+ 'items' => [$request->toArray()],
+]);
+
+$requestData = $this->Bootstrap->collapse([
+ 'title' => __('Message data'),
+ 'open' => true,
+ ],
+ sprintf('%s
', json_encode($request['data']['sent'], JSON_PRETTY_PRINT))
+);
+
+$rows = sprintf('%s | %s |
', __('URL'), h($request['data']['url']));
+$rows .= sprintf('%s | %s |
', __('Reason'), h($request['data']['reason']['message']) ?? '');
+$rows .= sprintf('%s | %s |
', __('Errors'), h(json_encode($request['data']['reason']['errors'])) ?? '');
+$table2 = sprintf('', $rows);
+
+$form = $this->element('genericElements/Form/genericForm', [
+ 'entity' => null,
+ 'ajax' => false,
+ 'raw' => true,
+ 'data' => [
+ 'model' => 'Inbox',
+ 'fields' => [
+ [
+ 'field' => 'is_delete',
+ 'type' => 'checkbox',
+ 'default' => false
+ ]
+ ],
+ 'submit' => [
+ 'action' => $this->request->getParam('action')
+ ]
+ ]
+]);
+$form = sprintf('%s
', $form);
+
+$bodyHtml = sprintf('%s',
+ $tools,
+ $table,
+ $table2,
+ $requestData,
+ $form
+);
+
+echo $this->Bootstrap->modal([
+ 'title' => $request['title'],
+ 'size' => 'xl',
+ 'type' => 'custom',
+ 'bodyHtml' => sprintf('%s
',
+ $bodyHtml
+ ),
+ 'footerButtons' => $footerButtons
+]);
+
+?>
+
+
\ No newline at end of file
diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php
index 55c2161..4c77192 100644
--- a/src/Controller/Component/ACLComponent.php
+++ b/src/Controller/Component/ACLComponent.php
@@ -650,22 +650,55 @@ class ACLComponent extends Component
'url' => '/inbox/index',
'label' => __('Inbox')
],
+ 'outbox' => [
+ 'url' => '/outbox/index',
+ 'label' => __('Outbox')
+ ],
'view' => [
'url' => '/inbox/view/{{id}}',
- 'label' => __('View Meta Template'),
+ 'label' => __('View Message'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'delete' => [
'url' => '/inbox/delete/{{id}}',
- 'label' => __('Delete Meta Template'),
+ 'label' => __('Delete Message'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'listProcessors' => [
'url' => '/inbox/listProcessors',
- 'label' => __('List Request Processors'),
+ 'label' => __('List Inbox Processors'),
+ 'skipTopMenu' => 1
+ ]
+ ]
+ ],
+ 'Outbox' => [
+ 'label' => __('Outbox'),
+ 'url' => '/outbox/index',
+ 'children' => [
+ 'index' => [
+ 'url' => '/outbox/index',
+ 'label' => __('Outbox'),
+ 'skipTopMenu' => 1
+ ],
+ 'view' => [
+ 'url' => '/outbox/view/{{id}}',
+ 'label' => __('View Message'),
+ 'actions' => ['delete', 'edit', 'view'],
+ 'skipTopMenu' => 1
+ ],
+ 'delete' => [
+ 'url' => '/outbox/delete/{{id}}',
+ 'label' => __('Delete Message'),
+ 'actions' => ['delete', 'edit', 'view'],
+ 'skipTopMenu' => 1,
+ 'popup' => 1
+ ],
+ 'listProcessors' => [
+ 'url' => '/outbox/listProcessors',
+ 'label' => __('List Outbox Processors'),
'skipTopMenu' => 1
]
]
diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php
index b71e2d5..0b4f605 100644
--- a/src/Controller/InboxController.php
+++ b/src/Controller/InboxController.php
@@ -119,7 +119,7 @@ class InboxController extends AppController
$this->set('data', $data);
}
- public function createInboxEntry($scope, $action)
+ public function createEntry($scope, $action)
{
if (!$this->request->is('post')) {
throw new MethodNotAllowedException(__('Only POST method is accepted'));
@@ -137,7 +137,7 @@ class InboxController extends AppController
$errors = $this->Inbox->checkUserBelongsToBroodOwnerOrg($this->ACL->getUser(), $entryData);
if (!empty($errors)) {
$message = __('Could not create inbox message');
- return $this->RestResponse->ajaxFailResponse(Inflector::singularize($this->Inbox->getAlias()), 'createInboxEntry', [], $message, $errors);
+ return $this->RestResponse->ajaxFailResponse(Inflector::singularize($this->Inbox->getAlias()), 'createEntry', [], $message, $errors);
}
} else {
$processor = $this->inboxProcessors->getProcessor($scope, $action);
diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php
index 835b8a1..a1eae55 100644
--- a/src/Controller/LocalToolsController.php
+++ b/src/Controller/LocalToolsController.php
@@ -237,7 +237,7 @@ class LocalToolsController extends AppController
$response = $this->RestResponse->ajaxSuccessResponse('LocalTool', 'connectionRequest', [], $inboxResult['message']);
} else {
$this->Flash->success($inboxResult['message']);
- $this->redirect(['action' => 'broodTools', $cerebrate_id]);
+ $response = $this->redirect(['action' => 'broodTools', $cerebrate_id]);
}
} else {
if ($this->ParamHandler->isRest()) {
@@ -246,7 +246,7 @@ class LocalToolsController extends AppController
$response = $this->RestResponse->ajaxFailResponse('LocalTool', 'connectionRequest', [], $inboxResult['message'], $inboxResult['errors']);
} else {
$this->Flash->error($inboxResult['message']);
- $this->redirect($this->referer());
+ $response = $this->redirect($this->referer());
}
}
return $response;
diff --git a/src/Controller/OutboxController.php b/src/Controller/OutboxController.php
new file mode 100644
index 0000000..5426883
--- /dev/null
+++ b/src/Controller/OutboxController.php
@@ -0,0 +1,125 @@
+set('metaGroup', 'Administration');
+ }
+
+
+ public function index()
+ {
+ $this->CRUD->index([
+ 'filters' => $this->filters,
+ 'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]],
+ 'contextFilters' => [
+ 'fields' => [
+ 'scope',
+ ]
+ ],
+ 'contain' => ['Users']
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function filtering()
+ {
+ $this->CRUD->filtering();
+ }
+
+ public function view($id)
+ {
+ $this->CRUD->view($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function delete($id)
+ {
+ $this->set('deletionTitle', __('Discard request'));
+ $this->set('deletionText', __('Are you sure you want to discard request #{0}?', $id));
+ $this->set('deletionConfirm', __('Discard'));
+ $this->CRUD->delete($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function process($id)
+ {
+ $request = $this->Outbox->get($id, ['contain' => ['Users' => ['Individuals' => ['Alignments' => 'Organisations']]]]);
+ $scope = $request->scope;
+ $action = $request->action;
+ $this->outboxProcessors = TableRegistry::getTableLocator()->get('OutboxProcessors');
+ $processor = $this->outboxProcessors->getProcessor($scope, $action);
+ if ($this->request->is('post')) {
+ $processResult = $processor->process($id, $this->request->getData(), $request);
+ return $processor->genHTTPReply($this, $processResult);
+ } else {
+ $renderedView = $processor->render($request, $this->request);
+ return $this->response->withStringBody($renderedView);
+ }
+ }
+
+ public function listProcessors()
+ {
+ $this->OutboxProcessors = TableRegistry::getTableLocator()->get('OutboxProcessors');
+ $processors = $this->OutboxProcessors->listProcessors();
+ if ($this->ParamHandler->isRest()) {
+ return $this->RestResponse->viewData($processors, 'json');
+ }
+ $data = [];
+ foreach ($processors as $scope => $scopedProcessors) {
+ foreach ($scopedProcessors as $processor) {
+ $data[] = [
+ 'enabled' => $processor->enabled,
+ 'scope' => $scope,
+ 'action' => $processor->action,
+ 'description' => isset($processor->getDescription) ? $processor->getDescription() : null,
+ 'notice' => $processor->notice ?? null,
+ 'error' => $processor->error ?? null,
+ ];
+ }
+ }
+ $this->set('data', $data);
+ }
+
+ public function createEntry($scope, $action)
+ {
+ if (!$this->request->is('post')) {
+ throw new MethodNotAllowedException(__('Only POST method is accepted'));
+ }
+ $entryData = [
+ 'user_id' => $this->ACL->getUser()['id'],
+ ];
+ $entryData['data'] = $this->request->getData() ?? [];
+ $this->OutboxProcessors = TableRegistry::getTableLocator()->get('OutboxProcessors');
+ $processor = $this->OutboxProcessors->getProcessor($scope, $action);
+ $creationResult = $this->OutboxProcessors->createOutboxEntry($processor, $entryData);
+ return $processor->genHTTPReply($this, $creationResult);
+ }
+}
diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php
index e1dbd27..570a60b 100644
--- a/src/Model/Table/BroodsTable.php
+++ b/src/Model/Table/BroodsTable.php
@@ -212,11 +212,7 @@ class BroodsTable extends AppTable
} else {
$response = $http->get($brood->url, $data, $config);
}
- if ($response->isOk()) {
- return $response;
- } else {
- throw new NotFoundException(__('Could not send to the requested resource.'));
- }
+ return $response;
}
private function injectRequiredData($params, $data): Array
@@ -230,30 +226,30 @@ class BroodsTable extends AppTable
public function sendLocalToolConnectionRequest($params, $data): array
{
- $url = '/inbox/createInboxEntry/LocalTool/IncomingConnectionRequest';
+ $url = '/inbox/createEntry/LocalTool/IncomingConnectionRequest';
$data = $this->injectRequiredData($params, $data);
- $response = $this->sendRequest($params['remote_cerebrate'], $url, true, $data);
try {
+ $response = $this->sendRequest($params['remote_cerebrate'], $url, true, $data);
$jsonReply = $response->getJson();
if (empty($jsonReply['success'])) {
- $this->handleMessageNotCreated($response);
+ $jsonReply = $this->handleMessageNotCreated($params['remote_cerebrate'], $url, $data, 'LocalTool', 'IncomingConnectionRequest', $response, $params);
}
} catch (NotFoundException $e) {
- $jsonReply = $this->handleSendingFailed($response);
+ $jsonReply = $this->handleSendingFailed($params['remote_cerebrate'], $url, $data, 'LocalTool', 'IncomingConnectionRequest', $e, $params);
}
return $jsonReply;
}
public function sendLocalToolAcceptedRequest($params, $data): Response
{
- $url = '/inbox/createInboxEntry/LocalTool/AcceptedRequest';
+ $url = '/inbox/createEntry/LocalTool/AcceptedRequest';
$data = $this->injectRequiredData($params, $data);
return $this->sendRequest($params['remote_cerebrate'], $url, true, $data);
}
public function sendLocalToolDeclinedRequest($params, $data): Response
{
- $url = '/inbox/createInboxEntry/LocalTool/DeclinedRequest';
+ $url = '/inbox/createEntry/LocalTool/DeclinedRequest';
$data = $this->injectRequiredData($params, $data);
return $this->sendRequest($params['remote_cerebrate'], $url, true, $data);
}
@@ -264,10 +260,19 @@ class BroodsTable extends AppTable
* @param Object $response
* @return array
*/
- private function handleSendingFailed(Object $response): array
+ private function handleSendingFailed($brood, $url, $data, $model, $action, $e, $params): array
{
- // debug('sending failed. Modify state and add entry in outbox');
- throw new NotFoundException(__('sending failed. Modify state and add entry in outbox'));
+ $reason = [
+ 'message' => __('Failed to send message to remote cerebrate. It has been placed in the outbox.'),
+ 'errors' => [$e->getMessage()],
+ ];
+ $this->saveErrorInOutbox($brood, $url, $data, $reasonMessage, $params);
+ $creationResult = [
+ 'success' => false,
+ 'message' => $reason['message'],
+ 'errors' => $reason['errors'],
+ ];
+ return $creationResult;
}
/**
@@ -276,8 +281,43 @@ class BroodsTable extends AppTable
* @param Object $response
* @return array
*/
- private function handleMessageNotCreated(Object $response): array
+ private function handleMessageNotCreated($brood, $url, $data, $model, $action, $response, $params): array
{
- // debug('Saving message failed. Modify state and add entry in outbox');
+ $responseErrors = $response->getStringBody();
+ if (!is_null($response->getJson())) {
+ $responseErrors = $response->getJson()['errors'] ?? $response->getJson()['message'];
+ }
+ $reason = [
+ 'message' => __('Message rejected by the remote cerebrate. It has been placed in the outbox.'),
+ 'errors' => [$responseErrors],
+ ];
+ $this->saveErrorInOutbox($brood, $url, $data, $reason, $model, $action, $params);
+ $creationResult = [
+ 'success' => false,
+ 'message' => $reason['message'],
+ 'errors' => $reason['errors'],
+ ];
+ return $creationResult;
+ }
+
+ private function saveErrorInOutbox($brood, $url, $data, $reason, $model, $action, $params): array
+ {
+ $this->OutboxProcessors = TableRegistry::getTableLocator()->get('OutboxProcessors');
+ $processor = $this->OutboxProcessors->getProcessor('Broods', 'ResendFailedMessage');
+ $entryData = [
+ 'data' => [
+ 'sent' => $data,
+ 'url' => $url,
+ 'brood_id' => $brood->id,
+ 'reason' => $reason,
+ 'local_tool_id' => $params['connection']['id'],
+ 'remote_tool' => $params['remote_tool'],
+ ],
+ 'brood' => $brood,
+ 'model' => $model,
+ 'action' => $action,
+ ];
+ $creationResult = $processor->create($entryData);
+ return $creationResult;
}
}
diff --git a/src/Model/Table/InboxTable.php b/src/Model/Table/InboxTable.php
index 1640bc2..21d6da6 100644
--- a/src/Model/Table/InboxTable.php
+++ b/src/Model/Table/InboxTable.php
@@ -86,4 +86,10 @@ class InboxTable extends AppTable
}
return $errors;
}
+
+ public function createEntry($entryData)
+ {
+ $savedEntry = $this->save($entryData);
+ return $savedEntry;
+ }
}
diff --git a/src/Model/Table/OutboxProcessorsTable.php b/src/Model/Table/OutboxProcessorsTable.php
new file mode 100644
index 0000000..6c6be33
--- /dev/null
+++ b/src/Model/Table/OutboxProcessorsTable.php
@@ -0,0 +1,134 @@
+ [
+ 'ResendFailedMessageProcessor' => true,
+ ],
+ ];
+
+ public function initialize(array $config): void
+ {
+ parent::initialize($config);
+ $this->loadProcessors();
+ }
+
+ public function getProcessor($scope, $action=null)
+ {
+ if (isset($this->outboxProcessors[$scope])) {
+ if (is_null($action)) {
+ return $this->outboxProcessors[$scope];
+ } else if (!empty($this->outboxProcessors[$scope]->{$action})) {
+ return $this->outboxProcessors[$scope]->{$action};
+ } else {
+ throw new \Exception(__('Processor {0}.{1} not found', $scope, $action));
+ }
+ }
+ throw new MissingOutboxProcessorException(__('Processor not found'));
+ }
+
+ public function listProcessors($scope=null)
+ {
+ if (is_null($scope)) {
+ return $this->outboxProcessors;
+ } else {
+ if (isset($this->outboxProcessors[$scope])) {
+ return $this->outboxProcessors[$scope];
+ } else {
+ throw new MissingOutboxProcessorException(__('Processors for {0} not found', $scope));
+ }
+ }
+ }
+
+ private function loadProcessors()
+ {
+ $processorDir = new Folder($this->processorsDirectory);
+ $processorFiles = $processorDir->find('.*OutboxProcessor\.php', true);
+ foreach ($processorFiles as $processorFile) {
+ if ($processorFile == 'GenericOutboxProcessor.php') {
+ continue;
+ }
+ $processorMainClassName = str_replace('.php', '', $processorFile);
+ $processorMainClassNameShort = str_replace('OutboxProcessor.php', '', $processorFile);
+ $processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName);
+ if (is_object($processorMainClass)) {
+ $this->outboxProcessors[$processorMainClassNameShort] = $processorMainClass;
+ foreach ($this->outboxProcessors[$processorMainClassNameShort]->getRegisteredActions() as $registeredAction) {
+ $scope = $this->outboxProcessors[$processorMainClassNameShort]->getScope();
+ if (!empty($this->enabledProcessors[$scope][$registeredAction])) {
+ $this->outboxProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = true;
+ } else {
+ $this->outboxProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = false;
+ }
+ }
+ } else {
+ $this->outboxProcessors[$processorMainClassNameShort] = new \stdClass();
+ $this->outboxProcessors[$processorMainClassNameShort]->{$registeredAction} = new \stdClass();
+ $this->outboxProcessors[$processorMainClassNameShort]->{$registeredAction}->action = "N/A";
+ $this->outboxProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = false;
+ $this->outboxProcessors[$processorMainClassNameShort]->{$registeredAction}->error = $processorMainClass;
+ }
+ }
+ }
+
+ /**
+ * getProcessorClass
+ *
+ * @param string $filePath
+ * @param string $processorMainClassName
+ * @return object|string Object loading success, string containing the error if failure
+ */
+ private function getProcessorClass($filePath, $processorMainClassName)
+ {
+ try {
+ require_once($filePath);
+ try {
+ $reflection = new \ReflectionClass($processorMainClassName);
+ } catch (\ReflectionException $e) {
+ return $e->getMessage();
+ }
+ $processorMainClass = $reflection->newInstance(true);
+ if ($processorMainClass->checkLoading() === 'Assimilation successful!') {
+ return $processorMainClass;
+ }
+ } catch (Exception $e) {
+ return $e->getMessage();
+ }
+ }
+
+ /**
+ * createOutboxEntry
+ *
+ * @param Object|Array $processor can either be the processor object or an array containing data to fetch it
+ * @param Array $data
+ * @return Array
+ */
+ public function createOutboxEntry($processor, $data)
+ {
+ if (!is_object($processor) && !is_array($processor)) {
+ throw new MethodNotAllowedException(__("Invalid processor passed"));
+ }
+ if (is_array($processor)) {
+ if (empty($processor['scope']) || empty($processor['action'])) {
+ throw new MethodNotAllowedException(__("Invalid data passed. Missing either `scope` or `action`"));
+ }
+ $processor = $this->getProcessor('User', 'Registration');
+ }
+ return $processor->create($data);
+ }
+}
diff --git a/src/Model/Table/OutboxTable.php b/src/Model/Table/OutboxTable.php
new file mode 100644
index 0000000..05ed8fc
--- /dev/null
+++ b/src/Model/Table/OutboxTable.php
@@ -0,0 +1,70 @@
+addBehavior('UUID');
+ $this->addBehavior('Timestamp', [
+ 'events' => [
+ 'Model.beforeSave' => [
+ 'created' => 'new'
+ ]
+ ]
+ ]);
+
+ $this->belongsTo('Users');
+ $this->setDisplayField('title');
+ }
+
+ protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
+ {
+ $schema->setColumnType('data', 'json');
+
+ return $schema;
+ }
+
+ public function validationDefault(Validator $validator): Validator
+ {
+ $validator
+ ->notEmptyString('scope')
+ ->notEmptyString('action')
+ ->notEmptyString('title')
+ ->datetime('created')
+
+ ->requirePresence([
+ 'scope' => ['message' => __('The field `scope` is required')],
+ 'action' => ['message' => __('The field `action` is required')],
+ 'title' => ['message' => __('The field `title` is required')],
+ ], 'create');
+ return $validator;
+ }
+
+ public function buildRules(RulesChecker $rules): RulesChecker
+ {
+ $rules->add($rules->existsIn('user_id', 'Users'), [
+ 'message' => 'The provided `user_id` does not exist'
+ ]);
+
+ return $rules;
+ }
+
+ public function createEntry($entryData, $user = null)
+ {
+ $savedEntry = $this->save($entryData);
+ return $savedEntry;
+ }
+}
diff --git a/src/View/MonadView.php b/src/View/MonadView.php
index f0806a0..18f38d9 100644
--- a/src/View/MonadView.php
+++ b/src/View/MonadView.php
@@ -23,6 +23,7 @@ class MonadView extends AppView
{
private $additionalTemplatePaths = [
ROOT . '/libraries/default/InboxProcessors/templates/',
+ ROOT . '/libraries/default/OutboxProcessors/templates/',
];
protected function _paths(?string $plugin = null, bool $cached = true): array
diff --git a/templates/Outbox/index.php b/templates/Outbox/index.php
new file mode 100644
index 0000000..79a8d18
--- /dev/null
+++ b/templates/Outbox/index.php
@@ -0,0 +1,94 @@
+Html->scriptBlock(sprintf(
+ 'var csrfToken = %s;',
+ json_encode($this->request->getAttribute('csrfToken'))
+));
+echo $this->element('genericElements/IndexTable/index_table', [
+ 'data' => [
+ 'data' => $data,
+ 'top_bar' => [
+ 'children' => [
+ [
+ 'type' => 'context_filters',
+ 'context_filters' => !empty($filteringContexts) ? $filteringContexts : []
+ ],
+ [
+ 'type' => 'search',
+ 'button' => __('Filter'),
+ 'placeholder' => __('Enter value to search'),
+ 'data' => '',
+ 'searchKey' => 'value',
+ 'allowFilering' => true
+ ]
+ ]
+ ],
+ 'fields' => [
+ [
+ 'name' => '#',
+ 'sort' => 'id',
+ 'data_path' => 'id',
+ ],
+ [
+ 'name' => 'created',
+ 'sort' => 'created',
+ 'data_path' => 'created',
+ 'element' => 'datetime'
+ ],
+ [
+ 'name' => 'scope',
+ 'sort' => 'scope',
+ 'data_path' => 'scope',
+ ],
+ [
+ 'name' => 'action',
+ 'sort' => 'action',
+ 'data_path' => 'action',
+ ],
+ [
+ 'name' => 'title',
+ 'sort' => 'title',
+ 'data_path' => 'title',
+ ],
+ [
+ 'name' => 'user',
+ 'sort' => 'user_id',
+ 'data_path' => 'user',
+ 'element' => 'user'
+ ],
+ [
+ 'name' => 'description',
+ 'sort' => 'description',
+ 'data_path' => 'description',
+ ],
+ [
+ 'name' => 'comment',
+ 'sort' => 'comment',
+ 'data_path' => 'comment',
+ ],
+ ],
+ 'title' => __('Outbox'),
+ 'description' => __('A list of requests to be manually processed'),
+ 'actions' => [
+ [
+ 'url' => '/outbox/view',
+ 'url_params_data_paths' => ['id'],
+ 'icon' => 'eye',
+ 'title' => __('View request')
+ ],
+ [
+ 'open_modal' => '/outbox/process/[onclick_params_data_path]',
+ 'modal_params_data_path' => 'id',
+ 'icon' => 'cogs',
+ 'title' => __('Process request')
+ ],
+ [
+ 'open_modal' => '/outbox/delete/[onclick_params_data_path]',
+ 'modal_params_data_path' => 'id',
+ 'icon' => 'trash',
+ 'title' => __('Discard request')
+ ],
+ ]
+ ]
+]);
+echo '';
+?>
diff --git a/templates/Outbox/list_processors.php b/templates/Outbox/list_processors.php
new file mode 100644
index 0000000..38c87f2
--- /dev/null
+++ b/templates/Outbox/list_processors.php
@@ -0,0 +1,54 @@
+element('genericElements/IndexTable/index_table', [
+ 'data' => [
+ 'skip_pagination' => true,
+ 'data' => $data,
+ 'top_bar' => [
+ 'children' => [
+ [
+ 'type' => 'context_filters',
+ 'context_filters' => !empty($filteringContexts) ? $filteringContexts : []
+ ],
+ [
+ 'type' => 'search',
+ 'button' => __('Filter'),
+ 'placeholder' => __('Enter value to search'),
+ 'data' => '',
+ 'searchKey' => 'value',
+ 'allowFilering' => true
+ ]
+ ]
+ ],
+ 'fields' => [
+ [
+ 'name' => __('Enabled'),
+ 'data_path' => 'enabled',
+ 'element' => 'boolean'
+ ],
+ [
+ 'name' => __('Processor scope'),
+ 'data_path' => 'scope',
+ ],
+ [
+ 'name' => __('Processor action'),
+ 'data_path' => 'action',
+ ],
+ [
+ 'name' => __('Description'),
+ 'data_path' => 'description',
+ ],
+ [
+ 'name' => __('Notice'),
+ 'data_path' => 'notice',
+ ],
+ [
+ 'name' => __('Error'),
+ 'data_path' => 'error',
+ ],
+ ],
+ 'title' => __('Available Outbox Request Processors'),
+ 'description' => __('The list of Outbox Request Processors available on this server.'),
+ 'actions' => [
+ ]
+ ]
+]);
\ No newline at end of file
diff --git a/templates/Outbox/view.php b/templates/Outbox/view.php
new file mode 100644
index 0000000..5e6b234
--- /dev/null
+++ b/templates/Outbox/view.php
@@ -0,0 +1,47 @@
+element(
+ '/genericElements/SingleViews/single_view',
+ [
+ 'data' => $entity,
+ 'fields' => [
+ [
+ 'key' => __('ID'),
+ 'path' => 'id'
+ ],
+ [
+ 'key' => 'created',
+ 'path' => 'created',
+ ],
+ [
+ 'key' => 'scope',
+ 'path' => 'scope',
+ ],
+ [
+ 'key' => 'action',
+ 'path' => 'action',
+ ],
+ [
+ 'key' => 'title',
+ 'path' => 'title',
+ ],
+ [
+ 'key' => 'user_id',
+ 'path' => 'user_id',
+ ],
+ [
+ 'key' => 'description',
+ 'path' => 'description',
+ ],
+ [
+ 'key' => 'comment',
+ 'path' => 'comment',
+ ],
+ [
+ 'key' => 'data',
+ 'path' => 'data',
+ 'type' => 'json'
+ ],
+ ],
+ 'children' => []
+ ]
+);