Compare commits
37 Commits
12a5e826df
...
cccb715996
Author | SHA1 | Date |
---|---|---|
DocArmoryTech | cccb715996 | |
iglocska | d3f08205fb | |
iglocska | be64a186e0 | |
iglocska | e39a651c5b | |
iglocska | 0d7b73cafe | |
iglocska | e192c7844b | |
Sami Mokaddem | f0ba0d8316 | |
Sami Mokaddem | dd4ce865bf | |
Sami Mokaddem | 63593cfd56 | |
iglocska | 92b35f9306 | |
iglocska | 9305e7ceea | |
Sami Mokaddem | 1a7320e363 | |
Sami Mokaddem | b987444da2 | |
Sami Mokaddem | 1c6c7f346a | |
Sami Mokaddem | 3ea5b7830d | |
iglocska | d23e393a9a | |
iglocska | 49d4c959aa | |
iglocska | 58f75e44c1 | |
iglocska | 6d6a0ec993 | |
iglocska | 6b53d6d81a | |
iglocska | b01a3bf83e | |
iglocska | a0fedb011c | |
Sami Mokaddem | b6b4310da4 | |
Sami Mokaddem | 845ba2bfc9 | |
Sami Mokaddem | 0d5dee3524 | |
Luciano Righetti | 16f8ea8c2b | |
Luciano Righetti | a42599be32 | |
Luciano Righetti | eb95c44528 | |
Luciano Righetti | 47ff4db826 | |
Luciano Righetti | 9ba00dc334 | |
Luciano Righetti | 4ebdbf8ba3 | |
Luciano Righetti | 9b7c693bb9 | |
Luciano Righetti | d1cd1da67f | |
Luciano Righetti | fe124bb658 | |
Luciano Righetti | b733ca169e | |
DocArmoryTech | 9f689cff24 | |
DocArmoryTech | cf565484b9 |
|
@ -2,7 +2,7 @@ name: test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, develop]
|
branches: [main, develop, fix-test-action]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, develop]
|
branches: [main, develop]
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04]
|
os: [ubuntu-20.04]
|
||||||
php: ["7.4"]
|
php: ["8.2"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Create config files
|
- name: Create config files
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -6,6 +6,8 @@ tmp
|
||||||
vendor
|
vendor
|
||||||
webroot/theme/node_modules
|
webroot/theme/node_modules
|
||||||
webroot/scss/*.css
|
webroot/scss/*.css
|
||||||
|
webroot/js/node_modules/
|
||||||
|
!webroot/js/node_modules/mermaid/dist/
|
||||||
.vscode
|
.vscode
|
||||||
docker/run/
|
docker/run/
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Migrations\AbstractMigration;
|
||||||
|
use Phinx\Db\Adapter\MysqlAdapter;
|
||||||
|
|
||||||
|
final class SGExtend extends AbstractMigration
|
||||||
|
{
|
||||||
|
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change Method.
|
||||||
|
*
|
||||||
|
* Write your reversible migrations using this method.
|
||||||
|
*
|
||||||
|
* More information on writing migrations is available here:
|
||||||
|
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||||
|
*
|
||||||
|
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||||
|
* with the Table class.
|
||||||
|
*/
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('sgo');
|
||||||
|
$exists = $table->hasColumn('extend');
|
||||||
|
if (!$exists) {
|
||||||
|
$table
|
||||||
|
->addColumn('extend', 'boolean', [
|
||||||
|
'default' => 0,
|
||||||
|
'null' => false,
|
||||||
|
])->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -178,7 +178,6 @@ return [
|
||||||
*/
|
*/
|
||||||
'Error' => [
|
'Error' => [
|
||||||
'errorLevel' => E_ALL,
|
'errorLevel' => E_ALL,
|
||||||
'exceptionRenderer' => ExceptionRenderer::class,
|
|
||||||
'skipLog' => [],
|
'skipLog' => [],
|
||||||
'log' => true,
|
'log' => true,
|
||||||
'trace' => true,
|
'trace' => true,
|
||||||
|
|
|
@ -194,6 +194,11 @@ ServerRequest::addDetector('tablet', function ($request) {
|
||||||
|
|
||||||
return $detector->isTablet();
|
return $detector->isTablet();
|
||||||
});
|
});
|
||||||
|
ServerRequest::addDetector('csv', [
|
||||||
|
'accept' => ['text/csv',],
|
||||||
|
'param' => '_ext',
|
||||||
|
'value' => 'csv',
|
||||||
|
]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* You can set whether the ORM uses immutable or mutable Time types.
|
* You can set whether the ORM uses immutable or mutable Time types.
|
||||||
|
|
|
@ -45,7 +45,7 @@ use Cake\Routing\RouteBuilder;
|
||||||
/** @var \Cake\Routing\RouteBuilder $routes */
|
/** @var \Cake\Routing\RouteBuilder $routes */
|
||||||
$routes->setRouteClass(DashedRoute::class);
|
$routes->setRouteClass(DashedRoute::class);
|
||||||
$routes->scope('/', function (RouteBuilder $builder) {
|
$routes->scope('/', function (RouteBuilder $builder) {
|
||||||
$builder->setExtensions(['json']);
|
$builder->setExtensions(['json', 'csv']);
|
||||||
// Register scoped middleware for in scopes.
|
// Register scoped middleware for in scopes.
|
||||||
$builder->registerMiddleware('csrf', new CsrfProtectionMiddleware([
|
$builder->registerMiddleware('csrf', new CsrfProtectionMiddleware([
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
<testsuite name="api">
|
<testsuite name="api">
|
||||||
<directory>./tests/TestCase/Api</directory>
|
<directory>./tests/TestCase/Api</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
|
<testsuite name="e2e">
|
||||||
|
<directory>./tests/TestCase/Integration</directory>
|
||||||
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
<extensions>
|
<extensions>
|
||||||
|
|
|
@ -106,7 +106,7 @@ class TagHelper extends Helper
|
||||||
$deleteButton = $this->Bootstrap->button([
|
$deleteButton = $this->Bootstrap->button([
|
||||||
'size' => 'sm',
|
'size' => 'sm',
|
||||||
'icon' => 'times',
|
'icon' => 'times',
|
||||||
'class' => ['ms-1', 'border-0', "text-${textColour}"],
|
'class' => ['ms-1', 'border-0', "text-$textColour"],
|
||||||
'variant' => 'text',
|
'variant' => 'text',
|
||||||
'title' => __('Delete tag'),
|
'title' => __('Delete tag'),
|
||||||
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
|
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
|
||||||
|
|
|
@ -136,7 +136,7 @@ class ImporterCommand extends Command
|
||||||
$entity = null;
|
$entity = null;
|
||||||
if (isset($item[$primary_key])) {
|
if (isset($item[$primary_key])) {
|
||||||
$query = $table->find('all')
|
$query = $table->find('all')
|
||||||
->where(["${primary_key}" => $item[$primary_key]]);
|
->where(["$primary_key" => $item[$primary_key]]);
|
||||||
$entity = $query->first();
|
$entity = $query->first();
|
||||||
}
|
}
|
||||||
if (is_null($entity)) {
|
if (is_null($entity)) {
|
||||||
|
|
|
@ -40,5 +40,4 @@ class AuditLogsController extends AppController
|
||||||
{
|
{
|
||||||
$this->CRUD->filtering();
|
$this->CRUD->filtering();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,7 @@ class ACLComponent extends Component
|
||||||
'viewTags' => ['*']
|
'viewTags' => ['*']
|
||||||
],
|
],
|
||||||
'Instance' => [
|
'Instance' => [
|
||||||
|
'downloadTopology' => ['perm_admin'],
|
||||||
'home' => ['*'],
|
'home' => ['*'],
|
||||||
'migrate' => ['perm_admin'],
|
'migrate' => ['perm_admin'],
|
||||||
'migrationIndex' => ['perm_admin'],
|
'migrationIndex' => ['perm_admin'],
|
||||||
|
@ -114,7 +115,8 @@ class ACLComponent extends Component
|
||||||
'saveSetting' => ['perm_admin'],
|
'saveSetting' => ['perm_admin'],
|
||||||
'searchAll' => ['*'],
|
'searchAll' => ['*'],
|
||||||
'settings' => ['perm_admin'],
|
'settings' => ['perm_admin'],
|
||||||
'status' => ['*']
|
'status' => ['*'],
|
||||||
|
'topology' => ['perm_admin'],
|
||||||
],
|
],
|
||||||
'LocalTools' => [
|
'LocalTools' => [
|
||||||
'action' => ['perm_admin'],
|
'action' => ['perm_admin'],
|
||||||
|
|
|
@ -19,7 +19,7 @@ use App\Utility\UI\IndexSetting;
|
||||||
|
|
||||||
class CRUDComponent extends Component
|
class CRUDComponent extends Component
|
||||||
{
|
{
|
||||||
public $components = ['RestResponse'];
|
public $components = ['RestResponse', 'APIRearrange'];
|
||||||
|
|
||||||
public function initialize(array $config): void
|
public function initialize(array $config): void
|
||||||
{
|
{
|
||||||
|
@ -102,12 +102,12 @@ class CRUDComponent extends Component
|
||||||
|
|
||||||
if (!$this->Controller->ParamHandler->isRest()) {
|
if (!$this->Controller->ParamHandler->isRest()) {
|
||||||
$this->setRequestedEntryAmount();
|
$this->setRequestedEntryAmount();
|
||||||
} else if (!empty($this->request->getQuery('limit'))) {
|
} else if (empty($this->request->getQuery('limit'))) {
|
||||||
$this->Controller->paginate['limit'] = PHP_INT_MAX; // Make sure to download the entire filtered table
|
$this->Controller->paginate['limit'] = PHP_INT_MAX; // Make sure to download the entire filtered table
|
||||||
}
|
}
|
||||||
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
|
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
|
||||||
$totalCount = $this->Controller->getRequest()->getAttribute('paging')[$this->TableAlias]['count'];
|
$totalCount = $this->Controller->getRequest()->getAttribute('paging')[$this->TableAlias]['count'];
|
||||||
if ($this->Controller->ParamHandler->isRest()) {
|
if ($this->Controller->ParamHandler->isRest() || $this->request->is('csv')) {
|
||||||
if (isset($options['hidden'])) {
|
if (isset($options['hidden'])) {
|
||||||
$data->each(function($value, $key) use ($options) {
|
$data->each(function($value, $key) use ($options) {
|
||||||
$hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']];
|
$hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']];
|
||||||
|
@ -138,9 +138,21 @@ class CRUDComponent extends Component
|
||||||
return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates);
|
return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if ($this->request->is('csv')) {
|
||||||
|
require_once(ROOT . '/src/Lib/Tools/CsvConverter.php');
|
||||||
|
$rearranged = $this->APIRearrange->rearrangeForAPI($data, ['smartFlattenMetafields' => true]);
|
||||||
|
$rearranged = $rearranged->map(function($e) {
|
||||||
|
return $e->toArray();
|
||||||
|
})->toList();
|
||||||
|
$data = \App\Lib\Tools\CsvConverter::flattenJSON($rearranged, []);
|
||||||
|
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'csv', false, false, false, [
|
||||||
|
'X-Total-Count' => $totalCount,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json', false, false, false, [
|
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json', false, false, false, [
|
||||||
'X-Total-Count' => $totalCount,
|
'X-Total-Count' => $totalCount,
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->Controller->setResponse($this->Controller->getResponse()->withHeader('X-Total-Count', $totalCount));
|
$this->Controller->setResponse($this->Controller->getResponse()->withHeader('X-Total-Count', $totalCount));
|
||||||
if (isset($options['afterFind'])) {
|
if (isset($options['afterFind'])) {
|
||||||
|
@ -281,7 +293,7 @@ class CRUDComponent extends Component
|
||||||
*/
|
*/
|
||||||
public function getResponsePayload()
|
public function getResponsePayload()
|
||||||
{
|
{
|
||||||
if ($this->Controller->ParamHandler->isRest()) {
|
if ($this->Controller->ParamHandler->isRest() || $this->request->is('csv')) {
|
||||||
return $this->Controller->restResponsePayload;
|
return $this->Controller->restResponsePayload;
|
||||||
} else if ($this->Controller->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) {
|
} else if ($this->Controller->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) {
|
||||||
return $this->Controller->ajaxResponsePayload;
|
return $this->Controller->ajaxResponsePayload;
|
||||||
|
@ -1305,6 +1317,7 @@ class CRUDComponent extends Component
|
||||||
}
|
}
|
||||||
$query = $this->setMetaFieldFilters($query, $filteringMetaFields);
|
$query = $this->setMetaFieldFilters($query, $filteringMetaFields);
|
||||||
}
|
}
|
||||||
|
$activeFilters['_here'] = $this->request->getRequestTarget();
|
||||||
|
|
||||||
$this->Controller->set('activeFilters', $activeFilters);
|
$this->Controller->set('activeFilters', $activeFilters);
|
||||||
return $query;
|
return $query;
|
||||||
|
@ -1489,7 +1502,7 @@ class CRUDComponent extends Component
|
||||||
{
|
{
|
||||||
$prefixedConditions = [];
|
$prefixedConditions = [];
|
||||||
foreach ($conditions as $condField => $condValue) {
|
foreach ($conditions as $condField => $condValue) {
|
||||||
$prefixedConditions["${prefix}.${condField}"] = $condValue;
|
$prefixedConditions["$prefix.$condField"] = $condValue;
|
||||||
}
|
}
|
||||||
return $prefixedConditions;
|
return $prefixedConditions;
|
||||||
}
|
}
|
||||||
|
@ -1613,13 +1626,13 @@ class CRUDComponent extends Component
|
||||||
[sprintf('%s.id = %s.%s', $this->Table->getAlias(), $associatedTable->getAlias(), $association->getForeignKey())]
|
[sprintf('%s.id = %s.%s', $this->Table->getAlias(), $associatedTable->getAlias(), $association->getForeignKey())]
|
||||||
)
|
)
|
||||||
->where([
|
->where([
|
||||||
["${field} IS NOT" => NULL]
|
["$field IS NOT" => NULL]
|
||||||
]);
|
]);
|
||||||
} else if ($associationType == 'manyToOne') {
|
} else if ($associationType == 'manyToOne') {
|
||||||
$fieldToExtract = sprintf('%s.%s', Inflector::singularize(strtolower($model)), $subField);
|
$fieldToExtract = sprintf('%s.%s', Inflector::singularize(strtolower($model)), $subField);
|
||||||
$query = $this->Table->find()->contain($model);
|
$query = $this->Table->find()->contain($model);
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Association ${associationType} not supported in CRUD Component");
|
throw new Exception("Association $associationType not supported in CRUD Component");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$fieldToExtract = $field;
|
$fieldToExtract = $field;
|
||||||
|
|
|
@ -8,7 +8,7 @@ class OrgGroupsNavigation extends BaseNavigation
|
||||||
public function addLinks()
|
public function addLinks()
|
||||||
{
|
{
|
||||||
$controller = 'OrgGroups';
|
$controller = 'OrgGroups';
|
||||||
if (empty($this->viewVars['canEdit'])) {
|
if (empty($this->viewVars['canEditDefinition'])) {
|
||||||
$this->bcf->removeLink($controller, 'view', $controller, 'edit');
|
$this->bcf->removeLink($controller, 'view', $controller, 'edit');
|
||||||
$this->bcf->removeLink($controller, 'edit', $controller, 'edit');
|
$this->bcf->removeLink($controller, 'edit', $controller, 'edit');
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,10 @@ class OrgGroupsNavigation extends BaseNavigation
|
||||||
public function addActions()
|
public function addActions()
|
||||||
{
|
{
|
||||||
$controller = 'OrgGroups';
|
$controller = 'OrgGroups';
|
||||||
if (empty($this->viewVars['canEdit'])) {
|
if (empty($this->viewVars['canEditDefinition'])) {
|
||||||
$this->bcf->removeAction($controller, 'view', $controller, 'delete');
|
$this->bcf->removeAction($controller, 'view', $controller, 'delete');
|
||||||
$this->bcf->removeAction($controller, 'edit', $controller, 'delete');
|
$this->bcf->removeAction($controller, 'edit', $controller, 'delete');
|
||||||
|
$this->bcf->removeAction($controller, 'view', $controller, 'add');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,11 @@ class Sidemenu {
|
||||||
'label' => __('Instance'),
|
'label' => __('Instance'),
|
||||||
'icon' => $this->iconTable['Instance'],
|
'icon' => $this->iconTable['Instance'],
|
||||||
'children' => [
|
'children' => [
|
||||||
|
'Topology' => [
|
||||||
|
'label' => __('Topology'),
|
||||||
|
'url' => '/instance/topology',
|
||||||
|
'icon' => 'project-diagram',
|
||||||
|
],
|
||||||
'Settings' => [
|
'Settings' => [
|
||||||
'label' => __('Settings'),
|
'label' => __('Settings'),
|
||||||
'url' => '/instance/settings',
|
'url' => '/instance/settings',
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Controller\Component;
|
||||||
|
|
||||||
use Cake\Controller\Component;
|
use Cake\Controller\Component;
|
||||||
use Cake\Core\Configure;
|
use Cake\Core\Configure;
|
||||||
|
use Cake\Utility\Hash;
|
||||||
use Cake\Utility\Inflector;
|
use Cake\Utility\Inflector;
|
||||||
|
|
||||||
class RestResponseComponent extends Component
|
class RestResponseComponent extends Component
|
||||||
|
@ -390,7 +391,7 @@ class RestResponseComponent extends Component
|
||||||
return '[]';
|
return '[]';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveFailResponse($controller, $action, $id = false, $validationErrors, $format = false)
|
public function saveFailResponse($controller, $action, $id, $validationErrors, $format = false)
|
||||||
{
|
{
|
||||||
$this->autoRender = false;
|
$this->autoRender = false;
|
||||||
$response = array();
|
$response = array();
|
||||||
|
|
|
@ -181,4 +181,21 @@ class InstanceController extends AppController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function topology()
|
||||||
|
{
|
||||||
|
$this->set('title', __('Topology'));
|
||||||
|
$this->set('description', __('A list of all instances and local tools connected .'));
|
||||||
|
$this->set('data', $this->Instance->getTopology());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadTopology()
|
||||||
|
{
|
||||||
|
$topologyMd = $this->Instance->getTopology();
|
||||||
|
$response = $this->response;
|
||||||
|
$response = $response->withStringBody($topologyMd);
|
||||||
|
$response = $response->withType('text/markdown');
|
||||||
|
$response = $response->withDownload('topology.md');
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ class OrgGroupsController extends AppController
|
||||||
return $responsePayload;
|
return $responsePayload;
|
||||||
}
|
}
|
||||||
$this->set('canEdit', $this->canEdit($id));
|
$this->set('canEdit', $this->canEdit($id));
|
||||||
|
$this->set('canEditDefinition', $this->canEditDefinition($id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit($id)
|
public function edit($id)
|
||||||
|
@ -136,6 +137,15 @@ class OrgGroupsController extends AppController
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function canEditDefinition($groupId): bool
|
||||||
|
{
|
||||||
|
$currentUser = $this->ACL->getUser();
|
||||||
|
if ($currentUser['role']['perm_admin']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Listing should be available to all, it's purely informational
|
// Listing should be available to all, it's purely informational
|
||||||
public function listAdmins($groupId)
|
public function listAdmins($groupId)
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,6 +8,7 @@ use Cake\Utility\Text;
|
||||||
use \Cake\Database\Expression\QueryExpression;
|
use \Cake\Database\Expression\QueryExpression;
|
||||||
use Cake\Error\Debugger;
|
use Cake\Error\Debugger;
|
||||||
use Cake\Http\Exception\NotFoundException;
|
use Cake\Http\Exception\NotFoundException;
|
||||||
|
use Cake\ORM\TableRegistry;
|
||||||
|
|
||||||
class SharingGroupsController extends AppController
|
class SharingGroupsController extends AppController
|
||||||
{
|
{
|
||||||
|
@ -171,9 +172,13 @@ class SharingGroupsController extends AppController
|
||||||
$input['organisation_id'] = [$input['organisation_id']];
|
$input['organisation_id'] = [$input['organisation_id']];
|
||||||
}
|
}
|
||||||
$result = true;
|
$result = true;
|
||||||
|
$this->SGO = TableRegistry::getTableLocator()->get('SGOs');
|
||||||
foreach ($input['organisation_id'] as $org_id) {
|
foreach ($input['organisation_id'] as $org_id) {
|
||||||
$org = $this->SharingGroups->SharingGroupOrgs->get($org_id);
|
$additional_data = [];
|
||||||
$result &= (bool)$this->SharingGroups->SharingGroupOrgs->link($sharingGroup, [$org]);
|
if (!empty($input['extend'])) {
|
||||||
|
$additional_data['extend'] = $input['extend'];
|
||||||
|
}
|
||||||
|
$result &= $this->SGO->attach($sharingGroup['id'], $org_id, $additional_data);
|
||||||
}
|
}
|
||||||
if ($result) {
|
if ($result) {
|
||||||
$message = __('Organisation(s) added to the sharing group.');
|
$message = __('Organisation(s) added to the sharing group.');
|
||||||
|
@ -216,8 +221,8 @@ class SharingGroupsController extends AppController
|
||||||
throw new NotFoundException(__('Invalid SharingGroup.'));
|
throw new NotFoundException(__('Invalid SharingGroup.'));
|
||||||
}
|
}
|
||||||
if ($this->request->is('post')) {
|
if ($this->request->is('post')) {
|
||||||
$org = $this->SharingGroups->SharingGroupOrgs->get($org_id);
|
$this->SGO = TableRegistry::getTableLocator()->get('SGOs');
|
||||||
$result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup, [$org]);
|
$result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup['id'], $org_id);
|
||||||
if ($result) {
|
if ($result) {
|
||||||
$message = __('Organisation(s) removed from the sharing group.');
|
$message = __('Organisation(s) removed from the sharing group.');
|
||||||
} else {
|
} else {
|
||||||
|
@ -253,9 +258,10 @@ class SharingGroupsController extends AppController
|
||||||
|
|
||||||
public function listOrgs($id)
|
public function listOrgs($id)
|
||||||
{
|
{
|
||||||
$sharingGroup = $this->SharingGroups->get($id, [
|
$sharingGroup = $this->SharingGroups->find()->where(['id' => $id])->contain(['SharingGroupOrgs'])->first();
|
||||||
'contain' => 'SharingGroupOrgs'
|
foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {
|
||||||
]);
|
$sharingGroup['sharing_group_orgs'][$k]['extend'] = $org['_joinData']['extend'];
|
||||||
|
}
|
||||||
$params = $this->ParamHandler->harvestParams(['quickFilter']);
|
$params = $this->ParamHandler->harvestParams(['quickFilter']);
|
||||||
if (!empty($params['quickFilter'])) {
|
if (!empty($params['quickFilter'])) {
|
||||||
foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {
|
foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ use Cake\Http\Exception\NotFoundException;
|
||||||
|
|
||||||
class UsersController extends AppController
|
class UsersController extends AppController
|
||||||
{
|
{
|
||||||
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name', 'Organisation.nationality'];
|
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name', 'Organisations.nationality'];
|
||||||
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
|
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
|
||||||
public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations', 'OrgGroups'];
|
public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations', 'OrgGroups'];
|
||||||
|
|
||||||
|
@ -63,6 +63,11 @@ class UsersController extends AppController
|
||||||
$this->set('validOrgIDsFOrEdition', $validOrgIDsFOrEdition);
|
$this->set('validOrgIDsFOrEdition', $validOrgIDsFOrEdition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function filtering()
|
||||||
|
{
|
||||||
|
$this->CRUD->filtering();
|
||||||
|
}
|
||||||
|
|
||||||
public function add()
|
public function add()
|
||||||
{
|
{
|
||||||
$currentUser = $this->ACL->getUser();
|
$currentUser = $this->ACL->getUser();
|
||||||
|
@ -72,7 +77,7 @@ class UsersController extends AppController
|
||||||
];
|
];
|
||||||
$individual_ids = [];
|
$individual_ids = [];
|
||||||
if (!$currentUser['role']['perm_admin']) {
|
if (!$currentUser['role']['perm_admin']) {
|
||||||
if (!$currentUser['role']['perm_group_admin']) {
|
if ($currentUser['role']['perm_group_admin']) {
|
||||||
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0, 'perm_group_admin' => 0])->all()->toArray();
|
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0, 'perm_group_admin' => 0])->all()->toArray();
|
||||||
$individual_ids = $this->Users->Individuals->find('aligned', ['organisation_id' => $currentUser['organisation_id']])->all()->extract('id')->toArray();
|
$individual_ids = $this->Users->Individuals->find('aligned', ['organisation_id' => $currentUser['organisation_id']])->all()->extract('id')->toArray();
|
||||||
} else {
|
} else {
|
||||||
|
@ -219,12 +224,12 @@ class UsersController extends AppController
|
||||||
{
|
{
|
||||||
$currentUser = $this->ACL->getUser();
|
$currentUser = $this->ACL->getUser();
|
||||||
$validRoles = [];
|
$validRoles = [];
|
||||||
$individuals_params = [
|
|
||||||
'sort' => ['email' => 'asc']
|
|
||||||
];
|
|
||||||
$individual_ids = [];
|
|
||||||
if (!$currentUser['role']['perm_admin']) {
|
if (!$currentUser['role']['perm_admin']) {
|
||||||
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0, 'perm_org_admin' => 0])->all()->toArray();
|
if ($currentUser['role']['perm_group_admin']) {
|
||||||
|
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0, 'perm_group_admin' => 0])->all()->toArray();
|
||||||
|
} else {
|
||||||
|
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0, 'perm_group_admin' => 0, 'perm_org_admin' => 0])->all()->toArray();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
|
$validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
|
||||||
}
|
}
|
||||||
|
@ -448,13 +453,17 @@ class UsersController extends AppController
|
||||||
{
|
{
|
||||||
$editingAnotherUser = false;
|
$editingAnotherUser = false;
|
||||||
$currentUser = $this->ACL->getUser();
|
$currentUser = $this->ACL->getUser();
|
||||||
if (empty($currentUser['role']['perm_admin']) || $user_id == $currentUser->id) {
|
if ((empty($currentUser['role']['perm_admin']) && empty($currentUser['role']['perm_group_admin'])) || $user_id == $currentUser->id) {
|
||||||
$user = $currentUser;
|
$user = $currentUser;
|
||||||
} else {
|
} else {
|
||||||
$user = $this->Users->get($user_id, [
|
$user = $this->Users->get($user_id, [
|
||||||
'contain' => ['Roles', 'Individuals' => 'Organisations', 'Organisations', 'UserSettings']
|
'contain' => ['Roles', 'Individuals' => 'Organisations', 'Organisations', 'UserSettings']
|
||||||
]);
|
]);
|
||||||
$editingAnotherUser = true;
|
$editingAnotherUser = true;
|
||||||
|
if (!empty($currentUser['role']['perm_group_admin']) && !$this->ACL->canEditUser($currentUser, $user)) {
|
||||||
|
$user = $currentUser;
|
||||||
|
$editingAnotherUser = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$this->set('editingAnotherUser', $editingAnotherUser);
|
$this->set('editingAnotherUser', $editingAnotherUser);
|
||||||
$this->set('user', $user);
|
$this->set('user', $user);
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Lib\Tools;
|
||||||
|
|
||||||
|
use Cake\Utility\Hash;
|
||||||
|
|
||||||
|
|
||||||
|
class CsvConverter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $data
|
||||||
|
* @param array $options
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function flattenJSON(array $data, $options=[]): string
|
||||||
|
{
|
||||||
|
$csv = '';
|
||||||
|
$toConvert = [];
|
||||||
|
if (!self::array_is_list($data)) {
|
||||||
|
$toConvert = [$data];
|
||||||
|
} else {
|
||||||
|
$toConvert = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = self::collectHeaders($toConvert);
|
||||||
|
$csv .= implode(',', self::quoteArray($headers)) . PHP_EOL;
|
||||||
|
foreach ($toConvert as $i => $item) {
|
||||||
|
$csv .= self::getRow($headers, $item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $csv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function collectHeaders(array $items): array
|
||||||
|
{
|
||||||
|
$allHeaders = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$headers = Hash::flatten($item);
|
||||||
|
foreach ($headers as $head => $value) {
|
||||||
|
if (str_starts_with($head, '_')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_array($value) && empty($value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$allHeaders[$head] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_keys($allHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getRow(array $headers, array $item): string
|
||||||
|
{
|
||||||
|
$tmp = [];
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
$value = Hash::get($item, $header);
|
||||||
|
if (!isset($value)) {
|
||||||
|
$value = '';
|
||||||
|
}
|
||||||
|
if (is_bool($value)) {
|
||||||
|
$value = !empty($value) ? '1' : '0';
|
||||||
|
}
|
||||||
|
$tmp[] = '"' . $value . '"';
|
||||||
|
}
|
||||||
|
$row = implode(',', $tmp) . PHP_EOL;
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function quoteArray(array $arr): array
|
||||||
|
{
|
||||||
|
return array_map(function($item) {
|
||||||
|
return '"' . $item . '"';
|
||||||
|
}, $arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function array_is_list(array $arr): bool
|
||||||
|
{
|
||||||
|
if ($arr === []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return array_keys($arr) === range(0, count($arr) - 1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ namespace CommonConnectorTools;
|
||||||
use Cake\ORM\Locator\LocatorAwareTrait;
|
use Cake\ORM\Locator\LocatorAwareTrait;
|
||||||
use Cake\Log\Log;
|
use Cake\Log\Log;
|
||||||
use Cake\Log\Engine\FileLog;
|
use Cake\Log\Engine\FileLog;
|
||||||
|
use Cake\Utility\Hash;
|
||||||
|
|
||||||
|
|
||||||
class CommonConnectorTools
|
class CommonConnectorTools
|
||||||
{
|
{
|
||||||
|
@ -88,13 +90,115 @@ class CommonConnectorTools
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function captureSharingGroup($input): bool
|
public function getOrganisation(string $uuid): ?array
|
||||||
|
{
|
||||||
|
$organisations = \Cake\ORM\TableRegistry::getTableLocator()->get('Organisations');
|
||||||
|
$org = $organisations->find()->where(['Organisations.uuid' => $uuid])->disableHydration()->first();
|
||||||
|
return $org;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrganisations(): array
|
||||||
|
{
|
||||||
|
$organisations = \Cake\ORM\TableRegistry::getTableLocator()->get('Organisations');
|
||||||
|
$orgs = $organisations->find()->disableHydration()->toArray();
|
||||||
|
return $orgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSharingGroups(): array
|
||||||
|
{
|
||||||
|
$sgs = \Cake\ORM\TableRegistry::getTableLocator()->get('SharingGroups');
|
||||||
|
$sgs = $sgs->find()
|
||||||
|
->contain(['Organisations' => ['fields' => ['uuid']], 'SharingGroupOrgs' => ['fields' => ['uuid']]])
|
||||||
|
->disableHydration()
|
||||||
|
->toArray();
|
||||||
|
return $sgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilteredOrganisations($filters, $returnObjects = false): array
|
||||||
|
{
|
||||||
|
$organisations = \Cake\ORM\TableRegistry::getTableLocator()->get('Organisations');
|
||||||
|
$orgs = $organisations->find();
|
||||||
|
$filterFields = ['type', 'nationality', 'sector'];
|
||||||
|
foreach ($filterFields as $filterField) {
|
||||||
|
if (!empty($filters[$filterField]) && $filters[$filterField] !== 'ALL') {
|
||||||
|
$orgs = $orgs->where([$filterField => $filters[$filterField]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($filters['local']) && $filters['local'] !== '0') {
|
||||||
|
$users = \Cake\ORM\TableRegistry::getTableLocator()->get('users');
|
||||||
|
$org_ids = array_values(array_unique($users->find('list', [
|
||||||
|
'valueField' => 'organisation_id'
|
||||||
|
])->toArray()));
|
||||||
|
$orgs = $orgs->where(['id IN' => $org_ids]);
|
||||||
|
}
|
||||||
|
if ($returnObjects) {
|
||||||
|
$orgs = $orgs->toArray();
|
||||||
|
} else {
|
||||||
|
$orgs = $orgs->disableHydration()->all();
|
||||||
|
}
|
||||||
|
return $orgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilteredSharingGroups($filters, $returnObjects = false): array
|
||||||
|
{
|
||||||
|
$SG = \Cake\ORM\TableRegistry::getTableLocator()->get('SharingGroups');
|
||||||
|
$sgs = $SG->find();
|
||||||
|
$filterFields = ['name', 'releasability'];
|
||||||
|
$sgs->contain(['SharingGroupOrgs', 'Organisations']);
|
||||||
|
foreach ($filterFields as $filterField) {
|
||||||
|
if (!empty($filters[$filterField]) && $filters[$filterField] !== 'ALL') {
|
||||||
|
if (is_string($filters[$filterField]) && strpos($filters[$filterField], '%') !== false) {
|
||||||
|
$sgs = $sgs->where(['SharingGroups.' . $filterField . ' LIKE' => $filters[$filterField]]);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$sgs = $sgs->where(['SharingGroups.' . $filterField => $filters[$filterField]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($returnObjects) {
|
||||||
|
$sgs = $sgs->toArray();
|
||||||
|
} else {
|
||||||
|
$sgs = $sgs->disableHydration()->all();
|
||||||
|
}
|
||||||
|
return $sgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrganisationSelectorValues(): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
$orgTable = \Cake\ORM\TableRegistry::getTableLocator()->get('Organisations');
|
||||||
|
$fields = [
|
||||||
|
'nationality' => 'nat',
|
||||||
|
'sector' => 'sect',
|
||||||
|
'type' => 'typ'
|
||||||
|
];
|
||||||
|
foreach ($fields as $field => $temp_field) {
|
||||||
|
$temp = Hash::extract(
|
||||||
|
$orgTable->find()
|
||||||
|
->select([$temp_field => 'DISTINCT (' . $field . ')'])
|
||||||
|
->order([$temp_field => 'DESC'])
|
||||||
|
->disableHydration()->toArray(),
|
||||||
|
'{n}.' . $temp_field
|
||||||
|
);
|
||||||
|
foreach ($temp as $k => $v) {
|
||||||
|
if (empty($v)) {
|
||||||
|
unset($temp[$k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asort($temp, SORT_FLAG_CASE | SORT_NATURAL);
|
||||||
|
$temp = array_merge(['ALL' => 'ALL'], $temp);
|
||||||
|
$results[$field] = array_combine($temp, $temp);
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function captureSharingGroup($input, $user_id): bool
|
||||||
{
|
{
|
||||||
if (empty($input['uuid'])) {
|
if (empty($input['uuid'])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$sharing_groups = \Cake\ORM\TableRegistry::getTableLocator()->get('SharingGroups');
|
$sharing_groups = \Cake\ORM\TableRegistry::getTableLocator()->get('SharingGroups');
|
||||||
$sharing_groups->captureSharingGroup($input);
|
$sharing_groups->captureSharingGroup($input, $user_id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +253,11 @@ class CommonConnectorTools
|
||||||
$this->remoteToolConnectionStatus($params, self::STATE_CONNECTED);
|
$this->remoteToolConnectionStatus($params, self::STATE_CONNECTED);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function diagnostics(array $params): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -15,6 +15,12 @@ class MispConnector extends CommonConnectorTools
|
||||||
public $name = 'MISP';
|
public $name = 'MISP';
|
||||||
|
|
||||||
public $exposedFunctions = [
|
public $exposedFunctions = [
|
||||||
|
'diagnosticsAction' => [
|
||||||
|
'type' => 'index',
|
||||||
|
'scope' => 'child',
|
||||||
|
'params' => [
|
||||||
|
]
|
||||||
|
],
|
||||||
'serverSettingsAction' => [
|
'serverSettingsAction' => [
|
||||||
'type' => 'index',
|
'type' => 'index',
|
||||||
'scope' => 'child',
|
'scope' => 'child',
|
||||||
|
@ -48,6 +54,11 @@ class MispConnector extends CommonConnectorTools
|
||||||
'direction'
|
'direction'
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
'restartWorkersAction' => [
|
||||||
|
'type' => 'formAction',
|
||||||
|
'scope' => 'childAction',
|
||||||
|
'redirect' => 'diagnosticsAction'
|
||||||
|
],
|
||||||
'fetchOrganisationAction' => [
|
'fetchOrganisationAction' => [
|
||||||
'type' => 'formAction',
|
'type' => 'formAction',
|
||||||
'scope' => 'childAction',
|
'scope' => 'childAction',
|
||||||
|
@ -56,6 +67,46 @@ class MispConnector extends CommonConnectorTools
|
||||||
],
|
],
|
||||||
'redirect' => 'organisationsAction'
|
'redirect' => 'organisationsAction'
|
||||||
],
|
],
|
||||||
|
'fetchSelectedOrganisationsAction' => [
|
||||||
|
'type' => 'formAction',
|
||||||
|
'scope' => 'childAction',
|
||||||
|
'params' => [
|
||||||
|
'uuid'
|
||||||
|
],
|
||||||
|
'redirect' => 'organisationsAction'
|
||||||
|
],
|
||||||
|
'fetchSelectedSharingGroupsAction' => [
|
||||||
|
'type' => 'formAction',
|
||||||
|
'scope' => 'childAction',
|
||||||
|
'params' => [
|
||||||
|
'uuid'
|
||||||
|
],
|
||||||
|
'redirect' => 'sharingGroupsAction'
|
||||||
|
],
|
||||||
|
'pushOrganisationAction' => [
|
||||||
|
'type' => 'formAction',
|
||||||
|
'scope' => 'childAction',
|
||||||
|
'params' => [
|
||||||
|
'uuid'
|
||||||
|
],
|
||||||
|
'redirect' => 'organisationsAction'
|
||||||
|
],
|
||||||
|
'pushOrganisationsAction' => [
|
||||||
|
'type' => 'formAction',
|
||||||
|
'scope' => 'childAction',
|
||||||
|
'params' => [
|
||||||
|
'uuid'
|
||||||
|
],
|
||||||
|
'redirect' => 'organisationsAction'
|
||||||
|
],
|
||||||
|
'pushSharingGroupsAction' => [
|
||||||
|
'type' => 'formAction',
|
||||||
|
'scope' => 'childAction',
|
||||||
|
'params' => [
|
||||||
|
'uuid'
|
||||||
|
],
|
||||||
|
'redirect' => 'sharingGroupsAction'
|
||||||
|
],
|
||||||
'fetchSharingGroupAction' => [
|
'fetchSharingGroupAction' => [
|
||||||
'type' => 'formAction',
|
'type' => 'formAction',
|
||||||
'scope' => 'childAction',
|
'scope' => 'childAction',
|
||||||
|
@ -108,7 +159,7 @@ class MispConnector extends CommonConnectorTools
|
||||||
'icon' => 'terminal',
|
'icon' => 'terminal',
|
||||||
'variant' => 'primary',
|
'variant' => 'primary',
|
||||||
]
|
]
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
public $version = '0.1';
|
public $version = '0.1';
|
||||||
public $settings = [
|
public $settings = [
|
||||||
|
@ -229,9 +280,6 @@ class MispConnector extends CommonConnectorTools
|
||||||
$list = explode('.', $params['sort']);
|
$list = explode('.', $params['sort']);
|
||||||
$params['sort'] = end($list);
|
$params['sort'] = end($list);
|
||||||
}
|
}
|
||||||
if (!isset($params['limit'])) {
|
|
||||||
$params['limit'] = 50;
|
|
||||||
}
|
|
||||||
$url = $this->urlAppendParams($url, $params);
|
$url = $this->urlAppendParams($url, $params);
|
||||||
$response = $this->HTTPClientGET($url, $params['connection']);
|
$response = $this->HTTPClientGET($url, $params['connection']);
|
||||||
if ($response->isOk()) {
|
if ($response->isOk()) {
|
||||||
|
@ -240,7 +288,7 @@ class MispConnector extends CommonConnectorTools
|
||||||
if (!empty($params['softError'])) {
|
if (!empty($params['softError'])) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
$errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
|
$errorMsg = __('Could not GET from the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
|
||||||
$this->logError($errorMsg);
|
$this->logError($errorMsg);
|
||||||
throw new NotFoundException($errorMsg);
|
throw new NotFoundException($errorMsg);
|
||||||
}
|
}
|
||||||
|
@ -261,6 +309,9 @@ class MispConnector extends CommonConnectorTools
|
||||||
if ($response->isOk()) {
|
if ($response->isOk()) {
|
||||||
return $response;
|
return $response;
|
||||||
} else {
|
} else {
|
||||||
|
if (!empty($params['softError'])) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
$errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
|
$errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
|
||||||
$this->logError($errorMsg);
|
$this->logError($errorMsg);
|
||||||
throw new NotFoundException($errorMsg);
|
throw new NotFoundException($errorMsg);
|
||||||
|
@ -288,10 +339,183 @@ class MispConnector extends CommonConnectorTools
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function diagnostics(array $params): array
|
||||||
|
{
|
||||||
|
$urlParams = h($params['connection']['id']) . '/serverSettingsAction';
|
||||||
|
$response = $this->getData('/servers/serverSettings', $params);
|
||||||
|
if (!$response->isOk()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$data = $response->getJson();
|
||||||
|
$issues = [];
|
||||||
|
if ($data['version']['upToDate'] !== 'same') {
|
||||||
|
$issues['version'] = [
|
||||||
|
'type' => 'danger',
|
||||||
|
'message' => __('Outdated ({0}).', $data['version']['current']),
|
||||||
|
'remediation' => [
|
||||||
|
'icon' => 'fa-sync',
|
||||||
|
'title' => __('Update MISP'),
|
||||||
|
'url' => '/localTools/action/' . h($params['connection']['id']) . '/updateMISP'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($data['phpSettings']['memory_limit']['value'] < $data['phpSettings']['memory_limit']['recommended']) {
|
||||||
|
$issues['php_memory'] = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => __('Low PHP memory ({0}M).', $data['phpSettings']['memory_limit']['value'])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$worker_issues = [];
|
||||||
|
foreach ($data['workers'] as $queue => $worker_data) {
|
||||||
|
if (in_array($queue, ['proc_accessible', 'scheduler', 'controls'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty($worker_data['ok'])) {
|
||||||
|
$worker_issues['down'][] = $queue;
|
||||||
|
}
|
||||||
|
if ($worker_data['jobCount'] > 100) {
|
||||||
|
$worker_issues['stalled'][] = $queue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($worker_issues['down'])) {
|
||||||
|
$issues['workers_down'] = [
|
||||||
|
'type' => 'danger',
|
||||||
|
'message' => __('Worker(s) down: {0}', implode(', ', $worker_issues['down'])),
|
||||||
|
'remediation' => [
|
||||||
|
'icon' => 'fa-sync',
|
||||||
|
'title' => __('Restart workers'),
|
||||||
|
'url' => '/localTools/action/' . h($params['connection']['id']) . '/restartWorkersAction'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($worker_issues['stalled'])) {
|
||||||
|
$issues['workers_stalled'] = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => __('Worker(s) stalled: {0}', implode(', ', $worker_issues['stalled'])),
|
||||||
|
'remediation' => [
|
||||||
|
'icon' => 'fa-sync',
|
||||||
|
'title' => __('Restart workers'),
|
||||||
|
'url' => '/localTools/action/' . h($params['connection']['id']) . '/restartWorkersAction'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (!empty($data['dbConfiguration'])) {
|
||||||
|
foreach ($data['dbConfiguration'] as $dbConfig) {
|
||||||
|
if ($dbConfig['name'] === 'innodb_buffer_pool_size' && $dbConfig['value'] < $dbConfig['recommended']) {
|
||||||
|
$issues['innodb_buffer_pool_size'] = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => __('InnoDB buffer pool size is low ({0}M).', (round($dbConfig['value']/1024/1024)))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($data['dbSchemaDiagnostics'])) {
|
||||||
|
if ($data['dbSchemaDiagnostics']['expected_db_version'] > $data['dbSchemaDiagnostics']['actual_db_version'])
|
||||||
|
$issues['schema_version'] = [
|
||||||
|
'type' => 'danger',
|
||||||
|
'message' => __('DB schame outdated.'),
|
||||||
|
'remediation' => [
|
||||||
|
'icon' => 'fa-sync',
|
||||||
|
'title' => __('Update DB schema'),
|
||||||
|
'url' => '/localTools/action/' . h($params['connection']['id']) . '/updateSchemaAction'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restartWorkersAction(array $params): array
|
||||||
|
{
|
||||||
|
if ($params['request']->is(['get'])) {
|
||||||
|
return [
|
||||||
|
'data' => [
|
||||||
|
'title' => __('Restart workers'),
|
||||||
|
'description' => __('Would you like to trigger a restart of all attached workers?'),
|
||||||
|
'submit' => [
|
||||||
|
'action' => $params['request']->getParam('action')
|
||||||
|
],
|
||||||
|
'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'restartWorkersAction']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} elseif ($params['request']->is(['post'])) {
|
||||||
|
$response = $this->postData('/servers/restartWorkers', $params);
|
||||||
|
if ($response->isOk()) {
|
||||||
|
return ['success' => 1, 'message' => __('Workers restarted.')];
|
||||||
|
} else {
|
||||||
|
return ['success' => 0, 'message' => __('Could not restart workers.')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$response = $this->postData('/servers/restartWorkers', $params);
|
||||||
|
if ($response->isOk()) {
|
||||||
|
return [
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => __('Workers restarted.')
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
'type' => 'danger',
|
||||||
|
'message' => __('Something went wrong.')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function diagnosticsAction(array $params): array
|
public function diagnosticsAction(array $params): array
|
||||||
{
|
{
|
||||||
|
$diagnostics = $this->diagnostics($params);
|
||||||
|
$data = [];
|
||||||
|
foreach ($diagnostics as $error => $error_data) {
|
||||||
|
$data[] = [
|
||||||
|
'error' => $error,
|
||||||
|
'type' => $error_data['type'],
|
||||||
|
'message' => $error_data['message'],
|
||||||
|
'remediation' => $error_data['remediation'] ?? false
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'type' => 'index',
|
||||||
|
'data' => [
|
||||||
|
'data' => $data,
|
||||||
|
'skip_pagination' => 1,
|
||||||
|
'top_bar' => [
|
||||||
|
'children' => []
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'name' => 'error',
|
||||||
|
'data_path' => 'error',
|
||||||
|
'name' => __('Error'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'message',
|
||||||
|
'data_path' => 'message',
|
||||||
|
'name' => __('Message'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Remediation'),
|
||||||
|
'element' => 'function',
|
||||||
|
'function' => function($row, $context) {
|
||||||
|
$remediation = $context->Hash->extract($row, 'remediation');
|
||||||
|
if (!empty($remediation['title'])) {
|
||||||
|
echo sprintf(
|
||||||
|
'<a href="%s" class="btn btn-primary btn-sm" title="%s"><i class="fa %s"></i></a>',
|
||||||
|
h($remediation['url']),
|
||||||
|
h($remediation['title']),
|
||||||
|
h($remediation['icon'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'title' => false,
|
||||||
|
'description' => false,
|
||||||
|
'pull' => 'right'
|
||||||
|
]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function serverSettingsAction(array $params): array
|
public function serverSettingsAction(array $params): array
|
||||||
|
@ -552,6 +776,84 @@ class MispConnector extends CommonConnectorTools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function __compareOrgs(array $data, array $existingOrgs): array
|
||||||
|
{
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
$data[$k]['Organisation']['local_copy'] = false;
|
||||||
|
if (!empty($existingOrgs[$v['Organisation']['uuid']])) {
|
||||||
|
$remoteOrg = $existingOrgs[$v['Organisation']['uuid']];
|
||||||
|
$localOrg = $v['Organisation'];
|
||||||
|
$same = true;
|
||||||
|
$fieldsToCheck = [
|
||||||
|
'nationality', 'sector', 'type', 'name'
|
||||||
|
];
|
||||||
|
foreach (['nationality', 'sector', 'type', 'name'] as $fieldToCheck) {
|
||||||
|
if ($remoteOrg[$fieldToCheck] != $localOrg[$fieldToCheck]) {
|
||||||
|
$same = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data[$k]['Organisation']['local_copy'] = $same ? 'same' : 'different';
|
||||||
|
} else {
|
||||||
|
$data[$k]['Organisation']['local_copy'] = 'not_found';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __compareSgs(array $data, array $existingSgs): array
|
||||||
|
{
|
||||||
|
foreach ($existingSgs as $k => $existingSg) {
|
||||||
|
$existingSgs[$k]['org_uuids'] = [];
|
||||||
|
foreach ($existingSg['sharing_group_orgs'] as $sgo) {
|
||||||
|
$existingSgs[$k]['org_uuids'][$sgo['uuid']] = $sgo['_joinData']['extend'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
$data[$k]['SharingGroup']['local_copy'] = false;
|
||||||
|
$data[$k]['SharingGroup']['differences'] = [];
|
||||||
|
if (!empty($existingSgs[$v['SharingGroup']['uuid']])) {
|
||||||
|
$data[$k]['SharingGroup']['roaming'] = !empty($v['SharingGroup']['roaming']);
|
||||||
|
$localSg = $existingSgs[$v['SharingGroup']['uuid']];
|
||||||
|
$remoteSg = $v['SharingGroup'];
|
||||||
|
$same = true;
|
||||||
|
foreach (['description', 'name', 'releasability', 'active'] as $fieldToCheck) {
|
||||||
|
if ($remoteSg[$fieldToCheck] != $localSg[$fieldToCheck]) {
|
||||||
|
$same = false;
|
||||||
|
$data[$k]['differences']['metadata'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($v['Organisation']['uuid'] != $localSg['organisation']['uuid']) {
|
||||||
|
$same = false;
|
||||||
|
$data[$k]['SharingGroup']['differences']['uuid'] = true;
|
||||||
|
}
|
||||||
|
$v['org_uuids'] = [];
|
||||||
|
foreach ($v['SharingGroupOrg'] as $sgo) {
|
||||||
|
$v['org_uuids'][$sgo['Organisation']['uuid']] = $sgo['extend'];
|
||||||
|
}
|
||||||
|
if (count($localSg['org_uuids']) !== count($v['org_uuids'])) {
|
||||||
|
$same = false;
|
||||||
|
$data[$k]['SharingGroup']['differences']['orgs_count'] = true;
|
||||||
|
}
|
||||||
|
foreach ($localSg['org_uuids'] as $org_uuid => $extend) {
|
||||||
|
if (!isset($v['org_uuids'][$org_uuid])) {
|
||||||
|
$same = false;
|
||||||
|
$data[$k]['SharingGroup']['differences']['remote_orgs_missing'] = true;
|
||||||
|
} else {
|
||||||
|
if ($extend != $v['org_uuids'][$org_uuid]) {
|
||||||
|
$same = false;
|
||||||
|
$data[$k]['SharingGroup']['differences']['remote_orgs_extend'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data[$k]['SharingGroup']['local_copy'] = $same ? 'same' : 'different';
|
||||||
|
} else {
|
||||||
|
$data[$k]['SharingGroup']['local_copy'] = 'not_found';
|
||||||
|
}
|
||||||
|
$data[$k]['SharingGroup']['differences'] = array_keys($data[$k]['SharingGroup']['differences']);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function organisationsAction(array $params): array
|
public function organisationsAction(array $params): array
|
||||||
{
|
{
|
||||||
|
@ -563,6 +865,30 @@ class MispConnector extends CommonConnectorTools
|
||||||
$urlParams = h($params['connection']['id']) . '/organisationsAction';
|
$urlParams = h($params['connection']['id']) . '/organisationsAction';
|
||||||
$response = $this->getData('/organisations/index', $params);
|
$response = $this->getData('/organisations/index', $params);
|
||||||
$data = $response->getJson();
|
$data = $response->getJson();
|
||||||
|
$temp = $this->getOrganisations();
|
||||||
|
$existingOrgs = [];
|
||||||
|
foreach ($temp as $k => $v) {
|
||||||
|
$existingOrgs[$v['uuid']] = $v;
|
||||||
|
unset($temp[$k]);
|
||||||
|
}
|
||||||
|
$statusLevels = [
|
||||||
|
'same' => [
|
||||||
|
'colour' => 'success',
|
||||||
|
'message' => __('Remote organisation is the same as local copy'),
|
||||||
|
'icon' => 'check-circle'
|
||||||
|
],
|
||||||
|
'different' => [
|
||||||
|
'colour' => 'warning',
|
||||||
|
'message' => __('Local and remote versions of the organisations are different.'),
|
||||||
|
'icon' => 'exclamation-circle'
|
||||||
|
],
|
||||||
|
'not_found' => [
|
||||||
|
'colour' => 'danger',
|
||||||
|
'message' => __('Local organisation not found'),
|
||||||
|
'icon' => 'exclamation-triangle'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$data = $this->__compareOrgs($data, $existingOrgs);
|
||||||
if (!empty($data)) {
|
if (!empty($data)) {
|
||||||
return [
|
return [
|
||||||
'type' => 'index',
|
'type' => 'index',
|
||||||
|
@ -571,6 +897,31 @@ class MispConnector extends CommonConnectorTools
|
||||||
'skip_pagination' => 1,
|
'skip_pagination' => 1,
|
||||||
'top_bar' => [
|
'top_bar' => [
|
||||||
'children' => [
|
'children' => [
|
||||||
|
[
|
||||||
|
'type' => 'simple',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'class' => 'hidden mass-select',
|
||||||
|
'text' => __('Fetch selected organisations'),
|
||||||
|
'html' => '<i class="fas fa-download"></i> ',
|
||||||
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/organisationsAction',
|
||||||
|
'popover_url' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSelectedOrganisationsAction'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'text' => __('Fetch all organisations'),
|
||||||
|
'html' => '<i class="fas fa-download"></i> ',
|
||||||
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/organisationsAction',
|
||||||
|
'popover_url' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSelectedOrganisationsAction?ids=all'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'text' => __('Push organisations'),
|
||||||
|
'html' => '<i class="fas fa-upload"></i> ',
|
||||||
|
'class' => 'btn btn-primary',
|
||||||
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/organisationsAction',
|
||||||
|
'popover_url' => '/localTools/action/' . h($params['connection']['id']) . '/pushOrganisationsAction'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'search',
|
'type' => 'search',
|
||||||
'button' => __('Search'),
|
'button' => __('Search'),
|
||||||
|
@ -582,11 +933,32 @@ class MispConnector extends CommonConnectorTools
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'fields' => [
|
'fields' => [
|
||||||
|
[
|
||||||
|
'element' => 'selector',
|
||||||
|
'class' => 'short',
|
||||||
|
'data' => [
|
||||||
|
'id' => [
|
||||||
|
'value_path' => 'Organisation.uuid'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => 'Name',
|
'name' => 'Name',
|
||||||
'sort' => 'Organisation.name',
|
'sort' => 'Organisation.name',
|
||||||
'data_path' => 'Organisation.name',
|
'data_path' => 'Organisation.name',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Status',
|
||||||
|
'sort' => 'Organisation.local_copy',
|
||||||
|
'data_path' => 'Organisation.local_copy',
|
||||||
|
'element' => 'status',
|
||||||
|
'status_levels' => $statusLevels
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'uuid',
|
||||||
|
'sort' => 'Organisation.uuid',
|
||||||
|
'data_path' => 'Organisation.uuid'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => 'uuid',
|
'name' => 'uuid',
|
||||||
'sort' => 'Organisation.uuid',
|
'sort' => 'Organisation.uuid',
|
||||||
|
@ -597,6 +969,11 @@ class MispConnector extends CommonConnectorTools
|
||||||
'sort' => 'Organisation.nationality',
|
'sort' => 'Organisation.nationality',
|
||||||
'data_path' => 'Organisation.nationality'
|
'data_path' => 'Organisation.nationality'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'local',
|
||||||
|
'sort' => 'Organisation.local',
|
||||||
|
'data_path' => 'Organisation.local'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => 'sector',
|
'name' => 'sector',
|
||||||
'sort' => 'Organisation.sector',
|
'sort' => 'Organisation.sector',
|
||||||
|
@ -607,11 +984,33 @@ class MispConnector extends CommonConnectorTools
|
||||||
'description' => false,
|
'description' => false,
|
||||||
'pull' => 'right',
|
'pull' => 'right',
|
||||||
'actions' => [
|
'actions' => [
|
||||||
|
[
|
||||||
|
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/pushOrganisationAction?uuid={{0}}',
|
||||||
|
'modal_params_data_path' => ['Organisation.uuid'],
|
||||||
|
'icon' => 'upload',
|
||||||
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/organisationsAction',
|
||||||
|
'complex_requirement' => [
|
||||||
|
'function' => function ($row, $options) {
|
||||||
|
if ($row['Organisation']['local_copy'] === 'different') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/fetchOrganisationAction?uuid={{0}}',
|
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/fetchOrganisationAction?uuid={{0}}',
|
||||||
'modal_params_data_path' => ['Organisation.uuid'],
|
'modal_params_data_path' => ['Organisation.uuid'],
|
||||||
'icon' => 'download',
|
'icon' => 'download',
|
||||||
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/organisationsAction'
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/organisationsAction',
|
||||||
|
'complex_requirement' => [
|
||||||
|
'function' => function ($row, $options) {
|
||||||
|
if ($row['Organisation']['local_copy'] === 'different' || $row['Organisation']['local_copy'] === 'not_found') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -631,6 +1030,35 @@ class MispConnector extends CommonConnectorTools
|
||||||
$urlParams = h($params['connection']['id']) . '/sharingGroupsAction';
|
$urlParams = h($params['connection']['id']) . '/sharingGroupsAction';
|
||||||
$response = $this->getData('/sharing_groups/index', $params);
|
$response = $this->getData('/sharing_groups/index', $params);
|
||||||
$data = $response->getJson();
|
$data = $response->getJson();
|
||||||
|
$temp = $this->getSharingGroups();
|
||||||
|
$existingOrgs = [];
|
||||||
|
foreach ($temp as $k => $v) {
|
||||||
|
$existingSGs[$v['uuid']] = $v;
|
||||||
|
unset($temp[$k]);
|
||||||
|
}
|
||||||
|
$data['response'] = $this->__compareSgs($data['response'], $existingSGs);
|
||||||
|
$existingSGs = [];
|
||||||
|
foreach ($temp as $k => $v) {
|
||||||
|
$existingSGs[$v['uuid']] = $v;
|
||||||
|
unset($temp[$k]);
|
||||||
|
}
|
||||||
|
$statusLevels = [
|
||||||
|
'same' => [
|
||||||
|
'colour' => 'success',
|
||||||
|
'message' => __('Remote sharing group is the same as local copy'),
|
||||||
|
'icon' => 'check-circle'
|
||||||
|
],
|
||||||
|
'different' => [
|
||||||
|
'colour' => 'warning',
|
||||||
|
'message' => __('Local and remote versions of the sharing groups are different.'),
|
||||||
|
'icon' => 'exclamation-circle'
|
||||||
|
],
|
||||||
|
'not_found' => [
|
||||||
|
'colour' => 'danger',
|
||||||
|
'message' => __('Local sharing group not found'),
|
||||||
|
'icon' => 'exclamation-triangle'
|
||||||
|
]
|
||||||
|
];
|
||||||
if (!empty($data)) {
|
if (!empty($data)) {
|
||||||
return [
|
return [
|
||||||
'type' => 'index',
|
'type' => 'index',
|
||||||
|
@ -640,21 +1068,53 @@ class MispConnector extends CommonConnectorTools
|
||||||
'top_bar' => [
|
'top_bar' => [
|
||||||
'children' => [
|
'children' => [
|
||||||
[
|
[
|
||||||
'type' => 'search',
|
'type' => 'simple',
|
||||||
'button' => __('Search'),
|
'children' => [
|
||||||
'placeholder' => __('Enter value to search'),
|
[
|
||||||
'data' => '',
|
'class' => 'hidden mass-select',
|
||||||
'searchKey' => 'value',
|
'text' => __('Fetch selected sharing groups'),
|
||||||
'additionalUrlParams' => $urlParams
|
'html' => '<i class="fas fa-download"></i> ',
|
||||||
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/sharingGroupsAction',
|
||||||
|
'popover_url' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSelectedSharingGroupsAction'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'text' => __('Push sharing groups'),
|
||||||
|
'html' => '<i class="fas fa-upload"></i> ',
|
||||||
|
'class' => 'btn btn-primary',
|
||||||
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/SharingGroupsAction',
|
||||||
|
'popover_url' => '/localTools/action/' . h($params['connection']['id']) . '/pushSharingGroupsAction'
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
]
|
||||||
|
],
|
||||||
'fields' => [
|
'fields' => [
|
||||||
|
[
|
||||||
|
'element' => 'selector',
|
||||||
|
'class' => 'short',
|
||||||
|
'data' => [
|
||||||
|
'id' => [
|
||||||
|
'value_path' => 'SharingGroup.uuid'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => 'Name',
|
'name' => 'Name',
|
||||||
'sort' => 'SharingGroup.name',
|
'sort' => 'SharingGroup.name',
|
||||||
'data_path' => 'SharingGroup.name',
|
'data_path' => 'SharingGroup.name',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Status',
|
||||||
|
'sort' => 'SharingGroup.local_copy',
|
||||||
|
'data_path' => 'SharingGroup.local_copy',
|
||||||
|
'element' => 'status',
|
||||||
|
'status_levels' => $statusLevels
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Differences',
|
||||||
|
'data_path' => 'SharingGroup.differences',
|
||||||
|
'element' => 'list'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => 'uuid',
|
'name' => 'uuid',
|
||||||
'sort' => 'SharingGroup.uuid',
|
'sort' => 'SharingGroup.uuid',
|
||||||
|
@ -662,21 +1122,16 @@ class MispConnector extends CommonConnectorTools
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'Organisations',
|
'name' => 'Organisations',
|
||||||
'sort' => 'Organisation',
|
'sort' => 'SharingGroupOrg',
|
||||||
'data_path' => 'Organisation',
|
'data_path' => 'SharingGroupOrg',
|
||||||
'element' => 'count_summary'
|
'element' => 'count_summary',
|
||||||
|
'title' => 'foo'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'Roaming',
|
'name' => 'Roaming',
|
||||||
'sort' => 'SharingGroup.roaming',
|
'sort' => 'SharingGroup.roaming',
|
||||||
'data_path' => 'SharingGroup.roaming',
|
'data_path' => 'SharingGroup.roaming',
|
||||||
'element' => 'boolean'
|
'element' => 'boolean'
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'External servers',
|
|
||||||
'sort' => 'Server',
|
|
||||||
'data_path' => 'Server',
|
|
||||||
'element' => 'count_summary'
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'title' => false,
|
'title' => false,
|
||||||
|
@ -687,7 +1142,15 @@ class MispConnector extends CommonConnectorTools
|
||||||
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSharingGroupAction?uuid={{0}}',
|
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSharingGroupAction?uuid={{0}}',
|
||||||
'modal_params_data_path' => ['SharingGroup.uuid'],
|
'modal_params_data_path' => ['SharingGroup.uuid'],
|
||||||
'icon' => 'download',
|
'icon' => 'download',
|
||||||
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/SharingGroupsAction'
|
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/SharingGroupsAction',
|
||||||
|
'complex_requirement' => [
|
||||||
|
'function' => function ($row, $options) {
|
||||||
|
if ($row['SharingGroup']['roaming']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -703,7 +1166,7 @@ class MispConnector extends CommonConnectorTools
|
||||||
return [
|
return [
|
||||||
'data' => [
|
'data' => [
|
||||||
'title' => __('Fetch organisation'),
|
'title' => __('Fetch organisation'),
|
||||||
'description' => __('Fetch and create/update organisation ({0}) from MISP.', $params['uuid']),
|
'description' => __('Fetch and create/update organisation ({0}) from MISP?', $params['uuid']),
|
||||||
'submit' => [
|
'submit' => [
|
||||||
'action' => $params['request']->getParam('action')
|
'action' => $params['request']->getParam('action')
|
||||||
],
|
],
|
||||||
|
@ -726,6 +1189,313 @@ class MispConnector extends CommonConnectorTools
|
||||||
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
|
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function fetchSelectedOrganisationsAction(array $params): array
|
||||||
|
{
|
||||||
|
$ids = $params['request']->getQuery('ids');
|
||||||
|
if ($params['request']->is(['get'])) {
|
||||||
|
return [
|
||||||
|
'data' => [
|
||||||
|
'title' => __('Fetch organisations'),
|
||||||
|
'description' => is_array($ids) ?
|
||||||
|
__('Fetch and create/update the selected {0} organisations from MISP?', count($ids)) :
|
||||||
|
__('Fetch and create/update ALL organisations from MISP?'),
|
||||||
|
'submit' => [
|
||||||
|
'action' => $params['request']->getParam('action')
|
||||||
|
],
|
||||||
|
'url' => is_array($ids) ?
|
||||||
|
['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'fetchSelectedOrganisationsAction'] :
|
||||||
|
['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'fetchSelectedOrganisationsAction?ids=all']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} elseif ($params['request']->is(['post'])) {
|
||||||
|
$successes = 0;
|
||||||
|
$errors = 0;
|
||||||
|
if (!is_array($ids) && $ids === 'all') {
|
||||||
|
$response = $this->getData('/organisations/index/scope:all', $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$orgs = $response->getJson();
|
||||||
|
foreach ($orgs as $org) {
|
||||||
|
$result = $this->captureOrganisation($org['Organisation']);
|
||||||
|
if ($result) {
|
||||||
|
$successes++;
|
||||||
|
} else {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$response = $this->getData('/organisations/view/' . $id, $params);
|
||||||
|
$result = $this->captureOrganisation($response->getJson()['Organisation']);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$successes++;
|
||||||
|
} else {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($successes) {
|
||||||
|
return ['success' => 1, 'message' => __('The fetching of organisations has succeeded. {0} organisations created/modified and {1} organisations could not be created/modified.', $successes, $errors)];
|
||||||
|
} else {
|
||||||
|
return ['success' => 0, 'message' => __('The fetching of organisations has failed. {0} organisations could not be created/modified.', $errors)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchSelectedSharingGroupsAction(array $params): array
|
||||||
|
{
|
||||||
|
$ids = $params['request']->getQuery('ids');
|
||||||
|
if ($params['request']->is(['get'])) {
|
||||||
|
return [
|
||||||
|
'data' => [
|
||||||
|
'title' => __('Fetch sharing groups'),
|
||||||
|
'description' => __('Fetch and create/update the selected {0} sharing groups from MISP?', count($ids)),
|
||||||
|
'submit' => [
|
||||||
|
'action' => $params['request']->getParam('action')
|
||||||
|
],
|
||||||
|
'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'fetchSelectedSharingGroupsAction']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} elseif ($params['request']->is(['post'])) {
|
||||||
|
$successes = 0;
|
||||||
|
$errors = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$response = $this->getData('/sharingGroups/view/' . $id, $params);
|
||||||
|
$temp = $response->getJson();
|
||||||
|
$sg = $temp['SharingGroup'];
|
||||||
|
$sg['organisation'] = $temp['Organisation'];
|
||||||
|
foreach ($temp['SharingGroupOrg'] as $sgo) {
|
||||||
|
$sg['sharing_group_orgs'][] = [
|
||||||
|
'extend' => $sgo['extend'],
|
||||||
|
'name' => $sgo['Organisation']['name'],
|
||||||
|
'uuid' => $sgo['Organisation']['uuid']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$result = $this->captureSharingGroup($sg, $params['user_id']);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$successes++;
|
||||||
|
} else {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($successes) {
|
||||||
|
return ['success' => 1, 'message' => __('The fetching of organisations has succeeded. {0} organisations created/modified and {1} organisations could not be created/modified.', $successes, $errors)];
|
||||||
|
} else {
|
||||||
|
return ['success' => 0, 'message' => __('The fetching of organisations has failed. {0} organisations could not be created/modified.', $errors)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pushOrganisationAction(array $params): array
|
||||||
|
{
|
||||||
|
if ($params['request']->is(['get'])) {
|
||||||
|
return [
|
||||||
|
'data' => [
|
||||||
|
'title' => __('Push organisation'),
|
||||||
|
'description' => __('Push or update organisation ({0}) on MISP.', $params['uuid']),
|
||||||
|
'submit' => [
|
||||||
|
'action' => $params['request']->getParam('action')
|
||||||
|
],
|
||||||
|
'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'pushOrganisationAction', $params['uuid']]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} elseif ($params['request']->is(['post'])) {
|
||||||
|
$org = $this->getOrganisation($params['uuid']);
|
||||||
|
if (empty($org)) {
|
||||||
|
return ['success' => 0, 'message' => __('Could not find the organisation.')];
|
||||||
|
}
|
||||||
|
$params['body'] = json_encode($org);
|
||||||
|
$response = $this->getData('/organisations/view/' . $params['uuid'], $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$response = $this->postData('/admin/organisations/edit/' . $params['uuid'], $params);
|
||||||
|
$result = $this->captureOrganisation($response->getJson()['Organisation']);
|
||||||
|
if ($response->getStatusCode() == 200 && $result) {
|
||||||
|
return ['success' => 1, 'message' => __('Organisation modified.')];
|
||||||
|
} else {
|
||||||
|
return ['success' => 0, 'message' => __('Could not save the changes to the organisation.')];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response = $this->postData('/admin/organisations/add/', $params);
|
||||||
|
$result = $this->captureOrganisation($response->getJson()['Organisation']);
|
||||||
|
if ($response->getStatusCode() == 200 && $result) {
|
||||||
|
return ['success' => 1, 'message' => __('Organisation created.')];
|
||||||
|
} else {
|
||||||
|
return ['success' => 0, 'message' => __('Could not create the organisation.')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pushOrganisationsAction(array $params): array
|
||||||
|
{
|
||||||
|
$orgSelectorValues = $this->getOrganisationSelectorValues();
|
||||||
|
if ($params['request']->is(['get'])) {
|
||||||
|
return [
|
||||||
|
'data' => [
|
||||||
|
'title' => __('Push organisation'),
|
||||||
|
'description' => __('Push or update organisations on MISP.'),
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'field' => 'local',
|
||||||
|
'label' => __('Only organisations with users'),
|
||||||
|
'type' => 'checkbox'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'field' => 'type',
|
||||||
|
'label' => __('Type'),
|
||||||
|
'type' => 'select',
|
||||||
|
'options' => $orgSelectorValues['type']
|
||||||
|
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'field' => 'sector',
|
||||||
|
'label' => __('Sector'),
|
||||||
|
'type' => 'select',
|
||||||
|
'options' => $orgSelectorValues['sector']
|
||||||
|
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'field' => 'nationality',
|
||||||
|
'label' => __('Country'),
|
||||||
|
'type' => 'select',
|
||||||
|
'options' => $orgSelectorValues['nationality']
|
||||||
|
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'submit' => [
|
||||||
|
'action' => $params['request']->getParam('action')
|
||||||
|
],
|
||||||
|
'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'pushOrganisationsAction']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} elseif ($params['request']->is(['post'])) {
|
||||||
|
$filters = $params['request']->getData();
|
||||||
|
$orgs = $this->getFilteredOrganisations($filters, true);
|
||||||
|
$created = 0;
|
||||||
|
$modified = 0;
|
||||||
|
$errors = 0;
|
||||||
|
$params['softError'] = 1;
|
||||||
|
if (empty($orgs)) {
|
||||||
|
return ['success' => 0, 'message' => __('Could not find any organisations matching the criteria.')];
|
||||||
|
}
|
||||||
|
foreach ($orgs as $org) {
|
||||||
|
$params['body'] = null;
|
||||||
|
$response = $this->getData('/organisations/view/' . $org->uuid, $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$params['body'] = json_encode($org);
|
||||||
|
$response = $this->postData('/admin/organisations/edit/' . $org->uuid, $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$modified++;
|
||||||
|
} else {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$params['body'] = json_encode($org);
|
||||||
|
$response = $this->postData('/admin/organisations/add', $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$created++;
|
||||||
|
} else {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($created || $modified) {
|
||||||
|
return ['success' => 1, 'message' => __('Organisations created: {0}, modified: {1}, errors: {2}', $created, $modified, $errors)];
|
||||||
|
} else {
|
||||||
|
return ['success' => 0, 'message' => __('Organisations could not be pushed. Errors: {0}', $errors)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pushSharingGroupsAction(array $params): array
|
||||||
|
{
|
||||||
|
if ($params['request']->is(['get'])) {
|
||||||
|
return [
|
||||||
|
'data' => [
|
||||||
|
'title' => __('Push sharing groups'),
|
||||||
|
'description' => __('Push or update sharinggroups to MISP.'),
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'field' => 'name',
|
||||||
|
'label' => __('Name'),
|
||||||
|
'placeholder' => __('Search for sharing groups by name. You can use the % character as a wildcard.'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'field' => 'releasability',
|
||||||
|
'label' => __('Releasable to'),
|
||||||
|
'placeholder' => __('Search for sharing groups by relesability. You can use the % character as a wildcard.'),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'submit' => [
|
||||||
|
'action' => $params['request']->getParam('action')
|
||||||
|
],
|
||||||
|
'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'pushSharingGroupsAction']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} elseif ($params['request']->is(['post'])) {
|
||||||
|
$filters = $params['request']->getData();
|
||||||
|
$sgs = $this->getFilteredSharingGroups($filters, true);
|
||||||
|
$created = 0;
|
||||||
|
$modified = 0;
|
||||||
|
$errors = 0;
|
||||||
|
$params['softError'] = 1;
|
||||||
|
if (empty($sgs)) {
|
||||||
|
return ['success' => 0, 'message' => __('Could not find any sharing groups matching the criteria.')];
|
||||||
|
}
|
||||||
|
foreach ($sgs as $sg) {
|
||||||
|
$params['body'] = null;
|
||||||
|
$sgToPush = [
|
||||||
|
'name' => $sg->name,
|
||||||
|
'uuid' => $sg->uuid,
|
||||||
|
'releasability' => $sg->releasability,
|
||||||
|
'description' => $sg->description,
|
||||||
|
'roaming' => true,
|
||||||
|
'organisation_uuid' => $sg->organisation->uuid,
|
||||||
|
'Organisation' => [
|
||||||
|
'uuid' => $sg->organisation->uuid,
|
||||||
|
'name' => $sg['organisation']['name']
|
||||||
|
],
|
||||||
|
'SharingGroupOrg' => []
|
||||||
|
];
|
||||||
|
foreach ($sg['sharing_group_orgs'] as $sgo) {
|
||||||
|
$sgToPush['SharingGroupOrg'][] = [
|
||||||
|
'extend' => $sgo['_joinData']['extend'],
|
||||||
|
'uuid' => $sgo['uuid'],
|
||||||
|
'name' => $sgo['name']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$response = $this->getData('/sharing_groups/view/' . $sg->uuid, $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$params['body'] = json_encode($sgToPush);
|
||||||
|
$response = $this->postData('/sharingGroups/edit/' . $sg->uuid, $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$modified++;
|
||||||
|
} else {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$params['body'] = json_encode($sgToPush);
|
||||||
|
$response = $this->postData('/sharingGroups/add', $params);
|
||||||
|
if ($response->getStatusCode() == 200) {
|
||||||
|
$created++;
|
||||||
|
} else {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($created || $modified) {
|
||||||
|
return ['success' => 1, 'message' => __('Sharing groups created: {0}, modified: {1}, errors: {2}', $created, $modified, $errors)];
|
||||||
|
} else {
|
||||||
|
return ['success' => 0, 'message' => __('Sharing groups could not be pushed. Errors: {0}', $errors)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
|
||||||
|
}
|
||||||
|
|
||||||
public function fetchSharingGroupAction(array $params): array
|
public function fetchSharingGroupAction(array $params): array
|
||||||
{
|
{
|
||||||
if ($params['request']->is(['get'])) {
|
if ($params['request']->is(['get'])) {
|
||||||
|
@ -752,7 +1522,11 @@ class MispConnector extends CommonConnectorTools
|
||||||
'sharing_group_orgs' => []
|
'sharing_group_orgs' => []
|
||||||
];
|
];
|
||||||
foreach ($mispSG['SharingGroupOrg'] as $sgo) {
|
foreach ($mispSG['SharingGroupOrg'] as $sgo) {
|
||||||
$sg['sharing_group_orgs'][] = $sgo['Organisation'];
|
$sg['sharing_group_orgs'][] = [
|
||||||
|
'name' => $sgo['Organisation']['name'],
|
||||||
|
'uuid' => $sgo['Organisation']['uuid'],
|
||||||
|
'extend' => $sgo['extend']
|
||||||
|
];
|
||||||
}
|
}
|
||||||
$result = $this->captureSharingGroup($sg, $params['user_id']);
|
$result = $this->captureSharingGroup($sg, $params['user_id']);
|
||||||
if ($result) {
|
if ($result) {
|
||||||
|
|
|
@ -78,6 +78,15 @@ class AppModel extends Entity
|
||||||
$this->meta_fields[$i]['template_namespace'] = $templates[$templateDirectoryId]['namespace'];
|
$this->meta_fields[$i]['template_namespace'] = $templates[$templateDirectoryId]['namespace'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!empty($options['smartFlattenMetafields'])) {
|
||||||
|
$smartFlatten = [];
|
||||||
|
foreach ($this->meta_fields as $metafield) {
|
||||||
|
$key = "{$metafield['template_name']}_v{$metafield['template_version']}:{$metafield['field']}";
|
||||||
|
$value = $metafield['value'];
|
||||||
|
$smartFlatten[$key] = $value;
|
||||||
|
}
|
||||||
|
$this->meta_fields = $smartFlatten;
|
||||||
|
}
|
||||||
// if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate'])) && !empty($this->MetaTemplates)) {
|
// if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate'])) && !empty($this->MetaTemplates)) {
|
||||||
if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate']))) {
|
if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate']))) {
|
||||||
unset($this->MetaTemplates);
|
unset($this->MetaTemplates);
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Entity;
|
||||||
|
|
||||||
|
use App\Model\Entity\AppModel;
|
||||||
|
use Cake\ORM\Entity;
|
||||||
|
|
||||||
|
class SGO extends AppModel
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -236,4 +236,140 @@ class InstanceTable extends AppTable
|
||||||
}
|
}
|
||||||
return $themes;
|
return $themes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTopology($mermaid = true): mixed
|
||||||
|
{
|
||||||
|
$BroodsModel = TableRegistry::getTableLocator()->get('Broods');
|
||||||
|
$LocalToolsModel = TableRegistry::getTableLocator()->get('LocalTools');
|
||||||
|
$connectors = $LocalToolsModel->getConnectors();
|
||||||
|
$connections = $LocalToolsModel->extractMeta($connectors, true);
|
||||||
|
$broods = $BroodsModel->find()->select(['id', 'uuid', 'url', 'name', 'pull'])->disableHydration()->toArray();
|
||||||
|
foreach ($broods as $k => $brood) {
|
||||||
|
$broods[$k]['status'] = $BroodsModel->queryStatus($brood['id']);
|
||||||
|
}
|
||||||
|
$data = [
|
||||||
|
'broods' => $broods,
|
||||||
|
'tools' => $connections
|
||||||
|
];
|
||||||
|
if ($mermaid) {
|
||||||
|
return $this->generateTopologyMermaid($data);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateTopologyMermaid($data)
|
||||||
|
{
|
||||||
|
$version = json_decode(file_get_contents(APP . 'VERSION.json'), true)["version"];
|
||||||
|
$newest = $version;
|
||||||
|
$broods = '';
|
||||||
|
$edges = '';
|
||||||
|
// pre-run the loop to get the latest version
|
||||||
|
foreach ($data['broods'] as $brood) {
|
||||||
|
if ($brood['status']['code'] === 200) {
|
||||||
|
if (version_compare($brood['status']['response']['version'], $newest) > 0) {
|
||||||
|
$newest = $brood['status']['response']['version'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($data['broods'] as $brood) {
|
||||||
|
$status = '';
|
||||||
|
if ($brood['status']['code'] === 200) {
|
||||||
|
$status = sprintf(
|
||||||
|
"<br />Ping: %sms<br />Version: <span class='%s'>v%s</span><br />Role: %s<br />",
|
||||||
|
h($brood['status']['ping']),
|
||||||
|
$brood['status']['response']['version'] === $newest ? 'text-success' : 'text-danger',
|
||||||
|
h($brood['status']['response']['version']) . ($brood['status']['response']['version'] !== $newest ? ' - outdated' : ''),
|
||||||
|
h($brood['status']['response']['role']['name'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$broods .= sprintf(
|
||||||
|
"%s%s end" . PHP_EOL,
|
||||||
|
sprintf(
|
||||||
|
' subgraph brood_%s[fas:fa-network-wired Brood #%s]' . PHP_EOL,
|
||||||
|
h($brood['id']),
|
||||||
|
h($brood['id'])
|
||||||
|
),
|
||||||
|
sprintf(
|
||||||
|
" cerebrate_%s[%s<br />%s<a href='/broods/view/%s'>fas:fa-eye</a>]" . PHP_EOL,
|
||||||
|
h($brood['id']),
|
||||||
|
"<span class='font-weight-bold'>" . h($brood['name']) . '</span>',
|
||||||
|
sprintf(
|
||||||
|
"Connected: <span class='%s' title='%s'>%s</span>%s",
|
||||||
|
$brood['status']['code'] === 200 ? 'text-success' : 'text-danger',
|
||||||
|
h($brood['status']['code']),
|
||||||
|
$brood['status']['code'] === 200 ? 'fas:fa-check' : 'fas:fa-times',
|
||||||
|
$status
|
||||||
|
),
|
||||||
|
h($brood['id']),
|
||||||
|
)
|
||||||
|
|
||||||
|
);
|
||||||
|
$edges .= sprintf(
|
||||||
|
' C1%s---cerebrate_%s' . PHP_EOL,
|
||||||
|
$brood['pull'] ? '<' : '',
|
||||||
|
h($brood['id'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$tools = '';
|
||||||
|
foreach ($data['tools'] as $k => $tool) {
|
||||||
|
$tools .= sprintf(
|
||||||
|
' subgraph instance_local_tools_%s[%s %s connector]' . PHP_EOL . ' direction TB' . PHP_EOL,
|
||||||
|
h($k),
|
||||||
|
isset($tool['logo']) ? "<img src='/img/local_tools/" . h($tool['logo']) . "' style='width: 50px; height:50px;' />" : 'fas:fa-wrench',
|
||||||
|
h($tool['name'])
|
||||||
|
);
|
||||||
|
foreach ($tool['connections'] as $k2 => $connection) {
|
||||||
|
$diagnostic_output = '';
|
||||||
|
if (!empty($connection['diagnostics'])) {
|
||||||
|
foreach ($connection['diagnostics'] as $diagnostic => $diagnostic_data) {
|
||||||
|
$diagnostic_output .= sprintf(
|
||||||
|
"%s: <span class='text-%s'>%s</span><br />",
|
||||||
|
h($diagnostic),
|
||||||
|
h($diagnostic_data['type']),
|
||||||
|
h($diagnostic_data['message'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$tools .= sprintf(
|
||||||
|
" connection%s[\"%s<br />%s<br />%s%s\"]" . PHP_EOL,
|
||||||
|
h($k2),
|
||||||
|
h($connection['name']),
|
||||||
|
sprintf(
|
||||||
|
__('Health') . ": <span title='%s' class='%s'>%s</span>",
|
||||||
|
h($connection['message']),
|
||||||
|
$connection['health'] === 1 ? 'text-success' : 'text-danger',
|
||||||
|
$connection['health'] === 1 ? 'fas:fa-check' : 'fas:fa-times'
|
||||||
|
),
|
||||||
|
empty($diagnostic_data) ? '' : 'Diagnostics:<br />' . $diagnostic_output,
|
||||||
|
sprintf(
|
||||||
|
"<a href='%s'>fas:fa-eye</a>",
|
||||||
|
h($connection['url'])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$tools .= ' end' . PHP_EOL;
|
||||||
|
}
|
||||||
|
$this_cerebrate = sprintf(
|
||||||
|
"C1[My Cerebrate<br />Version: <span class='%s'>v%s</span>]",
|
||||||
|
$version === $newest ? 'text-success' : 'text-danger',
|
||||||
|
$version
|
||||||
|
);
|
||||||
|
$md = sprintf(
|
||||||
|
'flowchart TB
|
||||||
|
subgraph instance[fas:fa-network-wired My Brood]
|
||||||
|
direction TB
|
||||||
|
%s
|
||||||
|
subgraph instance_local_tools[fa:fa-tools Local Tools]
|
||||||
|
direction LR
|
||||||
|
%s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
%s%s',
|
||||||
|
$this_cerebrate,
|
||||||
|
$tools,
|
||||||
|
$broods,
|
||||||
|
$edges
|
||||||
|
);
|
||||||
|
return $md;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,14 +147,14 @@ class LocalToolsTable extends AppTable
|
||||||
'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [],
|
'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [],
|
||||||
];
|
];
|
||||||
if ($includeConnections) {
|
if ($includeConnections) {
|
||||||
$connector['connections'] = $this->healthCheck($connector_type, $connector_class);
|
$connector['connections'] = $this->healthCheck($connector_type, $connector_class, true);
|
||||||
}
|
}
|
||||||
$connectors[] = $connector;
|
$connectors[] = $connector;
|
||||||
}
|
}
|
||||||
return $connectors;
|
return $connectors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function healthCheck(string $connector_type, Object $connector_class): array
|
public function healthCheck(string $connector_type, Object $connector_class, bool $includeDiagnostics = false): array
|
||||||
{
|
{
|
||||||
$query = $this->find();
|
$query = $this->find();
|
||||||
$query->where([
|
$query->where([
|
||||||
|
@ -162,11 +162,28 @@ class LocalToolsTable extends AppTable
|
||||||
]);
|
]);
|
||||||
$connections = $query->all()->toList();
|
$connections = $query->all()->toList();
|
||||||
foreach ($connections as &$connection) {
|
foreach ($connections as &$connection) {
|
||||||
$connection = $this->healthCheckIndividual($connection);
|
$temp = $this->healthCheckIndividual($connection);
|
||||||
|
if ($includeDiagnostics && !empty($temp['health']) && $temp['health'] === 1) {
|
||||||
|
$temp['diagnostics'] = $this->diagnosticCheckIndividual($connection);
|
||||||
|
}
|
||||||
|
$connection = $temp;
|
||||||
}
|
}
|
||||||
return $connections;
|
return $connections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function diagnosticCheckIndividual(Object $connection): array
|
||||||
|
{
|
||||||
|
$connector_class = $this->getConnectors($connection->connector);
|
||||||
|
if (empty($connector_class[$connection->connector])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$connector_class = $connector_class[$connection->connector];
|
||||||
|
return $connector_class->diagnostics([
|
||||||
|
'connection' => $connection,
|
||||||
|
'softError' => 1
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function healthCheckIndividual(Object $connection): array
|
public function healthCheckIndividual(Object $connection): array
|
||||||
{
|
{
|
||||||
$connector_class = $this->getConnectors($connection->connector);
|
$connector_class = $this->getConnectors($connection->connector);
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Table;
|
||||||
|
|
||||||
|
use App\Model\Table\AppTable;
|
||||||
|
use Cake\ORM\Table;
|
||||||
|
use Cake\Validation\Validator;
|
||||||
|
use Cake\ORM\RulesChecker;
|
||||||
|
use Cake\ORM\TableRegistry;
|
||||||
|
|
||||||
|
class SGOsTable extends AppTable
|
||||||
|
{
|
||||||
|
public function initialize(array $config): void
|
||||||
|
{
|
||||||
|
$this->setTable('sgo');
|
||||||
|
parent::initialize($config);
|
||||||
|
$this->belongsTo('SharingGroups');
|
||||||
|
$this->belongsTo('Organisations');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attach(int $sg_id, int $org_id, array $additional_data = []): bool
|
||||||
|
{
|
||||||
|
$sgo = $this->find()->where([
|
||||||
|
'sharing_group_id' => $sg_id,
|
||||||
|
'organisation_id' => $org_id
|
||||||
|
])->first();
|
||||||
|
if (empty($sgo)) {
|
||||||
|
$sgo = $this->newEmptyEntity();
|
||||||
|
$sgo->sharing_group_id = $sg_id;
|
||||||
|
$sgo->organisation_id = $org_id;
|
||||||
|
}
|
||||||
|
$sgo->extend = empty($additional_data['extend']) ? 0 : 1;
|
||||||
|
if ($this->save($sgo)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detach(): bool
|
||||||
|
{
|
||||||
|
$sgo = $this->find()->where([
|
||||||
|
'sharing_group_id' => $sg_id,
|
||||||
|
'organisation_id' => $org_id
|
||||||
|
])->first();
|
||||||
|
if (!empty($sgo)) {
|
||||||
|
if (!$this->delete($sgo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,11 +25,11 @@ class SharingGroupsTable extends AppTable
|
||||||
$this->belongsToMany(
|
$this->belongsToMany(
|
||||||
'SharingGroupOrgs',
|
'SharingGroupOrgs',
|
||||||
[
|
[
|
||||||
|
'through' => 'SGOs',
|
||||||
'className' => 'Organisations',
|
'className' => 'Organisations',
|
||||||
'foreignKey' => 'sharing_group_id',
|
'foreignKey' => 'sharing_group_id',
|
||||||
'joinTable' => 'sgo',
|
|
||||||
'targetForeignKey' => 'organisation_id'
|
'targetForeignKey' => 'organisation_id'
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
$this->setDisplayField('name');
|
$this->setDisplayField('name');
|
||||||
}
|
}
|
||||||
|
@ -77,11 +77,17 @@ class SharingGroupsTable extends AppTable
|
||||||
|
|
||||||
public function postCaptureActions($savedEntity, $input): void
|
public function postCaptureActions($savedEntity, $input): void
|
||||||
{
|
{
|
||||||
$orgs = [];
|
$SGO = TableRegistry::getTableLocator()->get('SGOs');
|
||||||
foreach ($input['sharing_group_orgs'] as $sgo) {
|
foreach ($input['sharing_group_orgs'] as $sgo) {
|
||||||
$organisation_id = $this->Organisations->captureOrg($sgo);
|
$organisation_id = $this->Organisations->captureOrg($sgo);
|
||||||
$orgs[] = $this->SharingGroupOrgs->get($organisation_id);
|
$sgo_entity = $SGO->newEntity(
|
||||||
|
[
|
||||||
|
'sharing_group_id' => $savedEntity->id,
|
||||||
|
'organisation_id' => $organisation_id,
|
||||||
|
'extend' => $sgo['extend']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$SGO->save($sgo_entity);
|
||||||
}
|
}
|
||||||
$this->SharingGroupOrgs->link($savedEntity, $orgs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ class UsersTable extends AppTable
|
||||||
|
|
||||||
private function initAuthBehaviors()
|
private function initAuthBehaviors()
|
||||||
{
|
{
|
||||||
if (!empty(Configure::read('keycloak'))) {
|
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||||
$this->addBehavior('AuthKeycloak');
|
$this->addBehavior('AuthKeycloak');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,7 +279,7 @@ class UsersTable extends AppTable
|
||||||
|
|
||||||
public function enrollUserRouter($data): void
|
public function enrollUserRouter($data): void
|
||||||
{
|
{
|
||||||
if (!empty(Configure::read('keycloak'))) {
|
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||||
$this->enrollUser($data);
|
$this->enrollUser($data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"version": "1.16",
|
"version": "1.17",
|
||||||
"application": "Cerebrate"
|
"application": "Cerebrate"
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,6 +185,9 @@ class BootstrapDropdownMenu extends BootstrapGeneric
|
||||||
$classes = array_merge($classes, $entry['class']);
|
$classes = array_merge($classes, $entry['class']);
|
||||||
}
|
}
|
||||||
$params = $entry['attrs'] ?? [];
|
$params = $entry['attrs'] ?? [];
|
||||||
|
if (!empty($entry['onclick'])) {
|
||||||
|
$params['onclick'] = $entry['onclick'];
|
||||||
|
}
|
||||||
$params['href'] = '#';
|
$params['href'] = '#';
|
||||||
|
|
||||||
if (!empty($entry['menu'])) {
|
if (!empty($entry['menu'])) {
|
||||||
|
|
|
@ -61,6 +61,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
'title' => __('Broods Index'),
|
'title' => __('Broods Index'),
|
||||||
'description' => __('Cerebrate can connect to other Cerebrate instances to exchange trust information and to instrument interconnectivity between connected local tools. Each such Cerebrate instance with its connected tools is considered to be a brood.'),
|
'description' => __('Cerebrate can connect to other Cerebrate instances to exchange trust information and to instrument interconnectivity between connected local tools. Each such Cerebrate instance with its connected tools is considered to be a brood.'),
|
||||||
|
'includeAllPagination' => true,
|
||||||
'pull' => 'right',
|
'pull' => 'right',
|
||||||
'actions' => [
|
'actions' => [
|
||||||
[
|
[
|
||||||
|
|
|
@ -36,6 +36,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'class' => 'short',
|
'class' => 'short',
|
||||||
'data_path' => 'status',
|
'data_path' => 'status',
|
||||||
'sort' => 'status',
|
'sort' => 'status',
|
||||||
|
'display_field_data_path' => 'email',
|
||||||
'element' => 'brood_sync_status',
|
'element' => 'brood_sync_status',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
@ -37,6 +37,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'class' => 'short',
|
'class' => 'short',
|
||||||
'data_path' => 'status',
|
'data_path' => 'status',
|
||||||
'sort' => 'status',
|
'sort' => 'status',
|
||||||
|
'display_field_data_path' => 'name',
|
||||||
'element' => 'brood_sync_status',
|
'element' => 'brood_sync_status',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
@ -78,6 +78,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
'title' => __('ContactDB Individuals Index'),
|
'title' => __('ContactDB Individuals Index'),
|
||||||
'description' => __('A list of individuals known by your Cerebrate instance. This list can get populated either directly, by adding new individuals or by fetching them from trusted remote sources. Additionally, users created for the platform will always have an individual identity.'),
|
'description' => __('A list of individuals known by your Cerebrate instance. This list can get populated either directly, by adding new individuals or by fetching them from trusted remote sources. Additionally, users created for the platform will always have an individual identity.'),
|
||||||
|
'includeAllPagination' => true,
|
||||||
'actions' => [
|
'actions' => [
|
||||||
[
|
[
|
||||||
'url' => '/individuals/view',
|
'url' => '/individuals/view',
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
echo sprintf(
|
||||||
|
'<a class="btn btn-primary" href="%s/instance/downloadTopology">Download markdown</a>',
|
||||||
|
h($baseurl)
|
||||||
|
);
|
||||||
|
echo $this->element('genericElements/mermaid', [
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
|
@ -169,6 +169,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
'title' => __('Meta Field Templates'),
|
'title' => __('Meta Field Templates'),
|
||||||
'description' => __('The various templates used to enrich certain objects by a set of standardised fields.'),
|
'description' => __('The various templates used to enrich certain objects by a set of standardised fields.'),
|
||||||
|
'includeAllPagination' => true,
|
||||||
'actions' => [
|
'actions' => [
|
||||||
[
|
[
|
||||||
'url' => '/metaTemplates/view',
|
'url' => '/metaTemplates/view',
|
||||||
|
|
|
@ -62,6 +62,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
'title' => __('Organisation Groups Index'),
|
'title' => __('Organisation Groups Index'),
|
||||||
'description' => __('OrgGroups are an administrative concept, multiple organisations can belong to a grouping that allows common management by so called "GroupAdmins". This helps grouping organisations by sector, country or other commonalities into co-managed sub-communities.'),
|
'description' => __('OrgGroups are an administrative concept, multiple organisations can belong to a grouping that allows common management by so called "GroupAdmins". This helps grouping organisations by sector, country or other commonalities into co-managed sub-communities.'),
|
||||||
|
'includeAllPagination' => true,
|
||||||
'actions' => [
|
'actions' => [
|
||||||
[
|
[
|
||||||
'url' => '/orgGroups/view',
|
'url' => '/orgGroups/view',
|
||||||
|
|
|
@ -97,6 +97,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
'title' => __('ContactDB Organisation Index'),
|
'title' => __('ContactDB Organisation Index'),
|
||||||
'description' => __('A list of organisations known by your Cerebrate instance. This list can get populated either directly, by adding new organisations or by fetching them from trusted remote sources.'),
|
'description' => __('A list of organisations known by your Cerebrate instance. This list can get populated either directly, by adding new organisations or by fetching them from trusted remote sources.'),
|
||||||
|
'includeAllPagination' => true,
|
||||||
'actions' => [
|
'actions' => [
|
||||||
[
|
[
|
||||||
'url' => '/organisations/view',
|
'url' => '/organisations/view',
|
||||||
|
|
|
@ -77,6 +77,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
'title' => __('Roles Index'),
|
'title' => __('Roles Index'),
|
||||||
'description' => __('A list of configurable user roles. Create or modify user access roles based on the settings below.'),
|
'description' => __('A list of configurable user roles. Create or modify user access roles based on the settings below.'),
|
||||||
|
'includeAllPagination' => true,
|
||||||
'pull' => 'right',
|
'pull' => 'right',
|
||||||
'actions' => [
|
'actions' => [
|
||||||
[
|
[
|
||||||
|
|
|
@ -9,6 +9,11 @@
|
||||||
'label' => __('Owner organisation'),
|
'label' => __('Owner organisation'),
|
||||||
'options' => $dropdownData['organisation']
|
'options' => $dropdownData['organisation']
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'field' => 'extend',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('Can extend/administer')
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'submit' => [
|
'submit' => [
|
||||||
'action' => $this->request->getParam('action')
|
'action' => $this->request->getParam('action')
|
||||||
|
|
|
@ -57,6 +57,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
'title' => __('Sharing Groups Index'),
|
'title' => __('Sharing Groups Index'),
|
||||||
'description' => __('Sharing groups are distribution lists usable by tools that can exchange information with a list of trusted partners. Create recurring or ad hoc sharing groups and share them with the members of the sharing group.'),
|
'description' => __('Sharing groups are distribution lists usable by tools that can exchange information with a list of trusted partners. Create recurring or ad hoc sharing groups and share them with the members of the sharing group.'),
|
||||||
|
'includeAllPagination' => true,
|
||||||
'pull' => 'right',
|
'pull' => 'right',
|
||||||
'actions' => [
|
'actions' => [
|
||||||
[
|
[
|
||||||
|
|
|
@ -42,6 +42,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'sort' => 'uuid',
|
'sort' => 'uuid',
|
||||||
'class' => 'short',
|
'class' => 'short',
|
||||||
'data_path' => 'uuid',
|
'data_path' => 'uuid',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Can extend/administer'),
|
||||||
|
'sort' => 'extend',
|
||||||
|
'element' => 'boolean',
|
||||||
|
'class' => 'short',
|
||||||
|
'data_path' => 'extend',
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'pull' => 'right',
|
'pull' => 'right',
|
||||||
|
|
|
@ -17,15 +17,21 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'type' => 'context_filters',
|
||||||
|
'context_filters' => $filteringContexts
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'search',
|
'type' => 'search',
|
||||||
'button' => __('Search'),
|
'button' => __('Search'),
|
||||||
'placeholder' => __('Enter value to search'),
|
'placeholder' => __('Enter value to search'),
|
||||||
'data' => '',
|
'data' => '',
|
||||||
'searchKey' => 'value'
|
'searchKey' => 'value',
|
||||||
|
'allowFilering' => true
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'type' => 'table_action',
|
'type' => 'table_action',
|
||||||
|
'table_setting_id' => 'user_index',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
$data = $this->Hash->extract($row, $field['data_path'])[0];
|
||||||
|
$status_levels = $field['status_levels'];
|
||||||
|
echo sprintf(
|
||||||
|
'<i class="text-%s fas fa-%s" title="%s"></i>',
|
||||||
|
h($field['status_levels'][$data]['colour']),
|
||||||
|
empty($field['status_levels'][$data]['icon']) ? 'circle' : h($field['status_levels'][$data]['icon']),
|
||||||
|
h($field['status_levels'][$data]['message'])
|
||||||
|
);
|
||||||
|
?>
|
|
@ -73,6 +73,25 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function openModalForButton<?= $seed ?>(clicked, url, reloadUrl='') {
|
function openModalForButton<?= $seed ?>(clicked, url, reloadUrl='') {
|
||||||
|
var selected_ids = [];
|
||||||
|
$('.selectable_row:checkbox:checked').each(function () {
|
||||||
|
selected_ids.push($(this).data('id'));
|
||||||
|
});
|
||||||
|
if (selected_ids.length > 0) {
|
||||||
|
if (url.includes('?')) {
|
||||||
|
url += '&';
|
||||||
|
} else {
|
||||||
|
url += '?';
|
||||||
|
}
|
||||||
|
var first = true;
|
||||||
|
selected_ids.forEach(function (id) {
|
||||||
|
if (!first) {
|
||||||
|
url += '&';
|
||||||
|
}
|
||||||
|
url += 'ids[]=' + id;
|
||||||
|
first = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
const fallbackReloadUrl = '<?= $this->Url->build(['action' => 'index']); ?>'
|
const fallbackReloadUrl = '<?= $this->Url->build(['action' => 'index']); ?>'
|
||||||
reloadUrl = reloadUrl != '' ? reloadUrl : fallbackReloadUrl
|
reloadUrl = reloadUrl != '' ? reloadUrl : fallbackReloadUrl
|
||||||
UI.overlayUntilResolve(clicked, UI.submissionModalForIndex(url, reloadUrl, '<?= $tableRandomValue ?>'))
|
UI.overlayUntilResolve(clicked, UI.submissionModalForIndex(url, reloadUrl, '<?= $tableRandomValue ?>'))
|
||||||
|
|
|
@ -24,7 +24,10 @@
|
||||||
$filteringButton = '';
|
$filteringButton = '';
|
||||||
if (!empty($data['allowFilering'])) {
|
if (!empty($data['allowFilering'])) {
|
||||||
$activeFilters = !empty($activeFilters) ? $activeFilters : [];
|
$activeFilters = !empty($activeFilters) ? $activeFilters : [];
|
||||||
$numberActiveFilters = count($activeFilters);
|
$activeFiltersFiltered = array_filter($activeFilters, function ($k) {
|
||||||
|
return !str_starts_with($k, '_');
|
||||||
|
}, ARRAY_FILTER_USE_KEY);
|
||||||
|
$numberActiveFilters = count($activeFiltersFiltered);
|
||||||
if (!empty($activeFilters['filteringMetaFields'])) {
|
if (!empty($activeFilters['filteringMetaFields'])) {
|
||||||
$numberActiveFilters += count($activeFilters['filteringMetaFields']) - 1;
|
$numberActiveFilters += count($activeFilters['filteringMetaFields']) - 1;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +37,7 @@
|
||||||
'title' => __('Filter index'),
|
'title' => __('Filter index'),
|
||||||
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
||||||
];
|
];
|
||||||
if (count($activeFilters) > 0) {
|
if (count($activeFiltersFiltered) > 0) {
|
||||||
$buttonConfig['badge'] = [
|
$buttonConfig['badge'] = [
|
||||||
'variant' => 'light',
|
'variant' => 'light',
|
||||||
'text' => $numberActiveFilters,
|
'text' => $numberActiveFilters,
|
||||||
|
|
|
@ -5,6 +5,10 @@ use App\Utility\UI\IndexSetting;
|
||||||
if (empty($data['table_setting_id']) && empty($model)) {
|
if (empty($data['table_setting_id']) && empty($model)) {
|
||||||
throw new Exception(__('`table_setting_id` must be set in order to use the `table_action` table topbar'));
|
throw new Exception(__('`table_setting_id` must be set in order to use the `table_action` table topbar'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$now = date("Y-m-d_H-i-s");
|
||||||
|
$downloadFilename = sprintf('%s_%s', $data['table_setting_id'] ?? h($model), $now);
|
||||||
|
|
||||||
$data['table_setting_id'] = !empty($data['table_setting_id']) ? $data['table_setting_id'] : IndexSetting::getIDFromTable($model);
|
$data['table_setting_id'] = !empty($data['table_setting_id']) ? $data['table_setting_id'] : IndexSetting::getIDFromTable($model);
|
||||||
$tableSettings = IndexSetting::getTableSetting($loggedUser, $data['table_setting_id']);
|
$tableSettings = IndexSetting::getTableSetting($loggedUser, $data['table_setting_id']);
|
||||||
$compactDisplay = !empty($tableSettings['compact_display']);
|
$compactDisplay = !empty($tableSettings['compact_display']);
|
||||||
|
@ -52,6 +56,15 @@ $indexColumnMenu = array_merge(
|
||||||
$metaTemplateColumnMenu
|
$metaTemplateColumnMenu
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$indexDownloadMenu = [
|
||||||
|
['header' => true, 'text' => 'JSON', 'icon' => 'file-code'],
|
||||||
|
['text' => __('Download all'), 'onclick' => sprintf('downloadIndexTable(this, "%s")', $downloadFilename . '.json'), ],
|
||||||
|
['text' => __('Download filtered table'), 'onclick' => sprintf('downloadIndexTable(this, "%s", true)', $downloadFilename . '.json'), ],
|
||||||
|
['header' => true, 'text' => 'CSV', 'icon' => 'file-csv', ],
|
||||||
|
['text' => __('Download all'), 'onclick' => sprintf('downloadIndexTable(this, "%s")', $downloadFilename . '.csv'), ],
|
||||||
|
['text' => __('Download filtered table'), 'onclick' => sprintf('downloadIndexTable(this, "%s", true)', $downloadFilename . '.csv'), ],
|
||||||
|
];
|
||||||
|
|
||||||
$compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_action/compactDisplay', [
|
$compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_action/compactDisplay', [
|
||||||
'table_data' => $table_data,
|
'table_data' => $table_data,
|
||||||
'tableSettings' => $tableSettings,
|
'tableSettings' => $tableSettings,
|
||||||
|
@ -68,8 +81,6 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
|
||||||
?>
|
?>
|
||||||
<?php if (!isset($data['requirement']) || $data['requirement']) : ?>
|
<?php if (!isset($data['requirement']) || $data['requirement']) : ?>
|
||||||
<?php
|
<?php
|
||||||
$now = date("Y-m-d_H-i-s");
|
|
||||||
$downloadFilename = sprintf('%s_%s.json', $data['table_setting_id'] ?? h($model), $now);
|
|
||||||
echo $this->Bootstrap->dropdownMenu([
|
echo $this->Bootstrap->dropdownMenu([
|
||||||
'dropdown-class' => 'ms-1',
|
'dropdown-class' => 'ms-1',
|
||||||
'alignment' => 'end',
|
'alignment' => 'end',
|
||||||
|
@ -95,9 +106,8 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
|
||||||
[
|
[
|
||||||
'text' => __('Download'),
|
'text' => __('Download'),
|
||||||
'icon' => 'download',
|
'icon' => 'download',
|
||||||
'attrs' => [
|
'keepOpen' => true,
|
||||||
'onclick' => sprintf('downloadIndexTable(this, "%s")', $downloadFilename),
|
'menu' => $indexDownloadMenu,
|
||||||
],
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'html' => $compactDisplayHtml,
|
'html' => $compactDisplayHtml,
|
||||||
|
|
|
@ -37,7 +37,7 @@ if ($field['scope'] === 'individuals') {
|
||||||
foreach ($extracted['alignments'] as $alignment) {
|
foreach ($extracted['alignments'] as $alignment) {
|
||||||
$alignmentEntryHtml = '[' . $this->Bootstrap->node('span', ['class' => ['fw-bold']], h($alignment['type'])) . ']';
|
$alignmentEntryHtml = '[' . $this->Bootstrap->node('span', ['class' => ['fw-bold']], h($alignment['type'])) . ']';
|
||||||
$alignmentEntryHtml .= $this->Bootstrap->node('span', ['class' => ['ms-1']], sprintf(
|
$alignmentEntryHtml .= $this->Bootstrap->node('span', ['class' => ['ms-1']], sprintf(
|
||||||
'<a href="%s/organisations/view/%s">%s</a>',
|
'<a href="%s/individuals/view/%s">%s</a>',
|
||||||
$baseurl,
|
$baseurl,
|
||||||
h($alignment['individual']['id']),
|
h($alignment['individual']['id']),
|
||||||
h($alignment['individual']['email'])
|
h($alignment['individual']['email'])
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<pre class="mermaid"><?= $data ?></pre>
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from '/js/node_modules/mermaid/dist/mermaid.esm.min.mjs';
|
||||||
|
mermaid.initialize({ startOnLoad: true });
|
||||||
|
</script>
|
|
@ -56,7 +56,7 @@ class BroodsFixture extends TestFixture
|
||||||
'id' => self::BROOD_WIREMOCK_ID,
|
'id' => self::BROOD_WIREMOCK_ID,
|
||||||
'uuid' => $faker->uuid(),
|
'uuid' => $faker->uuid(),
|
||||||
'name' => 'wiremock',
|
'name' => 'wiremock',
|
||||||
'url' => 'http://localhost:8080',
|
'url' => sprintf('http://%s:%s', $_ENV['WIREMOCK_HOST'] ?? 'localhost', $_ENV['WIREMOCK_PORT'] ?? '8080'),
|
||||||
'description' => $faker->text,
|
'description' => $faker->text,
|
||||||
'organisation_id' => OrganisationsFixture::ORGANISATION_B_ID,
|
'organisation_id' => OrganisationsFixture::ORGANISATION_B_ID,
|
||||||
'trusted' => true,
|
'trusted' => true,
|
||||||
|
|
|
@ -11,8 +11,8 @@ class InboxFixture extends TestFixture
|
||||||
public $connection = 'test';
|
public $connection = 'test';
|
||||||
public $table = 'inbox';
|
public $table = 'inbox';
|
||||||
|
|
||||||
public const INBOX_USER_REGISTRATION_ID = 1;
|
public const INBOX_USER_REGISTRATION_UUID = 'e783b13a-7019-48f5-848e-582bb930a833';
|
||||||
public const INBOX_INCOMING_CONNECTION_REQUEST_ID = 2;
|
public const INBOX_INCOMING_CONNECTION_REQUEST_UUID = '9810bd94-16f9-42e0-b364-af59dba50a34';
|
||||||
|
|
||||||
public function init(): void
|
public function init(): void
|
||||||
{
|
{
|
||||||
|
@ -20,8 +20,7 @@ class InboxFixture extends TestFixture
|
||||||
|
|
||||||
$this->records = [
|
$this->records = [
|
||||||
[
|
[
|
||||||
'id' => self::INBOX_USER_REGISTRATION_ID,
|
'uuid' => self::INBOX_USER_REGISTRATION_UUID,
|
||||||
'uuid' => $faker->uuid(),
|
|
||||||
'scope' => 'User',
|
'scope' => 'User',
|
||||||
'action' => 'Registration',
|
'action' => 'Registration',
|
||||||
'title' => 'User account creation requested for foo@bar.com',
|
'title' => 'User account creation requested for foo@bar.com',
|
||||||
|
@ -37,15 +36,14 @@ class InboxFixture extends TestFixture
|
||||||
'modified' => $faker->dateTime()->getTimestamp()
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => self::INBOX_INCOMING_CONNECTION_REQUEST_ID,
|
'uuid' => self::INBOX_INCOMING_CONNECTION_REQUEST_UUID,
|
||||||
'uuid' => $faker->uuid(),
|
|
||||||
'scope' => 'LocalTool',
|
'scope' => 'LocalTool',
|
||||||
'action' => 'IncomingConnectionRequest',
|
'action' => 'IncomingConnectionRequest',
|
||||||
'title' => 'Request for MISP Inter-connection',
|
'title' => 'Request for MISP Inter-connection',
|
||||||
'origin' => 'http://127.0.0.1',
|
'origin' => 'http://127.0.0.1',
|
||||||
'comment' => null,
|
'comment' => null,
|
||||||
'description' => 'Handle Phase I of inter-connection when another cerebrate instance performs the request.',
|
'description' => 'Handle Phase I of inter-connection when another cerebrate instance performs the request.',
|
||||||
'user_id' => UsersFixture::USER_ORG_ADMIN_ID,
|
'user_id' => UsersFixture::USER_ADMIN_ID,
|
||||||
'data' => [
|
'data' => [
|
||||||
'connectorName' => 'MispConnector',
|
'connectorName' => 'MispConnector',
|
||||||
'cerebrateURL' => 'http://127.0.0.1',
|
'cerebrateURL' => 'http://127.0.0.1',
|
||||||
|
|
|
@ -15,6 +15,7 @@ class IndividualsFixture extends TestFixture
|
||||||
public const INDIVIDUAL_ORG_ADMIN_ID = 3;
|
public const INDIVIDUAL_ORG_ADMIN_ID = 3;
|
||||||
public const INDIVIDUAL_REGULAR_USER_ID = 4;
|
public const INDIVIDUAL_REGULAR_USER_ID = 4;
|
||||||
public const INDIVIDUAL_A_ID = 5;
|
public const INDIVIDUAL_A_ID = 5;
|
||||||
|
public const INDIVIDUAL_B_ID = 6;
|
||||||
|
|
||||||
public function init(): void
|
public function init(): void
|
||||||
{
|
{
|
||||||
|
@ -70,6 +71,16 @@ class IndividualsFixture extends TestFixture
|
||||||
'position' => 'user',
|
'position' => 'user',
|
||||||
'created' => $faker->dateTime()->getTimestamp(),
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
'modified' => $faker->dateTime()->getTimestamp()
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::INDIVIDUAL_B_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'email' => $faker->email(),
|
||||||
|
'first_name' => $faker->firstName,
|
||||||
|
'last_name' => $faker->lastName,
|
||||||
|
'position' => 'user',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
parent::init();
|
parent::init();
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Test\Fixture;
|
||||||
|
|
||||||
|
use Cake\TestSuite\Fixture\TestFixture;
|
||||||
|
|
||||||
|
class MetaTemplateDirectoryFixture extends TestFixture
|
||||||
|
{
|
||||||
|
public $connection = 'test';
|
||||||
|
|
||||||
|
public const META_TEMPLATE_DIRECTORY_ID = 1;
|
||||||
|
public const META_TEMPLATE_DIRECTORY_UUID = '99c7b7c0-23e2-4ba8-9ad4-97bcdadf4c81';
|
||||||
|
|
||||||
|
public function init(): void
|
||||||
|
{
|
||||||
|
$this->records = [
|
||||||
|
[
|
||||||
|
'id' => self::META_TEMPLATE_DIRECTORY_ID,
|
||||||
|
'uuid' => self::META_TEMPLATE_DIRECTORY_UUID,
|
||||||
|
'name' => 'Test Meta Template Directory',
|
||||||
|
'namespace' => 'cerebrate',
|
||||||
|
'version' => '1'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ class MetaTemplateFieldsFixture extends TestFixture
|
||||||
'field' => 'test_field_1',
|
'field' => 'test_field_1',
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'meta_template_id' => MetaTemplatesFixture::ENABLED_TEST_ORG_META_TEMPLATE_ID,
|
'meta_template_id' => MetaTemplatesFixture::ENABLED_TEST_ORG_META_TEMPLATE_ID,
|
||||||
|
'meta_template_directory_id' => MetaTemplateDirectoryFixture::META_TEMPLATE_DIRECTORY_ID,
|
||||||
'regex' => null,
|
'regex' => null,
|
||||||
'multiple' => 1,
|
'multiple' => 1,
|
||||||
'enabled' => 1,
|
'enabled' => 1,
|
||||||
|
|
|
@ -44,6 +44,7 @@ class MetaTemplatesFixture extends TestFixture
|
||||||
$this->records = [
|
$this->records = [
|
||||||
[
|
[
|
||||||
'id' => self::ENABLED_TEST_ORG_META_TEMPLATE_ID,
|
'id' => self::ENABLED_TEST_ORG_META_TEMPLATE_ID,
|
||||||
|
'meta_template_directory_id' => MetaTemplateDirectoryFixture::META_TEMPLATE_DIRECTORY_ID,
|
||||||
'scope' => 'organisation',
|
'scope' => 'organisation',
|
||||||
'name' => 'Test Meta Template (enabled)',
|
'name' => 'Test Meta Template (enabled)',
|
||||||
'namespace' => 'cerebrate',
|
'namespace' => 'cerebrate',
|
||||||
|
@ -58,6 +59,7 @@ class MetaTemplatesFixture extends TestFixture
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => self::DISABLED_TEST_ORG_META_TEMPLATE_ID,
|
'id' => self::DISABLED_TEST_ORG_META_TEMPLATE_ID,
|
||||||
|
'meta_template_directory_id' => MetaTemplateDirectoryFixture::META_TEMPLATE_DIRECTORY_ID,
|
||||||
'scope' => 'organisation',
|
'scope' => 'organisation',
|
||||||
'name' => 'Test Meta Template (disabled)',
|
'name' => 'Test Meta Template (disabled)',
|
||||||
'namespace' => 'cerebrate',
|
'namespace' => 'cerebrate',
|
||||||
|
|
|
@ -15,16 +15,18 @@ trait WireMockTestTrait
|
||||||
private $wiremock;
|
private $wiremock;
|
||||||
|
|
||||||
/** @var array<mixed> */
|
/** @var array<mixed> */
|
||||||
private $config = [
|
private $config;
|
||||||
'hostname' => 'localhost',
|
|
||||||
'port' => 8080
|
|
||||||
];
|
|
||||||
|
|
||||||
public function initializeWireMock(): void
|
public function initializeWireMock(): void
|
||||||
{
|
{
|
||||||
|
$this->config = [
|
||||||
|
'hostname' => $_ENV['WIREMOCK_HOST'] ?? 'localhost',
|
||||||
|
'port' => $_ENV['WIREMOCK_PORT'] ?? 8080
|
||||||
|
];
|
||||||
|
|
||||||
$this->wiremock = WireMock::create(
|
$this->wiremock = WireMock::create(
|
||||||
$_ENV['WIREMOCK_HOST'] ?? $this->config['hostname'],
|
$this->config['hostname'],
|
||||||
$_ENV['WIREMOCK_PORT'] ?? $this->config['port']
|
$this->config['port']
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!$this->wiremock->isAlive()) {
|
if (!$this->wiremock->isAlive()) {
|
||||||
|
|
|
@ -59,6 +59,7 @@ $ vendor/bin/phpunit --testsuite=api --testdox
|
||||||
Available suites:
|
Available suites:
|
||||||
* `app`: runs all test suites
|
* `app`: runs all test suites
|
||||||
* `api`: runs only api tests
|
* `api`: runs only api tests
|
||||||
|
* `e2e`: runs only integration tests (requires wiremock running)
|
||||||
* `controller`: runs only controller tests
|
* `controller`: runs only controller tests
|
||||||
* _to be continued ..._
|
* _to be continued ..._
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ class IndexInboxApiTest extends TestCase
|
||||||
$this->get(self::ENDPOINT);
|
$this->get(self::ENDPOINT);
|
||||||
|
|
||||||
$this->assertResponseOk();
|
$this->assertResponseOk();
|
||||||
$this->assertResponseContains(sprintf('"id": %d', InboxFixture::INBOX_USER_REGISTRATION_ID));
|
$this->assertResponseContains(sprintf('"uuid": "%s"', InboxFixture::INBOX_USER_REGISTRATION_UUID));
|
||||||
$this->assertResponseContains(sprintf('"id": %d', InboxFixture::INBOX_INCOMING_CONNECTION_REQUEST_ID));
|
$this->assertResponseContains(sprintf('"uuid": "%s"', InboxFixture::INBOX_INCOMING_CONNECTION_REQUEST_UUID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,20 +31,20 @@ class MispInterConnectionTest extends TestCase
|
||||||
];
|
];
|
||||||
|
|
||||||
/** constants related to the local Cerebrate instance */
|
/** constants related to the local Cerebrate instance */
|
||||||
private const LOCAL_CEREBRATE_URL = 'http://127.0.0.1';
|
private const LOCAL_CEREBRATE_URL = 'http://localhost';
|
||||||
|
|
||||||
/** constants related to the local MISP instance */
|
/** constants related to the local MISP instance */
|
||||||
private const LOCAL_MISP_INSTANCE_URL = 'http://localhost:8080/MISP_LOCAL';
|
private const LOCAL_MISP_INSTANCE_URL = '/MISP_LOCAL';
|
||||||
private const LOCAL_MISP_ADMIN_USER_AUTHKEY = 'b17ce79ac0f05916f382ab06ea4790665dbc174c';
|
private const LOCAL_MISP_ADMIN_USER_AUTHKEY = 'b17ce79ac0f05916f382ab06ea4790665dbc174c';
|
||||||
|
|
||||||
/** constants related to the remote Cerebrate instance */
|
/** constants related to the remote Cerebrate instance */
|
||||||
private const REMOTE_CEREBRATE_URL = 'http://127.0.0.1:8080/CEREBRATE_REMOTE';
|
private const REMOTE_CEREBRATE_URL = '/CEREBRATE_REMOTE';
|
||||||
private const REMOTE_CEREBRATE_AUTHKEY = 'a192ba3c749b545f9cec6b6bba0643736f6c3022';
|
private const REMOTE_CEREBRATE_AUTHKEY = 'a192ba3c749b545f9cec6b6bba0643736f6c3022';
|
||||||
|
|
||||||
/** constants related to the remote MISP instance */
|
/** constants related to the remote MISP instance */
|
||||||
private const REMOTE_MISP_SYNC_USER_ID = 333;
|
private const REMOTE_MISP_SYNC_USER_ID = 333;
|
||||||
private const REMOTE_MISP_SYNC_USER_EMAIL = 'sync@misp.remote';
|
private const REMOTE_MISP_SYNC_USER_EMAIL = 'sync@misp.remote';
|
||||||
private const REMOTE_MISP_INSTANCE_URL = 'http://localhost:8080/MISP_REMOTE';
|
private const REMOTE_MISP_INSTANCE_URL = '/MISP_REMOTE';
|
||||||
private const REMOTE_MISP_AUTHKEY = '19ca57ecebd2fe34c1c17d729980678eb648d541';
|
private const REMOTE_MISP_AUTHKEY = '19ca57ecebd2fe34c1c17d729980678eb648d541';
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class MispInterConnectionTest extends TestCase
|
||||||
'name' => 'MISP_LOCAL',
|
'name' => 'MISP_LOCAL',
|
||||||
'connector' => 'MispConnector',
|
'connector' => 'MispConnector',
|
||||||
'settings' => json_encode([
|
'settings' => json_encode([
|
||||||
'url' => self::LOCAL_MISP_INSTANCE_URL,
|
'url' => $this->getWireMockBaseUrl() . self::LOCAL_MISP_INSTANCE_URL,
|
||||||
'authkey' => self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
'authkey' => self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||||
'skip_ssl' => true,
|
'skip_ssl' => true,
|
||||||
]),
|
]),
|
||||||
|
@ -90,7 +90,7 @@ class MispInterConnectionTest extends TestCase
|
||||||
[
|
[
|
||||||
'uuid' => $LOCAL_BROOD_UUID,
|
'uuid' => $LOCAL_BROOD_UUID,
|
||||||
'name' => 'Local Brood',
|
'name' => 'Local Brood',
|
||||||
'url' => self::REMOTE_CEREBRATE_URL,
|
'url' => $this->getWireMockBaseUrl() . self::REMOTE_CEREBRATE_URL,
|
||||||
'description' => $faker->text,
|
'description' => $faker->text,
|
||||||
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
'trusted' => true,
|
'trusted' => true,
|
||||||
|
@ -228,10 +228,10 @@ class MispInterConnectionTest extends TestCase
|
||||||
[
|
[
|
||||||
'email' => self::REMOTE_MISP_SYNC_USER_EMAIL,
|
'email' => self::REMOTE_MISP_SYNC_USER_EMAIL,
|
||||||
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
||||||
'url' => self::REMOTE_MISP_INSTANCE_URL,
|
'url' => $this->getWireMockBaseUrl() . self::REMOTE_MISP_INSTANCE_URL,
|
||||||
'reflected_user_id' => self::REMOTE_MISP_SYNC_USER_ID,
|
'reflected_user_id' => self::REMOTE_MISP_SYNC_USER_ID,
|
||||||
'connectorName' => 'MispConnector',
|
'connectorName' => 'MispConnector',
|
||||||
'cerebrateURL' => self::REMOTE_CEREBRATE_URL,
|
'cerebrateURL' => $this->getWireMockBaseUrl() . self::REMOTE_CEREBRATE_URL,
|
||||||
'local_tool_id' => 1,
|
'local_tool_id' => 1,
|
||||||
'remote_tool_id' => 1,
|
'remote_tool_id' => 1,
|
||||||
'tool_name' => 'MISP_REMOTE'
|
'tool_name' => 'MISP_REMOTE'
|
||||||
|
@ -250,7 +250,7 @@ class MispInterConnectionTest extends TestCase
|
||||||
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||||
[
|
[
|
||||||
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
||||||
'url' => self::REMOTE_MISP_INSTANCE_URL,
|
'url' => $this->getWireMockBaseUrl() . self::REMOTE_MISP_INSTANCE_URL,
|
||||||
'name' => 'MISP_REMOTE',
|
'name' => 'MISP_REMOTE',
|
||||||
'remote_org_id' => OrganisationsFixture::ORGANISATION_A_ID
|
'remote_org_id' => OrganisationsFixture::ORGANISATION_A_ID
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace App\Test\TestCase\Api\Users;
|
||||||
|
|
||||||
use Cake\TestSuite\TestCase;
|
use Cake\TestSuite\TestCase;
|
||||||
use App\Test\Fixture\AuthKeysFixture;
|
use App\Test\Fixture\AuthKeysFixture;
|
||||||
use App\Test\Fixture\UsersFixture;
|
use App\Test\Fixture\IndividualsFixture;
|
||||||
use App\Test\Fixture\OrganisationsFixture;
|
use App\Test\Fixture\OrganisationsFixture;
|
||||||
use App\Test\Fixture\RolesFixture;
|
use App\Test\Fixture\RolesFixture;
|
||||||
use App\Test\Helper\ApiTestTrait;
|
use App\Test\Helper\ApiTestTrait;
|
||||||
|
@ -31,18 +31,20 @@ class AddUserApiTest extends TestCase
|
||||||
$this->post(
|
$this->post(
|
||||||
self::ENDPOINT,
|
self::ENDPOINT,
|
||||||
[
|
[
|
||||||
'individual_id' => UsersFixture::USER_REGULAR_USER_ID,
|
'individual_id' => IndividualsFixture::INDIVIDUAL_B_ID,
|
||||||
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
'role_id' => RolesFixture::ROLE_REGULAR_USER_ID,
|
'role_id' => RolesFixture::ROLE_REGULAR_USER_ID,
|
||||||
'disabled' => false,
|
'disabled' => false,
|
||||||
'username' => 'test',
|
'username' => 'test123',
|
||||||
'password' => 'Password123456!',
|
'password' => 'Password123456!',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print_r($this->getJsonResponseAsArray());
|
||||||
|
|
||||||
$this->assertResponseOk();
|
$this->assertResponseOk();
|
||||||
$this->assertResponseContains('"username": "test"');
|
$this->assertResponseContains('"username": "test123"');
|
||||||
$this->assertDbRecordExists('Users', ['username' => 'test']);
|
$this->assertDbRecordExists('Users', ['username' => 'test123']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAddUserNotAllowedAsRegularUser(): void
|
public function testAddUserNotAllowedAsRegularUser(): void
|
||||||
|
@ -51,7 +53,7 @@ class AddUserApiTest extends TestCase
|
||||||
$this->post(
|
$this->post(
|
||||||
self::ENDPOINT,
|
self::ENDPOINT,
|
||||||
[
|
[
|
||||||
'individual_id' => UsersFixture::USER_REGULAR_USER_ID,
|
'individual_id' => IndividualsFixture::INDIVIDUAL_B_ID,
|
||||||
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
'role_id' => RolesFixture::ROLE_REGULAR_USER_ID,
|
'role_id' => RolesFixture::ROLE_REGULAR_USER_ID,
|
||||||
'disabled' => false,
|
'disabled' => false,
|
||||||
|
@ -61,6 +63,6 @@ class AddUserApiTest extends TestCase
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertResponseCode(405);
|
$this->assertResponseCode(405);
|
||||||
$this->assertDbRecordNotExists('Users', ['username' => 'test']);
|
$this->assertDbRecordNotExists('Users', ['username' => 'test123']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Test\TestCase\Api\Users;
|
namespace App\Test\TestCase\Api\Users;
|
||||||
|
|
||||||
|
use Cake\Core\Configure;
|
||||||
use Cake\TestSuite\TestCase;
|
use Cake\TestSuite\TestCase;
|
||||||
use App\Test\Fixture\AuthKeysFixture;
|
use App\Test\Fixture\AuthKeysFixture;
|
||||||
use App\Test\Fixture\UsersFixture;
|
use App\Test\Fixture\UsersFixture;
|
||||||
|
@ -25,6 +26,7 @@ class DeleteUserApiTest extends TestCase
|
||||||
|
|
||||||
public function testDeleteUser(): void
|
public function testDeleteUser(): void
|
||||||
{
|
{
|
||||||
|
Configure::write('user.allow-user-deletion', true);
|
||||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
$url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_REGULAR_USER_ID);
|
$url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_REGULAR_USER_ID);
|
||||||
$this->delete($url);
|
$this->delete($url);
|
||||||
|
|
|
@ -40,14 +40,12 @@ class ApplicationTest extends IntegrationTestCase
|
||||||
$app->bootstrap();
|
$app->bootstrap();
|
||||||
$plugins = $app->getPlugins();
|
$plugins = $app->getPlugins();
|
||||||
|
|
||||||
$this->assertCount(7, $plugins);
|
|
||||||
$this->assertSame('Bake', $plugins->get('Bake')->getName());
|
$this->assertSame('Bake', $plugins->get('Bake')->getName());
|
||||||
$this->assertSame('DebugKit', $plugins->get('DebugKit')->getName());
|
$this->assertSame('DebugKit', $plugins->get('DebugKit')->getName());
|
||||||
$this->assertSame('Migrations', $plugins->get('Migrations')->getName());
|
$this->assertSame('Migrations', $plugins->get('Migrations')->getName());
|
||||||
$this->assertSame('Authentication', $plugins->get('Authentication')->getName());
|
$this->assertSame('Authentication', $plugins->get('Authentication')->getName());
|
||||||
$this->assertSame('ADmad/SocialAuth', $plugins->get('ADmad/SocialAuth')->getName());
|
$this->assertSame('ADmad/SocialAuth', $plugins->get('ADmad/SocialAuth')->getName());
|
||||||
$this->assertSame('Tags', $plugins->get('Tags')->getName());
|
$this->assertSame('Tags', $plugins->get('Tags')->getName());
|
||||||
$this->assertSame('Cake/TwigView', $plugins->get('Cake/TwigView')->getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Test\TestCase\Integration\Broods;
|
||||||
|
|
||||||
|
use Cake\TestSuite\TestCase;
|
||||||
|
use App\Test\Fixture\AuthKeysFixture;
|
||||||
|
use App\Test\Fixture\BroodsFixture;
|
||||||
|
use App\Test\Helper\ApiTestTrait;
|
||||||
|
use App\Test\Helper\WireMockTestTrait;
|
||||||
|
use \WireMock\Client\WireMock;
|
||||||
|
|
||||||
|
class TestBroodConnectionApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use ApiTestTrait;
|
||||||
|
use WireMockTestTrait;
|
||||||
|
|
||||||
|
protected const ENDPOINT = '/broods/testConnection';
|
||||||
|
|
||||||
|
protected $fixtures = [
|
||||||
|
'app.Organisations',
|
||||||
|
'app.Individuals',
|
||||||
|
'app.Roles',
|
||||||
|
'app.Users',
|
||||||
|
'app.AuthKeys',
|
||||||
|
'app.Broods'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function testTestBroodConnection(): void
|
||||||
|
{
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->initializeWireMock();
|
||||||
|
$stub = $this->mockCerebrateStatusResponse();
|
||||||
|
|
||||||
|
$url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_WIREMOCK_ID);
|
||||||
|
$this->get($url);
|
||||||
|
|
||||||
|
$this->verifyStubCalled($stub);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertResponseContains('"user": "wiremock"');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockCerebrateStatusResponse(): \WireMock\Stubbing\StubMapping
|
||||||
|
{
|
||||||
|
return $this->getWireMock()->stubFor(
|
||||||
|
WireMock::get(WireMock::urlEqualTo('/instance/status.json'))
|
||||||
|
->willReturn(WireMock::aResponse()
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody((string)json_encode(
|
||||||
|
[
|
||||||
|
"version" => "0.1",
|
||||||
|
"application" => "Cerebrate",
|
||||||
|
"user" => [
|
||||||
|
"id" => 1,
|
||||||
|
"username" => "wiremock",
|
||||||
|
"role" => [
|
||||||
|
"id" => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,405 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Test\TestCase\Integration\LocalTools;
|
||||||
|
|
||||||
|
use Cake\TestSuite\TestCase;
|
||||||
|
use App\Test\Fixture\OrganisationsFixture;
|
||||||
|
use App\Test\Fixture\AuthKeysFixture;
|
||||||
|
use App\Test\Fixture\UsersFixture;
|
||||||
|
use App\Test\Fixture\RolesFixture;
|
||||||
|
use App\Test\Helper\ApiTestTrait;
|
||||||
|
use App\Test\Helper\WireMockTestTrait;
|
||||||
|
use \WireMock\Client\WireMock;
|
||||||
|
|
||||||
|
class MispInterConnectionTest extends TestCase
|
||||||
|
{
|
||||||
|
use ApiTestTrait;
|
||||||
|
use WireMockTestTrait;
|
||||||
|
|
||||||
|
protected $fixtures = [
|
||||||
|
'app.Organisations',
|
||||||
|
'app.Individuals',
|
||||||
|
'app.Roles',
|
||||||
|
'app.Users',
|
||||||
|
'app.AuthKeys',
|
||||||
|
'app.Broods',
|
||||||
|
'app.LocalTools',
|
||||||
|
'app.RemoteToolConnections',
|
||||||
|
'app.Inbox'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** constants related to the local Cerebrate instance */
|
||||||
|
private const LOCAL_CEREBRATE_URL = 'http://localhost';
|
||||||
|
|
||||||
|
/** constants related to the local MISP instance */
|
||||||
|
private const LOCAL_MISP_INSTANCE_URL = '/MISP_LOCAL';
|
||||||
|
private const LOCAL_MISP_ADMIN_USER_AUTHKEY = 'b17ce79ac0f05916f382ab06ea4790665dbc174c';
|
||||||
|
|
||||||
|
/** constants related to the remote Cerebrate instance */
|
||||||
|
private const REMOTE_CEREBRATE_URL = '/CEREBRATE_REMOTE';
|
||||||
|
private const REMOTE_CEREBRATE_AUTHKEY = 'a192ba3c749b545f9cec6b6bba0643736f6c3022';
|
||||||
|
|
||||||
|
/** constants related to the remote MISP instance */
|
||||||
|
private const REMOTE_MISP_SYNC_USER_ID = 333;
|
||||||
|
private const REMOTE_MISP_SYNC_USER_EMAIL = 'sync@misp.remote';
|
||||||
|
private const REMOTE_MISP_INSTANCE_URL = '/MISP_REMOTE';
|
||||||
|
private const REMOTE_MISP_AUTHKEY = '19ca57ecebd2fe34c1c17d729980678eb648d541';
|
||||||
|
|
||||||
|
|
||||||
|
public function testInterConnectMispViaCerebrate(): void
|
||||||
|
{
|
||||||
|
$this->initializeWireMock();
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Create LocalTool connection to `MISP LOCAL` (local MISP instance)
|
||||||
|
*/
|
||||||
|
$this->post(
|
||||||
|
sprintf('%s/localTools/add', self::LOCAL_CEREBRATE_URL),
|
||||||
|
[
|
||||||
|
'name' => 'MISP_LOCAL',
|
||||||
|
'connector' => 'MispConnector',
|
||||||
|
'settings' => json_encode([
|
||||||
|
'url' => $this->getWireMockBaseUrl() . self::LOCAL_MISP_INSTANCE_URL,
|
||||||
|
'authkey' => self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||||
|
'skip_ssl' => true,
|
||||||
|
]),
|
||||||
|
'description' => 'MISP local instance',
|
||||||
|
'exposed' => true
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertDbRecordExists('LocalTools', ['name' => 'MISP_LOCAL']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. Create a new Brood (connect to a remote Cerebrate instance)
|
||||||
|
* This step assumes that the remote Cerebrate instance is already
|
||||||
|
* running and has a user created for the local Cerebrate instance.
|
||||||
|
*
|
||||||
|
* NOTE: Uses OrganisationsFixture::ORGANISATION_A_ID from the
|
||||||
|
* fixtures as the local Organisation.
|
||||||
|
*/
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$LOCAL_BROOD_UUID = $faker->uuid;
|
||||||
|
$this->post(
|
||||||
|
'/broods/add',
|
||||||
|
[
|
||||||
|
'uuid' => $LOCAL_BROOD_UUID,
|
||||||
|
'name' => 'Local Brood',
|
||||||
|
'url' => $this->getWireMockBaseUrl() . self::REMOTE_CEREBRATE_URL,
|
||||||
|
'description' => $faker->text,
|
||||||
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
|
'trusted' => true,
|
||||||
|
'pull' => true,
|
||||||
|
'skip_proxy' => true,
|
||||||
|
'authkey' => self::REMOTE_CEREBRATE_AUTHKEY,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertDbRecordExists('Broods', ['uuid' => $LOCAL_BROOD_UUID]);
|
||||||
|
$brood = $this->getJsonResponseAsArray();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. Create a new Cerebrate local user for the remote Cerebrate
|
||||||
|
* These includes:
|
||||||
|
* - 3.a: Create a new Organisation
|
||||||
|
* - 3.b: Create a new Individual
|
||||||
|
* - 3.c: Create a new User
|
||||||
|
* - 3.d: Create a new Authkey
|
||||||
|
*/
|
||||||
|
// Create Organisation
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$remoteOrgUuid = $faker->uuid;
|
||||||
|
$this->post(
|
||||||
|
'/organisations/add',
|
||||||
|
[
|
||||||
|
'name' => 'Remote Organisation',
|
||||||
|
'description' => $faker->text,
|
||||||
|
'uuid' => $remoteOrgUuid,
|
||||||
|
'url' => 'http://cerebrate.remote',
|
||||||
|
'nationality' => 'US',
|
||||||
|
'sector' => 'sector',
|
||||||
|
'type' => 'type',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertDbRecordExists('Organisations', ['uuid' => $remoteOrgUuid]);
|
||||||
|
$remoteOrg = $this->getJsonResponseAsArray();
|
||||||
|
|
||||||
|
// Create Individual
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->post(
|
||||||
|
'/individuals/add',
|
||||||
|
[
|
||||||
|
'email' => 'sync@cerebrate.remote',
|
||||||
|
'first_name' => 'Remote',
|
||||||
|
'last_name' => 'Cerebrate'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertDbRecordExists('Individuals', ['email' => 'sync@cerebrate.remote']);
|
||||||
|
$remoteIndividual = $this->getJsonResponseAsArray();
|
||||||
|
|
||||||
|
// Create User
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->post(
|
||||||
|
'/users/add',
|
||||||
|
[
|
||||||
|
'individual_id' => $remoteIndividual['id'],
|
||||||
|
'organisation_id' => $remoteOrg['id'],
|
||||||
|
'role_id' => RolesFixture::ROLE_SYNC_ID,
|
||||||
|
'disabled' => false,
|
||||||
|
'username' => 'remote_cerebrate',
|
||||||
|
'password' => 'Password123456!',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertDbRecordExists('Users', ['username' => 'remote_cerebrate']);
|
||||||
|
$user = $this->getJsonResponseAsArray();
|
||||||
|
|
||||||
|
// Create Authkey
|
||||||
|
$remoteCerebrateAuthkey = $faker->sha1;
|
||||||
|
$remoteAuthkeyUuid = $faker->uuid;
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->post(
|
||||||
|
'/authKeys/add',
|
||||||
|
[
|
||||||
|
'uuid' => $remoteAuthkeyUuid,
|
||||||
|
'authkey' => $remoteCerebrateAuthkey,
|
||||||
|
'expiration' => 0,
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'comment' => $faker->text
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertDbRecordExists('AuthKeys', ['uuid' => $remoteAuthkeyUuid]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4. Get remote Cerebrate exposed tools
|
||||||
|
*/
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->mockCerebrateGetExposedToolsResponse('CEREBRATE_REMOTE', self::REMOTE_CEREBRATE_AUTHKEY);
|
||||||
|
$this->get(sprintf('/localTools/broodTools/%s', $brood['id']));
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$tools = $this->getJsonResponseAsArray();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5. Issue a connection request to the remote MISP instance
|
||||||
|
*/
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->mockCerebrateGetExposedToolsResponse('CEREBRATE_REMOTE', self::REMOTE_CEREBRATE_AUTHKEY);
|
||||||
|
$this->mockMispViewOrganisationByUuid(
|
||||||
|
'MISP_LOCAL',
|
||||||
|
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||||
|
OrganisationsFixture::ORGANISATION_A_UUID,
|
||||||
|
OrganisationsFixture::ORGANISATION_A_ID
|
||||||
|
);
|
||||||
|
$this->mockMispCreateSyncUser(
|
||||||
|
'MISP_LOCAL',
|
||||||
|
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||||
|
self::REMOTE_MISP_SYNC_USER_ID,
|
||||||
|
self::REMOTE_MISP_SYNC_USER_EMAIL
|
||||||
|
);
|
||||||
|
$this->mockCerebrateCreateMispIncommingConnectionRequest(
|
||||||
|
'CEREBRATE_REMOTE',
|
||||||
|
UsersFixture::USER_ADMIN_ID,
|
||||||
|
self::LOCAL_CEREBRATE_URL,
|
||||||
|
self::REMOTE_CEREBRATE_AUTHKEY,
|
||||||
|
self::LOCAL_MISP_INSTANCE_URL
|
||||||
|
);
|
||||||
|
$this->post(
|
||||||
|
sprintf('/localTools/connectionRequest/%s/%s', $brood['id'], $tools[0]['id']),
|
||||||
|
[
|
||||||
|
'local_tool_id' => 1
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 6. Remote Cerebrate accepts the connection request
|
||||||
|
*/
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->post(
|
||||||
|
'/inbox/createEntry/LocalTool/AcceptedRequest',
|
||||||
|
[
|
||||||
|
'email' => self::REMOTE_MISP_SYNC_USER_EMAIL,
|
||||||
|
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
||||||
|
'url' => $this->getWireMockBaseUrl() . self::REMOTE_MISP_INSTANCE_URL,
|
||||||
|
'reflected_user_id' => self::REMOTE_MISP_SYNC_USER_ID,
|
||||||
|
'connectorName' => 'MispConnector',
|
||||||
|
'cerebrateURL' => $this->getWireMockBaseUrl() . self::REMOTE_CEREBRATE_URL,
|
||||||
|
'local_tool_id' => 1,
|
||||||
|
'remote_tool_id' => 1,
|
||||||
|
'tool_name' => 'MISP_REMOTE'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$acceptRequest = $this->getJsonResponseAsArray();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 7. Finalize the connection
|
||||||
|
*/
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->mockEnableMispSyncUser('MISP_LOCAL', self::LOCAL_MISP_ADMIN_USER_AUTHKEY, self::REMOTE_MISP_SYNC_USER_ID);
|
||||||
|
$this->mockAddMispServer(
|
||||||
|
'MISP_LOCAL',
|
||||||
|
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||||
|
[
|
||||||
|
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
||||||
|
'url' => $this->getWireMockBaseUrl() . self::REMOTE_MISP_INSTANCE_URL,
|
||||||
|
'name' => 'MISP_REMOTE',
|
||||||
|
'remote_org_id' => OrganisationsFixture::ORGANISATION_A_ID
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->post(sprintf('/inbox/process/%s', $acceptRequest['data']['id']));
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertResponseContains('"success": true');
|
||||||
|
$this->verifyAllStubsCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockCerebrateGetExposedToolsResponse(string $instance, string $cerebrateAuthkey): \WireMock\Stubbing\StubMapping
|
||||||
|
{
|
||||||
|
return $this->getWireMock()->stubFor(
|
||||||
|
WireMock::get(WireMock::urlEqualTo("/$instance/localTools/exposedTools"))
|
||||||
|
->withHeader('Authorization', WireMock::equalTo($cerebrateAuthkey))
|
||||||
|
->willReturn(WireMock::aResponse()
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody((string)json_encode(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"id" => 1,
|
||||||
|
"name" => "MISP ($instance)",
|
||||||
|
"connector" => "MispConnector",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockMispViewOrganisationByUuid(string $instance, string $mispAuthkey, string $orgUuid, int $orgId): \WireMock\Stubbing\StubMapping
|
||||||
|
{
|
||||||
|
return $this->getWireMock()->stubFor(
|
||||||
|
WireMock::get(WireMock::urlEqualTo("/$instance/organisations/view/$orgUuid/limit:50"))
|
||||||
|
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||||
|
->willReturn(WireMock::aResponse()
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody((string)json_encode(
|
||||||
|
[
|
||||||
|
"Organisation" => [
|
||||||
|
"id" => $orgId,
|
||||||
|
"name" => $instance . ' Organisation',
|
||||||
|
"uuid" => $orgUuid,
|
||||||
|
"local" => true
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockMispCreateSyncUser(string $instance, string $mispAuthkey, int $userId, string $email): \WireMock\Stubbing\StubMapping
|
||||||
|
{
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
return $this->getWireMock()->stubFor(
|
||||||
|
WireMock::post(WireMock::urlEqualTo("/$instance/admin/users/add"))
|
||||||
|
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||||
|
->willReturn(WireMock::aResponse()
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody((string)json_encode(
|
||||||
|
[
|
||||||
|
"User" => [
|
||||||
|
"id" => $userId,
|
||||||
|
"email" => $email,
|
||||||
|
"authkey" => $faker->sha1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockCerebrateCreateMispIncommingConnectionRequest(
|
||||||
|
string $instance,
|
||||||
|
int $userId,
|
||||||
|
string $cerebrateUrl,
|
||||||
|
string $cerebrateAuthkey,
|
||||||
|
string $mispUrl
|
||||||
|
): \WireMock\Stubbing\StubMapping {
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
|
||||||
|
return $this->getWireMock()->stubFor(
|
||||||
|
WireMock::post(WireMock::urlEqualTo("/$instance/inbox/createEntry/LocalTool/IncomingConnectionRequest"))
|
||||||
|
->withHeader('Authorization', WireMock::equalTo($cerebrateAuthkey))
|
||||||
|
->willReturn(WireMock::aResponse()
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody((string)json_encode(
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'id' => $faker->randomNumber(),
|
||||||
|
'uuid' => $faker->uuid,
|
||||||
|
'origin' => $cerebrateUrl,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'data' => [
|
||||||
|
'connectorName' => 'MispConnector',
|
||||||
|
'cerebrateURL' => $cerebrateUrl,
|
||||||
|
'url' => $mispUrl,
|
||||||
|
'tool_connector' => 'MispConnector',
|
||||||
|
'local_tool_id' => 1,
|
||||||
|
'remote_tool_id' => 1,
|
||||||
|
],
|
||||||
|
'title' => 'Request for MISP Inter-connection',
|
||||||
|
'scope' => 'LocalTool',
|
||||||
|
'action' => 'IncomingConnectionRequest',
|
||||||
|
'description' => 'Handle Phase I of inter-connection when another cerebrate instance performs the request.',
|
||||||
|
'local_tool_connector_name' => 'MispConnector',
|
||||||
|
'created' => date('c'),
|
||||||
|
'modified' => date('c')
|
||||||
|
],
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'LocalTool request for IncomingConnectionRequest created',
|
||||||
|
'errors' => [],
|
||||||
|
]
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockEnableMispSyncUser(string $instance, string $mispAuthkey, int $userId): \WireMock\Stubbing\StubMapping
|
||||||
|
{
|
||||||
|
return $this->getWireMock()->stubFor(
|
||||||
|
WireMock::post(WireMock::urlEqualTo("/$instance/admin/users/edit/$userId"))
|
||||||
|
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||||
|
->withRequestBody(WireMock::equalToJson(json_encode(['disabled' => false])))
|
||||||
|
->willReturn(WireMock::aResponse()
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody((string)json_encode(
|
||||||
|
[
|
||||||
|
"User" => [
|
||||||
|
"id" => $userId,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockAddMispServer(string $instance, string $mispAuthkey, array $body): \WireMock\Stubbing\StubMapping
|
||||||
|
{
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
|
||||||
|
return $this->getWireMock()->stubFor(
|
||||||
|
WireMock::post(WireMock::urlEqualTo("/$instance/servers/add"))
|
||||||
|
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||||
|
->withRequestBody(WireMock::equalToJson(json_encode($body)))
|
||||||
|
->willReturn(WireMock::aResponse()
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody((string)json_encode(
|
||||||
|
[
|
||||||
|
'Server' => [
|
||||||
|
'id' => $faker->randomNumber()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,9 @@ class AJAXApi {
|
||||||
static genericRequestConfigGETJSON = {
|
static genericRequestConfigGETJSON = {
|
||||||
headers: new Headers(Object.assign({}, AJAXApi.genericRequestHeaders, {Accept: 'application/json'}))
|
headers: new Headers(Object.assign({}, AJAXApi.genericRequestHeaders, {Accept: 'application/json'}))
|
||||||
}
|
}
|
||||||
|
static genericRequestConfigGETCSV = {
|
||||||
|
headers: new Headers(Object.assign({}, AJAXApi.genericRequestHeaders, {Accept: 'text/csv'}))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @namespace
|
* @namespace
|
||||||
|
@ -32,6 +35,7 @@ class AJAXApi {
|
||||||
},
|
},
|
||||||
successToastOptions: {
|
successToastOptions: {
|
||||||
},
|
},
|
||||||
|
fetchOptions: {},
|
||||||
}
|
}
|
||||||
options = {}
|
options = {}
|
||||||
loadingOverlay = false
|
loadingOverlay = false
|
||||||
|
@ -45,6 +49,13 @@ class AJAXApi {
|
||||||
this.mergeOptions(options)
|
this.mergeOptions(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge the configuration object to be used by fetch based on the options passed in the constructor
|
||||||
|
*/
|
||||||
|
mergeFetchConfig(base) {
|
||||||
|
return Object.assign({}, base, this.options.fetchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on the current configuration, provide feedback to the user via toast, console or do not
|
* Based on the current configuration, provide feedback to the user via toast, console or do not
|
||||||
* @param {Object} toastOptions - The options supported by Toaster#defaultOptions
|
* @param {Object} toastOptions - The options supported by Toaster#defaultOptions
|
||||||
|
@ -149,11 +160,25 @@ class AJAXApi {
|
||||||
* @return {Promise<Object>} Promise object resolving to the fetched HTML
|
* @return {Promise<Object>} Promise object resolving to the fetched HTML
|
||||||
*/
|
*/
|
||||||
static async quickFetchJSON(url, options={}) {
|
static async quickFetchJSON(url, options={}) {
|
||||||
const constAlteredOptions = Object.assign({}, {provideFeedback: false}, options)
|
const constAlteredOptions = Object.assign({}, { provideFeedback: false, }, options)
|
||||||
const tmpApi = new AJAXApi(constAlteredOptions)
|
const tmpApi = new AJAXApi(constAlteredOptions)
|
||||||
return tmpApi.fetchJSON(url, constAlteredOptions.skipRequestHooks)
|
return tmpApi.fetchJSON(url, constAlteredOptions.skipRequestHooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} url - The URL to fetch
|
||||||
|
* @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions
|
||||||
|
* @return {Promise<Object>} Promise object resolving to the fetched CSV
|
||||||
|
*/
|
||||||
|
static async quickFetchCSV(url, options={}) {
|
||||||
|
const constAlteredOptions = Object.assign({}, {
|
||||||
|
provideFeedback: false,
|
||||||
|
fetchOptions: AJAXApi.genericRequestConfigGETCSV
|
||||||
|
}, options)
|
||||||
|
const tmpApi = new AJAXApi(constAlteredOptions)
|
||||||
|
return tmpApi.fetchURL(url, constAlteredOptions.skipRequestHooks)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} url - The URL to fetch
|
* @param {string} url - The URL to fetch
|
||||||
* @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions
|
* @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions
|
||||||
|
@ -213,7 +238,7 @@ class AJAXApi {
|
||||||
}
|
}
|
||||||
let toReturn
|
let toReturn
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, AJAXApi.genericRequestConfigGET);
|
const response = await fetch(url, this.mergeFetchConfig(AJAXApi.genericRequestConfigGET));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Network response was not ok. \`${response.statusText}\``)
|
throw new Error(`Network response was not ok. \`${response.statusText}\``)
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,21 +206,34 @@ function deleteBookmark(bookmark, forSidebar=false) {
|
||||||
}).catch((e) => { })
|
}).catch((e) => { })
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadIndexTable(downloadButton, filename) {
|
function downloadIndexTable(downloadButton, filename, filtered) {
|
||||||
const $dropdownMenu = $(downloadButton).closest('.dropdown')
|
const $dropdownMenu = $(downloadButton).closest('.dropdown')
|
||||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||||
const $filterButton = $(`#toggleFilterButton-${tableRandomValue}`)
|
const $filterButton = $(`#toggleFilterButton-${tableRandomValue}`)
|
||||||
const activeFilters = $filterButton.data('activeFilters')
|
const activeFilters = $filterButton.data('activeFilters')
|
||||||
const additionalUrlParams = $filterButton.data('additionalUrlParams') ? $filterButton.data('additionalUrlParams') : ''
|
// const additionalUrlParams = $filterButton.data('additionalUrlParams') ? $filterButton.data('additionalUrlParams') : ''
|
||||||
const searchParam = jQuery.param(activeFilters);
|
// const searchParam = jQuery.param(activeFilters);
|
||||||
const url = $table.data('reload-url') + additionalUrlParams + '?' + searchParam
|
// const url = $table.data('reload-url') + additionalUrlParams + '?' + searchParam
|
||||||
|
let url = $table.data('reload-url')
|
||||||
|
if (filtered) {
|
||||||
|
url = activeFilters._here
|
||||||
|
}
|
||||||
let options = {}
|
let options = {}
|
||||||
const downloadPromise = AJAXApi.quickFetchJSON(url, options)
|
let downloadPromise;
|
||||||
|
if (filename.endsWith('.csv')) {
|
||||||
|
downloadPromise = AJAXApi.quickFetchCSV(url, options)
|
||||||
|
} else {
|
||||||
|
downloadPromise = AJAXApi.quickFetchJSON(url, options)
|
||||||
|
}
|
||||||
UI.overlayUntilResolve($dropdownMenu, downloadPromise)
|
UI.overlayUntilResolve($dropdownMenu, downloadPromise)
|
||||||
downloadPromise.then((data) => {
|
downloadPromise.then((data) => {
|
||||||
|
if (filename.endsWith('.csv')) {
|
||||||
|
download(filename, data)
|
||||||
|
} else {
|
||||||
download(filename, JSON.stringify(data, undefined, 4))
|
download(filename, JSON.stringify(data, undefined, 4))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { DetailedError } from './utils.js';
|
||||||
|
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
|
||||||
|
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
|
||||||
|
/**
|
||||||
|
* An object representing a parsed mermaid diagram definition.
|
||||||
|
* @privateRemarks This is exported as part of the public mermaidAPI.
|
||||||
|
*/
|
||||||
|
export declare class Diagram {
|
||||||
|
text: string;
|
||||||
|
metadata: Pick<DiagramMetadata, 'title'>;
|
||||||
|
type: string;
|
||||||
|
parser: DiagramDefinition['parser'];
|
||||||
|
renderer: DiagramDefinition['renderer'];
|
||||||
|
db: DiagramDefinition['db'];
|
||||||
|
private init?;
|
||||||
|
private detectError?;
|
||||||
|
constructor(text: string, metadata?: Pick<DiagramMetadata, 'title'>);
|
||||||
|
parse(): void;
|
||||||
|
render(id: string, version: string): Promise<void>;
|
||||||
|
getParser(): import("./diagram-api/types.js").ParserDefinition;
|
||||||
|
getType(): string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse the text asynchronously and generate a Diagram object asynchronously.
|
||||||
|
* **Warning:** This function may be changed in the future.
|
||||||
|
* @alpha
|
||||||
|
* @param text - The mermaid diagram definition.
|
||||||
|
* @param metadata - Diagram metadata, defined in YAML.
|
||||||
|
* @returns A the Promise of a Diagram object.
|
||||||
|
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
|
||||||
|
* @privateRemarks This is exported as part of the public mermaidAPI.
|
||||||
|
*/
|
||||||
|
export declare const getDiagramFromText: (text: string, metadata?: Pick<DiagramMetadata, 'title'>) => Promise<Diagram>;
|
|
@ -0,0 +1,13 @@
|
||||||
|
export declare const mermaidAPI: {
|
||||||
|
render: import("@vitest/spy").Mock<any, any>;
|
||||||
|
parse: (text: string, parseOptions?: import("../mermaidAPI.js").ParseOptions | undefined) => Promise<boolean>;
|
||||||
|
initialize: import("@vitest/spy").Mock<any, any>;
|
||||||
|
getConfig: () => import("../config.type.js").MermaidConfig;
|
||||||
|
setConfig: (conf: import("../config.type.js").MermaidConfig) => import("../config.type.js").MermaidConfig;
|
||||||
|
getSiteConfig: () => import("../config.type.js").MermaidConfig;
|
||||||
|
updateSiteConfig: (conf: import("../config.type.js").MermaidConfig) => import("../config.type.js").MermaidConfig;
|
||||||
|
reset: () => void;
|
||||||
|
globalReset: () => void;
|
||||||
|
defaultConfig: import("../config.type.js").MermaidConfig;
|
||||||
|
};
|
||||||
|
export default mermaidAPI;
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Accessibility (a11y) functions, types, helpers.
|
||||||
|
*
|
||||||
|
* @see https://www.w3.org/WAI/
|
||||||
|
* @see https://www.w3.org/TR/wai-aria-1.1/
|
||||||
|
* @see https://www.w3.org/TR/svg-aam-1.0/
|
||||||
|
*/
|
||||||
|
import type { D3Element } from './mermaidAPI.js';
|
||||||
|
/**
|
||||||
|
* Add role and aria-roledescription to the svg element.
|
||||||
|
*
|
||||||
|
* @param svg - d3 object that contains the SVG HTML element
|
||||||
|
* @param diagramType - diagram name for to the aria-roledescription
|
||||||
|
*/
|
||||||
|
export declare function setA11yDiagramInfo(svg: D3Element, diagramType: string): void;
|
||||||
|
/**
|
||||||
|
* Add an accessible title and/or description element to a chart.
|
||||||
|
* The title is usually not displayed and the description is never displayed.
|
||||||
|
*
|
||||||
|
* The following charts display their title as a visual and accessibility element: gantt.
|
||||||
|
*
|
||||||
|
* @param svg - d3 node to insert the a11y title and desc info
|
||||||
|
* @param a11yTitle - a11y title. undefined or empty strings mean to skip them
|
||||||
|
* @param a11yDesc - a11y description. undefined or empty strings mean to skip them
|
||||||
|
* @param baseId - id used to construct the a11y title and description id
|
||||||
|
*/
|
||||||
|
export declare function addSVGa11yTitleDescription(svg: D3Element, a11yTitle: string | undefined, a11yDesc: string | undefined, baseId: string): void;
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { c as constant, p as path } from "./constant-b644328d.js";
|
||||||
|
import { aW as pi, aX as cos, aY as sin, aZ as halfPi, a_ as epsilon, W as tau, a$ as sqrt, b0 as min, b1 as abs, b2 as atan2, b3 as asin, b4 as acos, b5 as max } from "./mermaid-491db2d9.js";
|
||||||
|
function arcInnerRadius(d) {
|
||||||
|
return d.innerRadius;
|
||||||
|
}
|
||||||
|
function arcOuterRadius(d) {
|
||||||
|
return d.outerRadius;
|
||||||
|
}
|
||||||
|
function arcStartAngle(d) {
|
||||||
|
return d.startAngle;
|
||||||
|
}
|
||||||
|
function arcEndAngle(d) {
|
||||||
|
return d.endAngle;
|
||||||
|
}
|
||||||
|
function arcPadAngle(d) {
|
||||||
|
return d && d.padAngle;
|
||||||
|
}
|
||||||
|
function intersect(x0, y0, x1, y1, x2, y2, x3, y3) {
|
||||||
|
var x10 = x1 - x0, y10 = y1 - y0, x32 = x3 - x2, y32 = y3 - y2, t = y32 * x10 - x32 * y10;
|
||||||
|
if (t * t < epsilon)
|
||||||
|
return;
|
||||||
|
t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
|
||||||
|
return [x0 + t * x10, y0 + t * y10];
|
||||||
|
}
|
||||||
|
function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
|
||||||
|
var x01 = x0 - x1, y01 = y0 - y1, lo = (cw ? rc : -rc) / sqrt(x01 * x01 + y01 * y01), ox = lo * y01, oy = -lo * x01, x11 = x0 + ox, y11 = y0 + oy, x10 = x1 + ox, y10 = y1 + oy, x00 = (x11 + x10) / 2, y00 = (y11 + y10) / 2, dx = x10 - x11, dy = y10 - y11, d2 = dx * dx + dy * dy, r = r1 - rc, D = x11 * y10 - x10 * y11, d = (dy < 0 ? -1 : 1) * sqrt(max(0, r * r * d2 - D * D)), cx0 = (D * dy - dx * d) / d2, cy0 = (-D * dx - dy * d) / d2, cx1 = (D * dy + dx * d) / d2, cy1 = (-D * dx + dy * d) / d2, dx0 = cx0 - x00, dy0 = cy0 - y00, dx1 = cx1 - x00, dy1 = cy1 - y00;
|
||||||
|
if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1)
|
||||||
|
cx0 = cx1, cy0 = cy1;
|
||||||
|
return {
|
||||||
|
cx: cx0,
|
||||||
|
cy: cy0,
|
||||||
|
x01: -ox,
|
||||||
|
y01: -oy,
|
||||||
|
x11: cx0 * (r1 / r - 1),
|
||||||
|
y11: cy0 * (r1 / r - 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function d3arc() {
|
||||||
|
var innerRadius = arcInnerRadius, outerRadius = arcOuterRadius, cornerRadius = constant(0), padRadius = null, startAngle = arcStartAngle, endAngle = arcEndAngle, padAngle = arcPadAngle, context = null;
|
||||||
|
function arc() {
|
||||||
|
var buffer, r, r0 = +innerRadius.apply(this, arguments), r1 = +outerRadius.apply(this, arguments), a0 = startAngle.apply(this, arguments) - halfPi, a1 = endAngle.apply(this, arguments) - halfPi, da = abs(a1 - a0), cw = a1 > a0;
|
||||||
|
if (!context)
|
||||||
|
context = buffer = path();
|
||||||
|
if (r1 < r0)
|
||||||
|
r = r1, r1 = r0, r0 = r;
|
||||||
|
if (!(r1 > epsilon))
|
||||||
|
context.moveTo(0, 0);
|
||||||
|
else if (da > tau - epsilon) {
|
||||||
|
context.moveTo(r1 * cos(a0), r1 * sin(a0));
|
||||||
|
context.arc(0, 0, r1, a0, a1, !cw);
|
||||||
|
if (r0 > epsilon) {
|
||||||
|
context.moveTo(r0 * cos(a1), r0 * sin(a1));
|
||||||
|
context.arc(0, 0, r0, a1, a0, cw);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var a01 = a0, a11 = a1, a00 = a0, a10 = a1, da0 = da, da1 = da, ap = padAngle.apply(this, arguments) / 2, rp = ap > epsilon && (padRadius ? +padRadius.apply(this, arguments) : sqrt(r0 * r0 + r1 * r1)), rc = min(abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)), rc0 = rc, rc1 = rc, t0, t1;
|
||||||
|
if (rp > epsilon) {
|
||||||
|
var p0 = asin(rp / r0 * sin(ap)), p1 = asin(rp / r1 * sin(ap));
|
||||||
|
if ((da0 -= p0 * 2) > epsilon)
|
||||||
|
p0 *= cw ? 1 : -1, a00 += p0, a10 -= p0;
|
||||||
|
else
|
||||||
|
da0 = 0, a00 = a10 = (a0 + a1) / 2;
|
||||||
|
if ((da1 -= p1 * 2) > epsilon)
|
||||||
|
p1 *= cw ? 1 : -1, a01 += p1, a11 -= p1;
|
||||||
|
else
|
||||||
|
da1 = 0, a01 = a11 = (a0 + a1) / 2;
|
||||||
|
}
|
||||||
|
var x01 = r1 * cos(a01), y01 = r1 * sin(a01), x10 = r0 * cos(a10), y10 = r0 * sin(a10);
|
||||||
|
if (rc > epsilon) {
|
||||||
|
var x11 = r1 * cos(a11), y11 = r1 * sin(a11), x00 = r0 * cos(a00), y00 = r0 * sin(a00), oc;
|
||||||
|
if (da < pi && (oc = intersect(x01, y01, x00, y00, x11, y11, x10, y10))) {
|
||||||
|
var ax = x01 - oc[0], ay = y01 - oc[1], bx = x11 - oc[0], by = y11 - oc[1], kc = 1 / sin(acos((ax * bx + ay * by) / (sqrt(ax * ax + ay * ay) * sqrt(bx * bx + by * by))) / 2), lc = sqrt(oc[0] * oc[0] + oc[1] * oc[1]);
|
||||||
|
rc0 = min(rc, (r0 - lc) / (kc - 1));
|
||||||
|
rc1 = min(rc, (r1 - lc) / (kc + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(da1 > epsilon))
|
||||||
|
context.moveTo(x01, y01);
|
||||||
|
else if (rc1 > epsilon) {
|
||||||
|
t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
|
||||||
|
t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);
|
||||||
|
context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);
|
||||||
|
if (rc1 < rc)
|
||||||
|
context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
|
||||||
|
else {
|
||||||
|
context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
|
||||||
|
context.arc(0, 0, r1, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
|
||||||
|
context.arc(t1.cx, t1.cy, rc1, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);
|
||||||
|
if (!(r0 > epsilon) || !(da0 > epsilon))
|
||||||
|
context.lineTo(x10, y10);
|
||||||
|
else if (rc0 > epsilon) {
|
||||||
|
t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
|
||||||
|
t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);
|
||||||
|
context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);
|
||||||
|
if (rc0 < rc)
|
||||||
|
context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
|
||||||
|
else {
|
||||||
|
context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
|
||||||
|
context.arc(0, 0, r0, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
|
||||||
|
context.arc(t1.cx, t1.cy, rc0, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
context.arc(0, 0, r0, a10, a00, cw);
|
||||||
|
}
|
||||||
|
context.closePath();
|
||||||
|
if (buffer)
|
||||||
|
return context = null, buffer + "" || null;
|
||||||
|
}
|
||||||
|
arc.centroid = function() {
|
||||||
|
var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2, a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - pi / 2;
|
||||||
|
return [cos(a) * r, sin(a) * r];
|
||||||
|
};
|
||||||
|
arc.innerRadius = function(_) {
|
||||||
|
return arguments.length ? (innerRadius = typeof _ === "function" ? _ : constant(+_), arc) : innerRadius;
|
||||||
|
};
|
||||||
|
arc.outerRadius = function(_) {
|
||||||
|
return arguments.length ? (outerRadius = typeof _ === "function" ? _ : constant(+_), arc) : outerRadius;
|
||||||
|
};
|
||||||
|
arc.cornerRadius = function(_) {
|
||||||
|
return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : constant(+_), arc) : cornerRadius;
|
||||||
|
};
|
||||||
|
arc.padRadius = function(_) {
|
||||||
|
return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : constant(+_), arc) : padRadius;
|
||||||
|
};
|
||||||
|
arc.startAngle = function(_) {
|
||||||
|
return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant(+_), arc) : startAngle;
|
||||||
|
};
|
||||||
|
arc.endAngle = function(_) {
|
||||||
|
return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), arc) : endAngle;
|
||||||
|
};
|
||||||
|
arc.padAngle = function(_) {
|
||||||
|
return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant(+_), arc) : padAngle;
|
||||||
|
};
|
||||||
|
arc.context = function(_) {
|
||||||
|
return arguments.length ? (context = _ == null ? null : _, arc) : context;
|
||||||
|
};
|
||||||
|
return arc;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
d3arc as d
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { c as X, p as sn } from "./constant-2fe7eae5.js";
|
||||||
|
import { aW as en, aX as j, aY as P, aZ as an, a_ as y, W as ln, a$ as C, b0 as k, b1 as rn, b2 as t, b3 as un, b4 as on, b5 as tn } from "./mermaid-e4a58915.js";
|
||||||
|
function fn(l) {
|
||||||
|
return l.innerRadius;
|
||||||
|
}
|
||||||
|
function cn(l) {
|
||||||
|
return l.outerRadius;
|
||||||
|
}
|
||||||
|
function yn(l) {
|
||||||
|
return l.startAngle;
|
||||||
|
}
|
||||||
|
function gn(l) {
|
||||||
|
return l.endAngle;
|
||||||
|
}
|
||||||
|
function mn(l) {
|
||||||
|
return l && l.padAngle;
|
||||||
|
}
|
||||||
|
function pn(l, x, w, W, h, v, Y, a) {
|
||||||
|
var s = w - l, n = W - x, m = Y - h, i = a - v, r = i * s - m * n;
|
||||||
|
if (!(r * r < y))
|
||||||
|
return r = (m * (x - v) - i * (l - h)) / r, [l + r * s, x + r * n];
|
||||||
|
}
|
||||||
|
function K(l, x, w, W, h, v, Y) {
|
||||||
|
var a = l - w, s = x - W, n = (Y ? v : -v) / C(a * a + s * s), m = n * s, i = -n * a, r = l + m, f = x + i, c = w + m, D = W + i, o = (r + c) / 2, E = (f + D) / 2, p = c - r, g = D - f, A = p * p + g * g, I = h - v, b = r * D - c * f, O = (g < 0 ? -1 : 1) * C(tn(0, I * I * A - b * b)), S = (b * g - p * O) / A, d = (-b * p - g * O) / A, R = (b * g + p * O) / A, T = (-b * p + g * O) / A, e = S - o, u = d - E, Z = R - o, $ = T - E;
|
||||||
|
return e * e + u * u > Z * Z + $ * $ && (S = R, d = T), {
|
||||||
|
cx: S,
|
||||||
|
cy: d,
|
||||||
|
x01: -m,
|
||||||
|
y01: -i,
|
||||||
|
x11: S * (h / I - 1),
|
||||||
|
y11: d * (h / I - 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function hn() {
|
||||||
|
var l = fn, x = cn, w = X(0), W = null, h = yn, v = gn, Y = mn, a = null;
|
||||||
|
function s() {
|
||||||
|
var n, m, i = +l.apply(this, arguments), r = +x.apply(this, arguments), f = h.apply(this, arguments) - an, c = v.apply(this, arguments) - an, D = rn(c - f), o = c > f;
|
||||||
|
if (a || (a = n = sn()), r < i && (m = r, r = i, i = m), !(r > y))
|
||||||
|
a.moveTo(0, 0);
|
||||||
|
else if (D > ln - y)
|
||||||
|
a.moveTo(r * j(f), r * P(f)), a.arc(0, 0, r, f, c, !o), i > y && (a.moveTo(i * j(c), i * P(c)), a.arc(0, 0, i, c, f, o));
|
||||||
|
else {
|
||||||
|
var E = f, p = c, g = f, A = c, I = D, b = D, O = Y.apply(this, arguments) / 2, S = O > y && (W ? +W.apply(this, arguments) : C(i * i + r * r)), d = k(rn(r - i) / 2, +w.apply(this, arguments)), R = d, T = d, e, u;
|
||||||
|
if (S > y) {
|
||||||
|
var Z = un(S / i * P(O)), $ = un(S / r * P(O));
|
||||||
|
(I -= Z * 2) > y ? (Z *= o ? 1 : -1, g += Z, A -= Z) : (I = 0, g = A = (f + c) / 2), (b -= $ * 2) > y ? ($ *= o ? 1 : -1, E += $, p -= $) : (b = 0, E = p = (f + c) / 2);
|
||||||
|
}
|
||||||
|
var z = r * j(E), B = r * P(E), F = i * j(A), G = i * P(A);
|
||||||
|
if (d > y) {
|
||||||
|
var H = r * j(p), J = r * P(p), L = i * j(g), M = i * P(g), q;
|
||||||
|
if (D < en && (q = pn(z, B, L, M, H, J, F, G))) {
|
||||||
|
var N = z - q[0], Q = B - q[1], U = H - q[0], V = J - q[1], _ = 1 / P(on((N * U + Q * V) / (C(N * N + Q * Q) * C(U * U + V * V))) / 2), nn = C(q[0] * q[0] + q[1] * q[1]);
|
||||||
|
R = k(d, (i - nn) / (_ - 1)), T = k(d, (r - nn) / (_ + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b > y ? T > y ? (e = K(L, M, z, B, r, T, o), u = K(H, J, F, G, r, T, o), a.moveTo(e.cx + e.x01, e.cy + e.y01), T < d ? a.arc(e.cx, e.cy, T, t(e.y01, e.x01), t(u.y01, u.x01), !o) : (a.arc(e.cx, e.cy, T, t(e.y01, e.x01), t(e.y11, e.x11), !o), a.arc(0, 0, r, t(e.cy + e.y11, e.cx + e.x11), t(u.cy + u.y11, u.cx + u.x11), !o), a.arc(u.cx, u.cy, T, t(u.y11, u.x11), t(u.y01, u.x01), !o))) : (a.moveTo(z, B), a.arc(0, 0, r, E, p, !o)) : a.moveTo(z, B), !(i > y) || !(I > y) ? a.lineTo(F, G) : R > y ? (e = K(F, G, H, J, i, -R, o), u = K(z, B, L, M, i, -R, o), a.lineTo(e.cx + e.x01, e.cy + e.y01), R < d ? a.arc(e.cx, e.cy, R, t(e.y01, e.x01), t(u.y01, u.x01), !o) : (a.arc(e.cx, e.cy, R, t(e.y01, e.x01), t(e.y11, e.x11), !o), a.arc(0, 0, i, t(e.cy + e.y11, e.cx + e.x11), t(u.cy + u.y11, u.cx + u.x11), o), a.arc(u.cx, u.cy, R, t(u.y11, u.x11), t(u.y01, u.x01), !o))) : a.arc(0, 0, i, A, g, o);
|
||||||
|
}
|
||||||
|
if (a.closePath(), n)
|
||||||
|
return a = null, n + "" || null;
|
||||||
|
}
|
||||||
|
return s.centroid = function() {
|
||||||
|
var n = (+l.apply(this, arguments) + +x.apply(this, arguments)) / 2, m = (+h.apply(this, arguments) + +v.apply(this, arguments)) / 2 - en / 2;
|
||||||
|
return [j(m) * n, P(m) * n];
|
||||||
|
}, s.innerRadius = function(n) {
|
||||||
|
return arguments.length ? (l = typeof n == "function" ? n : X(+n), s) : l;
|
||||||
|
}, s.outerRadius = function(n) {
|
||||||
|
return arguments.length ? (x = typeof n == "function" ? n : X(+n), s) : x;
|
||||||
|
}, s.cornerRadius = function(n) {
|
||||||
|
return arguments.length ? (w = typeof n == "function" ? n : X(+n), s) : w;
|
||||||
|
}, s.padRadius = function(n) {
|
||||||
|
return arguments.length ? (W = n == null ? null : typeof n == "function" ? n : X(+n), s) : W;
|
||||||
|
}, s.startAngle = function(n) {
|
||||||
|
return arguments.length ? (h = typeof n == "function" ? n : X(+n), s) : h;
|
||||||
|
}, s.endAngle = function(n) {
|
||||||
|
return arguments.length ? (v = typeof n == "function" ? n : X(+n), s) : v;
|
||||||
|
}, s.padAngle = function(n) {
|
||||||
|
return arguments.length ? (Y = typeof n == "function" ? n : X(+n), s) : Y;
|
||||||
|
}, s.context = function(n) {
|
||||||
|
return arguments.length ? (a = n ?? null, s) : a;
|
||||||
|
}, s;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
hn as d
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
function t(r) {
|
||||||
|
return typeof r == "object" && "length" in r ? r : Array.from(r);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
t as a
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
function array(x) {
|
||||||
|
return typeof x === "object" && "length" in x ? x : Array.from(x);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
array as a
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* assignWithDepth Extends the functionality of {@link Object.assign} with the
|
||||||
|
* ability to merge arbitrary-depth objects For each key in src with path `k` (recursively)
|
||||||
|
* performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of
|
||||||
|
* undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and
|
||||||
|
* effectively merged with src[`k`]<p> Additionally, dissimilar types will not clobber unless the
|
||||||
|
* config.clobber parameter === true. Example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* const config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
|
||||||
|
* const config_1 = { foo: 'foo', bar: 'bar' };
|
||||||
|
* const result = assignWithDepth(config_0, config_1);
|
||||||
|
* console.log(result);
|
||||||
|
* //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Traditional Object.assign would have clobbered foo in config_0 with foo in config_1. If src is a
|
||||||
|
* destructured array of objects and dst is not an array, assignWithDepth will apply each element
|
||||||
|
* of src to dst in order.
|
||||||
|
* @param dst - The destination of the merge
|
||||||
|
* @param src - The source object(s) to merge into destination
|
||||||
|
* @param config -
|
||||||
|
* * depth: depth to traverse within src and dst for merging
|
||||||
|
* * clobber: should dissimilar types clobber
|
||||||
|
*/
|
||||||
|
declare const assignWithDepth: (dst: any, src: any, { depth, clobber }?: {
|
||||||
|
depth?: number | undefined;
|
||||||
|
clobber?: boolean | undefined;
|
||||||
|
}) => any;
|
||||||
|
export default assignWithDepth;
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,362 @@
|
||||||
|
import { p as parser, d as db, s as styles } from "./styles-8b67d7cb.js";
|
||||||
|
import { line, curveBasis, select } from "d3";
|
||||||
|
import { layout } from "dagre-d3-es/src/dagre/index.js";
|
||||||
|
import * as graphlib from "dagre-d3-es/src/graphlib/index.js";
|
||||||
|
import { u as utils, l as log, v as parseGenericTypes, c as getConfig, i as configureSvgSize } from "./mermaid-0d192ec3.js";
|
||||||
|
import "ts-dedent";
|
||||||
|
import "dayjs";
|
||||||
|
import "@braintree/sanitize-url";
|
||||||
|
import "dompurify";
|
||||||
|
import "khroma";
|
||||||
|
import "lodash-es/memoize.js";
|
||||||
|
import "lodash-es/merge.js";
|
||||||
|
import "stylis";
|
||||||
|
import "lodash-es/isEmpty.js";
|
||||||
|
let edgeCount = 0;
|
||||||
|
const drawEdge = function(elem, path, relation, conf, diagObj) {
|
||||||
|
const getRelationType = function(type) {
|
||||||
|
switch (type) {
|
||||||
|
case diagObj.db.relationType.AGGREGATION:
|
||||||
|
return "aggregation";
|
||||||
|
case diagObj.db.relationType.EXTENSION:
|
||||||
|
return "extension";
|
||||||
|
case diagObj.db.relationType.COMPOSITION:
|
||||||
|
return "composition";
|
||||||
|
case diagObj.db.relationType.DEPENDENCY:
|
||||||
|
return "dependency";
|
||||||
|
case diagObj.db.relationType.LOLLIPOP:
|
||||||
|
return "lollipop";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
path.points = path.points.filter((p) => !Number.isNaN(p.y));
|
||||||
|
const lineData = path.points;
|
||||||
|
const lineFunction = line().x(function(d) {
|
||||||
|
return d.x;
|
||||||
|
}).y(function(d) {
|
||||||
|
return d.y;
|
||||||
|
}).curve(curveBasis);
|
||||||
|
const svgPath = elem.append("path").attr("d", lineFunction(lineData)).attr("id", "edge" + edgeCount).attr("class", "relation");
|
||||||
|
let url = "";
|
||||||
|
if (conf.arrowMarkerAbsolute) {
|
||||||
|
url = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search;
|
||||||
|
url = url.replace(/\(/g, "\\(");
|
||||||
|
url = url.replace(/\)/g, "\\)");
|
||||||
|
}
|
||||||
|
if (relation.relation.lineType == 1) {
|
||||||
|
svgPath.attr("class", "relation dashed-line");
|
||||||
|
}
|
||||||
|
if (relation.relation.lineType == 10) {
|
||||||
|
svgPath.attr("class", "relation dotted-line");
|
||||||
|
}
|
||||||
|
if (relation.relation.type1 !== "none") {
|
||||||
|
svgPath.attr(
|
||||||
|
"marker-start",
|
||||||
|
"url(" + url + "#" + getRelationType(relation.relation.type1) + "Start)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (relation.relation.type2 !== "none") {
|
||||||
|
svgPath.attr(
|
||||||
|
"marker-end",
|
||||||
|
"url(" + url + "#" + getRelationType(relation.relation.type2) + "End)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let x, y;
|
||||||
|
const l = path.points.length;
|
||||||
|
let labelPosition = utils.calcLabelPosition(path.points);
|
||||||
|
x = labelPosition.x;
|
||||||
|
y = labelPosition.y;
|
||||||
|
let p1_card_x, p1_card_y;
|
||||||
|
let p2_card_x, p2_card_y;
|
||||||
|
if (l % 2 !== 0 && l > 1) {
|
||||||
|
let cardinality_1_point = utils.calcCardinalityPosition(
|
||||||
|
relation.relation.type1 !== "none",
|
||||||
|
path.points,
|
||||||
|
path.points[0]
|
||||||
|
);
|
||||||
|
let cardinality_2_point = utils.calcCardinalityPosition(
|
||||||
|
relation.relation.type2 !== "none",
|
||||||
|
path.points,
|
||||||
|
path.points[l - 1]
|
||||||
|
);
|
||||||
|
log.debug("cardinality_1_point " + JSON.stringify(cardinality_1_point));
|
||||||
|
log.debug("cardinality_2_point " + JSON.stringify(cardinality_2_point));
|
||||||
|
p1_card_x = cardinality_1_point.x;
|
||||||
|
p1_card_y = cardinality_1_point.y;
|
||||||
|
p2_card_x = cardinality_2_point.x;
|
||||||
|
p2_card_y = cardinality_2_point.y;
|
||||||
|
}
|
||||||
|
if (relation.title !== void 0) {
|
||||||
|
const g = elem.append("g").attr("class", "classLabel");
|
||||||
|
const label = g.append("text").attr("class", "label").attr("x", x).attr("y", y).attr("fill", "red").attr("text-anchor", "middle").text(relation.title);
|
||||||
|
window.label = label;
|
||||||
|
const bounds = label.node().getBBox();
|
||||||
|
g.insert("rect", ":first-child").attr("class", "box").attr("x", bounds.x - conf.padding / 2).attr("y", bounds.y - conf.padding / 2).attr("width", bounds.width + conf.padding).attr("height", bounds.height + conf.padding);
|
||||||
|
}
|
||||||
|
log.info("Rendering relation " + JSON.stringify(relation));
|
||||||
|
if (relation.relationTitle1 !== void 0 && relation.relationTitle1 !== "none") {
|
||||||
|
const g = elem.append("g").attr("class", "cardinality");
|
||||||
|
g.append("text").attr("class", "type1").attr("x", p1_card_x).attr("y", p1_card_y).attr("fill", "black").attr("font-size", "6").text(relation.relationTitle1);
|
||||||
|
}
|
||||||
|
if (relation.relationTitle2 !== void 0 && relation.relationTitle2 !== "none") {
|
||||||
|
const g = elem.append("g").attr("class", "cardinality");
|
||||||
|
g.append("text").attr("class", "type2").attr("x", p2_card_x).attr("y", p2_card_y).attr("fill", "black").attr("font-size", "6").text(relation.relationTitle2);
|
||||||
|
}
|
||||||
|
edgeCount++;
|
||||||
|
};
|
||||||
|
const drawClass = function(elem, classDef, conf, diagObj) {
|
||||||
|
log.debug("Rendering class ", classDef, conf);
|
||||||
|
const id = classDef.id;
|
||||||
|
const classInfo = {
|
||||||
|
id,
|
||||||
|
label: classDef.id,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
const g = elem.append("g").attr("id", diagObj.db.lookUpDomId(id)).attr("class", "classGroup");
|
||||||
|
let title;
|
||||||
|
if (classDef.link) {
|
||||||
|
title = g.append("svg:a").attr("xlink:href", classDef.link).attr("target", classDef.linkTarget).append("text").attr("y", conf.textHeight + conf.padding).attr("x", 0);
|
||||||
|
} else {
|
||||||
|
title = g.append("text").attr("y", conf.textHeight + conf.padding).attr("x", 0);
|
||||||
|
}
|
||||||
|
let isFirst = true;
|
||||||
|
classDef.annotations.forEach(function(member) {
|
||||||
|
const titleText2 = title.append("tspan").text("«" + member + "»");
|
||||||
|
if (!isFirst) {
|
||||||
|
titleText2.attr("dy", conf.textHeight);
|
||||||
|
}
|
||||||
|
isFirst = false;
|
||||||
|
});
|
||||||
|
let classTitleString = getClassTitleString(classDef);
|
||||||
|
const classTitle = title.append("tspan").text(classTitleString).attr("class", "title");
|
||||||
|
if (!isFirst) {
|
||||||
|
classTitle.attr("dy", conf.textHeight);
|
||||||
|
}
|
||||||
|
const titleHeight = title.node().getBBox().height;
|
||||||
|
let membersLine;
|
||||||
|
let membersBox;
|
||||||
|
let methodsLine;
|
||||||
|
if (classDef.members.length > 0) {
|
||||||
|
membersLine = g.append("line").attr("x1", 0).attr("y1", conf.padding + titleHeight + conf.dividerMargin / 2).attr("y2", conf.padding + titleHeight + conf.dividerMargin / 2);
|
||||||
|
const members = g.append("text").attr("x", conf.padding).attr("y", titleHeight + conf.dividerMargin + conf.textHeight).attr("fill", "white").attr("class", "classText");
|
||||||
|
isFirst = true;
|
||||||
|
classDef.members.forEach(function(member) {
|
||||||
|
addTspan(members, member, isFirst, conf);
|
||||||
|
isFirst = false;
|
||||||
|
});
|
||||||
|
membersBox = members.node().getBBox();
|
||||||
|
}
|
||||||
|
if (classDef.methods.length > 0) {
|
||||||
|
methodsLine = g.append("line").attr("x1", 0).attr("y1", conf.padding + titleHeight + conf.dividerMargin + membersBox.height).attr("y2", conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
|
||||||
|
const methods = g.append("text").attr("x", conf.padding).attr("y", titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight).attr("fill", "white").attr("class", "classText");
|
||||||
|
isFirst = true;
|
||||||
|
classDef.methods.forEach(function(method) {
|
||||||
|
addTspan(methods, method, isFirst, conf);
|
||||||
|
isFirst = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const classBox = g.node().getBBox();
|
||||||
|
var cssClassStr = " ";
|
||||||
|
if (classDef.cssClasses.length > 0) {
|
||||||
|
cssClassStr = cssClassStr + classDef.cssClasses.join(" ");
|
||||||
|
}
|
||||||
|
const rect = g.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", classBox.width + 2 * conf.padding).attr("height", classBox.height + conf.padding + 0.5 * conf.dividerMargin).attr("class", cssClassStr);
|
||||||
|
const rectWidth = rect.node().getBBox().width;
|
||||||
|
title.node().childNodes.forEach(function(x) {
|
||||||
|
x.setAttribute("x", (rectWidth - x.getBBox().width) / 2);
|
||||||
|
});
|
||||||
|
if (classDef.tooltip) {
|
||||||
|
title.insert("title").text(classDef.tooltip);
|
||||||
|
}
|
||||||
|
if (membersLine) {
|
||||||
|
membersLine.attr("x2", rectWidth);
|
||||||
|
}
|
||||||
|
if (methodsLine) {
|
||||||
|
methodsLine.attr("x2", rectWidth);
|
||||||
|
}
|
||||||
|
classInfo.width = rectWidth;
|
||||||
|
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
|
||||||
|
return classInfo;
|
||||||
|
};
|
||||||
|
const getClassTitleString = function(classDef) {
|
||||||
|
let classTitleString = classDef.id;
|
||||||
|
if (classDef.type) {
|
||||||
|
classTitleString += "<" + parseGenericTypes(classDef.type) + ">";
|
||||||
|
}
|
||||||
|
return classTitleString;
|
||||||
|
};
|
||||||
|
const drawNote = function(elem, note, conf, diagObj) {
|
||||||
|
log.debug("Rendering note ", note, conf);
|
||||||
|
const id = note.id;
|
||||||
|
const noteInfo = {
|
||||||
|
id,
|
||||||
|
text: note.text,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
const g = elem.append("g").attr("id", id).attr("class", "classGroup");
|
||||||
|
let text = g.append("text").attr("y", conf.textHeight + conf.padding).attr("x", 0);
|
||||||
|
const lines = JSON.parse(`"${note.text}"`).split("\n");
|
||||||
|
lines.forEach(function(line2) {
|
||||||
|
log.debug(`Adding line: ${line2}`);
|
||||||
|
text.append("tspan").text(line2).attr("class", "title").attr("dy", conf.textHeight);
|
||||||
|
});
|
||||||
|
const noteBox = g.node().getBBox();
|
||||||
|
const rect = g.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", noteBox.width + 2 * conf.padding).attr(
|
||||||
|
"height",
|
||||||
|
noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin
|
||||||
|
);
|
||||||
|
const rectWidth = rect.node().getBBox().width;
|
||||||
|
text.node().childNodes.forEach(function(x) {
|
||||||
|
x.setAttribute("x", (rectWidth - x.getBBox().width) / 2);
|
||||||
|
});
|
||||||
|
noteInfo.width = rectWidth;
|
||||||
|
noteInfo.height = noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin;
|
||||||
|
return noteInfo;
|
||||||
|
};
|
||||||
|
const addTspan = function(textEl, member, isFirst, conf) {
|
||||||
|
const { displayText, cssStyle } = member.getDisplayDetails();
|
||||||
|
const tSpan = textEl.append("tspan").attr("x", conf.padding).text(displayText);
|
||||||
|
if (cssStyle !== "") {
|
||||||
|
tSpan.attr("style", member.cssStyle);
|
||||||
|
}
|
||||||
|
if (!isFirst) {
|
||||||
|
tSpan.attr("dy", conf.textHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const svgDraw = {
|
||||||
|
getClassTitleString,
|
||||||
|
drawClass,
|
||||||
|
drawEdge,
|
||||||
|
drawNote
|
||||||
|
};
|
||||||
|
let idCache = {};
|
||||||
|
const padding = 20;
|
||||||
|
const getGraphId = function(label) {
|
||||||
|
const foundEntry = Object.entries(idCache).find((entry) => entry[1].label === label);
|
||||||
|
if (foundEntry) {
|
||||||
|
return foundEntry[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const insertMarkers = function(elem) {
|
||||||
|
elem.append("defs").append("marker").attr("id", "extensionStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 1,7 L18,13 V 1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "extensionEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 1,1 V 13 L18,7 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "compositionStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "compositionEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "aggregationStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "aggregationEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "dependencyStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 5,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "dependencyEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L14,7 L9,1 Z");
|
||||||
|
};
|
||||||
|
const draw = function(text, id, _version, diagObj) {
|
||||||
|
const conf = getConfig().class;
|
||||||
|
idCache = {};
|
||||||
|
log.info("Rendering diagram " + text);
|
||||||
|
const securityLevel = getConfig().securityLevel;
|
||||||
|
let sandboxElement;
|
||||||
|
if (securityLevel === "sandbox") {
|
||||||
|
sandboxElement = select("#i" + id);
|
||||||
|
}
|
||||||
|
const root = securityLevel === "sandbox" ? select(sandboxElement.nodes()[0].contentDocument.body) : select("body");
|
||||||
|
const diagram2 = root.select(`[id='${id}']`);
|
||||||
|
insertMarkers(diagram2);
|
||||||
|
const g = new graphlib.Graph({
|
||||||
|
multigraph: true
|
||||||
|
});
|
||||||
|
g.setGraph({
|
||||||
|
isMultiGraph: true
|
||||||
|
});
|
||||||
|
g.setDefaultEdgeLabel(function() {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const classes = diagObj.db.getClasses();
|
||||||
|
const keys = Object.keys(classes);
|
||||||
|
for (const key of keys) {
|
||||||
|
const classDef = classes[key];
|
||||||
|
const node = svgDraw.drawClass(diagram2, classDef, conf, diagObj);
|
||||||
|
idCache[node.id] = node;
|
||||||
|
g.setNode(node.id, node);
|
||||||
|
log.info("Org height: " + node.height);
|
||||||
|
}
|
||||||
|
const relations = diagObj.db.getRelations();
|
||||||
|
relations.forEach(function(relation) {
|
||||||
|
log.info(
|
||||||
|
"tjoho" + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation)
|
||||||
|
);
|
||||||
|
g.setEdge(
|
||||||
|
getGraphId(relation.id1),
|
||||||
|
getGraphId(relation.id2),
|
||||||
|
{
|
||||||
|
relation
|
||||||
|
},
|
||||||
|
relation.title || "DEFAULT"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const notes = diagObj.db.getNotes();
|
||||||
|
notes.forEach(function(note) {
|
||||||
|
log.debug(`Adding note: ${JSON.stringify(note)}`);
|
||||||
|
const node = svgDraw.drawNote(diagram2, note, conf, diagObj);
|
||||||
|
idCache[node.id] = node;
|
||||||
|
g.setNode(node.id, node);
|
||||||
|
if (note.class && note.class in classes) {
|
||||||
|
g.setEdge(
|
||||||
|
note.id,
|
||||||
|
getGraphId(note.class),
|
||||||
|
{
|
||||||
|
relation: {
|
||||||
|
id1: note.id,
|
||||||
|
id2: note.class,
|
||||||
|
relation: {
|
||||||
|
type1: "none",
|
||||||
|
type2: "none",
|
||||||
|
lineType: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DEFAULT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layout(g);
|
||||||
|
g.nodes().forEach(function(v) {
|
||||||
|
if (v !== void 0 && g.node(v) !== void 0) {
|
||||||
|
log.debug("Node " + v + ": " + JSON.stringify(g.node(v)));
|
||||||
|
root.select("#" + (diagObj.db.lookUpDomId(v) || v)).attr(
|
||||||
|
"transform",
|
||||||
|
"translate(" + (g.node(v).x - g.node(v).width / 2) + "," + (g.node(v).y - g.node(v).height / 2) + " )"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
g.edges().forEach(function(e) {
|
||||||
|
if (e !== void 0 && g.edge(e) !== void 0) {
|
||||||
|
log.debug("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e)));
|
||||||
|
svgDraw.drawEdge(diagram2, g.edge(e), g.edge(e).relation, conf, diagObj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const svgBounds = diagram2.node().getBBox();
|
||||||
|
const width = svgBounds.width + padding * 2;
|
||||||
|
const height = svgBounds.height + padding * 2;
|
||||||
|
configureSvgSize(diagram2, height, width, conf.useMaxWidth);
|
||||||
|
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`;
|
||||||
|
log.debug(`viewBox ${vBox}`);
|
||||||
|
diagram2.attr("viewBox", vBox);
|
||||||
|
};
|
||||||
|
const renderer = {
|
||||||
|
draw
|
||||||
|
};
|
||||||
|
const diagram = {
|
||||||
|
parser,
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
styles,
|
||||||
|
init: (cnf) => {
|
||||||
|
if (!cnf.class) {
|
||||||
|
cnf.class = {};
|
||||||
|
}
|
||||||
|
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||||
|
db.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
diagram
|
||||||
|
};
|
|
@ -0,0 +1,354 @@
|
||||||
|
import { p as parser, d as db, s as styles } from "./styles-6e7f2b1b.js";
|
||||||
|
import { F as curveBasis, z as utils, l as log, G as parseGenericTypes, c as getConfig, j as d3select, k as configureSvgSize } from "./mermaid-491db2d9.js";
|
||||||
|
import { G as Graph, l as layout } from "./layout-a7b9ff07.js";
|
||||||
|
import { l as line } from "./line-8fd2bd69.js";
|
||||||
|
import "./array-b7dcf730.js";
|
||||||
|
import "./constant-b644328d.js";
|
||||||
|
let edgeCount = 0;
|
||||||
|
const drawEdge = function(elem, path, relation, conf, diagObj) {
|
||||||
|
const getRelationType = function(type) {
|
||||||
|
switch (type) {
|
||||||
|
case diagObj.db.relationType.AGGREGATION:
|
||||||
|
return "aggregation";
|
||||||
|
case diagObj.db.relationType.EXTENSION:
|
||||||
|
return "extension";
|
||||||
|
case diagObj.db.relationType.COMPOSITION:
|
||||||
|
return "composition";
|
||||||
|
case diagObj.db.relationType.DEPENDENCY:
|
||||||
|
return "dependency";
|
||||||
|
case diagObj.db.relationType.LOLLIPOP:
|
||||||
|
return "lollipop";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
path.points = path.points.filter((p) => !Number.isNaN(p.y));
|
||||||
|
const lineData = path.points;
|
||||||
|
const lineFunction = line().x(function(d) {
|
||||||
|
return d.x;
|
||||||
|
}).y(function(d) {
|
||||||
|
return d.y;
|
||||||
|
}).curve(curveBasis);
|
||||||
|
const svgPath = elem.append("path").attr("d", lineFunction(lineData)).attr("id", "edge" + edgeCount).attr("class", "relation");
|
||||||
|
let url = "";
|
||||||
|
if (conf.arrowMarkerAbsolute) {
|
||||||
|
url = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search;
|
||||||
|
url = url.replace(/\(/g, "\\(");
|
||||||
|
url = url.replace(/\)/g, "\\)");
|
||||||
|
}
|
||||||
|
if (relation.relation.lineType == 1) {
|
||||||
|
svgPath.attr("class", "relation dashed-line");
|
||||||
|
}
|
||||||
|
if (relation.relation.lineType == 10) {
|
||||||
|
svgPath.attr("class", "relation dotted-line");
|
||||||
|
}
|
||||||
|
if (relation.relation.type1 !== "none") {
|
||||||
|
svgPath.attr(
|
||||||
|
"marker-start",
|
||||||
|
"url(" + url + "#" + getRelationType(relation.relation.type1) + "Start)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (relation.relation.type2 !== "none") {
|
||||||
|
svgPath.attr(
|
||||||
|
"marker-end",
|
||||||
|
"url(" + url + "#" + getRelationType(relation.relation.type2) + "End)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let x, y;
|
||||||
|
const l = path.points.length;
|
||||||
|
let labelPosition = utils.calcLabelPosition(path.points);
|
||||||
|
x = labelPosition.x;
|
||||||
|
y = labelPosition.y;
|
||||||
|
let p1_card_x, p1_card_y;
|
||||||
|
let p2_card_x, p2_card_y;
|
||||||
|
if (l % 2 !== 0 && l > 1) {
|
||||||
|
let cardinality_1_point = utils.calcCardinalityPosition(
|
||||||
|
relation.relation.type1 !== "none",
|
||||||
|
path.points,
|
||||||
|
path.points[0]
|
||||||
|
);
|
||||||
|
let cardinality_2_point = utils.calcCardinalityPosition(
|
||||||
|
relation.relation.type2 !== "none",
|
||||||
|
path.points,
|
||||||
|
path.points[l - 1]
|
||||||
|
);
|
||||||
|
log.debug("cardinality_1_point " + JSON.stringify(cardinality_1_point));
|
||||||
|
log.debug("cardinality_2_point " + JSON.stringify(cardinality_2_point));
|
||||||
|
p1_card_x = cardinality_1_point.x;
|
||||||
|
p1_card_y = cardinality_1_point.y;
|
||||||
|
p2_card_x = cardinality_2_point.x;
|
||||||
|
p2_card_y = cardinality_2_point.y;
|
||||||
|
}
|
||||||
|
if (relation.title !== void 0) {
|
||||||
|
const g = elem.append("g").attr("class", "classLabel");
|
||||||
|
const label = g.append("text").attr("class", "label").attr("x", x).attr("y", y).attr("fill", "red").attr("text-anchor", "middle").text(relation.title);
|
||||||
|
window.label = label;
|
||||||
|
const bounds = label.node().getBBox();
|
||||||
|
g.insert("rect", ":first-child").attr("class", "box").attr("x", bounds.x - conf.padding / 2).attr("y", bounds.y - conf.padding / 2).attr("width", bounds.width + conf.padding).attr("height", bounds.height + conf.padding);
|
||||||
|
}
|
||||||
|
log.info("Rendering relation " + JSON.stringify(relation));
|
||||||
|
if (relation.relationTitle1 !== void 0 && relation.relationTitle1 !== "none") {
|
||||||
|
const g = elem.append("g").attr("class", "cardinality");
|
||||||
|
g.append("text").attr("class", "type1").attr("x", p1_card_x).attr("y", p1_card_y).attr("fill", "black").attr("font-size", "6").text(relation.relationTitle1);
|
||||||
|
}
|
||||||
|
if (relation.relationTitle2 !== void 0 && relation.relationTitle2 !== "none") {
|
||||||
|
const g = elem.append("g").attr("class", "cardinality");
|
||||||
|
g.append("text").attr("class", "type2").attr("x", p2_card_x).attr("y", p2_card_y).attr("fill", "black").attr("font-size", "6").text(relation.relationTitle2);
|
||||||
|
}
|
||||||
|
edgeCount++;
|
||||||
|
};
|
||||||
|
const drawClass = function(elem, classDef, conf, diagObj) {
|
||||||
|
log.debug("Rendering class ", classDef, conf);
|
||||||
|
const id = classDef.id;
|
||||||
|
const classInfo = {
|
||||||
|
id,
|
||||||
|
label: classDef.id,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
const g = elem.append("g").attr("id", diagObj.db.lookUpDomId(id)).attr("class", "classGroup");
|
||||||
|
let title;
|
||||||
|
if (classDef.link) {
|
||||||
|
title = g.append("svg:a").attr("xlink:href", classDef.link).attr("target", classDef.linkTarget).append("text").attr("y", conf.textHeight + conf.padding).attr("x", 0);
|
||||||
|
} else {
|
||||||
|
title = g.append("text").attr("y", conf.textHeight + conf.padding).attr("x", 0);
|
||||||
|
}
|
||||||
|
let isFirst = true;
|
||||||
|
classDef.annotations.forEach(function(member) {
|
||||||
|
const titleText2 = title.append("tspan").text("«" + member + "»");
|
||||||
|
if (!isFirst) {
|
||||||
|
titleText2.attr("dy", conf.textHeight);
|
||||||
|
}
|
||||||
|
isFirst = false;
|
||||||
|
});
|
||||||
|
let classTitleString = getClassTitleString(classDef);
|
||||||
|
const classTitle = title.append("tspan").text(classTitleString).attr("class", "title");
|
||||||
|
if (!isFirst) {
|
||||||
|
classTitle.attr("dy", conf.textHeight);
|
||||||
|
}
|
||||||
|
const titleHeight = title.node().getBBox().height;
|
||||||
|
let membersLine;
|
||||||
|
let membersBox;
|
||||||
|
let methodsLine;
|
||||||
|
if (classDef.members.length > 0) {
|
||||||
|
membersLine = g.append("line").attr("x1", 0).attr("y1", conf.padding + titleHeight + conf.dividerMargin / 2).attr("y2", conf.padding + titleHeight + conf.dividerMargin / 2);
|
||||||
|
const members = g.append("text").attr("x", conf.padding).attr("y", titleHeight + conf.dividerMargin + conf.textHeight).attr("fill", "white").attr("class", "classText");
|
||||||
|
isFirst = true;
|
||||||
|
classDef.members.forEach(function(member) {
|
||||||
|
addTspan(members, member, isFirst, conf);
|
||||||
|
isFirst = false;
|
||||||
|
});
|
||||||
|
membersBox = members.node().getBBox();
|
||||||
|
}
|
||||||
|
if (classDef.methods.length > 0) {
|
||||||
|
methodsLine = g.append("line").attr("x1", 0).attr("y1", conf.padding + titleHeight + conf.dividerMargin + membersBox.height).attr("y2", conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
|
||||||
|
const methods = g.append("text").attr("x", conf.padding).attr("y", titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight).attr("fill", "white").attr("class", "classText");
|
||||||
|
isFirst = true;
|
||||||
|
classDef.methods.forEach(function(method) {
|
||||||
|
addTspan(methods, method, isFirst, conf);
|
||||||
|
isFirst = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const classBox = g.node().getBBox();
|
||||||
|
var cssClassStr = " ";
|
||||||
|
if (classDef.cssClasses.length > 0) {
|
||||||
|
cssClassStr = cssClassStr + classDef.cssClasses.join(" ");
|
||||||
|
}
|
||||||
|
const rect = g.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", classBox.width + 2 * conf.padding).attr("height", classBox.height + conf.padding + 0.5 * conf.dividerMargin).attr("class", cssClassStr);
|
||||||
|
const rectWidth = rect.node().getBBox().width;
|
||||||
|
title.node().childNodes.forEach(function(x) {
|
||||||
|
x.setAttribute("x", (rectWidth - x.getBBox().width) / 2);
|
||||||
|
});
|
||||||
|
if (classDef.tooltip) {
|
||||||
|
title.insert("title").text(classDef.tooltip);
|
||||||
|
}
|
||||||
|
if (membersLine) {
|
||||||
|
membersLine.attr("x2", rectWidth);
|
||||||
|
}
|
||||||
|
if (methodsLine) {
|
||||||
|
methodsLine.attr("x2", rectWidth);
|
||||||
|
}
|
||||||
|
classInfo.width = rectWidth;
|
||||||
|
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
|
||||||
|
return classInfo;
|
||||||
|
};
|
||||||
|
const getClassTitleString = function(classDef) {
|
||||||
|
let classTitleString = classDef.id;
|
||||||
|
if (classDef.type) {
|
||||||
|
classTitleString += "<" + parseGenericTypes(classDef.type) + ">";
|
||||||
|
}
|
||||||
|
return classTitleString;
|
||||||
|
};
|
||||||
|
const drawNote = function(elem, note, conf, diagObj) {
|
||||||
|
log.debug("Rendering note ", note, conf);
|
||||||
|
const id = note.id;
|
||||||
|
const noteInfo = {
|
||||||
|
id,
|
||||||
|
text: note.text,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
const g = elem.append("g").attr("id", id).attr("class", "classGroup");
|
||||||
|
let text = g.append("text").attr("y", conf.textHeight + conf.padding).attr("x", 0);
|
||||||
|
const lines = JSON.parse(`"${note.text}"`).split("\n");
|
||||||
|
lines.forEach(function(line2) {
|
||||||
|
log.debug(`Adding line: ${line2}`);
|
||||||
|
text.append("tspan").text(line2).attr("class", "title").attr("dy", conf.textHeight);
|
||||||
|
});
|
||||||
|
const noteBox = g.node().getBBox();
|
||||||
|
const rect = g.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", noteBox.width + 2 * conf.padding).attr(
|
||||||
|
"height",
|
||||||
|
noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin
|
||||||
|
);
|
||||||
|
const rectWidth = rect.node().getBBox().width;
|
||||||
|
text.node().childNodes.forEach(function(x) {
|
||||||
|
x.setAttribute("x", (rectWidth - x.getBBox().width) / 2);
|
||||||
|
});
|
||||||
|
noteInfo.width = rectWidth;
|
||||||
|
noteInfo.height = noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin;
|
||||||
|
return noteInfo;
|
||||||
|
};
|
||||||
|
const addTspan = function(textEl, member, isFirst, conf) {
|
||||||
|
const { displayText, cssStyle } = member.getDisplayDetails();
|
||||||
|
const tSpan = textEl.append("tspan").attr("x", conf.padding).text(displayText);
|
||||||
|
if (cssStyle !== "") {
|
||||||
|
tSpan.attr("style", member.cssStyle);
|
||||||
|
}
|
||||||
|
if (!isFirst) {
|
||||||
|
tSpan.attr("dy", conf.textHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const svgDraw = {
|
||||||
|
getClassTitleString,
|
||||||
|
drawClass,
|
||||||
|
drawEdge,
|
||||||
|
drawNote
|
||||||
|
};
|
||||||
|
let idCache = {};
|
||||||
|
const padding = 20;
|
||||||
|
const getGraphId = function(label) {
|
||||||
|
const foundEntry = Object.entries(idCache).find((entry) => entry[1].label === label);
|
||||||
|
if (foundEntry) {
|
||||||
|
return foundEntry[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const insertMarkers = function(elem) {
|
||||||
|
elem.append("defs").append("marker").attr("id", "extensionStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 1,7 L18,13 V 1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "extensionEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 1,1 V 13 L18,7 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "compositionStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "compositionEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "aggregationStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "aggregationEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "dependencyStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 5,7 L9,13 L1,7 L9,1 Z");
|
||||||
|
elem.append("defs").append("marker").attr("id", "dependencyEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L14,7 L9,1 Z");
|
||||||
|
};
|
||||||
|
const draw = function(text, id, _version, diagObj) {
|
||||||
|
const conf = getConfig().class;
|
||||||
|
idCache = {};
|
||||||
|
log.info("Rendering diagram " + text);
|
||||||
|
const securityLevel = getConfig().securityLevel;
|
||||||
|
let sandboxElement;
|
||||||
|
if (securityLevel === "sandbox") {
|
||||||
|
sandboxElement = d3select("#i" + id);
|
||||||
|
}
|
||||||
|
const root = securityLevel === "sandbox" ? d3select(sandboxElement.nodes()[0].contentDocument.body) : d3select("body");
|
||||||
|
const diagram2 = root.select(`[id='${id}']`);
|
||||||
|
insertMarkers(diagram2);
|
||||||
|
const g = new Graph({
|
||||||
|
multigraph: true
|
||||||
|
});
|
||||||
|
g.setGraph({
|
||||||
|
isMultiGraph: true
|
||||||
|
});
|
||||||
|
g.setDefaultEdgeLabel(function() {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const classes = diagObj.db.getClasses();
|
||||||
|
const keys = Object.keys(classes);
|
||||||
|
for (const key of keys) {
|
||||||
|
const classDef = classes[key];
|
||||||
|
const node = svgDraw.drawClass(diagram2, classDef, conf, diagObj);
|
||||||
|
idCache[node.id] = node;
|
||||||
|
g.setNode(node.id, node);
|
||||||
|
log.info("Org height: " + node.height);
|
||||||
|
}
|
||||||
|
const relations = diagObj.db.getRelations();
|
||||||
|
relations.forEach(function(relation) {
|
||||||
|
log.info(
|
||||||
|
"tjoho" + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation)
|
||||||
|
);
|
||||||
|
g.setEdge(
|
||||||
|
getGraphId(relation.id1),
|
||||||
|
getGraphId(relation.id2),
|
||||||
|
{
|
||||||
|
relation
|
||||||
|
},
|
||||||
|
relation.title || "DEFAULT"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const notes = diagObj.db.getNotes();
|
||||||
|
notes.forEach(function(note) {
|
||||||
|
log.debug(`Adding note: ${JSON.stringify(note)}`);
|
||||||
|
const node = svgDraw.drawNote(diagram2, note, conf, diagObj);
|
||||||
|
idCache[node.id] = node;
|
||||||
|
g.setNode(node.id, node);
|
||||||
|
if (note.class && note.class in classes) {
|
||||||
|
g.setEdge(
|
||||||
|
note.id,
|
||||||
|
getGraphId(note.class),
|
||||||
|
{
|
||||||
|
relation: {
|
||||||
|
id1: note.id,
|
||||||
|
id2: note.class,
|
||||||
|
relation: {
|
||||||
|
type1: "none",
|
||||||
|
type2: "none",
|
||||||
|
lineType: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DEFAULT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layout(g);
|
||||||
|
g.nodes().forEach(function(v) {
|
||||||
|
if (v !== void 0 && g.node(v) !== void 0) {
|
||||||
|
log.debug("Node " + v + ": " + JSON.stringify(g.node(v)));
|
||||||
|
root.select("#" + (diagObj.db.lookUpDomId(v) || v)).attr(
|
||||||
|
"transform",
|
||||||
|
"translate(" + (g.node(v).x - g.node(v).width / 2) + "," + (g.node(v).y - g.node(v).height / 2) + " )"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
g.edges().forEach(function(e) {
|
||||||
|
if (e !== void 0 && g.edge(e) !== void 0) {
|
||||||
|
log.debug("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e)));
|
||||||
|
svgDraw.drawEdge(diagram2, g.edge(e), g.edge(e).relation, conf, diagObj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const svgBounds = diagram2.node().getBBox();
|
||||||
|
const width = svgBounds.width + padding * 2;
|
||||||
|
const height = svgBounds.height + padding * 2;
|
||||||
|
configureSvgSize(diagram2, height, width, conf.useMaxWidth);
|
||||||
|
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`;
|
||||||
|
log.debug(`viewBox ${vBox}`);
|
||||||
|
diagram2.attr("viewBox", vBox);
|
||||||
|
};
|
||||||
|
const renderer = {
|
||||||
|
draw
|
||||||
|
};
|
||||||
|
const diagram = {
|
||||||
|
parser,
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
styles,
|
||||||
|
init: (cnf) => {
|
||||||
|
if (!cnf.class) {
|
||||||
|
cnf.class = {};
|
||||||
|
}
|
||||||
|
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||||
|
db.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
diagram
|
||||||
|
};
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { p as G, d as S, s as A } from "./styles-483f8ae9.js";
|
||||||
|
import { F as W, z as B, l as u, G as I, c as H, j as M, k as O } from "./mermaid-e4a58915.js";
|
||||||
|
import { G as P, l as X } from "./layout-545b2d5b.js";
|
||||||
|
import { l as Y } from "./line-4ba3c4fa.js";
|
||||||
|
import "./array-2ff2c7a6.js";
|
||||||
|
import "./constant-2fe7eae5.js";
|
||||||
|
let _ = 0;
|
||||||
|
const $ = function(i, a, t, o, p) {
|
||||||
|
const g = function(e) {
|
||||||
|
switch (e) {
|
||||||
|
case p.db.relationType.AGGREGATION:
|
||||||
|
return "aggregation";
|
||||||
|
case p.db.relationType.EXTENSION:
|
||||||
|
return "extension";
|
||||||
|
case p.db.relationType.COMPOSITION:
|
||||||
|
return "composition";
|
||||||
|
case p.db.relationType.DEPENDENCY:
|
||||||
|
return "dependency";
|
||||||
|
case p.db.relationType.LOLLIPOP:
|
||||||
|
return "lollipop";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
a.points = a.points.filter((e) => !Number.isNaN(e.y));
|
||||||
|
const s = a.points, c = Y().x(function(e) {
|
||||||
|
return e.x;
|
||||||
|
}).y(function(e) {
|
||||||
|
return e.y;
|
||||||
|
}).curve(W), n = i.append("path").attr("d", c(s)).attr("id", "edge" + _).attr("class", "relation");
|
||||||
|
let r = "";
|
||||||
|
o.arrowMarkerAbsolute && (r = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search, r = r.replace(/\(/g, "\\("), r = r.replace(/\)/g, "\\)")), t.relation.lineType == 1 && n.attr("class", "relation dashed-line"), t.relation.lineType == 10 && n.attr("class", "relation dotted-line"), t.relation.type1 !== "none" && n.attr(
|
||||||
|
"marker-start",
|
||||||
|
"url(" + r + "#" + g(t.relation.type1) + "Start)"
|
||||||
|
), t.relation.type2 !== "none" && n.attr(
|
||||||
|
"marker-end",
|
||||||
|
"url(" + r + "#" + g(t.relation.type2) + "End)"
|
||||||
|
);
|
||||||
|
let f, h;
|
||||||
|
const x = a.points.length;
|
||||||
|
let k = B.calcLabelPosition(a.points);
|
||||||
|
f = k.x, h = k.y;
|
||||||
|
let y, m, w, b;
|
||||||
|
if (x % 2 !== 0 && x > 1) {
|
||||||
|
let e = B.calcCardinalityPosition(
|
||||||
|
t.relation.type1 !== "none",
|
||||||
|
a.points,
|
||||||
|
a.points[0]
|
||||||
|
), d = B.calcCardinalityPosition(
|
||||||
|
t.relation.type2 !== "none",
|
||||||
|
a.points,
|
||||||
|
a.points[x - 1]
|
||||||
|
);
|
||||||
|
u.debug("cardinality_1_point " + JSON.stringify(e)), u.debug("cardinality_2_point " + JSON.stringify(d)), y = e.x, m = e.y, w = d.x, b = d.y;
|
||||||
|
}
|
||||||
|
if (t.title !== void 0) {
|
||||||
|
const e = i.append("g").attr("class", "classLabel"), d = e.append("text").attr("class", "label").attr("x", f).attr("y", h).attr("fill", "red").attr("text-anchor", "middle").text(t.title);
|
||||||
|
window.label = d;
|
||||||
|
const l = d.node().getBBox();
|
||||||
|
e.insert("rect", ":first-child").attr("class", "box").attr("x", l.x - o.padding / 2).attr("y", l.y - o.padding / 2).attr("width", l.width + o.padding).attr("height", l.height + o.padding);
|
||||||
|
}
|
||||||
|
u.info("Rendering relation " + JSON.stringify(t)), t.relationTitle1 !== void 0 && t.relationTitle1 !== "none" && i.append("g").attr("class", "cardinality").append("text").attr("class", "type1").attr("x", y).attr("y", m).attr("fill", "black").attr("font-size", "6").text(t.relationTitle1), t.relationTitle2 !== void 0 && t.relationTitle2 !== "none" && i.append("g").attr("class", "cardinality").append("text").attr("class", "type2").attr("x", w).attr("y", b).attr("fill", "black").attr("font-size", "6").text(t.relationTitle2), _++;
|
||||||
|
}, J = function(i, a, t, o) {
|
||||||
|
u.debug("Rendering class ", a, t);
|
||||||
|
const p = a.id, g = {
|
||||||
|
id: p,
|
||||||
|
label: a.id,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
}, s = i.append("g").attr("id", o.db.lookUpDomId(p)).attr("class", "classGroup");
|
||||||
|
let c;
|
||||||
|
a.link ? c = s.append("svg:a").attr("xlink:href", a.link).attr("target", a.linkTarget).append("text").attr("y", t.textHeight + t.padding).attr("x", 0) : c = s.append("text").attr("y", t.textHeight + t.padding).attr("x", 0);
|
||||||
|
let n = !0;
|
||||||
|
a.annotations.forEach(function(d) {
|
||||||
|
const l = c.append("tspan").text("«" + d + "»");
|
||||||
|
n || l.attr("dy", t.textHeight), n = !1;
|
||||||
|
});
|
||||||
|
let r = C(a);
|
||||||
|
const f = c.append("tspan").text(r).attr("class", "title");
|
||||||
|
n || f.attr("dy", t.textHeight);
|
||||||
|
const h = c.node().getBBox().height;
|
||||||
|
let x, k, y;
|
||||||
|
if (a.members.length > 0) {
|
||||||
|
x = s.append("line").attr("x1", 0).attr("y1", t.padding + h + t.dividerMargin / 2).attr("y2", t.padding + h + t.dividerMargin / 2);
|
||||||
|
const d = s.append("text").attr("x", t.padding).attr("y", h + t.dividerMargin + t.textHeight).attr("fill", "white").attr("class", "classText");
|
||||||
|
n = !0, a.members.forEach(function(l) {
|
||||||
|
v(d, l, n, t), n = !1;
|
||||||
|
}), k = d.node().getBBox();
|
||||||
|
}
|
||||||
|
if (a.methods.length > 0) {
|
||||||
|
y = s.append("line").attr("x1", 0).attr("y1", t.padding + h + t.dividerMargin + k.height).attr("y2", t.padding + h + t.dividerMargin + k.height);
|
||||||
|
const d = s.append("text").attr("x", t.padding).attr("y", h + 2 * t.dividerMargin + k.height + t.textHeight).attr("fill", "white").attr("class", "classText");
|
||||||
|
n = !0, a.methods.forEach(function(l) {
|
||||||
|
v(d, l, n, t), n = !1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const m = s.node().getBBox();
|
||||||
|
var w = " ";
|
||||||
|
a.cssClasses.length > 0 && (w = w + a.cssClasses.join(" "));
|
||||||
|
const e = s.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", m.width + 2 * t.padding).attr("height", m.height + t.padding + 0.5 * t.dividerMargin).attr("class", w).node().getBBox().width;
|
||||||
|
return c.node().childNodes.forEach(function(d) {
|
||||||
|
d.setAttribute("x", (e - d.getBBox().width) / 2);
|
||||||
|
}), a.tooltip && c.insert("title").text(a.tooltip), x && x.attr("x2", e), y && y.attr("x2", e), g.width = e, g.height = m.height + t.padding + 0.5 * t.dividerMargin, g;
|
||||||
|
}, C = function(i) {
|
||||||
|
let a = i.id;
|
||||||
|
return i.type && (a += "<" + I(i.type) + ">"), a;
|
||||||
|
}, Z = function(i, a, t, o) {
|
||||||
|
u.debug("Rendering note ", a, t);
|
||||||
|
const p = a.id, g = {
|
||||||
|
id: p,
|
||||||
|
text: a.text,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
}, s = i.append("g").attr("id", p).attr("class", "classGroup");
|
||||||
|
let c = s.append("text").attr("y", t.textHeight + t.padding).attr("x", 0);
|
||||||
|
const n = JSON.parse(`"${a.text}"`).split(`
|
||||||
|
`);
|
||||||
|
n.forEach(function(x) {
|
||||||
|
u.debug(`Adding line: ${x}`), c.append("tspan").text(x).attr("class", "title").attr("dy", t.textHeight);
|
||||||
|
});
|
||||||
|
const r = s.node().getBBox(), h = s.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", r.width + 2 * t.padding).attr(
|
||||||
|
"height",
|
||||||
|
r.height + n.length * t.textHeight + t.padding + 0.5 * t.dividerMargin
|
||||||
|
).node().getBBox().width;
|
||||||
|
return c.node().childNodes.forEach(function(x) {
|
||||||
|
x.setAttribute("x", (h - x.getBBox().width) / 2);
|
||||||
|
}), g.width = h, g.height = r.height + n.length * t.textHeight + t.padding + 0.5 * t.dividerMargin, g;
|
||||||
|
}, v = function(i, a, t, o) {
|
||||||
|
const { displayText: p, cssStyle: g } = a.getDisplayDetails(), s = i.append("tspan").attr("x", o.padding).text(p);
|
||||||
|
g !== "" && s.attr("style", a.cssStyle), t || s.attr("dy", o.textHeight);
|
||||||
|
}, N = {
|
||||||
|
getClassTitleString: C,
|
||||||
|
drawClass: J,
|
||||||
|
drawEdge: $,
|
||||||
|
drawNote: Z
|
||||||
|
};
|
||||||
|
let T = {};
|
||||||
|
const E = 20, L = function(i) {
|
||||||
|
const a = Object.entries(T).find((t) => t[1].label === i);
|
||||||
|
if (a)
|
||||||
|
return a[0];
|
||||||
|
}, R = function(i) {
|
||||||
|
i.append("defs").append("marker").attr("id", "extensionStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 1,7 L18,13 V 1 Z"), i.append("defs").append("marker").attr("id", "extensionEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 1,1 V 13 L18,7 Z"), i.append("defs").append("marker").attr("id", "compositionStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"), i.append("defs").append("marker").attr("id", "compositionEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"), i.append("defs").append("marker").attr("id", "aggregationStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"), i.append("defs").append("marker").attr("id", "aggregationEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"), i.append("defs").append("marker").attr("id", "dependencyStart").attr("class", "extension").attr("refX", 0).attr("refY", 7).attr("markerWidth", 190).attr("markerHeight", 240).attr("orient", "auto").append("path").attr("d", "M 5,7 L9,13 L1,7 L9,1 Z"), i.append("defs").append("marker").attr("id", "dependencyEnd").attr("refX", 19).attr("refY", 7).attr("markerWidth", 20).attr("markerHeight", 28).attr("orient", "auto").append("path").attr("d", "M 18,7 L9,13 L14,7 L9,1 Z");
|
||||||
|
}, F = function(i, a, t, o) {
|
||||||
|
const p = H().class;
|
||||||
|
T = {}, u.info("Rendering diagram " + i);
|
||||||
|
const g = H().securityLevel;
|
||||||
|
let s;
|
||||||
|
g === "sandbox" && (s = M("#i" + a));
|
||||||
|
const c = g === "sandbox" ? M(s.nodes()[0].contentDocument.body) : M("body"), n = c.select(`[id='${a}']`);
|
||||||
|
R(n);
|
||||||
|
const r = new P({
|
||||||
|
multigraph: !0
|
||||||
|
});
|
||||||
|
r.setGraph({
|
||||||
|
isMultiGraph: !0
|
||||||
|
}), r.setDefaultEdgeLabel(function() {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const f = o.db.getClasses(), h = Object.keys(f);
|
||||||
|
for (const e of h) {
|
||||||
|
const d = f[e], l = N.drawClass(n, d, p, o);
|
||||||
|
T[l.id] = l, r.setNode(l.id, l), u.info("Org height: " + l.height);
|
||||||
|
}
|
||||||
|
o.db.getRelations().forEach(function(e) {
|
||||||
|
u.info(
|
||||||
|
"tjoho" + L(e.id1) + L(e.id2) + JSON.stringify(e)
|
||||||
|
), r.setEdge(
|
||||||
|
L(e.id1),
|
||||||
|
L(e.id2),
|
||||||
|
{
|
||||||
|
relation: e
|
||||||
|
},
|
||||||
|
e.title || "DEFAULT"
|
||||||
|
);
|
||||||
|
}), o.db.getNotes().forEach(function(e) {
|
||||||
|
u.debug(`Adding note: ${JSON.stringify(e)}`);
|
||||||
|
const d = N.drawNote(n, e, p, o);
|
||||||
|
T[d.id] = d, r.setNode(d.id, d), e.class && e.class in f && r.setEdge(
|
||||||
|
e.id,
|
||||||
|
L(e.class),
|
||||||
|
{
|
||||||
|
relation: {
|
||||||
|
id1: e.id,
|
||||||
|
id2: e.class,
|
||||||
|
relation: {
|
||||||
|
type1: "none",
|
||||||
|
type2: "none",
|
||||||
|
lineType: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DEFAULT"
|
||||||
|
);
|
||||||
|
}), X(r), r.nodes().forEach(function(e) {
|
||||||
|
e !== void 0 && r.node(e) !== void 0 && (u.debug("Node " + e + ": " + JSON.stringify(r.node(e))), c.select("#" + (o.db.lookUpDomId(e) || e)).attr(
|
||||||
|
"transform",
|
||||||
|
"translate(" + (r.node(e).x - r.node(e).width / 2) + "," + (r.node(e).y - r.node(e).height / 2) + " )"
|
||||||
|
));
|
||||||
|
}), r.edges().forEach(function(e) {
|
||||||
|
e !== void 0 && r.edge(e) !== void 0 && (u.debug("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(r.edge(e))), N.drawEdge(n, r.edge(e), r.edge(e).relation, p, o));
|
||||||
|
});
|
||||||
|
const y = n.node().getBBox(), m = y.width + E * 2, w = y.height + E * 2;
|
||||||
|
O(n, w, m, p.useMaxWidth);
|
||||||
|
const b = `${y.x - E} ${y.y - E} ${m} ${w}`;
|
||||||
|
u.debug(`viewBox ${b}`), n.attr("viewBox", b);
|
||||||
|
}, z = {
|
||||||
|
draw: F
|
||||||
|
}, D = {
|
||||||
|
parser: G,
|
||||||
|
db: S,
|
||||||
|
renderer: z,
|
||||||
|
styles: A,
|
||||||
|
init: (i) => {
|
||||||
|
i.class || (i.class = {}), i.class.arrowMarkerAbsolute = i.arrowMarkerAbsolute, S.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
D as diagram
|
||||||
|
};
|
|
@ -0,0 +1,288 @@
|
||||||
|
import { p as parser, d as db, s as styles } from "./styles-6e7f2b1b.js";
|
||||||
|
import { l as log, c as getConfig, j as d3select, z as utils, r as setupGraphViewbox, q as interpolateToCurve, n as curveLinear, o as getStylesFromArray, f as common } from "./mermaid-491db2d9.js";
|
||||||
|
import { G as Graph } from "./layout-a7b9ff07.js";
|
||||||
|
import { r as render } from "./index-cc269c15.js";
|
||||||
|
import "./edges-9bf94b2d.js";
|
||||||
|
import "./createText-2660bae1.js";
|
||||||
|
import "./line-8fd2bd69.js";
|
||||||
|
import "./array-b7dcf730.js";
|
||||||
|
import "./constant-b644328d.js";
|
||||||
|
const sanitizeText = (txt) => common.sanitizeText(txt, getConfig());
|
||||||
|
let conf = {
|
||||||
|
dividerMargin: 10,
|
||||||
|
padding: 5,
|
||||||
|
textHeight: 10,
|
||||||
|
curve: void 0
|
||||||
|
};
|
||||||
|
const addNamespaces = function(namespaces, g, _id, diagObj) {
|
||||||
|
const keys = Object.keys(namespaces);
|
||||||
|
log.info("keys:", keys);
|
||||||
|
log.info(namespaces);
|
||||||
|
keys.forEach(function(id) {
|
||||||
|
var _a, _b;
|
||||||
|
const vertex = namespaces[id];
|
||||||
|
const shape = "rect";
|
||||||
|
const node = {
|
||||||
|
shape,
|
||||||
|
id: vertex.id,
|
||||||
|
domId: vertex.domId,
|
||||||
|
labelText: sanitizeText(vertex.id),
|
||||||
|
labelStyle: "",
|
||||||
|
style: "fill: none; stroke: black",
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((_a = getConfig().flowchart) == null ? void 0 : _a.padding) ?? ((_b = getConfig().class) == null ? void 0 : _b.padding)
|
||||||
|
};
|
||||||
|
g.setNode(vertex.id, node);
|
||||||
|
addClasses(vertex.classes, g, _id, diagObj, vertex.id);
|
||||||
|
log.info("setNode", node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const addClasses = function(classes, g, _id, diagObj, parent) {
|
||||||
|
const keys = Object.keys(classes);
|
||||||
|
log.info("keys:", keys);
|
||||||
|
log.info(classes);
|
||||||
|
keys.filter((id) => classes[id].parent == parent).forEach(function(id) {
|
||||||
|
var _a, _b;
|
||||||
|
const vertex = classes[id];
|
||||||
|
const cssClassStr = vertex.cssClasses.join(" ");
|
||||||
|
const styles2 = { labelStyle: "", style: "" };
|
||||||
|
const vertexText = vertex.label ?? vertex.id;
|
||||||
|
const radius = 0;
|
||||||
|
const shape = "class_box";
|
||||||
|
const node = {
|
||||||
|
labelStyle: styles2.labelStyle,
|
||||||
|
shape,
|
||||||
|
labelText: sanitizeText(vertexText),
|
||||||
|
classData: vertex,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
class: cssClassStr,
|
||||||
|
style: styles2.style,
|
||||||
|
id: vertex.id,
|
||||||
|
domId: vertex.domId,
|
||||||
|
tooltip: diagObj.db.getTooltip(vertex.id, parent) || "",
|
||||||
|
haveCallback: vertex.haveCallback,
|
||||||
|
link: vertex.link,
|
||||||
|
width: vertex.type === "group" ? 500 : void 0,
|
||||||
|
type: vertex.type,
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((_a = getConfig().flowchart) == null ? void 0 : _a.padding) ?? ((_b = getConfig().class) == null ? void 0 : _b.padding)
|
||||||
|
};
|
||||||
|
g.setNode(vertex.id, node);
|
||||||
|
if (parent) {
|
||||||
|
g.setParent(vertex.id, parent);
|
||||||
|
}
|
||||||
|
log.info("setNode", node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const addNotes = function(notes, g, startEdgeId, classes) {
|
||||||
|
log.info(notes);
|
||||||
|
notes.forEach(function(note, i) {
|
||||||
|
var _a, _b;
|
||||||
|
const vertex = note;
|
||||||
|
const cssNoteStr = "";
|
||||||
|
const styles2 = { labelStyle: "", style: "" };
|
||||||
|
const vertexText = vertex.text;
|
||||||
|
const radius = 0;
|
||||||
|
const shape = "note";
|
||||||
|
const node = {
|
||||||
|
labelStyle: styles2.labelStyle,
|
||||||
|
shape,
|
||||||
|
labelText: sanitizeText(vertexText),
|
||||||
|
noteData: vertex,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
class: cssNoteStr,
|
||||||
|
style: styles2.style,
|
||||||
|
id: vertex.id,
|
||||||
|
domId: vertex.id,
|
||||||
|
tooltip: "",
|
||||||
|
type: "note",
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((_a = getConfig().flowchart) == null ? void 0 : _a.padding) ?? ((_b = getConfig().class) == null ? void 0 : _b.padding)
|
||||||
|
};
|
||||||
|
g.setNode(vertex.id, node);
|
||||||
|
log.info("setNode", node);
|
||||||
|
if (!vertex.class || !(vertex.class in classes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const edgeId = startEdgeId + i;
|
||||||
|
const edgeData = {
|
||||||
|
id: `edgeNote${edgeId}`,
|
||||||
|
//Set relationship style and line type
|
||||||
|
classes: "relation",
|
||||||
|
pattern: "dotted",
|
||||||
|
// Set link type for rendering
|
||||||
|
arrowhead: "none",
|
||||||
|
//Set edge extra labels
|
||||||
|
startLabelRight: "",
|
||||||
|
endLabelLeft: "",
|
||||||
|
//Set relation arrow types
|
||||||
|
arrowTypeStart: "none",
|
||||||
|
arrowTypeEnd: "none",
|
||||||
|
style: "fill:none",
|
||||||
|
labelStyle: "",
|
||||||
|
curve: interpolateToCurve(conf.curve, curveLinear)
|
||||||
|
};
|
||||||
|
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const addRelations = function(relations, g) {
|
||||||
|
const conf2 = getConfig().flowchart;
|
||||||
|
let cnt = 0;
|
||||||
|
relations.forEach(function(edge) {
|
||||||
|
var _a;
|
||||||
|
cnt++;
|
||||||
|
const edgeData = {
|
||||||
|
//Set relationship style and line type
|
||||||
|
classes: "relation",
|
||||||
|
pattern: edge.relation.lineType == 1 ? "dashed" : "solid",
|
||||||
|
id: "id" + cnt,
|
||||||
|
// Set link type for rendering
|
||||||
|
arrowhead: edge.type === "arrow_open" ? "none" : "normal",
|
||||||
|
//Set edge extra labels
|
||||||
|
startLabelRight: edge.relationTitle1 === "none" ? "" : edge.relationTitle1,
|
||||||
|
endLabelLeft: edge.relationTitle2 === "none" ? "" : edge.relationTitle2,
|
||||||
|
//Set relation arrow types
|
||||||
|
arrowTypeStart: getArrowMarker(edge.relation.type1),
|
||||||
|
arrowTypeEnd: getArrowMarker(edge.relation.type2),
|
||||||
|
style: "fill:none",
|
||||||
|
labelStyle: "",
|
||||||
|
curve: interpolateToCurve(conf2 == null ? void 0 : conf2.curve, curveLinear)
|
||||||
|
};
|
||||||
|
log.info(edgeData, edge);
|
||||||
|
if (edge.style !== void 0) {
|
||||||
|
const styles2 = getStylesFromArray(edge.style);
|
||||||
|
edgeData.style = styles2.style;
|
||||||
|
edgeData.labelStyle = styles2.labelStyle;
|
||||||
|
}
|
||||||
|
edge.text = edge.title;
|
||||||
|
if (edge.text === void 0) {
|
||||||
|
if (edge.style !== void 0) {
|
||||||
|
edgeData.arrowheadStyle = "fill: #333";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edgeData.arrowheadStyle = "fill: #333";
|
||||||
|
edgeData.labelpos = "c";
|
||||||
|
if (((_a = getConfig().flowchart) == null ? void 0 : _a.htmlLabels) ?? getConfig().htmlLabels) {
|
||||||
|
edgeData.labelType = "html";
|
||||||
|
edgeData.label = '<span class="edgeLabel">' + edge.text + "</span>";
|
||||||
|
} else {
|
||||||
|
edgeData.labelType = "text";
|
||||||
|
edgeData.label = edge.text.replace(common.lineBreakRegex, "\n");
|
||||||
|
if (edge.style === void 0) {
|
||||||
|
edgeData.style = edgeData.style || "stroke: #333; stroke-width: 1.5px;fill:none";
|
||||||
|
}
|
||||||
|
edgeData.labelStyle = edgeData.labelStyle.replace("color:", "fill:");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.setEdge(edge.id1, edge.id2, edgeData, cnt);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const setConf = function(cnf) {
|
||||||
|
conf = {
|
||||||
|
...conf,
|
||||||
|
...cnf
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const draw = async function(text, id, _version, diagObj) {
|
||||||
|
log.info("Drawing class - ", id);
|
||||||
|
const conf2 = getConfig().flowchart ?? getConfig().class;
|
||||||
|
const securityLevel = getConfig().securityLevel;
|
||||||
|
log.info("config:", conf2);
|
||||||
|
const nodeSpacing = (conf2 == null ? void 0 : conf2.nodeSpacing) ?? 50;
|
||||||
|
const rankSpacing = (conf2 == null ? void 0 : conf2.rankSpacing) ?? 50;
|
||||||
|
const g = new Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true
|
||||||
|
}).setGraph({
|
||||||
|
rankdir: diagObj.db.getDirection(),
|
||||||
|
nodesep: nodeSpacing,
|
||||||
|
ranksep: rankSpacing,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8
|
||||||
|
}).setDefaultEdgeLabel(function() {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const namespaces = diagObj.db.getNamespaces();
|
||||||
|
const classes = diagObj.db.getClasses();
|
||||||
|
const relations = diagObj.db.getRelations();
|
||||||
|
const notes = diagObj.db.getNotes();
|
||||||
|
log.info(relations);
|
||||||
|
addNamespaces(namespaces, g, id, diagObj);
|
||||||
|
addClasses(classes, g, id, diagObj);
|
||||||
|
addRelations(relations, g);
|
||||||
|
addNotes(notes, g, relations.length + 1, classes);
|
||||||
|
let sandboxElement;
|
||||||
|
if (securityLevel === "sandbox") {
|
||||||
|
sandboxElement = d3select("#i" + id);
|
||||||
|
}
|
||||||
|
const root = securityLevel === "sandbox" ? d3select(sandboxElement.nodes()[0].contentDocument.body) : d3select("body");
|
||||||
|
const svg = root.select(`[id="${id}"]`);
|
||||||
|
const element = root.select("#" + id + " g");
|
||||||
|
await render(
|
||||||
|
element,
|
||||||
|
g,
|
||||||
|
["aggregation", "extension", "composition", "dependency", "lollipop"],
|
||||||
|
"classDiagram",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
utils.insertTitle(svg, "classTitleText", (conf2 == null ? void 0 : conf2.titleTopMargin) ?? 5, diagObj.db.getDiagramTitle());
|
||||||
|
setupGraphViewbox(g, svg, conf2 == null ? void 0 : conf2.diagramPadding, conf2 == null ? void 0 : conf2.useMaxWidth);
|
||||||
|
if (!(conf2 == null ? void 0 : conf2.htmlLabels)) {
|
||||||
|
const doc = securityLevel === "sandbox" ? sandboxElement.nodes()[0].contentDocument : document;
|
||||||
|
const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||||
|
for (const label of labels) {
|
||||||
|
const dim = label.getBBox();
|
||||||
|
const rect = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||||
|
rect.setAttribute("rx", 0);
|
||||||
|
rect.setAttribute("ry", 0);
|
||||||
|
rect.setAttribute("width", dim.width);
|
||||||
|
rect.setAttribute("height", dim.height);
|
||||||
|
label.insertBefore(rect, label.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function getArrowMarker(type) {
|
||||||
|
let marker;
|
||||||
|
switch (type) {
|
||||||
|
case 0:
|
||||||
|
marker = "aggregation";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
marker = "extension";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
marker = "composition";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
marker = "dependency";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
marker = "lollipop";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
marker = "none";
|
||||||
|
}
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
const renderer = {
|
||||||
|
setConf,
|
||||||
|
draw
|
||||||
|
};
|
||||||
|
const diagram = {
|
||||||
|
parser,
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
styles,
|
||||||
|
init: (cnf) => {
|
||||||
|
if (!cnf.class) {
|
||||||
|
cnf.class = {};
|
||||||
|
}
|
||||||
|
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||||
|
db.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
diagram
|
||||||
|
};
|
|
@ -0,0 +1,204 @@
|
||||||
|
import { p as R, d as N, s as B } from "./styles-483f8ae9.js";
|
||||||
|
import { l as c, c as r, j as k, z as G, r as _, q as E, n as C, o as z, f as A } from "./mermaid-e4a58915.js";
|
||||||
|
import { G as q } from "./layout-545b2d5b.js";
|
||||||
|
import { r as P } from "./index-5f5016a9.js";
|
||||||
|
import "./edges-020bfa8c.js";
|
||||||
|
import "./createText-4be7776a.js";
|
||||||
|
import "./line-4ba3c4fa.js";
|
||||||
|
import "./array-2ff2c7a6.js";
|
||||||
|
import "./constant-2fe7eae5.js";
|
||||||
|
const S = (o) => A.sanitizeText(o, r());
|
||||||
|
let v = {
|
||||||
|
dividerMargin: 10,
|
||||||
|
padding: 5,
|
||||||
|
textHeight: 10,
|
||||||
|
curve: void 0
|
||||||
|
};
|
||||||
|
const $ = function(o, t, p, n) {
|
||||||
|
const e = Object.keys(o);
|
||||||
|
c.info("keys:", e), c.info(o), e.forEach(function(s) {
|
||||||
|
var y, d;
|
||||||
|
const l = o[s], i = {
|
||||||
|
shape: "rect",
|
||||||
|
id: l.id,
|
||||||
|
domId: l.domId,
|
||||||
|
labelText: S(l.id),
|
||||||
|
labelStyle: "",
|
||||||
|
style: "fill: none; stroke: black",
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((y = r().flowchart) == null ? void 0 : y.padding) ?? ((d = r().class) == null ? void 0 : d.padding)
|
||||||
|
};
|
||||||
|
t.setNode(l.id, i), I(l.classes, t, p, n, l.id), c.info("setNode", i);
|
||||||
|
});
|
||||||
|
}, I = function(o, t, p, n, e) {
|
||||||
|
const s = Object.keys(o);
|
||||||
|
c.info("keys:", s), c.info(o), s.filter((l) => o[l].parent == e).forEach(function(l) {
|
||||||
|
var h, u;
|
||||||
|
const a = o[l], i = a.cssClasses.join(" "), y = { labelStyle: "", style: "" }, d = a.label ?? a.id, f = 0, m = "class_box", b = {
|
||||||
|
labelStyle: y.labelStyle,
|
||||||
|
shape: m,
|
||||||
|
labelText: S(d),
|
||||||
|
classData: a,
|
||||||
|
rx: f,
|
||||||
|
ry: f,
|
||||||
|
class: i,
|
||||||
|
style: y.style,
|
||||||
|
id: a.id,
|
||||||
|
domId: a.domId,
|
||||||
|
tooltip: n.db.getTooltip(a.id, e) || "",
|
||||||
|
haveCallback: a.haveCallback,
|
||||||
|
link: a.link,
|
||||||
|
width: a.type === "group" ? 500 : void 0,
|
||||||
|
type: a.type,
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((h = r().flowchart) == null ? void 0 : h.padding) ?? ((u = r().class) == null ? void 0 : u.padding)
|
||||||
|
};
|
||||||
|
t.setNode(a.id, b), e && t.setParent(a.id, e), c.info("setNode", b);
|
||||||
|
});
|
||||||
|
}, F = function(o, t, p, n) {
|
||||||
|
c.info(o), o.forEach(function(e, s) {
|
||||||
|
var u, x;
|
||||||
|
const l = e, a = "", i = { labelStyle: "", style: "" }, y = l.text, d = 0, f = "note", m = {
|
||||||
|
labelStyle: i.labelStyle,
|
||||||
|
shape: f,
|
||||||
|
labelText: S(y),
|
||||||
|
noteData: l,
|
||||||
|
rx: d,
|
||||||
|
ry: d,
|
||||||
|
class: a,
|
||||||
|
style: i.style,
|
||||||
|
id: l.id,
|
||||||
|
domId: l.id,
|
||||||
|
tooltip: "",
|
||||||
|
type: "note",
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((u = r().flowchart) == null ? void 0 : u.padding) ?? ((x = r().class) == null ? void 0 : x.padding)
|
||||||
|
};
|
||||||
|
if (t.setNode(l.id, m), c.info("setNode", m), !l.class || !(l.class in n))
|
||||||
|
return;
|
||||||
|
const b = p + s, h = {
|
||||||
|
id: `edgeNote${b}`,
|
||||||
|
//Set relationship style and line type
|
||||||
|
classes: "relation",
|
||||||
|
pattern: "dotted",
|
||||||
|
// Set link type for rendering
|
||||||
|
arrowhead: "none",
|
||||||
|
//Set edge extra labels
|
||||||
|
startLabelRight: "",
|
||||||
|
endLabelLeft: "",
|
||||||
|
//Set relation arrow types
|
||||||
|
arrowTypeStart: "none",
|
||||||
|
arrowTypeEnd: "none",
|
||||||
|
style: "fill:none",
|
||||||
|
labelStyle: "",
|
||||||
|
curve: E(v.curve, C)
|
||||||
|
};
|
||||||
|
t.setEdge(l.id, l.class, h, b);
|
||||||
|
});
|
||||||
|
}, H = function(o, t) {
|
||||||
|
const p = r().flowchart;
|
||||||
|
let n = 0;
|
||||||
|
o.forEach(function(e) {
|
||||||
|
var l;
|
||||||
|
n++;
|
||||||
|
const s = {
|
||||||
|
//Set relationship style and line type
|
||||||
|
classes: "relation",
|
||||||
|
pattern: e.relation.lineType == 1 ? "dashed" : "solid",
|
||||||
|
id: "id" + n,
|
||||||
|
// Set link type for rendering
|
||||||
|
arrowhead: e.type === "arrow_open" ? "none" : "normal",
|
||||||
|
//Set edge extra labels
|
||||||
|
startLabelRight: e.relationTitle1 === "none" ? "" : e.relationTitle1,
|
||||||
|
endLabelLeft: e.relationTitle2 === "none" ? "" : e.relationTitle2,
|
||||||
|
//Set relation arrow types
|
||||||
|
arrowTypeStart: D(e.relation.type1),
|
||||||
|
arrowTypeEnd: D(e.relation.type2),
|
||||||
|
style: "fill:none",
|
||||||
|
labelStyle: "",
|
||||||
|
curve: E(p == null ? void 0 : p.curve, C)
|
||||||
|
};
|
||||||
|
if (c.info(s, e), e.style !== void 0) {
|
||||||
|
const a = z(e.style);
|
||||||
|
s.style = a.style, s.labelStyle = a.labelStyle;
|
||||||
|
}
|
||||||
|
e.text = e.title, e.text === void 0 ? e.style !== void 0 && (s.arrowheadStyle = "fill: #333") : (s.arrowheadStyle = "fill: #333", s.labelpos = "c", ((l = r().flowchart) == null ? void 0 : l.htmlLabels) ?? r().htmlLabels ? (s.labelType = "html", s.label = '<span class="edgeLabel">' + e.text + "</span>") : (s.labelType = "text", s.label = e.text.replace(A.lineBreakRegex, `
|
||||||
|
`), e.style === void 0 && (s.style = s.style || "stroke: #333; stroke-width: 1.5px;fill:none"), s.labelStyle = s.labelStyle.replace("color:", "fill:"))), t.setEdge(e.id1, e.id2, s, n);
|
||||||
|
});
|
||||||
|
}, V = function(o) {
|
||||||
|
v = {
|
||||||
|
...v,
|
||||||
|
...o
|
||||||
|
};
|
||||||
|
}, W = async function(o, t, p, n) {
|
||||||
|
c.info("Drawing class - ", t);
|
||||||
|
const e = r().flowchart ?? r().class, s = r().securityLevel;
|
||||||
|
c.info("config:", e);
|
||||||
|
const l = (e == null ? void 0 : e.nodeSpacing) ?? 50, a = (e == null ? void 0 : e.rankSpacing) ?? 50, i = new q({
|
||||||
|
multigraph: !0,
|
||||||
|
compound: !0
|
||||||
|
}).setGraph({
|
||||||
|
rankdir: n.db.getDirection(),
|
||||||
|
nodesep: l,
|
||||||
|
ranksep: a,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8
|
||||||
|
}).setDefaultEdgeLabel(function() {
|
||||||
|
return {};
|
||||||
|
}), y = n.db.getNamespaces(), d = n.db.getClasses(), f = n.db.getRelations(), m = n.db.getNotes();
|
||||||
|
c.info(f), $(y, i, t, n), I(d, i, t, n), H(f, i), F(m, i, f.length + 1, d);
|
||||||
|
let b;
|
||||||
|
s === "sandbox" && (b = k("#i" + t));
|
||||||
|
const h = s === "sandbox" ? k(b.nodes()[0].contentDocument.body) : k("body"), u = h.select(`[id="${t}"]`), x = h.select("#" + t + " g");
|
||||||
|
if (await P(
|
||||||
|
x,
|
||||||
|
i,
|
||||||
|
["aggregation", "extension", "composition", "dependency", "lollipop"],
|
||||||
|
"classDiagram",
|
||||||
|
t
|
||||||
|
), G.insertTitle(u, "classTitleText", (e == null ? void 0 : e.titleTopMargin) ?? 5, n.db.getDiagramTitle()), _(i, u, e == null ? void 0 : e.diagramPadding, e == null ? void 0 : e.useMaxWidth), !(e != null && e.htmlLabels)) {
|
||||||
|
const T = s === "sandbox" ? b.nodes()[0].contentDocument : document, M = T.querySelectorAll('[id="' + t + '"] .edgeLabel .label');
|
||||||
|
for (const w of M) {
|
||||||
|
const L = w.getBBox(), g = T.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||||
|
g.setAttribute("rx", 0), g.setAttribute("ry", 0), g.setAttribute("width", L.width), g.setAttribute("height", L.height), w.insertBefore(g, w.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function D(o) {
|
||||||
|
let t;
|
||||||
|
switch (o) {
|
||||||
|
case 0:
|
||||||
|
t = "aggregation";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
t = "extension";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
t = "composition";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
t = "dependency";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
t = "lollipop";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
t = "none";
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
const J = {
|
||||||
|
setConf: V,
|
||||||
|
draw: W
|
||||||
|
}, te = {
|
||||||
|
parser: R,
|
||||||
|
db: N,
|
||||||
|
renderer: J,
|
||||||
|
styles: B,
|
||||||
|
init: (o) => {
|
||||||
|
o.class || (o.class = {}), o.class.arrowMarkerAbsolute = o.arrowMarkerAbsolute, N.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
te as diagram
|
||||||
|
};
|
|
@ -0,0 +1,298 @@
|
||||||
|
import { p as parser, d as db, s as styles } from "./styles-8b67d7cb.js";
|
||||||
|
import { select, curveLinear } from "d3";
|
||||||
|
import * as graphlib from "dagre-d3-es/src/graphlib/index.js";
|
||||||
|
import { l as log, c as getConfig, u as utils, o as setupGraphViewbox, n as interpolateToCurve, k as getStylesFromArray, e as common } from "./mermaid-0d192ec3.js";
|
||||||
|
import { r as render } from "./index-f9462f3f.js";
|
||||||
|
import "ts-dedent";
|
||||||
|
import "dayjs";
|
||||||
|
import "@braintree/sanitize-url";
|
||||||
|
import "dompurify";
|
||||||
|
import "khroma";
|
||||||
|
import "lodash-es/memoize.js";
|
||||||
|
import "lodash-es/merge.js";
|
||||||
|
import "stylis";
|
||||||
|
import "lodash-es/isEmpty.js";
|
||||||
|
import "dagre-d3-es/src/dagre/index.js";
|
||||||
|
import "dagre-d3-es/src/graphlib/json.js";
|
||||||
|
import "./edges-f15a7e05.js";
|
||||||
|
import "./createText-80c3befb.js";
|
||||||
|
import "mdast-util-from-markdown";
|
||||||
|
const sanitizeText = (txt) => common.sanitizeText(txt, getConfig());
|
||||||
|
let conf = {
|
||||||
|
dividerMargin: 10,
|
||||||
|
padding: 5,
|
||||||
|
textHeight: 10,
|
||||||
|
curve: void 0
|
||||||
|
};
|
||||||
|
const addNamespaces = function(namespaces, g, _id, diagObj) {
|
||||||
|
const keys = Object.keys(namespaces);
|
||||||
|
log.info("keys:", keys);
|
||||||
|
log.info(namespaces);
|
||||||
|
keys.forEach(function(id) {
|
||||||
|
var _a, _b;
|
||||||
|
const vertex = namespaces[id];
|
||||||
|
const shape = "rect";
|
||||||
|
const node = {
|
||||||
|
shape,
|
||||||
|
id: vertex.id,
|
||||||
|
domId: vertex.domId,
|
||||||
|
labelText: sanitizeText(vertex.id),
|
||||||
|
labelStyle: "",
|
||||||
|
style: "fill: none; stroke: black",
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((_a = getConfig().flowchart) == null ? void 0 : _a.padding) ?? ((_b = getConfig().class) == null ? void 0 : _b.padding)
|
||||||
|
};
|
||||||
|
g.setNode(vertex.id, node);
|
||||||
|
addClasses(vertex.classes, g, _id, diagObj, vertex.id);
|
||||||
|
log.info("setNode", node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const addClasses = function(classes, g, _id, diagObj, parent) {
|
||||||
|
const keys = Object.keys(classes);
|
||||||
|
log.info("keys:", keys);
|
||||||
|
log.info(classes);
|
||||||
|
keys.filter((id) => classes[id].parent == parent).forEach(function(id) {
|
||||||
|
var _a, _b;
|
||||||
|
const vertex = classes[id];
|
||||||
|
const cssClassStr = vertex.cssClasses.join(" ");
|
||||||
|
const styles2 = { labelStyle: "", style: "" };
|
||||||
|
const vertexText = vertex.label ?? vertex.id;
|
||||||
|
const radius = 0;
|
||||||
|
const shape = "class_box";
|
||||||
|
const node = {
|
||||||
|
labelStyle: styles2.labelStyle,
|
||||||
|
shape,
|
||||||
|
labelText: sanitizeText(vertexText),
|
||||||
|
classData: vertex,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
class: cssClassStr,
|
||||||
|
style: styles2.style,
|
||||||
|
id: vertex.id,
|
||||||
|
domId: vertex.domId,
|
||||||
|
tooltip: diagObj.db.getTooltip(vertex.id, parent) || "",
|
||||||
|
haveCallback: vertex.haveCallback,
|
||||||
|
link: vertex.link,
|
||||||
|
width: vertex.type === "group" ? 500 : void 0,
|
||||||
|
type: vertex.type,
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((_a = getConfig().flowchart) == null ? void 0 : _a.padding) ?? ((_b = getConfig().class) == null ? void 0 : _b.padding)
|
||||||
|
};
|
||||||
|
g.setNode(vertex.id, node);
|
||||||
|
if (parent) {
|
||||||
|
g.setParent(vertex.id, parent);
|
||||||
|
}
|
||||||
|
log.info("setNode", node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const addNotes = function(notes, g, startEdgeId, classes) {
|
||||||
|
log.info(notes);
|
||||||
|
notes.forEach(function(note, i) {
|
||||||
|
var _a, _b;
|
||||||
|
const vertex = note;
|
||||||
|
const cssNoteStr = "";
|
||||||
|
const styles2 = { labelStyle: "", style: "" };
|
||||||
|
const vertexText = vertex.text;
|
||||||
|
const radius = 0;
|
||||||
|
const shape = "note";
|
||||||
|
const node = {
|
||||||
|
labelStyle: styles2.labelStyle,
|
||||||
|
shape,
|
||||||
|
labelText: sanitizeText(vertexText),
|
||||||
|
noteData: vertex,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
class: cssNoteStr,
|
||||||
|
style: styles2.style,
|
||||||
|
id: vertex.id,
|
||||||
|
domId: vertex.id,
|
||||||
|
tooltip: "",
|
||||||
|
type: "note",
|
||||||
|
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||||
|
padding: ((_a = getConfig().flowchart) == null ? void 0 : _a.padding) ?? ((_b = getConfig().class) == null ? void 0 : _b.padding)
|
||||||
|
};
|
||||||
|
g.setNode(vertex.id, node);
|
||||||
|
log.info("setNode", node);
|
||||||
|
if (!vertex.class || !(vertex.class in classes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const edgeId = startEdgeId + i;
|
||||||
|
const edgeData = {
|
||||||
|
id: `edgeNote${edgeId}`,
|
||||||
|
//Set relationship style and line type
|
||||||
|
classes: "relation",
|
||||||
|
pattern: "dotted",
|
||||||
|
// Set link type for rendering
|
||||||
|
arrowhead: "none",
|
||||||
|
//Set edge extra labels
|
||||||
|
startLabelRight: "",
|
||||||
|
endLabelLeft: "",
|
||||||
|
//Set relation arrow types
|
||||||
|
arrowTypeStart: "none",
|
||||||
|
arrowTypeEnd: "none",
|
||||||
|
style: "fill:none",
|
||||||
|
labelStyle: "",
|
||||||
|
curve: interpolateToCurve(conf.curve, curveLinear)
|
||||||
|
};
|
||||||
|
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const addRelations = function(relations, g) {
|
||||||
|
const conf2 = getConfig().flowchart;
|
||||||
|
let cnt = 0;
|
||||||
|
relations.forEach(function(edge) {
|
||||||
|
var _a;
|
||||||
|
cnt++;
|
||||||
|
const edgeData = {
|
||||||
|
//Set relationship style and line type
|
||||||
|
classes: "relation",
|
||||||
|
pattern: edge.relation.lineType == 1 ? "dashed" : "solid",
|
||||||
|
id: "id" + cnt,
|
||||||
|
// Set link type for rendering
|
||||||
|
arrowhead: edge.type === "arrow_open" ? "none" : "normal",
|
||||||
|
//Set edge extra labels
|
||||||
|
startLabelRight: edge.relationTitle1 === "none" ? "" : edge.relationTitle1,
|
||||||
|
endLabelLeft: edge.relationTitle2 === "none" ? "" : edge.relationTitle2,
|
||||||
|
//Set relation arrow types
|
||||||
|
arrowTypeStart: getArrowMarker(edge.relation.type1),
|
||||||
|
arrowTypeEnd: getArrowMarker(edge.relation.type2),
|
||||||
|
style: "fill:none",
|
||||||
|
labelStyle: "",
|
||||||
|
curve: interpolateToCurve(conf2 == null ? void 0 : conf2.curve, curveLinear)
|
||||||
|
};
|
||||||
|
log.info(edgeData, edge);
|
||||||
|
if (edge.style !== void 0) {
|
||||||
|
const styles2 = getStylesFromArray(edge.style);
|
||||||
|
edgeData.style = styles2.style;
|
||||||
|
edgeData.labelStyle = styles2.labelStyle;
|
||||||
|
}
|
||||||
|
edge.text = edge.title;
|
||||||
|
if (edge.text === void 0) {
|
||||||
|
if (edge.style !== void 0) {
|
||||||
|
edgeData.arrowheadStyle = "fill: #333";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edgeData.arrowheadStyle = "fill: #333";
|
||||||
|
edgeData.labelpos = "c";
|
||||||
|
if (((_a = getConfig().flowchart) == null ? void 0 : _a.htmlLabels) ?? getConfig().htmlLabels) {
|
||||||
|
edgeData.labelType = "html";
|
||||||
|
edgeData.label = '<span class="edgeLabel">' + edge.text + "</span>";
|
||||||
|
} else {
|
||||||
|
edgeData.labelType = "text";
|
||||||
|
edgeData.label = edge.text.replace(common.lineBreakRegex, "\n");
|
||||||
|
if (edge.style === void 0) {
|
||||||
|
edgeData.style = edgeData.style || "stroke: #333; stroke-width: 1.5px;fill:none";
|
||||||
|
}
|
||||||
|
edgeData.labelStyle = edgeData.labelStyle.replace("color:", "fill:");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.setEdge(edge.id1, edge.id2, edgeData, cnt);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const setConf = function(cnf) {
|
||||||
|
conf = {
|
||||||
|
...conf,
|
||||||
|
...cnf
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const draw = async function(text, id, _version, diagObj) {
|
||||||
|
log.info("Drawing class - ", id);
|
||||||
|
const conf2 = getConfig().flowchart ?? getConfig().class;
|
||||||
|
const securityLevel = getConfig().securityLevel;
|
||||||
|
log.info("config:", conf2);
|
||||||
|
const nodeSpacing = (conf2 == null ? void 0 : conf2.nodeSpacing) ?? 50;
|
||||||
|
const rankSpacing = (conf2 == null ? void 0 : conf2.rankSpacing) ?? 50;
|
||||||
|
const g = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true
|
||||||
|
}).setGraph({
|
||||||
|
rankdir: diagObj.db.getDirection(),
|
||||||
|
nodesep: nodeSpacing,
|
||||||
|
ranksep: rankSpacing,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8
|
||||||
|
}).setDefaultEdgeLabel(function() {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const namespaces = diagObj.db.getNamespaces();
|
||||||
|
const classes = diagObj.db.getClasses();
|
||||||
|
const relations = diagObj.db.getRelations();
|
||||||
|
const notes = diagObj.db.getNotes();
|
||||||
|
log.info(relations);
|
||||||
|
addNamespaces(namespaces, g, id, diagObj);
|
||||||
|
addClasses(classes, g, id, diagObj);
|
||||||
|
addRelations(relations, g);
|
||||||
|
addNotes(notes, g, relations.length + 1, classes);
|
||||||
|
let sandboxElement;
|
||||||
|
if (securityLevel === "sandbox") {
|
||||||
|
sandboxElement = select("#i" + id);
|
||||||
|
}
|
||||||
|
const root = securityLevel === "sandbox" ? select(sandboxElement.nodes()[0].contentDocument.body) : select("body");
|
||||||
|
const svg = root.select(`[id="${id}"]`);
|
||||||
|
const element = root.select("#" + id + " g");
|
||||||
|
await render(
|
||||||
|
element,
|
||||||
|
g,
|
||||||
|
["aggregation", "extension", "composition", "dependency", "lollipop"],
|
||||||
|
"classDiagram",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
utils.insertTitle(svg, "classTitleText", (conf2 == null ? void 0 : conf2.titleTopMargin) ?? 5, diagObj.db.getDiagramTitle());
|
||||||
|
setupGraphViewbox(g, svg, conf2 == null ? void 0 : conf2.diagramPadding, conf2 == null ? void 0 : conf2.useMaxWidth);
|
||||||
|
if (!(conf2 == null ? void 0 : conf2.htmlLabels)) {
|
||||||
|
const doc = securityLevel === "sandbox" ? sandboxElement.nodes()[0].contentDocument : document;
|
||||||
|
const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||||
|
for (const label of labels) {
|
||||||
|
const dim = label.getBBox();
|
||||||
|
const rect = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||||
|
rect.setAttribute("rx", 0);
|
||||||
|
rect.setAttribute("ry", 0);
|
||||||
|
rect.setAttribute("width", dim.width);
|
||||||
|
rect.setAttribute("height", dim.height);
|
||||||
|
label.insertBefore(rect, label.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function getArrowMarker(type) {
|
||||||
|
let marker;
|
||||||
|
switch (type) {
|
||||||
|
case 0:
|
||||||
|
marker = "aggregation";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
marker = "extension";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
marker = "composition";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
marker = "dependency";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
marker = "lollipop";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
marker = "none";
|
||||||
|
}
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
const renderer = {
|
||||||
|
setConf,
|
||||||
|
draw
|
||||||
|
};
|
||||||
|
const diagram = {
|
||||||
|
parser,
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
styles,
|
||||||
|
init: (cnf) => {
|
||||||
|
if (!cnf.class) {
|
||||||
|
cnf.class = {};
|
||||||
|
}
|
||||||
|
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||||
|
db.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
diagram
|
||||||
|
};
|
|
@ -0,0 +1,98 @@
|
||||||
|
import type { MermaidConfig } from './config.type.js';
|
||||||
|
export declare const defaultConfig: MermaidConfig;
|
||||||
|
export declare const updateCurrentConfig: (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => MermaidConfig;
|
||||||
|
/**
|
||||||
|
* ## setSiteConfig
|
||||||
|
*
|
||||||
|
* | Function | Description | Type | Values |
|
||||||
|
* | ------------- | ------------------------------------- | ----------- | --------------------------------------- |
|
||||||
|
* | setSiteConfig | Sets the siteConfig to desired values | Put Request | Any Values, except ones in secure array |
|
||||||
|
*
|
||||||
|
* **Notes:** Sets the siteConfig. The siteConfig is a protected configuration for repeat use. Calls
|
||||||
|
* to reset() will reset the currentConfig to siteConfig. Calls to reset(configApi.defaultConfig)
|
||||||
|
* will reset siteConfig and currentConfig to the defaultConfig Note: currentConfig is set in this
|
||||||
|
* function _Default value: At default, will mirror Global Config_
|
||||||
|
*
|
||||||
|
* @param conf - The base currentConfig to use as siteConfig
|
||||||
|
* @returns The new siteConfig
|
||||||
|
*/
|
||||||
|
export declare const setSiteConfig: (conf: MermaidConfig) => MermaidConfig;
|
||||||
|
export declare const saveConfigFromInitialize: (conf: MermaidConfig) => void;
|
||||||
|
export declare const updateSiteConfig: (conf: MermaidConfig) => MermaidConfig;
|
||||||
|
/**
|
||||||
|
* ## getSiteConfig
|
||||||
|
*
|
||||||
|
* | Function | Description | Type | Values |
|
||||||
|
* | ------------- | ------------------------------------------------- | ----------- | -------------------------------- |
|
||||||
|
* | setSiteConfig | Returns the current siteConfig base configuration | Get Request | Returns Any Values in siteConfig |
|
||||||
|
*
|
||||||
|
* **Notes**: Returns **any** values in siteConfig.
|
||||||
|
*
|
||||||
|
* @returns The siteConfig
|
||||||
|
*/
|
||||||
|
export declare const getSiteConfig: () => MermaidConfig;
|
||||||
|
/**
|
||||||
|
* ## setConfig
|
||||||
|
*
|
||||||
|
* | Function | Description | Type | Values |
|
||||||
|
* | ------------- | ------------------------------------- | ----------- | --------------------------------------- |
|
||||||
|
* | setSiteConfig | Sets the siteConfig to desired values | Put Request | Any Values, except ones in secure array |
|
||||||
|
*
|
||||||
|
* **Notes**: Sets the currentConfig. The parameter conf is sanitized based on the siteConfig.secure
|
||||||
|
* keys. Any values found in conf with key found in siteConfig.secure will be replaced with the
|
||||||
|
* corresponding siteConfig value.
|
||||||
|
*
|
||||||
|
* @param conf - The potential currentConfig
|
||||||
|
* @returns The currentConfig merged with the sanitized conf
|
||||||
|
*/
|
||||||
|
export declare const setConfig: (conf: MermaidConfig) => MermaidConfig;
|
||||||
|
/**
|
||||||
|
* ## getConfig
|
||||||
|
*
|
||||||
|
* | Function | Description | Type | Return Values |
|
||||||
|
* | --------- | ------------------------- | ----------- | ------------------------------ |
|
||||||
|
* | getConfig | Obtains the currentConfig | Get Request | Any Values from current Config |
|
||||||
|
*
|
||||||
|
* **Notes**: Returns **any** the currentConfig
|
||||||
|
*
|
||||||
|
* @returns The currentConfig
|
||||||
|
*/
|
||||||
|
export declare const getConfig: () => MermaidConfig;
|
||||||
|
/**
|
||||||
|
* ## sanitize
|
||||||
|
*
|
||||||
|
* | Function | Description | Type | Values |
|
||||||
|
* | -------- | -------------------------------------- | ----------- | ------ |
|
||||||
|
* | sanitize | Sets the siteConfig to desired values. | Put Request | None |
|
||||||
|
*
|
||||||
|
* Ensures options parameter does not attempt to override siteConfig secure keys **Notes**: modifies
|
||||||
|
* options in-place
|
||||||
|
*
|
||||||
|
* @param options - The potential setConfig parameter
|
||||||
|
*/
|
||||||
|
export declare const sanitize: (options: any) => void;
|
||||||
|
/**
|
||||||
|
* Pushes in a directive to the configuration
|
||||||
|
*
|
||||||
|
* @param directive - The directive to push in
|
||||||
|
*/
|
||||||
|
export declare const addDirective: (directive: MermaidConfig) => void;
|
||||||
|
/**
|
||||||
|
* ## reset
|
||||||
|
*
|
||||||
|
* | Function | Description | Type | Required | Values |
|
||||||
|
* | -------- | ---------------------------- | ----------- | -------- | ------ |
|
||||||
|
* | reset | Resets currentConfig to conf | Put Request | Required | None |
|
||||||
|
*
|
||||||
|
* ## conf
|
||||||
|
*
|
||||||
|
* | Parameter | Description | Type | Required | Values |
|
||||||
|
* | --------- | -------------------------------------------------------------- | ---------- | -------- | -------------------------------------------- |
|
||||||
|
* | conf | base set of values, which currentConfig could be **reset** to. | Dictionary | Required | Any Values, with respect to the secure Array |
|
||||||
|
*
|
||||||
|
* **Notes**: (default: current siteConfig ) (optional, default `getSiteConfig()`)
|
||||||
|
*
|
||||||
|
* @param config - base set of values, which currentConfig could be **reset** to.
|
||||||
|
* Defaults to the current siteConfig (e.g returned by {@link getSiteConfig}).
|
||||||
|
*/
|
||||||
|
export declare const reset: (config?: MermaidConfig) => void;
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,63 @@
|
||||||
|
const p = Math.PI, v = 2 * p, c = 1e-6, q = v - c;
|
||||||
|
function y() {
|
||||||
|
this._x0 = this._y0 = // start of current subpath
|
||||||
|
this._x1 = this._y1 = null, this._ = "";
|
||||||
|
}
|
||||||
|
function C() {
|
||||||
|
return new y();
|
||||||
|
}
|
||||||
|
y.prototype = C.prototype = {
|
||||||
|
constructor: y,
|
||||||
|
moveTo: function(i, s) {
|
||||||
|
this._ += "M" + (this._x0 = this._x1 = +i) + "," + (this._y0 = this._y1 = +s);
|
||||||
|
},
|
||||||
|
closePath: function() {
|
||||||
|
this._x1 !== null && (this._x1 = this._x0, this._y1 = this._y0, this._ += "Z");
|
||||||
|
},
|
||||||
|
lineTo: function(i, s) {
|
||||||
|
this._ += "L" + (this._x1 = +i) + "," + (this._y1 = +s);
|
||||||
|
},
|
||||||
|
quadraticCurveTo: function(i, s, t, h) {
|
||||||
|
this._ += "Q" + +i + "," + +s + "," + (this._x1 = +t) + "," + (this._y1 = +h);
|
||||||
|
},
|
||||||
|
bezierCurveTo: function(i, s, t, h, _, e) {
|
||||||
|
this._ += "C" + +i + "," + +s + "," + +t + "," + +h + "," + (this._x1 = +_) + "," + (this._y1 = +e);
|
||||||
|
},
|
||||||
|
arcTo: function(i, s, t, h, _) {
|
||||||
|
i = +i, s = +s, t = +t, h = +h, _ = +_;
|
||||||
|
var e = this._x1, r = this._y1, f = t - i, a = h - s, n = e - i, u = r - s, o = n * n + u * u;
|
||||||
|
if (_ < 0)
|
||||||
|
throw new Error("negative radius: " + _);
|
||||||
|
if (this._x1 === null)
|
||||||
|
this._ += "M" + (this._x1 = i) + "," + (this._y1 = s);
|
||||||
|
else if (o > c)
|
||||||
|
if (!(Math.abs(u * f - a * n) > c) || !_)
|
||||||
|
this._ += "L" + (this._x1 = i) + "," + (this._y1 = s);
|
||||||
|
else {
|
||||||
|
var M = t - e, l = h - r, d = f * f + a * a, g = M * M + l * l, b = Math.sqrt(d), T = Math.sqrt(o), A = _ * Math.tan((p - Math.acos((d + o - g) / (2 * b * T))) / 2), x = A / T, L = A / b;
|
||||||
|
Math.abs(x - 1) > c && (this._ += "L" + (i + x * n) + "," + (s + x * u)), this._ += "A" + _ + "," + _ + ",0,0," + +(u * M > n * l) + "," + (this._x1 = i + L * f) + "," + (this._y1 = s + L * a);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
arc: function(i, s, t, h, _, e) {
|
||||||
|
i = +i, s = +s, t = +t, e = !!e;
|
||||||
|
var r = t * Math.cos(h), f = t * Math.sin(h), a = i + r, n = s + f, u = 1 ^ e, o = e ? h - _ : _ - h;
|
||||||
|
if (t < 0)
|
||||||
|
throw new Error("negative radius: " + t);
|
||||||
|
this._x1 === null ? this._ += "M" + a + "," + n : (Math.abs(this._x1 - a) > c || Math.abs(this._y1 - n) > c) && (this._ += "L" + a + "," + n), t && (o < 0 && (o = o % v + v), o > q ? this._ += "A" + t + "," + t + ",0,1," + u + "," + (i - r) + "," + (s - f) + "A" + t + "," + t + ",0,1," + u + "," + (this._x1 = a) + "," + (this._y1 = n) : o > c && (this._ += "A" + t + "," + t + ",0," + +(o >= p) + "," + u + "," + (this._x1 = i + t * Math.cos(_)) + "," + (this._y1 = s + t * Math.sin(_))));
|
||||||
|
},
|
||||||
|
rect: function(i, s, t, h) {
|
||||||
|
this._ += "M" + (this._x0 = this._x1 = +i) + "," + (this._y0 = this._y1 = +s) + "h" + +t + "v" + +h + "h" + -t + "Z";
|
||||||
|
},
|
||||||
|
toString: function() {
|
||||||
|
return this._;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function E(i) {
|
||||||
|
return function() {
|
||||||
|
return i;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
E as c,
|
||||||
|
C as p
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
const pi = Math.PI, tau = 2 * pi, epsilon = 1e-6, tauEpsilon = tau - epsilon;
|
||||||
|
function Path() {
|
||||||
|
this._x0 = this._y0 = // start of current subpath
|
||||||
|
this._x1 = this._y1 = null;
|
||||||
|
this._ = "";
|
||||||
|
}
|
||||||
|
function path() {
|
||||||
|
return new Path();
|
||||||
|
}
|
||||||
|
Path.prototype = path.prototype = {
|
||||||
|
constructor: Path,
|
||||||
|
moveTo: function(x, y) {
|
||||||
|
this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y);
|
||||||
|
},
|
||||||
|
closePath: function() {
|
||||||
|
if (this._x1 !== null) {
|
||||||
|
this._x1 = this._x0, this._y1 = this._y0;
|
||||||
|
this._ += "Z";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lineTo: function(x, y) {
|
||||||
|
this._ += "L" + (this._x1 = +x) + "," + (this._y1 = +y);
|
||||||
|
},
|
||||||
|
quadraticCurveTo: function(x1, y1, x, y) {
|
||||||
|
this._ += "Q" + +x1 + "," + +y1 + "," + (this._x1 = +x) + "," + (this._y1 = +y);
|
||||||
|
},
|
||||||
|
bezierCurveTo: function(x1, y1, x2, y2, x, y) {
|
||||||
|
this._ += "C" + +x1 + "," + +y1 + "," + +x2 + "," + +y2 + "," + (this._x1 = +x) + "," + (this._y1 = +y);
|
||||||
|
},
|
||||||
|
arcTo: function(x1, y1, x2, y2, r) {
|
||||||
|
x1 = +x1, y1 = +y1, x2 = +x2, y2 = +y2, r = +r;
|
||||||
|
var x0 = this._x1, y0 = this._y1, x21 = x2 - x1, y21 = y2 - y1, x01 = x0 - x1, y01 = y0 - y1, l01_2 = x01 * x01 + y01 * y01;
|
||||||
|
if (r < 0)
|
||||||
|
throw new Error("negative radius: " + r);
|
||||||
|
if (this._x1 === null) {
|
||||||
|
this._ += "M" + (this._x1 = x1) + "," + (this._y1 = y1);
|
||||||
|
} else if (!(l01_2 > epsilon))
|
||||||
|
;
|
||||||
|
else if (!(Math.abs(y01 * x21 - y21 * x01) > epsilon) || !r) {
|
||||||
|
this._ += "L" + (this._x1 = x1) + "," + (this._y1 = y1);
|
||||||
|
} else {
|
||||||
|
var x20 = x2 - x0, y20 = y2 - y0, l21_2 = x21 * x21 + y21 * y21, l20_2 = x20 * x20 + y20 * y20, l21 = Math.sqrt(l21_2), l01 = Math.sqrt(l01_2), l = r * Math.tan((pi - Math.acos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2), t01 = l / l01, t21 = l / l21;
|
||||||
|
if (Math.abs(t01 - 1) > epsilon) {
|
||||||
|
this._ += "L" + (x1 + t01 * x01) + "," + (y1 + t01 * y01);
|
||||||
|
}
|
||||||
|
this._ += "A" + r + "," + r + ",0,0," + +(y01 * x20 > x01 * y20) + "," + (this._x1 = x1 + t21 * x21) + "," + (this._y1 = y1 + t21 * y21);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
arc: function(x, y, r, a0, a1, ccw) {
|
||||||
|
x = +x, y = +y, r = +r, ccw = !!ccw;
|
||||||
|
var dx = r * Math.cos(a0), dy = r * Math.sin(a0), x0 = x + dx, y0 = y + dy, cw = 1 ^ ccw, da = ccw ? a0 - a1 : a1 - a0;
|
||||||
|
if (r < 0)
|
||||||
|
throw new Error("negative radius: " + r);
|
||||||
|
if (this._x1 === null) {
|
||||||
|
this._ += "M" + x0 + "," + y0;
|
||||||
|
} else if (Math.abs(this._x1 - x0) > epsilon || Math.abs(this._y1 - y0) > epsilon) {
|
||||||
|
this._ += "L" + x0 + "," + y0;
|
||||||
|
}
|
||||||
|
if (!r)
|
||||||
|
return;
|
||||||
|
if (da < 0)
|
||||||
|
da = da % tau + tau;
|
||||||
|
if (da > tauEpsilon) {
|
||||||
|
this._ += "A" + r + "," + r + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + r + "," + r + ",0,1," + cw + "," + (this._x1 = x0) + "," + (this._y1 = y0);
|
||||||
|
} else if (da > epsilon) {
|
||||||
|
this._ += "A" + r + "," + r + ",0," + +(da >= pi) + "," + cw + "," + (this._x1 = x + r * Math.cos(a1)) + "," + (this._y1 = y + r * Math.sin(a1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rect: function(x, y, w, h) {
|
||||||
|
this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y) + "h" + +w + "v" + +h + "h" + -w + "Z";
|
||||||
|
},
|
||||||
|
toString: function() {
|
||||||
|
return this._;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function constant(x) {
|
||||||
|
return function constant2() {
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
constant as c,
|
||||||
|
path as p
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,238 @@
|
||||||
|
import { l as log, J as decodeEntities } from "./mermaid-0d192ec3.js";
|
||||||
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||||
|
import { dedent } from "ts-dedent";
|
||||||
|
function preprocessMarkdown(markdown) {
|
||||||
|
const withoutMultipleNewlines = markdown.replace(/\n{2,}/g, "\n");
|
||||||
|
const withoutExtraSpaces = dedent(withoutMultipleNewlines);
|
||||||
|
return withoutExtraSpaces;
|
||||||
|
}
|
||||||
|
function markdownToLines(markdown) {
|
||||||
|
const preprocessedMarkdown = preprocessMarkdown(markdown);
|
||||||
|
const { children } = fromMarkdown(preprocessedMarkdown);
|
||||||
|
const lines = [[]];
|
||||||
|
let currentLine = 0;
|
||||||
|
function processNode(node, parentType = "normal") {
|
||||||
|
if (node.type === "text") {
|
||||||
|
const textLines = node.value.split("\n");
|
||||||
|
textLines.forEach((textLine, index) => {
|
||||||
|
if (index !== 0) {
|
||||||
|
currentLine++;
|
||||||
|
lines.push([]);
|
||||||
|
}
|
||||||
|
textLine.split(" ").forEach((word) => {
|
||||||
|
if (word) {
|
||||||
|
lines[currentLine].push({ content: word, type: parentType });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (node.type === "strong" || node.type === "emphasis") {
|
||||||
|
node.children.forEach((contentNode) => {
|
||||||
|
processNode(contentNode, node.type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.forEach((treeNode) => {
|
||||||
|
if (treeNode.type === "paragraph") {
|
||||||
|
treeNode.children.forEach((contentNode) => {
|
||||||
|
processNode(contentNode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
function markdownToHTML(markdown) {
|
||||||
|
const { children } = fromMarkdown(markdown);
|
||||||
|
function output(node) {
|
||||||
|
if (node.type === "text") {
|
||||||
|
return node.value.replace(/\n/g, "<br/>");
|
||||||
|
} else if (node.type === "strong") {
|
||||||
|
return `<strong>${node.children.map(output).join("")}</strong>`;
|
||||||
|
} else if (node.type === "emphasis") {
|
||||||
|
return `<em>${node.children.map(output).join("")}</em>`;
|
||||||
|
} else if (node.type === "paragraph") {
|
||||||
|
return `<p>${node.children.map(output).join("")}</p>`;
|
||||||
|
}
|
||||||
|
return `Unsupported markdown: ${node.type}`;
|
||||||
|
}
|
||||||
|
return children.map(output).join("");
|
||||||
|
}
|
||||||
|
function splitTextToChars(text) {
|
||||||
|
if (Intl.Segmenter) {
|
||||||
|
return [...new Intl.Segmenter().segment(text)].map((s) => s.segment);
|
||||||
|
}
|
||||||
|
return [...text];
|
||||||
|
}
|
||||||
|
function splitWordToFitWidth(checkFit, word) {
|
||||||
|
const characters = splitTextToChars(word.content);
|
||||||
|
return splitWordToFitWidthRecursion(checkFit, [], characters, word.type);
|
||||||
|
}
|
||||||
|
function splitWordToFitWidthRecursion(checkFit, usedChars, remainingChars, type) {
|
||||||
|
if (remainingChars.length === 0) {
|
||||||
|
return [
|
||||||
|
{ content: usedChars.join(""), type },
|
||||||
|
{ content: "", type }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const [nextChar, ...rest] = remainingChars;
|
||||||
|
const newWord = [...usedChars, nextChar];
|
||||||
|
if (checkFit([{ content: newWord.join(""), type }])) {
|
||||||
|
return splitWordToFitWidthRecursion(checkFit, newWord, rest, type);
|
||||||
|
}
|
||||||
|
if (usedChars.length === 0 && nextChar) {
|
||||||
|
usedChars.push(nextChar);
|
||||||
|
remainingChars.shift();
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ content: usedChars.join(""), type },
|
||||||
|
{ content: remainingChars.join(""), type }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function splitLineToFitWidth(line, checkFit) {
|
||||||
|
if (line.some(({ content }) => content.includes("\n"))) {
|
||||||
|
throw new Error("splitLineToFitWidth does not support newlines in the line");
|
||||||
|
}
|
||||||
|
return splitLineToFitWidthRecursion(line, checkFit);
|
||||||
|
}
|
||||||
|
function splitLineToFitWidthRecursion(words, checkFit, lines = [], newLine = []) {
|
||||||
|
if (words.length === 0) {
|
||||||
|
if (newLine.length > 0) {
|
||||||
|
lines.push(newLine);
|
||||||
|
}
|
||||||
|
return lines.length > 0 ? lines : [];
|
||||||
|
}
|
||||||
|
let joiner = "";
|
||||||
|
if (words[0].content === " ") {
|
||||||
|
joiner = " ";
|
||||||
|
words.shift();
|
||||||
|
}
|
||||||
|
const nextWord = words.shift() ?? { content: " ", type: "normal" };
|
||||||
|
const lineWithNextWord = [...newLine];
|
||||||
|
if (joiner !== "") {
|
||||||
|
lineWithNextWord.push({ content: joiner, type: "normal" });
|
||||||
|
}
|
||||||
|
lineWithNextWord.push(nextWord);
|
||||||
|
if (checkFit(lineWithNextWord)) {
|
||||||
|
return splitLineToFitWidthRecursion(words, checkFit, lines, lineWithNextWord);
|
||||||
|
}
|
||||||
|
if (newLine.length > 0) {
|
||||||
|
lines.push(newLine);
|
||||||
|
words.unshift(nextWord);
|
||||||
|
} else if (nextWord.content) {
|
||||||
|
const [line, rest] = splitWordToFitWidth(checkFit, nextWord);
|
||||||
|
lines.push([line]);
|
||||||
|
if (rest.content) {
|
||||||
|
words.unshift(rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return splitLineToFitWidthRecursion(words, checkFit, lines);
|
||||||
|
}
|
||||||
|
function applyStyle(dom, styleFn) {
|
||||||
|
if (styleFn) {
|
||||||
|
dom.attr("style", styleFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function addHtmlSpan(element, node, width, classes, addBackground = false) {
|
||||||
|
const fo = element.append("foreignObject");
|
||||||
|
const div = fo.append("xhtml:div");
|
||||||
|
const label = node.label;
|
||||||
|
const labelClass = node.isNode ? "nodeLabel" : "edgeLabel";
|
||||||
|
div.html(
|
||||||
|
`
|
||||||
|
<span class="${labelClass} ${classes}" ` + (node.labelStyle ? 'style="' + node.labelStyle + '"' : "") + ">" + label + "</span>"
|
||||||
|
);
|
||||||
|
applyStyle(div, node.labelStyle);
|
||||||
|
div.style("display", "table-cell");
|
||||||
|
div.style("white-space", "nowrap");
|
||||||
|
div.style("max-width", width + "px");
|
||||||
|
div.attr("xmlns", "http://www.w3.org/1999/xhtml");
|
||||||
|
if (addBackground) {
|
||||||
|
div.attr("class", "labelBkg");
|
||||||
|
}
|
||||||
|
let bbox = div.node().getBoundingClientRect();
|
||||||
|
if (bbox.width === width) {
|
||||||
|
div.style("display", "table");
|
||||||
|
div.style("white-space", "break-spaces");
|
||||||
|
div.style("width", width + "px");
|
||||||
|
bbox = div.node().getBoundingClientRect();
|
||||||
|
}
|
||||||
|
fo.style("width", bbox.width);
|
||||||
|
fo.style("height", bbox.height);
|
||||||
|
return fo.node();
|
||||||
|
}
|
||||||
|
function createTspan(textElement, lineIndex, lineHeight) {
|
||||||
|
return textElement.append("tspan").attr("class", "text-outer-tspan").attr("x", 0).attr("y", lineIndex * lineHeight - 0.1 + "em").attr("dy", lineHeight + "em");
|
||||||
|
}
|
||||||
|
function computeWidthOfText(parentNode, lineHeight, line) {
|
||||||
|
const testElement = parentNode.append("text");
|
||||||
|
const testSpan = createTspan(testElement, 1, lineHeight);
|
||||||
|
updateTextContentAndStyles(testSpan, line);
|
||||||
|
const textLength = testSpan.node().getComputedTextLength();
|
||||||
|
testElement.remove();
|
||||||
|
return textLength;
|
||||||
|
}
|
||||||
|
function createFormattedText(width, g, structuredText, addBackground = false) {
|
||||||
|
const lineHeight = 1.1;
|
||||||
|
const labelGroup = g.append("g");
|
||||||
|
const bkg = labelGroup.insert("rect").attr("class", "background");
|
||||||
|
const textElement = labelGroup.append("text").attr("y", "-10.1");
|
||||||
|
let lineIndex = 0;
|
||||||
|
for (const line of structuredText) {
|
||||||
|
const checkWidth = (line2) => computeWidthOfText(labelGroup, lineHeight, line2) <= width;
|
||||||
|
const linesUnderWidth = checkWidth(line) ? [line] : splitLineToFitWidth(line, checkWidth);
|
||||||
|
for (const preparedLine of linesUnderWidth) {
|
||||||
|
const tspan = createTspan(textElement, lineIndex, lineHeight);
|
||||||
|
updateTextContentAndStyles(tspan, preparedLine);
|
||||||
|
lineIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addBackground) {
|
||||||
|
const bbox = textElement.node().getBBox();
|
||||||
|
const padding = 2;
|
||||||
|
bkg.attr("x", -padding).attr("y", -padding).attr("width", bbox.width + 2 * padding).attr("height", bbox.height + 2 * padding);
|
||||||
|
return labelGroup.node();
|
||||||
|
} else {
|
||||||
|
return textElement.node();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateTextContentAndStyles(tspan, wrappedLine) {
|
||||||
|
tspan.text("");
|
||||||
|
wrappedLine.forEach((word, index) => {
|
||||||
|
const innerTspan = tspan.append("tspan").attr("font-style", word.type === "emphasis" ? "italic" : "normal").attr("class", "text-inner-tspan").attr("font-weight", word.type === "strong" ? "bold" : "normal");
|
||||||
|
if (index === 0) {
|
||||||
|
innerTspan.text(word.content);
|
||||||
|
} else {
|
||||||
|
innerTspan.text(" " + word.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const createText = (el, text = "", {
|
||||||
|
style = "",
|
||||||
|
isTitle = false,
|
||||||
|
classes = "",
|
||||||
|
useHtmlLabels = true,
|
||||||
|
isNode = true,
|
||||||
|
width = 200,
|
||||||
|
addSvgBackground = false
|
||||||
|
} = {}) => {
|
||||||
|
log.info("createText", text, style, isTitle, classes, useHtmlLabels, isNode, addSvgBackground);
|
||||||
|
if (useHtmlLabels) {
|
||||||
|
const htmlText = markdownToHTML(text);
|
||||||
|
const node = {
|
||||||
|
isNode,
|
||||||
|
label: decodeEntities(htmlText).replace(
|
||||||
|
/fa[blrs]?:fa-[\w-]+/g,
|
||||||
|
(s) => `<i class='${s.replace(":", " ")}'></i>`
|
||||||
|
),
|
||||||
|
labelStyle: style.replace("fill:", "color:")
|
||||||
|
};
|
||||||
|
const vertexNode = addHtmlSpan(el, node, width, classes, addSvgBackground);
|
||||||
|
return vertexNode;
|
||||||
|
} else {
|
||||||
|
const structuredText = markdownToLines(text);
|
||||||
|
const svgLabel = createFormattedText(width, el, structuredText, addSvgBackground);
|
||||||
|
return svgLabel;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
createText as c
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
export function insertCluster(elem: any, node: any): void;
|
||||||
|
export function getClusterTitleWidth(elem: any, node: any): number;
|
||||||
|
export function clear(): void;
|
||||||
|
export function positionCluster(node: any): void;
|
9
webroot/js/node_modules/mermaid/dist/dagre-wrapper/createLabel.d.ts
generated
vendored
Normal file
9
webroot/js/node_modules/mermaid/dist/dagre-wrapper/createLabel.d.ts
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export default createLabel;
|
||||||
|
/**
|
||||||
|
* @param _vertexText
|
||||||
|
* @param style
|
||||||
|
* @param isTitle
|
||||||
|
* @param isNode
|
||||||
|
* @deprecated svg-util/createText instead
|
||||||
|
*/
|
||||||
|
declare function createLabel(_vertexText: any, style: any, isTitle: any, isNode: any): SVGTextElement | SVGForeignObjectElement;
|
|
@ -0,0 +1,14 @@
|
||||||
|
export function clear(): void;
|
||||||
|
export function insertEdgeLabel(elem: any, edge: any): any;
|
||||||
|
export function positionEdgeLabel(edge: any, paths: any): void;
|
||||||
|
export function intersection(node: any, outsidePoint: any, insidePoint: any): {
|
||||||
|
x: any;
|
||||||
|
y: number;
|
||||||
|
} | {
|
||||||
|
x: number;
|
||||||
|
y: any;
|
||||||
|
};
|
||||||
|
export function insertEdge(elem: any, e: any, edge: any, clusterDb: any, diagramType: any, graph: any, id: any): {
|
||||||
|
updatedPath: any;
|
||||||
|
originalPath: any;
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export function render(elem: any, graph: any, markers: any, diagramtype: any, id: any): Promise<void>;
|
13
webroot/js/node_modules/mermaid/dist/dagre-wrapper/intersect/index.d.ts
generated
vendored
Normal file
13
webroot/js/node_modules/mermaid/dist/dagre-wrapper/intersect/index.d.ts
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
declare namespace _default {
|
||||||
|
export { node };
|
||||||
|
export { circle };
|
||||||
|
export { ellipse };
|
||||||
|
export { polygon };
|
||||||
|
export { rect };
|
||||||
|
}
|
||||||
|
export default _default;
|
||||||
|
import node from './intersect-node.js';
|
||||||
|
import circle from './intersect-circle.js';
|
||||||
|
import ellipse from './intersect-ellipse.js';
|
||||||
|
import polygon from './intersect-polygon.js';
|
||||||
|
import rect from './intersect-rect.js';
|
10
webroot/js/node_modules/mermaid/dist/dagre-wrapper/intersect/intersect-circle.d.ts
generated
vendored
Normal file
10
webroot/js/node_modules/mermaid/dist/dagre-wrapper/intersect/intersect-circle.d.ts
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export default intersectCircle;
|
||||||
|
/**
|
||||||
|
* @param node
|
||||||
|
* @param rx
|
||||||
|
* @param point
|
||||||
|
*/
|
||||||
|
declare function intersectCircle(node: any, rx: any, point: any): {
|
||||||
|
x: any;
|
||||||
|
y: any;
|
||||||
|
};
|
11
webroot/js/node_modules/mermaid/dist/dagre-wrapper/intersect/intersect-ellipse.d.ts
generated
vendored
Normal file
11
webroot/js/node_modules/mermaid/dist/dagre-wrapper/intersect/intersect-ellipse.d.ts
generated
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export default intersectEllipse;
|
||||||
|
/**
|
||||||
|
* @param node
|
||||||
|
* @param rx
|
||||||
|
* @param ry
|
||||||
|
* @param point
|
||||||
|
*/
|
||||||
|
declare function intersectEllipse(node: any, rx: any, ry: any, point: any): {
|
||||||
|
x: any;
|
||||||
|
y: any;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue