mirror of https://github.com/MISP/MISP
new: [inbox] Migrated inbox system from Cerebrate
- TODO: Migrate changes done in Cerebrate's crudcomponent3.x-inbox
parent
db56924f67
commit
f7c1fb5fd7
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
|
||||
final class AddTableInbox extends AbstractMigration
|
||||
{
|
||||
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
|
||||
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('inbox')->drop()->save(); // restart from fresh
|
||||
$table = $this->table('inbox', [
|
||||
'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 on which the request should be performed onto',
|
||||
])
|
||||
->addColumn('action', 'string', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
'limit' => 191,
|
||||
'comment' => 'A specific action belonging to the model',
|
||||
])
|
||||
->addColumn('title', 'string', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
'limit' => 191,
|
||||
])
|
||||
->addColumn('origin', 'string', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
'limit' => 191,
|
||||
])
|
||||
->addColumn('user_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'length' => null,
|
||||
])
|
||||
->addColumn('message', 'text', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
])
|
||||
->addColumn('data', 'text', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'limit' => MysqlAdapter::TEXT_LONG
|
||||
])
|
||||
->addColumn('severity', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addColumn('created', 'datetime', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('modified', '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('origin')
|
||||
->addIndex('created')
|
||||
->addIndex('modified')
|
||||
->addIndex('user_id')
|
||||
->addIndex('severity');
|
||||
|
||||
$table->create();
|
||||
}
|
||||
}
|
|
@ -1,418 +0,0 @@
|
|||
<?php
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Filesystem\File;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
|
||||
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericInboxProcessor.php');
|
||||
|
||||
class LocalToolInboxProcessor extends GenericInboxProcessor
|
||||
{
|
||||
protected $scope = 'LocalTool';
|
||||
protected $action = 'not-specified'; //overriden when extending
|
||||
protected $description = ''; // overriden when extending
|
||||
protected $registeredActions = [
|
||||
'IncomingConnectionRequest',
|
||||
'AcceptedRequest',
|
||||
'DeclinedRequest',
|
||||
];
|
||||
protected $processingTemplate = 'LocalTool/GenericRequest';
|
||||
protected $Broods;
|
||||
protected $LocalTools;
|
||||
|
||||
public function __construct($loadFromAction=false)
|
||||
{
|
||||
parent::__construct($loadFromAction);
|
||||
$this->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')
|
||||
->notEmptyString('connectorName', 'The connector name must be provided')
|
||||
->requirePresence('cerebrateURL')
|
||||
->notEmptyString('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);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericInboxProcessor.php');
|
||||
|
||||
class SynchronisationInboxProcessor extends GenericInboxProcessor
|
||||
{
|
||||
protected $scope = 'Synchronisation';
|
||||
protected $action = 'not-specified'; //overriden when extending
|
||||
protected $description = ''; // overriden when extending
|
||||
protected $registeredActions = [
|
||||
'DataExchange'
|
||||
];
|
||||
|
||||
public function __construct($loadFromAction=false) {
|
||||
parent::__construct($loadFromAction);
|
||||
}
|
||||
|
||||
public function create($requestData)
|
||||
{
|
||||
return parent::create($requestData);
|
||||
}
|
||||
}
|
||||
|
||||
class DataExchangeProcessor extends SynchronisationInboxProcessor implements GenericInboxProcessorActionI {
|
||||
public $action = 'DataExchange';
|
||||
protected $description;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->description = __('Handle exchange of data between two cerebrate instances');
|
||||
$this->Users = TableRegistry::getTableLocator()->get('Users');
|
||||
}
|
||||
|
||||
protected function addValidatorRules($validator)
|
||||
{
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function create($requestData) {
|
||||
$this->validateRequestData($requestData);
|
||||
$requestData['title'] = __('Data exchange requested for record `{0}`', 'recordname');
|
||||
return parent::create($requestData);
|
||||
}
|
||||
|
||||
public function process($id, $requestData, $inboxRequest)
|
||||
{
|
||||
$dataExchangeAccepted = false;
|
||||
$saveResult = [];
|
||||
if ($dataExchangeAccepted) {
|
||||
$this->discard($id, $requestData);
|
||||
}
|
||||
return $this->genActionResult(
|
||||
$saveResult,
|
||||
$dataExchangeAccepted,
|
||||
$dataExchangeAccepted ? __('Record `{0}` exchanged', 'recordname') : __('Could not exchange record `{0}`.', 'recordname'),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
public function discard($id, $requestData)
|
||||
{
|
||||
return parent::discard($id, $requestData);
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
<?php
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'OutboxProcessors' . DS . 'GenericOutboxProcessor.php');
|
||||
|
||||
class BroodsOutboxProcessor extends GenericOutboxProcessor
|
||||
{
|
||||
protected $scope = 'Broods';
|
||||
protected $action = 'not-specified'; //overriden when extending
|
||||
protected $description = ''; // overriden when extending
|
||||
protected $registeredActions = [
|
||||
'ResendFailedMessage',
|
||||
];
|
||||
|
||||
public function __construct($loadFromAction=false) {
|
||||
parent::__construct($loadFromAction);
|
||||
}
|
||||
|
||||
public function create($requestData)
|
||||
{
|
||||
return parent::create($requestData);
|
||||
}
|
||||
|
||||
protected function getIssuerBrood($broodId)
|
||||
{
|
||||
$brood = $this->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();
|
||||
if (is_null($jsonReply)) {
|
||||
$jsonReply = [
|
||||
'success' => false,
|
||||
'errors' => [
|
||||
__('Brood returned an invalid JSON.')
|
||||
]
|
||||
];
|
||||
}
|
||||
$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,
|
||||
$jsonReply['errors'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public function discard($id, $requestData)
|
||||
{
|
||||
return parent::discard($id, $requestData);
|
||||
}
|
||||
}
|
|
@ -1,218 +0,0 @@
|
|||
<?php
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Filesystem\File;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\View\ViewBuilder;
|
||||
use Cake\Routing\Router;
|
||||
|
||||
interface GenericOutboxProcessorActionI
|
||||
{
|
||||
public function create($requestData);
|
||||
public function process($requestID, $serverRequest, $outboxRequest);
|
||||
public function discard($requestID ,$requestData);
|
||||
}
|
||||
|
||||
class GenericOutboxProcessor
|
||||
{
|
||||
protected $Outbox;
|
||||
protected $registeredActions = [];
|
||||
protected $validator;
|
||||
protected $processingTemplate = '/genericTemplates/confirm';
|
||||
protected $processingTemplatesDirectory = ROOT . '/libraries/default/OutboxProcessors/templates';
|
||||
|
||||
public function __construct($registerActions=false) {
|
||||
$this->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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericOutboxProcessor.php');
|
||||
|
||||
class SCOPE_OutboxProcessor extends GenericOutboxProcessor
|
||||
{
|
||||
protected $scope = '~to-be-defined~';
|
||||
protected $action = 'not-specified'; //overriden when extending
|
||||
protected $description = ''; // overriden when extending
|
||||
protected $registeredActions = [
|
||||
'ACTION'
|
||||
];
|
||||
|
||||
public function __construct($loadFromAction=false) {
|
||||
parent::__construct($loadFromAction);
|
||||
}
|
||||
|
||||
public function create($requestData)
|
||||
{
|
||||
return parent::create($requestData);
|
||||
}
|
||||
}
|
||||
|
||||
class SCOPE_ACTION_Processor extends ProposalOutboxProcessor implements GenericOutboxProcessorActionI {
|
||||
public $action = 'ACTION';
|
||||
protected $description;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -202,6 +202,11 @@ class SidemenuNavigation
|
|||
'icon' => $this->iconTable['Roles'],
|
||||
'url' => '/roles/index',
|
||||
],
|
||||
'Messages' => [
|
||||
'label' => __('Inbox'),
|
||||
'icon' => $this->iconTable['Inbox'],
|
||||
'url' => '/inbox/index',
|
||||
],
|
||||
'ServerSettings' => [
|
||||
'label' => __('Settings & Maintenance'),
|
||||
'icon' => $this->iconTable['ServerSettings'],
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
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;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\ForbiddenException;
|
||||
|
||||
|
||||
class InboxController extends AppController
|
||||
{
|
||||
public $filterFields = ['scope', 'action', 'Inbox.created', 'severity', 'title', 'origin', 'message', 'Users.id', ['name' => 'Users.username', 'multiple' => true, 'options' => 'getAllUsername', 'select2' => true],];
|
||||
public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true], 'origin'];
|
||||
public $containFields = ['Users'];
|
||||
|
||||
public $paginate = [
|
||||
'order' => [
|
||||
'Inbox.created' => 'desc'
|
||||
],
|
||||
];
|
||||
|
||||
public function beforeFilter(EventInterface $event)
|
||||
{
|
||||
parent::beforeFilter($event);
|
||||
$this->set('metaGroup', 'Administration');
|
||||
}
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'contextFilters' => [
|
||||
'custom' => [
|
||||
[
|
||||
'default' => true,
|
||||
'label' => __('My Notifications'),
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where(function(QueryExpression $exp) {
|
||||
return $exp->or(['user_id' => $this->ACL->getUser()['id']])
|
||||
->isNull('user_id');
|
||||
});
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => __('User Registration'),
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'scope' => 'User',
|
||||
'action' => 'Registration',
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:primary',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_PRIMARY,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:info',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_INFO,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:warning',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_WARNING,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:danger',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_DANGER,
|
||||
]);
|
||||
}
|
||||
],
|
||||
],
|
||||
],
|
||||
'contain' => $this->containFields
|
||||
]);
|
||||
$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)
|
||||
{
|
||||
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 message'));
|
||||
if (!empty($id)) {
|
||||
$this->set('deletionText', __('Are you sure you want to discard message #{0}?', $id));
|
||||
} else {
|
||||
$this->set('deletionText', __('Are you sure you want to discard the selected message?'));
|
||||
}
|
||||
$this->set('deletionConfirm', __('Discard'));
|
||||
$this->CRUD->delete($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function process($id)
|
||||
{
|
||||
$request = $this->Inbox->get($id, ['contain' => ['Users' => ['Individuals' => ['Alignments' => 'Organisations']]]]);
|
||||
$scope = $request->scope;
|
||||
$action = $request->action;
|
||||
$this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||
$processor = $this->InboxProcessors->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->inboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||
$processors = $this->inboxProcessors->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 = [
|
||||
'origin' => $this->request->clientIp(),
|
||||
'user_id' => $this->ACL->getUser()['id'],
|
||||
];
|
||||
$entryData['data'] = $this->request->getData() ?? [];
|
||||
$this->inboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||
$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'));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\Filesystem\Folder;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Core\Exception\Exception;
|
||||
|
||||
class MissingInboxProcessorException extends Exception
|
||||
{
|
||||
protected $_defaultCode = 404;
|
||||
}
|
||||
|
||||
class InboxProcessorsTable extends AppTable
|
||||
{
|
||||
private $processorsDirectory = ROOT . '/libraries/default/InboxProcessors';
|
||||
private $inboxProcessors;
|
||||
private $enabledProcessors = [ // to be defined in config
|
||||
'Proposal' => [
|
||||
'ProposalEdit' => 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 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Database\Schema\TableSchemaInterface;
|
||||
// use Cake\Database\Type;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\ORM\RulesChecker;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
|
||||
use App\Utility\UI\Notification;
|
||||
|
||||
// Type::map('json', 'Cake\Database\Type\JsonType'); // Not sure what it is for but it was in Cerebrate
|
||||
|
||||
class InboxTable extends AppTable
|
||||
{
|
||||
public const SEVERITY_PRIMARY = 0,
|
||||
SEVERITY_INFO = 1,
|
||||
SEVERITY_WARNING = 2,
|
||||
SEVERITY_DANGER = 3;
|
||||
|
||||
public $severityVariant = [
|
||||
self::SEVERITY_PRIMARY => 'primary',
|
||||
self::SEVERITY_INFO => 'info',
|
||||
self::SEVERITY_WARNING => 'warning',
|
||||
self::SEVERITY_DANGER => 'danger',
|
||||
];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->addBehavior(
|
||||
'JsonFields',
|
||||
[
|
||||
'fields' => ['data' => []],
|
||||
]
|
||||
);
|
||||
$this->belongsTo('Users');
|
||||
$this->setDisplayField('title');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->notEmptyString('scope')
|
||||
->notEmptyString('action')
|
||||
->notEmptyString('title')
|
||||
->notEmptyString('origin')
|
||||
->datetime('created')
|
||||
|
||||
->requirePresence([
|
||||
'scope' => ['message' => __('The field `scope` is required')],
|
||||
'action' => ['message' => __('The field `action` is required')],
|
||||
'title' => ['message' => __('The field `title` is required')],
|
||||
'origin' => ['message' => __('The field `origin` 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 getAllUsername($currentUser): array
|
||||
{
|
||||
$this->Users = \Cake\ORM\TableRegistry::getTableLocator()->get('Users');
|
||||
$conditions = [];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$conditions['organisation_id IN'] = [$currentUser['organisation_id']];
|
||||
}
|
||||
$users = $this->Users->find()->where($conditions)->all()->extract('username')->toList();
|
||||
return Hash::combine($users, '{n}', '{n}');
|
||||
}
|
||||
|
||||
public function createEntry($entryData)
|
||||
{
|
||||
$savedEntry = $this->save($entryData);
|
||||
return $savedEntry;
|
||||
}
|
||||
|
||||
public function collectNotifications(\App\Model\Entity\User $user): array
|
||||
{
|
||||
$allNotifications = [];
|
||||
$inboxNotifications = $this->getNotificationsForUser($user);
|
||||
foreach ($inboxNotifications as $notification) {
|
||||
$title = $notification->title;
|
||||
$details = $notification->message;
|
||||
$router = [
|
||||
'controller' => 'inbox',
|
||||
'action' => 'process',
|
||||
'plugin' => null,
|
||||
$notification->id
|
||||
];
|
||||
$allNotifications[] = (new Notification($title, $router, [
|
||||
'icon' => 'envelope',
|
||||
'details' => $details,
|
||||
'datetime' => $notification->created,
|
||||
'variant' => $notification->severity_variant,
|
||||
'_useModal' => true,
|
||||
'_sidebarId' => 'inbox',
|
||||
]))->get();
|
||||
}
|
||||
return $allNotifications;
|
||||
}
|
||||
|
||||
public function getNotificationsForUser(\App\Model\Entity\User $user): iterable
|
||||
{
|
||||
$query = $this->find();
|
||||
$conditions = [
|
||||
'Inbox.user_id' => $user->id
|
||||
];
|
||||
$query->where($conditions);
|
||||
$notifications = $query->all();
|
||||
return $notifications;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
echo $this->Html->scriptBlock(sprintf(
|
||||
'var csrfToken = %s;',
|
||||
json_encode($this->request->getAttribute('csrfToken'))
|
||||
));
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'multi_select_actions',
|
||||
'children' => [
|
||||
[
|
||||
'text' => __('Discard message'),
|
||||
'variant' => 'danger',
|
||||
'onclick' => 'discardMessages',
|
||||
]
|
||||
],
|
||||
'data' => [
|
||||
'id' => [
|
||||
'value_path' => 'id'
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'context_filters',
|
||||
'context_filters' => !empty($filteringContexts) ? $filteringContexts : []
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
'allowFilering' => true
|
||||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
'table_setting_id' => 'inbox_index',
|
||||
]
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => '#',
|
||||
'sort' => 'Inbox.id',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => 'created',
|
||||
'sort' => 'Inbox.created',
|
||||
'data_path' => 'created',
|
||||
'element' => 'datetime'
|
||||
],
|
||||
[
|
||||
'name' => 'severity',
|
||||
'sort' => 'severity',
|
||||
'data_path' => 'severity',
|
||||
'element' => 'function',
|
||||
'function' => function ($entry, $context) {
|
||||
return $context->Bootstrap->badge([
|
||||
'text' => $entry->severity_variant,
|
||||
'variant' => $entry->severity_variant,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'name' => 'scope',
|
||||
'sort' => 'scope',
|
||||
'data_path' => 'scope',
|
||||
],
|
||||
[
|
||||
'name' => 'action',
|
||||
'sort' => 'action',
|
||||
'data_path' => 'action',
|
||||
],
|
||||
[
|
||||
'name' => 'title',
|
||||
'sort' => 'title',
|
||||
'data_path' => 'title',
|
||||
],
|
||||
[
|
||||
'name' => 'origin',
|
||||
'sort' => 'origin',
|
||||
'data_path' => 'origin',
|
||||
],
|
||||
[
|
||||
'name' => 'user',
|
||||
'sort' => 'Inbox.user_id',
|
||||
'data_path' => 'user',
|
||||
'element' => 'user'
|
||||
],
|
||||
[
|
||||
'name' => 'message',
|
||||
'sort' => 'message',
|
||||
'data_path' => 'message',
|
||||
],
|
||||
],
|
||||
'title' => __('Inbox'),
|
||||
'description' => __('A list of requests to be manually processed'),
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/inbox/view',
|
||||
'url_params_data_paths' => ['id'],
|
||||
'icon' => 'eye',
|
||||
'title' => __('View request')
|
||||
],
|
||||
[
|
||||
'open_modal' => '/inbox/process/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'cogs',
|
||||
'title' => __('Process request')
|
||||
],
|
||||
[
|
||||
'open_modal' => '/inbox/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash',
|
||||
'title' => __('Discard message')
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
|
||||
<script>
|
||||
function discardMessages(idList, selectedData, $table) {
|
||||
const successCallback = function([data, modalObject]) {
|
||||
UI.reload('/inbox/index', UI.getContainerForTable($table), $table)
|
||||
}
|
||||
const failCallback = ([data, modalObject]) => {
|
||||
const tableData = selectedData.map(row => {
|
||||
entryInError = data.filter(error => error.data.id == row.id)[0]
|
||||
$faIcon = $('<i class="fa"></i>').addClass(entryInError.success ? 'fa-check text-success' : 'fa-times text-danger')
|
||||
return [row.id, row.scope, row.action, row.title, entryInError.message, JSON.stringify(entryInError.errors), $faIcon]
|
||||
});
|
||||
handleMessageTable(
|
||||
modalObject.$modal,
|
||||
['<?= __('ID') ?>', '<?= __('Scope') ?>', '<?= __('Action') ?>', '<?= __('Title') ?>', '<?= __('Message') ?>', '<?= __('Error') ?>', '<?= __('State') ?>'],
|
||||
tableData
|
||||
)
|
||||
const $footer = $(modalObject.ajaxApi.statusNode).parent()
|
||||
modalObject.ajaxApi.statusNode.remove()
|
||||
const $cancelButton = $footer.find('button[data-bs-dismiss="modal"]')
|
||||
$cancelButton.text('<?= __('OK') ?>').removeClass('btn-secondary').addClass('btn-primary')
|
||||
}
|
||||
UI.submissionModal('/inbox/delete', successCallback, failCallback).then(([modalObject, ajaxApi]) => {
|
||||
const $idsInput = modalObject.$modal.find('form').find('input#ids-field')
|
||||
$idsInput.val(JSON.stringify(idList))
|
||||
const tableData = selectedData.map(row => {
|
||||
return [row.id, row.scope, row.action, row.title]
|
||||
});
|
||||
handleMessageTable(
|
||||
modalObject.$modal,
|
||||
['<?= __('ID') ?>', '<?= __('Scope') ?>', '<?= __('Action') ?>', '<?= __('Title') ?>'],
|
||||
tableData
|
||||
)
|
||||
})
|
||||
|
||||
function constructMessageTable(header, data) {
|
||||
return HtmlHelper.table(
|
||||
header,
|
||||
data,
|
||||
{
|
||||
small: true,
|
||||
borderless: true,
|
||||
tableClass: ['message-table', 'mt-4 mb-0'],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleMessageTable($modal, header, data) {
|
||||
const $modalBody = $modal.find('.modal-body')
|
||||
const $messageTable = $modalBody.find('table.message-table')
|
||||
const messageTableHTML = constructMessageTable(header, data)[0].outerHTML
|
||||
if ($messageTable.length) {
|
||||
$messageTable.html(messageTableHTML)
|
||||
} else {
|
||||
$modalBody.append(messageTableHTML)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
echo $this->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' => __('Search'),
|
||||
'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' => [
|
||||
]
|
||||
]
|
||||
]);
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
echo $this->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' => 'origin',
|
||||
'path' => 'origin',
|
||||
],
|
||||
[
|
||||
'key' => 'user_id',
|
||||
'path' => 'user_id',
|
||||
],
|
||||
[
|
||||
'key' => 'message',
|
||||
'path' => 'message',
|
||||
],
|
||||
[
|
||||
'key' => 'comment',
|
||||
'path' => 'comment',
|
||||
],
|
||||
[
|
||||
'key' => 'data',
|
||||
'path' => 'data',
|
||||
'type' => 'json'
|
||||
],
|
||||
],
|
||||
'children' => []
|
||||
]
|
||||
);
|
Loading…
Reference in New Issue