diff --git a/config/Migrations/20210311110021_InboxSystem.php b/config/Migrations/20210311110021_InboxSystem.php index 65f143a..8357ff7 100644 --- a/config/Migrations/20210311110021_InboxSystem.php +++ b/config/Migrations/20210311110021_InboxSystem.php @@ -56,11 +56,6 @@ class InboxSystem extends AbstractMigration 'null' => false, 'limit' => 191, ]) - // ->addColumn('ip', 'string', [ - // 'limit' => 191, - // 'default' => null, - // 'null' => true, - // ]) ->addColumn('user_id', 'integer', [ 'default' => null, 'null' => true, @@ -92,7 +87,6 @@ class InboxSystem extends AbstractMigration ->addIndex('action') ->addIndex('title') ->addIndex('origin') - // ->addIndex('ip') ->addIndex('created') ->addIndex('user_id'); diff --git a/config/Migrations/20210612140828_RemoteToolConnections.php b/config/Migrations/20210612140828_RemoteToolConnections.php new file mode 100644 index 0000000..b75e112 --- /dev/null +++ b/config/Migrations/20210612140828_RemoteToolConnections.php @@ -0,0 +1,84 @@ +table('remote_tool_connections', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci', + ]); + $table + ->addColumn('id', 'integer', [ + 'autoIncrement' => true, + 'limit' => 10, + 'signed' => false, + ]) + ->addPrimaryKey('id') + ->addColumn('local_tool_id', 'integer', [ + 'null' => false, + 'signed' => false, + 'length' => 10, + ]) + ->addColumn('remote_tool_id', 'integer', [ + 'null' => false, + 'signed' => false, + 'length' => 10, + ]) + ->addColumn('remote_tool_name', 'string', [ + 'null' => false, + 'limit' => 191, + ]) + ->addColumn('brood_id', 'integer', [ + 'null' => false, + 'signed' => false, + 'length' => 10, + ]) + ->addColumn('name', 'string', [ + 'null' => true, + 'limit' => 191, + ]) + ->addColumn('settings', 'text', [ + 'null' => true, + 'limit' => MysqlAdapter::TEXT_LONG + ]) + ->addColumn('status', 'string', [ + 'null' => true, + 'limit' => 32, + 'encoding' => 'ascii', + ]) + ->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]) + ->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + + $table->addForeignKey('local_tool_id', 'local_tools', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']); + + $table->addIndex('remote_tool_id') + ->addIndex('remote_tool_name') + ->addIndex('status') + ->addIndex('name'); + + $table->create(); + } +} + 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/config/Migrations/20210628104235_RolesPermSync.php b/config/Migrations/20210628104235_RolesPermSync.php new file mode 100644 index 0000000..04c36ae --- /dev/null +++ b/config/Migrations/20210628104235_RolesPermSync.php @@ -0,0 +1,18 @@ +table('roles') + ->addColumn('perm_sync', 'boolean', [ + 'default' => 0, + 'null' => false, + ]) + ->update(); + } +} \ No newline at end of file diff --git a/libraries/default/InboxProcessors/BroodInboxProcessor.php b/libraries/default/InboxProcessors/BroodInboxProcessor.php new file mode 100644 index 0000000..c7d655f --- /dev/null +++ b/libraries/default/InboxProcessors/BroodInboxProcessor.php @@ -0,0 +1,65 @@ +description = __('Handle tool interconnection request from other cerebrate instance'); + $this->Broods = TableRegistry::getTableLocator()->get('Broods'); + } + + protected function addValidatorRules($validator) + { + return $validator; + } + + public function create($requestData) { + $this->validateRequestData($requestData); + $requestData['title'] = __('Cerebrate instance {0} requested interconnection for tool {1}', 'Insert brood name', 'Insert tool name'); + return parent::create($requestData); + } + + public function process($id, $requestData, $inboxRequest) + { + $connectionSuccessfull = false; + $interConnectionResult = []; + if ($connectionSuccessfull) { + $this->discard($id, $requestData); + } + return $this->genActionResult( + $interConnectionResult, + $connectionSuccessfull, + $connectionSuccessfull ? __('Interconnection for `{0}` created', 'Insert tool name') : __('Could interconnect tool `{0}`.', 'Insert tool name'), + [] + ); + } + + public function discard($id, $requestData) + { + return parent::discard($id, $requestData); + } +} diff --git a/libraries/default/RequestProcessors/GenericRequestProcessor.php b/libraries/default/InboxProcessors/GenericInboxProcessor.php similarity index 85% rename from libraries/default/RequestProcessors/GenericRequestProcessor.php rename to libraries/default/InboxProcessors/GenericInboxProcessor.php index b5d2300..1ca84ea 100644 --- a/libraries/default/RequestProcessors/GenericRequestProcessor.php +++ b/libraries/default/InboxProcessors/GenericInboxProcessor.php @@ -5,26 +5,31 @@ use Cake\Utility\Inflector; use Cake\Validation\Validator; use Cake\View\ViewBuilder; -interface GenericProcessorActionI +interface GenericInboxProcessorActionI { public function create($requestData); - public function process($requestID, $serverRequest); + public function process($requestID, $serverRequest, $inboxRequest); public function discard($requestID ,$requestData); } -class GenericRequestProcessor +class GenericInboxProcessor { protected $Inbox; protected $registeredActions = []; protected $validator; - private $processingTemplate = '/genericTemplates/confirm'; - private $processingTemplatesDirectory = ROOT . '/libraries/default/RequestProcessors/templates'; + protected $processingTemplate = '/genericTemplates/confirm'; + protected $processingTemplatesDirectory = ROOT . '/libraries/default/InboxProcessors/templates'; public function __construct($registerActions=false) { $this->Inbox = TableRegistry::getTableLocator()->get('Inbox'); if ($registerActions) { $this->registerActionInProcessor(); } + $this->assignProcessingTemplate(); + } + + private function assignProcessingTemplate() + { $processingTemplatePath = $this->getProcessingTemplatePath(); $file = new File($this->processingTemplatesDirectory . DS . $processingTemplatePath); if ($file->exists()) { @@ -33,6 +38,10 @@ class GenericRequestProcessor $file->close(); } + protected function updateProcessingTemplate($request) + { + } + public function getRegisteredActions() { return $this->registeredActions; @@ -41,14 +50,16 @@ class GenericRequestProcessor { return $this->scope; } - - private function getProcessingTemplatePath() + public function getDescription() + { + return $this->description ?? ''; + } + + protected function getProcessingTemplatePath() { - $class = str_replace('RequestProcessor', '', get_parent_class($this)); - $action = strtolower(str_replace('Processor', '', get_class($this))); return sprintf('%s/%s.php', - $class, - $action + $this->scope, + $this->action ); } @@ -57,15 +68,17 @@ class GenericRequestProcessor return $this->processingTemplate; } - public function render($request=[]) + public function render($request=[], Cake\Http\ServerRequest $serverRequest) { - $processingTemplate = $this->getProcessingTemplate(); $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(); } @@ -141,7 +154,7 @@ class GenericRequestProcessor if ($controller->ParamHandler->isRest()) { $response = $controller->RestResponse->viewData($processResult, 'json'); } else if ($controller->ParamHandler->isAjax()) { - $response = $controller->RestResponse->ajaxSuccessResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message); + $response = $controller->RestResponse->ajaxSuccessResponse('InboxProcessor', "{$scope}.{$action}", $processResult['data'], $message); } else { $controller->Flash->success($message); if (!is_null($redirect)) { @@ -155,7 +168,7 @@ class GenericRequestProcessor if ($controller->ParamHandler->isRest()) { $response = $controller->RestResponse->viewData($processResult, 'json'); } else if ($controller->ParamHandler->isAjax()) { - $response = $controller->RestResponse->ajaxFailResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message, $processResult['errors']); + $response = $controller->RestResponse->ajaxFailResponse('InboxProcessor', "{$scope}.{$action}", $processResult['data'], $message, $processResult['errors']); } else { $controller->Flash->error($message); if (!is_null($redirect)) { @@ -180,7 +193,7 @@ class GenericRequestProcessor $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/InboxProcessors/LocalToolInboxProcessor.php b/libraries/default/InboxProcessors/LocalToolInboxProcessor.php new file mode 100644 index 0000000..8852f9e --- /dev/null +++ b/libraries/default/InboxProcessors/LocalToolInboxProcessor.php @@ -0,0 +1,418 @@ +Broods = TableRegistry::getTableLocator()->get('Broods'); + $this->LocalTools = TableRegistry::getTableLocator()->get('LocalTools'); + } + + public function create($requestData) + { + return parent::create($requestData); + } + + protected function updateProcessingTemplate($request) + { + $connectorName = $request->connector['connector']; + $processingTemplatePath = sprintf('%s/%s/%s.php', $this->scope, $connectorName, $this->action); + $file = new File($this->processingTemplatesDirectory . DS . $processingTemplatePath); + if ($file->exists()) { + $this->processingTemplate = str_replace('.php', '', $processingTemplatePath); + } + $file->close(); + } + + protected function validateConnectorName($requestData) + { + if (empty($requestData['data']['connectorName'])) { + throw new NotFoundException('Error while validating request data. Connector name is missing.'); + } + $connector = $this->getConnectorFromClassname($requestData['data']['connectorName']); + if (is_null($connector)) { + throw new NotFoundException(__('Error while validating request data. Unkown connector `{0}`', $requestData['data']['connectorName'])); + } + } + + protected function getIssuerBrood($request) + { + $brood = $this->Broods->find() + ->where(['url' => $request['origin']]) + ->first(); + return $brood; + } + + protected function getConnection($requestData) + { + $local_tool_id = $requestData['remote_tool_id']; // local_tool_id is actually the remote_tool_id for the sender + $connection = $this->LocalTools->find()->where(['id' => $local_tool_id])->first(); + return $connection; + } + + protected function filterAlignmentsForBrood($individual, $brood) + { + foreach ($individual->alignments as $i => $alignment) { + if ($alignment->organisation_id != $brood->organisation_id) { + unset($individual->alignments[$i]); + } + } + return $individual; + } + + protected function getConnector($request) + { + try { + $connectorClasses = $this->LocalTools->getConnectors($request->local_tool_connector_name); + if (!empty($connectorClasses)) { + $connector = array_values($connectorClasses)[0]; + } + } catch (NotFoundException $e) { + $connector = null; + } + return $connector; + } + + protected function getConnectorMeta($request) + { + try { + $className = $request->local_tool_connector_name; + $connector = $this->getConnectorFromClassname($className); + $connectorMeta = $this->LocalTools->extractMeta([$className => $connector])[0]; + } catch (NotFoundException $e) { + $connectorMeta = []; + } + return $connectorMeta; + } + + protected function getConnectorFromClassname($className) + { + try { + $connectorClasses = $this->LocalTools->getConnectors($className); + if (!empty($connectorClasses)) { + $connector = array_values($connectorClasses)[0]; + } + } catch (NotFoundException $e) { + $connector = null; + } + return $connector; + } + + protected function getConnectorMetaFromClassname($className) + { + try { + $connector = $this->getConnectorFromClassname($className); + $connectorMeta = $this->LocalTools->extractMeta([$className => $connector])[0]; + } catch (NotFoundException $e) { + $connectorMeta = []; + } + return $connectorMeta; + } + + protected function attachRequestAssociatedData($request) + { + $request->brood = $this->getIssuerBrood($request); + $request->connector = $this->getConnectorMeta($request); + $request->individual = $request->user->individual; + $request->individual = $this->filterAlignmentsForBrood($request->individual, $request->brood); + return $request; + } + + protected function genBroodParam($remoteCerebrate, $connection, $connector, $requestData) + { + $local_tool_id = $requestData['remote_tool_id']; // local_tool_id is actually the remote_tool_id for the sender + $remote_tool_id = $requestData['local_tool_id']; // remote_tool_id is actually the local_tool_id for the sender + $remote_org = $this->Broods->Organisations->find()->where(['id' => $remoteCerebrate->organisation_id])->first(); + return [ + 'remote_tool' => [ + 'id' => $remote_tool_id, + 'connector' => $connector->connectorName, + 'name' => $requestData['tool_name'], + ], + 'remote_org' => $remote_org, + 'remote_tool_data' => $requestData, + 'remote_cerebrate' => $remoteCerebrate, + 'connection' => $connection, + 'connector' => [$connector->connectorName => $connector], + ]; + } + + protected function addBaseValidatorRules($validator) + { + return $validator + ->requirePresence('connectorName') + ->notEmpty('connectorName', 'The connector name must be provided') + ->requirePresence('cerebrateURL') + ->notEmpty('cerebrateURL', 'A url must be provided') + ->requirePresence('local_tool_id') + ->numeric('local_tool_id', 'A local_tool_id must be provided') + ->requirePresence('remote_tool_id') + ->numeric('remote_tool_id', 'A remote_tool_id must be provided'); + // ->add('url', 'validFormat', [ + // 'rule' => 'url', + // 'message' => 'URL must be valid' + // ]); + } +} + +class IncomingConnectionRequestProcessor extends LocalToolInboxProcessor implements GenericInboxProcessorActionI { + public $action = 'IncomingConnectionRequest'; + protected $description; + + public function __construct() { + parent::__construct(); + $this->description = __('Handle Phase I of inter-connection when another cerebrate instance performs the request.'); + } + + protected function addValidatorRules($validator) + { + return $this->addBaseValidatorRules($validator); + } + + public function create($requestData) { + $this->validateConnectorName($requestData); + $this->validateRequestData($requestData); + $connectorMeta = $this->getConnectorMetaFromClassname($requestData['data']['connectorName']); + $requestData['title'] = __('Request for {0} Inter-connection', $connectorMeta['name']); + return parent::create($requestData); + } + + public function getViewVariables($request) + { + $request = $this->attachRequestAssociatedData($request); + return [ + 'request' => $request, + 'progressStep' => 0, + ]; + } + + public function process($id, $requestData, $inboxRequest) + { + /** + * /!\ Should how should sent message be? be fire and forget? Only for delined? + */ + $interConnectionResult = []; + $remoteCerebrate = $this->getIssuerBrood($inboxRequest); + $connector = $this->getConnector($inboxRequest); + if (!empty($requestData['is_discard'])) { // -> declined + $connectorResult = $this->declineConnection($connector, $remoteCerebrate, $inboxRequest['data']); // Fire-and-forget? + $connectionSuccessfull = !empty($connectorResult['success']); + $resultTitle = __('Could not sent declined message to `{0}`\'s for {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']); + $errors = []; + if ($connectionSuccessfull) { + $resultTitle = __('Declined message successfully sent to `{0}`\'s for {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']); + $this->discard($id, $inboxRequest); + } + } else { + $errors = []; + $connectorResult = []; + $thrownErrorMessage = ''; + try { + $connectorResult = $this->acceptConnection($connector, $remoteCerebrate, $inboxRequest['data']); + $connectionSuccessfull = !empty($connectorResult['success']); + } catch (\Throwable $th) { + $connectionSuccessfull = false; + $thrownErrorMessage = $th->getMessage(); + } + $resultTitle = $connectorResult['message'] ?? __('Could not inter-connect `{0}`\'s {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']); + $errors = $connectorResult['errors'] ?? $thrownErrorMessage; + if ($connectionSuccessfull) { + $resultTitle = __('Interconnection for `{0}`\'s {1} created', $inboxRequest['origin'], $inboxRequest['local_tool_name']); + } + if ($connectionSuccessfull || !empty($connectorResult['placed_in_outbox'])) { + $this->discard($id, $inboxRequest); + } + } + return $this->genActionResult( + $connectorResult, + $connectionSuccessfull, + $resultTitle, + $errors + ); + } + + public function discard($id, $requestData) + { + return parent::discard($id, $requestData); + } + + protected function acceptConnection($connector, $remoteCerebrate, $requestData) + { + $connection = $this->getConnection($requestData); + $params = $this->genBroodParam($remoteCerebrate, $connection, $connector, $requestData); + $connectorResult = $connector->acceptConnectionWrapper($params); + $response = $this->sendAcceptedRequestToRemote($params, $connectorResult); + return $response; + } + + protected function declineConnection($connector, $remoteCerebrate, $requestData) + { + $connection = $this->getConnection($requestData); + $params = $this->genBroodParam($remoteCerebrate, $connection, $connector, $requestData); + $connectorResult = $connector->declineConnectionWrapper($params); + $response = $this->sendDeclinedRequestToRemote($params, $connectorResult); + return $response; + } + + protected function sendAcceptedRequestToRemote($params, $connectorResult) + { + $response = $this->Broods->sendLocalToolAcceptedRequest($params, $connectorResult); + return $response; + } + + protected function sendDeclinedRequestToRemote($remoteCerebrate, $connectorResult) + { + $response = $this->Broods->sendLocalToolDeclinedRequest($params, $connectorResult); + return $response; + } +} + +class AcceptedRequestProcessor extends LocalToolInboxProcessor implements GenericInboxProcessorActionI { + public $action = 'AcceptedRequest'; + protected $description; + + public function __construct() { + parent::__construct(); + $this->description = __('Handle Phase II of inter-connection when initial request has been accepted by the remote cerebrate.'); + } + + protected function addValidatorRules($validator) + { + return $this->addBaseValidatorRules($validator); + } + + public function create($requestData) { + $this->validateConnectorName($requestData); + $this->validateRequestData($requestData); + $connectorMeta = $this->getConnectorMetaFromClassname($requestData['data']['connectorName']); + $requestData['title'] = __('Inter-connection for {0} has been accepted', $connectorMeta['name']); + return parent::create($requestData); + } + + public function getViewVariables($request) + { + $request = $this->attachRequestAssociatedData($request); + return [ + 'request' => $request, + 'progressStep' => 1, + ]; + } + + public function process($id, $requestData, $inboxRequest) + { + $connector = $this->getConnector($inboxRequest); + $remoteCerebrate = $this->getIssuerBrood($inboxRequest); + + $errors = []; + $connectorResult = []; + $thrownErrorMessage = ''; + try { + $connectorResult = $this->finaliseConnection($connector, $remoteCerebrate, $inboxRequest['data']); + $connectionSuccessfull = !empty($connectorResult['success']); + } catch (\Throwable $th) { + $connectionSuccessfull = false; + $errors = $th->getMessage(); + } + $resultTitle = __('Could not finalise inter-connection for `{0}`\'s {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']); + $errors = $connectorResult['errors'] ?? $thrownErrorMessage; + if ($connectionSuccessfull) { + $resultTitle = __('Interconnection for `{0}`\'s {1} finalised', $inboxRequest['origin'], $inboxRequest['local_tool_name']); + $this->discard($id, $requestData); + } + return $this->genActionResult( + $connectorResult, + $connectionSuccessfull, + $resultTitle, + $errors + ); + } + + public function discard($id, $requestData) + { + return parent::discard($id, $requestData); + } + + protected function finaliseConnection($connector, $remoteCerebrate, $requestData) + { + $connection = $this->getConnection($requestData); + $params = $this->genBroodParam($remoteCerebrate, $connection, $connector, $requestData); + $connectorResult = $connector->finaliseConnectionWrapper($params); + return [ + 'success' => true + ]; + } +} + +class DeclinedRequestProcessor extends LocalToolInboxProcessor implements GenericInboxProcessorActionI { + public $action = 'DeclinedRequest'; + protected $description; + + public function __construct() { + parent::__construct(); + $this->description = __('Handle Phase II of MISP inter-connection when initial request has been declined by the remote cerebrate.'); + } + + protected function addValidatorRules($validator) + { + return $this->addBaseValidatorRules($validator); + } + + public function create($requestData) { + $this->validateConnectorName($requestData); + $this->validateRequestData($requestData); + $connectorMeta = $this->getConnectorMetaFromClassname($requestData['data']['connectorName']); + $requestData['title'] = __('Declined inter-connection for {0}', $connectorMeta['name']); + return parent::create($requestData); + } + + public function getViewVariables($request) + { + $request = $this->attachRequestAssociatedData($request); + return [ + 'request' => $request, + 'progressStep' => 1, + 'progressVariant' => 'danger', + 'steps' => [ + 1 => ['icon' => 'times', 'text' => __('Request Declined'), 'confirmButton' => __('Clean-up')], + 2 => ['icon' => 'trash', 'text' => __('Clean-up')], + ] + ]; + } + + public function process($id, $requestData, $inboxRequest) + { + $connectionSuccessfull = false; + $interConnectionResult = []; + if ($connectionSuccessfull) { + $this->discard($id, $requestData); + } + return $this->genActionResult( + $interConnectionResult, + $connectionSuccessfull, + $connectionSuccessfull ? __('Interconnection for `{0}`\'s {1} finalised', $requestData['origin'], $requestData['local_tool_name']) : __('Could not inter-connect `{0}`\'s {1}', $requestData['origin'], $requestData['local_tool_name']), + [] + ); + } + public function discard($id, $requestData) + { + return parent::discard($id, $requestData); + } +} diff --git a/libraries/default/RequestProcessors/ProposalRequestProcessor.php b/libraries/default/InboxProcessors/ProposalInboxProcessor.php similarity index 82% rename from libraries/default/RequestProcessors/ProposalRequestProcessor.php rename to libraries/default/InboxProcessors/ProposalInboxProcessor.php index 3421edf..01b6bb7 100644 --- a/libraries/default/RequestProcessors/ProposalRequestProcessor.php +++ b/libraries/default/InboxProcessors/ProposalInboxProcessor.php @@ -1,9 +1,9 @@ Users->Individuals->newEntity([ diff --git a/libraries/default/InboxProcessors/templates/LocalTool/GenericRequest.php b/libraries/default/InboxProcessors/templates/LocalTool/GenericRequest.php new file mode 100644 index 0000000..15ea91b --- /dev/null +++ b/libraries/default/InboxProcessors/templates/LocalTool/GenericRequest.php @@ -0,0 +1,147 @@ + __('Request Sent'), + 'icon' => 'paper-plane', + 'title' => __(''), + 'confirmButton' => __('Accept Request'), + 'canDiscard' => true, + ], + [ + 'text' => __('Request Accepted'), + 'icon' => 'check-square', + 'title' => __(''), + 'confirmButton' => __('Finalise Connection') + ], + [ + 'text' => __('Connection Done'), + 'icon' => 'exchange-alt', + 'title' => __(''), + ] +]; + +$footerButtons = []; + +$progressVariant = !empty($progressVariant) ? $progressVariant : 'info'; +$finalSteps = array_replace($defaultSteps, $steps ?? []); +$currentStep = $finalSteps[$progressStep]; +$progress = $this->Bootstrap->progressTimeline([ + 'variant' => $progressVariant, + 'selected' => !empty($progressStep) ? $progressStep : 0, + 'steps' => $finalSteps, +]); + +$footerButtons[] = [ + 'clickFunction' => 'cancel', + 'variant' => 'secondary', + 'text' => __('Cancel'), +]; +if (!empty($currentStep['canDiscard'])) { + $footerButtons[] = [ + 'clickFunction' => 'discard', + 'variant' => 'danger', + 'text' => __('Decline Request'), + ]; +} +$footerButtons[] = [ + 'clickFunction' => 'accept', + 'text' => $currentStep['confirmButton'] ?? __('Submit'), +]; + +$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' => 'connector', 'label' => __('Tool Name'), 'formatter' => function($connector, $row) { + return sprintf('%s', + $this->Url->build(['controller' => 'localTools', 'action' => 'viewConnector', $connector['name']]), + sprintf('%s (v%s)', h($connector['name']), h($connector['connector_version'])) + ); + }], + ['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()], +]); +$form = $this->element('genericElements/Form/genericForm', [ + 'entity' => null, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'model' => 'Inbox', + 'fields' => [], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] +]); +$localToolHTML = $this->fetch('content', sprintf('
%s
', $form));; + +$requestData = $this->Bootstrap->collapse( + [ + 'title' => __('Inter-connection data'), + 'open' => true, + ], + sprintf('
%s
', json_encode($request['data'], JSON_PRETTY_PRINT)) +); + +$bodyHtml = sprintf('
%s
%s
%s', + $table, + $requestData, + $localToolHTML +); + +echo $this->Bootstrap->modal([ + 'title' => __('Interconnection Request for {0}', h($request->local_tool_connector_name)), + 'size' => 'xl', + 'type' => 'custom', + 'bodyHtml' => sprintf('
%s
%s
', + $progress, + $bodyHtml + ), + 'footerButtons' => $footerButtons +]); + +?> + + \ No newline at end of file diff --git a/libraries/default/InboxProcessors/templates/LocalTool/MispConnector/IncomingConnectionRequest.php b/libraries/default/InboxProcessors/templates/LocalTool/MispConnector/IncomingConnectionRequest.php new file mode 100644 index 0000000..52bfd82 --- /dev/null +++ b/libraries/default/InboxProcessors/templates/LocalTool/MispConnector/IncomingConnectionRequest.php @@ -0,0 +1,21 @@ +extend('LocalTool/GenericRequest'); +$form = $this->element('genericElements/Form/genericForm', [ + 'entity' => null, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'model' => 'Inbox', + 'fields' => [ + [ + 'field' => 'is_discard', + 'type' => 'checkbox', + 'default' => false + ] + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] +]); +echo sprintf('
%s
', $form); \ No newline at end of file diff --git a/libraries/default/RequestProcessors/templates/User/registration.php b/libraries/default/InboxProcessors/templates/User/Registration.php similarity index 100% rename from libraries/default/RequestProcessors/templates/User/registration.php rename to libraries/default/InboxProcessors/templates/User/Registration.php diff --git a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php new file mode 100644 index 0000000..946e548 --- /dev/null +++ b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php @@ -0,0 +1,137 @@ +Broods->find() + ->where(['id' => $broodId]) + ->first(); + return $brood; + } + + protected function getLocalTool($toolId) + { + $tool = $this->LocalTools->find() + ->where(['id' => $toolId]) + ->first(); + return $tool; + } + + protected function getConnector($className) + { + try { + $connectorClasses = $this->LocalTools->getConnectors($className); + if (!empty($connectorClasses)) { + $connector = array_values($connectorClasses)[0]; + } + } catch (NotFoundException $e) { + $connector = null; + } + return $connector; + } + + protected function setRemoteToolConnectionStatus(Object $brood, Object $outboxRequest, String $status): void + { + $connector = $this->getConnector($outboxRequest->data['remote_tool']['connector']); + $connection = $this->getLocalTool($outboxRequest->data['local_tool_id']); + $connectorParams = [ + 'connection' => $connection, + 'remote_tool' => $outboxRequest->data['remote_tool'], + 'remote_cerebrate' => $brood, + ]; + $connector->remoteToolConnectionStatus($connectorParams, constant(get_class($connector) . '::' . $status)); + } +} + +class ResendFailedMessageProcessor extends BroodsOutboxProcessor implements GenericOutboxProcessorActionI { + 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) + { + $brood = $this->getIssuerBrood((int) $outboxRequest->data['brood_id']); + if (!empty($requestData['is_delete'])) { // -> declined + $success = true; + $messageSucess = __('Message successfully deleted'); + $messageFail = ''; + $this->setRemoteToolConnectionStatus($brood, $outboxRequest, 'STATE_CANCELLED'); + } else { + $url = $outboxRequest->data['url']; + $dataSent = $outboxRequest->data['sent']; + $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->setRemoteToolConnectionStatus($brood, $outboxRequest, $outboxRequest->data['next_connector_state']); + } else { + $this->setRemoteToolConnectionStatus($brood, $outboxRequest, 'STATE_SENDING_ERROR'); + } + } + 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..c230465 --- /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..8c5a506 --- /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..9962b15 --- /dev/null +++ b/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php @@ -0,0 +1,143 @@ + '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); + +$messageSent = $this->Bootstrap->card([ + 'headerHTML' => __('Message Sent'), + 'bodyHTML' => sprintf('%s%s', $table2, $requestData), + 'bodyClass' => 'p-2', +]); + +$bodyHtml = sprintf('
%s
%s
%s
%s', + $tools, + $table, + $messageSent, + $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/libraries/default/RequestProcessors/BroodRequestProcessor.php b/libraries/default/RequestProcessors/BroodRequestProcessor.php deleted file mode 100644 index 13a0c33..0000000 --- a/libraries/default/RequestProcessors/BroodRequestProcessor.php +++ /dev/null @@ -1,108 +0,0 @@ -description = __('Handle tool interconnection request from other cerebrate instance'); - $this->Broods = TableRegistry::getTableLocator()->get('Broods'); - } - - protected function addValidatorRules($validator) - { - return $validator; - } - - public function create($requestData) { - $this->validateRequestData($requestData); - $requestData['title'] = __('Cerebrate instance {0} requested interconnection for tool {1}', 'Insert brood name', 'Insert tool name'); - return parent::create($requestData); - } - - public function process($id, $requestData) - { - $connectionSuccessfull = false; - $interConnectionResult = []; - if ($connectionSuccessfull) { - $this->discard($id, $requestData); - } - return $this->genActionResult( - $interConnectionResult, - $connectionSuccessfull, - $connectionSuccessfull ? __('Interconnection for `{0}` created', 'Insert tool name') : __('Could interconnect tool `{0}`.', 'Insert tool name'), - [] - ); - } - - public function discard($id, $requestData) - { - return parent::discard($id, $requestData); - } -} - -class OneWaySynchronizationProcessor extends BroodRequestProcessor implements GenericProcessorActionI { - public $action = 'OneWaySynchronization'; - protected $description; - - public function __construct() { - parent::__construct(); - $this->description = __('Handle cerebrate connection request for another cerebrate instance'); - $this->Broods = TableRegistry::getTableLocator()->get('Broods'); - } - - protected function addValidatorRules($validator) - { - return $validator; - } - - public function create($requestData) { - $this->validateRequestData($requestData); - $requestData['title'] = __('Cerebrate instance {0} requested interconnection', 'Insert cerebrate name'); - return parent::create($requestData); - } - - public function process($id, $requestData) - { - $connectionSuccessfull = false; - $interConnectionResult = []; - if ($connectionSuccessfull) { - $this->discard($id, $requestData); - } - return $this->genActionResult( - $interConnectionResult, - $connectionSuccessfull, - $connectionSuccessfull ? __('Interconnection with `{0}` created', 'Insert cerebrate name') : __('Could interconnect with `{0}`.', 'Insert cerebrate name'), - [] - ); - } - - public function discard($id, $requestData) - { - return parent::discard($id, $requestData); - } -} \ No newline at end of file diff --git a/libraries/default/meta_fields/enisa-csirt-inventory.json b/libraries/default/meta_fields/enisa-csirt-inventory.json new file mode 100644 index 0000000..9171a5c --- /dev/null +++ b/libraries/default/meta_fields/enisa-csirt-inventory.json @@ -0,0 +1,74 @@ +{ + "description": "Template based on the ENISA's CSIRTs inventory", + "metaFields": [ + { + "field": "ISO 3166-1 Code", + "type": "text", + "regex": "[a-z]{2,3}" + }, + { + "field": "website", + "type": "text", + "regex": "(http(s)?:\\\/\\\/.)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&\/\/=]*)" + }, + { + "field": "enisa-geo-group", + "type": "text" + }, + { + "field": "is-approved", + "type": "boolean" + }, + { + "field": "first-member-type", + "type": "text" + }, + { + "field": "team-name", + "type": "text" + }, + { + "field": "oes-coverage", + "type": "text" + }, + { + "field": "enisa-tistatus", + "type": "text" + }, + { + "field": "csirt-network-status", + "type": "text" + }, + { + "field": "constituency", + "type": "text" + }, + { + "field": "establishment", + "type": "text" + }, + { + "field": "email", + "type": "text", + "regex": "(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])" + }, + { + "field": "country-name", + "type": "text" + }, + { + "field": "short-team-name", + "type": "text" + }, + { + "field": "key", + "type": "text" + } + ], + "name": "ENISA CSIRT Network", + "namespace": "cnw", + "scope": "organisation", + "source": "enisa.europa.eu/topics/csirts-in-europe/csirt-inventory/certs-by-country-interactive-map", + "uuid": "089c68c7-d97e-4f21-a798-159cd10f7864", + "version": 1 +} \ No newline at end of file diff --git a/src/Application.php b/src/Application.php index 8eb1704..2da0df1 100644 --- a/src/Application.php +++ b/src/Application.php @@ -21,6 +21,7 @@ use Cake\Core\Exception\MissingPluginException; use Cake\Error\Middleware\ErrorHandlerMiddleware; use Cake\Http\BaseApplication; use Cake\Http\MiddlewareQueue; +use Cake\Http\Middleware\BodyParserMiddleware; use Cake\Routing\Middleware\AssetMiddleware; use Cake\Routing\Middleware\RoutingMiddleware; use Authentication\AuthenticationService; @@ -28,7 +29,6 @@ use Authentication\AuthenticationServiceInterface; use Authentication\AuthenticationServiceProviderInterface; use Authentication\Middleware\AuthenticationMiddleware; use Psr\Http\Message\ServerRequestInterface; - /** * Application setup class. * @@ -87,7 +87,8 @@ class Application extends BaseApplication implements AuthenticationServiceProvid // using it's second constructor argument: // `new RoutingMiddleware($this, '_cake_routes_')` ->add(new RoutingMiddleware($this)) - ->add(new AuthenticationMiddleware($this)); + ->add(new AuthenticationMiddleware($this)) + ->add(new BodyParserMiddleware()); return $middlewareQueue; } diff --git a/src/Command/ImporterCommand.php b/src/Command/ImporterCommand.php index 30cdc3d..c79ba76 100644 --- a/src/Command/ImporterCommand.php +++ b/src/Command/ImporterCommand.php @@ -161,7 +161,7 @@ class ImporterCommand extends Command 'valueField' => 'id' ])->where(['meta_template_id' => $metaTemplate->id])->toArray(); } else { - $this->io->error("Unkown template for UUID $metaTemplateUUID"); + $this->io->error("Unkown template for UUID {$config['metaTemplateUUID']}"); die(1); } } diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 70116d9..5d49631 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -91,6 +91,9 @@ class AppController extends Controller $this->loadModel('Users'); $this->Users->checkForNewInstance(); $this->authApiUser(); + if ($this->ParamHandler->isRest()) { + $this->Security->setConfig('unlockedActions', [$this->request->getParam('action')]); + } $this->ACL->setPublicInterfaces(); if (!empty($this->request->getAttribute('identity'))) { $user = $this->Users->get($this->request->getAttribute('identity')->getIdentifier(), [ @@ -112,6 +115,10 @@ class AppController extends Controller $this->Security->setConfig('validatePost', false); } $this->Security->setConfig('unlockedActions', ['index']); + if ($this->ParamHandler->isRest()) { + $this->Security->setConfig('unlockedActions', [$this->request->getParam('action')]); + $this->Security->setConfig('validatePost', false); + } $this->ACL->checkAccess(); $this->set('menu', $this->ACL->getMenu()); diff --git a/src/Controller/BroodsController.php b/src/Controller/BroodsController.php index d954f77..94afeda 100644 --- a/src/Controller/BroodsController.php +++ b/src/Controller/BroodsController.php @@ -156,10 +156,31 @@ class BroodsController extends AppController } } + public function downloadSharingGroup($brood_id, $sg_id) + { + $result = $this->Broods->downloadSharingGroup($brood_id, $sg_id, $this->ACL->getUser()['id']); + $success = __('Sharing group fetched from remote.'); + $fail = __('Could not save the remote sharing group'); + if ($this->ParamHandler->isRest()) { + if ($result) { + return $this->RestResponse->saveSuccessResponse('Brood', 'downloadSharingGroup', $brood_id, 'json', $success); + } else { + return $this->RestResponse->saveFailResponse('Brood', 'downloadSharingGroup', $brood_id, $fail, 'json'); + } + } else { + if ($result) { + $this->Flash->success($success); + } else { + $this->Flash->error($fail); + } + $this->redirect($this->referer()); + } + } + public function interconnectTools() { - $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); - $processor = $this->requestProcessor->getProcessor('Brood', 'ToolInterconnection'); + $this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); + $processor = $this->InboxProcessors->getProcessor('Brood', 'ToolInterconnection'); $data = [ 'origin' => '127.0.0.1', 'comment' => 'Test comment', diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index eec94ce..8367536 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -650,18 +650,56 @@ 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 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 ] ] ], @@ -722,7 +760,17 @@ class ACLComponent extends Component 'skipTopMenu' => 1 ] ] - ] + ], + 'Instance' => [ + __('Instance'), + 'url' => '/instance/home', + 'children' => [ + 'migration' => [ + 'url' => '/instance/migrationIndex', + 'label' => __('Database migration') + ] + ] + ], ], 'Cerebrate' => [ 'Roles' => [ @@ -756,10 +804,6 @@ class ACLComponent extends Component 'url' => '/instance/home', 'label' => __('Home') ], - 'migration' => [ - 'url' => '/instance/migrationIndex', - 'label' => __('Database migration') - ] ] ], 'Users' => [ diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 31de16c..86bb1ec 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -7,9 +7,12 @@ use Cake\Error\Debugger; use Cake\Utility\Hash; use Cake\Utility\Inflector; use Cake\View\ViewBuilder; +use Cake\Http\Exception\MethodNotAllowedException; +use Cake\Http\Exception\NotFoundException; class CRUDComponent extends Component { + protected $components = ['RestResponse']; public function initialize(array $config): void { @@ -58,7 +61,7 @@ class CRUDComponent extends Component $data = $this->Table->{$options['afterFind']}($data); } } - $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); + $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); } else { $this->Controller->loadComponent('Paginator'); $data = $this->Controller->Paginator->paginate($query); @@ -126,7 +129,8 @@ class CRUDComponent extends Component } if ($this->request->is('post')) { $patchEntityParams = [ - 'associated' => [] + 'associated' => [], + 'accessibleFields' => $data->getAccessibleFieldForNew(), ]; if (!empty($params['id'])) { unset($params['id']); @@ -147,9 +151,9 @@ class CRUDComponent extends Component } else if ($this->Controller->ParamHandler->isAjax()) { if (!empty($params['displayOnSuccess'])) { $displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]); - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message, ['displayOnSuccess' => $displayOnSuccess]); + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message, ['displayOnSuccess' => $displayOnSuccess]); } else { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message); + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message); } } else { $this->Controller->Flash->success($message); @@ -168,8 +172,9 @@ class CRUDComponent extends Component empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage) ); if ($this->Controller->ParamHandler->isRest()) { + $this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json'); } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'add', $data, $message, $validationMessage); + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'add', $data, $message, $validationMessage); } else { $this->Controller->Flash->error($message); } @@ -246,7 +251,7 @@ class CRUDComponent extends Component if ($this->Controller->ParamHandler->isRest()) { $this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json'); } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'edit', $savedData, $message); + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'edit', $savedData, $message); } else { $this->Controller->Flash->success($message); if (empty($params['redirect'])) { @@ -263,7 +268,7 @@ class CRUDComponent extends Component ); if ($this->Controller->ParamHandler->isRest()) { } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'edit', $data, $message, $data->getErrors()); + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'edit', $data, $message, $data->getErrors()); } else { $this->Controller->Flash->error($message); } @@ -333,38 +338,122 @@ class CRUDComponent extends Component $data = $params['afterFind']($data); } if ($this->Controller->ParamHandler->isRest()) { - $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); + $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); } $this->Controller->set('entity', $data); } - public function delete(int $id): void + public function delete($id=false): void { - if (empty($id)) { - throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); - } - $data = $this->Table->get($id); - if ($this->request->is('post') || $this->request->is('delete')) { - if ($this->Table->delete($data)) { - $message = __('{0} deleted.', $this->ObjectAlias); - if ($this->Controller->ParamHandler->isRest()) { - $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); - } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'delete', $data, $message); - } else { - $this->Controller->Flash->success($message); - $this->Controller->redirect($this->Controller->referer()); + if ($this->request->is('get')) { + if(!empty($id)) { + $data = $this->Table->get($id); + $this->Controller->set('id', $data['id']); + $this->Controller->set('data', $data); + $this->Controller->set('bulkEnabled', false); + } else { + $this->Controller->set('bulkEnabled', true); + } + } else if ($this->request->is('post') || $this->request->is('delete')) { + $ids = $this->getIdsOrFail($id); + $isBulk = count($ids) > 1; + $bulkSuccesses = 0; + foreach ($ids as $id) { + $data = $this->Table->get($id); + $success = $this->Table->delete($data); + $success = true; + if ($success) { + $bulkSuccesses++; } } + $message = $this->getMessageBasedOnResult( + $bulkSuccesses == count($ids), + $isBulk, + __('{0} deleted.', $this->ObjectAlias), + __('All {0} have been deleted.', Inflector::pluralize($this->ObjectAlias)), + __('Could not delete {0}.', $this->ObjectAlias), + __('{0} / {1} {2} have been deleted.', + $bulkSuccesses, + count($ids), + Inflector::pluralize($this->ObjectAlias) + ) + ); + $this->setResponseForController('delete', $bulkSuccesses, $message, $data); } $this->Controller->set('metaGroup', 'ContactDB'); $this->Controller->set('scope', 'users'); - $this->Controller->set('id', $data['id']); - $this->Controller->set('data', $data); $this->Controller->viewBuilder()->setLayout('ajax'); $this->Controller->render('/genericTemplates/delete'); } + public function setResponseForController($action, $success, $message, $data=[], $errors=null) + { + if ($success) { + if ($this->Controller->ParamHandler->isRest()) { + $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); + } elseif ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, $action, $data, $message); + } else { + $this->Controller->Flash->success($message); + $this->Controller->redirect($this->Controller->referer()); + } + } else { + if ($this->Controller->ParamHandler->isRest()) { + $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); + } elseif ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, $action, $data, $message, !is_null($errors) ? $errors : $data->getErrors()); + } else { + $this->Controller->Flash->error($message); + $this->Controller->redirect($this->Controller->referer()); + } + } + } + + private function getMessageBasedOnResult($isSuccess, $isBulk, $messageSingleSuccess, $messageBulkSuccess, $messageSingleFailure, $messageBulkFailure) + { + if ($isSuccess) { + $message = $isBulk ? $messageBulkSuccess : $messageSingleSuccess; + } else { + $message = $isBulk ? $messageBulkFailure : $messageSingleFailure; + } + return $message; + } + + /** + * getIdsOrFail + * + * @param mixed $id + * @return Array The ID converted to a list or the list of provided IDs from the request + * @throws NotFoundException when no ID could be found + */ + public function getIdsOrFail($id=false): Array + { + $params = $this->Controller->ParamHandler->harvestParams(['ids']); + if (!empty($params['ids'])) { + $params['ids'] = json_decode($params['ids']); + } + $ids = []; + if (empty($id)) { + if (empty($params['ids'])) { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } + $ids = $params['ids']; + } else { + $id = $this->getInteger($id); + if (!is_null($id)) { + $ids = [$id]; + } else { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } + } + return $ids; + } + + private function getInteger($value) + { + return is_numeric($value) ? intval($value) : null; + } + protected function massageFilters(array $params): array { $massagedFilters = [ @@ -600,7 +689,7 @@ class CRUDComponent extends Component if ($this->Controller->ParamHandler->isRest()) { $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'toggle', $savedData, $message); + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'toggle', $savedData, $message); } else { $this->Controller->Flash->success($message); if (empty($params['redirect'])) { @@ -618,7 +707,7 @@ class CRUDComponent extends Component ); if ($this->Controller->ParamHandler->isRest()) { } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationMessage); + $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationMessage); } else { $this->Controller->Flash->error($message); if (empty($params['redirect'])) { @@ -644,10 +733,10 @@ class CRUDComponent extends Component if ($this->request->is('post')) { $data[$fieldName] = $data[$fieldName] ? true : false; $this->Table->save($data); - $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData(['value' => $data[$fieldName]], 'json'); + $this->Controller->restResponsePayload = $this->RestResponse->viewData(['value' => $data[$fieldName]], 'json'); } else { if ($this->Controller->ParamHandler->isRest()) { - $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData(['value' => $data[$fieldName]], 'json'); + $this->Controller->restResponsePayload = $this->RestResponse->viewData(['value' => $data[$fieldName]], 'json'); } else { $this->Controller->set('fieldName', $fieldName); $this->Controller->set('currentValue', $data[$fieldName]); diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php index 6c0f978..4b8a788 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -6,6 +6,7 @@ use App\Controller\AppController; use Cake\Database\Expression\QueryExpression; use Cake\Event\EventInterface; use Cake\ORM\TableRegistry; +use Cake\Utility\Inflector; use Cake\Utility\Hash; use Cake\Utility\Text; use Cake\Http\Exception\NotFoundException; @@ -32,7 +33,6 @@ class InboxController extends AppController 'contextFilters' => [ 'fields' => [ 'scope', - 'action', ] ], 'contain' => ['Users'] @@ -57,17 +57,46 @@ class InboxController extends AppController } } - public function delete($id) + public function delete($id=false) { - if ($this->request->is('post')) { - $request = $this->Inbox->get($id); - $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); - $processor = $this->requestProcessor->getProcessor($request->scope, $request->action); - $discardResult = $processor->discard($id, $request); - return $processor->genHTTPReply($this, $discardResult); + if ($this->request->is('post')) { // cannot rely on CRUD's delete as inbox's processor discard function is responsible to handle their messages + $ids = $this->CRUD->getIdsOrFail($id); + $discardSuccesses = 0; + $discardResults = []; + $discardErrors = []; + foreach ($ids as $id) { + $request = $this->Inbox->get($id, ['contain' => ['Users' => ['Individuals' => ['Alignments' => 'Organisations']]]]); + $this->inboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); + $processor = $this->inboxProcessors->getProcessor($request->scope, $request->action); + $discardResult = $processor->discard($id, $request); + $discardResults[] = $discardResult; + if ($discardResult['success']) { + $discardSuccesses++; + } else { + $discardErrors[] = $discardResult; + } + } + if (count($ids) == 1) { + return $processor->genHTTPReply($this, $discardResult); + } else { + $success = $discardSuccesses == count($ids); + $message = __('{0} {1} have been discarded.', + $discardSuccesses == count($ids) ? __('All') : sprintf('%s / %s', $discardSuccesses, count($ids)), + sprintf('%s %s', Inflector::singularize($this->Inbox->getAlias()), __('messages')) + ); + $this->CRUD->setResponseForController('delete', $success, $message, $discardResults, $discardResults); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } } $this->set('deletionTitle', __('Discard request')); - $this->set('deletionText', __('Are you sure you want to discard request #{0}?', $id)); + if (!empty($id)) { + $this->set('deletionText', __('Are you sure you want to discard request #{0}?', $id)); + } else { + $this->set('deletionText', __('Are you sure you want to discard the selected requests?')); + } $this->set('deletionConfirm', __('Discard')); $this->CRUD->delete($id); $responsePayload = $this->CRUD->getResponsePayload(); @@ -78,54 +107,78 @@ class InboxController extends AppController public function process($id) { - $request = $this->Inbox->get($id); + $request = $this->Inbox->get($id, ['contain' => ['Users' => ['Individuals' => ['Alignments' => 'Organisations']]]]); $scope = $request->scope; $action = $request->action; - $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); - $processor = $this->requestProcessor->getProcessor($request->scope, $request->action); + $this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); + if ($scope == 'LocalTool') { + $processor = $this->InboxProcessors->getLocalToolProcessor($action, $request->local_tool_name); + } else { + $processor = $this->InboxProcessors->getProcessor($scope, $action); + } if ($this->request->is('post')) { - $processResult = $processor->process($id, $this->request->getData()); + $processResult = $processor->process($id, $this->request->getData(), $request); return $processor->genHTTPReply($this, $processResult); } else { - $renderedView = $processor->render($request); + $renderedView = $processor->render($request, $this->request); return $this->response->withStringBody($renderedView); } } public function listProcessors() { - $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); - $requestProcessors = $this->requestProcessor->listProcessors(); + $this->inboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); + $processors = $this->inboxProcessors->listProcessors(); if ($this->ParamHandler->isRest()) { - return $this->RestResponse->viewData($requestProcessors, 'json'); + return $this->RestResponse->viewData($processors, 'json'); } $data = []; - foreach ($requestProcessors as $scope => $processors) { - foreach ($processors as $processor) { + foreach ($processors as $scope => $scopedProcessors) { + foreach ($scopedProcessors as $processor) { $data[] = [ 'enabled' => $processor->enabled, 'scope' => $scope, - 'action' => $processor->action + 'action' => $processor->action, + 'description' => isset($processor->getDescription) ? $processor->getDescription() : null, + 'notice' => $processor->notice ?? null, + 'error' => $processor->error ?? null, ]; } } - $this->set('title', 'Available request processors'); - $this->set('fields', [ - [ - 'name' => 'Enabled', - 'data_path' => 'enabled', - 'element' => 'boolean' - ], - [ - 'name' => 'Processor scope', - 'data_path' => 'scope', - ], - [ - 'name' => 'Processor action', - 'data_path' => 'action', - ] - ]); $this->set('data', $data); - $this->render('/genericTemplates/index_simple'); + } + + public function createEntry($scope, $action) + { + if (!$this->request->is('post')) { + throw new MethodNotAllowedException(__('Only POST method is accepted')); + } + $entryData = [ + 'origin' => $this->request->clientIp(), + 'user_id' => $this->ACL->getUser()['id'], + ]; + $entryData['data'] = $this->request->getData() ?? []; + $this->inboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); + if ($scope == 'LocalTool') { + $this->validateLocalToolRequestEntry($entryData); + $entryData['origin'] = $entryData['data']['cerebrateURL']; + $processor = $this->inboxProcessors->getLocalToolProcessor($action, $entryData['data']['connectorName']); + $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()), 'createEntry', [], $message, $errors); + } + } else { + $processor = $this->inboxProcessors->getProcessor($scope, $action); + } + $creationResult = $this->inboxProcessors->createInboxEntry($processor, $entryData); + return $processor->genHTTPReply($this, $creationResult); + } + + private function validateLocalToolRequestEntry($entryData) + { + if (empty($entryData['data']['connectorName']) || empty($entryData['data']['cerebrateURL'])) { + throw new MethodNotAllowedException(__('Could not create entry. Tool name or URL is missing')); + } } } diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php index 9d4a20c..00bb446 100644 --- a/src/Controller/LocalToolsController.php +++ b/src/Controller/LocalToolsController.php @@ -186,11 +186,9 @@ class LocalToolsController extends AppController }, 'afterFind' => function($data) { foreach ($data as $connector) { - $connector = [ - 'id' => $connector['id'], - 'name' => $connector['name'], - 'connector' => $connector['connector'] - ]; + $connectorById = $this->LocalTools->getConnectorByConnectionId($connector['id']); + $className = array_keys($connectorById)[0]; + $connector['connectorName'] = $className; } return $data; } @@ -205,6 +203,9 @@ class LocalToolsController extends AppController { $this->loadModel('Broods'); $tools = $this->Broods->queryLocalTools($id); + foreach ($tools as $k => $tool) { + $tools[$k]['local_tools'] = $this->LocalTools->appendLocalToolConnections($id, $tool); + } if ($this->ParamHandler->isRest()) { return $this->RestResponse->viewData($tools, 'json'); } @@ -219,18 +220,37 @@ class LocalToolsController extends AppController 'cerebrate_id' => $cerebrate_id, 'remote_tool_id' => $remote_tool_id ]; + $this->loadModel('Broods'); + $remoteCerebrate = $this->Broods->find()->where(['id' => $params['cerebrate_id']])->first(); if ($this->request->is(['post', 'put'])) { $postParams = $this->ParamHandler->harvestParams(['local_tool_id']); if (empty($postParams['local_tool_id'])) { throw new MethodNotAllowedException(__('No local tool ID supplied.')); } $params['local_tool_id'] = $postParams['local_tool_id']; - $result = $this->LocalTools->encodeConnection($params); - // Send message to remote inbox - debug($result); + $encodingResult = $this->LocalTools->encodeConnection($params); + $inboxResult = $encodingResult['inboxResult']; + if ($inboxResult['success']) { + if ($this->ParamHandler->isRest()) { + $response = $this->RestResponse->viewData($inboxResult, 'json'); + } else if ($this->ParamHandler->isAjax()) { + $response = $this->RestResponse->ajaxSuccessResponse('LocalTool', 'connectionRequest', [], $inboxResult['message']); + } else { + $this->Flash->success($inboxResult['message']); + $response = $this->redirect(['action' => 'broodTools', $cerebrate_id]); + } + } else { + if ($this->ParamHandler->isRest()) { + $response = $this->RestResponse->viewData($inboxResult, 'json'); + } else if ($this->ParamHandler->isAjax()) { + $response = $this->RestResponse->ajaxFailResponse('LocalTool', 'connectionRequest', [], $inboxResult['message'], $inboxResult['errors']); + } else { + $this->Flash->error($inboxResult['message']); + $response = $this->redirect($this->referer()); + } + } + return $response; } else { - $this->loadModel('Broods'); - $remoteCerebrate = $this->Broods->find()->where(['id' => $params['cerebrate_id']])->first(); $remoteTool = $this->LocalTools->getRemoteToolById($params); $local_tools = $this->LocalTools->encodeConnectionChoice($params); if (empty($local_tools)) { @@ -243,4 +263,34 @@ class LocalToolsController extends AppController ]); } } + + public function connectLocal($local_tool_id) + { + $params = [ + 'local_tool_id' => $local_tool_id + ]; + $local_tool = $this->LocalTools->fetchConnection($local_tool_id); + if ($this->request->is(['post', 'put'])) { + $postParams = $this->ParamHandler->harvestParams(['target_tool_id']); + if (empty($postParams['target_tool_id'])) { + throw new MethodNotAllowedException(__('No target tool ID supplied.')); + } + + $params['target_tool_id'] = $postParams['target_tool_id']; + $result = $this->LocalTools->encodeLocalConnection($params); + // Send message to remote inbox + debug($result); + } else { + $target_tools = $this->LocalTools->findConnectable($local_tool); + debug($target_tools); + if (empty($target_tools)) { + throw new NotFoundException(__('No tools found to connect.')); + } + $this->set('data', [ + 'remoteCerebrate' => $remoteCerebrate, + 'remoteTool' => $remoteTool, + 'local_tools' => $local_tools + ]); + } + } } diff --git a/src/Controller/OutboxController.php b/src/Controller/OutboxController.php new file mode 100644 index 0000000..946a9ea --- /dev/null +++ b/src/Controller/OutboxController.php @@ -0,0 +1,129 @@ +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=false) + { + $this->set('deletionTitle', __('Confirm message deletion')); + if (!empty($id)) { + $this->set('deletionText', __('Are you sure you want to delete message #{0}?', $id)); + } else { + $this->set('deletionText', __('Are you sure you want to delete the selected messages?')); + } + $this->set('deletionConfirm', __('Delete')); + $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/Controller/UsersController.php b/src/Controller/UsersController.php index 8a40151..9126e87 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -138,8 +138,8 @@ class UsersController extends AppController public function register() { - $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); - $processor = $this->requestProcessor->getProcessor('User', 'Registration'); + $this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); + $processor = $this->InboxProcessors->getProcessor('User', 'Registration'); $data = [ 'origin' => '127.0.0.1', 'comment' => 'Hi there!, please create an account', diff --git a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php index 13184c5..7e865c0 100644 --- a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php +++ b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php @@ -7,11 +7,19 @@ class CommonConnectorTools { public $description = ''; public $name = ''; + public $connectorName = ''; public $exposedFunctions = [ 'diagnostics' ]; public $version = '???'; + const STATE_INITIAL = 'Request issued'; + const STATE_ACCEPT = 'Request accepted'; + const STATE_CONNECTED = 'Connected'; + const STATE_SENDING_ERROR = 'Error while sending request'; + const STATE_CANCELLED = 'Request cancelled'; + const STATE_DECLINED = 'Request declined by remote'; + public function addExposedFunction(string $functionName): void { $this->exposedFunctions[] = $functionName; @@ -49,11 +57,57 @@ class CommonConnectorTools return true; } - public function encodeConnection(array $params): array + public function remoteToolConnectionStatus(array $params, string $status): void { - $result = $this->encodeConnection($params); + $remoteToolConnections = \Cake\ORM\TableRegistry::getTableLocator()->get('RemoteToolConnections'); + $remoteToolConnection = $remoteToolConnections->find()->where( + [ + 'local_tool_id' => $params['connection']['id'], + 'remote_tool_id' => $params['remote_tool']['id'], + 'brood_id' => $params['remote_cerebrate']['id'] + ] + )->first(); + if (empty($remoteToolConnection)) { + $data = $remoteToolConnections->newEmptyEntity(); + $entry = [ + 'local_tool_id' => $params['connection']['id'], + 'remote_tool_id' => $params['remote_tool']['id'], + 'remote_tool_name' => $params['remote_tool']['name'], + 'brood_id' => $params['remote_cerebrate']['id'], + 'name' => '', + 'settings' => '', + 'status' => $status, + 'created' => time(), + 'modified' => time() + ]; + $data = $remoteToolConnections->patchEntity($data, $entry); + $remoteToolConnections->save($data); + } else { + $data = $remoteToolConnections->patchEntity($remoteToolConnection, ['status' => $status, 'modified' => time()]); + $remoteToolConnections->save($data); + } + } + + public function initiateConnectionWrapper(array $params): array + { + $result = $this->initiateConnection($params); + $this->remoteToolConnectionStatus($params, self::STATE_INITIAL); return $result; } + + public function acceptConnectionWrapper(array $params): array + { + $result = $this->acceptConnection($params); + $this->remoteToolConnectionStatus($params, self::STATE_ACCEPT); + return $result; + } + + public function finaliseConnectionWrapper(array $params): bool + { + $result = $this->finaliseConnection($params); + $this->remoteToolConnectionStatus($params, self::STATE_CONNECTED); + return false; + } } ?> diff --git a/src/Lib/default/local_tool_connectors/MispConnector.php b/src/Lib/default/local_tool_connectors/MispConnector.php index be202e5..01a372b 100644 --- a/src/Lib/default/local_tool_connectors/MispConnector.php +++ b/src/Lib/default/local_tool_connectors/MispConnector.php @@ -11,6 +11,7 @@ class MispConnector extends CommonConnectorTools { public $description = 'MISP connector, handling diagnostics, organisation and sharing group management of your instance. Synchronisation requests can also be managed through the connector.'; + public $connectorName = 'MispConnector'; public $name = 'MISP'; public $exposedFunctions = [ @@ -71,6 +72,28 @@ class MispConnector extends CommonConnectorTools 'value' ], 'redirect' => 'serverSettingsAction' + ], + 'serversAction' => [ + 'type' => 'index', + 'scope' => 'child', + 'params' => [ + 'quickFilter', + 'limit', + 'page', + 'sort', + 'direction' + ] + ], + 'usersAction' => [ + 'type' => 'index', + 'scope' => 'child', + 'params' => [ + 'quickFilter', + 'limit', + 'page', + 'sort', + 'direction' + ] ] ]; public $version = '0.1'; @@ -89,17 +112,54 @@ class MispConnector extends CommonConnectorTools } } - public function health(Object $connection): array + private function genHTTPClient(Object $connection, array $options=[]): Object { $settings = json_decode($connection->settings, true); - $http = new Client(); - $response = $http->post($settings['url'] . '/users/view/me.json', '{}', [ + $defaultOptions = [ 'headers' => [ - 'AUTHORIZATION' => $settings['authkey'], - 'Accept' => 'Application/json', - 'Content-type' => 'Application/json' - ] - ]); + 'Authorization' => $settings['authkey'], + ], + ]; + if (empty($options['type'])) { + $options['type'] = 'json'; + } + if (!empty($settings['skip_ssl'])) { + $options['ssl_verify_peer'] = false; + $options['ssl_verify_host'] = false; + $options['ssl_verify_peer_name'] = false; + $options['ssl_allow_self_signed'] = true; + } + $options = array_merge($defaultOptions, $options); + $http = new Client($options); + return $http; + } + + public function HTTPClientGET(String $relativeURL, Object $connection, array $data=[], array $options=[]): Object + { + $settings = json_decode($connection->settings, true); + $http = $this->genHTTPClient($connection, $options); + $url = sprintf('%s%s', $settings['url'], $relativeURL); + return $http->get($url, $data, $options); + } + + public function HTTPClientPOST(String $relativeURL, Object $connection, $data, array $options=[]): Object + { + $settings = json_decode($connection->settings, true); + $http = $this->genHTTPClient($connection, $options); + $url = sprintf('%s%s', $settings['url'], $relativeURL); + return $http->post($url, $data, $options); + } + + public function health(Object $connection): array + { + try { + $response = $this->HTTPClientPOST('/users/view/me.json', $connection, '{}'); + } catch (\Exception $e) { + return [ + 'status' => 0, + 'message' => __('Connection issue.') + ]; + } $responseCode = $response->getStatusCode(); if ($response->isOk()) { $status = 1; @@ -123,8 +183,6 @@ class MispConnector extends CommonConnectorTools if (empty($params['connection'])) { throw new NotFoundException(__('No connection object received.')); } - $settings = json_decode($params['connection']->settings, true); - $http = new Client(); if (!empty($params['sort'])) { $list = explode('.', $params['sort']); $params['sort'] = end($list); @@ -133,13 +191,7 @@ class MispConnector extends CommonConnectorTools $params['limit'] = 50; } $url = $this->urlAppendParams($url, $params); - $response = $http->get($settings['url'] . $url, false, [ - 'headers' => [ - 'AUTHORIZATION' => $settings['authkey'], - 'Accept' => 'application/json', - 'Content-type' => 'application/json' - ] - ]); + $response = $this->HTTPClientGET($url, $params['connection']); if ($response->isOk()) { return $response; } else { @@ -155,20 +207,12 @@ class MispConnector extends CommonConnectorTools if (empty($params['connection'])) { throw new NotFoundException(__('No connection object received.')); } - $settings = json_decode($params['connection']->settings, true); - $http = new Client(); $url = $this->urlAppendParams($url, $params); - $response = $http->post($settings['url'] . $url, json_encode($params['body']), [ - 'headers' => [ - 'AUTHORIZATION' => $settings['authkey'], - 'Accept' => 'application/json' - ], - 'type' => 'json' - ]); + $response = $this->HTTPClientPOST($url, $params['connection'], json_encode($params['body'])); if ($response->isOk()) { return $response; } else { - throw new NotFoundException(__('Could not post to the requested resource.')); + throw new NotFoundException(__('Could not post to the requested resource. Remote returned:') . PHP_EOL . $response->getStringBody()); } } @@ -248,6 +292,7 @@ class MispConnector extends CommonConnectorTools 'name' => __('Value'), 'sort' => 'value', 'data_path' => 'value', + 'options' => 'options' ], [ 'name' => __('Type'), @@ -288,6 +333,172 @@ class MispConnector extends CommonConnectorTools } } + public function serversAction(array $params): array + { + $params['validParams'] = [ + 'limit' => 'limit', + 'page' => 'page', + 'quickFilter' => 'searchall' + ]; + $urlParams = h($params['connection']['id']) . '/serversAction'; + $response = $this->getData('/servers/index', $params); + $data = $response->getJson(); + if (!empty($data)) { + return [ + 'type' => 'index', + 'data' => [ + 'data' => $data, + 'skip_pagination' => 1, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value', + 'additionalUrlParams' => $urlParams, + 'quickFilter' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => 'Id', + 'sort' => 'Server.id', + 'data_path' => 'Server.id', + ], + [ + 'name' => 'Name', + 'sort' => 'Server.name', + 'data_path' => 'Server.name', + ], + [ + 'name' => 'Url', + 'sort' => 'Server.url', + 'data_path' => 'Server.url' + ], + [ + 'name' => 'Pull', + 'sort' => 'Server.pull', + 'element' => 'function', + 'function' => function($row, $context) { + $pull = $context->Hash->extract($row, 'Server.pull')[0]; + $pull_rules = $context->Hash->extract($row, 'Server.pull_rules')[0]; + $pull_rules = json_encode(json_decode($pull_rules, true), JSON_PRETTY_PRINT); + echo sprintf( + '', + h($pull_rules), + $pull ? 'check' : 'times' + ); + } + ], + [ + 'name' => 'Push', + 'element' => 'function', + 'function' => function($row, $context) { + $push = $context->Hash->extract($row, 'Server.push')[0]; + $push_rules = $context->Hash->extract($row, 'Server.push_rules')[0]; + $push_rules = json_encode(json_decode($push_rules, true), JSON_PRETTY_PRINT); + echo sprintf( + '', + h($push_rules), + $push ? 'check' : 'times' + ); + } + ], + [ + 'name' => 'Caching', + 'element' => 'boolean', + 'data_path' => 'Server.caching_enabled' + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right' + ] + ]; + } else { + return []; + } + } + + public function usersAction(array $params): array + { + $params['validParams'] = [ + 'limit' => 'limit', + 'page' => 'page', + 'quickFilter' => 'searchall' + ]; + $urlParams = h($params['connection']['id']) . '/usersAction'; + $response = $this->getData('/admin/users/index', $params); + $data = $response->getJson(); + if (!empty($data)) { + return [ + 'type' => 'index', + 'data' => [ + 'data' => $data, + 'skip_pagination' => 1, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value', + 'additionalUrlParams' => $urlParams, + 'quickFilter' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => 'Id', + 'sort' => 'User.id', + 'data_path' => 'User.id', + ], + [ + 'name' => 'Organisation', + 'sort' => 'Organisation.name', + 'data_path' => 'Organisation.name', + ], + [ + 'name' => 'Email', + 'sort' => 'User.email', + 'data_path' => 'User.email', + ], + [ + 'name' => 'Role', + 'sort' => 'Role.name', + 'data_path' => 'Role.name' + ] + ], + 'actions' => [ + [ + 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/editUser?id={{0}}', + 'modal_params_data_path' => ['User.id'], + 'icon' => 'edit', + 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/editAction' + ], + [ + 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/deleteUser?id={{0}}', + 'modal_params_data_path' => ['User.id'], + 'icon' => 'trash', + 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/serversAction' + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right' + ] + ]; + } else { + return []; + } + } + + public function organisationsAction(array $params): array { $params['validParams'] = [ @@ -515,18 +726,30 @@ class MispConnector extends CommonConnectorTools 'boolean' => 'checkbox', 'numeric' => 'number' ]; - $fields = [ - [ - 'field' => 'value', - 'label' => __('Value'), - 'default' => h($response['value']), - 'type' => $types[$response['type']] - ], - ]; + if (!empty($response['options'])) { + $fields = [ + [ + 'field' => 'value', + 'label' => __('Value'), + 'default' => h($response['value']), + 'type' => 'dropdown', + 'options' => $response['options'] + ] + ]; + } else { + $fields = [ + [ + 'field' => 'value', + 'label' => __('Value'), + 'default' => h($response['value']), + 'type' => $types[$response['type']] + ] + ]; + } return [ 'data' => [ 'title' => __('Modify server setting'), - 'description' => __('Modify setting ({0}) on connected MISP instance.', $params['setting']), + 'description' => __('Modify setting ({0}) on selected MISP instance(s).', $params['setting']), 'fields' => $fields, 'submit' => [ 'action' => $params['request']->getParam('action') @@ -540,18 +763,60 @@ class MispConnector extends CommonConnectorTools if ($response->getStatusCode() == 200) { return ['success' => 1, 'message' => __('Setting saved.')]; } else { - return ['success' => 0, 'message' => __('Could not save the setting.')]; + return ['success' => 0, 'message' => __('Could not update.')]; } } throw new MethodNotAllowedException(__('Invalid http request type for the given action.')); } - public function encodeConnectionAction(array $params): array + public function initiateConnection(array $params): array { - if (empty($params['org_uuid'])) { - throw new MethodNotAllowedException(__('No org uuid passed, cannot encode connection.')); - } - return []; + $params['connection_settings'] = json_decode($params['connection']['settings'], true); + $params['misp_organisation'] = $this->getSetOrg($params); + $params['sync_user'] = $this->createSyncUser($params, true); + return [ + 'email' => $params['sync_user']['email'], + 'user_id' => $params['sync_user']['id'], + 'authkey' => $params['sync_user']['authkey'], + 'url' => $params['connection_settings']['url'], + ]; + } + + public function acceptConnection(array $params): array + { + $params['sync_user_enabled'] = true; + $params['connection_settings'] = json_decode($params['connection']['settings'], true); + $params['misp_organisation'] = $this->getSetOrg($params); + $params['sync_user'] = $this->createSyncUser($params, false); + $serverParams = $params; + $serverParams['body'] = [ + 'authkey' => $params['remote_tool_data']['authkey'], + 'url' => $params['remote_tool_data']['url'], + 'name' => !empty($params['remote_tool_data']['tool_name']) ? $params['remote_tool_data']['tool_name'] : sprintf('MISP for %s', $params['remote_tool_data']['url']), + 'remote_org_id' => $params['misp_organisation']['id'] + ]; + $params['sync_connection'] = $this->addServer($serverParams); + return [ + 'email' => $params['sync_user']['email'], + 'authkey' => $params['sync_user']['authkey'], + 'url' => $params['connection_settings']['url'], + 'reflected_user_id' => $params['remote_tool_data']['user_id'] // request initiator Cerebrate to enable the MISP user + ]; + } + + public function finaliseConnection(array $params): bool + { + $params['misp_organisation'] = $this->getSetOrg($params); + $user = $this->enableUser($params, intval($params['remote_tool_data']['reflected_user_id'])); + $serverParams = $params; + $serverParams['body'] = [ + 'authkey' => $params['remote_tool_data']['authkey'], + 'url' => $params['remote_tool_data']['url'], + 'name' => !empty($params['remote_tool_data']['tool_name']) ? $params['remote_tool_data']['tool_name'] : sprintf('MISP for %s', $params['remote_tool_data']['url']), + 'remote_org_id' => $params['misp_organisation']['id'] + ]; + $params['sync_connection'] = $this->addServer($serverParams); + return true; } private function getSetOrg(array $params): array @@ -562,6 +827,7 @@ class MispConnector extends CommonConnectorTools $organisation = $response->getJson()['Organisation']; if (!$organisation['local']) { $organisation['local'] = 1; + $params['body'] = $organisation; $response = $this->postData('/admin/organisations/edit/' . $organisation['id'], $params); if (!$response->isOk()) { throw new MethodNotAllowedException(__('Could not update the organisation in MISP.')); @@ -583,39 +849,70 @@ class MispConnector extends CommonConnectorTools return $organisation; } - private function createSyncUser(array $params): array + private function createSyncUser(array $params, $disabled=true): array { $params['softError'] = 1; - $username = sprintf( - 'sync_%s@%s', - \Cake\Utility\Security::randomString(8), - parse_url($params['remote_cerebrate']['url'])['host'] - ); - $params['body'] = [ - 'email' => $username, + $user = [ + 'email' => 'sync_%s@' . parse_url($params['remote_cerebrate']['url'])['host'], 'org_id' => $params['misp_organisation']['id'], 'role_id' => empty($params['connection_settings']['role_id']) ? 5 : $params['connection_settings']['role_id'], - 'disabled' => 1, + 'disabled' => $disabled, 'change_pw' => 0, 'termsaccepted' => 1 ]; + return $this->createUser($user, $params); + } + + private function enableUser(array $params, int $userID): array + { + $params['softError'] = 1; + $user = [ + 'disabled' => false, + ]; + return $this->updateUser($userID, $user, $params); + } + + private function addServer(array $params): array + { + if ( + empty($params['body']['authkey']) || + empty($params['body']['url']) || + empty($params['body']['remote_org_id']) || + empty($params['body']['name']) + ) { + throw new MethodNotAllowedException(__('Required data missing from the sync connection object. The following fields are required: [name, url, authkey, org_id].')); + } + $response = $this->postData('/servers/add', $params); + if (!$response->isOk()) { + throw new MethodNotAllowedException(__('Could not add Server in MISP.')); + } + return $response->getJson()['Server']; + } + + private function createUser(array $user, array $params): array + { + if (strpos($user['email'], '%s') !== false) { + $user['email'] = sprintf( + $user['email'], + \Cake\Utility\Security::randomString(8) + ); + } + $params['body'] = $user; $response = $this->postData('/admin/users/add', $params); if (!$response->isOk()) { - throw new MethodNotAllowedException(__('Could not update the organisation in MISP.')); + throw new MethodNotAllowedException(__('Could not add the user in MISP.')); } return $response->getJson()['User']; } - public function connectToRemoteTool(array $params): array + private function updateUser(int $userID, array $user, array $params): array { - $params['connection_settings'] = json_decode($params['connection']['settings'], true); - $params['misp_organisation'] = $this->getSetOrg($params); - $params['sync_user'] = $this->createSyncUser($params); - return [ - 'email' => $params['sync_user']['email'], - 'authkey' => $params['sync_user']['authkey'], - 'url' => $params['connection_settings']['url'] - ]; + $params['body'] = $user; + $response = $this->postData(sprintf('/admin/users/edit/%s', $userID), $params); + if (!$response->isOk()) { + throw new MethodNotAllowedException(__('Could not edit the user in MISP.')); + } + return $response->getJson()['User']; } } diff --git a/src/Lib/default/local_tool_interconnectors/CommonTools.php b/src/Lib/default/local_tool_interconnectors/CommonTools.php new file mode 100644 index 0000000..cb50bc0 --- /dev/null +++ b/src/Lib/default/local_tool_interconnectors/CommonTools.php @@ -0,0 +1,24 @@ +connects; + } +} + +?> diff --git a/src/Lib/default/local_tool_interconnectors/MispToMispInterconnector.php b/src/Lib/default/local_tool_interconnectors/MispToMispInterconnector.php new file mode 100644 index 0000000..ef61de6 --- /dev/null +++ b/src/Lib/default/local_tool_interconnectors/MispToMispInterconnector.php @@ -0,0 +1,20 @@ + diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php index d4704fe..84c86b3 100644 --- a/src/Model/Entity/AppModel.php +++ b/src/Model/Entity/AppModel.php @@ -6,5 +6,8 @@ use Cake\ORM\Entity; class AppModel extends Entity { - + public function getAccessibleFieldForNew(): array + { + return $this->_accessibleOnNew ?? []; + } } diff --git a/src/Model/Entity/Inbox.php b/src/Model/Entity/Inbox.php index a8f546e..820e29d 100644 --- a/src/Model/Entity/Inbox.php +++ b/src/Model/Entity/Inbox.php @@ -7,5 +7,14 @@ use Cake\ORM\Entity; class Inbox extends AppModel { + protected $_virtual = ['local_tool_connector_name']; + protected function _getLocalToolConnectorName() + { + $localConnectorName = null; + if (!empty($this->data) && !empty($this->data['connectorName'])) { + $localConnectorName = $this->data['connectorName']; + } + return $localConnectorName; + } } diff --git a/src/Model/Entity/Individual.php b/src/Model/Entity/Individual.php index ef8e5db..a14e339 100644 --- a/src/Model/Entity/Individual.php +++ b/src/Model/Entity/Individual.php @@ -7,5 +7,13 @@ use Cake\ORM\Entity; class Individual extends AppModel { + protected $_accessible = [ + '*' => true, + 'id' => false, + 'uuid' => false, + ]; + protected $_accessibleOnNew = [ + 'uuid' => true, + ]; } diff --git a/src/Model/Entity/Organisation.php b/src/Model/Entity/Organisation.php index 89fb838..bc78378 100644 --- a/src/Model/Entity/Organisation.php +++ b/src/Model/Entity/Organisation.php @@ -7,5 +7,13 @@ use Cake\ORM\Entity; class Organisation extends AppModel { + protected $_accessible = [ + '*' => true, + 'id' => false, + 'uuid' => false, + ]; + protected $_accessibleOnNew = [ + 'uuid' => true, + ]; } diff --git a/src/Model/Entity/RemoteToolConnection.php b/src/Model/Entity/RemoteToolConnection.php new file mode 100644 index 0000000..164ddd8 --- /dev/null +++ b/src/Model/Entity/RemoteToolConnection.php @@ -0,0 +1,11 @@ + true, + 'id' => false, + 'uuid' => false, + 'organisation_id' => false, + 'user_id' => false, + ]; + + protected $_accessibleOnNew = [ + 'uuid' => true, + 'organisation_id' => true, + 'user_id' => true, + ]; +} diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php index 98a62ae..4502a76 100644 --- a/src/Model/Table/BroodsTable.php +++ b/src/Model/Table/BroodsTable.php @@ -5,7 +5,10 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; use Cake\Validation\Validator; +use Cake\Core\Configure; use Cake\Http\Client; +use Cake\Http\Client\Response; +use Cake\Http\Exception\NotFoundException; use Cake\ORM\TableRegistry; use Cake\Error\Debugger; @@ -26,18 +29,40 @@ class BroodsTable extends AppTable return $validator; } + public function genHTTPClient(Object $brood, array $options=[]): Object + { + $defaultOptions = [ + 'headers' => [ + 'Authorization' => $brood->authkey, + ], + ]; + if (empty($options['type'])) { + $options['type'] = 'json'; + } + $options = array_merge($defaultOptions, $options); + $http = new Client($options); + return $http; + } + + public function HTTPClientGET(String $relativeURL, Object $brood, array $data=[], array $options=[]): Object + { + $http = $this->genHTTPClient($brood, $options); + $url = sprintf('%s%s', $brood->url, $relativeURL); + return $http->get($url, $data, $options); + } + + public function HTTPClientPOST(String $relativeURL, Object $brood, $data, array $options=[]): Object + { + $http = $this->genHTTPClient($brood, $options); + $url = sprintf('%s%s', $brood->url, $relativeURL); + return $http->post($url, $data, $options); + } + public function queryStatus($id) { $brood = $this->find()->where(['id' => $id])->first(); - $http = new Client(); $start = microtime(true); - $response = $http->get($brood['url'] . '/instance/status.json', [], [ - 'headers' => [ - 'Authorization' => $brood['authkey'], - 'Accept' => 'Application/json', - 'Content-type' => 'Application/json' - ] - ]); + $response = $this->HTTPClientGET('/instance/status.json', $brood); $ping = ((int)(100 * (microtime(true) - $start))); $errors = [ 403 => [ @@ -81,15 +106,8 @@ class BroodsTable extends AppTable if (empty($brood)) { throw new NotFoundException(__('Brood not found')); } - $http = new Client(); $filterQuery = empty($filter) ? '' : '?quickFilter=' . urlencode($filter); - $response = $http->get($brood['url'] . '/' . $scope . '/index.json' . $filterQuery , [], [ - 'headers' => [ - 'Authorization' => $brood['authkey'], - 'Accept' => 'Application/json', - 'Content-type' => 'Application/json' - ] - ]); + $response = $this->HTTPClientGET(sprintf('/%s/index.json%s', $scope, $filterQuery), $brood); if ($response->isOk()) { return $response->getJson(); } else { @@ -97,6 +115,7 @@ class BroodsTable extends AppTable } } + // TODO: Delete this function? public function downloadAndCapture($brood_id, $object_id, $scope, $path) { $query = $this->find(); @@ -104,14 +123,7 @@ class BroodsTable extends AppTable if (empty($brood)) { throw new NotFoundException(__('Brood not found')); } - $http = new Client(); - $response = $http->get($brood['url'] . '/' . $scope . '/view/' . $org_id . '/index.json' , [], [ - 'headers' => [ - 'Authorization' => $brood['authkey'], - 'Accept' => 'Application/json', - 'Content-type' => 'Application/json' - ] - ]); + $response = $this->HTTPClientGET(sprintf('/%s/view/%s.json', $scope, $org_id), $brood); if ($response->isOk()) { $org = $response->getJson(); $this->Organisation = TableRegistry::getTableLocator()->get('Organisations'); @@ -129,14 +141,7 @@ class BroodsTable extends AppTable if (empty($brood)) { throw new NotFoundException(__('Brood not found')); } - $http = new Client(); - $response = $http->get($brood['url'] . '/organisations/view/' . $org_id . '/index.json' , [], [ - 'headers' => [ - 'Authorization' => $brood['authkey'], - 'Accept' => 'Application/json', - 'Content-type' => 'Application/json' - ] - ]); + $response = $this->HTTPClientGET(sprintf('/organisations/view/%s.json', $org_id), $brood); if ($response->isOk()) { $org = $response->getJson(); $this->Organisation = TableRegistry::getTableLocator()->get('Organisations'); @@ -154,18 +159,29 @@ class BroodsTable extends AppTable if (empty($brood)) { throw new NotFoundException(__('Brood not found')); } - $http = new Client(); - $response = $http->get($brood['url'] . '/individuals/view/' . $individual_id . '/index.json' , [], [ - 'headers' => [ - 'Authorization' => $brood['authkey'], - 'Accept' => 'Application/json', - 'Content-type' => 'Application/json' - ] - ]); + $response = $this->HTTPClientGET(sprintf('/individuals/view/%s.json', $individual_id), $brood); if ($response->isOk()) { - $org = $response->getJson(); - $this->Individual = TableRegistry::getTableLocator()->get('Individual'); - $result = $this->Individual->captureIndividual($individual); + $individual = $response->getJson(); + $this->Individuals = TableRegistry::getTableLocator()->get('Individuals'); + $result = $this->Individuals->captureIndividual($individual); + return $result; + } else { + return false; + } + } + + public function downloadSharingGroup($brood_id, $sg_id, $user_id) + { + $query = $this->find(); + $brood = $query->where(['id' => $brood_id])->first(); + if (empty($brood)) { + throw new NotFoundException(__('Brood not found')); + } + $response = $this->HTTPClientGET(sprintf('/sharing-groups/view/%s.json', $sg_id), $brood); + if ($response->isOk()) { + $individual = $response->getJson(); + $this->SharingGroups = TableRegistry::getTableLocator()->get('SharingGroups'); + $result = $this->SharingGroups->captureSharingGroup($individual, $user_id); return $result; } else { return false; @@ -179,17 +195,161 @@ class BroodsTable extends AppTable if (empty($brood)) { throw new NotFoundException(__('Brood not found')); } - $http = new Client(); - $response = $http->get($brood['url'] . '/localTools/exposedTools' , [], [ - 'headers' => [ - 'Authorization' => $brood['authkey'] - ], - 'type' => 'json' - ]); + $response = $this->HTTPClientGET('/localTools/exposedTools', $brood); if ($response->isOk()) { return $response->getJson(); } else { return false; } } + + public function sendRequest($brood, $urlPath, $methodPost = true, $data = []): Response + { + if ($methodPost) { + $response = $this->HTTPClientPOST($urlPath, $brood, json_encode($data)); + } else { + $response = $this->HTTPClientGET($urlPath, $brood, $data); + } + return $response; + } + + private function injectRequiredData($params, $data): Array + { + $data['connectorName'] = $params['remote_tool']['connector']; + $data['cerebrateURL'] = Configure::read('App.fullBaseUrl'); + $data['local_tool_id'] = $params['connection']['id']; + $data['remote_tool_id'] = $params['remote_tool']['id']; + $data['tool_name'] = $params['connection']['name']; + return $data; + } + + public function sendLocalToolConnectionRequest($params, $data): array + { + $url = '/inbox/createEntry/LocalTool/IncomingConnectionRequest'; + $data = $this->injectRequiredData($params, $data); + try { + $response = $this->sendRequest($params['remote_cerebrate'], $url, true, $data); + $jsonReply = $response->getJson(); + if (empty($jsonReply['success'])) { + $jsonReply = $this->handleMessageNotCreated($params['remote_cerebrate'], $url, $data, 'LocalTool', 'IncomingConnectionRequest', $response, $params, 'STATE_INITIAL'); + } + } catch (NotFoundException $e) { + $jsonReply = $this->handleSendingFailed($params['remote_cerebrate'], $url, $data, 'LocalTool', 'IncomingConnectionRequest', $e, $params, 'STATE_INITIAL'); + } + return $jsonReply; + } + + public function sendLocalToolAcceptedRequest($params, $data): array + { + $url = '/inbox/createEntry/LocalTool/AcceptedRequest'; + $data = $this->injectRequiredData($params, $data); + try { + $response = $this->sendRequest($params['remote_cerebrate'], $url, true, $data); + $jsonReply = $response->getJson(); + if (empty($jsonReply['success'])) { + $jsonReply = $this->handleMessageNotCreated($params['remote_cerebrate'], $url, $data, 'LocalTool', 'AcceptedRequest', $response, $params, 'STATE_CONNECTED'); + } else { + $this->setRemoteToolConnectionStatus($params, 'STATE_CONNECTED'); + } + } catch (NotFoundException $e) { + $jsonReply = $this->handleSendingFailed($params['remote_cerebrate'], $url, $data, 'LocalTool', 'AcceptedRequest', $e, $params, 'STATE_CONNECTED'); + } + return $jsonReply; + } + + public function sendLocalToolDeclinedRequest($params, $data): array + { + $url = '/inbox/createEntry/LocalTool/DeclinedRequest'; + $data = $this->injectRequiredData($params, $data); + try { + $response = $this->sendRequest($params['remote_cerebrate'], $url, true, $data); + $jsonReply = $response->getJson(); + if (empty($jsonReply['success'])) { + $jsonReply = $this->handleMessageNotCreated($params['remote_cerebrate'], $url, $data, 'LocalTool', 'AcceptedRequest', $response, $params, 'STATE_DECLINED'); + } + } catch (NotFoundException $e) { + $jsonReply = $this->handleSendingFailed($params['remote_cerebrate'], $url, $data, 'LocalTool', 'AcceptedRequest', $e, $params, 'STATE_DECLINED'); + } + return $jsonReply; + } + + /** + * handleSendingFailed - Handle the case if the request could not be sent or if the remote rejected the connection request + * + * @param Object $response + * @return array + */ + private function handleSendingFailed($brood, $url, $data, $model, $action, $e, $params, $next_connector_state): array + { + $connector = $params['connector'][$params['remote_tool']['connector']]; + $reason = [ + 'message' => __('Failed to send message to remote cerebrate. It has been placed in the outbox.'), + 'errors' => [$e->getMessage()], + ]; + $outboxSaveResult = $this->saveErrorInOutbox($brood, $url, $data, $reasonMessage, $params, $next_connector_state); + $connector->remoteToolConnectionStatus($params, $connector::STATE_SENDING_ERROR); + $creationResult = [ + 'success' => false, + 'message' => $reason['message'], + 'errors' => $reason['errors'], + 'placed_in_outbox' => !empty($outboxSaveResult['success']), + ]; + return $creationResult; + } + + /** + * handleMessageNotCreated - Handle the case if the request was sent but the remote brood did not save the message in the inbox + * + * @param Object $response + * @return array + */ + private function handleMessageNotCreated($brood, $url, $data, $model, $action, $response, $params, $next_connector_state): array + { + $connector = $params['connector'][$params['remote_tool']['connector']]; + $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], + ]; + $outboxSaveResult = $this->saveErrorInOutbox($brood, $url, $data, $reason, $model, $action, $params, $next_connector_state); + $connector->remoteToolConnectionStatus($params, $connector::STATE_SENDING_ERROR); + $creationResult = [ + 'success' => false, + 'message' => $reason['message'], + 'errors' => $reason['errors'], + 'placed_in_outbox' => !empty($outboxSaveResult['success']), + ]; + return $creationResult; + } + + private function saveErrorInOutbox($brood, $url, $data, $reason, $model, $action, $params, $next_connector_state): 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'], + 'next_connector_state' => $next_connector_state, + ], + 'brood' => $brood, + 'model' => $model, + 'action' => $action, + ]; + $creationResult = $processor->create($entryData); + return $creationResult; + } + + private function setRemoteToolConnectionStatus($params, String $status): void + { + $connector = $params['connector'][$params['remote_tool']['connector']]; + $connector->remoteToolConnectionStatus($params, constant(get_class($connector) . '::' . $status)); + } } diff --git a/src/Model/Table/InboxProcessorsTable.php b/src/Model/Table/InboxProcessorsTable.php new file mode 100644 index 0000000..4bb1aea --- /dev/null +++ b/src/Model/Table/InboxProcessorsTable.php @@ -0,0 +1,156 @@ + [ + 'ToolInterconnection' => false, + 'OneWaySynchronization' => false, + ], + 'Proposal' => [ + 'ProposalEdit' => false, + ], + 'Synchronisation' => [ + 'DataExchange' => false, + ], + 'User' => [ + 'Registration' => true, + ], + ]; + + public function initialize(array $config): void + { + parent::initialize($config); + $this->loadProcessors(); + } + + public function getProcessor($scope, $action=null) + { + if (isset($this->inboxProcessors[$scope])) { + if (is_null($action)) { + return $this->inboxProcessors[$scope]; + } else if (!empty($this->inboxProcessors[$scope]->{$action})) { + return $this->inboxProcessors[$scope]->{$action}; + } else { + throw new \Exception(__('Processor {0}.{1} not found', $scope, $action)); + } + } + throw new MissingInboxProcessorException(__('Processor not found')); + } + + public function getLocalToolProcessor($action, $connectorName) + { + $scope = "LocalTool"; + $specificScope = "{$connectorName}LocalTool"; + try { // try to get specific processor for module name or fall back to generic local tool processor + $processor = $this->getProcessor($specificScope, $action); + } catch (MissingInboxProcessorException $e) { + $processor = $this->getProcessor($scope, $action); + } + return $processor; + } + + public function listProcessors($scope=null) + { + if (is_null($scope)) { + return $this->inboxProcessors; + } else { + if (isset($this->inboxProcessors[$scope])) { + return $this->inboxProcessors[$scope]; + } else { + throw new MissingInboxProcessorException(__('Processors for {0} not found', $scope)); + } + } + } + + private function loadProcessors() + { + $processorDir = new Folder($this->processorsDirectory); + $processorFiles = $processorDir->find('.*InboxProcessor\.php', true); + foreach ($processorFiles as $processorFile) { + if ($processorFile == 'GenericInboxProcessor.php') { + continue; + } + $processorMainClassName = str_replace('.php', '', $processorFile); + $processorMainClassNameShort = str_replace('InboxProcessor.php', '', $processorFile); + $processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName); + if (is_object($processorMainClass)) { + $this->inboxProcessors[$processorMainClassNameShort] = $processorMainClass; + foreach ($this->inboxProcessors[$processorMainClassNameShort]->getRegisteredActions() as $registeredAction) { + $scope = $this->inboxProcessors[$processorMainClassNameShort]->getScope(); + if (!empty($this->enabledProcessors[$scope][$registeredAction])) { + $this->inboxProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = true; + } else { + $this->inboxProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = false; + } + } + } else { + $this->inboxProcessors[$processorMainClassNameShort] = new \stdClass(); + $this->inboxProcessors[$processorMainClassNameShort]->{$registeredAction} = new \stdClass(); + $this->inboxProcessors[$processorMainClassNameShort]->{$registeredAction}->action = "N/A"; + $this->inboxProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = false; + $this->inboxProcessors[$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(); + } + } + + /** + * createInboxEntry + * + * @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 createInboxEntry($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/InboxTable.php b/src/Model/Table/InboxTable.php index 93a7f22..21d6da6 100644 --- a/src/Model/Table/InboxTable.php +++ b/src/Model/Table/InboxTable.php @@ -7,6 +7,7 @@ use Cake\Database\Type; use Cake\ORM\Table; use Cake\ORM\RulesChecker; use Cake\Validation\Validator; +use Cake\Http\Exception\NotFoundException; Type::map('json', 'Cake\Database\Type\JsonType'); @@ -44,7 +45,7 @@ class InboxTable extends AppTable ->notEmptyString('title') ->notEmptyString('origin') ->datetime('created') - + ->requirePresence([ 'scope' => ['message' => __('The field `scope` is required')], 'action' => ['message' => __('The field `action` is required')], @@ -62,4 +63,33 @@ class InboxTable extends AppTable return $rules; } + + public function checkUserBelongsToBroodOwnerOrg($user, $entryData) { + $this->Broods = \Cake\ORM\TableRegistry::getTableLocator()->get('Broods'); + $this->Individuals = \Cake\ORM\TableRegistry::getTableLocator()->get('Individuals'); + $errors = []; + $brood = $this->Broods->find() + ->where(['url' => $entryData['origin']]) + ->first(); + if (empty($brood)) { + $errors[] = __('Unkown brood `{0}`', $entryData['data']['cerebrateURL']); + } + + $found = false; + foreach ($user->individual->organisations as $organisations) { + if ($organisations->id == $brood->organisation_id) { + $found = true; + } + } + if (!$found) { + $errors[] = __('User `{0}` is not part of the brood\'s organisation. Make sure `{0}` is aligned with the organisation owning the brood.', $user->individual->email); + } + return $errors; + } + + public function createEntry($entryData) + { + $savedEntry = $this->save($entryData); + return $savedEntry; + } } diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index d0df577..da73d92 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -55,26 +55,20 @@ class IndividualsTable extends AppTable return null; } if (empty($existingIndividual)) { - $entity = $this->newEntity($individual, ['associated' => []]); - if (!$this->save($entity)) { - return null; - } - $individual = $entity; + $entityToSave = $this->newEmptyEntity(); + $this->patchEntity($entityToSave, $individual, [ + 'accessibleFields' => $entityToSave->getAccessibleFieldForNew() + ]); } else { - $reserved = ['id', 'uuid', 'metaFields']; - foreach ($individual as $field => $value) { - if (in_array($field, $reserved)) { - continue; - } - $existingIndividual->$field = $value; - } - if (!$this->save($existingIndividual, ['associated' => false])) { - return null; - } - $individual = $existingIndividua; + $this->patchEntity($existingIndividual, $individual); + $entityToSave = $existingIndividual; } - $this->postCaptureActions($individual); - return $individual->id; + $savedEntity = $this->save($entityToSave, ['associated' => false]); + if (!$savedEntity) { + return null; + } + $this->postCaptureActions($savedEntity); + return $savedEntity->id; } public function postCaptureActions($individual): void diff --git a/src/Model/Table/LocalToolsTable.php b/src/Model/Table/LocalToolsTable.php index f5e1b46..9d818ef 100644 --- a/src/Model/Table/LocalToolsTable.php +++ b/src/Model/Table/LocalToolsTable.php @@ -73,6 +73,12 @@ class LocalToolsTable extends AppTable throw new NotFoundException(__('Invalid connector module action requested.')); } + public function getConnectorByToolName($toolName): array + { + $toolName = sprintf('%sConnector', ucfirst(strtolower($toolName))); + return $this->getConnectors($toolName); + } + public function getConnectors(string $name = null): array { $connectors = []; @@ -95,6 +101,29 @@ class LocalToolsTable extends AppTable return $connectors; } + public function getInterconnectors(string $name = null): array + { + $connectors = []; + $dirs = [ + ROOT . '/src/Lib/default/local_tool_interconnectors', + ROOT . '/src/Lib/custom/local_tool_interconnectors' + ]; + foreach ($dirs as $dir) { + $dir = new Folder($dir); + $files = $dir->find('.*Interconnector\.php'); + foreach ($files as $file) { + require_once($dir->pwd() . '/'. $file); + $className = substr($file, 0, -4); + $classNamespace = '\\' . $className . '\\' . $className; + $tempClass = new $classNamespace; + if (empty($name) || $tempClass->getConnectors()[0] === $name) { + $connectors[$tempClass->getConnectors()[0]][] = new $classNamespace; + } + } + } + return $connectors; + } + public function extractMeta(array $connector_classes, bool $includeConnections = false): array { $connectors = []; @@ -197,6 +226,17 @@ class LocalToolsTable extends AppTable } public function encodeConnection(array $params): array + { + $params = $this->buildConnectionParams($params); + $localResult = $params['connector'][$params['remote_tool']['connector']]->initiateConnectionWrapper($params); + $inboxResult = $this->sendEncodedConnection($params, $localResult); + return [ + 'inboxResult' => $inboxResult, + 'localResult' => $localResult + ]; + } + + public function buildConnectionParams(array $params): array { $remote_tool = $this->getRemoteToolById($params); $broods = \Cake\ORM\TableRegistry::getTableLocator()->get('Broods'); @@ -207,13 +247,54 @@ class LocalToolsTable extends AppTable if (empty($connector[$remote_tool['connector']])) { throw new NotFoundException(__('No valid connector found for the remote tool.')); } - $result = $connector[$remote_tool['connector']]->connectToRemoteTool([ + return [ 'remote_cerebrate' => $remote_cerebrate, 'remote_org' => $remote_org, 'remote_tool' => $remote_tool, 'connector' => $connector, - 'connection' => $connection - ]); - return $result; + 'connection' => $connection, + //'message' => + ]; + } + + public function appendLocalToolConnections(int $brood_id, array $tool): array + { + $remoteToolConnections = \Cake\ORM\TableRegistry::getTableLocator()->get('RemoteToolConnections'); + $connections = $remoteToolConnections->find()->where(['remote_tool_id' => $tool['id'], 'brood_id' => $brood_id])->toArray(); + $local_tools = []; + foreach ($connections as $k => $connection) { + $temp = $this->find()->where(['id' => $connection['local_tool_id']])->select(['id', 'name'])->enableHydration(false)->first(); + $temp['status'] = $connection['status']; + $local_tools[] = $temp; + } + return $local_tools; + } + + public function sendEncodedConnection($params, $encodedConnection) + { + $this->Broods = \Cake\ORM\TableRegistry::getTableLocator()->get('Broods'); + $jsonReply = $this->Broods->sendLocalToolConnectionRequest($params, $encodedConnection); + return $jsonReply; + } + + public function findConnectable($local_tool): array + { + $connectors = $this->getInterconnectors($local_tool['connector']); + $validTargets = []; + if (!empty($connectors)) { + foreach ($connectors[$local_tool['connector']] as $connector) { + $validTargets[$connector['connects'][1]] = 1; + } + } + + } + + public function fetchConnection($id): object + { + $connection = $this->find()->where(['id' => $id])->first(); + if (empty($connection)) { + throw new NotFoundException(__('Local tool not found.')); + } + return $connection; } } diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php index c23ee7b..8587864 100644 --- a/src/Model/Table/OrganisationsTable.php +++ b/src/Model/Table/OrganisationsTable.php @@ -55,9 +55,6 @@ class OrganisationsTable extends AppTable public function captureOrg($org): ?int { - if (!empty($org['id'])) { - unset($org['id']); - } if (!empty($org['uuid'])) { $existingOrg = $this->find()->where([ 'uuid' => $org['uuid'] @@ -66,27 +63,20 @@ class OrganisationsTable extends AppTable return null; } if (empty($existingOrg)) { - $data = $this->newEmptyEntity(); - $data = $this->patchEntity($data, $org, ['associated' => []]); - if (!$this->save($data)) { - return null; - } - $savedOrg = $data; + $entityToSave = $this->newEmptyEntity(); + $this->patchEntity($entityToSave, $org, [ + 'accessibleFields' => $entityToSave->getAccessibleFieldForNew() + ]); } else { - $reserved = ['id', 'uuid', 'metaFields']; - foreach ($org as $field => $value) { - if (in_array($field, $reserved)) { - continue; - } - $existingOrg->$field = $value; - } - if (!$this->save($existingOrg)) { - return null; - } - $savedOrg = $existingOrg; + $this->patchEntity($existingOrg, $org); + $entityToSave = $existingOrg; } - $this->postCaptureActions($savedOrg->id, $org); - return $savedOrg->id; + $savedEntity = $this->save($entityToSave, ['associated' => false]); + if (!$savedEntity) { + return null; + } + $this->postCaptureActions($savedEntity->id, $org); + return $savedEntity->id; } public function postCaptureActions($id, $org) diff --git a/src/Model/Table/OutboxProcessorsTable.php b/src/Model/Table/OutboxProcessorsTable.php new file mode 100644 index 0000000..da26692 --- /dev/null +++ b/src/Model/Table/OutboxProcessorsTable.php @@ -0,0 +1,136 @@ + [ + 'ResendFailedMessageProcessor' => true, + ], + ]; + + public function initialize(array $config): void + { + parent::initialize($config); + if (empty($this->outboxProcessors)) { + $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/Model/Table/RemoteToolConnectionsTable.php b/src/Model/Table/RemoteToolConnectionsTable.php new file mode 100644 index 0000000..7e8cf21 --- /dev/null +++ b/src/Model/Table/RemoteToolConnectionsTable.php @@ -0,0 +1,27 @@ +BelongsTo( + 'LocalTools' + ); + $this->setDisplayField('id'); + } + + public function validationDefault(Validator $validator): Validator + { + return $validator; + } +} diff --git a/src/Model/Table/RequestProcessorTable.php b/src/Model/Table/RequestProcessorTable.php deleted file mode 100644 index e76fdab..0000000 --- a/src/Model/Table/RequestProcessorTable.php +++ /dev/null @@ -1,95 +0,0 @@ - [ - 'ToolInterconnection' => false, - 'OneWaySynchronization' => false, - ], - 'Proposal' => [ - 'ProposalEdit' => false, - ], - 'Synchronisation' => [ - 'DataExchange' => false, - ], - 'User' => [ - 'Registration' => true, - ], - ]; - - public function initialize(array $config): void - { - parent::initialize($config); - $this->loadProcessors(); - } - - public function getProcessor($scope, $action=null) - { - if (isset($this->requestProcessors[$scope])) { - if (is_null($action)) { - return $this->requestProcessors[$scope]; - } else if (!empty($this->requestProcessors[$scope]->{$action})) { - return $this->requestProcessors[$scope]->{$action}; - } else { - throw new \Exception(__('Processor {0}.{1} not found', $scope, $action)); - } - } - throw new \Exception(__('Processor not found'), 1); - } - - public function listProcessors($scope=null) - { - if (is_null($scope)) { - return $this->requestProcessors; - } else { - if (isset($this->requestProcessors[$scope])) { - return $this->requestProcessors[$scope]; - } else { - throw new \Exception(__('Processors for {0} not found', $scope)); - } - } - } - - private function loadProcessors() - { - $processorDir = new Folder($this->processorsDirectory); - $processorFiles = $processorDir->find('.*RequestProcessor\.php', true); - foreach ($processorFiles as $processorFile) { - if ($processorFile == 'GenericRequestProcessor.php') { - continue; - } - $processorMainClassName = str_replace('.php', '', $processorFile); - $processorMainClassNameShort = str_replace('RequestProcessor.php', '', $processorFile); - $processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName); - if ($processorMainClass !== false) { - $this->requestProcessors[$processorMainClassNameShort] = $processorMainClass; - foreach ($this->requestProcessors[$processorMainClassNameShort]->getRegisteredActions() as $registeredAction) { - $scope = $this->requestProcessors[$processorMainClassNameShort]->getScope(); - if (!empty($this->enabledProcessors[$scope][$registeredAction])) { - $this->requestProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = true; - } else { - $this->requestProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = false; - } - } - } - } - } - - private function getProcessorClass($filePath, $processorMainClassName) - { - require_once($filePath); - $reflection = new \ReflectionClass($processorMainClassName); - $processorMainClass = $reflection->newInstance(true); - if ($processorMainClass->checkLoading() === 'Assimilation successful!') { - return $processorMainClass; - } - } -} diff --git a/src/Model/Table/SharingGroupsTable.php b/src/Model/Table/SharingGroupsTable.php index 41cf20c..6f18157 100644 --- a/src/Model/Table/SharingGroupsTable.php +++ b/src/Model/Table/SharingGroupsTable.php @@ -46,9 +46,6 @@ class SharingGroupsTable extends AppTable public function captureSharingGroup($input, int $user_id = 0): ?int { - if (!empty($input['id'])) { - unset($input['id']); - } if (!empty($input['uuid'])) { $existingSG = $this->find()->where([ 'uuid' => $input['uuid'] @@ -57,41 +54,31 @@ class SharingGroupsTable extends AppTable return null; } if (empty($existingSG)) { - $data = $this->newEmptyEntity(); + $entityToSave = $this->newEmptyEntity(); $input['organisation_id'] = $this->Organisations->captureOrg($input['organisation']); $input['user_id'] = $user_id; - $data = $this->patchEntity($data, $input, ['associated' => []]); - if (!$this->save($data)) { - return null; - } - $savedSG = $data; + $this->patchEntity($entityToSave, $input, [ + 'accessibleFields' => $entityToSave->getAccessibleFieldForNew() + ]); } else { - $reserved = ['id', 'uuid', 'metaFields']; - foreach ($input as $field => $value) { - if (in_array($field, $reserved)) { - continue; - } - $existingSG->$field = $value; - } - if (!$this->save($existingSG)) { - return null; - } - $savedSG = $existingSG; + $this->patchEntity($existingSG, $input); + $entityToSave = $existingSG; } - $this->postCaptureActions($savedSG->id, $input); - return $savedSG->id; + $savedEntity = $this->save($entityToSave, ['associated' => false]); + if (!$savedEntity) { + return null; + } + $this->postCaptureActions($savedEntity, $input); + return $savedEntity->id; } - public function postCaptureActions($id, $input): void + public function postCaptureActions($savedEntity, $input): void { - $sharingGroup = $this->find()->where([ - 'id' => $id - ])->first(); $orgs = []; foreach ($input['sharing_group_orgs'] as $sgo) { $organisation_id = $this->Organisations->captureOrg($sgo); $orgs[] = $this->SharingGroupOrgs->get($organisation_id); } - $this->SharingGroupOrgs->link($sharingGroup, $orgs); + $this->SharingGroupOrgs->link($savedEntity, $orgs); } } diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index b63e5f6..b5502d2 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -42,12 +42,15 @@ namespace App\View\Helper; use Cake\View\Helper; +use Cake\Utility\Hash; use Cake\Utility\Inflector; use Cake\Utility\Security; use InvalidArgumentException; class BootstrapHelper extends Helper { + public $helpers = ['FontAwesome']; + public function tabs($options) { $bsTabs = new BootstrapTabs($options); @@ -80,14 +83,50 @@ class BootstrapHelper extends Helper public function modal($options) { - $bsButton = new BoostrapModal($options); - return $bsButton->modal(); + $bsModal = new BoostrapModal($options); + return $bsModal->modal(); + } + + public function card($options) + { + $bsCard = new BoostrapCard($options); + return $bsCard->card(); + } + + public function progress($options) + { + $bsProgress = new BoostrapProgress($options); + return $bsProgress->progress(); + } + + public function collapse($options, $content) + { + $bsCollapse = new BoostrapCollapse($options, $content, $this); + return $bsCollapse->collapse(); + } + + public function progressTimeline($options) + { + $bsProgressTimeline = new BoostrapProgressTimeline($options, $this); + return $bsProgressTimeline->progressTimeline(); } } class BootstrapGeneric { public static $variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent']; + public static $textClassByVariants = [ + 'primary' => 'text-white', + 'secondary' => 'text-white', + 'success' => 'text-white', + 'danger' => 'text-white', + 'warning' => 'text-black', + 'info' => 'text-white', + 'light' => 'text-black', + 'dark' => 'text-white', + 'white' => 'text-black', + 'transparent' => 'text-black' + ]; protected $allowedOptionValues = []; protected $options = []; @@ -146,6 +185,11 @@ class BootstrapGeneric 'arial-hidden' => 'true' ], '×')); } + + protected static function getTextClassForVariant($variant) + { + return !empty(self::$textClassByVariants[$variant]) ? self::$textClassByVariants[$variant] : 'text-black'; + } } class BootstrapTabs extends BootstrapGeneric @@ -543,7 +587,7 @@ class BoostrapTable extends BootstrapGeneric { } else { $key = $field; } - $cellValue = $row[$key]; + $cellValue = Hash::get($row, $key); $html .= $this->genCell($cellValue, $field, $row); } } else { // indexed array @@ -571,7 +615,7 @@ class BoostrapTable extends BootstrapGeneric { private function genCaption() { - return $this->genNode('caption', [], h($this->caption)); + return !empty($this->caption) ? $this->genNode('caption', [], h($this->caption)) : ''; } } @@ -730,7 +774,7 @@ class BoostrapModal extends BootstrapGeneric { function __construct($options) { $this->allowedOptionValues = [ 'size' => ['sm', 'lg', 'xl', ''], - 'type' => ['ok-only','confirm','confirm-success','confirm-warning','confirm-danger'], + 'type' => ['ok-only','confirm','confirm-success','confirm-warning','confirm-danger', 'custom'], 'variant' => array_merge(BootstrapGeneric::$variants, ['']), ]; $this->processOptions($options); @@ -796,7 +840,10 @@ class BoostrapModal extends BootstrapGeneric { private function genFooter() { - $footer = $this->openNode('div', ['class' => array_merge(['modal-footer'], $this->options['footerClass'])]); + $footer = $this->openNode('div', [ + 'class' => array_merge(['modal-footer'], $this->options['footerClass']), + 'data-custom-footer' => $this->options['type'] == 'custom' + ]); if (!empty($this->options['footerHtml'])) { $footer .= $this->options['footerHtml']; } else { @@ -811,6 +858,8 @@ class BoostrapModal extends BootstrapGeneric { return $this->getFooterOkOnly(); } else if (str_contains($this->options['type'], 'confirm')) { return $this->getFooterConfirm(); + } else if ($this->options['type'] == 'custom') { + return $this->getFooterCustom(); } else { return $this->getFooterOkOnly(); } @@ -849,10 +898,350 @@ class BoostrapModal extends BootstrapGeneric { 'text' => h($this->options['confirmText']), 'class' => 'modal-confirm-button', 'params' => [ - 'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal', + // 'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal', 'data-confirmFunction' => sprintf('%s', $this->options['confirmFunction']) ] ]))->button(); return $buttonCancel . $buttonConfirm; } + + private function getFooterCustom() + { + $buttons = []; + foreach ($this->options['footerButtons'] as $buttonConfig) { + $buttons[] = (new BoostrapButton([ + 'variant' => h($buttonConfig['variant'] ?? 'primary'), + 'text' => h($buttonConfig['text']), + 'class' => 'modal-confirm-button', + 'params' => [ + 'data-dismiss' => !empty($buttonConfig['clickFunction']) ? '' : 'modal', + 'data-clickFunction' => sprintf('%s', $buttonConfig['clickFunction']) + ] + ]))->button(); + } + return implode('', $buttons); + } } + +class BoostrapCard extends BootstrapGeneric +{ + private $defaultOptions = [ + 'variant' => '', + 'headerText' => '', + 'footerText' => '', + 'bodyText' => '', + 'headerHTML' => '', + 'footerHTML' => '', + 'bodyHTML' => '', + 'headerClass' => '', + 'bodyClass' => '', + 'footerClass' => '', + ]; + + public function __construct($options) + { + $this->allowedOptionValues = [ + 'variant' => array_merge(BootstrapGeneric::$variants, ['']), + ]; + $this->processOptions($options); + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + } + + public function card() + { + return $this->genCard(); + } + + private function genCard() + { + $card = $this->genNode('div', [ + 'class' => [ + 'card', + !empty($this->options['variant']) ? "bg-{$this->options['variant']}" : '', + !empty($this->options['variant']) ? $this->getTextClassForVariant($this->options['variant']) : '', + ], + ], implode('', [$this->genHeader(), $this->genBody(), $this->genFooter()])); + return $card; + } + + private function genHeader() + { + if (empty($this->options['headerHTML']) && empty($this->options['headerText'])) { + return ''; + } + $content = !empty($this->options['headerHTML']) ? $this->options['headerHTML'] : h($this->options['headerText']); + $header = $this->genNode('div', [ + 'class' => [ + 'card-header', + h($this->options['headerClass']), + ], + ], $content); + return $header; + } + + private function genBody() + { + if (empty($this->options['bodyHTML']) && empty($this->options['bodyText'])) { + return ''; + } + $content = !empty($this->options['bodyHTML']) ? $this->options['bodyHTML'] : h($this->options['bodyText']); + $body = $this->genNode('div', [ + 'class' => [ + 'card-body', + h($this->options['bodyClass']), + ], + ], $content); + return $body; + } + + private function genFooter() + { + if (empty($this->options['footerHTML']) && empty($this->options['footerText'])) { + return ''; + } + $content = !empty($this->options['footerHTML']) ? $this->options['footerHTML'] : h($this->options['footerText']); + $footer = $this->genNode('div', [ + 'class' => [ + 'card-footer', + h($this->options['footerClass']), + ], + ], $content); + return $footer; + } +} + +class BoostrapProgress extends BootstrapGeneric { + private $defaultOptions = [ + 'value' => 0, + 'total' => 100, + 'text' => '', + 'title' => '', + 'variant' => 'primary', + 'height' => '', + 'striped' => false, + 'animated' => false, + 'label' => true + ]; + + function __construct($options) { + $this->allowedOptionValues = [ + 'variant' => BootstrapGeneric::$variants, + ]; + $this->processOptions($options); + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + } + + public function progress() + { + return $this->genProgress(); + } + + private function genProgress() + { + $percentage = round(100 * $this->options['value'] / $this->options['total']); + $heightStyle = !empty($this->options['height']) ? sprintf('height: %s;', h($this->options['height'])) : ''; + $widthStyle = sprintf('width: %s%%;', $percentage); + $label = $this->options['label'] ? "{$percentage}%" : ''; + $pb = $this->genNode('div', [ + 'class' => [ + 'progress-bar', + "bg-{$this->options['variant']}", + $this->options['striped'] ? 'progress-bar-striped' : '', + $this->options['animated'] ? 'progress-bar-animated' : '', + ], + 'role' => "progressbar", + 'aria-valuemin' => "0", 'aria-valuemax' => "100",'aria-valuenow' => $percentage, + 'style' => "${widthStyle}", + 'title' => $this->options['title'] + ], $label); + $container = $this->genNode('div', [ + 'class' => [ + 'progress', + ], + 'style' => "${heightStyle}", + 'title' => h($this->options['title']), + ], $pb); + return $container; + } +} + +class BoostrapCollapse extends BootstrapGeneric { + private $defaultOptions = [ + 'text' => '', + 'open' => false, + ]; + + function __construct($options, $content, $btHelper) { + $this->allowedOptionValues = []; + $this->processOptions($options); + $this->content = $content; + $this->btHelper = $btHelper; + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + } + + public function collapse() + { + return $this->genCollapse(); + } + + private function genControl() + { + $html = $this->genNode('a', [ + 'class' => ['text-decoration-none'], + 'data-toggle' => 'collapse', + 'href' => '#collapseExample', + 'role' => 'button', + 'aria-expanded' => 'false', + 'aria-controls' => 'collapseExample', + ], h($this->options['title'])); + return $html; + } + + private function genContent() + { + $content = $this->genNode('div', [ + 'class' => 'card', + ], $this->content); + $container = $this->genNode('div', [ + 'class' => ['collapse', $this->options['open'] ? 'show' : ''], + 'id' => 'collapseExample', + ], $content); + return $container; + } + + private function genCollapse() + { + $html = $this->genControl(); + $html .= $this->genContent(); + return $html; + } +} + +class BoostrapProgressTimeline extends BootstrapGeneric { + private $defaultOptions = [ + 'steps' => [], + 'selected' => 0, + 'variant' => 'info', + 'variantInactive' => 'secondary', + ]; + + function __construct($options, $btHelper) { + $this->allowedOptionValues = [ + 'variant' => BootstrapGeneric::$variants, + 'variantInactive' => BootstrapGeneric::$variants, + ]; + $this->processOptions($options); + $this->btHelper = $btHelper; + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + } + + public function progressTimeline() + { + return $this->genProgressTimeline(); + } + + private function getStepIcon($step, $i, $nodeActive, $lineActive) + { + $icon = $this->genNode('b', [ + 'class' => [ + !empty($step['icon']) ? h($this->btHelper->FontAwesome->getClass($step['icon'])) : '', + $this->getTextClassForVariant($this->options['variant']) + ], + ], empty($step['icon']) ? h($i+1) : ''); + $iconContainer = $this->genNode('span', [ + 'class' => [ + 'd-flex', 'align-items-center', 'justify-content-center', + 'rounded-circle', + $nodeActive ? "bg-{$this->options['variant']}" : "bg-{$this->options['variantInactive']}" + ], + 'style' => 'width:50px; height:50px' + ], $icon); + $li = $this->genNode('li', [ + 'class' => [ + 'd-flex', 'flex-column', + $nodeActive ? 'progress-active' : 'progress-inactive', + ], + ], $iconContainer); + $html = $li . $this->getHorizontalLine($i, $nodeActive, $lineActive); + return $html; + } + + private function getHorizontalLine($i, $nodeActive, $lineActive) + { + $stepCount = count($this->options['steps']); + if ($i == $stepCount-1) { + return ''; + } + $progressBar = (new BoostrapProgress([ + 'label' => false, + 'value' => $nodeActive ? ($lineActive ? 100 : 50) : 0, + 'height' => '2px', + 'variant' => $this->options['variant'] + ]))->progress(); + $line = $this->genNode('span', [ + 'class' => [ + 'progress-line', + 'flex-grow-1', 'align-self-center', + $lineActive ? "bg-{$this->options['variant']}" : '' + ], + ], $progressBar); + return $line; + } + + private function getStepText($step, $isActive) + { + return $this->genNode('li', [ + 'class' => [ + 'text-center', + 'font-weight-bold', + $isActive ? 'progress-active' : 'progress-inactive', + ], + ], h($step['text'] ?? '')); + } + + private function genProgressTimeline() + { + $iconLis = ''; + $textLis = ''; + foreach ($this->options['steps'] as $i => $step) { + $nodeActive = $i <= $this->options['selected']; + $lineActive = $i < $this->options['selected']; + $iconLis .= $this->getStepIcon($step, $i, $nodeActive, $lineActive); + $textLis .= $this->getStepText($step, $nodeActive); + } + $ulIcons = $this->genNode('ul', [ + 'class' => [ + 'd-flex', 'justify-content-around', + ], + ], $iconLis); + $ulText = $this->genNode('ul', [ + 'class' => [ + 'd-flex', 'justify-content-between', + ], + ], $textLis); + $html = $this->genNode('div', [ + 'class' => ['progress-timeline', 'mw-75', 'mx-auto'] + ], $ulIcons . $ulText); + return $html; + } +} \ No newline at end of file diff --git a/src/View/MonadView.php b/src/View/MonadView.php index 6c72ef3..18f38d9 100644 --- a/src/View/MonadView.php +++ b/src/View/MonadView.php @@ -22,7 +22,8 @@ namespace App\View; class MonadView extends AppView { private $additionalTemplatePaths = [ - ROOT . '/libraries/default/RequestProcessors/templates/', + ROOT . '/libraries/default/InboxProcessors/templates/', + ROOT . '/libraries/default/OutboxProcessors/templates/', ]; protected function _paths(?string $plugin = null, bool $cached = true): array diff --git a/templates/EncryptionKeys/add.php b/templates/EncryptionKeys/add.php index 3f1800e..a0b00dd 100644 --- a/templates/EncryptionKeys/add.php +++ b/templates/EncryptionKeys/add.php @@ -2,7 +2,7 @@ echo $this->element('genericElements/Form/genericForm', [ 'data' => [ 'title' => __('Add new encryption key'), - 'description' => __('Alignments indicate that an individual belongs to an organisation in one way or another. The type of relationship is defined by the type field.'), + 'description' => __('Assign encryption keys to the user, used to securely communicate or validate messages coming from the user.'), 'model' => 'Organisations', 'fields' => [ [ diff --git a/templates/Inbox/index.php b/templates/Inbox/index.php index 55eabd3..b9d02e1 100644 --- a/templates/Inbox/index.php +++ b/templates/Inbox/index.php @@ -8,6 +8,16 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => $data, 'top_bar' => [ 'children' => [ + [ + 'children' => [ + [ + 'text' => __('Discard requests'), + 'variant' => 'danger', + 'onclick' => 'discardRequests', + ] + ], + 'type' => 'multi_select_actions', + ], [ 'type' => 'context_filters', 'context_filters' => !empty($filteringContexts) ? $filteringContexts : [] @@ -23,6 +33,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ ] ], 'fields' => [ + [ + 'element' => 'selector', + 'class' => 'short', + 'data' => [ + 'id' => [ + 'value_path' => 'id' + ] + ] + ], [ 'name' => '#', 'sort' => 'id', @@ -95,5 +114,62 @@ echo $this->element('genericElements/IndexTable/index_table', [ ] ] ]); -echo ''; ?> + + \ No newline at end of file diff --git a/templates/Inbox/list_processors.php b/templates/Inbox/list_processors.php new file mode 100644 index 0000000..fd93199 --- /dev/null +++ b/templates/Inbox/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 Inbox Request Processors'), + 'description' => __('The list of Inbox Request Processors available on this server.'), + 'actions' => [ + ] + ] +]); \ No newline at end of file diff --git a/templates/LocalTools/brood_tools.php b/templates/LocalTools/brood_tools.php index ae41159..87a8508 100644 --- a/templates/LocalTools/brood_tools.php +++ b/templates/LocalTools/brood_tools.php @@ -29,6 +29,11 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'name' => __('Description'), 'data_path' => 'description', + ], + [ + 'name' => __('Connected Local Tools'), + 'data_path' => 'local_tool', + 'element' => 'local_tools_status' ] ], 'title' => __('Local tools made available by the remote Cerebrate'), @@ -37,8 +42,9 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'skip_pagination' => 1, 'actions' => [ [ - 'url' => '/localTools/connectionRequest', - 'url_params_data_paths' => ['id'], + 'open_modal' => sprintf('/localTools/connectionRequest/%s/[onclick_params_data_path]', h($id)), + 'reload_url' => $this->Url->build(['action' => 'broodTools', $id]), + 'modal_params_data_path' => 'id', 'title' => 'Issue a connection request', 'icon' => 'plug' ] diff --git a/templates/LocalTools/connection_request.php b/templates/LocalTools/connection_request.php index 8f672e7..0b8a52e 100644 --- a/templates/LocalTools/connection_request.php +++ b/templates/LocalTools/connection_request.php @@ -16,7 +16,12 @@ 'field' => 'local_tool_id', 'options' => $dropdown, 'type' => 'dropdown' - ] + ], + [ + 'field' => 'tool_name', + 'default' => $data['remoteTool']['connectorName'], + 'type' => 'hidden' + ], ], 'submit' => [ 'action' => $this->request->getParam('action') diff --git a/templates/LocalTools/connector_index.php b/templates/LocalTools/connector_index.php index 8a2bf4e..5b2a57c 100644 --- a/templates/LocalTools/connector_index.php +++ b/templates/LocalTools/connector_index.php @@ -60,6 +60,11 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'url_params_data_paths' => ['id'], 'icon' => 'eye' ], + [ + 'open_modal' => '/localTools/connectLocal/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'icon' => 'plug' + ], [ 'open_modal' => '/localTools/edit/[onclick_params_data_path]', 'modal_params_data_path' => 'id', diff --git a/templates/Outbox/index.php b/templates/Outbox/index.php new file mode 100644 index 0000000..c7ac4f9 --- /dev/null +++ b/templates/Outbox/index.php @@ -0,0 +1,151 @@ +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' => [ + [ + 'children' => [ + [ + 'text' => __('Delete messages'), + 'variant' => 'danger', + 'onclick' => 'deleteMessages', + ] + ], + 'type' => 'multi_select_actions', + ], + [ + 'type' => 'context_filters', + 'context_filters' => !empty($filteringContexts) ? $filteringContexts : [] + ], + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value', + 'allowFilering' => true + ] + ] + ], + 'fields' => [ + [ + 'element' => 'selector', + 'class' => 'short', + 'data' => [ + 'id' => [ + 'value_path' => 'id' + ] + ] + ], + [ + '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') + ], + ] + ] +]); +?> + + 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' => [] + ] +); diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php index 9e3ba2e..5807da6 100644 --- a/templates/element/genericElements/Form/genericForm.php +++ b/templates/element/genericElements/Form/genericForm.php @@ -83,7 +83,7 @@ $submitButtonData['ajaxSubmit'] = $ajaxSubmit; } $ajaxFlashMessage = ''; - if ($ajax) { + if (!empty($ajax)) { $ajaxFlashMessage = sprintf( '
%s
', $this->Flash->render() diff --git a/templates/element/genericElements/Form/submitButton.php b/templates/element/genericElements/Form/submitButton.php index 134e857..55062dd 100644 --- a/templates/element/genericElements/Form/submitButton.php +++ b/templates/element/genericElements/Form/submitButton.php @@ -1,5 +1,5 @@ '; + echo ''; foreach ($actions as $action) { if (isset($action['requirement']) && !$action['requirement']) { continue; diff --git a/templates/element/genericElements/IndexTable/Fields/function.php b/templates/element/genericElements/IndexTable/Fields/function.php new file mode 100644 index 0000000..db22a56 --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/function.php @@ -0,0 +1,3 @@ + diff --git a/templates/element/genericElements/IndexTable/Fields/generic_field.php b/templates/element/genericElements/IndexTable/Fields/generic_field.php index 2ef0980..abc8eca 100644 --- a/templates/element/genericElements/IndexTable/Fields/generic_field.php +++ b/templates/element/genericElements/IndexTable/Fields/generic_field.php @@ -18,6 +18,12 @@ ); } else { $data = h($data); + if (!empty($field['options'])) { + $options = $this->Hash->extract($row, $field['options']); + if (!empty($options)) { + $data = h($options[$data]); + } + } if (!empty($field['privacy'])) { $data = sprintf( '**************************************** ', diff --git a/templates/element/genericElements/IndexTable/Fields/local_tools_status.php b/templates/element/genericElements/IndexTable/Fields/local_tools_status.php new file mode 100644 index 0000000..118b915 --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/local_tools_status.php @@ -0,0 +1,13 @@ +Hash->extract($row, 'local_tools'); + $output = []; + foreach ($tools as $tool) { + $output[] = sprintf( + '%s: %s', + h($tool['id']), + h($tool['name']), + h($tool['status']) + ); + } + echo implode('
', $output); +?> diff --git a/templates/element/genericElements/IndexTable/Fields/selector.php b/templates/element/genericElements/IndexTable/Fields/selector.php index f7c6530..006552c 100644 --- a/templates/element/genericElements/IndexTable/Fields/selector.php +++ b/templates/element/genericElements/IndexTable/Fields/selector.php @@ -13,7 +13,7 @@ } } echo sprintf( - '', + '', h($k), empty($data) ? '' : implode(' ', $data) ); diff --git a/templates/element/genericElements/IndexTable/headers.php b/templates/element/genericElements/IndexTable/headers.php index c518b11..d6b33bd 100644 --- a/templates/element/genericElements/IndexTable/headers.php +++ b/templates/element/genericElements/IndexTable/headers.php @@ -14,7 +14,7 @@ $header_data = sprintf( '', empty($header['select_all_class']) ? 'select_all' : $header['select_all_class'], - empty($header['select_all_function']) ? 'onclick="toggleAllAttributeCheckboxes();"' : 'onclick="' . $header['select_all_function'] . '"' + empty($header['select_all_function']) ? 'onclick="toggleAllAttributeCheckboxes(this);"' : 'onclick="' . $header['select_all_function'] . '"' ); } else { $header_data = h($header['name']); @@ -38,14 +38,3 @@ $thead .= ''; echo $thead; ?> - diff --git a/templates/element/genericElements/IndexTable/index_table.php b/templates/element/genericElements/IndexTable/index_table.php index d9b04ec..9a407d4 100644 --- a/templates/element/genericElements/IndexTable/index_table.php +++ b/templates/element/genericElements/IndexTable/index_table.php @@ -93,7 +93,8 @@ } $tbody = '' . $rows . ''; echo sprintf( - '%s%s
', + '%s%s
', + $tableRandomValue, $tableRandomValue, $this->element( '/genericElements/IndexTable/headers', @@ -114,6 +115,7 @@ ?> \ No newline at end of file diff --git a/templates/element/genericElements/ListTopBar/group_search.php b/templates/element/genericElements/ListTopBar/group_search.php index 7822e07..814ffcc 100644 --- a/templates/element/genericElements/ListTopBar/group_search.php +++ b/templates/element/genericElements/ListTopBar/group_search.php @@ -12,6 +12,9 @@ * - id: element ID for the input field - defaults to quickFilterField */ if (!isset($data['requirement']) || $data['requirement']) { + if (!empty($data['quickFilter'])) { + $quickFilter = $data['quickFilter']; + } $filterEffective = !empty($quickFilter); // No filters will be picked up, thus rendering the filtering useless $filteringButton = ''; if (!empty($data['allowFilering'])) { diff --git a/templates/element/genericElements/SingleViews/Fields/jsonField.php b/templates/element/genericElements/SingleViews/Fields/jsonField.php index 9c8b16b..ef14c58 100644 --- a/templates/element/genericElements/SingleViews/Fields/jsonField.php +++ b/templates/element/genericElements/SingleViews/Fields/jsonField.php @@ -4,7 +4,7 @@ $string = $field['raw']; } else { $value = Cake\Utility\Hash::extract($data, $field['path']); - $string = empty($value[0]) ? '' : $value[0]; + $string = count($value) == 0 ? '' : $value[0]; } echo sprintf( '
', diff --git a/templates/genericTemplates/index_simple.php b/templates/element/genericElements/index_simple.php similarity index 100% rename from templates/genericTemplates/index_simple.php rename to templates/element/genericElements/index_simple.php diff --git a/templates/genericTemplates/delete.php b/templates/genericTemplates/delete.php index 6b07d6a..f2b7449 100644 --- a/templates/genericTemplates/delete.php +++ b/templates/genericTemplates/delete.php @@ -1,39 +1,31 @@ - - +element('genericElements/Form/genericForm', [ + 'entity' => null, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'fields' => [ + [ + 'type' => 'text', + 'field' => 'ids', + 'default' => !empty($id) ? json_encode([$id]) : '' + ] + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] +]); +$formHTML = sprintf('
%s
', $form); + +$bodyMessage = !empty($deletionText) ? __($deletionText) : __('Are you sure you want to delete {0} #{1}?', h(Cake\Utility\Inflector::singularize($this->request->getParam('controller'))), h($id)); +$bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage); + +echo $this->Bootstrap->modal([ + 'size' => 'lg', + 'title' => !empty($deletionTitle) ? $deletionTitle : __('Delete {0}', h(Cake\Utility\Inflector::singularize(Cake\Utility\Inflector::humanize($this->request->getParam('controller'))))), + 'type' => 'confirm-danger', + 'confirmText' => !empty($deletionConfirm) ? $deletionConfirm : __('Delete'), + 'bodyHtml' => $bodyHTML, +]); +?> diff --git a/webroot/css/bootstrap-additional.css b/webroot/css/bootstrap-additional.css index 981324c..95e2e80 100644 --- a/webroot/css/bootstrap-additional.css +++ b/webroot/css/bootstrap-additional.css @@ -1,3 +1,23 @@ +/* utils */ +.mw-75 { + max-width: 75% !important; + } +.mw-50 { + max-width: 50% !important; +} +.mw-25 { + max-width: 25% !important; +} +.mh-75 { + max-width: 75% !important; +} +.mh-50 { + max-width: 50% !important; +} +.mh-25 { + max-width: 25% !important; +} + /* Toast */ .toast { min-width: 250px; @@ -66,3 +86,25 @@ .toast-dark strong { color: #040505; } + +div.progress-timeline { + padding: 0.2em 0.2em 0.5em 0.2em; +} +div.progress-timeline ul { + position: relative; + padding: 0; +} +div.progress-timeline li { + list-style-type: none; + position: relative +} +div.progress-timeline li.progress-inactive { + opacity: 0.5; +} +div.progress-timeline .progress-line { + height: 2px; + /* background: gray; */ +} +div.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} \ No newline at end of file diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index 8cb39e3..2d50a49 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -31,7 +31,7 @@ class UIFactory { * @param {ModalFactory~POSTFailCallback} POSTFailCallback - The callback that handles form submissions errors and validation errors. * @return {Promise} Promise object resolving to the ModalFactory object */ - submissionModal(url, POSTSuccessCallback, POSTFailCallback) { + async submissionModal(url, POSTSuccessCallback, POSTFailCallback) { return AJAXApi.quickFetchURL(url).then((modalHTML) => { const theModal = new ModalFactory({ rawHtml: modalHTML, @@ -41,7 +41,13 @@ class UIFactory { theModal.makeModal() theModal.show() theModal.$modal.data('modalObject', theModal) - return theModal + return [theModal, theModal.ajaxApi] + }).catch((error) => { + UI.toast({ + variant: 'danger', + title: 'Error while loading the processor', + body: error.message + }) }) } @@ -83,6 +89,11 @@ class UIFactory { return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode); } + getContainerForTable($table) { + const tableRandomID = $table.data('table-random-value') + return $table.closest(`#table-container-${tableRandomID}`) + } + /** * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation. * Supports `displayOnSuccess` option to show another modal after the submission @@ -113,7 +124,7 @@ class UIFactory { $statusNode = $elligibleTable } else { if ($table instanceof jQuery) { - $reloadedElement = $table + $reloadedElement = this.getContainerForTable($table) $statusNode = $table.find('table.table') } else { $reloadedElement = $(`#table-container-${$table}`) @@ -141,7 +152,6 @@ class UIFactory { */ submissionModalAutoGuess(url, reloadUrl=false, $table=false) { let currentAction = location.pathname.split('/')[2] - currentAction += 'cdsc' if (currentAction !== undefined) { if (currentAction === 'index') { return UI.submissionModalForIndex(url, reloadUrl, $table) @@ -386,6 +396,7 @@ class ModalFactory { if (this.options.backdropStatic) { this.bsModalOptions['backdrop'] = 'static' } + this.ajaxApi = new AJAXApi() } /** @@ -651,8 +662,8 @@ class ModalFactory { const $buttonConfirm = $('') .addClass('btn-' + variant) .text(this.options.confirmText) - .click(this.getConfirmationHandlerFunction()) .attr('data-dismiss', (this.options.closeManually || this.options.closeOnSuccess) ? '' : 'modal') + $buttonConfirm.click(this.getConfirmationHandlerFunction($buttonConfirm)) return [$buttonCancel, $buttonConfirm] } @@ -662,14 +673,28 @@ class ModalFactory { } /** Generate the function that will be called when the user confirm the modal */ - getConfirmationHandlerFunction() { + getConfirmationHandlerFunction($buttonConfirm, buttonIndex) { + if (this.options.APIConfirms) { + if (Array.isArray(this.ajaxApi)) { + const tmpApi = new AJAXApi({ + statusNode: $buttonConfirm[0] + }) + this.ajaxApi.push(tmpApi) + } else { + this.ajaxApi.statusNode = $buttonConfirm[0] + this.ajaxApi = [this.ajaxApi]; + } + } else { + this.ajaxApi.statusNode = $buttonConfirm[0] + } return (evt) => { let confirmFunction = this.options.confirm - if (this.options.APIConfirm) { - const tmpApi = new AJAXApi({ - statusNode: evt.target - }) - confirmFunction = () => { return this.options.APIConfirm(tmpApi) } + if (this.options.APIConfirms) { + if (buttonIndex !== undefined && this.options.APIConfirms[buttonIndex] !== undefined) { + confirmFunction = () => { return this.options.APIConfirms[buttonIndex](this.ajaxApi[buttonIndex]) } + } + } else if (this.options.APIConfirm) { + confirmFunction = () => { return this.options.APIConfirm(this.ajaxApi) } } let confirmResult = confirmFunction(() => { this.hide() }, this, evt) if (confirmResult === undefined) { @@ -689,58 +714,91 @@ class ModalFactory { /** Attach the submission click listener for modals that have been generated by raw HTML */ findSubmitButtonAndAddListener() { - let $submitButton = this.$modal.find('.modal-footer #submitButton') - if (!$submitButton[0]) { - $submitButton = this.$modal.find('.modal-footer .modal-confirm-button') - } - if ($submitButton[0]) { - const formID = $submitButton.data('form-id') - let $form - if (formID) { - $form = $(formID) - } else { - $form = this.$modal.find('form') + let $modalFooter = this.$modal.find('.modal-footer') + if ($modalFooter.data('custom-footer')) { // setup basic listener as callback are defined in the template + let $submitButtons = this.$modal.find('.modal-footer .modal-confirm-button') + var selfModal = this; + selfModal.options.APIConfirms = []; + $submitButtons.each(function(i) { + const $submitButton = $(this) + if ($submitButton.data('clickfunction') !== undefined && $submitButton.data('clickfunction') !== '') { + const clickHandler = window[$submitButton.data('clickfunction')] + selfModal.options.APIConfirms[i] = (tmpApi) => { + let clickResult = clickHandler(selfModal, tmpApi) + if (clickResult !== undefined) { + return clickResult + .then((data) => { + if (data.success) { + selfModal.options.POSTSuccessCallback(data) + } else { // Validation error + selfModal.injectFormValidationFeedback(form, data.errors) + return Promise.reject('Validation error'); + } + }) + .catch((errorMessage) => { + selfModal.options.POSTFailCallback(errorMessage) + return Promise.reject(errorMessage); + }) + } + } + } + $submitButton.click(selfModal.getConfirmationHandlerFunction($submitButton, i)) + }) + } else { + let $submitButton = this.$modal.find('.modal-footer #submitButton') + if (!$submitButton[0]) { + $submitButton = this.$modal.find('.modal-footer .modal-confirm-button') } - if ($submitButton.data('confirmfunction') !== undefined && $submitButton.data('confirmfunction') !== '') { - const clickHandler = window[$submitButton.data('confirmfunction')] - this.options.APIConfirm = (tmpApi) => { - let clickResult = clickHandler(this, tmpApi) - if (clickResult !== undefined) { - return clickResult + if ($submitButton[0]) { + const formID = $submitButton.data('form-id') + let $form + if (formID) { + $form = $(formID) + } else { + $form = this.$modal.find('form') + } + if ($submitButton.data('confirmfunction') !== undefined && $submitButton.data('confirmfunction') !== '') { + const clickHandler = window[$submitButton.data('confirmfunction')] + this.options.APIConfirm = (tmpApi) => { + let clickResult = clickHandler(this, tmpApi) + if (clickResult !== undefined) { + return clickResult + .then((data) => { + if (data.success) { + this.options.POSTSuccessCallback(data) + } else { // Validation error + this.injectFormValidationFeedback(form, data.errors) + return Promise.reject('Validation error'); + } + }) + .catch((errorMessage) => { + this.options.POSTFailCallback(errorMessage) + return Promise.reject(errorMessage); + }) + } + } + } else { + $submitButton[0].removeAttribute('onclick') + this.options.APIConfirm = (tmpApi) => { + return tmpApi.postForm($form[0]) .then((data) => { if (data.success) { - this.options.POSTSuccessCallback(data) + // this.options.POSTSuccessCallback(data) + this.options.POSTSuccessCallback([data, this]) } else { // Validation error this.injectFormValidationFeedback(form, data.errors) return Promise.reject('Validation error'); } }) .catch((errorMessage) => { - this.options.POSTFailCallback(errorMessage) + this.options.POSTFailCallback([errorMessage, this]) + // this.options.POSTFailCallback(errorMessage) return Promise.reject(errorMessage); }) } } - } else { - $submitButton[0].removeAttribute('onclick') - this.options.APIConfirm = (tmpApi) => { - return tmpApi.postForm($form[0]) - .then((data) => { - if (data.success) { - this.options.POSTSuccessCallback(data) - } else { // Validation error - this.injectFormValidationFeedback(form, data.errors) - return Promise.reject('Validation error'); - } - }) - .catch((errorMessage) => { - this.options.POSTFailCallback(errorMessage) - return Promise.reject(errorMessage); - }) - } + $submitButton.click(this.getConfirmationHandlerFunction($submitButton)) } - $submitButton.click(this.getConfirmationHandlerFunction()) - } } } @@ -956,13 +1014,17 @@ class FormValidationHelper { } else { $messageNode.addClass('invalid-feedback') } - const hasMultipleErrors = Object.keys(errors).length > 1 - for (const [ruleName, error] of Object.entries(errors)) { - if (hasMultipleErrors) { - $messageNode.append($('
  • ').text(error)) - } else { - $messageNode.text(error) + if (typeof errors === 'object') { + const hasMultipleErrors = Object.keys(errors).length > 1 + for (const [ruleName, error] of Object.entries(errors)) { + if (hasMultipleErrors) { + $messageNode.append($('
  • ').text(error)) + } else { + $messageNode.text(error) + } } + } else { + $messageNode.text(errors) } return $messageNode } diff --git a/webroot/js/main.js b/webroot/js/main.js index ea39c37..d950781 100644 --- a/webroot/js/main.js +++ b/webroot/js/main.js @@ -18,6 +18,13 @@ function executeStateDependencyChecks(dependenceSourceSelector) { }); } +function toggleAllAttributeCheckboxes(clicked) { + let $clicked = $(clicked) + let $table = $clicked.closest('table') + let $inputs = $table.find('input.selectable_row') + $inputs.prop('checked', $clicked.prop('checked')) +} + function testConnection(id) { $container = $(`#connection_test_${id}`) UI.overlayUntilResolve(