Merge branch 'develop'

pull/116/merge v1.17
iglocska 2023-11-03 15:47:29 +01:00
commit be64a186e0
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
373 changed files with 490247 additions and 125 deletions

View File

@ -2,7 +2,7 @@ name: test
on:
push:
branches: [main, develop]
branches: [main, develop, fix-test-action]
pull_request:
branches: [main, develop]
@ -14,9 +14,9 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-20.04]
php: ["7.4"]
php: ["8.2"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Create config files
run: |

2
.gitignore vendored
View File

@ -6,6 +6,8 @@ tmp
vendor
webroot/theme/node_modules
webroot/scss/*.css
webroot/js/node_modules/
!webroot/js/node_modules/mermaid/dist/
.vscode
docker/run/
.phpunit.result.cache

View File

@ -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();
}
}
}

View File

@ -178,7 +178,6 @@ return [
*/
'Error' => [
'errorLevel' => E_ALL,
'exceptionRenderer' => ExceptionRenderer::class,
'skipLog' => [],
'log' => true,
'trace' => true,

View File

@ -194,6 +194,11 @@ ServerRequest::addDetector('tablet', function ($request) {
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.

View File

@ -45,7 +45,7 @@ use Cake\Routing\RouteBuilder;
/** @var \Cake\Routing\RouteBuilder $routes */
$routes->setRouteClass(DashedRoute::class);
$routes->scope('/', function (RouteBuilder $builder) {
$builder->setExtensions(['json']);
$builder->setExtensions(['json', 'csv']);
// Register scoped middleware for in scopes.
$builder->registerMiddleware('csrf', new CsrfProtectionMiddleware([
'httponly' => true,

View File

@ -20,6 +20,9 @@
<testsuite name="api">
<directory>./tests/TestCase/Api</directory>
</testsuite>
<testsuite name="e2e">
<directory>./tests/TestCase/Integration</directory>
</testsuite>
</testsuites>
<extensions>

View File

@ -106,7 +106,7 @@ class TagHelper extends Helper
$deleteButton = $this->Bootstrap->button([
'size' => 'sm',
'icon' => 'times',
'class' => ['ms-1', 'border-0', "text-${textColour}"],
'class' => ['ms-1', 'border-0', "text-$textColour"],
'variant' => 'text',
'title' => __('Delete tag'),
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',

View File

@ -136,7 +136,7 @@ class ImporterCommand extends Command
$entity = null;
if (isset($item[$primary_key])) {
$query = $table->find('all')
->where(["${primary_key}" => $item[$primary_key]]);
->where(["$primary_key" => $item[$primary_key]]);
$entity = $query->first();
}
if (is_null($entity)) {

View File

@ -40,5 +40,4 @@ class AuditLogsController extends AppController
{
$this->CRUD->filtering();
}
}

View File

@ -107,6 +107,7 @@ class ACLComponent extends Component
'viewTags' => ['*']
],
'Instance' => [
'downloadTopology' => ['perm_admin'],
'home' => ['*'],
'migrate' => ['perm_admin'],
'migrationIndex' => ['perm_admin'],
@ -114,7 +115,8 @@ class ACLComponent extends Component
'saveSetting' => ['perm_admin'],
'searchAll' => ['*'],
'settings' => ['perm_admin'],
'status' => ['*']
'status' => ['*'],
'topology' => ['perm_admin'],
],
'LocalTools' => [
'action' => ['perm_admin'],

View File

@ -19,7 +19,7 @@ use App\Utility\UI\IndexSetting;
class CRUDComponent extends Component
{
public $components = ['RestResponse'];
public $components = ['RestResponse', 'APIRearrange'];
public function initialize(array $config): void
{
@ -102,12 +102,12 @@ class CRUDComponent extends Component
if (!$this->Controller->ParamHandler->isRest()) {
$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
}
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
$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'])) {
$data->each(function($value, $key) use ($options) {
$hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']];
@ -138,9 +138,21 @@ class CRUDComponent extends Component
return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates);
});
}
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json', false, false, false, [
'X-Total-Count' => $totalCount,
]);
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, [
'X-Total-Count' => $totalCount,
]);
}
} else {
$this->Controller->setResponse($this->Controller->getResponse()->withHeader('X-Total-Count', $totalCount));
if (isset($options['afterFind'])) {
@ -281,7 +293,7 @@ class CRUDComponent extends Component
*/
public function getResponsePayload()
{
if ($this->Controller->ParamHandler->isRest()) {
if ($this->Controller->ParamHandler->isRest() || $this->request->is('csv')) {
return $this->Controller->restResponsePayload;
} else if ($this->Controller->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) {
return $this->Controller->ajaxResponsePayload;
@ -1305,6 +1317,7 @@ class CRUDComponent extends Component
}
$query = $this->setMetaFieldFilters($query, $filteringMetaFields);
}
$activeFilters['_here'] = $this->request->getRequestTarget();
$this->Controller->set('activeFilters', $activeFilters);
return $query;
@ -1489,7 +1502,7 @@ class CRUDComponent extends Component
{
$prefixedConditions = [];
foreach ($conditions as $condField => $condValue) {
$prefixedConditions["${prefix}.${condField}"] = $condValue;
$prefixedConditions["$prefix.$condField"] = $condValue;
}
return $prefixedConditions;
}
@ -1613,13 +1626,13 @@ class CRUDComponent extends Component
[sprintf('%s.id = %s.%s', $this->Table->getAlias(), $associatedTable->getAlias(), $association->getForeignKey())]
)
->where([
["${field} IS NOT" => NULL]
["$field IS NOT" => NULL]
]);
} else if ($associationType == 'manyToOne') {
$fieldToExtract = sprintf('%s.%s', Inflector::singularize(strtolower($model)), $subField);
$query = $this->Table->find()->contain($model);
} else {
throw new Exception("Association ${associationType} not supported in CRUD Component");
throw new Exception("Association $associationType not supported in CRUD Component");
}
} else {
$fieldToExtract = $field;

View File

@ -8,7 +8,7 @@ class OrgGroupsNavigation extends BaseNavigation
public function addLinks()
{
$controller = 'OrgGroups';
if (empty($this->viewVars['canEdit'])) {
if (empty($this->viewVars['canEditDefinition'])) {
$this->bcf->removeLink($controller, 'view', $controller, 'edit');
$this->bcf->removeLink($controller, 'edit', $controller, 'edit');
}
@ -17,9 +17,10 @@ class OrgGroupsNavigation extends BaseNavigation
public function addActions()
{
$controller = 'OrgGroups';
if (empty($this->viewVars['canEdit'])) {
if (empty($this->viewVars['canEditDefinition'])) {
$this->bcf->removeAction($controller, 'view', $controller, 'delete');
$this->bcf->removeAction($controller, 'edit', $controller, 'delete');
$this->bcf->removeAction($controller, 'view', $controller, 'add');
}
}
}

View File

@ -113,6 +113,11 @@ class Sidemenu {
'label' => __('Instance'),
'icon' => $this->iconTable['Instance'],
'children' => [
'Topology' => [
'label' => __('Topology'),
'url' => '/instance/topology',
'icon' => 'project-diagram',
],
'Settings' => [
'label' => __('Settings'),
'url' => '/instance/settings',

View File

@ -4,6 +4,7 @@ namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Core\Configure;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
class RestResponseComponent extends Component
@ -390,7 +391,7 @@ class RestResponseComponent extends Component
return '[]';
}
public function saveFailResponse($controller, $action, $id = false, $validationErrors, $format = false)
public function saveFailResponse($controller, $action, $id, $validationErrors, $format = false)
{
$this->autoRender = false;
$response = array();

View File

@ -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;
}
}

View File

@ -58,6 +58,7 @@ class OrgGroupsController extends AppController
return $responsePayload;
}
$this->set('canEdit', $this->canEdit($id));
$this->set('canEditDefinition', $this->canEditDefinition($id));
}
public function edit($id)
@ -136,6 +137,15 @@ class OrgGroupsController extends AppController
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
public function listAdmins($groupId)
{

View File

@ -8,6 +8,7 @@ use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\Error\Debugger;
use Cake\Http\Exception\NotFoundException;
use Cake\ORM\TableRegistry;
class SharingGroupsController extends AppController
{
@ -171,9 +172,13 @@ class SharingGroupsController extends AppController
$input['organisation_id'] = [$input['organisation_id']];
}
$result = true;
$this->SGO = TableRegistry::getTableLocator()->get('SGOs');
foreach ($input['organisation_id'] as $org_id) {
$org = $this->SharingGroups->SharingGroupOrgs->get($org_id);
$result &= (bool)$this->SharingGroups->SharingGroupOrgs->link($sharingGroup, [$org]);
$additional_data = [];
if (!empty($input['extend'])) {
$additional_data['extend'] = $input['extend'];
}
$result &= $this->SGO->attach($sharingGroup['id'], $org_id, $additional_data);
}
if ($result) {
$message = __('Organisation(s) added to the sharing group.');
@ -216,8 +221,8 @@ class SharingGroupsController extends AppController
throw new NotFoundException(__('Invalid SharingGroup.'));
}
if ($this->request->is('post')) {
$org = $this->SharingGroups->SharingGroupOrgs->get($org_id);
$result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup, [$org]);
$this->SGO = TableRegistry::getTableLocator()->get('SGOs');
$result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup['id'], $org_id);
if ($result) {
$message = __('Organisation(s) removed from the sharing group.');
} else {
@ -253,9 +258,10 @@ class SharingGroupsController extends AppController
public function listOrgs($id)
{
$sharingGroup = $this->SharingGroups->get($id, [
'contain' => 'SharingGroupOrgs'
]);
$sharingGroup = $this->SharingGroups->find()->where(['id' => $id])->contain(['SharingGroupOrgs'])->first();
foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {
$sharingGroup['sharing_group_orgs'][$k]['extend'] = $org['_joinData']['extend'];
}
$params = $this->ParamHandler->harvestParams(['quickFilter']);
if (!empty($params['quickFilter'])) {
foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {

View File

@ -11,7 +11,7 @@ use Cake\Http\Exception\NotFoundException;
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 $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations', 'OrgGroups'];
@ -63,6 +63,11 @@ class UsersController extends AppController
$this->set('validOrgIDsFOrEdition', $validOrgIDsFOrEdition);
}
public function filtering()
{
$this->CRUD->filtering();
}
public function add()
{
$currentUser = $this->ACL->getUser();
@ -72,7 +77,7 @@ class UsersController extends AppController
];
$individual_ids = [];
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();
$individual_ids = $this->Users->Individuals->find('aligned', ['organisation_id' => $currentUser['organisation_id']])->all()->extract('id')->toArray();
} else {
@ -219,12 +224,12 @@ class UsersController extends AppController
{
$currentUser = $this->ACL->getUser();
$validRoles = [];
$individuals_params = [
'sort' => ['email' => 'asc']
];
$individual_ids = [];
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 {
$validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
}
@ -448,13 +453,17 @@ class UsersController extends AppController
{
$editingAnotherUser = false;
$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;
} else {
$user = $this->Users->get($user_id, [
'contain' => ['Roles', 'Individuals' => 'Organisations', 'Organisations', 'UserSettings']
]);
$editingAnotherUser = true;
if (!empty($currentUser['role']['perm_group_admin']) && !$this->ACL->canEditUser($currentUser, $user)) {
$user = $currentUser;
$editingAnotherUser = false;
}
}
$this->set('editingAnotherUser', $editingAnotherUser);
$this->set('user', $user);

View File

@ -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);
}
}

View File

@ -4,6 +4,8 @@ namespace CommonConnectorTools;
use Cake\ORM\Locator\LocatorAwareTrait;
use Cake\Log\Log;
use Cake\Log\Engine\FileLog;
use Cake\Utility\Hash;
class CommonConnectorTools
{
@ -88,13 +90,115 @@ class CommonConnectorTools
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'])) {
return false;
}
$sharing_groups = \Cake\ORM\TableRegistry::getTableLocator()->get('SharingGroups');
$sharing_groups->captureSharingGroup($input);
$sharing_groups->captureSharingGroup($input, $user_id);
return true;
}
@ -149,6 +253,11 @@ class CommonConnectorTools
$this->remoteToolConnectionStatus($params, self::STATE_CONNECTED);
return false;
}
public function diagnostics(array $params): array
{
return [];
}
}
?>

View File

@ -15,6 +15,12 @@ class MispConnector extends CommonConnectorTools
public $name = 'MISP';
public $exposedFunctions = [
'diagnosticsAction' => [
'type' => 'index',
'scope' => 'child',
'params' => [
]
],
'serverSettingsAction' => [
'type' => 'index',
'scope' => 'child',
@ -48,6 +54,11 @@ class MispConnector extends CommonConnectorTools
'direction'
]
],
'restartWorkersAction' => [
'type' => 'formAction',
'scope' => 'childAction',
'redirect' => 'diagnosticsAction'
],
'fetchOrganisationAction' => [
'type' => 'formAction',
'scope' => 'childAction',
@ -56,6 +67,46 @@ class MispConnector extends CommonConnectorTools
],
'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' => [
'type' => 'formAction',
'scope' => 'childAction',
@ -108,7 +159,7 @@ class MispConnector extends CommonConnectorTools
'icon' => 'terminal',
'variant' => 'primary',
]
]
],
];
public $version = '0.1';
public $settings = [
@ -229,9 +280,6 @@ class MispConnector extends CommonConnectorTools
$list = explode('.', $params['sort']);
$params['sort'] = end($list);
}
if (!isset($params['limit'])) {
$params['limit'] = 50;
}
$url = $this->urlAppendParams($url, $params);
$response = $this->HTTPClientGET($url, $params['connection']);
if ($response->isOk()) {
@ -240,7 +288,7 @@ class MispConnector extends CommonConnectorTools
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 GET from the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
$this->logError($errorMsg);
throw new NotFoundException($errorMsg);
}
@ -261,6 +309,9 @@ class MispConnector extends CommonConnectorTools
if ($response->isOk()) {
return $response;
} else {
if (!empty($params['softError'])) {
return $response;
}
$errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
$this->logError($errorMsg);
throw new NotFoundException($errorMsg);
@ -288,10 +339,183 @@ class MispConnector extends CommonConnectorTools
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
{
$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
@ -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
{
@ -563,6 +865,30 @@ class MispConnector extends CommonConnectorTools
$urlParams = h($params['connection']['id']) . '/organisationsAction';
$response = $this->getData('/organisations/index', $params);
$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)) {
return [
'type' => 'index',
@ -571,6 +897,25 @@ class MispConnector extends CommonConnectorTools
'skip_pagination' => 1,
'top_bar' => [
'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' => __('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',
'button' => __('Search'),
@ -582,11 +927,32 @@ class MispConnector extends CommonConnectorTools
]
],
'fields' => [
[
'element' => 'selector',
'class' => 'short',
'data' => [
'id' => [
'value_path' => 'Organisation.uuid'
]
]
],
[
'name' => 'Name',
'sort' => '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',
'sort' => 'Organisation.uuid',
@ -597,6 +963,11 @@ class MispConnector extends CommonConnectorTools
'sort' => 'Organisation.nationality',
'data_path' => 'Organisation.nationality'
],
[
'name' => 'local',
'sort' => 'Organisation.local',
'data_path' => 'Organisation.local'
],
[
'name' => 'sector',
'sort' => 'Organisation.sector',
@ -607,11 +978,33 @@ class MispConnector extends CommonConnectorTools
'description' => false,
'pull' => 'right',
'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}}',
'modal_params_data_path' => ['Organisation.uuid'],
'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 +1024,35 @@ class MispConnector extends CommonConnectorTools
$urlParams = h($params['connection']['id']) . '/sharingGroupsAction';
$response = $this->getData('/sharing_groups/index', $params);
$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)) {
return [
'type' => 'index',
@ -640,21 +1062,53 @@ class MispConnector extends CommonConnectorTools
'top_bar' => [
'children' => [
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'additionalUrlParams' => $urlParams
]
'type' => 'simple',
'children' => [
[
'class' => 'hidden mass-select',
'text' => __('Fetch selected sharing groups'),
'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' => [
[
'element' => 'selector',
'class' => 'short',
'data' => [
'id' => [
'value_path' => 'SharingGroup.uuid'
]
]
],
[
'name' => 'Name',
'sort' => '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',
'sort' => 'SharingGroup.uuid',
@ -662,21 +1116,16 @@ class MispConnector extends CommonConnectorTools
],
[
'name' => 'Organisations',
'sort' => 'Organisation',
'data_path' => 'Organisation',
'element' => 'count_summary'
'sort' => 'SharingGroupOrg',
'data_path' => 'SharingGroupOrg',
'element' => 'count_summary',
'title' => 'foo'
],
[
'name' => 'Roaming',
'sort' => 'SharingGroup.roaming',
'data_path' => 'SharingGroup.roaming',
'element' => 'boolean'
],
[
'name' => 'External servers',
'sort' => 'Server',
'data_path' => 'Server',
'element' => 'count_summary'
]
],
'title' => false,
@ -687,7 +1136,15 @@ class MispConnector extends CommonConnectorTools
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSharingGroupAction?uuid={{0}}',
'modal_params_data_path' => ['SharingGroup.uuid'],
'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 +1160,7 @@ class MispConnector extends CommonConnectorTools
return [
'data' => [
'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' => [
'action' => $params['request']->getParam('action')
],
@ -726,6 +1183,294 @@ class MispConnector extends CommonConnectorTools
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' => __('Fetch and create/update the selected {0} organisations from MISP?', count($ids)),
'submit' => [
'action' => $params['request']->getParam('action')
],
'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'fetchSelectedOrganisationsAction']
]
];
} elseif ($params['request']->is(['post'])) {
$successes = 0;
$errors = 0;
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
{
if ($params['request']->is(['get'])) {
@ -752,7 +1497,11 @@ class MispConnector extends CommonConnectorTools
'sharing_group_orgs' => []
];
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']);
if ($result) {

View File

@ -78,6 +78,15 @@ class AppModel extends Entity
$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']))) {
unset($this->MetaTemplates);

11
src/Model/Entity/SGO.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class SGO extends AppModel
{
}

View File

@ -236,4 +236,140 @@ class InstanceTable extends AppTable
}
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;
}
}

View File

@ -147,14 +147,14 @@ class LocalToolsTable extends AppTable
'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [],
];
if ($includeConnections) {
$connector['connections'] = $this->healthCheck($connector_type, $connector_class);
$connector['connections'] = $this->healthCheck($connector_type, $connector_class, true);
}
$connectors[] = $connector;
}
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->where([
@ -162,11 +162,28 @@ class LocalToolsTable extends AppTable
]);
$connections = $query->all()->toList();
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;
}
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
{
$connector_class = $this->getConnectors($connection->connector);

View File

@ -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;
}
}

View File

@ -25,11 +25,11 @@ class SharingGroupsTable extends AppTable
$this->belongsToMany(
'SharingGroupOrgs',
[
'through' => 'SGOs',
'className' => 'Organisations',
'foreignKey' => 'sharing_group_id',
'joinTable' => 'sgo',
'targetForeignKey' => 'organisation_id'
]
],
);
$this->setDisplayField('name');
}
@ -77,11 +77,17 @@ class SharingGroupsTable extends AppTable
public function postCaptureActions($savedEntity, $input): void
{
$orgs = [];
$SGO = TableRegistry::getTableLocator()->get('SGOs');
foreach ($input['sharing_group_orgs'] as $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);
}
}

View File

@ -1,4 +1,4 @@
{
"version": "1.16",
"version": "1.17",
"application": "Cerebrate"
}

View File

@ -185,6 +185,9 @@ class BootstrapDropdownMenu extends BootstrapGeneric
$classes = array_merge($classes, $entry['class']);
}
$params = $entry['attrs'] ?? [];
if (!empty($entry['onclick'])) {
$params['onclick'] = $entry['onclick'];
}
$params['href'] = '#';
if (!empty($entry['menu'])) {

View File

@ -61,6 +61,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'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.'),
'includeAllPagination' => true,
'pull' => 'right',
'actions' => [
[

View File

@ -36,6 +36,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'class' => 'short',
'data_path' => 'status',
'sort' => 'status',
'display_field_data_path' => 'email',
'element' => 'brood_sync_status',
],
[

View File

@ -37,6 +37,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'class' => 'short',
'data_path' => 'status',
'sort' => 'status',
'display_field_data_path' => 'name',
'element' => 'brood_sync_status',
],
[

View File

@ -78,6 +78,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'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.'),
'includeAllPagination' => true,
'actions' => [
[
'url' => '/individuals/view',

View File

@ -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,
]);

View File

@ -169,6 +169,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'title' => __('Meta Field Templates'),
'description' => __('The various templates used to enrich certain objects by a set of standardised fields.'),
'includeAllPagination' => true,
'actions' => [
[
'url' => '/metaTemplates/view',

View File

@ -62,6 +62,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'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.'),
'includeAllPagination' => true,
'actions' => [
[
'url' => '/orgGroups/view',

View File

@ -97,6 +97,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'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.'),
'includeAllPagination' => true,
'actions' => [
[
'url' => '/organisations/view',

View File

@ -77,6 +77,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'title' => __('Roles Index'),
'description' => __('A list of configurable user roles. Create or modify user access roles based on the settings below.'),
'includeAllPagination' => true,
'pull' => 'right',
'actions' => [
[

View File

@ -9,6 +9,11 @@
'label' => __('Owner organisation'),
'options' => $dropdownData['organisation']
],
[
'field' => 'extend',
'type' => 'checkbox',
'label' => __('Can extend/administer')
],
],
'submit' => [
'action' => $this->request->getParam('action')

View File

@ -57,6 +57,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'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.'),
'includeAllPagination' => true,
'pull' => 'right',
'actions' => [
[

View File

@ -42,6 +42,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
'sort' => 'uuid',
'class' => 'short',
'data_path' => 'uuid',
],
[
'name' => __('Can extend/administer'),
'sort' => 'extend',
'element' => 'boolean',
'class' => 'short',
'data_path' => 'extend',
]
],
'pull' => 'right',

View File

@ -17,15 +17,21 @@ echo $this->element('genericElements/IndexTable/index_table', [
]
]
],
[
'type' => 'context_filters',
'context_filters' => $filteringContexts
],
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
'searchKey' => 'value',
'allowFilering' => true
],
[
'type' => 'table_action',
'table_setting_id' => 'user_index',
]
]
],

View File

@ -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'])
);
?>

View File

@ -73,6 +73,25 @@
<script>
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']); ?>'
reloadUrl = reloadUrl != '' ? reloadUrl : fallbackReloadUrl
UI.overlayUntilResolve(clicked, UI.submissionModalForIndex(url, reloadUrl, '<?= $tableRandomValue ?>'))

View File

@ -24,7 +24,10 @@
$filteringButton = '';
if (!empty($data['allowFilering'])) {
$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'])) {
$numberActiveFilters += count($activeFilters['filteringMetaFields']) - 1;
}
@ -34,7 +37,7 @@
'title' => __('Filter index'),
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
];
if (count($activeFilters) > 0) {
if (count($activeFiltersFiltered) > 0) {
$buttonConfig['badge'] = [
'variant' => 'light',
'text' => $numberActiveFilters,

View File

@ -5,6 +5,10 @@ use App\Utility\UI\IndexSetting;
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'));
}
$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);
$tableSettings = IndexSetting::getTableSetting($loggedUser, $data['table_setting_id']);
$compactDisplay = !empty($tableSettings['compact_display']);
@ -52,6 +56,15 @@ $indexColumnMenu = array_merge(
$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', [
'table_data' => $table_data,
'tableSettings' => $tableSettings,
@ -68,8 +81,6 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
?>
<?php if (!isset($data['requirement']) || $data['requirement']) : ?>
<?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([
'dropdown-class' => 'ms-1',
'alignment' => 'end',
@ -95,9 +106,8 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
[
'text' => __('Download'),
'icon' => 'download',
'attrs' => [
'onclick' => sprintf('downloadIndexTable(this, "%s")', $downloadFilename),
],
'keepOpen' => true,
'menu' => $indexDownloadMenu,
],
[
'html' => $compactDisplayHtml,

View File

@ -37,7 +37,7 @@ if ($field['scope'] === 'individuals') {
foreach ($extracted['alignments'] as $alignment) {
$alignmentEntryHtml = '[' . $this->Bootstrap->node('span', ['class' => ['fw-bold']], h($alignment['type'])) . ']';
$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,
h($alignment['individual']['id']),
h($alignment['individual']['email'])

View File

@ -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>

View File

@ -56,7 +56,7 @@ class BroodsFixture extends TestFixture
'id' => self::BROOD_WIREMOCK_ID,
'uuid' => $faker->uuid(),
'name' => 'wiremock',
'url' => 'http://localhost:8080',
'url' => sprintf('http://%s:%s', $_ENV['WIREMOCK_HOST'] ?? 'localhost', $_ENV['WIREMOCK_PORT'] ?? '8080'),
'description' => $faker->text,
'organisation_id' => OrganisationsFixture::ORGANISATION_B_ID,
'trusted' => true,

View File

@ -11,8 +11,8 @@ class InboxFixture extends TestFixture
public $connection = 'test';
public $table = 'inbox';
public const INBOX_USER_REGISTRATION_ID = 1;
public const INBOX_INCOMING_CONNECTION_REQUEST_ID = 2;
public const INBOX_USER_REGISTRATION_UUID = 'e783b13a-7019-48f5-848e-582bb930a833';
public const INBOX_INCOMING_CONNECTION_REQUEST_UUID = '9810bd94-16f9-42e0-b364-af59dba50a34';
public function init(): void
{
@ -20,8 +20,7 @@ class InboxFixture extends TestFixture
$this->records = [
[
'id' => self::INBOX_USER_REGISTRATION_ID,
'uuid' => $faker->uuid(),
'uuid' => self::INBOX_USER_REGISTRATION_UUID,
'scope' => 'User',
'action' => 'Registration',
'title' => 'User account creation requested for foo@bar.com',
@ -37,15 +36,14 @@ class InboxFixture extends TestFixture
'modified' => $faker->dateTime()->getTimestamp()
],
[
'id' => self::INBOX_INCOMING_CONNECTION_REQUEST_ID,
'uuid' => $faker->uuid(),
'uuid' => self::INBOX_INCOMING_CONNECTION_REQUEST_UUID,
'scope' => 'LocalTool',
'action' => 'IncomingConnectionRequest',
'title' => 'Request for MISP Inter-connection',
'origin' => 'http://127.0.0.1',
'comment' => null,
'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' => [
'connectorName' => 'MispConnector',
'cerebrateURL' => 'http://127.0.0.1',

View File

@ -15,6 +15,7 @@ class IndividualsFixture extends TestFixture
public const INDIVIDUAL_ORG_ADMIN_ID = 3;
public const INDIVIDUAL_REGULAR_USER_ID = 4;
public const INDIVIDUAL_A_ID = 5;
public const INDIVIDUAL_B_ID = 6;
public function init(): void
{
@ -70,6 +71,16 @@ class IndividualsFixture extends TestFixture
'position' => 'user',
'created' => $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();

View File

@ -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();
}
}

View File

@ -17,6 +17,7 @@ class MetaTemplateFieldsFixture extends TestFixture
'field' => 'test_field_1',
'type' => 'text',
'meta_template_id' => MetaTemplatesFixture::ENABLED_TEST_ORG_META_TEMPLATE_ID,
'meta_template_directory_id' => MetaTemplateDirectoryFixture::META_TEMPLATE_DIRECTORY_ID,
'regex' => null,
'multiple' => 1,
'enabled' => 1,

View File

@ -44,6 +44,7 @@ class MetaTemplatesFixture extends TestFixture
$this->records = [
[
'id' => self::ENABLED_TEST_ORG_META_TEMPLATE_ID,
'meta_template_directory_id' => MetaTemplateDirectoryFixture::META_TEMPLATE_DIRECTORY_ID,
'scope' => 'organisation',
'name' => 'Test Meta Template (enabled)',
'namespace' => 'cerebrate',
@ -58,6 +59,7 @@ class MetaTemplatesFixture extends TestFixture
],
[
'id' => self::DISABLED_TEST_ORG_META_TEMPLATE_ID,
'meta_template_directory_id' => MetaTemplateDirectoryFixture::META_TEMPLATE_DIRECTORY_ID,
'scope' => 'organisation',
'name' => 'Test Meta Template (disabled)',
'namespace' => 'cerebrate',

View File

@ -15,16 +15,18 @@ trait WireMockTestTrait
private $wiremock;
/** @var array<mixed> */
private $config = [
'hostname' => 'localhost',
'port' => 8080
];
private $config;
public function initializeWireMock(): void
{
$this->config = [
'hostname' => $_ENV['WIREMOCK_HOST'] ?? 'localhost',
'port' => $_ENV['WIREMOCK_PORT'] ?? 8080
];
$this->wiremock = WireMock::create(
$_ENV['WIREMOCK_HOST'] ?? $this->config['hostname'],
$_ENV['WIREMOCK_PORT'] ?? $this->config['port']
$this->config['hostname'],
$this->config['port']
);
if (!$this->wiremock->isAlive()) {

View File

@ -59,6 +59,7 @@ $ vendor/bin/phpunit --testsuite=api --testdox
Available suites:
* `app`: runs all test suites
* `api`: runs only api tests
* `e2e`: runs only integration tests (requires wiremock running)
* `controller`: runs only controller tests
* _to be continued ..._

View File

@ -30,7 +30,7 @@ class IndexInboxApiTest extends TestCase
$this->get(self::ENDPOINT);
$this->assertResponseOk();
$this->assertResponseContains(sprintf('"id": %d', InboxFixture::INBOX_USER_REGISTRATION_ID));
$this->assertResponseContains(sprintf('"id": %d', InboxFixture::INBOX_INCOMING_CONNECTION_REQUEST_ID));
$this->assertResponseContains(sprintf('"uuid": "%s"', InboxFixture::INBOX_USER_REGISTRATION_UUID));
$this->assertResponseContains(sprintf('"uuid": "%s"', InboxFixture::INBOX_INCOMING_CONNECTION_REQUEST_UUID));
}
}

View File

@ -31,20 +31,20 @@ class MispInterConnectionTest extends TestCase
];
/** 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 */
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';
/** 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';
/** 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 = 'http://localhost:8080/MISP_REMOTE';
private const REMOTE_MISP_INSTANCE_URL = '/MISP_REMOTE';
private const REMOTE_MISP_AUTHKEY = '19ca57ecebd2fe34c1c17d729980678eb648d541';
@ -64,7 +64,7 @@ class MispInterConnectionTest extends TestCase
'name' => 'MISP_LOCAL',
'connector' => 'MispConnector',
'settings' => json_encode([
'url' => self::LOCAL_MISP_INSTANCE_URL,
'url' => $this->getWireMockBaseUrl() . self::LOCAL_MISP_INSTANCE_URL,
'authkey' => self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
'skip_ssl' => true,
]),
@ -90,7 +90,7 @@ class MispInterConnectionTest extends TestCase
[
'uuid' => $LOCAL_BROOD_UUID,
'name' => 'Local Brood',
'url' => self::REMOTE_CEREBRATE_URL,
'url' => $this->getWireMockBaseUrl() . self::REMOTE_CEREBRATE_URL,
'description' => $faker->text,
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
'trusted' => true,
@ -228,10 +228,10 @@ class MispInterConnectionTest extends TestCase
[
'email' => self::REMOTE_MISP_SYNC_USER_EMAIL,
'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,
'connectorName' => 'MispConnector',
'cerebrateURL' => self::REMOTE_CEREBRATE_URL,
'cerebrateURL' => $this->getWireMockBaseUrl() . self::REMOTE_CEREBRATE_URL,
'local_tool_id' => 1,
'remote_tool_id' => 1,
'tool_name' => 'MISP_REMOTE'
@ -250,7 +250,7 @@ class MispInterConnectionTest extends TestCase
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
[
'authkey' => self::REMOTE_MISP_AUTHKEY,
'url' => self::REMOTE_MISP_INSTANCE_URL,
'url' => $this->getWireMockBaseUrl() . self::REMOTE_MISP_INSTANCE_URL,
'name' => 'MISP_REMOTE',
'remote_org_id' => OrganisationsFixture::ORGANISATION_A_ID
]

View File

@ -6,7 +6,7 @@ namespace App\Test\TestCase\Api\Users;
use Cake\TestSuite\TestCase;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\UsersFixture;
use App\Test\Fixture\IndividualsFixture;
use App\Test\Fixture\OrganisationsFixture;
use App\Test\Fixture\RolesFixture;
use App\Test\Helper\ApiTestTrait;
@ -31,18 +31,20 @@ class AddUserApiTest extends TestCase
$this->post(
self::ENDPOINT,
[
'individual_id' => UsersFixture::USER_REGULAR_USER_ID,
'individual_id' => IndividualsFixture::INDIVIDUAL_B_ID,
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
'role_id' => RolesFixture::ROLE_REGULAR_USER_ID,
'disabled' => false,
'username' => 'test',
'username' => 'test123',
'password' => 'Password123456!',
]
);
print_r($this->getJsonResponseAsArray());
$this->assertResponseOk();
$this->assertResponseContains('"username": "test"');
$this->assertDbRecordExists('Users', ['username' => 'test']);
$this->assertResponseContains('"username": "test123"');
$this->assertDbRecordExists('Users', ['username' => 'test123']);
}
public function testAddUserNotAllowedAsRegularUser(): void
@ -51,7 +53,7 @@ class AddUserApiTest extends TestCase
$this->post(
self::ENDPOINT,
[
'individual_id' => UsersFixture::USER_REGULAR_USER_ID,
'individual_id' => IndividualsFixture::INDIVIDUAL_B_ID,
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
'role_id' => RolesFixture::ROLE_REGULAR_USER_ID,
'disabled' => false,
@ -61,6 +63,6 @@ class AddUserApiTest extends TestCase
);
$this->assertResponseCode(405);
$this->assertDbRecordNotExists('Users', ['username' => 'test']);
$this->assertDbRecordNotExists('Users', ['username' => 'test123']);
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Test\TestCase\Api\Users;
use Cake\Core\Configure;
use Cake\TestSuite\TestCase;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\UsersFixture;
@ -25,6 +26,7 @@ class DeleteUserApiTest extends TestCase
public function testDeleteUser(): void
{
Configure::write('user.allow-user-deletion', true);
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_REGULAR_USER_ID);
$this->delete($url);

View File

@ -40,14 +40,12 @@ class ApplicationTest extends IntegrationTestCase
$app->bootstrap();
$plugins = $app->getPlugins();
$this->assertCount(7, $plugins);
$this->assertSame('Bake', $plugins->get('Bake')->getName());
$this->assertSame('DebugKit', $plugins->get('DebugKit')->getName());
$this->assertSame('Migrations', $plugins->get('Migrations')->getName());
$this->assertSame('Authentication', $plugins->get('Authentication')->getName());
$this->assertSame('ADmad/SocialAuth', $plugins->get('ADmad/SocialAuth')->getName());
$this->assertSame('Tags', $plugins->get('Tags')->getName());
$this->assertSame('Cake/TwigView', $plugins->get('Cake/TwigView')->getName());
}
/**

View File

@ -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
]
]
]
)))
);
}
}

View File

@ -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()
]
]
)))
);
}
}

View File

@ -14,6 +14,9 @@ class AJAXApi {
static genericRequestConfigGETJSON = {
headers: new Headers(Object.assign({}, AJAXApi.genericRequestHeaders, {Accept: 'application/json'}))
}
static genericRequestConfigGETCSV = {
headers: new Headers(Object.assign({}, AJAXApi.genericRequestHeaders, {Accept: 'text/csv'}))
}
/**
* @namespace
@ -32,6 +35,7 @@ class AJAXApi {
},
successToastOptions: {
},
fetchOptions: {},
}
options = {}
loadingOverlay = false
@ -45,6 +49,13 @@ class AJAXApi {
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
* @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
*/
static async quickFetchJSON(url, options={}) {
const constAlteredOptions = Object.assign({}, {provideFeedback: false}, options)
const constAlteredOptions = Object.assign({}, { provideFeedback: false, }, options)
const tmpApi = new AJAXApi(constAlteredOptions)
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 {Object} [options={}] - The options supported by AJAXApi#defaultOptions
@ -213,7 +238,7 @@ class AJAXApi {
}
let toReturn
try {
const response = await fetch(url, AJAXApi.genericRequestConfigGET);
const response = await fetch(url, this.mergeFetchConfig(AJAXApi.genericRequestConfigGET));
if (!response.ok) {
throw new Error(`Network response was not ok. \`${response.statusText}\``)
}

View File

@ -206,21 +206,34 @@ function deleteBookmark(bookmark, forSidebar=false) {
}).catch((e) => { })
}
function downloadIndexTable(downloadButton, filename) {
function downloadIndexTable(downloadButton, filename, filtered) {
const $dropdownMenu = $(downloadButton).closest('.dropdown')
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
const $filterButton = $(`#toggleFilterButton-${tableRandomValue}`)
const activeFilters = $filterButton.data('activeFilters')
const additionalUrlParams = $filterButton.data('additionalUrlParams') ? $filterButton.data('additionalUrlParams') : ''
const searchParam = jQuery.param(activeFilters);
const url = $table.data('reload-url') + additionalUrlParams + '?' + searchParam
// const additionalUrlParams = $filterButton.data('additionalUrlParams') ? $filterButton.data('additionalUrlParams') : ''
// const searchParam = jQuery.param(activeFilters);
// const url = $table.data('reload-url') + additionalUrlParams + '?' + searchParam
let url = $table.data('reload-url')
if (filtered) {
url = activeFilters._here
}
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)
downloadPromise.then((data) => {
download(filename, JSON.stringify(data, undefined, 4))
if (filename.endsWith('.csv')) {
download(filename, data)
} else {
download(filename, JSON.stringify(data, undefined, 4))
}
})
}

33
webroot/js/node_modules/mermaid/dist/Diagram.d.ts generated vendored Normal file
View File

@ -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>;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1 @@
export {};

144
webroot/js/node_modules/mermaid/dist/arc-947d8396.js generated vendored Normal file
View File

@ -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
};

84
webroot/js/node_modules/mermaid/dist/arc-dcf06dea.js generated vendored Normal file
View File

@ -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
};

View File

@ -0,0 +1,6 @@
function t(r) {
return typeof r == "object" && "length" in r ? r : Array.from(r);
}
export {
t as a
};

View File

@ -0,0 +1,6 @@
function array(x) {
return typeof x === "object" && "length" in x ? x : Array.from(x);
}
export {
array as a
};

View File

@ -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

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

98
webroot/js/node_modules/mermaid/dist/config.d.ts generated vendored Normal file
View File

@ -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;

View File

@ -0,0 +1 @@
export {};

1313
webroot/js/node_modules/mermaid/dist/config.type.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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
};

View File

@ -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

View File

@ -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
};

View File

@ -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;

View 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;

View File

@ -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;
};

View File

@ -0,0 +1 @@
export function render(elem: any, graph: any, markers: any, diagramtype: any, id: any): Promise<void>;

View 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';

View 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;
};

View 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;
};

View File

@ -0,0 +1,13 @@
export default intersectLine;
/**
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
*
* @param p1
* @param p2
* @param q1
* @param q2
*/
declare function intersectLine(p1: any, p2: any, q1: any, q2: any): {
x: number;
y: number;
} | undefined;

Some files were not shown because too many files have changed in this diff Show More