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('
%s @ %s
', + 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('%s
', $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
%s
%s
%s
%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' => [] + ] +);